Add vim bindings and directory expand/collapse

- Replace arrow keys with j/k for navigation
- Add h/l for collapse/expand directories
- Remove emoji icons, use clean text markers
- Show directories with [-]/[+] expand markers
- Track expanded state per directory path
- Add directory suffix (/) for clarity
- Update help text with vim bindings
This commit is contained in:
2025-12-06 12:39:11 +01:00
parent 7ce264fd96
commit 8104f54887
3 changed files with 86 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
use crate::cache::{Cache, FileTreeNode};
use crate::config::Config;
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -21,6 +22,7 @@ pub struct AppState {
pub volume: i64,
pub should_quit: bool,
pub flattened_items: Vec<FlattenedItem>,
pub expanded_dirs: HashSet<PathBuf>,
}
#[derive(Debug, Clone)]
@@ -32,7 +34,11 @@ pub struct FlattenedItem {
impl AppState {
pub fn new(cache: Cache, config: Config) -> Self {
let flattened_items = flatten_tree(&cache.file_tree, 0);
let mut expanded_dirs = HashSet::new();
// Start with all directories expanded
collect_all_dirs(&cache.file_tree, &mut expanded_dirs);
let flattened_items = flatten_tree(&cache.file_tree, 0, &expanded_dirs);
Self {
cache,
@@ -46,6 +52,7 @@ impl AppState {
volume: 100,
should_quit: false,
flattened_items,
expanded_dirs,
}
}
@@ -65,28 +72,79 @@ impl AppState {
self.flattened_items.get(self.selected_index)
}
pub fn toggle_expand(&mut self) {
if let Some(item) = self.get_selected_item() {
if item.node.is_dir {
let path = item.node.path.clone();
if self.expanded_dirs.contains(&path) {
self.expanded_dirs.remove(&path);
} else {
self.expanded_dirs.insert(path);
}
self.rebuild_flattened_items();
}
}
}
pub fn collapse_selected(&mut self) {
if let Some(item) = self.get_selected_item() {
if item.node.is_dir {
let path = item.node.path.clone();
self.expanded_dirs.remove(&path);
self.rebuild_flattened_items();
}
}
}
pub fn expand_selected(&mut self) {
if let Some(item) = self.get_selected_item() {
if item.node.is_dir {
let path = item.node.path.clone();
self.expanded_dirs.insert(path);
self.rebuild_flattened_items();
}
}
}
pub fn refresh_flattened_items(&mut self) {
self.flattened_items = flatten_tree(&self.cache.file_tree, 0);
self.expanded_dirs.clear();
collect_all_dirs(&self.cache.file_tree, &mut self.expanded_dirs);
self.rebuild_flattened_items();
}
fn rebuild_flattened_items(&mut self) {
self.flattened_items = flatten_tree(&self.cache.file_tree, 0, &self.expanded_dirs);
if self.selected_index >= self.flattened_items.len() {
self.selected_index = self.flattened_items.len().saturating_sub(1);
}
}
}
fn flatten_tree(nodes: &[FileTreeNode], depth: usize) -> Vec<FlattenedItem> {
fn flatten_tree(nodes: &[FileTreeNode], depth: usize, expanded_dirs: &HashSet<PathBuf>) -> Vec<FlattenedItem> {
let mut result = Vec::new();
for node in nodes {
let is_expanded = expanded_dirs.contains(&node.path);
result.push(FlattenedItem {
node: node.clone(),
depth,
is_expanded: true, // For now, all directories are expanded
is_expanded,
});
if node.is_dir && !node.children.is_empty() {
result.extend(flatten_tree(&node.children, depth + 1));
if node.is_dir && !node.children.is_empty() && is_expanded {
result.extend(flatten_tree(&node.children, depth + 1, expanded_dirs));
}
}
result
}
fn collect_all_dirs(nodes: &[FileTreeNode], dirs: &mut HashSet<PathBuf>) {
for node in nodes {
if node.is_dir {
dirs.insert(node.path.clone());
collect_all_dirs(&node.children, dirs);
}
}
}