feat(ui): filter repos
All checks were successful
Test / build (map[name:stable]) (push) Successful in 12m46s
Test / build (map[name:nightly]) (push) Successful in 12m0s
Release Please / Release-plz (push) Successful in 1m12s
Release Please / Docker image (push) Successful in 7m59s

Closes kemitix/git-next#206
This commit is contained in:
Paul Campbell 2025-01-19 13:58:24 +00:00
parent 90779527cf
commit 3833ba86db
8 changed files with 127 additions and 29 deletions

View file

@ -82,11 +82,12 @@ standardwebhooks = "1.0"
# boilerplate # boilerplate
bon = "3.0" bon = "3.0"
derive_more = { version = "1.0.0-beta", features = [ derive_more = { version = "1.0.0", features = [
"as_ref", "as_ref",
"constructor", "constructor",
"display", "display",
"deref", "deref",
"deref_mut",
"from", "from",
] } ] }
derive-with = "0.5" derive-with = "0.5"

View file

@ -23,7 +23,7 @@ pub use model::*;
use crate::tell; use crate::tell;
use super::Tick; use super::{components::key_focus::KeyFocus, Tick};
#[derive(Debug)] #[derive(Debug)]
pub struct Tui { pub struct Tui {
@ -92,7 +92,8 @@ impl Tui {
if key.kind != KeyEventKind::Press { if key.kind != KeyEventKind::Press {
return Ok(()); return Ok(());
} }
match key.code { match self.state.key_focus {
KeyFocus::None => match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
actor_tui.kill(); actor_tui.kill();
} }
@ -110,7 +111,23 @@ impl Tui {
KeyCode::Char('G') | KeyCode::End => { KeyCode::Char('G') | KeyCode::End => {
self.scroll_view_state.scroll_to_bottom(); 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(()) Ok(())

View file

@ -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::{ use ratatui::{
layout::Alignment, layout::Alignment,
prelude::{Buffer, Rect}, prelude::{Buffer, Rect},
@ -14,16 +19,21 @@ use tui_scrollview::ScrollViewState;
use git_next_core::{ use git_next_core::{
git::{self, graph::Log, Commit}, 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct State { pub struct State {
last_update: Instant, last_update: Instant,
started: Instant, started: Instant,
pub mode: ServerState, pub mode: ServerState,
pub filter: UIRepoFilter,
pub key_focus: KeyFocus,
} }
impl State { impl State {
pub fn initial() -> Self { pub fn initial() -> Self {
@ -31,6 +41,8 @@ impl State {
last_update: Instant::now(), last_update: Instant::now(),
started: Instant::now(), started: Instant::now(),
mode: ServerState::Initial { tick: 0 }, 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() chrono::Local::now().format("%H:%M").to_string()
} }
newtype!(
UIRepoFilter,
String,
Default,
DerefMut,
Display,
"Filter to limit repos shown"
);
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerState { pub enum ServerState {
/// UI has started but has no information on the state of the server /// UI has started but has no information on the state of the server
@ -232,6 +253,14 @@ pub enum RepoState {
}, },
} }
impl 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] #[tracing::instrument]
pub fn update_branches(&mut self, branches: RepoBranches) { pub fn update_branches(&mut self, branches: RepoBranches) {
match self { match self {
@ -359,13 +388,36 @@ impl StatefulWidget for &State {
Self: Sized, Self: Sized,
{ {
let block = Block::bordered() 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( .title_top(
Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold()) Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
.alignment(Alignment::Center), .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( .title_bottom(
Line::from(vec![ Line::from(vec![
" [q]uit ".into(), " [q]uit [/] filter ".into(),
self.beating_heart().into(), self.beating_heart().into(),
" ".into(), " ".into(),
]) ])
@ -380,7 +432,11 @@ impl StatefulWidget for &State {
.centered() .centered()
.render(interior, buf), .render(interior, buf),
ServerState::Configured { forges } => { ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf, state); ConfiguredAppWidget {
forges,
filter: &self.filter,
}
.render(interior, buf, state);
} }
} }
} }

View file

@ -8,12 +8,13 @@ use ratatui::{
}; };
use tui_scrollview::{ScrollView, ScrollViewState}; use tui_scrollview::{ScrollView, ScrollViewState};
use crate::tui::actor::ForgeState; use crate::tui::actor::{ForgeState, UIRepoFilter};
use super::{forge::ForgeWidget, HeightContraintLength}; use super::{forge::ForgeWidget, HeightContraintLength};
pub struct ConfiguredAppWidget<'a> { pub struct ConfiguredAppWidget<'a> {
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>, pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
pub filter: &'a UIRepoFilter,
} }
impl HeightContraintLength for ConfiguredAppWidget<'_> { impl HeightContraintLength for ConfiguredAppWidget<'_> {
fn height_constraint_length(&self) -> u16 { fn height_constraint_length(&self) -> u16 {
@ -62,6 +63,7 @@ impl<'a> ConfiguredAppWidget<'a> {
forge_alias, forge_alias,
repos: &state.repos, repos: &state.repos,
view_state: state.view_state, view_state: state.view_state,
filter: self.filter,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View file

@ -10,13 +10,14 @@ use ratatui::{
}; };
use crate::tui::{ use crate::tui::{
actor::RepoState, actor::{RepoState, UIRepoFilter},
components::{repo::RepoWidget, HeightContraintLength}, components::{repo::RepoWidget, HeightContraintLength},
}; };
pub struct ExpandedForgeWidget<'a> { pub struct ExpandedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias, pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>, pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub filter: &'a UIRepoFilter,
} }
impl HeightContraintLength for ExpandedForgeWidget<'_> { impl HeightContraintLength for ExpandedForgeWidget<'_> {
fn height_constraint_length(&self) -> u16 { fn height_constraint_length(&self) -> u16 {
@ -55,6 +56,15 @@ impl<'a> ExpandedForgeWidget<'a> {
fn children(&self) -> Vec<RepoWidget<'a>> { fn children(&self) -> Vec<RepoWidget<'a>> {
self.repos self.repos
.values() .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 }) .map(|repo_state| RepoWidget { repo_state })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View file

@ -9,7 +9,7 @@ use expanded::ExpandedForgeWidget;
use git_next_core::{ForgeAlias, RepoAlias}; use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::tui::actor::{RepoState, ViewState}; use crate::tui::actor::{RepoState, UIRepoFilter, ViewState};
use super::HeightContraintLength; use super::HeightContraintLength;
@ -17,6 +17,7 @@ pub struct ForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias, pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>, pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub view_state: ViewState, pub view_state: ViewState,
pub filter: &'a UIRepoFilter,
} }
impl HeightContraintLength for ForgeWidget<'_> { impl HeightContraintLength for ForgeWidget<'_> {
fn height_constraint_length(&self) -> u16 { fn height_constraint_length(&self) -> u16 {
@ -28,6 +29,7 @@ impl HeightContraintLength for ForgeWidget<'_> {
ViewState::Expanded => ExpandedForgeWidget { ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias, forge_alias: self.forge_alias,
repos: self.repos, repos: self.repos,
filter: self.filter,
} }
.height_constraint_length(), .height_constraint_length(),
} }
@ -46,6 +48,7 @@ impl Widget for ForgeWidget<'_> {
ViewState::Expanded => ExpandedForgeWidget { ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias, forge_alias: self.forge_alias,
repos: self.repos, repos: self.repos,
filter: self.filter,
} }
.render(area, buf), .render(area, buf),
} }

View file

@ -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,
}

View file

@ -2,6 +2,7 @@
mod configured_app; mod configured_app;
mod forge; mod forge;
mod history; mod history;
pub mod key_focus;
mod repo; mod repo;
pub use configured_app::ConfiguredAppWidget; pub use configured_app::ConfiguredAppWidget;