A lot of progress on specific widgets

This commit is contained in:
Xithrius 2023-04-23 02:37:33 -07:00
parent d88c23355e
commit 4482ba98fa
No known key found for this signature in database
GPG Key ID: A867F27CC80B28C1
25 changed files with 855 additions and 632 deletions

View File

@ -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.

View File

@ -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<String, (String, bool)>,

View File

@ -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<Table>,
}
impl DebugWindow {
const fn new(visible: bool, raw_config: Option<Table>) -> 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<MessageData>,
/// 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<Table>) -> Self {
impl App<'_> {
pub fn new(
config: CompleteConfig,
raw_config: Option<Table>,
tx: Sender<TwitchAction>,
) -> 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<B: Backend>(&mut self, f: &mut Frame<B>, 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<TerminalAction> {
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);
}
}

View File

@ -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<RefCell<CompleteConfig>>;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct CompleteConfig {

View File

@ -40,13 +40,13 @@ impl ToString for Key {
}
}
pub enum Event<I> {
Input(I),
pub enum Event {
Input(Key),
Tick,
}
pub struct Events {
rx: mpsc::Receiver<Event<Key>>,
rx: mpsc::Receiver<Event>,
}
#[derive(Debug, Clone, Copy)]
@ -131,7 +131,7 @@ impl Events {
Self { rx }
}
pub async fn next(&mut self) -> Option<Event<Key>> {
pub async fn next(&mut self) -> Option<Event> {
self.rx.recv().await
}
}

View File

@ -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),

View File

@ -1,2 +1,3 @@
pub mod events;
pub mod input;
pub mod scrolling;

View File

@ -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);
}
}

View File

@ -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)
}

View File

@ -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<TwitchAction>,
config: CompleteConfig,
mut app: App<'_>,
mut rx: Receiver<MessageData>,
mut erx: Receiver<Emotes>,
) {
@ -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();

View File

@ -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<TwitchAction>,
// TODO: Extract this out to shared [`Rc`]
emotes: Option<Emotes>,
input: LineBuffer,
}
impl ChannelSwitcherWidget {
pub fn new(config: SharedCompleteConfig, tx: Sender<TwitchAction>) -> 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::<Vec<String>>(),
// 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<B: Backend>(&mut self, f: &mut Frame<B>, 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<TerminalAction> {
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
}
}

View File

@ -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<T: Backend>(window: WindowAttributes<T>, 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::<Vec<String>>(),
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())
})),
);
}

View File

@ -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<T: Backend>(window: WindowAttributes<T>, mention_suggestions: bool) {
let WindowAttributes {
frame: _,
app,
layout: _,
frontend: _,
} = &window;
pub fn render_chat_box<T: Backend>(mention_suggestions: bool) {
let input_buffer = &app.input_buffer;
let current_input = input_buffer.to_string();

View File

@ -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::<Vec<ListItem>>(),
)
.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<T: Backend>(frame: &mut Frame<T>, v_chunks: &mut Iter<Rect>) {
let w = Paragraph::new(
DASHBOARD_TITLE
.iter()
.map(|&s| Spans::from(vec![Span::raw(s)]))
.collect::<Vec<Spans>>(),
)
.style(DASHBOARD_TITLE_COLOR);
frame.render_widget(w, *v_chunks.next().unwrap());
}
fn render_channel_selection_widget<T: Backend>(
frame: &mut Frame<T>,
v_chunks: &mut Iter<Rect>,
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<T: Backend>(frame: &mut Frame<T>, v_chunks: &mut Iter<Rect>) {
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::<Vec<ListItem>>(),
)
.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<T: Backend>(
&self,
frame: &mut Frame<T>,
v_chunks: &mut Iter<Rect>,
) {
let w = Paragraph::new(
DASHBOARD_TITLE
.iter()
.map(|&s| Spans::from(vec![Span::raw(s)]))
.collect::<Vec<Spans>>(),
)
.style(DASHBOARD_TITLE_COLOR);
pub fn render_dashboard_ui<T: Backend>(
frame: &mut Frame<T>,
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<T: Backend>(
&self,
frame: &mut Frame<T>,
v_chunks: &mut Iter<Rect>,
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<T: Backend>(
&self,
frame: &mut Frame<T>,
v_chunks: &mut Iter<Rect>,
) {
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<B: Backend>(&self, f: &mut Frame<B>, area: Option<Rect>) {
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<TerminalAction> {
if matches!(event, Event::Input(Key::Char('q'))) {
return Some(TerminalAction::Quitting);
}
None
}
}

View File

@ -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<T: Backend>(
frame: &mut Frame<T>,
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<Table>,
// }
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<Table>) -> 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<TomlTable>,
visible: bool,
}
impl DebugWidget {
pub fn new(config: SharedCompleteConfig, raw_config: Option<TomlTable>) -> Self {
Self {
config,
raw_config,
visible: false,
}
}
}
impl Component for DebugWidget {
fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Option<Rect>) {
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);
}

View File

@ -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<T: Backend>(frame: &mut Frame<T>, 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::<Vec<Spans>>(),
)
.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<B: Backend>(&self, f: &mut Frame<B>, area: Option<Rect>) {
let paragraph = Paragraph::new(
WINDOW_SIZE_ERROR_MESSAGE
.iter()
.map(|&s| Spans::from(vec![Span::raw(s)]))
.collect::<Vec<Spans>>(),
)
.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());
}
}

View File

@ -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<T: Backend>(window: WindowAttributes<T>) {
let WindowAttributes {
frame,
app,
layout,
frontend,
} = window;
pub fn render_help_window<T: Backend>(f: &mut Frame<T>, area: Rect) {
let mut rows = vec![];
for (s, v) in HELP_KEYBINDS.iter() {
@ -46,22 +36,17 @@ pub fn render_help_window<T: Backend>(window: WindowAttributes<T>) {
}
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);
}

View File

@ -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<B: Backend>(&self, f: &mut Frame<B>, area: Option<Rect>) {
todo!()
}
fn event(&mut self, event: Event) -> Option<TerminalAction> {
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<Table>,
tx: Sender<TwitchAction>,
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),
}
}
}

View File

@ -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<T: Backend>(
frame: &mut Frame<T>,
layout: &LayoutAttributes,
current_state: &State,
) {
pub fn render_state_tabs<T: Backend>(f: &mut Frame<T>, 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<T: Backend>(
.unwrap(),
);
frame.render_widget(tabs, layout.last_chunk());
f.render_widget(tabs, area);
}

View File

@ -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<T: Backend>(
window: WindowAttributes<T>,
pub fn render_insert_box<B: Backend>(
f: &mut Frame<B>,
area: Rect,
config: SharedCompleteConfig,
box_title: &str,
input_rectangle: Option<Rect>,
suggestion: Option<String>,
input_validation: Option<Box<dyn FnOnce(String) -> 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<T: Backend>(
.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;
}

View File

@ -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;

View File

@ -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]
}

View File

@ -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<Constraint>,
chunks: Vec<Rect>,
}
impl LayoutAttributes {
pub fn new(constraints: Vec<Constraint>, chunks: Vec<Rect>) -> 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<LayoutAttributes>,
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<LayoutAttributes>,
frontend: FrontendConfig,
) -> Self {
Self {
frame,
app,
layout,
frontend,
}
}
}

View File

@ -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<T: Backend>(
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<T: Backend>(
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<T: Backend>(
)
.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<Spans<'a>> {
// 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
};

View File

@ -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;