aboutsummaryrefslogtreecommitdiffstats
path: root/src/tui/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui/app.rs')
-rw-r--r--src/tui/app.rs130
1 files changed, 130 insertions, 0 deletions
diff --git a/src/tui/app.rs b/src/tui/app.rs
new file mode 100644
index 0000000..1e7d021
--- /dev/null
+++ b/src/tui/app.rs
@@ -0,0 +1,130 @@
+/// Compile-time channel to join on startup
+pub const CHANNEL: &str = "#speakez";
+
+/// A single chat message in the log
+#[derive(Clone)]
+pub struct ChatLine {
+ pub nick: String,
+ pub text: String,
+ /// true = server/system message (JOIN, PART, topic, etc.)
+ pub is_system: bool,
+ /// true = NOTICE
+ pub is_notice: bool,
+ /// true = this is our own message
+ pub is_self: bool,
+}
+
+/// All mutable state the TUI needs to render and respond to input
+pub struct AppState {
+ /// Our nick
+ pub nick: String,
+ /// The active channel name
+ pub channel: String,
+ /// Chat log for the active channel
+ pub messages: Vec<ChatLine>,
+ /// Member list for the active channel
+ pub members: Vec<String>,
+ /// Current contents of the input box
+ pub input: String,
+ /// Cursor position within `input` (byte index)
+ pub cursor: usize,
+ /// Scroll offset from the bottom (0 = pinned to latest)
+ pub scroll: usize,
+ /// Status line text (connection state, errors, etc.)
+ pub status: String,
+ /// Whether we've fully registered
+ pub connected: bool,
+}
+
+impl AppState {
+ pub fn new(nick: impl Into<String>, channel: impl Into<String>) -> Self {
+ Self {
+ nick: nick.into(),
+ channel: channel.into(),
+ messages: Vec::new(),
+ members: Vec::new(),
+ input: String::new(),
+ cursor: 0,
+ scroll: 0,
+ status: "Connecting...".into(),
+ connected: false,
+ }
+ }
+
+ /// Push a chat message
+ pub fn push_message(&mut self, nick: &str, text: &str, is_self: bool) {
+ self.messages.push(ChatLine {
+ nick: nick.to_string(),
+ text: text.to_string(),
+ is_system: false,
+ is_notice: false,
+ is_self,
+ });
+ }
+
+ /// Push a system/event line (joins, parts, topic changes)
+ pub fn push_system(&mut self, text: &str) {
+ self.messages.push(ChatLine {
+ nick: String::new(),
+ text: text.to_string(),
+ is_system: true,
+ is_notice: false,
+ is_self: false,
+ });
+ }
+
+ /// Insert a character at the cursor
+ pub fn input_insert(&mut self, ch: char) {
+ self.input.insert(self.cursor, ch);
+ self.cursor += ch.len_utf8();
+ }
+
+ /// Delete the character before the cursor
+ pub fn input_backspace(&mut self) {
+ if self.cursor == 0 {
+ return;
+ }
+ // Find the start of the previous character
+ let prev = self.input[..self.cursor]
+ .char_indices()
+ .last()
+ .map(|(i, _)| i)
+ .unwrap_or(0);
+ self.input.remove(prev);
+ self.cursor = prev;
+ }
+
+ /// Move cursor left one character
+ pub fn cursor_left(&mut self) {
+ self.cursor = self.input[..self.cursor]
+ .char_indices()
+ .last()
+ .map(|(i, _)| i)
+ .unwrap_or(0);
+ }
+
+ /// Move cursor right one character
+ pub fn cursor_right(&mut self) {
+ if self.cursor < self.input.len() {
+ let ch = self.input[self.cursor..].chars().next().unwrap();
+ self.cursor += ch.len_utf8();
+ }
+ }
+
+ /// Take the current input, clear the box, return the text
+ pub fn take_input(&mut self) -> String {
+ self.cursor = 0;
+ std::mem::take(&mut self.input)
+ }
+
+ /// Sort and deduplicate the member list
+ pub fn sort_members(&mut self) {
+ self.members.sort_by(|a, b| {
+ // Strip sigils for sorting (@, +, %)
+ let a = a.trim_start_matches(&['@', '+', '%', '~', '&'][..]);
+ let b = b.trim_start_matches(&['@', '+', '%', '~', '&'][..]);
+ a.to_lowercase().cmp(&b.to_lowercase())
+ });
+ self.members.dedup();
+ }
+}