Skip to content

Commit 5883c41

Browse files
committed
feat: add vim-style z commands for view positioning
- zz: center current line on screen - zt: move current line to top of viewport - zb: move current line to bottom of viewport Add ZPending input mode for two-key command sequence.
1 parent 07ac194 commit 5883c41

File tree

7 files changed

+70
-3
lines changed

7 files changed

+70
-3
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ A fast, universal terminal-based log viewer with live filtering and follow mode.
3131
- `g` - Jump to start (first line)
3232
- `G` - Jump to end (last line)
3333
- `:123` - Jump to line 123 (vim-style)
34+
- `zz` - Center selection on screen
35+
- `zt` - Move selection to top of screen
36+
- `zb` - Move selection to bottom of screen
3437
- `f` - Toggle follow mode (auto-scroll to new logs)
3538
- Mouse wheel - Scroll up/down (selection follows scroll)
3639

src/app.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub enum InputMode {
2323
Normal,
2424
EnteringFilter,
2525
EnteringLineJump,
26+
/// Waiting for second key after 'z' (for zz, zt, zb commands)
27+
ZPending,
2628
}
2729

2830
/// Main application state
@@ -463,6 +465,23 @@ impl App {
463465
AppEvent::HistoryUp => self.history_up(),
464466
AppEvent::HistoryDown => self.history_down(),
465467

468+
// View positioning (vim z commands)
469+
AppEvent::EnterZMode => {
470+
self.input_mode = InputMode::ZPending;
471+
}
472+
AppEvent::ExitZMode => {
473+
self.input_mode = InputMode::Normal;
474+
}
475+
AppEvent::CenterView => {
476+
self.active_tab_mut().center_view();
477+
}
478+
AppEvent::ViewToTop => {
479+
self.active_tab_mut().view_to_top();
480+
}
481+
AppEvent::ViewToBottom => {
482+
self.active_tab_mut().view_to_bottom();
483+
}
484+
466485
// Future events - not yet implemented
467486
AppEvent::StartFilter { .. } => {
468487
// Will be handled in main loop to trigger background filter

src/event.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ pub enum AppEvent {
6464
HistoryUp,
6565
HistoryDown,
6666

67+
// View positioning (vim z commands)
68+
CenterView, // zz
69+
ViewToTop, // zt
70+
ViewToBottom, // zb
71+
EnterZMode, // z pressed, waiting for second key
72+
ExitZMode, // cancel z mode
73+
6774
// System events
6875
Quit,
6976
}

src/handlers/input.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub fn handle_input_event(key: KeyEvent, app: &App) -> Vec<AppEvent> {
1313
match app.input_mode {
1414
InputMode::EnteringFilter => handle_filter_input_mode(key),
1515
InputMode::EnteringLineJump => handle_line_jump_input_mode(key),
16+
InputMode::ZPending => handle_z_pending_mode(key),
1617
InputMode::Normal => handle_normal_mode(key),
1718
}
1819
}
@@ -53,6 +54,18 @@ fn handle_line_jump_input_mode(key: KeyEvent) -> Vec<AppEvent> {
5354
}
5455
}
5556

57+
/// Handle keyboard input in z pending mode (waiting for zz, zt, zb)
58+
fn handle_z_pending_mode(key: KeyEvent) -> Vec<AppEvent> {
59+
match key.code {
60+
KeyCode::Char('z') => vec![AppEvent::CenterView, AppEvent::ExitZMode],
61+
KeyCode::Char('t') => vec![AppEvent::ViewToTop, AppEvent::ExitZMode],
62+
KeyCode::Char('b') => vec![AppEvent::ViewToBottom, AppEvent::ExitZMode],
63+
KeyCode::Esc => vec![AppEvent::ExitZMode],
64+
// Any other key cancels z mode
65+
_ => vec![AppEvent::ExitZMode],
66+
}
67+
}
68+
5669
/// Handle keyboard input in normal navigation mode
5770
fn handle_normal_mode(key: KeyEvent) -> Vec<AppEvent> {
5871
match key.code {
@@ -78,6 +91,7 @@ fn handle_normal_mode(key: KeyEvent) -> Vec<AppEvent> {
7891
KeyCode::Char('/') => vec![AppEvent::StartFilterInput],
7992
KeyCode::Char(':') => vec![AppEvent::StartLineJumpInput],
8093
KeyCode::Char('?') => vec![AppEvent::ShowHelp],
94+
KeyCode::Char('z') => vec![AppEvent::EnterZMode],
8195
KeyCode::Esc => vec![AppEvent::ClearFilter],
8296
// Tab navigation
8397
KeyCode::Tab => vec![AppEvent::NextTab],

src/tab.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,24 @@ impl TabState {
316316
self.viewport.jump_to_start(&self.line_indices);
317317
self.sync_from_viewport();
318318
}
319+
320+
/// Center the current selection on screen (zz)
321+
pub fn center_view(&mut self) {
322+
self.viewport.center(&self.line_indices);
323+
self.sync_from_viewport();
324+
}
325+
326+
/// Move current selection to top of viewport (zt)
327+
pub fn view_to_top(&mut self) {
328+
self.viewport.anchor_to_top(&self.line_indices);
329+
self.sync_from_viewport();
330+
}
331+
332+
/// Move current selection to bottom of viewport (zb)
333+
pub fn view_to_bottom(&mut self) {
334+
self.viewport.anchor_to_bottom(&self.line_indices);
335+
self.sync_from_viewport();
336+
}
319337
}
320338

321339
#[cfg(test)]

src/ui/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ fn render_help_overlay(f: &mut Frame, area: Rect) {
305305
Line::from(" g Jump to start"),
306306
Line::from(" G Jump to end"),
307307
Line::from(" :123 Jump to line 123"),
308+
Line::from(" zz Center selection on screen"),
309+
Line::from(" zt Move selection to top"),
310+
Line::from(" zb Move selection to bottom"),
308311
Line::from(""),
309312
Line::from(vec![Span::styled(
310313
"Tabs",

src/viewport.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
//! The anchor_line (file line number) is stable across filter changes.
66
77
/// Result of resolving the viewport against current content
8-
#[allow(dead_code)]
98
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109
pub struct ResolvedView {
1110
/// Index into line_indices for the selected line
@@ -15,7 +14,6 @@ pub struct ResolvedView {
1514
}
1615

1716
/// Viewport manages selection and scrolling with vim-like behavior
18-
#[allow(dead_code)]
1917
#[derive(Debug, Clone)]
2018
pub struct Viewport {
2119
/// The file line number that is selected (stable across filter changes)
@@ -34,7 +32,6 @@ pub struct Viewport {
3432
cache: Option<ResolvedView>,
3533
}
3634

37-
#[allow(dead_code)]
3835
impl Viewport {
3936
/// Create a new viewport anchored to the given line
4037
pub fn new(initial_line: usize) -> Self {
@@ -150,6 +147,7 @@ impl Viewport {
150147

151148
/// Move viewport by delta lines without moving selection
152149
/// (selection stays on same line, but moves on screen)
150+
#[allow(dead_code)] // Future: Ctrl+E/Ctrl+Y vim commands
153151
pub fn move_viewport(&mut self, delta: i32, line_indices: &[usize]) {
154152
if line_indices.is_empty() || self.height == 0 {
155153
return;
@@ -244,6 +242,7 @@ impl Viewport {
244242
}
245243

246244
/// Jump to a specific index in the current view
245+
#[allow(dead_code)] // Future: direct index jumping
247246
pub fn jump_to_index(&mut self, index: usize, line_indices: &[usize]) {
248247
if line_indices.is_empty() {
249248
return;
@@ -344,21 +343,25 @@ impl Viewport {
344343
}
345344

346345
/// Get the cached selected index (call resolve() first)
346+
#[allow(dead_code)] // Public API for future use
347347
pub fn selected_index(&self) -> usize {
348348
self.cache.map(|c| c.selected_index).unwrap_or(0)
349349
}
350350

351351
/// Get the cached scroll position (call resolve() first)
352+
#[allow(dead_code)] // Public API for future use
352353
pub fn scroll_position(&self) -> usize {
353354
self.cache.map(|c| c.scroll_position).unwrap_or(0)
354355
}
355356

356357
/// Get current height
358+
#[allow(dead_code)] // Public API for future use
357359
pub fn height(&self) -> usize {
358360
self.height
359361
}
360362

361363
/// Set height (usually called during resolve, but can be set explicitly)
364+
#[allow(dead_code)] // Public API for future use
362365
pub fn set_height(&mut self, height: usize) {
363366
if self.height != height {
364367
self.height = height;

0 commit comments

Comments
 (0)