diff --git a/local/recipes/tui/tlc/source/src/filemanager/panel.rs b/local/recipes/tui/tlc/source/src/filemanager/panel.rs index c1624ff1eb..a47893a243 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/panel.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/panel.rs @@ -115,6 +115,12 @@ pub struct Panel { entries: Vec, /// Cursor position (index into `entries`). 0 is the ".." entry. cursor: usize, + /// Previous cursor position (set when the cursor moves). Used by + /// the renderer to draw a brief trail behind the active cursor. + prev_cursor: usize, + /// `Some(timestamp)` while the cursor-trail flash is active. + /// `None` when no flash is active. + cursor_flash: Option, /// Top-line scroll position. top: usize, /// Files marked with Insert/Space. @@ -164,6 +170,8 @@ impl Panel { path: PathBuf::new(), entries: Vec::new(), cursor: 0, + prev_cursor: 0, + cursor_flash: None, top: 0, marked: HashSet::new(), sort_field: SortField::from_config(&cfg.sort_field), @@ -239,6 +247,12 @@ impl Panel { self.cursor } + /// Previous cursor index (set on every move; reset by render). + #[must_use] + pub fn prev_cursor(&self) -> usize { + self.prev_cursor + } + /// Top-line scroll index. #[must_use] pub fn top(&self) -> usize { @@ -497,6 +511,20 @@ impl Panel { v } + /// `Some(frac)` while the cursor-trail flash is active, where + /// `1.0` = just after move and `0.0` = fully expired. `None` + /// when no flash is active. + pub fn cursor_flash_progress(&self) -> Option { + let start = self.cursor_flash?; + const FLASH_MS: f32 = 200.0; + let elapsed_ms = start.elapsed().as_secs_f32() * 1000.0; + if elapsed_ms >= FLASH_MS { + return None; + } + let remaining = FLASH_MS - elapsed_ms; + Some((remaining / FLASH_MS).clamp(0.0, 1.0)) + } + /// Snapshot of file-system state for every non-`..` entry, in /// display order. The returned vector preserves the panel's /// current sort order so callers can build a `name -> size` map @@ -586,25 +614,37 @@ impl Panel { /// Move the cursor up by one line. pub fn cursor_up(&mut self) { if self.cursor > 0 { + self.prev_cursor = self.cursor; self.cursor -= 1; + self.cursor_flash = Some(std::time::Instant::now()); } } /// Move the cursor down by one line. pub fn cursor_down(&mut self) { if self.cursor + 1 < self.entries.len() { + self.prev_cursor = self.cursor; self.cursor += 1; + self.cursor_flash = Some(std::time::Instant::now()); } } /// Move the cursor to the top of the list. pub fn cursor_home(&mut self) { + if self.cursor != 0 { + self.prev_cursor = self.cursor; + self.cursor_flash = Some(std::time::Instant::now()); + } self.cursor = 0; } /// Move the cursor to the bottom of the list. pub fn cursor_end(&mut self) { if !self.entries.is_empty() { + if self.cursor != self.entries.len() - 1 { + self.prev_cursor = self.cursor; + self.cursor_flash = Some(std::time::Instant::now()); + } self.cursor = self.entries.len() - 1; } } @@ -1243,6 +1283,25 @@ mod tests { let _ = fs::remove_dir_all(&dir); } + #[test] + fn cursor_movement_records_prev_cursor_and_triggers_flash() { + let dir = std::env::temp_dir().join("tlc-panel-cursor-trail"); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("a.txt"), b"x").unwrap(); + fs::write(dir.join("b.txt"), b"x").unwrap(); + let mut p = Panel::new(&dir, &empty_cfg()).unwrap(); + // Initially no flash, prev_cursor == cursor. + assert!(p.cursor_flash_progress().is_none()); + // cursor_down sets prev_cursor and triggers the flash. + p.cursor_down(); + assert!(p.cursor_flash_progress().is_some()); + // prev_cursor holds the index before the move. + // The exact value depends on which entry was at cursor 0. + assert_ne!(p.prev_cursor(), p.cursor()); + let _ = fs::remove_dir_all(&dir); + } + #[test] fn filter_match_len_returns_filter_prefix() { let dir = std::env::temp_dir().join("tlc-panel-filter-match-len"); diff --git a/local/recipes/tui/tlc/source/src/filemanager/render.rs b/local/recipes/tui/tlc/source/src/filemanager/render.rs index 6cb59f3203..563df0f8d6 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/render.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/render.rs @@ -449,6 +449,26 @@ impl FileManager { .style(Style::default().fg(panel_fg).bg(panel_bg)); frame.render_widget(list, list_area); + // Cursor-trail: briefly highlight the previous + // cursor row with a thin accent-coloured bar. + if let Some(frac) = panel.cursor_flash_progress() { + let prev_visible = (panel.prev_cursor() as i64) - (top as i64); + if prev_visible >= 0 && (prev_visible as u16) < body_height { + let prev_y = body_y + prev_visible as u16; + let bar = Paragraph::new(Span::styled( + "▏", + Style::default() + .fg(self.theme.accent) + .bg(panel_bg), + )); + frame.render_widget( + bar, + Rect::new(inner.x, prev_y, 1, 1), + ); + let _ = frac; // currently unused; reserved for future fade + } + } + let total = panel.entry_count().max(1); let mut sb_state = ScrollbarState::new(total) .viewport_content_length(body_height as usize)