diff --git a/src/application/ui.rs b/src/application/ui.rs index b3fe159..e8aaeea 100644 --- a/src/application/ui.rs +++ b/src/application/ui.rs @@ -13,6 +13,7 @@ use crossterm::terminal::EnterAlternateScreen; use crossterm::terminal::LeaveAlternateScreen; use ratatui::backend::CrosstermBackend; use ratatui::prelude::*; +use ratatui::widgets::Paragraph; use ratatui::widgets::Scrollbar; use ratatui::widgets::ScrollbarOrientation; use ratatui::Terminal; @@ -30,8 +31,27 @@ use crate::domain::models::SlashCommand; use crate::domain::models::TextArea; use crate::domain::services::events::EventsService; use crate::domain::services::AppState; +use crate::domain::services::Bubble; use crate::infrastructure::editors::EditorManager; +/// Verifies that the current window size is large enough to handle the bare +/// minimum width that includes the model name, username, bubbles, and padding. +fn is_line_width_sufficient(line_width: u16) -> bool { + let author_lengths = vec![Author::User, Author::Oatmeal, Author::Model] + .into_iter() + .map(|e| return e.to_string().len()) + .max() + .unwrap(); + + let bubble_style = Bubble::style_confg(); + let min_width = + (author_lengths + bubble_style.magic_spacing + bubble_style.border_elements_length) as i32; + let trimmed_line_width = + ((line_width as f32 * (1.0 - bubble_style.outer_padding_percentage)).ceil()) as i32; + + return trimmed_line_width >= min_width; +} + async fn start_loop( terminal: &mut Terminal, app_state: &mut AppState<'_>, @@ -50,6 +70,14 @@ async fn start_loop( loop { terminal.draw(|frame| { + if !is_line_width_sufficient(frame.size().width) { + frame.render_widget( + Paragraph::new("I'm too small, make me bigger!").alignment(Alignment::Left), + frame.size(), + ); + return; + } + let textarea_len = (textarea.lines().len() + 3).try_into().unwrap(); let layout = Layout::default() .direction(Direction::Vertical) @@ -180,7 +208,7 @@ async fn start_loop( textarea.set_yank_text(text.replace('\r', "\n")); textarea.paste(); } - Event::UIResize() => { + Event::UITick() => { continue; } Event::UIScrollDown() => { diff --git a/src/domain/models/author.rs b/src/domain/models/author.rs new file mode 100644 index 0000000..88e6f3d --- /dev/null +++ b/src/domain/models/author.rs @@ -0,0 +1,22 @@ +use serde_derive::Deserialize; +use serde_derive::Serialize; + +use crate::config::Config; +use crate::config::ConfigKey; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Author { + User, + Oatmeal, + Model, +} + +impl ToString for Author { + fn to_string(&self) -> String { + match self { + Author::User => return Config::get(ConfigKey::Username), + Author::Oatmeal => return String::from("Oatmeal"), + Author::Model => return Config::get(ConfigKey::Model), + } + } +} diff --git a/src/domain/models/event.rs b/src/domain/models/event.rs index 5ee7e0b..747efb8 100644 --- a/src/domain/models/event.rs +++ b/src/domain/models/event.rs @@ -11,7 +11,7 @@ pub enum Event { KeyboardCTRLR(), KeyboardEnter(), KeyboardPaste(String), - UIResize(), + UITick(), UIScrollDown(), UIScrollUp(), UIScrollPageDown(), diff --git a/src/domain/models/message.rs b/src/domain/models/message.rs index e45dbcc..8c5ec90 100644 --- a/src/domain/models/message.rs +++ b/src/domain/models/message.rs @@ -4,15 +4,7 @@ mod tests; use serde_derive::Deserialize; use serde_derive::Serialize; -use crate::config::Config; -use crate::config::ConfigKey; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Author { - User, - Oatmeal, - Model, -} +use super::Author; #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum MessageType { @@ -24,23 +16,13 @@ pub enum MessageType { pub struct Message { pub author: Author, pub text: String, - pub author_formatted: String, mtype: MessageType, } -fn formatted_author(author: Author) -> String { - return match author { - Author::User => Config::get(ConfigKey::Username), - Author::Oatmeal => "Oatmeal".to_string(), - Author::Model => Config::get(ConfigKey::Model), - }; -} - impl Message { pub fn new(author: Author, text: &str) -> Message { return Message { author: author.clone(), - author_formatted: formatted_author(author), text: text.to_string().replace('\t', " "), mtype: MessageType::Normal, }; @@ -49,7 +31,6 @@ impl Message { pub fn new_with_type(author: Author, mtype: MessageType, text: &str) -> Message { return Message { author: author.clone(), - author_formatted: formatted_author(author), text: text.to_string().replace('\t', " "), mtype, }; diff --git a/src/domain/models/message_test.rs b/src/domain/models/message_test.rs index 5bb993e..8a5efcc 100644 --- a/src/domain/models/message_test.rs +++ b/src/domain/models/message_test.rs @@ -8,7 +8,7 @@ use super::MessageType; fn it_executes_new() { let msg = Message::new(Author::Oatmeal, "Hi there!"); assert_eq!(msg.author, Author::Oatmeal); - assert_eq!(msg.author_formatted, "Oatmeal"); + assert_eq!(msg.author.to_string(), "Oatmeal"); assert_eq!(msg.text, "Hi there!".to_string()); assert_eq!(msg.mtype, MessageType::Normal); } @@ -17,7 +17,7 @@ fn it_executes_new() { fn it_executes_new_replacing_tabs() { let msg = Message::new(Author::Oatmeal, "\t\tHi there!"); assert_eq!(msg.author, Author::Oatmeal); - assert_eq!(msg.author_formatted, "Oatmeal"); + assert_eq!(msg.author.to_string(), "Oatmeal"); assert_eq!(msg.text, " Hi there!".to_string()); assert_eq!(msg.mtype, MessageType::Normal); } @@ -26,7 +26,7 @@ fn it_executes_new_replacing_tabs() { fn it_executes_new_with_type() { let msg = Message::new_with_type(Author::Oatmeal, MessageType::Error, "It broke!"); assert_eq!(msg.author, Author::Oatmeal); - assert_eq!(msg.author_formatted, "Oatmeal"); + assert_eq!(msg.author.to_string(), "Oatmeal"); assert_eq!(msg.text, "It broke!".to_string()); assert_eq!(msg.mtype, MessageType::Error); } @@ -35,7 +35,7 @@ fn it_executes_new_with_type() { fn it_executes_new_with_type_replacing_tabs() { let msg = Message::new_with_type(Author::Oatmeal, MessageType::Error, "\t\tIt broke!"); assert_eq!(msg.author, Author::Oatmeal); - assert_eq!(msg.author_formatted, "Oatmeal"); + assert_eq!(msg.author.to_string(), "Oatmeal"); assert_eq!(msg.text, " It broke!".to_string()); assert_eq!(msg.mtype, MessageType::Error); } diff --git a/src/domain/models/mod.rs b/src/domain/models/mod.rs index 9b2a183..9a9fda9 100644 --- a/src/domain/models/mod.rs +++ b/src/domain/models/mod.rs @@ -1,4 +1,5 @@ mod action; +mod author; mod backend; mod editor; mod event; @@ -9,6 +10,7 @@ mod slash_commands; mod textarea; pub use action::*; +pub use author::*; pub use backend::*; pub use editor::*; pub use event::*; diff --git a/src/domain/services/actions.rs b/src/domain/services/actions.rs index 8fc986f..501ae2f 100644 --- a/src/domain/services/actions.rs +++ b/src/domain/services/actions.rs @@ -161,7 +161,7 @@ fn copy_messages(messages: Vec, tx: &mpsc::UnboundedSender) -> R let formatted = messages .iter() .map(|message| { - return format!("{}: {}", message.author_formatted, message.text); + return format!("{}: {}", message.author.to_string(), message.text); }) .collect::>() .join("\n\n"); diff --git a/src/domain/services/bubble.rs b/src/domain/services/bubble.rs index e1ea9e5..79fd0ce 100644 --- a/src/domain/services/bubble.rs +++ b/src/domain/services/bubble.rs @@ -28,6 +28,30 @@ pub struct Bubble { codeblock_counter: usize, } +pub struct BubbleConfig { + pub magic_spacing: usize, + pub border_elements_length: usize, + pub outer_padding_percentage: f32, +} + +fn repeat_from_subtractions(text: &str, subtractions: Vec) -> String { + let count = subtractions + .into_iter() + .map(|e| { + return i32::try_from(e).unwrap(); + }) + .reduce(|a, b| { + return a - b; + }) + .unwrap(); + + if count <= 0 { + return "".to_string(); + } + + return [text].repeat(count.try_into().unwrap()).join(""); +} + impl<'a> Bubble { pub fn new( message: Message, @@ -43,6 +67,17 @@ impl<'a> Bubble { }; } + pub fn style_confg() -> BubbleConfig { + return BubbleConfig { + // TODO wtf is 8 + magic_spacing: 8, + // left border + left padding + (text, not counted) + right padding + right border + + // scrollbar. + border_elements_length: 5, + outer_padding_percentage: 0.04, + }; + } + pub fn as_lines(&mut self, theme: &Theme) -> Vec> { // Lazy default let mut highlight = HighlightLines::new(Syntaxes::get("text"), theme); @@ -106,7 +141,8 @@ impl<'a> Bubble { } } - let bubble_padding = [" "].repeat(self.window_max_width - line_length).join(""); + let bubble_padding = + repeat_from_subtractions(" ", vec![self.window_max_width, line_length]); if self.alignment == BubbleAlignment::Left { spans.push(Span::from(bubble_padding)); @@ -123,11 +159,13 @@ impl<'a> Bubble { fn get_message_lines(&self) -> (Vec, usize) { // Add a minimum 4% of padding on the side. - let min_bubble_padding_length = ((self.window_max_width as f32 * 0.04).ceil()) as usize; + let min_bubble_padding_length = ((self.window_max_width as f32 + * Bubble::style_confg().outer_padding_percentage) + .ceil()) as usize; - // left border + left padding + (text, not counted) + right padding + right - // border + scrollbar. And then minimum bubble padding. - let line_border_width = 5 + min_bubble_padding_length; + // Border elements + minimum bubble padding. + let line_border_width = + Bubble::style_confg().border_elements_length + min_bubble_padding_length; let message_lines = self .message @@ -141,7 +179,7 @@ impl<'a> Bubble { .max() .unwrap(); - let username = &self.message.author_formatted; + let username = &self.message.author.to_string(); if max_line_length < username.len() { max_line_length = username.len(); } @@ -155,12 +193,16 @@ impl<'a> Bubble { let top_left_border = "╭"; let mut top_bar = format!("{top_left_border}{inner_bar}╮"); let bottom_bar = format!("╰{inner_bar}╯"); - let bar_bubble_padding = [" "] - // TODO WTF is 8? - .repeat(self.window_max_width - max_line_length - 8) - .join(""); + let bar_bubble_padding = repeat_from_subtractions( + " ", + vec![ + self.window_max_width, + max_line_length, + Bubble::style_confg().magic_spacing, + ], + ); - let username = &self.message.author_formatted; + let username = &self.message.author.to_string(); if self.alignment == BubbleAlignment::Left { let top_replace = ["─"].repeat(username.len()).join(""); @@ -193,7 +235,7 @@ impl<'a> Bubble { max_line_length: usize, mut spans: Vec>, ) -> (Vec>, usize) { - let fill = [" "].repeat(max_line_length - line_str.len()).join(""); + let fill = repeat_from_subtractions(" ", vec![max_line_length, line_str.len()]); let line_length = format!("│ {line_str}{fill} │").len(); let mut spans_res = vec![self.highlight_span("│ ".to_string())]; diff --git a/src/domain/services/events.rs b/src/domain/services/events.rs index 0d9a02c..02739df 100644 --- a/src/domain/services/events.rs +++ b/src/domain/services/events.rs @@ -3,6 +3,7 @@ use crossterm::event::Event as CrosstermEvent; use crossterm::event::EventStream; use futures::StreamExt; use tokio::sync::mpsc; +use tokio::time; use tui_textarea::Input; use tui_textarea::Key; @@ -23,9 +24,6 @@ impl EventsService { fn handle_crossterm(&self, event: CrosstermEvent) -> Option { match event { - CrosstermEvent::Resize(_, _) => { - return Some(Event::UIResize()); - } CrosstermEvent::Paste(text) => { return Some(Event::KeyboardPaste(text)); } @@ -110,6 +108,7 @@ impl EventsService { Some(Err(_)) => None, None => None }, + _ = time::sleep(time::Duration::from_millis(500)) => Some(Event::UITick()) }; if let Some(event) = evt {