diff --git a/default-config.toml b/default-config.toml index 46c9033..0848ac2 100644 --- a/default-config.toml +++ b/default-config.toml @@ -44,7 +44,7 @@ title_shown = true # The amount of space between the chat window and the terminal border. margin = 0 # Show twitch badges next to usernames. -badges = true +badges = false # The theme of the frontend, dark (default), or light. theme = "dark" # If the user's name appears in chat, highlight it. diff --git a/src/emotes/mod.rs b/src/emotes/mod.rs index dfeebcb..41e68aa 100644 --- a/src/emotes/mod.rs +++ b/src/emotes/mod.rs @@ -38,7 +38,7 @@ pub struct LoadedEmote { pub overlay: bool, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct Emotes { /// Map of emote name, filename, and if the emote is an overlay pub emotes: HashMap, diff --git a/src/handlers/app.rs b/src/handlers/app.rs index 3206374..77a2d12 100644 --- a/src/handlers/app.rs +++ b/src/handlers/app.rs @@ -1,80 +1,30 @@ -use std::{cmp::PartialEq, collections::VecDeque}; +use std::{cell::RefCell, collections::VecDeque, rc::Rc}; use rustyline::line_buffer::LineBuffer; +use tokio::sync::broadcast::Sender; use toml::Table; +use tui::{backend::Backend, Frame}; -use crate::handlers::{ - config::{CompleteConfig, Theme}, - data::MessageData, - filters::Filters, - state::State, - storage::Storage, +use crate::{ + emotes::Emotes, + handlers::{ + config::{CompleteConfig, Theme}, + data::MessageData, + filters::Filters, + state::State, + storage::Storage, + user_input::{events::Event, input::TerminalAction, scrolling::Scrolling}, + }, + twitch::TwitchAction, + ui::{ + components::{Component, Components}, + statics::LINE_BUFFER_CAPACITY, + }, }; -const INPUT_BUFFER_LIMIT: usize = 4096; - -pub struct Scrolling { - /// Offset of scroll - offset: usize, - /// If the scrolling is currently inverted - inverted: bool, -} - -impl Scrolling { - pub const fn new(inverted: bool) -> Self { - Self { - offset: 0, - inverted, - } - } - - /// Scrolling upwards, towards the start of the chat - pub fn up(&mut self) { - self.offset += 1; - } - - /// Scrolling downwards, towards the most recent message(s) - pub fn down(&mut self) { - self.offset = self.offset.saturating_sub(1); - } - - pub const fn inverted(&self) -> bool { - self.inverted - } - - pub fn jump_to(&mut self, index: usize) { - self.offset = index; - } - - pub const fn get_offset(&self) -> usize { - self.offset - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct DebugWindow { - visible: bool, - pub raw_config: Option, -} - -impl DebugWindow { - const fn new(visible: bool, raw_config: Option
) -> Self { - Self { - visible, - raw_config, - } - } - - pub const fn is_visible(&self) -> bool { - self.visible - } - - pub fn toggle(&mut self) { - self.visible = !self.visible; - } -} - -pub struct App { +pub struct App<'a> { + /// All the available components. + pub components: Components<'a>, /// History of recorded messages (time, username, message, etc). pub messages: VecDeque, /// Data loaded in from a JSON file. @@ -93,23 +43,69 @@ pub struct App { pub scrolling: Scrolling, /// The theme selected by the user. pub theme: Theme, - /// If the debug window is visible. - pub debug: DebugWindow, } -impl App { - pub fn new(config: &CompleteConfig, raw_config: Option
) -> Self { +impl App<'_> { + pub fn new( + config: CompleteConfig, + raw_config: Option
, + tx: Sender, + ) -> Self { + let shared_config = Rc::new(RefCell::new(config)); + + let shared_config_borrow = shared_config.borrow(); + + let storage = Storage::new("storage.json", &shared_config_borrow.storage); + + let components = Components::new(&shared_config, raw_config, tx, storage); + Self { - messages: VecDeque::with_capacity(config.terminal.maximum_messages), - storage: Storage::new("storage.json", &config.storage), - filters: Filters::new("filters.txt", &config.filters), - state: config.terminal.start_state.clone(), + components, + messages: VecDeque::with_capacity(shared_config_borrow.terminal.maximum_messages), + storage, + filters: Filters::new("filters.txt", &shared_config_borrow.filters), + state: shared_config_borrow.terminal.start_state.clone(), previous_state: None, - input_buffer: LineBuffer::with_capacity(INPUT_BUFFER_LIMIT), + input_buffer: LineBuffer::with_capacity(*LINE_BUFFER_CAPACITY), buffer_suggestion: None, - theme: config.frontend.theme.clone(), - scrolling: Scrolling::new(config.frontend.inverted_scrolling), - debug: DebugWindow::new(false, raw_config), + theme: shared_config_borrow.frontend.theme.clone(), + scrolling: Scrolling::new(shared_config_borrow.frontend.inverted_scrolling), + } + } + + pub fn draw(&mut self, f: &mut Frame, emotes: Emotes) { + let size = f.size(); + + if size.height < 10 || size.width < 60 { + self.components.error.draw(f, Some(size)); + } else { + match self.state { + State::Dashboard => todo!(), + State::Normal => todo!(), + State::Insert => todo!(), + State::Help => todo!(), + State::ChannelSwitch => self.components.channel_switcher.draw(f, emotes), + State::MessageSearch => todo!(), + } + } + // } else if app.get_state() == State::Dashboard + // || (Some(State::Dashboard) == app.get_previous_state() + // && State::ChannelSwitch == app.get_state()) + // { + // render_dashboard_ui(f, &mut app, &config); + // } else { + // render_chat_ui(f, &mut app, &config, &mut emotes); + // } + } + + pub fn event(&mut self, event: Event) -> Option { + match self.state { + State::Dashboard => todo!(), + State::Normal => todo!(), + State::Insert => todo!(), + State::Help => todo!(), + State::ChannelSwitch => self.components.channel_switcher.event(event), + State::MessageSearch => todo!(), } } @@ -141,17 +137,3 @@ impl App { todo!("Rotate through different themes") } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_no_scroll_overflow_not_inverted() { - let mut scroll = Scrolling::new(false); - assert_eq!(scroll.get_offset(), 0); - - scroll.down(); - assert_eq!(scroll.get_offset(), 0); - } -} diff --git a/src/handlers/config.rs b/src/handlers/config.rs index ccfb8c7..56a5a1c 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -2,10 +2,12 @@ use color_eyre::eyre::{bail, Error, Result}; use serde::{Deserialize, Serialize}; use serde_with::DeserializeFromStr; use std::{ + cell::RefCell, env, fs::{create_dir_all, read_to_string, File}, io::Write, path::Path, + rc::Rc, str::FromStr, }; use toml::Table; @@ -21,6 +23,8 @@ use crate::{ utils::pathing::{cache_path, config_path}, }; +pub type SharedCompleteConfig = Rc>; + #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(default)] pub struct CompleteConfig { diff --git a/src/handlers/user_input/events.rs b/src/handlers/user_input/events.rs index 0de2252..a25e680 100644 --- a/src/handlers/user_input/events.rs +++ b/src/handlers/user_input/events.rs @@ -40,13 +40,13 @@ impl ToString for Key { } } -pub enum Event { - Input(I), +pub enum Event { + Input(Key), Tick, } pub struct Events { - rx: mpsc::Receiver>, + rx: mpsc::Receiver, } #[derive(Debug, Clone, Copy)] @@ -131,7 +131,7 @@ impl Events { Self { rx } } - pub async fn next(&mut self) -> Option> { + pub async fn next(&mut self) -> Option { self.rx.recv().await } } diff --git a/src/handlers/user_input/input.rs b/src/handlers/user_input/input.rs index 39f059b..e4453ba 100644 --- a/src/handlers/user_input/input.rs +++ b/src/handlers/user_input/input.rs @@ -17,6 +17,7 @@ use crate::{ pub enum TerminalAction { Quitting, + BackOneLayer, } struct UserActionAttributes<'a, 'b> { @@ -236,7 +237,7 @@ pub async fn handle_stateful_user_input( Key::Ctrl('p') => { panic!("Manual panic triggered by user."); } - Key::Ctrl('d') => app.debug.toggle(), + // Key::Ctrl('d') => app.debug.toggle(), Key::Char('?') => app.set_state(State::Help), Key::Char('q') => return Some(TerminalAction::Quitting), Key::Char('s') => app.set_state(State::ChannelSwitch), @@ -293,7 +294,7 @@ pub async fn handle_stateful_user_input( Key::Char('c') => app.set_state(State::Normal), Key::Char('s') => app.set_state(State::ChannelSwitch), Key::Ctrl('f') => app.set_state(State::MessageSearch), - Key::Ctrl('d') => app.debug.toggle(), + // Key::Ctrl('d') => app.debug.toggle(), Key::Ctrl('t') => app.filters.toggle(), Key::Ctrl('r') => app.filters.reverse(), Key::Char('i') | Key::Insert => app.set_state(State::Insert), diff --git a/src/handlers/user_input/mod.rs b/src/handlers/user_input/mod.rs index 677b0a1..b027183 100644 --- a/src/handlers/user_input/mod.rs +++ b/src/handlers/user_input/mod.rs @@ -1,2 +1,3 @@ pub mod events; pub mod input; +pub mod scrolling; diff --git a/src/handlers/user_input/scrolling.rs b/src/handlers/user_input/scrolling.rs new file mode 100644 index 0000000..412b107 --- /dev/null +++ b/src/handlers/user_input/scrolling.rs @@ -0,0 +1,53 @@ +use crate::handlers::config::SharedCompleteConfig; + +pub struct Scrolling { + /// Offset of scroll + offset: usize, + /// If the scrolling is currently inverted + inverted: bool, +} + +impl Scrolling { + pub const fn new(inverted: bool) -> Self { + Self { + offset: 0, + inverted, + } + } + + /// Scrolling upwards, towards the start of the chat + pub fn up(&mut self) { + self.offset += 1; + } + + /// Scrolling downwards, towards the most recent message(s) + pub fn down(&mut self) { + self.offset = self.offset.saturating_sub(1); + } + + pub const fn inverted(&self) -> bool { + self.inverted + } + + pub fn jump_to(&mut self, index: usize) { + self.offset = index; + } + + pub const fn get_offset(&self) -> usize { + self.offset + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_no_scroll_overflow_not_inverted() { + let mut scroll = Scrolling::new(false); + assert_eq!(scroll.get_offset(), 0); + + scroll.down(); + assert_eq!(scroll.get_offset(), 0); + } +} diff --git a/src/main.rs b/src/main.rs index bfadcc4..704125c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,12 +68,12 @@ async fn main() -> Result<()> { info!("Logging system initialised"); - let app = App::new(&config, raw_config); - let (twitch_tx, terminal_rx) = mpsc::channel(100); let (terminal_tx, twitch_rx) = broadcast::channel(100); let (emotes_tx, emotes_rx) = mpsc::channel(1); + let app = App::new(config.clone(), raw_config, terminal_tx); + info!("Started tokio communication channels."); if emotes::emotes_enabled(&config.frontend) { @@ -100,7 +100,7 @@ async fn main() -> Result<()> { twitch::twitch_irc(config, twitch_tx, twitch_rx).await; }); - terminal::ui_driver(cloned_config, app, terminal_tx, terminal_rx, emotes_rx).await; + terminal::ui_driver(cloned_config, app, terminal_rx, emotes_rx).await; std::process::exit(0) } diff --git a/src/terminal.rs b/src/terminal.rs index 6c24e4d..e0bb49e 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,5 +1,5 @@ use log::{debug, info}; -use std::time::Duration; +use std::{borrow::Borrow, time::Duration}; use tokio::sync::{broadcast::Sender, mpsc::Receiver}; use tui::layout::Rect; @@ -10,23 +10,17 @@ use crate::{ app::App, config::CompleteConfig, data::MessageData, - state::State, user_input::{ events::{Config, Events, Key}, - input::{handle_stateful_user_input, TerminalAction}, + input::TerminalAction, }, }, twitch::TwitchAction, - ui::{ - components::{render_dashboard_ui, render_error_ui}, - render::render_chat_ui, - }, }; pub async fn ui_driver( - mut config: CompleteConfig, - mut app: App, - tx: Sender, + config: CompleteConfig, + mut app: App<'_>, mut rx: Receiver, mut erx: Receiver, ) { @@ -56,6 +50,7 @@ pub async fn ui_driver( terminal.clear().unwrap(); let mut emotes: Emotes = Emotes::default(); + let mut terminal_size = Rect::default(); loop { @@ -76,9 +71,28 @@ pub async fn ui_driver( } } + if let Some(event) = events.next().await { + if let Some(action) = app.event(event) { + match action { + TerminalAction::Quitting => { + quit_terminal(terminal); + + break; + } + TerminalAction::BackOneLayer => { + if let Some(previous_state) = app.get_previous_state() { + app.set_state(previous_state); + } else { + app.set_state(config.borrow().terminal.start_state.clone()); + } + } + } + } + } + terminal - .draw(|frame| { - let size = frame.size(); + .draw(|f| { + let size = f.size(); if size != terminal_size { terminal_size = size; @@ -86,35 +100,9 @@ pub async fn ui_driver( emotes.loaded.clear(); } - if size.height < 10 || size.width < 60 { - render_error_ui( - frame, - &[ - "Window to small!", - "Must allow for at least 60x10.", - "Restart and resize.", - ], - ); - } else if app.get_state() == State::Dashboard - || (Some(State::Dashboard) == app.get_previous_state() - && State::ChannelSwitch == app.get_state()) - { - render_dashboard_ui(frame, &mut app, &config); - } else { - render_chat_ui(frame, &mut app, &config, &mut emotes); - } + app.draw(f, emotes.clone()); }) .unwrap(); - - if matches!( - handle_stateful_user_input(&mut events, &mut app, &mut config, tx.clone(), &mut emotes) - .await, - Some(TerminalAction::Quitting) - ) { - quit_terminal(terminal); - - break; - } } app.cleanup(); diff --git a/src/ui/components/channel_switcher.rs b/src/ui/components/channel_switcher.rs new file mode 100644 index 0000000..614cc31 --- /dev/null +++ b/src/ui/components/channel_switcher.rs @@ -0,0 +1,204 @@ +use std::string::ToString; + +use rustyline::{line_buffer::LineBuffer, At, Word}; +use tokio::sync::broadcast::Sender; +use tui::{ + backend::Backend, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +use crate::{ + emotes::Emotes, + handlers::{ + config::SharedCompleteConfig, + data::DataBuilder, + user_input::{ + events::{Event, Key}, + input::TerminalAction, + }, + }, + twitch::TwitchAction, + ui::{ + components::utils::centered_rect, + statics::{LINE_BUFFER_CAPACITY, TWITCH_MESSAGE_LIMIT}, + }, + utils::text::{get_cursor_position, title_spans, TitleStyle}, +}; + +#[derive(Debug)] +pub struct ChannelSwitcherWidget { + config: SharedCompleteConfig, + tx: Sender, + // TODO: Extract this out to shared [`Rc`] + emotes: Option, + input: LineBuffer, +} + +impl ChannelSwitcherWidget { + pub fn new(config: SharedCompleteConfig, tx: Sender) -> Self { + Self { + config, + tx, + emotes: None, + input: LineBuffer::with_capacity(*LINE_BUFFER_CAPACITY), + } + } +} + +// let suggestion = if channel_suggestions { +// first_similarity( +// &app.storage +// .get("channels") +// .iter() +// .map(ToString::to_string) +// .collect::>(), +// input_buffer, +// ) +// } else { +// None +// }; + +// This can't implement the [`Component`] trait due to needing +// emotes to be passed through when drawing. +impl ChannelSwitcherWidget { + pub fn draw(&mut self, f: &mut Frame, emotes: Emotes) { + self.emotes = Some(emotes); + + // let area = area.map_or_else(|| centered_rect(60, 20, f.size()), |a| a); + let area = centered_rect(60, 20, f.size()); + + let cursor_pos = get_cursor_position(&self.input); + + f.set_cursor( + (area.x + cursor_pos as u16 + 1).min(area.x + area.width.saturating_sub(2)), + area.y + 1, + ); + + let current_input = self.input.as_str(); + + let binding = [TitleStyle::Single("Channel Switcher")]; + + let status_color = Color::Green; + + let paragraph = Paragraph::new(current_input) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(self.config.borrow().frontend.border_type.clone().into()) + .border_style(Style::default().fg(status_color)) + .title(title_spans( + &binding, + Style::default() + .fg(status_color) + .add_modifier(Modifier::BOLD), + )), + ) + .scroll((0, ((cursor_pos + 3) as u16).saturating_sub(area.width))); + + f.render_widget(Clear, area); + f.render_widget(paragraph, area); + } + + pub fn event(&mut self, event: Event) -> Option { + if let Event::Input(key) = event { + match key { + Key::Ctrl('f') | Key::Right => { + self.input.move_forward(1); + } + Key::Ctrl('b') | Key::Left => { + self.input.move_backward(1); + } + Key::Ctrl('a') | Key::Home => { + self.input.move_home(); + } + Key::Ctrl('e') | Key::End => { + self.input.move_end(); + } + Key::Alt('f') => { + self.input.move_to_next_word(At::AfterEnd, Word::Emacs, 1); + } + Key::Alt('b') => { + self.input.move_to_prev_word(Word::Emacs, 1); + } + Key::Ctrl('t') => { + self.input.transpose_chars(); + } + Key::Alt('t') => { + self.input.transpose_words(1); + } + Key::Ctrl('u') => { + self.input.discard_line(); + } + Key::Ctrl('k') => { + self.input.kill_line(); + } + Key::Ctrl('w') => { + self.input.delete_prev_word(Word::Emacs, 1); + } + Key::Ctrl('d') => { + self.input.delete(1); + } + Key::Backspace | Key::Delete => { + self.input.backspace(1); + } + // Key::Tab => { + // let suggestion = app.buffer_suggestion.clone(); + + // if let Some(suggestion_buffer) = suggestion { + // app.self.input + // .update(suggestion_buffer.as_str(), suggestion_buffer.len()); + // } + // } + Key::Enter => { + let input_message = &mut self.input; + + if input_message.is_empty() || input_message.len() > *TWITCH_MESSAGE_LIMIT { + return None; + } + + let mut message = DataBuilder::user( + self.config.borrow().twitch.username.to_string(), + input_message.to_string(), + ); + if let Some(mut emotes) = self.emotes.clone() { + message.parse_emotes(&mut emotes); + } + + // app.messages.push_front(message); + + self.tx + .send(TwitchAction::Privmsg(input_message.to_string())) + .unwrap(); + + // if let Some(msg) = input_message.strip_prefix('@') { + // app.storage.add("mentions", msg.to_string()); + // } + + let mut possible_command = String::new(); + + input_message.clone_into(&mut possible_command); + + input_message.update("", 0); + + // if possible_command.as_str() == "/clear" { + // app.clear_messages(); + // } + } + Key::Ctrl('q') => return Some(TerminalAction::Quitting), + Key::Char(c) => { + self.input.insert(c, 1); + } + Key::Esc => { + self.input.update("", 0); + + return Some(TerminalAction::BackOneLayer); + } + _ => {} + } + } + + None + } +} diff --git a/src/ui/components/channels.rs b/src/ui/components/channels.rs deleted file mode 100644 index 48b1559..0000000 --- a/src/ui/components/channels.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::string::ToString; - -use regex::Regex; -use tui::backend::Backend; - -use crate::{ - ui::{ - components::utils::{centered_popup, render_insert_box}, - statics::NAME_RESTRICTION_REGEX, - WindowAttributes, - }, - utils::text::first_similarity, -}; - -pub fn render_channel_switcher(window: WindowAttributes, channel_suggestions: bool) { - let WindowAttributes { - frame, - app, - layout: _, - frontend: _, - } = &window; - - let input_buffer = &app.input_buffer; - - let input_rect = centered_popup(frame.size(), frame.size().height); - - let suggestion = if channel_suggestions { - first_similarity( - &app.storage - .get("channels") - .iter() - .map(ToString::to_string) - .collect::>(), - input_buffer, - ) - } else { - None - }; - - render_insert_box( - window, - "Channel", - Some(input_rect), - suggestion, - Some(Box::new(|s: String| -> bool { - Regex::new(&NAME_RESTRICTION_REGEX) - .unwrap() - .is_match(s.as_str()) - })), - ); -} diff --git a/src/ui/components/chatting.rs b/src/ui/components/chatting.rs index ab182c0..8c7aa15 100644 --- a/src/ui/components/chatting.rs +++ b/src/ui/components/chatting.rs @@ -4,19 +4,11 @@ use crate::{ ui::{ components::utils::render_insert_box, statics::{COMMANDS, TWITCH_MESSAGE_LIMIT}, - WindowAttributes, }, utils::text::first_similarity, }; -pub fn render_chat_box(window: WindowAttributes, mention_suggestions: bool) { - let WindowAttributes { - frame: _, - app, - layout: _, - frontend: _, - } = &window; - +pub fn render_chat_box(mention_suggestions: bool) { let input_buffer = &app.input_buffer; let current_input = input_buffer.to_string(); diff --git a/src/ui/components/dashboard.rs b/src/ui/components/dashboard.rs index 46a8ad9..84b6250 100644 --- a/src/ui/components/dashboard.rs +++ b/src/ui/components/dashboard.rs @@ -1,4 +1,4 @@ -use std::slice::Iter; +use std::{collections::HashMap, slice::Iter}; use tui::{ backend::Backend, @@ -10,11 +10,17 @@ use tui::{ }; use crate::{ - handlers::{app::App, config::CompleteConfig, state::State}, - ui::{ - components::{render_channel_switcher, render_debug_window}, - WindowAttributes, + handlers::{ + app::App, + config::{CompleteConfig, SharedCompleteConfig}, + state::State, + storage::Storage, + user_input::{ + events::{Event, Key}, + input::TerminalAction, + }, }, + ui::components::Component, utils::styles::DASHBOARD_TITLE_COLOR, }; @@ -26,174 +32,185 @@ const DASHBOARD_TITLE: [&str; 5] = [ "\\__/ |__/|__/_/\\__/\\___/_/ /_/ \\__/\\__,_/_/ ", ]; -fn create_interactive_list_widget(items: &[String], index_offset: usize) -> List<'_> { - List::new( - items - .iter() - .enumerate() - .map(|(i, s)| { - ListItem::new(Spans::from(vec![ - Span::raw("["), - Span::styled( - (i + index_offset).to_string(), - Style::default().fg(Color::LightMagenta), - ), - Span::raw("] "), - Span::raw(s), - ])) - }) - .collect::>(), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) +#[derive(Debug)] +pub struct DashboardWidget<'a> { + config: SharedCompleteConfig, + storage: &'a Storage, } -fn render_dashboard_title_widget(frame: &mut Frame, v_chunks: &mut Iter) { - let w = Paragraph::new( - DASHBOARD_TITLE - .iter() - .map(|&s| Spans::from(vec![Span::raw(s)])) - .collect::>(), - ) - .style(DASHBOARD_TITLE_COLOR); - - frame.render_widget(w, *v_chunks.next().unwrap()); -} - -fn render_channel_selection_widget( - frame: &mut Frame, - v_chunks: &mut Iter, - app: &App, - current_channel: String, - default_channels: &[String], -) { - frame.render_widget( - Paragraph::new("Currently selected channel").style(Style::default().fg(Color::LightRed)), - *v_chunks.next().unwrap(), - ); - - let current_channel_selection = Paragraph::new(Spans::from(vec![ - Span::raw("["), - Span::styled( - "ENTER".to_string(), - Style::default().fg(Color::LightMagenta), - ), - Span::raw("] "), - Span::raw(current_channel), - ])); - - frame.render_widget(current_channel_selection, *v_chunks.next().unwrap()); - - frame.render_widget( - Paragraph::new("Configured default channels").style(Style::default().fg(Color::LightRed)), - *v_chunks.next().unwrap(), - ); - - if default_channels.is_empty() { - frame.render_widget(Paragraph::new("None"), *v_chunks.next().unwrap()); - } else { - let default_channels_widget = create_interactive_list_widget(default_channels, 0); - - frame.render_widget(default_channels_widget, *v_chunks.next().unwrap()); - } - - frame.render_widget( - Paragraph::new("Most recent channels").style(Style::default().fg(Color::LightRed)), - *v_chunks.next().unwrap(), - ); - - let recent_channels = app.storage.get_last_n("channels", 5, true); - - if recent_channels.is_empty() { - frame.render_widget(Paragraph::new("None"), *v_chunks.next().unwrap()); - } else { - let recent_channels_widget = - create_interactive_list_widget(&recent_channels, default_channels.len()); - - frame.render_widget(recent_channels_widget, *v_chunks.next().unwrap()); +impl<'a> DashboardWidget<'a> { + pub fn new(config: SharedCompleteConfig, storage: &'a Storage) -> Self { + Self { config, storage } } } -fn render_quit_selection_widget(frame: &mut Frame, v_chunks: &mut Iter) { - let quit_option = Paragraph::new(Spans::from(vec![ - Span::raw("["), - Span::styled("q", Style::default().fg(Color::LightMagenta)), - Span::raw("] "), - Span::raw("Quit"), - ])); +impl<'a> DashboardWidget<'a> { + fn create_interactive_list_widget(&self, items: &'a [String], index_offset: usize) -> List<'_> { + List::new( + items + .iter() + .enumerate() + .map(|(i, s)| { + ListItem::new(Spans::from(vec![ + Span::raw("["), + Span::styled( + (i + index_offset).to_string(), + Style::default().fg(Color::LightMagenta), + ), + Span::raw("] "), + Span::raw(s), + ])) + }) + .collect::>(), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) + } - frame.render_widget(quit_option, *v_chunks.next().unwrap()); -} + fn render_dashboard_title_widget( + &self, + frame: &mut Frame, + v_chunks: &mut Iter, + ) { + let w = Paragraph::new( + DASHBOARD_TITLE + .iter() + .map(|&s| Spans::from(vec![Span::raw(s)])) + .collect::>(), + ) + .style(DASHBOARD_TITLE_COLOR); -pub fn render_dashboard_ui( - frame: &mut Frame, - app: &mut App, - config: &CompleteConfig, -) { - let start_screen_channels_len = config.frontend.start_screen_channels.len() as u16; + frame.render_widget(w, *v_chunks.next().unwrap()); + } - let recent_channels_len = app.storage.get("channels").len() as u16; - - let h_chunk_binding = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(1), Constraint::Percentage(50)]) - .split(frame.size()); - - if app.debug.is_visible() { - render_debug_window( - frame, - h_chunk_binding[1], - app.debug.clone(), - config.frontend.clone(), + fn render_channel_selection_widget( + &self, + frame: &mut Frame, + v_chunks: &mut Iter, + current_channel: String, + default_channels: &[String], + ) { + frame.render_widget( + Paragraph::new("Currently selected channel") + .style(Style::default().fg(Color::LightRed)), + *v_chunks.next().unwrap(), ); + + let current_channel_selection = Paragraph::new(Spans::from(vec![ + Span::raw("["), + Span::styled( + "ENTER".to_string(), + Style::default().fg(Color::LightMagenta), + ), + Span::raw("] "), + Span::raw(current_channel), + ])); + + frame.render_widget(current_channel_selection, *v_chunks.next().unwrap()); + + frame.render_widget( + Paragraph::new("Configured default channels") + .style(Style::default().fg(Color::LightRed)), + *v_chunks.next().unwrap(), + ); + + if default_channels.is_empty() { + frame.render_widget(Paragraph::new("None"), *v_chunks.next().unwrap()); + } else { + let default_channels_widget = self.create_interactive_list_widget(default_channels, 0); + + frame.render_widget(default_channels_widget, *v_chunks.next().unwrap()); + } + + frame.render_widget( + Paragraph::new("Most recent channels").style(Style::default().fg(Color::LightRed)), + *v_chunks.next().unwrap(), + ); + + let recent_channels = self.storage.get_last_n("channels", 5, true); + + if recent_channels.is_empty() { + frame.render_widget(Paragraph::new("None"), *v_chunks.next().unwrap()); + } else { + let recent_channels_widget = + self.create_interactive_list_widget(&recent_channels, default_channels.len()); + + frame.render_widget(recent_channels_widget, *v_chunks.next().unwrap()); + } } - let v_chunk_binding = Layout::default() - .direction(Direction::Vertical) - .constraints([ - // Twitch-tui ASCII logo - Constraint::Min(DASHBOARD_TITLE.len() as u16 + 2), - // Currently selected channel title, content - Constraint::Length(2), - Constraint::Min(2), - // Configured default channels title, content - Constraint::Length(2), - Constraint::Min(if start_screen_channels_len == 0 { - 2 - } else { - start_screen_channels_len + 1 - }), - // Recent channel title, content - Constraint::Length(2), - Constraint::Min(if recent_channels_len == 0 { - 2 - } else { - recent_channels_len + 1 - }), - // Quit - Constraint::Min(1), - ]) - .margin(2) - .split(h_chunk_binding[0]); + fn render_quit_selection_widget( + &self, + frame: &mut Frame, + v_chunks: &mut Iter, + ) { + let quit_option = Paragraph::new(Spans::from(vec![ + Span::raw("["), + Span::styled("q", Style::default().fg(Color::LightMagenta)), + Span::raw("] "), + Span::raw("Quit"), + ])); - let mut v_chunks = v_chunk_binding.iter(); - - render_dashboard_title_widget(frame, &mut v_chunks); - - render_channel_selection_widget( - frame, - &mut v_chunks, - app, - config.twitch.channel.clone(), - &config.frontend.start_screen_channels.clone(), - ); - - render_quit_selection_widget(frame, &mut v_chunks); - - if Some(State::Dashboard) == app.get_previous_state() { - render_channel_switcher( - WindowAttributes::new(frame, app, None, config.frontend.clone()), - config.storage.channels, - ); + frame.render_widget(quit_option, *v_chunks.next().unwrap()); + } +} + +impl<'a> Component for DashboardWidget<'a> { + fn draw(&self, f: &mut Frame, area: Option) { + let area = area.map_or_else(|| f.size(), |a| a); + + let start_screen_channels_len = + self.config.borrow().frontend.start_screen_channels.len() as u16; + + let recent_channels_len = self.storage.get("channels").len() as u16; + + let v_chunk_binding = Layout::default() + .direction(Direction::Vertical) + .constraints([ + // Twitch-tui ASCII logo + Constraint::Min(DASHBOARD_TITLE.len() as u16 + 2), + // Currently selected channel title, content + Constraint::Length(2), + Constraint::Min(2), + // Configured default channels title, content + Constraint::Length(2), + Constraint::Min(if start_screen_channels_len == 0 { + 2 + } else { + start_screen_channels_len + 1 + }), + // Recent channel title, content + Constraint::Length(2), + Constraint::Min(if recent_channels_len == 0 { + 2 + } else { + recent_channels_len + 1 + }), + // Quit + Constraint::Min(1), + ]) + .margin(2) + .split(area); + + let mut v_chunks = v_chunk_binding.iter(); + + self.render_dashboard_title_widget(f, &mut v_chunks); + + self.render_channel_selection_widget( + f, + &mut v_chunks, + self.config.borrow().twitch.channel.clone(), + &self.config.borrow().frontend.start_screen_channels.clone(), + ); + + self.render_quit_selection_widget(f, &mut v_chunks); + } + + fn event(&mut self, event: Event) -> Option { + if matches!(event, Event::Input(Key::Char('q'))) { + return Some(TerminalAction::Quitting); + } + + None } } diff --git a/src/ui/components/debug.rs b/src/ui/components/debug.rs index aaee676..cbd7214 100644 --- a/src/ui/components/debug.rs +++ b/src/ui/components/debug.rs @@ -1,3 +1,4 @@ +use toml::Table as TomlTable; use tui::{ backend::Backend, layout::{Constraint, Rect}, @@ -7,54 +8,93 @@ use tui::{ }; use crate::{ - handlers::{app::DebugWindow, config::FrontendConfig}, + handlers::config::{FrontendConfig, SharedCompleteConfig}, utils::text::{title_spans, TitleStyle}, }; -pub fn render_debug_window( - frame: &mut Frame, - area: Rect, - debug: DebugWindow, - frontend: FrontendConfig, -) { - let mut rows = vec![]; +use super::Component; - if let Some(mut raw) = debug.raw_config { - // To avoid getting the user's token leaked in front of others. - raw.remove("twitch"); +// #[derive(Debug, PartialEq, Clone)] +// pub struct DebugWindow { +// visible: bool, +// pub raw_config: Option
, +// } - for item in raw.iter() { - rows.push(Row::new(vec![item.0.to_string()])); - let inner_map = item.1.as_table(); - if let Some(inner) = inner_map { - for inner_item in inner.iter() { - rows.push(Row::new(vec![ - " ".to_string(), - inner_item.0.to_string(), - inner_item.1.to_string(), - ])); +// impl DebugWindow { +// const fn new(visible: bool, raw_config: Option
) -> Self { +// Self { +// visible, +// raw_config, +// } +// } + +// pub const fn is_visible(&self) -> bool { +// self.visible +// } + +// pub fn toggle(&mut self) { +// self.visible = !self.visible; +// } +// } + +#[derive(Debug, Clone)] +pub struct DebugWidget { + config: SharedCompleteConfig, + raw_config: Option, + visible: bool, +} + +impl DebugWidget { + pub fn new(config: SharedCompleteConfig, raw_config: Option) -> Self { + Self { + config, + raw_config, + visible: false, + } + } +} + +impl Component for DebugWidget { + fn draw(&self, f: &mut Frame, area: Option) { + let mut rows = vec![]; + + if let Some(mut raw) = self.raw_config.clone() { + // To avoid getting the user's token leaked in front of others. + raw.remove("twitch"); + + for item in raw.iter() { + rows.push(Row::new(vec![item.0.to_string()])); + let inner_map = item.1.as_table(); + if let Some(inner) = inner_map { + for inner_item in inner.iter() { + rows.push(Row::new(vec![ + " ".to_string(), + inner_item.0.to_string(), + inner_item.1.to_string(), + ])); + } } } } + + let title_binding = [TitleStyle::Single("Debug")]; + + let table = Table::new(rows) + .block( + Block::default() + .title(title_spans( + &title_binding, + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_type(self.config.borrow().frontend.border_type.clone().into()), + ) + .widths(&[ + Constraint::Length(10), + Constraint::Length(25), + Constraint::Min(50), + ]); + + f.render_widget(table, area.unwrap()); } - - let title_binding = [TitleStyle::Single("Debug")]; - - let table = Table::new(rows) - .block( - Block::default() - .title(title_spans( - &title_binding, - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - .border_type(frontend.border_type.into()), - ) - .widths(&[ - Constraint::Length(10), - Constraint::Length(25), - Constraint::Min(50), - ]); - - frame.render_widget(table, area); } diff --git a/src/ui/components/error.rs b/src/ui/components/error.rs index 7b9b778..cddbd80 100644 --- a/src/ui/components/error.rs +++ b/src/ui/components/error.rs @@ -1,27 +1,46 @@ use tui::{ backend::Backend, - layout::{Alignment, Constraint, Direction, Layout}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Style}, terminal::Frame, text::{Span, Spans}, widgets::{Block, Borders, Paragraph}, }; -pub fn render_error_ui(frame: &mut Frame, messages: &[&str]) { - let v_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1)]) - .split(frame.size()); +use crate::handlers::config::SharedCompleteConfig; - let paragraph = Paragraph::new( - messages - .iter() - .map(|&s| Spans::from(vec![Span::raw(s)])) - .collect::>(), - ) - .block(Block::default().borders(Borders::NONE)) - .style(Style::default().fg(Color::White)) - .alignment(Alignment::Center); +use super::Component; - frame.render_widget(paragraph, v_chunks[0]); +const WINDOW_SIZE_ERROR_MESSAGE: [&str; 3] = [ + "Window to small!", + "Must allow for at least 60x10.", + "Restart and resize.", +]; + +#[derive(Debug, Clone)] +pub struct ErrorWidget { + config: SharedCompleteConfig, +} + +impl ErrorWidget { + pub const fn new(config: SharedCompleteConfig) -> Self { + Self { config } + } +} + +impl Component for ErrorWidget { + fn draw(&self, f: &mut Frame, area: Option) { + let paragraph = Paragraph::new( + WINDOW_SIZE_ERROR_MESSAGE + .iter() + .map(|&s| Spans::from(vec![Span::raw(s)])) + .collect::>(), + ) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center); + + // TODO: Make this a non-unwrap statement. + f.render_widget(paragraph, area.unwrap()); + } } diff --git a/src/ui/components/help.rs b/src/ui/components/help.rs index ded2938..52ee230 100644 --- a/src/ui/components/help.rs +++ b/src/ui/components/help.rs @@ -1,17 +1,14 @@ use tui::{ backend::Backend, - layout::Constraint, + layout::{Constraint, Rect}, style::{Modifier, Style}, widgets::{Block, Borders, Cell, Clear, Row, Table}, + Frame, }; use crate::{ - handlers::config::Theme, - ui::{ - statics::{HELP_COLUMN_TITLES, HELP_KEYBINDS}, - WindowAttributes, - }, - utils::styles::{self, BORDER_NAME_DARK, BORDER_NAME_LIGHT}, + ui::statics::{HELP_COLUMN_TITLES, HELP_KEYBINDS}, + utils::styles::COLUMN_TITLE, }; // Once a solution is found to calculate constraints, @@ -19,14 +16,7 @@ use crate::{ const TABLE_CONSTRAINTS: [Constraint; 3] = [Constraint::Min(11), Constraint::Min(8), Constraint::Min(38)]; -pub fn render_help_window(window: WindowAttributes) { - let WindowAttributes { - frame, - app, - layout, - frontend, - } = window; - +pub fn render_help_window(f: &mut Frame, area: Rect) { let mut rows = vec![]; for (s, v) in HELP_KEYBINDS.iter() { @@ -46,22 +36,17 @@ pub fn render_help_window(window: WindowAttributes) { } let help_table = Table::new(rows) - .header(Row::new(HELP_COLUMN_TITLES.iter().copied()).style(styles::COLUMN_TITLE)) + .header(Row::new(HELP_COLUMN_TITLES.iter().copied()).style(COLUMN_TITLE)) .block( - Block::default() - .borders(Borders::ALL) - .title("[ Keybinds ]") - .border_type(frontend.border_type.into()), + Block::default().borders(Borders::ALL).title("[ Keybinds ]"), // .border_type(frontend.border_type.into()), ) .widths(&TABLE_CONSTRAINTS) - .column_spacing(2) - .style(match app.theme { - Theme::Light => BORDER_NAME_LIGHT, - _ => BORDER_NAME_DARK, - }); + .column_spacing(2); + // .style(match app.theme { + // Theme::Light => BORDER_NAME_LIGHT, + // _ => BORDER_NAME_DARK, + // }); - if let Some(l) = layout { - frame.render_widget(Clear, l.first_chunk()); - frame.render_widget(help_table, l.first_chunk()); - } + f.render_widget(Clear, area); + f.render_widget(help_table, area); } diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 76af758..99dd7be 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,22 +1,80 @@ -mod channels; -pub use channels::render_channel_switcher; - +mod channel_switcher; mod chatting; -pub use chatting::render_chat_box; - mod dashboard; -pub use dashboard::render_dashboard_ui; - mod debug; -pub use debug::render_debug_window; - mod error; -pub use error::render_error_ui; - mod help; -pub use help::render_help_window; - mod state_tabs; +pub mod utils; + +pub use channel_switcher::ChannelSwitcherWidget; +pub use chatting::render_chat_box; +pub use dashboard::DashboardWidget; +pub use debug::DebugWidget; +pub use error::ErrorWidget; +pub use help::render_help_window; pub use state_tabs::render_state_tabs; -pub mod utils; +use tokio::sync::broadcast::Sender; +use toml::Table; +use tui::{backend::Backend, layout::Rect, Frame}; + +use crate::{ + handlers::{ + config::SharedCompleteConfig, + storage::Storage, + user_input::{ + events::{Event, Key}, + input::TerminalAction, + }, + }, + twitch::TwitchAction, +}; + +pub trait Component { + #[allow(unused_variables)] + fn draw(&self, f: &mut Frame, area: Option) { + todo!() + } + + fn event(&mut self, event: Event) -> Option { + if matches!(event, Event::Input(Key::Char('q'))) { + return Some(TerminalAction::Quitting); + } else if let Event::Input(key) = event { + match key { + _ => todo!(), + } + } + + None + } +} + +pub struct Components<'a> { + // Error window(s) + pub error: ErrorWidget, + + // Full window widgets + pub dashboard: DashboardWidget<'a>, + // pub chat: ChatWidget, + pub debug: DebugWidget, + + // Popup widgets + pub channel_switcher: ChannelSwitcherWidget, +} + +impl Components<'_> { + pub fn new( + config: &SharedCompleteConfig, + raw_config: Option
, + tx: Sender, + storage: Storage, + ) -> Self { + Self { + error: ErrorWidget::new(config.clone()), + dashboard: DashboardWidget::new(config.clone(), &storage), + debug: DebugWidget::new(config.clone(), raw_config), + channel_switcher: ChannelSwitcherWidget::new(config.clone(), tx), + } + } +} diff --git a/src/ui/components/state_tabs.rs b/src/ui/components/state_tabs.rs index 3738883..d977513 100644 --- a/src/ui/components/state_tabs.rs +++ b/src/ui/components/state_tabs.rs @@ -1,5 +1,6 @@ use tui::{ backend::Backend, + layout::Rect, style::{Color, Modifier, Style}, symbols::DOT, text::Spans, @@ -7,7 +8,7 @@ use tui::{ Frame, }; -use crate::{handlers::state::State, ui::LayoutAttributes}; +use crate::handlers::state::State; const TABS_TO_RENDER: [State; 5] = [ State::Normal, @@ -17,11 +18,7 @@ const TABS_TO_RENDER: [State; 5] = [ State::MessageSearch, ]; -pub fn render_state_tabs( - frame: &mut Frame, - layout: &LayoutAttributes, - current_state: &State, -) { +pub fn render_state_tabs(f: &mut Frame, area: Rect, current_state: &State) { let tab_titles = TABS_TO_RENDER .iter() .map(|t| Spans::from(t.to_string())) @@ -44,5 +41,5 @@ pub fn render_state_tabs( .unwrap(), ); - frame.render_widget(tabs, layout.last_chunk()); + f.render_widget(tabs, area); } diff --git a/src/ui/components/utils/insert_box.rs b/src/ui/components/utils/insert_box.rs index d72dc41..e5941af 100644 --- a/src/ui/components/utils/insert_box.rs +++ b/src/ui/components/utils/insert_box.rs @@ -6,51 +6,28 @@ use tui::{ style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, Clear, Paragraph}, + Frame, }; use crate::{ - handlers::state::State, - ui::{components::utils::centered_popup, WindowAttributes}, + handlers::{config::SharedCompleteConfig, state::State}, + ui::components::utils::centered_rect, utils::text::{get_cursor_position, title_spans, TitleStyle}, }; -/// Puts a box for user input at the bottom of the screen, -/// with an interactive cursor. -/// `input_validation` checks if the user's input is valid, changes window -/// theme to red if invalid, default otherwise. -pub fn render_insert_box( - window: WindowAttributes, +pub fn render_insert_box( + f: &mut Frame, + area: Rect, + config: SharedCompleteConfig, box_title: &str, - input_rectangle: Option, suggestion: Option, input_validation: Option bool>>, ) { - let WindowAttributes { - frame, - layout, - app, - frontend, - } = window; - - let buffer = &app.input_buffer; - let cursor_pos = get_cursor_position(buffer); - let input_rect = input_rectangle.map_or_else( - || { - if let Some(l) = layout { - l.chunks[l.constraints.len() - (if frontend.state_tabs { 2 } else { 1 })] - } else { - centered_popup(frame.size(), frame.size().height) - } - }, - |r| r, - ); - - frame.set_cursor( - (input_rect.x + cursor_pos as u16 + 1) - .min(input_rect.x + input_rect.width.saturating_sub(2)), - input_rect.y + 1, + f.set_cursor( + (area.x + cursor_pos as u16 + 1).min(area.x + area.width.saturating_sub(2)), + area.y + 1, ); let current_input = buffer.as_str(); @@ -93,16 +70,13 @@ pub fn render_insert_box( .add_modifier(Modifier::BOLD), )), ) - .scroll(( - 0, - ((cursor_pos + 3) as u16).saturating_sub(input_rect.width), - )); + .scroll((0, ((cursor_pos + 3) as u16).saturating_sub(area.width))); - if matches!(app.get_state(), State::ChannelSwitch) { - frame.render_widget(Clear, input_rect); - } + // if matches!(app.get_state(), State::ChannelSwitch) { + // frame.render_widget(Clear, area); + // } - frame.render_widget(paragraph, input_rect); + f.render_widget(paragraph, area); - app.buffer_suggestion = suggestion; + // app.buffer_suggestion = suggestion; } diff --git a/src/ui/components/utils/mod.rs b/src/ui/components/utils/mod.rs index 1b6cbbf..2a399e7 100644 --- a/src/ui/components/utils/mod.rs +++ b/src/ui/components/utils/mod.rs @@ -2,4 +2,4 @@ mod insert_box; pub use insert_box::render_insert_box; mod popups; -pub use popups::centered_popup; +pub use popups::centered_rect; diff --git a/src/ui/components/utils/popups.rs b/src/ui/components/utils/popups.rs index 1d1eff3..e6232ba 100644 --- a/src/ui/components/utils/popups.rs +++ b/src/ui/components/utils/popups.rs @@ -6,21 +6,48 @@ const HORIZONTAL_CONSTRAINTS: [Constraint; 3] = [ Constraint::Percentage(15), ]; -pub fn centered_popup(size: Rect, terminal_height: u16) -> Rect { +// pub fn centered_rect(size: Rect, terminal_height: u16) -> Rect { +// let popup_layout = Layout::default() +// .direction(Direction::Vertical) +// .constraints( +// [ +// Constraint::Length((terminal_height / 2) - 6), +// Constraint::Length(3), +// Constraint::Min(0), +// ] +// .as_ref(), +// ) +// .split(size); + +// Layout::default() +// .direction(Direction::Horizontal) +// .constraints(HORIZONTAL_CONSTRAINTS.as_ref()) +// .split(popup_layout[1])[1] +// } + +pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Length((terminal_height / 2) - 6), + Constraint::Percentage((100 - percent_y) / 2), + // Constraint::Percentage(percent_y), Constraint::Length(3), - Constraint::Min(0), + Constraint::Percentage((100 - percent_y) / 2), ] .as_ref(), ) - .split(size); + .split(r); Layout::default() .direction(Direction::Horizontal) - .constraints(HORIZONTAL_CONSTRAINTS.as_ref()) + .constraints( + [ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ] + .as_ref(), + ) .split(popup_layout[1])[1] } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d95eec6..1480a91 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use tui::{ backend::Backend, layout::{Constraint, Rect}, @@ -9,49 +11,3 @@ use crate::handlers::{app::App, config::FrontendConfig}; pub mod components; pub mod render; pub mod statics; - -#[derive(Debug, Clone)] -pub struct LayoutAttributes { - constraints: Vec, - chunks: Vec, -} - -impl LayoutAttributes { - pub fn new(constraints: Vec, chunks: Vec) -> Self { - Self { - constraints, - chunks, - } - } - - pub fn first_chunk(&self) -> Rect { - self.chunks[0] - } - - pub fn last_chunk(&self) -> Rect { - self.chunks[self.chunks.len() - 1] - } -} - -pub struct WindowAttributes<'a, 'b, 'c, T: Backend> { - frame: &'a mut Frame<'b, T>, - app: &'c mut App, - layout: Option, - frontend: FrontendConfig, -} - -impl<'a, 'b, 'c, T: Backend> WindowAttributes<'a, 'b, 'c, T> { - pub fn new( - frame: &'a mut Frame<'b, T>, - app: &'c mut App, - layout: Option, - frontend: FrontendConfig, - ) -> Self { - Self { - frame, - app, - layout, - frontend, - } - } -} diff --git a/src/ui/render.rs b/src/ui/render.rs index 8206602..556a924 100644 --- a/src/ui/render.rs +++ b/src/ui/render.rs @@ -1,10 +1,10 @@ -use std::{collections::VecDeque, vec}; +use std::{collections::VecDeque, rc::Rc, vec}; use chrono::offset::Local; use log::warn; use tui::{ backend::Backend, - layout::{Constraint, Direction, Layout}, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, terminal::Frame, text::{Span, Spans, Text}, @@ -21,13 +21,9 @@ use crate::{ config::{CompleteConfig, Theme}, state::State, }, - ui::{ - components::{ - render_channel_switcher, render_chat_box, render_debug_window, render_help_window, - render_state_tabs, - utils::{centered_popup, render_insert_box}, - }, - LayoutAttributes, WindowAttributes, + ui::components::{ + render_chat_box, render_help_window, render_state_tabs, + utils::{centered_rect, render_insert_box}, }, utils::{ styles::{BORDER_NAME_DARK, BORDER_NAME_LIGHT}, @@ -52,35 +48,17 @@ pub fn render_chat_ui( v_constraints.push(Constraint::Length(1)); } - let h_chunk_binding = if app.debug.is_visible() { - let h_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(frame.size()); + let h_chunk_binding = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .split(frame.size()); - render_debug_window( - frame, - h_chunks[1], - app.debug.clone(), - config.frontend.clone(), - ); - - h_chunks - } else { - Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .split(frame.size()) - }; - - let v_chunks = Layout::default() + let v_chunks: Rc<[Rect]> = Layout::default() .direction(Direction::Vertical) .margin(config.frontend.margin) .constraints(v_constraints.as_ref()) .split(h_chunk_binding[0]); - let layout = LayoutAttributes::new(v_constraints, v_chunks.to_vec()); - if app.messages.len() > config.terminal.maximum_messages { for data in app.messages.range(config.terminal.maximum_messages..) { hide_message_emotes(&data.emotes, &mut emotes.displayed, data.payload.width()); @@ -93,7 +71,7 @@ pub fn render_chat_ui( hide_all_emotes(emotes); VecDeque::new() } else { - get_messages(frame, app, config, emotes, &layout) + get_messages(frame, app, config, emotes, v_chunks) }; let current_time = Local::now() @@ -147,36 +125,32 @@ pub fn render_chat_ui( ) .style(Style::default().fg(Color::White)); - frame.render_widget(list, layout.first_chunk()); + frame.render_widget(list, v_chunks[0]); - if config.frontend.state_tabs { - render_state_tabs(frame, &layout, &app.get_state()); - } + // if config.frontend.state_tabs { + // render_state_tabs(frame, &layout, &app.get_state()); + // } - let window = WindowAttributes::new(frame, app, Some(layout), config.frontend.clone()); + // match window.app.get_state() { + // // States of the application that require a chunk of the main window + // State::Insert => render_chat_box(window, config.storage.mentions), + // State::MessageSearch => { + // let checking_func = |s: String| -> bool { !s.is_empty() }; - match window.app.get_state() { - // States of the application that require a chunk of the main window - State::Insert => render_chat_box(window, config.storage.mentions), - State::MessageSearch => { - let checking_func = |s: String| -> bool { !s.is_empty() }; + // render_insert_box( + // window, + // "Message Search", + // None, + // None, + // Some(Box::new(checking_func)), + // ); + // } - render_insert_box( - window, - "Message Search", - None, - None, - Some(Box::new(checking_func)), - ); - } - - // States that require popups - State::Help => render_help_window(window), - State::ChannelSwitch => { - render_channel_switcher(window, config.storage.channels); - } - _ => {} - } + // // States that require popups + // State::Help => render_help_window(window), + // // State::ChannelSwitch => app.components.channel_switcher.draw(frame), + // _ => {} + // } } fn get_messages<'a, T: Backend>( @@ -184,7 +158,7 @@ fn get_messages<'a, T: Backend>( app: &'a App, config: &CompleteConfig, emotes: &mut Emotes, - layout: &LayoutAttributes, + v_chunks: Rc<[Rect]>, ) -> VecDeque> { // Accounting for not all heights of rows to be the same due to text wrapping, // so extra space needs to be used in order to scroll correctly. @@ -194,7 +168,7 @@ fn get_messages<'a, T: Backend>( let mut scroll_offset = app.scrolling.get_offset(); - let general_chunk_height = layout.first_chunk().height as usize - 2; + let general_chunk_height = v_chunks[0].height as usize - 2; // Horizontal chunks represents the list within the main chat window. let h_chunk = Layout::default() @@ -205,7 +179,7 @@ fn get_messages<'a, T: Backend>( let message_chunk_width = h_chunk[0].width as usize; let channel_switcher = if app.get_state() == State::ChannelSwitch { - Some(centered_popup(frame.size(), frame.size().height)) + Some(centered_rect(60, 20, frame.size())) } else { None }; diff --git a/src/ui/statics.rs b/src/ui/statics.rs index 989bb00..f542d4c 100644 --- a/src/ui/statics.rs +++ b/src/ui/statics.rs @@ -74,6 +74,8 @@ lazy_static! { "w", ]; + pub static ref LINE_BUFFER_CAPACITY: usize = 4096; + // https://discuss.dev.twitch.tv/t/irc-bot-and-message-lengths/23327/4 pub static ref TWITCH_MESSAGE_LIMIT: usize = 500;