feat: Add complete keyboard navigation system

Implemented a full keyboard navigation system with directional commands (C-h/j/k/l) for moving between views,
vim-style navigation (j/k for rows, h/l for columns), and TAB cycling.
Added table header navigation mode where users can move between columns and change sort order with keyboard shortcuts.
The navigation follows visual position: left sidebar, table, and filter bar can all be reached with directional keys.

Created a keymap system with context-aware bindings that resolve commands based on
current focus (Table, TableHeaders, FilterBar, Sidebar).
Commands are dispatched through a centralized handler that manages focus transitions and view state.
Added helper methods to cycle through column headers with proper wrapping.

Auto-select first task on load when tasks exist, and set initial window focus so keyboard shortcuts
work immediately without requiring a mouse click.
This commit is contained in:
Ignacio Perez
2025-12-28 20:48:19 -03:00
parent 785e8ee932
commit 893e382d7f
18 changed files with 2445 additions and 87 deletions
+15
View File
@@ -206,6 +206,21 @@ impl FilterState {
self.due_filter = DueFilter::default();
}
pub fn clear_project(&mut self) {
self.selected_project = None;
}
pub fn clear_tags(&mut self) {
self.active_tags.clear();
}
pub fn clear_search_and_dropdowns(&mut self) {
self.search_text.clear();
self.status_filter = StatusFilter::default();
self.priority_filter = PriorityFilter::default();
self.due_filter = DueFilter::default();
}
pub fn has_active_filters(&self) -> bool {
self.selected_project.is_some()
|| !self.active_tags.is_empty()
+15
View File
@@ -152,6 +152,21 @@ impl ProjectTree {
}
}
pub fn get_expanded_paths(&self) -> HashMap<String, bool> {
self.expanded_paths.clone()
}
pub fn restore_expanded_paths(&mut self, paths: HashMap<String, bool>) {
for (path, is_expanded) in &paths {
if *is_expanded {
if let Some(&idx) = self.path_to_index.get(path) {
self.nodes[idx].is_expanded = true;
}
}
}
self.expanded_paths = paths;
}
pub fn expand_path(&mut self, full_path: &str) {
let segments: Vec<&str> = full_path.split('.').collect();
let mut current_path = String::new();