Implement vim-style navigation and visual mode
All checks were successful
Build and Release / build-and-release (push) Successful in 51s

- Add vim-style h key: closes folders and jumps to parent
- Implement stop() method in player for proper playback stopping
- Add space key to restart playback when stopped
- Add playlist color coding: green (playing), blue (paused), yellow (stopped)
- Fix n/p keys to preserve player state when switching tracks
- Implement vim-style visual mode with v key for multi-file selection
- Visual mode exits automatically on play/add actions
- Remove unused play_previous method
- Bump version to 0.1.7
This commit is contained in:
2025-12-06 17:35:11 +01:00
parent 006aeb0c90
commit f9534bacf3
5 changed files with 178 additions and 42 deletions

View File

@@ -33,6 +33,8 @@ pub struct AppState {
pub search_match_index: usize,
pub tab_search_results: Vec<PathBuf>,
pub tab_search_index: usize,
pub visual_mode: bool,
pub visual_anchor: usize,
}
#[derive(Debug, Clone)]
@@ -71,6 +73,8 @@ impl AppState {
search_match_index: 0,
tab_search_results: Vec::new(),
tab_search_index: 0,
visual_mode: false,
visual_anchor: 0,
}
}
@@ -81,12 +85,20 @@ impl AppState {
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
}
// Update visual selection if in visual mode
if self.visual_mode {
self.update_visual_selection();
}
}
}
pub fn move_selection_down(&mut self) {
if self.selected_index < self.flattened_items.len().saturating_sub(1) {
self.selected_index += 1;
// Update visual selection if in visual mode
if self.visual_mode {
self.update_visual_selection();
}
}
}
@@ -120,11 +132,39 @@ impl AppState {
}
pub fn collapse_selected(&mut self) {
if let Some(item) = self.get_selected_item() {
let item = self.get_selected_item().cloned();
if let Some(item) = item {
if item.node.is_dir {
let path = item.node.path.clone();
self.expanded_dirs.remove(&path);
self.rebuild_flattened_items();
let was_expanded = self.expanded_dirs.contains(&path);
if was_expanded {
// Close the expanded folder
self.expanded_dirs.remove(&path);
self.rebuild_flattened_items();
} else {
// Folder is collapsed, close parent instead and jump to it
if let Some(parent) = path.parent() {
let parent_buf = parent.to_path_buf();
self.expanded_dirs.remove(&parent_buf);
self.rebuild_flattened_items();
// Jump to parent folder
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.node.path == parent_buf) {
self.selected_index = parent_idx;
}
}
}
} else {
// Close parent folder when on a file and jump to it
if let Some(parent) = item.node.path.parent() {
let parent_buf = parent.to_path_buf();
self.expanded_dirs.remove(&parent_buf);
self.rebuild_flattened_items();
// Jump to parent folder
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.node.path == parent_buf) {
self.selected_index = parent_idx;
}
}
}
}
}
@@ -140,13 +180,37 @@ impl AppState {
}
pub fn toggle_mark(&mut self) {
if let Some(item) = self.get_selected_item() {
if !item.node.is_dir {
let path = item.node.path.clone();
if self.marked_files.contains(&path) {
self.marked_files.remove(&path);
} else {
self.marked_files.insert(path);
if self.visual_mode {
// Exit visual mode and mark all files in range
self.update_visual_selection();
self.visual_mode = false;
} else {
// Enter visual mode
self.visual_mode = true;
self.visual_anchor = self.selected_index;
// Clear previous marks when entering visual mode
self.marked_files.clear();
// Mark current file
if let Some(item) = self.get_selected_item() {
if !item.node.is_dir {
self.marked_files.insert(item.node.path.clone());
}
}
}
}
fn update_visual_selection(&mut self) {
// Clear marks
self.marked_files.clear();
// Mark all files between anchor and current position
let start = self.visual_anchor.min(self.selected_index);
let end = self.visual_anchor.max(self.selected_index);
for i in start..=end {
if let Some(item) = self.flattened_items.get(i) {
if !item.node.is_dir {
self.marked_files.insert(item.node.path.clone());
}
}
}
@@ -229,14 +293,6 @@ impl AppState {
}
}
pub fn play_previous(&mut self) {
if self.playlist_index > 0 {
self.playlist_index -= 1;
self.current_file = Some(self.playlist[self.playlist_index].clone());
self.player_state = PlayerState::Playing;
}
}
pub fn refresh_flattened_items(&mut self) {
// Keep current expanded state after rescan
self.rebuild_flattened_items();