diff --git a/Cargo.lock b/Cargo.lock index 683c497..2613fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tui", + "unicode-width", ] [[package]] diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index 1c3e858..cf39b89 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -18,3 +18,4 @@ tokio-tungstenite = { version = "0.16.1", features = [ "rustls-tls-native-roots", ] } tui = "0.17.0" +unicode-width = "0.1.9" diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index 7e64167..31f40a9 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -2,124 +2,48 @@ mod config; mod never; mod replies; mod room; +mod textline; use std::io::{self, Stdout}; -use config::Config; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}; use crossterm::execute; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; -use palette::rgb::Rgb; -use palette::{FromColor, Hsl, Srgb}; -use tui::backend::CrosstermBackend; -use tui::layout::{Constraint, Direction, Layout}; -use tui::style::{Color, Modifier, Style}; -use tui::text::{Span, Spans}; -use tui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; -use tui::Terminal; +use textline::{TextLine, TextLineState}; +use tui::backend::{Backend, CrosstermBackend}; +use tui::{Frame, Terminal}; -async fn run(terminal: &mut Terminal>) -> anyhow::Result<()> { - terminal.draw(|f| { - let hchunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(20), - Constraint::Length(2), - Constraint::Min(0), - Constraint::Length(2), - Constraint::Length(20), - ]) - .split(f.size()); +#[derive(Debug, Default)] +struct Ui { + text: TextLineState, +} - // Borders - f.render_widget(Block::default().borders(Borders::LEFT), hchunks[1]); - f.render_widget(Block::default().borders(Borders::LEFT), hchunks[3]); +impl Ui { + fn draw(&mut self, f: &mut Frame) { + f.render_stateful_widget(TextLine, f.size(), &mut self.text); + self.text.set_cursor(f, f.size()); + } +} - // Room list - let room_style = Style::default().fg(Color::LightBlue); - let mut state = ListState::default(); - // state.select(Some(1)); - f.render_stateful_widget( - List::new(vec![ - ListItem::new(Span::styled( - "Cove", - Style::default().add_modifier(Modifier::BOLD), - )), - ListItem::new(Span::styled("&dunno", room_style)), - ListItem::new(Span::styled("&test", room_style)), - ListItem::new(" "), - ListItem::new(Span::styled( - "Euphoria", - Style::default().add_modifier(Modifier::BOLD), - )), - ListItem::new(Span::styled("&xkcd", room_style)), - ListItem::new(Span::styled("&music", room_style)), - ListItem::new(Span::styled("&bots", room_style)), - ListItem::new(" "), - ListItem::new(Span::styled( - "Instant", - Style::default().add_modifier(Modifier::BOLD), - )), - ListItem::new(Span::styled("&welcome", room_style)), - ]), - // .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - // .highlight_symbol(">"), - hchunks[0], - &mut state, - ); - // f.render_widget(Paragraph::new("foo"), hchunks[0]); +fn run(terminal: &mut Terminal>) -> anyhow::Result<()> { + let mut ui = Ui::default(); + loop { + terminal.draw(|f| ui.draw(f))?; - // Nick list - let nchunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(hchunks[4]); - f.render_widget( - Paragraph::new(Spans::from(vec![ - Span::styled("Users", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" "), - Span::styled("(13)", Style::default().fg(Color::Gray)), - ])), - nchunks[0], - ); - fn userstyle(r: u8, g: u8, b: u8) -> Style { - let rgb = Srgb::new(r, g, b).into_format::(); - let mut hsl = Hsl::from_color(rgb); - hsl.saturation = 1.0; - hsl.lightness = 0.7; - let rgb = Rgb::from_color(hsl).into_format::(); - Style::default().fg(Color::Rgb(rgb.red, rgb.green, rgb.blue)) + let event = crossterm::event::read()?; + + if let Event::Key(k) = event { + if k.code == KeyCode::Esc { + break; + } } - f.render_widget( - List::new([ - ListItem::new(Span::styled("TerryTvType", userstyle(192, 242, 238))), - ListItem::new(Span::styled("r*4", userstyle(192, 211, 242))), - ListItem::new(Span::styled("Swedish", userstyle(192, 242, 207))), - ListItem::new(Span::styled("Garmy", userstyle(242, 225, 192))), - ListItem::new(Span::styled("SRP", userstyle(242, 219, 192))), - ListItem::new(Span::styled("C", userstyle(192, 218, 242))), - ListItem::new(Span::styled("fill", userstyle(192, 197, 242))), - ListItem::new(Span::styled("ohnezo", userstyle(242, 203, 192))), - ListItem::new(Span::styled("Sumärzru", userstyle(242, 223, 192))), - ListItem::new(Span::styled("SuperGeek", userstyle(192, 242, 203))), - ListItem::new(Span::styled("certainlyhominid", userstyle(192, 242, 209))), - ListItem::new(Span::styled("Plugh", userstyle(192, 242, 215))), - ListItem::new(Span::styled( - "🎼\u{fe0e}🎷🎷🎷🎼\u{fe0e}", - userstyle(242, 192, 192), - )), - ]), - nchunks[1], - ); - })?; - let _ = crossterm::event::read(); + + ui.text.process_input(event); + } Ok(()) } -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let config = Config::load(); - +fn main() -> anyhow::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; crossterm::terminal::enable_raw_mode()?; @@ -130,7 +54,7 @@ async fn main() -> anyhow::Result<()> { )?; // Defer error handling so the terminal always gets restored properly - let result = run(&mut terminal).await; + let result = run(&mut terminal); crossterm::terminal::disable_raw_mode()?; execute!( diff --git a/cove-tui/src/textline.rs b/cove-tui/src/textline.rs new file mode 100644 index 0000000..fa8b5e2 --- /dev/null +++ b/cove-tui/src/textline.rs @@ -0,0 +1,49 @@ +use std::cmp; + +use crossterm::event::{Event, KeyCode}; +use tui::backend::Backend; +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::widgets::{Paragraph, StatefulWidget, Widget}; +use tui::Frame; +use unicode_width::UnicodeWidthStr; + +/// A simple single-line text box. +pub struct TextLine; + +impl StatefulWidget for TextLine { + type State = TextLineState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + Paragraph::new(&state.content as &str).render(area, buf); + // Paragraph::new("foo").render(area, buf); + } +} + +/// State for [`TextLine`]. +#[derive(Debug, Default)] +pub struct TextLineState { + content: String, +} + +impl TextLineState { + pub fn set_cursor(&self, f: &mut Frame, area: Rect) { + let x = area.x + (self.content.width() as u16); + let x = cmp::min(x, area.x + area.width); + f.set_cursor(x, area.y); + } + + pub fn process_input(&mut self, event: Event) { + if let Event::Key(k) = event { + match k.code { + KeyCode::Backspace => { + self.content.pop(); + } + KeyCode::Char(c) => { + self.content.push(c); + } + _ => {} + } + } + } +}