From b6bf444fcf574942461927fbeb5e42e506587973 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 12 Aug 2025 08:18:49 +0100 Subject: [PATCH 1/2] feat(blog): make blog title list interactive --- .config/config.json5 | 2 + src/action.rs | 6 ++- src/app.rs | 74 +++++++++++++++++++++++--------- src/components.rs | 2 + src/components/content.rs | 25 ++++------- src/components/selection_list.rs | 58 +++++++++++++++++++++++++ src/components/tabs.rs | 4 ++ src/ssh.rs | 2 +- 8 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 src/components/selection_list.rs 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 } } From caf29b35115f19e342413c20b670200a9a24bd10 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Tue, 12 Aug 2025 08:35:10 +0100 Subject: [PATCH 2/2] fix: broken build when `blog` feature is disabled Also brings back the warning on blog tab when feature isn't enabled. --- build.rs | 2 ++ src/app.rs | 33 +++++++++++++++++++++++---------- src/components.rs | 2 ++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/build.rs b/build.rs index 9d0c1d6..ddb1ff8 100644 --- a/build.rs +++ b/build.rs @@ -4,7 +4,9 @@ use anyhow::Result; use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey}; use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder}; +#[cfg(feature = "blog")] const ATPROTO_LEXICON_DIR: &str = "src/atproto/lexicons"; +#[cfg(feature = "blog")] const ATPROTO_CLIENT_DIR: &str = "src/atproto"; const SSH_KEY_ALGOS: &[(&'static str, Algorithm)] = &[ ("rsa.pem", Algorithm::Rsa { hash: None }), diff --git a/src/app.rs b/src/app.rs index d79f1dc..3103738 100644 --- a/src/app.rs +++ b/src/app.rs @@ -96,6 +96,7 @@ impl App { tabs, content, cat, + #[cfg(feature = "blog")] selection_list, }) } @@ -319,7 +320,7 @@ impl App { .map_err(|err| std::io::Error::other(err))?; // Render the content - let mut content_rect = Rect { + let content_rect = Rect { x: chunks[1].x, y: chunks[1].y, width: chunks[0].width, @@ -339,22 +340,34 @@ 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 + let mut content_rect = content_rect; 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, + #[cfg(feature = "blog")] + { + // Render the post selection list if the blog tab is selected + 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))?; + } + + #[cfg(not(feature = "blog"))] + { + // If blog feature is not enabled, render a placeholder + content_rect.height = 1; + let placeholder = Paragraph::new( + "Blog feature is disabled. Enable the `blog` feature to view this tab.", ) - .map_err(|err| std::io::Error::other(err))?; + .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)); + + frame.render_widget(placeholder, content_rect); + } } Ok::<_, std::io::Error>(()) diff --git a/src/components.rs b/src/components.rs index 9f3e076..46b19dd 100644 --- a/src/components.rs +++ b/src/components.rs @@ -15,11 +15,13 @@ use crate::{action::Action, config::Config, tui::Event}; mod tabs; mod content; mod cat; +#[cfg(feature = "blog")] mod selection_list; pub use tabs::*; pub use content::*; pub use cat::*; +#[cfg(feature = "blog")] pub use selection_list::*; /// `Component` is a trait that represents a visual and interactive element of the user interface.