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:
parent
7ce264fd96
commit
8104f54887
10
src/main.rs
10
src/main.rs
@ -101,12 +101,18 @@ async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()>
|
|||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
state.should_quit = true;
|
state.should_quit = true;
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Char('k') => {
|
||||||
state.move_selection_up();
|
state.move_selection_up();
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Char('j') => {
|
||||||
state.move_selection_down();
|
state.move_selection_down();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('h') => {
|
||||||
|
state.collapse_selected();
|
||||||
|
}
|
||||||
|
KeyCode::Char('l') => {
|
||||||
|
state.expand_selected();
|
||||||
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if let Some(item) = state.get_selected_item() {
|
if let Some(item) = state.get_selected_item() {
|
||||||
if !item.node.is_dir {
|
if !item.node.is_dir {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::cache::{Cache, FileTreeNode};
|
use crate::cache::{Cache, FileTreeNode};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -21,6 +22,7 @@ pub struct AppState {
|
|||||||
pub volume: i64,
|
pub volume: i64,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
pub flattened_items: Vec<FlattenedItem>,
|
pub flattened_items: Vec<FlattenedItem>,
|
||||||
|
pub expanded_dirs: HashSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -32,7 +34,11 @@ pub struct FlattenedItem {
|
|||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(cache: Cache, config: Config) -> Self {
|
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 {
|
Self {
|
||||||
cache,
|
cache,
|
||||||
@ -46,6 +52,7 @@ impl AppState {
|
|||||||
volume: 100,
|
volume: 100,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
flattened_items,
|
flattened_items,
|
||||||
|
expanded_dirs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,28 +72,79 @@ impl AppState {
|
|||||||
self.flattened_items.get(self.selected_index)
|
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) {
|
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() {
|
if self.selected_index >= self.flattened_items.len() {
|
||||||
self.selected_index = self.flattened_items.len().saturating_sub(1);
|
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();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
|
let is_expanded = expanded_dirs.contains(&node.path);
|
||||||
|
|
||||||
result.push(FlattenedItem {
|
result.push(FlattenedItem {
|
||||||
node: node.clone(),
|
node: node.clone(),
|
||||||
depth,
|
depth,
|
||||||
is_expanded: true, // For now, all directories are expanded
|
is_expanded,
|
||||||
});
|
});
|
||||||
|
|
||||||
if node.is_dir && !node.children.is_empty() {
|
if node.is_dir && !node.children.is_empty() && is_expanded {
|
||||||
result.extend(flatten_tree(&node.children, depth + 1));
|
result.extend(flatten_tree(&node.children, depth + 1, expanded_dirs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,8 +24,13 @@ fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, item)| {
|
.map(|(idx, item)| {
|
||||||
let indent = " ".repeat(item.depth);
|
let indent = " ".repeat(item.depth);
|
||||||
let prefix = if item.node.is_dir { "📁 " } else if item.node.metadata.as_ref().map(|m| m.is_video).unwrap_or(false) { "🎥 " } else { "🎵 " };
|
let expand_marker = if item.node.is_dir {
|
||||||
let text = format!("{}{}{}", indent, prefix, item.node.name);
|
if item.is_expanded { "[-] " } else { "[+] " }
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
let suffix = if item.node.is_dir { "/" } else { "" };
|
||||||
|
let text = format!("{}{}{}{}", indent, expand_marker, item.node.name, suffix);
|
||||||
|
|
||||||
let style = if idx == state.selected_index {
|
let style = if idx == state.selected_index {
|
||||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||||
@ -114,12 +119,16 @@ fn render_status_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
|||||||
let help_text = vec![
|
let help_text = vec![
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("↑/↓", Style::default().fg(Color::Cyan)),
|
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
||||||
Span::raw(" Navigate"),
|
Span::raw(" Navigate down/up"),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled("h/l", Style::default().fg(Color::Cyan)),
|
||||||
|
Span::raw(" Collapse/expand dir"),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
||||||
Span::raw(" Play"),
|
Span::raw(" Play file"),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Space", Style::default().fg(Color::Cyan)),
|
Span::styled("Space", Style::default().fg(Color::Cyan)),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user