@@ -103,6 +103,25 @@ pub struct ThemeSelectorState {
103103 pub scheme_names : Vec < String > ,
104104}
105105
106+ /// Information about a selected tree node
107+ #[ derive( Debug , Clone ) ]
108+ pub struct SelectedTreeNode {
109+ /// Whether this is a directory node
110+ pub is_directory : bool ,
111+
112+ /// Whether the node is currently expanded
113+ pub is_expanded : bool ,
114+
115+ /// Whether the node has children
116+ pub has_children : bool ,
117+
118+ /// The file record if this is a file node
119+ pub file_record : Option < FileRecord > ,
120+
121+ /// Path to this node in the tree (indices at each level)
122+ pub path : Vec < usize > ,
123+ }
124+
106125impl AppState {
107126 /// Create new application state with default theme
108127 ///
@@ -213,7 +232,8 @@ impl AppState {
213232 pub fn move_down ( & mut self ) {
214233 match self . focused_pane {
215234 FocusedPane :: Navigation => {
216- if self . selected_file_index + 1 < self . files . len ( ) {
235+ let max_index = self . visible_tree_node_count ( ) ;
236+ if self . selected_file_index + 1 < max_index {
217237 self . selected_file_index += 1 ;
218238 }
219239 }
@@ -237,8 +257,9 @@ impl AppState {
237257 pub fn go_bottom ( & mut self ) {
238258 match self . focused_pane {
239259 FocusedPane :: Navigation => {
240- if !self . files . is_empty ( ) {
241- self . selected_file_index = self . files . len ( ) - 1 ;
260+ let max_index = self . visible_tree_node_count ( ) ;
261+ if max_index > 0 {
262+ self . selected_file_index = max_index - 1 ;
242263 }
243264 }
244265 FocusedPane :: Detail => {
@@ -335,6 +356,130 @@ impl AppState {
335356 self . files . get ( self . selected_file_index )
336357 }
337358
359+ /// Get the selected node from the file tree view
360+ ///
361+ /// Returns the node at the current `selected_file_index` position in the
362+ /// flattened visible tree. This accounts for expand/collapse state.
363+ #[ must_use]
364+ pub fn selected_tree_node ( & self ) -> Option < SelectedTreeNode > {
365+ let trees = self . file_tree . as_ref ( ) ?;
366+
367+ let mut current_index = 0 ;
368+ for ( root_idx, tree) in trees. iter ( ) . enumerate ( ) {
369+ if let Some ( result) = Self :: find_node_at_index (
370+ tree,
371+ self . selected_file_index ,
372+ & mut current_index,
373+ & [ root_idx] ,
374+ ) {
375+ return Some ( result) ;
376+ }
377+ }
378+ None
379+ }
380+
381+ /// Recursively find the node at a given visual index
382+ fn find_node_at_index (
383+ node : & TreeNode < DirectoryNode > ,
384+ target_index : usize ,
385+ current_index : & mut usize ,
386+ path : & [ usize ] ,
387+ ) -> Option < SelectedTreeNode > {
388+ if * current_index == target_index {
389+ return Some ( SelectedTreeNode {
390+ is_directory : node. data . is_directory ,
391+ is_expanded : node. expanded ,
392+ has_children : node. has_children ( ) ,
393+ file_record : node. data . file_record . clone ( ) ,
394+ path : path. to_vec ( ) ,
395+ } ) ;
396+ }
397+
398+ * current_index += 1 ;
399+
400+ if node. expanded {
401+ for ( child_idx, child) in node. children . iter ( ) . enumerate ( ) {
402+ let mut child_path = path. to_vec ( ) ;
403+ child_path. push ( child_idx) ;
404+ if let Some ( result) =
405+ Self :: find_node_at_index ( child, target_index, current_index, & child_path)
406+ {
407+ return Some ( result) ;
408+ }
409+ }
410+ }
411+
412+ None
413+ }
414+
415+ /// Toggle expand/collapse on the currently selected tree node
416+ ///
417+ /// Returns `true` if a node was toggled, `false` if nothing was done.
418+ pub fn toggle_selected_node ( & mut self ) -> bool {
419+ let Some ( selected) = self . selected_tree_node ( ) else {
420+ return false ;
421+ } ;
422+
423+ if !selected. is_directory || !selected. has_children {
424+ return false ;
425+ }
426+
427+ let Some ( trees) = & mut self . file_tree else {
428+ return false ;
429+ } ;
430+
431+ // Navigate to the node using the path
432+ if selected. path . is_empty ( ) {
433+ return false ;
434+ }
435+
436+ let root_idx = selected. path [ 0 ] ;
437+ if root_idx >= trees. len ( ) {
438+ return false ;
439+ }
440+
441+ let mut node = & mut trees[ root_idx] ;
442+
443+ for & child_idx in & selected. path [ 1 ..] {
444+ if child_idx >= node. children . len ( ) {
445+ return false ;
446+ }
447+ node = & mut node. children [ child_idx] ;
448+ }
449+
450+ // Toggle
451+ if node. expanded {
452+ node. collapse ( ) ;
453+ } else {
454+ node. expand ( ) ;
455+ }
456+
457+ true
458+ }
459+
460+ /// Count total visible nodes in file tree
461+ ///
462+ /// Used for bounds checking in `move_up`/`move_down` when tree view is active.
463+ #[ must_use]
464+ pub fn visible_tree_node_count ( & self ) -> usize {
465+ let Some ( trees) = & self . file_tree else {
466+ return self . files . len ( ) ;
467+ } ;
468+
469+ trees. iter ( ) . map ( Self :: count_visible_nodes) . sum ( )
470+ }
471+
472+ /// Recursively count visible nodes in a tree
473+ fn count_visible_nodes ( node : & TreeNode < DirectoryNode > ) -> usize {
474+ let mut count = 1 ; // This node
475+ if node. expanded {
476+ for child in & node. children {
477+ count += Self :: count_visible_nodes ( child) ;
478+ }
479+ }
480+ count
481+ }
482+
338483 /// Get currently selected task
339484 #[ must_use]
340485 pub fn selected_task ( & self ) -> Option < & TaskRecord > {
0 commit comments