Files
wIRC-desktop/src/main/kotlin/data/Models.kt
T
weishauptandClaude Opus 5.5 d9a0297cc7 Add scrollback text selection/copy, route all /whois output to wIRC main, bump to 0.1.8
- Scrollback (channel windows and wIRC main): mouse drag selection, double-click word,
  triple-click line, shift-click extend, Ctrl+C / Ctrl+Insert, Ctrl+A, Esc, and a
  right-click menu (Copy / Copy line / Select all). Copies one line per message.
  Clipboard writes go off the UI thread with a timeout, like the paste handler.
  Auto-scroll pauses while selecting and catches up once the selection is cleared.
- /whois: header, every reply line, related numerics (401/402/716/263...) and server or
  services notices arriving right after it all go to wIRC main under a bold
  "===== WHOIS <nick> (<server>) =====" header, counted by its unread indicator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:07:52 -04:00

91 lines
4.1 KiB
Kotlin

package data
/** A configured IRC server/network connection. */
data class ServerConfig(
val id: String, // stable key, e.g. "rizon"
val label: String, // display name, e.g. "Rizon"
val host: String,
val port: Int = 6667,
val useTls: Boolean = false,
val nick: String,
val autoJoinChannels: List<String> = emptyList(),
val saslUser: String? = null,
val saslPass: String? = null,
// Raw IRC commands sent right after registration (001), before auto-join channels.
// e.g. "PRIVMSG NickServ :IDENTIFY hunter2" or "NS IDENTIFY hunter2".
// This is the "on join perform" list — saved to disk and replayed every connect.
val onConnectCommands: List<String> = emptyList()
)
/** True for channel-style targets ('#', '&', '+', '!' prefixes per the IRC spec). */
fun isChannelName(name: String): Boolean =
name.isNotEmpty() && name[0] in "#&+!"
enum class MessageKind { CHAT, ACTION, JOIN, PART, QUIT, KICK, MODE, NOTICE, NICK, TOPIC, SYSTEM }
// Process-wide, so every IrcMessage ever constructed while the app is running gets a
// genuinely unique seq, regardless of which buffer/server it belongs to.
private val nextMessageSeq = java.util.concurrent.atomic.AtomicLong(0)
data class IrcMessage(
val timestampMillis: Long,
val kind: MessageKind,
val sender: String?, // nick, or null for system/server lines
val rawText: String, // still contains mIRC control codes; parsed at render time
val target: String? = null, // e.g. for KICK, the nick that got kicked
// Monotonically increasing, guaranteed-unique identity for this message - used as the
// LazyColumn item key in ChannelScreen (see there). The old key there was
// "${timestampMillis}_${sender}_${rawText.hashCode()}" - in a genuinely busy channel,
// two messages sharing the same millisecond, sender, and text (not rare for short
// repeated lines - "lol", a single emoji, etc.) would produce the exact same key, and
// Compose requires unique keys per item. Not persisted to disk - history reloaded on
// restart just gets fresh seq values in load order, which is fine since uniqueness only
// needs to hold within a single running session, not across restarts.
val seq: Long = nextMessageSeq.getAndIncrement()
)
/** Buffer key: server id + channel/query name (channel names include '#'). */
data class BufferKey(val serverId: String, val name: String)
/**
* wIRC main's own buffer: server messages (MOTD, numerics, errors, all whois output) and
* services/server notices from every connected server. Server ids are built from the host
* with every non-alphanumeric character replaced by '_' (see ServerEditDialog), so one can
* never contain '-' - this id can't collide with a real server. Also used as the
* window_prefs.json key ("wirc-main__main") for the main window's saved position and size.
*/
val MAIN_BUFFER_KEY = BufferKey("wirc-main", "main")
// Nicks that are network services rather than people. Their PRIVMSGs and NOTICEs go to
// wIRC main instead of opening a query window. Anything ending in "serv" is also treated
// as a service, which covers the less common ones (MemoServ, HostServ, SaslServ, ...).
private val KNOWN_SERVICE_NICKS = setOf("nickserv", "chanserv", "global", "alis", "q", "x")
fun isServiceNick(nick: String): Boolean {
val lower = nick.lowercase()
return lower in KNOWN_SERVICE_NICKS || lower.endsWith("serv")
}
/**
* True when a message's source is a service or the server itself rather than a real user.
* A user prefix always has the "nick!user@host" form; a missing prefix or one without '!'
* is a server name (e.g. "irc.rizon.io").
*/
fun isServiceOrServerPrefix(prefix: String?): Boolean {
if (prefix == null || !prefix.contains('!')) return true
return isServiceNick(prefix.substringBefore('!'))
}
data class ChannelUser(
val nick: String,
val isOp: Boolean = false,
val isVoice: Boolean = false
)
data class ChannelBuffer(
val key: BufferKey,
val messages: List<IrcMessage> = emptyList(),
val users: List<ChannelUser> = emptyList(),
val topic: String = ""
)