tix/app/help.rs
1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4impl App {
5 pub(in crate::app) fn handle_help_key(&mut self, key: KeyEvent) -> Result<Action> {
6 // tix #1496: while the `/` line has the keyboard, every key belongs to the
7 // query. The list underneath filters as you type, so Enter only *commits* the
8 // input (the query stays and the list keeps its filter) and Esc clears it — the
9 // same bargain `/` makes on the board, so one gesture does not mean two things
10 // in the same tool. Closing the overlay is one Esc away in both states.
11 if self.help_searching {
12 match key.code {
13 KeyCode::Enter => self.help_searching = false,
14 KeyCode::Esc => {
15 self.help_query.clear();
16 self.help_searching = false;
17 }
18 KeyCode::Backspace => {
19 self.help_query.pop();
20 }
21 KeyCode::Char(c) => self.help_query.push(c),
22 _ => {}
23 }
24 // A narrower list starts at the top: keeping an offset from the wider list
25 // would scroll the matches away before the reader saw them.
26 self.help_scroll = 0;
27 return Ok(Action::None);
28 }
29 match key.code {
30 KeyCode::Char('?') | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
31 self.close_help();
32 }
33 // Not a toggle: `/` starts the input line, and the query it leaves behind is
34 // what filters the list. Reopening it with the query still there is how you
35 // extend a search, and Esc inside it is how you clear one.
36 KeyCode::Char('/') => self.help_searching = true,
37 // tix #1441/#1442: the overlay scrolls — one row for the cursor keys its
38 // context already binds (`next`/`previous`, which until now the help
39 // advertised and this handler ignored), one page for PgUp/PgDn, one wheel
40 // notch for the wheel. The same three steps the detail pane uses, because
41 // "scroll" meaning something different per surface is how a keyboard stops
42 // being learnable.
43 KeyCode::Down | KeyCode::Char('j') => self.scroll_help(1),
44 KeyCode::Up | KeyCode::Char('k') => self.scroll_help(-1),
45 KeyCode::PageDown => self.scroll_help(i32::from(self.help_page())),
46 KeyCode::PageUp => self.scroll_help(-i32::from(self.help_page())),
47 _ => {}
48 }
49 Ok(Action::None)
50 }
51
52 /// Rows one page step moves in the overlay: what fits, less a row of overlap.
53 pub(in crate::app) fn help_page(&self) -> u16 {
54 self.help_metrics.get().visible.saturating_sub(1).max(1)
55 }
56
57 /// Open the keybindings overlay, always at the top and unfiltered (tix #1441).
58 ///
59 /// A help screen that reappeared halfway down — from a scroll the user made some
60 /// minutes ago — would hide the first bindings from exactly the person who opened
61 /// it to look them up. The same goes for a `/` query left over from last time
62 /// (tix #1496).
63 pub(in crate::app) fn open_help(&mut self, return_to: Mode) {
64 self.help_scroll = 0;
65 self.help_query.clear();
66 self.help_searching = false;
67 self.mode = Mode::Help {
68 return_to: Box::new(return_to),
69 };
70 }
71
72 /// Scroll the overlay by `rows` (negative scrolls up), clamped to the list.
73 ///
74 /// The clamp is what makes the last row reachable *and* keeps the list from being
75 /// scrolled into emptiness — a bound the old truncating overlay did not need
76 /// because it could not move at all.
77 pub(in crate::app) fn scroll_help(&mut self, rows: i32) {
78 let max = i32::from(self.help_metrics.get().max_scroll);
79 let next = i32::from(self.help_scroll) + rows;
80 self.help_scroll = next.clamp(0, max) as u16;
81 }
82
83 pub(in crate::app) fn close_help(&mut self) {
84 if let Mode::Help { return_to } = std::mem::replace(&mut self.mode, Mode::Board) {
85 self.mode = *return_to;
86 }
87 }
88
89 /// Open the startup tips modal over whatever the session opened on (tix
90 /// #1632), starting at a random tip so the same advice does not wear out.
91 ///
92 /// A no-op when the embedded list is empty, so a build whose embedded tips
93 /// failed to parse simply has no dialog rather than an empty one.
94 pub fn open_tips(&mut self) {
95 let tips = crate::tips::all();
96 if tips.is_empty() {
97 return;
98 }
99 let return_to = std::mem::replace(&mut self.mode, Mode::Board);
100 self.tips = Some(crate::ui::tips::TipsState::new(crate::tips::random_start(
101 tips.len(),
102 )));
103 self.mode = Mode::Tips {
104 return_to: Box::new(return_to),
105 };
106 }
107
108 /// Close the tips modal, landing back on the mode it was opened from.
109 pub(in crate::app) fn close_tips(&mut self) {
110 self.tips = None;
111 if let Mode::Tips { return_to } = std::mem::replace(&mut self.mode, Mode::Board) {
112 self.mode = *return_to;
113 }
114 // The raw key path returns before the binding layer's `sync_top_mode`, so
115 // the rail would keep highlighting whatever it did before the modal opened.
116 // Close by re-deriving it.
117 self.sync_top_mode();
118 }
119
120 /// The tips modal's keys (tix #1632), read raw — see `handle_key`.
121 ///
122 /// `Left`/`h`/`p` and `Right`/`l`/`n` walk the list, `r` jumps to a random
123 /// tip (never the one already shown), `Enter`/`Esc`/`q` closes.
124 /// `d` turns the tips off and persists the setting, then the modal shows the
125 /// one line telling the reader how to turn them back on; the keys stay raw so
126 /// `d` here means "disable", not the board's "delete".
127 pub(in crate::app) fn handle_tips_key(&mut self, key: KeyEvent) -> Result<Action> {
128 match key.code {
129 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('Q') => {
130 self.close_tips();
131 }
132 KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('p') => {
133 let len = crate::tips::all().len();
134 if let Some(state) = self.tips.as_mut() {
135 state.previous(len);
136 }
137 }
138 KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('n') => {
139 let len = crate::tips::all().len();
140 if let Some(state) = self.tips.as_mut() {
141 state.next(len);
142 }
143 }
144 KeyCode::Char('r') | KeyCode::Char('R') => {
145 let len = crate::tips::all().len();
146 if let Some(state) = self.tips.as_mut() {
147 // Inert on the "tips are off" notice: there is no tip on
148 // screen to replace.
149 if !state.disabled {
150 state.randomize(len);
151 }
152 }
153 }
154 KeyCode::Char('d') | KeyCode::Char('D') => self.disable_tips(),
155 _ => {}
156 }
157 Ok(Action::None)
158 }
159
160 /// Turn the startup tips off and write the setting to `tix.toml` (tix #1632).
161 ///
162 /// The write goes through [`App::persist_config`], the same writer the settings
163 /// surface uses, so the value lands in the file the next session reads. The
164 /// modal stays open showing the "set `tips_on_startup = true` to bring it
165 /// back" line — the reader just made a permanent choice and is told so before
166 /// the dialog goes away.
167 pub(in crate::app) fn disable_tips(&mut self) {
168 self.store.config.tips_on_startup = false;
169 let saved = self.persist_config();
170 if let Some(state) = self.tips.as_mut() {
171 state.disabled = true;
172 }
173 if !saved {
174 // `persist_config` already put the reason in the status line; the
175 // modal's notice would otherwise promise a persistence that did not
176 // happen.
177 self.status_msg =
178 Some("tips disabled for this session — tix.toml could not be written".to_string());
179 }
180 }
181}