diff --git a/.config/config.json5 b/.config/config.json5 index 5fb9dc3..69cb4f8 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -8,6 +8,8 @@ "": "Suspend", // Suspend the application "": "NextTab", // Go to the next tab "": "PrevTab", // Go to the previous tab + "": "SelectNext", // Go to the next selection in options + "": "SelectPrev", // Go to the previous selection in options }, } } diff --git a/src/action.rs b/src/action.rs index 0340a7b..bcb6e8f 100644 --- a/src/action.rs +++ b/src/action.rs @@ -15,5 +15,9 @@ pub enum Action { // Tab management NextTab, - PrevTab + PrevTab, + + // Selection management + SelectNext, + SelectPrev, } diff --git a/src/app.rs b/src/app.rs index a0b47b8..d79f1dc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,6 +43,8 @@ pub struct App { tabs: Arc>, content: Arc>, cat: Arc>, + #[cfg(feature = "blog")] + selection_list: Arc>, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -70,6 +72,13 @@ impl App { let cat = Arc::new(Mutex::new(Cat::new())); + #[cfg(feature = "blog")] + let rt = tokio::runtime::Handle::current(); + #[cfg(feature = "blog")] + let selection_list = Arc::new(Mutex::new(SelectionList::new( + rt.block_on(content.blocking_lock().blog_content())?, + ))); + Ok(Self { tick_rate, frame_rate, @@ -87,6 +96,7 @@ impl App { tabs, content, cat, + selection_list, }) } @@ -248,6 +258,11 @@ impl App { if let Some(action) = self.cat.try_lock()?.update(action.clone())? { self.action_tx.send(action)?; } + + #[cfg(feature = "blog")] + if let Some(action) = self.selection_list.try_lock()?.update(action.clone())? { + self.action_tx.send(action)?; + } } Ok(()) } @@ -287,33 +302,34 @@ impl App { ); // Render the tabs - self.tabs + let mut tabs = self + .tabs .try_lock() - .map_err(|err| std::io::Error::other(err))? - .draw( - frame, - Rect { - x: chunks[0].x + 14, - y: chunks[0].y + 1, - width: chunks[0].width - 6, - height: chunks[0].height, - }, - ) .map_err(|err| std::io::Error::other(err))?; + tabs.draw( + frame, + Rect { + x: chunks[0].x + 14, + y: chunks[0].y + 1, + width: chunks[0].width - 6, + height: chunks[0].height, + }, + ) + .map_err(|err| std::io::Error::other(err))?; + // Render the content + let mut content_rect = Rect { + x: chunks[1].x, + y: chunks[1].y, + width: chunks[0].width, + height: frame.area().height - chunks[0].height, + }; + self.content .try_lock() .map_err(|err| std::io::Error::other(err))? - .draw( - frame, - Rect { - x: chunks[1].x, - y: chunks[1].y, - width: chunks[0].width, - height: frame.area().height - chunks[0].height, - }, - ) + .draw(frame, content_rect) .map_err(|err| std::io::Error::other(err))?; // Render the eepy cat :3 @@ -323,6 +339,24 @@ impl App { .draw(frame, frame.area()) .map_err(|err| std::io::Error::other(err))?; + #[cfg(feature = "blog")] + if tabs.current_tab() == 2 { + // Render the post selection list if the blog tab is selected + content_rect.x += 1; + content_rect.y += 1; + content_rect.width -= 2; + content_rect.height -= 2; + + self.selection_list + .try_lock() + .map_err(|err| std::io::Error::other(err))? + .draw( + frame, + content_rect, + ) + .map_err(|err| std::io::Error::other(err))?; + } + Ok::<_, std::io::Error>(()) })?; Ok(()) diff --git a/src/components.rs b/src/components.rs index 60027d6..9f3e076 100644 --- a/src/components.rs +++ b/src/components.rs @@ -15,10 +15,12 @@ use crate::{action::Action, config::Config, tui::Event}; mod tabs; mod content; mod cat; +mod selection_list; pub use tabs::*; pub use content::*; pub use cat::*; +pub use selection_list::*; /// `Component` is a trait that represents a visual and interactive element of the user interface. /// diff --git a/src/components/content.rs b/src/components/content.rs index ae3f341..bfa0165 100644 --- a/src/components/content.rs +++ b/src/components/content.rs @@ -177,12 +177,12 @@ impl Content { /// Generate the content for the "Blog" tab #[cfg(feature = "blog")] - async fn blog_content(&self) -> Result>> { + pub async fn blog_content(&self) -> Result> { Ok(crate::atproto::blog::get_all_posts() .await? .iter() - .map(|post| Line::from(post.title.clone().unwrap_or("".to_string()))) - .collect()) + .map(|post| post.title.clone().unwrap_or("".to_string())) + .collect::>()) } } @@ -207,22 +207,13 @@ impl Component for Content { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let content = match self.selected_tab.load(Ordering::Relaxed) { + let selected_tab = self.selected_tab.load(Ordering::Relaxed); + let content = match selected_tab { 0 => self.about_content(area)?, 1 => self.projects_content(), - 2 => { - #[cfg(feature = "blog")] - { - let rt = tokio::runtime::Handle::current(); - rt.block_on(self.blog_content())? - } - - #[cfg(not(feature = "blog"))] - vec![Line::from( - "Blog feature is disabled. Enable the `blog` feature to view this tab.", - )] - } - _ => unreachable!(), + /* Blog tab handled in `App::render` */ + + _ => vec![], }; // Create the border lines diff --git a/src/components/selection_list.rs b/src/components/selection_list.rs new file mode 100644 index 0000000..fd5c7a2 --- /dev/null +++ b/src/components/selection_list.rs @@ -0,0 +1,58 @@ +use color_eyre::eyre::Result; +use ratatui::{ + style::{Color, Style}, + widgets::{List, ListState}, +}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::{action::Action, components::Component, config::Config}; + +#[derive(Default)] +pub struct SelectionList { + command_tx: Option>, + config: Config, + options: List<'static>, + list_state: ListState, +} + +impl SelectionList { + pub fn new(options: Vec) -> Self { + let mut list_state = ListState::default(); + list_state.select_first(); + + Self { + options: List::new(options).highlight_style(Style::default().fg(Color::Yellow)), + list_state, + ..Default::default() + } + } +} + +impl Component for SelectionList { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Tick => {} + Action::Render => {} + Action::SelectNext => self.list_state.select_next(), + Action::SelectPrev => self.list_state.select_previous(), + _ => {} + }; + + Ok(None) + } + + fn draw(&mut self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) -> Result<()> { + frame.render_stateful_widget(self.options.clone(), area, &mut self.list_state); + Ok(()) + } +} diff --git a/src/components/tabs.rs b/src/components/tabs.rs index 2adeea5..cfe4e98 100644 --- a/src/components/tabs.rs +++ b/src/components/tabs.rs @@ -35,6 +35,10 @@ impl Tabs { self.selected_tab.fetch_sub(1, Ordering::Relaxed); } } + + pub fn current_tab(&self) -> usize { + self.selected_tab.load(Ordering::Relaxed) + } } impl Component for Tabs { diff --git a/src/ssh.rs b/src/ssh.rs index 21b6a0c..2441efa 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -258,7 +258,7 @@ impl Server for SshServer { #[instrument(skip(self))] fn new_client(&mut self, peer_addr: Option) -> Self::Handler { - let session = SshSession::new(); + let session = tokio::task::block_in_place(|| SshSession::new()); session } }