diff --git a/.cargo-husky/hooks/pre-push b/.cargo-husky/hooks/pre-push deleted file mode 100755 index ad5fb82..0000000 --- a/.cargo-husky/hooks/pre-push +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# Exit immediately if any command fails -set -e - -echo "Running pre-push checks..." - -echo "-> Running cargo check" -cargo check - -echo "-> Running cargo clippy" -cargo clippy -- -D warnings - -echo "-> Running cargo test" -cargo test diff --git a/src/main.rs b/src/main.rs index 8f3f176..8ca5e73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,45 @@ mod app; +mod state; mod ui; +use std::time::Duration; + use app::App; -use ratatui::crossterm::event::{self, Event}; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent}; #[tokio::main] async fn main() -> Result<(), Box> { - let mut terminal = ratatui::init(); + let servers = &*state::MOCK_SERVERS; + ratatui::run(|terminal| { + loop { + terminal.draw(|f| ui::render_state(f, servers)).unwrap(); + if event::poll(Duration::from_millis(16)).unwrap() + && let Event::Key(k) = event::read().unwrap() + && k.code == KeyCode::Char('q') + { + break; + } + } + }); - let mut app = App::new().await?; + // let mut terminal = ratatui::init(); + // let mut app = App::new().await?; - loop { - terminal.draw(|f| ui::render(f, &app))?; + // loop { + // terminal.draw(|f| ui::render(f, &app))?; - // Handle Keyboard Events - if event::poll(std::time::Duration::from_millis(16))? - && let Event::Key(key) = event::read()? - { - app.handle_key_event(key).await?; + // // Handle Keyboard Events + // if event::poll(std::time::Duration::from_millis(16))? + // && let Event::Key(key) = event::read()? + // { + // app.handle_key_event(key).await?; - if app.should_quit { - break; - } - } - } + // if app.should_quit { + // break; + // } + // } + // } - ratatui::restore(); + // ratatui::restore(); Ok(()) } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..0e7983e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,47 @@ +pub struct Server { + pub name: String, + // TODO: add in channel category separators, also probably add in cache mechanism for channels as well + pub channels: Vec, +} + +pub struct Channel { + pub name: String, + // will need to be replaced by a view into a cache mechanism + pub messages: Vec, + // we have a buffer per channel, such that you can write in a channel + // and then switch channels such that when you come back, the in-progress draft is still there + pub buffer: String, +} + +// hard coded mock data for development, remove once we've got everything else working +pub static MOCK_SERVERS: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + vec![ + Server { + name: "Test server".to_string(), + channels: vec![ + Channel { + name: "general".to_string(), + messages: vec!["howdy!".to_string()], + buffer: String::new(), + }, + Channel { + name: "media".to_string(), + messages: vec![ + "look at this cat!".to_string(), + "woahhh so pretty".to_string(), + "yessss I love cats!".to_string(), + ], + buffer: String::new(), + }, + ], + }, + Server { + name: "Epic server".to_string(), + channels: vec![Channel { + name: "Now this is epic".to_string(), + messages: vec!["Epic!!!".to_string()], + buffer: String::new(), + }], + }, + ] +}); diff --git a/src/ui.rs b/src/ui.rs index 51afe44..2f0ed93 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,9 +1,14 @@ -use crate::app::{App, AppState}; +use crate::{ + app::{App, AppState}, + state::{Channel, Server}, +}; use ratatui::{ Frame, layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, Paragraph}, + prelude::Rect, + style::{Color, Style, Stylize}, + text::Span, + widgets::{Block, Borders, List, ListItem, Paragraph, Widget}, }; pub fn render(f: &mut Frame, app: &App) { @@ -53,3 +58,118 @@ pub fn render(f: &mut Frame, app: &App) { } } } + +pub fn render_state(f: &mut Frame, servers: &[Server]) { + f.render_widget( + AppWidget { + servers, + focused_server: 0, + focused_channel: 1, + }, + f.area(), + ) +} + +struct AppWidget<'state> { + // list of servers to show on the left hand side + servers: &'state [Server], + // of the servers, which one are we focused on for the pannel in the center + focused_server: usize, + // which channel inside the server are we looking at + focused_channel: usize, +} + +const SERVER_LIST_WIDTH: u16 = 20; + +impl Widget for AppWidget<'_> { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let mut server_list_area = area; + server_list_area.width = SERVER_LIST_WIDTH; + let mut server_content_area = area; + server_content_area.x += SERVER_LIST_WIDTH; + server_content_area.width -= SERVER_LIST_WIDTH; + + List::new(self.servers.iter().enumerate().map(|(i, s)| { + let item = ListItem::new(s.name.as_str()); + if self.focused_server == i { + item.reversed() + } else { + item + } + })) + .block(Block::bordered()) + .render(server_list_area, buf); + ServerWidget { + server: &self.servers[self.focused_server], + focused_channel: self.focused_channel, + } + .render(server_content_area, buf); + } +} + +struct ServerWidget<'server> { + server: &'server Server, + focused_channel: usize, +} + +impl Widget for ServerWidget<'_> { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let mut channel_list_area = area; + channel_list_area.width = 20; + let mut channel_area = area; + channel_area.x += 20; + channel_area.width -= 20; + + List::new(self.server.channels.iter().enumerate().map(|(i, c)| { + let item = ListItem::new(c.name.as_str()); + if i == self.focused_channel { + item.reversed() + } else { + item + } + })) + .block(Block::bordered().title(self.server.name.as_str())) + .render(channel_list_area, buf); + ChannelWidget { + channel: &self.server.channels[self.focused_channel], + } + .render(channel_area, buf); + } +} + +struct ChannelWidget<'channel> { + channel: &'channel Channel, +} + +impl Widget for ChannelWidget<'_> { + fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let mut messages_area = area; + messages_area.height -= 5; + let mut input_area = area; + input_area.y = messages_area.height; + input_area.height = 5; + + List::new( + self.channel + .messages + .iter() + .rev() + .map(|msg| ListItem::new(msg.as_str())), + ) + .direction(ratatui::widgets::ListDirection::BottomToTop) + .block(Block::bordered().title(self.channel.name.as_str())) + .render(messages_area, buf); + Paragraph::new(self.channel.buffer.as_str()) + .block(Block::bordered()) + .render(input_area, buf); + } +}