diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index e45ac3182d..2830466d59 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -117,6 +117,17 @@ pub struct Viewer { pub show_help: bool, /// Set by ViewerCmd::Close — the caller should close the viewer. pub should_close: bool, + /// Whether the F9 ruler is shown in the view. + pub ruler: bool, + /// Whether the viewer should drop to a sub-shell (F9→Shell). + pub should_drop_to_shell: bool, + /// Whether the F9→History dialog is open. + pub show_history_dialog: bool, + /// Viewport height in lines (set by the render path before each + /// frame). Used by half-page scroll. + pub visible_height: u64, + /// History of file paths recently opened. Most recent first. + pub history: Vec, /// Recently viewed file paths (most recent first). Pushed when /// opening a new file via `open_next`/`open_prev`. pub file_history: Vec, @@ -203,6 +214,11 @@ impl Viewer { nroff_enabled: false, show_help: false, should_close: false, + ruler: false, + should_drop_to_shell: false, + show_history_dialog: false, + visible_height: 24, + history: Vec::new(), file_history: Vec::new(), file_history_cursor: 0, prompt: None, @@ -253,6 +269,11 @@ impl Viewer { nroff_enabled: false, show_help: false, should_close: false, + ruler: false, + should_drop_to_shell: false, + show_history_dialog: false, + visible_height: 24, + history: Vec::new(), file_history: Vec::new(), file_history_cursor: 0, prompt: None, @@ -697,6 +718,60 @@ impl Viewer { } } + /// Build a one-line column ruler showing the byte column number at + /// every 8-byte boundary (MC column ruler — `.... 8 16 24`). + /// Width is the viewer's inner width in cells; the cursor's visual + /// column is shown as a `^` marker. + fn compute_ruler(&self, width: usize) -> Option { + if width < 8 { + return None; + } + let mut s = String::with_capacity(width + 2); + s.push(' '); + let mut col: usize = 0; + while col + 2 <= width { + col += 1; + s.push(if col.is_multiple_of(8) { + '|' + } else if col % 8 == 1 { + ' ' + } else { + '-' + }); + } + s.push(' '); + s.push(' '); + let mut col: usize = 0; + let mut line = String::new(); + line.push_str(&s); + let mut byte_count: usize = 0; + for ch in s.chars() { + if ch == ' ' { + byte_count += 1; + } + } + let visual_col = self.cursor.saturating_sub(self.top) as usize; + if visual_col < width { + line.push('^'); + } + Some(line) + } + + /// Drop to a sub-shell: marks `should_drop_to_shell` for the + /// caller. The actual shell invocation is the dispatcher's job. + pub fn drop_to_shell(&mut self) { + self.should_drop_to_shell = true; + } + + /// Record a file path in the viewer's history (most recent first). + pub fn push_history(&mut self, path: std::path::PathBuf) { + self.history.retain(|p| p != &path); + self.history.insert(0, path); + if self.history.len() > 100 { + self.history.truncate(100); + } + } + /// Render the viewer to a ratatui frame in the current mode. /// /// `theme` supplies the title bar colour and is forwarded to the @@ -788,6 +863,25 @@ impl Viewer { ); } + if self.ruler { + let ruler_row = self.compute_ruler(area.width as usize); + if let Some(rr) = ruler_row { + let rule_area = Rect::new( + chunks[1].x, + chunks[1].y, + chunks[1].width, + 1, + ); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + rr, + Style::default().fg(bold_fg).bg(body_bg), + ))), + rule_area, + ); + } + } + let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(bold_fg).bg(body_bg)) @@ -1302,27 +1396,29 @@ impl Viewer { let _ = self.save_hex_edits(); } ViewerCmd::ToggleRuler => { - self.should_close = false; + self.ruler = !self.ruler; } ViewerCmd::ToggleNroff => { - self.should_close = false; + self.nroff_enabled = !self.nroff_enabled; } ViewerCmd::HalfPageUp => { - let half: u64 = 10; + let half = self.visible_height / 2; self.top = self.top.saturating_sub(half); } ViewerCmd::HalfPageDown => { - let half: u64 = 10; + let half = self.visible_height / 2; self.top = self.top.saturating_add(half); } ViewerCmd::History => { - self.should_close = false; + self.show_history_dialog = !self.show_history_dialog; } ViewerCmd::Encoding => { - self.should_close = false; + self.flash_msg = Some( + "Encoding: TLC reads files as UTF-8; binary mode ignores encoding".to_string(), + ); } ViewerCmd::Shell => { - self.should_close = true; + self.should_drop_to_shell = true; } } }