gwm/tui/state/command_logs.rs
1//! Command Logs modal state (issue #226).
2//!
3//! The scroll cursor for the lazygit-style Command Logs overlay plus an
4//! owned snapshot of the [`crate::command_log`] global. The `App` calls
5//! [`CommandLogs::sync`] when the overlay opens (and on each tick while it
6//! is up) so the renderer paints from this owned state rather than locking
7//! the global mid-frame — the same boundary the modal-render tests inject
8//! through.
9//!
10//! Scroll mirrors the help overlay (`App::help_scroll` / `help_max_scroll`):
11//! the cursor lives here, but `max_scroll` / `max_x_scroll` are republished
12//! by the renderer each frame against the real viewport, since only the
13//! renderer knows both the content length and the inner modal height. The
14//! scroll methods clamp against whatever bound the last frame published.
15
16use crate::command_log::{self, CommandLogEntry};
17
18/// Owned state for the Command Logs overlay: the snapshotted entries and
19/// the (vertical + horizontal) scroll cursor with its renderer-published
20/// bounds.
21#[derive(Debug, Default)]
22pub struct CommandLogs {
23 /// Snapshot of the global command log, oldest-first (the renderer paints
24 /// it newest-first). Refreshed by [`Self::sync`].
25 pub entries: Vec<CommandLogEntry>,
26 /// Vertical scroll offset, in rows. Clamped to `max_scroll`.
27 pub scroll: u16,
28 /// Maximum vertical scroll offset, republished by the renderer each
29 /// frame as `content_rows.saturating_sub(viewport_rows)`.
30 pub max_scroll: u16,
31 /// Horizontal scroll offset, in columns. Clamped to `max_x_scroll`.
32 pub x_scroll: u16,
33 /// Maximum horizontal scroll offset, republished by the renderer.
34 pub max_x_scroll: u16,
35}
36
37impl CommandLogs {
38 /// An empty overlay at the origin.
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 /// Copy the global command log into [`Self::entries`]. Cheap clone of a
44 /// bounded ring (≤ `command_log::MAX_ENTRIES`); called on open and per
45 /// tick while the overlay is up so it tracks commands that finish (e.g.
46 /// an off-thread GitHub fetch) while the user is reading.
47 pub fn sync(&mut self) {
48 self.entries = command_log::snapshot();
49 }
50
51 /// Scroll down one row, never past the last line.
52 pub fn scroll_down(&mut self) {
53 self.scroll = (self.scroll + 1).min(self.max_scroll);
54 }
55
56 /// Scroll up one row, never above the top.
57 pub fn scroll_up(&mut self) {
58 self.scroll = self.scroll.saturating_sub(1);
59 }
60
61 /// Scroll right one column, never past the widest line.
62 pub fn scroll_right(&mut self) {
63 self.x_scroll = (self.x_scroll + 1).min(self.max_x_scroll);
64 }
65
66 /// Scroll left one column, never before the first.
67 pub fn scroll_left(&mut self) {
68 self.x_scroll = self.x_scroll.saturating_sub(1);
69 }
70
71 /// Jump to the first row (`g`).
72 pub fn scroll_to_top(&mut self) {
73 self.scroll = 0;
74 }
75
76 /// Jump to the last row (`G`).
77 pub fn scroll_to_bottom(&mut self) {
78 self.scroll = self.max_scroll;
79 }
80
81 /// Reset the scroll cursor to the origin, keeping the snapshotted
82 /// entries. Called when the overlay opens.
83 pub fn reset(&mut self) {
84 self.scroll = 0;
85 self.x_scroll = 0;
86 }
87}