From 3833ba86db7ee4ef39e119b69abd97f5c43c6011 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 19 Jan 2025 13:58:24 +0000 Subject: [PATCH] feat(ui): filter repos Closes kemitix/git-next#206 --- Cargo.toml | 3 +- crates/cli/src/tui/actor/mod.rs | 57 ++++++++++------ crates/cli/src/tui/actor/model.rs | 66 +++++++++++++++++-- .../cli/src/tui/components/configured_app.rs | 4 +- .../cli/src/tui/components/forge/expanded.rs | 12 +++- crates/cli/src/tui/components/forge/mod.rs | 5 +- crates/cli/src/tui/components/key_focus.rs | 8 +++ crates/cli/src/tui/components/mod.rs | 1 + 8 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 crates/cli/src/tui/components/key_focus.rs diff --git a/Cargo.toml b/Cargo.toml index 08fec0a..62ca3f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,11 +82,12 @@ standardwebhooks = "1.0" # boilerplate bon = "3.0" -derive_more = { version = "1.0.0-beta", features = [ +derive_more = { version = "1.0.0", features = [ "as_ref", "constructor", "display", "deref", + "deref_mut", "from", ] } derive-with = "0.5" diff --git a/crates/cli/src/tui/actor/mod.rs b/crates/cli/src/tui/actor/mod.rs index f64db4e..5e8028e 100644 --- a/crates/cli/src/tui/actor/mod.rs +++ b/crates/cli/src/tui/actor/mod.rs @@ -23,7 +23,7 @@ pub use model::*; use crate::tell; -use super::Tick; +use super::{components::key_focus::KeyFocus, Tick}; #[derive(Debug)] pub struct Tui { @@ -92,25 +92,42 @@ impl Tui { if key.kind != KeyEventKind::Press { return Ok(()); } - match key.code { - KeyCode::Char('q') => { - actor_tui.kill(); - } - KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(), - KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(), - KeyCode::Char('f') | KeyCode::PageDown => { - self.scroll_view_state.scroll_page_down(); - } - KeyCode::Char('b') | KeyCode::PageUp => { - self.scroll_view_state.scroll_page_up(); - } - KeyCode::Char('g') | KeyCode::Home => { - self.scroll_view_state.scroll_to_top(); - } - KeyCode::Char('G') | KeyCode::End => { - self.scroll_view_state.scroll_to_bottom(); - } - _ => (), + match self.state.key_focus { + KeyFocus::None => match key.code { + KeyCode::Char('q') => { + actor_tui.kill(); + } + KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(), + KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(), + KeyCode::Char('f') | KeyCode::PageDown => { + self.scroll_view_state.scroll_page_down(); + } + KeyCode::Char('b') | KeyCode::PageUp => { + self.scroll_view_state.scroll_page_up(); + } + KeyCode::Char('g') | KeyCode::Home => { + self.scroll_view_state.scroll_to_top(); + } + KeyCode::Char('G') | KeyCode::End => { + self.scroll_view_state.scroll_to_bottom(); + } + KeyCode::Char('/') => { + self.state.key_focus = KeyFocus::Filter; + } + _ => (), + }, + KeyFocus::Filter => match key.code { + KeyCode::Char(char) => self.state.filter.push(char), + KeyCode::Backspace => { + self.state.filter.pop(); + } + KeyCode::Tab | KeyCode::Enter => self.state.key_focus = KeyFocus::None, + KeyCode::Esc => { + self.state.filter = UIRepoFilter::default(); + self.state.key_focus = KeyFocus::None; + } + _ => (), + }, } } Ok(()) diff --git a/crates/cli/src/tui/actor/model.rs b/crates/cli/src/tui/actor/model.rs index b746b0c..d99a15d 100644 --- a/crates/cli/src/tui/actor/model.rs +++ b/crates/cli/src/tui/actor/model.rs @@ -1,6 +1,11 @@ // -use std::{collections::BTreeMap, fmt::Display, time::Instant}; +use std::{ + collections::BTreeMap, + fmt::{Debug, Display}, + time::Instant, +}; +use derive_more::derive::{DerefMut, Display}; use ratatui::{ layout::Alignment, prelude::{Buffer, Rect}, @@ -14,16 +19,21 @@ use tui_scrollview::ScrollViewState; use git_next_core::{ git::{self, graph::Log, Commit}, - ForgeAlias, RepoAlias, RepoBranches, + newtype, s, ForgeAlias, RepoAlias, RepoBranches, }; -use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget}; +use crate::{ + server::actor::messages::ValidAppConfig, + tui::components::{key_focus::KeyFocus, ConfiguredAppWidget}, +}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct State { last_update: Instant, started: Instant, pub mode: ServerState, + pub filter: UIRepoFilter, + pub key_focus: KeyFocus, } impl State { pub fn initial() -> Self { @@ -31,6 +41,8 @@ impl State { last_update: Instant::now(), started: Instant::now(), mode: ServerState::Initial { tick: 0 }, + filter: UIRepoFilter::default(), + key_focus: KeyFocus::default(), } } @@ -55,6 +67,15 @@ fn time() -> String { chrono::Local::now().format("%H:%M").to_string() } +newtype!( + UIRepoFilter, + String, + Default, + DerefMut, + Display, + "Filter to limit repos shown" +); + #[derive(Clone, Debug, PartialEq, Eq)] pub enum ServerState { /// UI has started but has no information on the state of the server @@ -232,6 +253,14 @@ pub enum RepoState { }, } impl RepoState { + pub fn repo_alias(&self) -> &RepoAlias { + match self { + RepoState::Identified { repo_alias, .. } + | RepoState::Configured { repo_alias, .. } + | RepoState::Ready { repo_alias, .. } => repo_alias, + } + } + #[tracing::instrument] pub fn update_branches(&mut self, branches: RepoBranches) { match self { @@ -359,13 +388,36 @@ impl StatefulWidget for &State { Self: Sized, { let block = Block::bordered() + .title_top( + Line::from( + if self.filter.is_empty() && self.key_focus == KeyFocus::None { + s!("") + } else { + format!(" Filter: {} ", self.filter) + }, + ) + .alignment(Alignment::Left) + .style(if self.key_focus == KeyFocus::Filter { + Color::Red + } else { + Color::Green + }), + ) .title_top( Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold()) .alignment(Alignment::Center), ) + .title_bottom( + Line::from(if self.key_focus == KeyFocus::Filter { + s!(" [esc] clear [tab/enter] finish ") + } else { + s!("") + }) + .alignment(Alignment::Left), + ) .title_bottom( Line::from(vec![ - " [q]uit ".into(), + " [q]uit [/] filter ".into(), self.beating_heart().into(), " ".into(), ]) @@ -380,7 +432,11 @@ impl StatefulWidget for &State { .centered() .render(interior, buf), ServerState::Configured { forges } => { - ConfiguredAppWidget { forges }.render(interior, buf, state); + ConfiguredAppWidget { + forges, + filter: &self.filter, + } + .render(interior, buf, state); } } } diff --git a/crates/cli/src/tui/components/configured_app.rs b/crates/cli/src/tui/components/configured_app.rs index 64b3f8d..3230ec5 100644 --- a/crates/cli/src/tui/components/configured_app.rs +++ b/crates/cli/src/tui/components/configured_app.rs @@ -8,12 +8,13 @@ use ratatui::{ }; use tui_scrollview::{ScrollView, ScrollViewState}; -use crate::tui::actor::ForgeState; +use crate::tui::actor::{ForgeState, UIRepoFilter}; use super::{forge::ForgeWidget, HeightContraintLength}; pub struct ConfiguredAppWidget<'a> { pub forges: &'a BTreeMap, + pub filter: &'a UIRepoFilter, } impl HeightContraintLength for ConfiguredAppWidget<'_> { fn height_constraint_length(&self) -> u16 { @@ -62,6 +63,7 @@ impl<'a> ConfiguredAppWidget<'a> { forge_alias, repos: &state.repos, view_state: state.view_state, + filter: self.filter, }) .collect::>() } diff --git a/crates/cli/src/tui/components/forge/expanded.rs b/crates/cli/src/tui/components/forge/expanded.rs index 347215e..143d280 100644 --- a/crates/cli/src/tui/components/forge/expanded.rs +++ b/crates/cli/src/tui/components/forge/expanded.rs @@ -10,13 +10,14 @@ use ratatui::{ }; use crate::tui::{ - actor::RepoState, + actor::{RepoState, UIRepoFilter}, components::{repo::RepoWidget, HeightContraintLength}, }; pub struct ExpandedForgeWidget<'a> { pub forge_alias: &'a ForgeAlias, pub repos: &'a BTreeMap, + pub filter: &'a UIRepoFilter, } impl HeightContraintLength for ExpandedForgeWidget<'_> { fn height_constraint_length(&self) -> u16 { @@ -55,6 +56,15 @@ impl<'a> ExpandedForgeWidget<'a> { fn children(&self) -> Vec> { self.repos .values() + .filter(|repo_state| { + if self.filter.is_empty() { + true + } else { + let repo_alias = repo_state.repo_alias(); + // eprintln!("--> {} ? {}", self.filter.as_str(), repo_alias); + repo_alias.contains(self.filter.as_str()) + } + }) .map(|repo_state| RepoWidget { repo_state }) .collect::>() } diff --git a/crates/cli/src/tui/components/forge/mod.rs b/crates/cli/src/tui/components/forge/mod.rs index 0bc9afc..dcf350d 100644 --- a/crates/cli/src/tui/components/forge/mod.rs +++ b/crates/cli/src/tui/components/forge/mod.rs @@ -9,7 +9,7 @@ use expanded::ExpandedForgeWidget; use git_next_core::{ForgeAlias, RepoAlias}; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; -use crate::tui::actor::{RepoState, ViewState}; +use crate::tui::actor::{RepoState, UIRepoFilter, ViewState}; use super::HeightContraintLength; @@ -17,6 +17,7 @@ pub struct ForgeWidget<'a> { pub forge_alias: &'a ForgeAlias, pub repos: &'a BTreeMap, pub view_state: ViewState, + pub filter: &'a UIRepoFilter, } impl HeightContraintLength for ForgeWidget<'_> { fn height_constraint_length(&self) -> u16 { @@ -28,6 +29,7 @@ impl HeightContraintLength for ForgeWidget<'_> { ViewState::Expanded => ExpandedForgeWidget { forge_alias: self.forge_alias, repos: self.repos, + filter: self.filter, } .height_constraint_length(), } @@ -46,6 +48,7 @@ impl Widget for ForgeWidget<'_> { ViewState::Expanded => ExpandedForgeWidget { forge_alias: self.forge_alias, repos: self.repos, + filter: self.filter, } .render(area, buf), } diff --git a/crates/cli/src/tui/components/key_focus.rs b/crates/cli/src/tui/components/key_focus.rs new file mode 100644 index 0000000..e630ce7 --- /dev/null +++ b/crates/cli/src/tui/components/key_focus.rs @@ -0,0 +1,8 @@ +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum KeyFocus { + /// Keyboard is not focused on any input + #[default] + None, + /// Keyboard is focused on editing the filter + Filter, +} diff --git a/crates/cli/src/tui/components/mod.rs b/crates/cli/src/tui/components/mod.rs index 46dd2e9..c97cb55 100644 --- a/crates/cli/src/tui/components/mod.rs +++ b/crates/cli/src/tui/components/mod.rs @@ -2,6 +2,7 @@ mod configured_app; mod forge; mod history; +pub mod key_focus; mod repo; pub use configured_app::ConfiguredAppWidget;