Skip to content

Commit 97fc550

Browse files
authored
Merge pull request #3 from fixture-dev/claude/implement-label-view-012EiC2CYpfS8VTEqafQrkxQ
Implement label view in TUI navigation pane
2 parents dffdcbb + f659cb8 commit 97fc550

8 files changed

Lines changed: 262 additions & 62 deletions

File tree

‎crates/lash-tui/src/app.rs‎

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::io;
66
use std::path::Path;
77
use std::time::Duration;
88

9-
use lash_db::repository::{FileRepository, TaskRepository};
9+
use lash_db::repository::{FileRepository, LabelRepository, TaskRepository};
1010

1111
use crate::error::{TuiError, TuiResult};
1212
use crate::event::{poll_event, AppEvent};
@@ -73,6 +73,12 @@ impl TuiApp {
7373
.map_err(|e| TuiError::App(format!("Failed to load tasks: {e}")))?;
7474
}
7575

76+
// Load labels with stats
77+
let label_repo = LabelRepository::new(&conn);
78+
state.labels = label_repo
79+
.get_label_stats()
80+
.map_err(|e| TuiError::App(format!("Failed to load labels: {e}")))?;
81+
7682
Ok(Self {
7783
terminal,
7884
conn,
@@ -138,12 +144,13 @@ impl TuiApp {
138144
AppEvent::ExpandAll => self.handle_expand_all(),
139145
AppEvent::CollapseAll => self.handle_collapse_all(),
140146

147+
AppEvent::LabelFilter => self.handle_label_toggle()?,
148+
AppEvent::ClearFilters => self.state.clear_label_filter(),
149+
141150
// TODO: implement these features
142151
AppEvent::ExpandNode
143152
| AppEvent::CollapseNode
144-
| AppEvent::ClearFilters
145153
| AppEvent::Search
146-
| AppEvent::LabelFilter
147154
| AppEvent::DependencyGraph
148155
| AppEvent::PrevTask
149156
| AppEvent::NextTask
@@ -163,35 +170,47 @@ impl TuiApp {
163170

164171
/// Handle selection/enter in current pane
165172
fn handle_select(&mut self) -> TuiResult<()> {
173+
use crate::state::{FocusedPane, NavMode};
174+
166175
match self.state.focused_pane {
167-
crate::state::FocusedPane::Navigation => {
168-
// Check if tree view is active and get selected node info
169-
if let Some(selected) = self.state.selected_tree_node() {
170-
if selected.is_directory {
171-
// Toggle expand/collapse for directories
172-
self.state.toggle_selected_node();
173-
} else if let Some(file) = selected.file_record {
174-
// Load tasks for file and switch to detail pane
175-
let task_repo = TaskRepository::new(&self.conn);
176-
self.state.tasks = task_repo
177-
.get_by_file(file.id)
178-
.map_err(|e| TuiError::App(format!("Failed to load tasks: {e}")))?;
179-
self.state.selected_task_index = 0;
180-
self.state.switch_pane();
176+
FocusedPane::Navigation => {
177+
match self.state.nav_mode {
178+
NavMode::Files | NavMode::SearchResults => {
179+
// Check if tree view is active and get selected node info
180+
if let Some(selected) = self.state.selected_tree_node() {
181+
if selected.is_directory {
182+
// Toggle expand/collapse for directories
183+
self.state.toggle_selected_node();
184+
} else if let Some(file) = selected.file_record {
185+
// Load tasks for file and switch to detail pane
186+
let task_repo = TaskRepository::new(&self.conn);
187+
self.state.tasks = task_repo.get_by_file(file.id).map_err(|e| {
188+
TuiError::App(format!("Failed to load tasks: {e}"))
189+
})?;
190+
self.state.selected_task_index = 0;
191+
self.state.build_task_tree();
192+
self.state.switch_pane();
193+
}
194+
} else {
195+
// Fallback for flat view: load tasks for selected file
196+
if let Some(file) = self.state.selected_file() {
197+
let task_repo = TaskRepository::new(&self.conn);
198+
self.state.tasks = task_repo.get_by_file(file.id).map_err(|e| {
199+
TuiError::App(format!("Failed to load tasks: {e}"))
200+
})?;
201+
self.state.selected_task_index = 0;
202+
self.state.build_task_tree();
203+
self.state.switch_pane();
204+
}
205+
}
181206
}
182-
} else {
183-
// Fallback for flat view: load tasks for selected file
184-
if let Some(file) = self.state.selected_file() {
185-
let task_repo = TaskRepository::new(&self.conn);
186-
self.state.tasks = task_repo
187-
.get_by_file(file.id)
188-
.map_err(|e| TuiError::App(format!("Failed to load tasks: {e}")))?;
189-
self.state.selected_task_index = 0;
190-
self.state.switch_pane();
207+
NavMode::Labels => {
208+
// Select a label and filter tasks
209+
self.handle_label_select()?;
191210
}
192211
}
193212
}
194-
crate::state::FocusedPane::Detail => {
213+
FocusedPane::Detail => {
195214
// Enter on detail pane shows task details
196215
// TODO: Implement task detail view
197216
}
@@ -329,6 +348,43 @@ impl TuiApp {
329348
}
330349
}
331350
}
351+
352+
/// Handle toggling between files and labels view
353+
fn handle_label_toggle(&mut self) -> TuiResult<()> {
354+
// Reload labels to ensure we have latest stats
355+
let label_repo = LabelRepository::new(&self.conn);
356+
self.state.labels = label_repo
357+
.get_label_stats()
358+
.map_err(|e| TuiError::App(format!("Failed to load labels: {e}")))?;
359+
360+
self.state.toggle_nav_mode();
361+
Ok(())
362+
}
363+
364+
/// Handle selecting a label to filter tasks
365+
fn handle_label_select(&mut self) -> TuiResult<()> {
366+
let Some(label) = self.state.selected_label() else {
367+
return Ok(());
368+
};
369+
370+
let label_name = label.name.clone();
371+
372+
// Load tasks with this label
373+
let task_repo = TaskRepository::new(&self.conn);
374+
self.state.tasks = task_repo
375+
.find_by_label(&label_name)
376+
.map_err(|e| TuiError::App(format!("Failed to load tasks for label: {e}")))?;
377+
378+
// Set current filter and reset selection
379+
self.state.current_label_filter = Some(label_name);
380+
self.state.selected_task_index = 0;
381+
self.state.build_task_tree();
382+
383+
// Switch to detail pane to show filtered tasks
384+
self.state.focused_pane = crate::state::FocusedPane::Detail;
385+
386+
Ok(())
387+
}
332388
}
333389

334390
impl Drop for TuiApp {

‎crates/lash-tui/src/event.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ fn handle_key_event(key: KeyEvent) -> AppEvent {
114114
(KeyCode::Char(' '), KeyModifiers::NONE) => AppEvent::Select,
115115
(KeyCode::Char('e'), KeyModifiers::NONE) => AppEvent::OpenEditor,
116116
(KeyCode::Char('/'), KeyModifiers::NONE) => AppEvent::Search,
117+
(KeyCode::Char('F'), KeyModifiers::SHIFT) => AppEvent::LabelFilter,
117118
(KeyCode::Char('c'), KeyModifiers::NONE) => AppEvent::ClearFilters,
118119
(KeyCode::Char('g'), KeyModifiers::CONTROL) => AppEvent::DependencyGraph,
119120
(KeyCode::Char('?'), KeyModifiers::SHIFT) => AppEvent::Help,

‎crates/lash-tui/src/state.rs‎

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use crate::colors::Theme;
66
use lash_db::repository::files::FileRecord;
7+
use lash_db::repository::labels::LabelStats;
78
use lash_db::repository::tasks::TaskRecord;
89
use lash_types::tree::{TreeChars, TreeNode};
910
use std::path::PathBuf;
@@ -88,6 +89,15 @@ pub struct AppState {
8889

8990
/// Tree rendering characters (Unicode or ASCII)
9091
pub tree_chars: TreeChars,
92+
93+
/// Available labels with task counts (for label view)
94+
pub labels: Vec<LabelStats>,
95+
96+
/// Currently selected label (index into labels vec)
97+
pub selected_label_index: usize,
98+
99+
/// Current label filter (if any)
100+
pub current_label_filter: Option<String>,
91101
}
92102

93103
/// State for the theme selector modal
@@ -150,6 +160,9 @@ impl AppState {
150160
file_tree: None,
151161
task_tree: None,
152162
tree_chars: TreeChars::detect(),
163+
labels: Vec::new(),
164+
selected_label_index: 0,
165+
current_label_filter: None,
153166
}
154167
}
155168

@@ -215,11 +228,18 @@ impl AppState {
215228
/// Move selection up
216229
pub fn move_up(&mut self) {
217230
match self.focused_pane {
218-
FocusedPane::Navigation => {
219-
if self.selected_file_index > 0 {
220-
self.selected_file_index -= 1;
231+
FocusedPane::Navigation => match self.nav_mode {
232+
NavMode::Files | NavMode::SearchResults => {
233+
if self.selected_file_index > 0 {
234+
self.selected_file_index -= 1;
235+
}
221236
}
222-
}
237+
NavMode::Labels => {
238+
if self.selected_label_index > 0 {
239+
self.selected_label_index -= 1;
240+
}
241+
}
242+
},
223243
FocusedPane::Detail => {
224244
if self.selected_task_index > 0 {
225245
self.selected_task_index -= 1;
@@ -231,12 +251,19 @@ impl AppState {
231251
/// Move selection down
232252
pub fn move_down(&mut self) {
233253
match self.focused_pane {
234-
FocusedPane::Navigation => {
235-
let max_index = self.visible_tree_node_count();
236-
if self.selected_file_index + 1 < max_index {
237-
self.selected_file_index += 1;
254+
FocusedPane::Navigation => match self.nav_mode {
255+
NavMode::Files | NavMode::SearchResults => {
256+
let max_index = self.visible_tree_node_count();
257+
if self.selected_file_index + 1 < max_index {
258+
self.selected_file_index += 1;
259+
}
238260
}
239-
}
261+
NavMode::Labels => {
262+
if self.selected_label_index + 1 < self.labels.len() {
263+
self.selected_label_index += 1;
264+
}
265+
}
266+
},
240267
FocusedPane::Detail => {
241268
if self.selected_task_index + 1 < self.tasks.len() {
242269
self.selected_task_index += 1;
@@ -248,20 +275,30 @@ impl AppState {
248275
/// Go to top of current list
249276
pub fn go_top(&mut self) {
250277
match self.focused_pane {
251-
FocusedPane::Navigation => self.selected_file_index = 0,
278+
FocusedPane::Navigation => match self.nav_mode {
279+
NavMode::Files | NavMode::SearchResults => self.selected_file_index = 0,
280+
NavMode::Labels => self.selected_label_index = 0,
281+
},
252282
FocusedPane::Detail => self.selected_task_index = 0,
253283
}
254284
}
255285

256286
/// Go to bottom of current list
257287
pub fn go_bottom(&mut self) {
258288
match self.focused_pane {
259-
FocusedPane::Navigation => {
260-
let max_index = self.visible_tree_node_count();
261-
if max_index > 0 {
262-
self.selected_file_index = max_index - 1;
289+
FocusedPane::Navigation => match self.nav_mode {
290+
NavMode::Files | NavMode::SearchResults => {
291+
let max_index = self.visible_tree_node_count();
292+
if max_index > 0 {
293+
self.selected_file_index = max_index - 1;
294+
}
263295
}
264-
}
296+
NavMode::Labels => {
297+
if !self.labels.is_empty() {
298+
self.selected_label_index = self.labels.len() - 1;
299+
}
300+
}
301+
},
265302
FocusedPane::Detail => {
266303
if !self.tasks.is_empty() {
267304
self.selected_task_index = self.tasks.len() - 1;
@@ -486,6 +523,38 @@ impl AppState {
486523
self.tasks.get(self.selected_task_index)
487524
}
488525

526+
/// Get currently selected label
527+
#[must_use]
528+
pub fn selected_label(&self) -> Option<&LabelStats> {
529+
self.labels.get(self.selected_label_index)
530+
}
531+
532+
/// Switch to labels view mode
533+
pub fn switch_to_labels(&mut self) {
534+
self.nav_mode = NavMode::Labels;
535+
self.selected_label_index = 0;
536+
}
537+
538+
/// Switch to files view mode
539+
pub fn switch_to_files(&mut self) {
540+
self.nav_mode = NavMode::Files;
541+
self.current_label_filter = None;
542+
}
543+
544+
/// Toggle between files and labels view
545+
pub fn toggle_nav_mode(&mut self) {
546+
match self.nav_mode {
547+
NavMode::Files => self.switch_to_labels(),
548+
NavMode::Labels | NavMode::SearchResults => self.switch_to_files(),
549+
}
550+
}
551+
552+
/// Clear label filter and return to file view
553+
pub fn clear_label_filter(&mut self) {
554+
self.current_label_filter = None;
555+
self.nav_mode = NavMode::Files;
556+
}
557+
489558
/// Build file tree from flat file list
490559
///
491560
/// Converts the flat `self.files` list into a hierarchical directory tree.

‎crates/lash-tui/src/ui/detail_pane.rs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ pub fn render(frame: &mut Frame, area: Rect, state: &AppState) {
2323
themes::unfocused_border_style(theme)
2424
};
2525

26-
// Get title from selected file
27-
let title = state
28-
.selected_file()
29-
.map_or_else(|| " Tasks ".to_string(), |f| format!(" {} ", f.title));
26+
// Get title - show label filter if active, otherwise selected file
27+
let title = if let Some(label) = &state.current_label_filter {
28+
format!(" Tasks [#{label}] ")
29+
} else {
30+
state
31+
.selected_file()
32+
.map_or_else(|| " Tasks ".to_string(), |f| format!(" {} ", f.title))
33+
};
3034

3135
let block = Block::default()
3236
.borders(Borders::ALL)

‎crates/lash-tui/src/ui/help.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ pub fn render(frame: &mut Frame, area: Rect, _state: &AppState) {
5656
Line::from(" Space Toggle task status"),
5757
Line::from(" e Open file in $EDITOR"),
5858
Line::from(" / Search"),
59-
Line::from(" c Clear filters"),
59+
Line::from(" F Toggle Files/Labels view"),
60+
Line::from(" c Clear label filter"),
6061
Line::from(" Ctrl-g Show dependency graph"),
6162
Line::from(""),
6263
Line::from(vec![Span::styled(

0 commit comments

Comments
 (0)