mirror of
https://github.com/Xithrius/twitch-tui.git
synced 2024-10-04 09:07:33 +03:00
A lot of progress on specific widgets
This commit is contained in:
parent
d88c23355e
commit
4482ba98fa
@ -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.
|
||||
|
@ -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)>,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod events;
|
||||
pub mod input;
|
||||
pub mod scrolling;
|
||||
|
53
src/handlers/user_input/scrolling.rs
Normal file
53
src/handlers/user_input/scrolling.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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();
|
||||
|
204
src/ui/components/channel_switcher.rs
Normal file
204
src/ui/components/channel_switcher.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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())
|
||||
})),
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user