1use crate::config::{self, Config};
4use crate::theme;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SettingsPage {
8 About,
10 Setting,
12 Pet,
14 Help,
16}
17
18impl SettingsPage {
19 pub fn label(self) -> &'static str {
20 match self {
21 SettingsPage::About => "About",
22 SettingsPage::Setting => "Setting",
23 SettingsPage::Pet => "Pet",
24 SettingsPage::Help => "Help",
25 }
26 }
27
28 pub fn all() -> &'static [SettingsPage] {
29 &[
30 SettingsPage::About,
31 SettingsPage::Setting,
32 SettingsPage::Pet,
33 SettingsPage::Help,
34 ]
35 }
36
37 pub fn next(self) -> Self {
38 match self {
39 SettingsPage::About => SettingsPage::Setting,
40 SettingsPage::Setting => SettingsPage::Pet,
41 SettingsPage::Pet => SettingsPage::Help,
42 SettingsPage::Help => SettingsPage::About,
43 }
44 }
45
46 pub fn prev(self) -> Self {
47 match self {
48 SettingsPage::About => SettingsPage::Help,
49 SettingsPage::Setting => SettingsPage::About,
50 SettingsPage::Pet => SettingsPage::Setting,
51 SettingsPage::Help => SettingsPage::Pet,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct HelpEntry {
59 pub keys: &'static str,
60 pub desc: &'static str,
61 pub is_header: bool,
63}
64
65pub fn help_entries() -> &'static [HelpEntry] {
67 &[
68 HelpEntry {
69 keys: "",
70 desc: "General",
71 is_header: true,
72 },
73 HelpEntry {
74 keys: "Ctrl+,",
75 desc: "Settings (About / Setting / Pet / Help)",
76 is_header: false,
77 },
78 HelpEntry {
79 keys: ":screensaver / :ss",
80 desc: "xeifetch splash (clock · weather · Esc exit)",
81 is_header: false,
82 },
83 HelpEntry {
84 keys: ":pet path.gif",
85 desc: "Load desktop pet GIF (Kitty graphics)",
86 is_header: false,
87 },
88 HelpEntry {
89 keys: "Ctrl+S",
90 desc: "Save file",
91 is_header: false,
92 },
93 HelpEntry {
94 keys: "Ctrl+P / Cmd+P",
95 desc: "Quick open files",
96 is_header: false,
97 },
98 HelpEntry {
99 keys: "Ctrl+Shift+P",
100 desc: "Command palette",
101 is_header: false,
102 },
103 HelpEntry {
104 keys: "Ctrl+F",
105 desc: "Toggle file explorer",
106 is_header: false,
107 },
108 HelpEntry {
109 keys: "Ctrl+E",
110 desc: "Toggle XLC command panel",
111 is_header: false,
112 },
113 HelpEntry {
114 keys: "Ctrl+Shift+V",
115 desc: "Pretty preview (Markdown / JSON / CSV / image / audio)",
116 is_header: false,
117 },
118 HelpEntry {
119 keys: "za / zc / zo / zM / zR",
120 desc: "Toggle / close / open fold · close all / open all",
121 is_header: false,
122 },
123 HelpEntry {
124 keys: "Ctrl+B / gb",
125 desc: "Git blame panel (slides in · flame colors)",
126 is_header: false,
127 },
128 HelpEntry {
129 keys: "Space (leader)",
130 desc: "Which-key: f files · g git · l lsp · d debug · w window · …",
131 is_header: false,
132 },
133 HelpEntry {
134 keys: "F5 / Shift+F5",
135 desc: "DAP start/continue · stop session",
136 is_header: false,
137 },
138 HelpEntry {
139 keys: "F9 / F10 / F11 / S-F11",
140 desc: "Toggle breakpoint · step over · into · out",
141 is_header: false,
142 },
143 HelpEntry {
144 keys: "F6",
145 desc: "DAP pause a running program",
146 is_header: false,
147 },
148 HelpEntry {
149 keys: ":bp if expr / :bp log msg",
150 desc: "Conditional breakpoint · logpoint",
151 is_header: false,
152 },
153 HelpEntry {
154 keys: ":pr N",
155 desc: "PR review surface (files · diff · comments)",
156 is_header: false,
157 },
158 HelpEntry {
159 keys: "Ctrl+W v/s ×N · h j k l",
160 desc: "Split again for up to 4 panes · directional focus",
161 is_header: false,
162 },
163 HelpEntry {
164 keys: "zh zl zH zL",
165 desc: "Pan horizontally (wrap_lines = false)",
166 is_header: false,
167 },
168 HelpEntry {
169 keys: "Wheel / PageUp·Down",
170 desc: "Terminal scrollback (badge ↑N · typing snaps live)",
171 is_header: false,
172 },
173 HelpEntry {
174 keys: ":settings / SPC l a",
175 desc: "Settings · code actions (legacy terminals: Ctrl+,/Ctrl+. don't exist)",
176 is_header: false,
177 },
178 HelpEntry {
179 keys: "Ctrl+Shift+D / SPC d",
180 desc: "Debug panel (stack · vars · BPs · console)",
181 is_header: false,
182 },
183 HelpEntry {
184 keys: ":dap / :bp / :launch",
185 desc: "Debug panel · breakpoint · launch program",
186 is_header: false,
187 },
188 HelpEntry {
189 keys: "gC / gI / gH / :calls",
190 desc: "Call hierarchy (incoming / outgoing)",
191 is_header: false,
192 },
193 HelpEntry {
194 keys: ":rebase [N] / SPC g r",
195 desc: "Interactive rebase last N commits",
196 is_header: false,
197 },
198 HelpEntry {
199 keys: ":rebase-abort / :rebase-continue",
200 desc: "Abort or continue in-progress rebase",
201 is_header: false,
202 },
203 HelpEntry {
204 keys: ":codelens / SPC t l",
205 desc: "Toggle LSP code lens (EOL virtual text)",
206 is_header: false,
207 },
208 HelpEntry {
209 keys: "PR Enter / :pr N",
210 desc: "PR review · files + diff + comments",
211 is_header: false,
212 },
213 HelpEntry {
214 keys: "~/.xei/hooks.toml",
215 desc: "Plugin hooks: on_save / on_open / on_quit",
216 is_header: false,
217 },
218 HelpEntry {
219 keys: "]c / [c",
220 desc: "Next / previous git change (gutter hunk)",
221 is_header: false,
222 },
223 HelpEntry {
224 keys: "g / z / d c y / Ctrl+W",
225 desc: "Prefix chords open delayed which-key popup",
226 is_header: false,
227 },
228 HelpEntry {
229 keys: "Tab (Insert)",
230 desc: "Expand snippet (fn, for, if, …) or indent",
231 is_header: false,
232 },
233 HelpEntry {
234 keys: "Live reload",
235 desc: "Auto-reload when file changes on disk",
236 is_header: false,
237 },
238 HelpEntry {
239 keys: "Git · 9 Stash",
240 desc: "Stash list · Enter apply · d drop · p preview",
241 is_header: false,
242 },
243 HelpEntry {
244 keys: "Ctrl+D",
245 desc: "Multi-cursor: add next word match",
246 is_header: false,
247 },
248 HelpEntry {
249 keys: "Ctrl+Alt+j/k",
250 desc: "Multi-cursor: add caret below / above",
251 is_header: false,
252 },
253 HelpEntry {
254 keys: "Esc (multi)",
255 desc: "Clear extra carets (Insert: first Esc)",
256 is_header: false,
257 },
258 HelpEntry {
259 keys: "Explorer · Enter",
260 desc: "Open file · images/csv/npy/audio → preview",
261 is_header: false,
262 },
263 HelpEntry {
264 keys: "Preview ←/→",
265 desc: "Resize image preview",
266 is_header: false,
267 },
268 HelpEntry {
269 keys: "Preview Space",
270 desc: "Play / stop audio",
271 is_header: false,
272 },
273 HelpEntry {
274 keys: "Ctrl+Shift+F",
275 desc: "Find in files (workspace search)",
276 is_header: false,
277 },
278 HelpEntry {
279 keys: "Ctrl+Shift+O / gO",
280 desc: "Document symbols (outline)",
281 is_header: false,
282 },
283 HelpEntry {
284 keys: "Ctrl+.",
285 desc: "Code actions / quick fix",
286 is_header: false,
287 },
288 HelpEntry {
289 keys: "Ctrl+Shift+I",
290 desc: "Format document (LSP)",
291 is_header: false,
292 },
293 HelpEntry {
294 keys: "Cmd+C / V / X",
295 desc: "Copy / paste / cut (system clipboard)",
296 is_header: false,
297 },
298 HelpEntry {
299 keys: "Right-click",
300 desc: "Editor context menu (Insert / Normal / Visual)",
301 is_header: false,
302 },
303 HelpEntry {
304 keys: "",
305 desc: "Terminal",
306 is_header: true,
307 },
308 HelpEntry {
309 keys: "Ctrl+T",
310 desc: "Side terminal panel (Esc closes side term)",
311 is_header: false,
312 },
313 HelpEntry {
314 keys: "Ctrl+Shift+T",
315 desc: "Terminal window in a split pane",
316 is_header: false,
317 },
318 HelpEntry {
319 keys: "Ctrl+Shift+W",
320 desc: "Close terminal window (then y / n)",
321 is_header: false,
322 },
323 HelpEntry {
324 keys: "Esc (term focused)",
325 desc: "Sent to shell — not editor (vim/opencode exit)",
326 is_header: false,
327 },
328 HelpEntry {
329 keys: "Ctrl+C / D / Z …",
330 desc: "When term focused: real signals to the program",
331 is_header: false,
332 },
333 HelpEntry {
334 keys: "Ctrl+W w",
335 desc: "From terminal pane: focus the other split",
336 is_header: false,
337 },
338 HelpEntry {
339 keys: "F12",
340 desc: "Quick toggle side terminal",
341 is_header: false,
342 },
343 HelpEntry {
344 keys: "",
345 desc: "Splits",
346 is_header: true,
347 },
348 HelpEntry {
349 keys: "Ctrl+W v / s",
350 desc: "Vertical / horizontal split",
351 is_header: false,
352 },
353 HelpEntry {
354 keys: "Ctrl+W w",
355 desc: "Cycle focused split pane",
356 is_header: false,
357 },
358 HelpEntry {
359 keys: "Ctrl+W q",
360 desc: "Close current split",
361 is_header: false,
362 },
363 HelpEntry {
364 keys: "Ctrl+W =",
365 desc: "Equalize split sizes",
366 is_header: false,
367 },
368 HelpEntry {
369 keys: "Ctrl+W h/j/k/l",
370 desc: "Focus left / down / up / right pane",
371 is_header: false,
372 },
373 HelpEntry {
374 keys: "Ctrl+W < / >",
375 desc: "Resize split",
376 is_header: false,
377 },
378 HelpEntry {
379 keys: "Drag split edge",
380 desc: "Mouse-resize panes",
381 is_header: false,
382 },
383 HelpEntry {
384 keys: "",
385 desc: "Git",
386 is_header: true,
387 },
388 HelpEntry {
389 keys: "Ctrl+G",
390 desc: "Light Source Control (stage / commit)",
391 is_header: false,
392 },
393 HelpEntry {
394 keys: "Ctrl+Shift+G",
395 desc: "Full Git workbench (mini GitHub)",
396 is_header: false,
397 },
398 HelpEntry {
399 keys: "1–8 (in Git)",
400 desc: "Status / Log / Branches / Files / Diff / PRs / Issues / Auth",
401 is_header: false,
402 },
403 HelpEntry {
404 keys: "Tab (in Git)",
405 desc: "Cycle Changes · Log · Files columns",
406 is_header: false,
407 },
408 HelpEntry {
409 keys: "Space / s (Git Status)",
410 desc: "Stage / unstage selected file",
411 is_header: false,
412 },
413 HelpEntry {
414 keys: "c (Git Status)",
415 desc: "Edit commit message · Enter commits",
416 is_header: false,
417 },
418 HelpEntry {
419 keys: "Enter (Git)",
420 desc: "Open diff / commit detail / PR checkout",
421 is_header: false,
422 },
423 HelpEntry {
424 keys: "Right-click commit",
425 desc: "Cherry-pick / revert / copy hash / browse",
426 is_header: false,
427 },
428 HelpEntry {
429 keys: "v (Git Log)",
430 desc: "Toggle list / graph view",
431 is_header: false,
432 },
433 HelpEntry {
434 keys: "f p u (Git)",
435 desc: "Fetch / pull / push",
436 is_header: false,
437 },
438 HelpEntry {
439 keys: "r (Git)",
440 desc: "Refresh current tab",
441 is_header: false,
442 },
443 HelpEntry {
444 keys: ":gh-login",
445 desc: "GitHub CLI auth (web)",
446 is_header: false,
447 },
448 HelpEntry {
449 keys: "",
450 desc: "Modes & editing",
451 is_header: true,
452 },
453 HelpEntry {
454 keys: "i a A o O",
455 desc: "Enter Insert mode",
456 is_header: false,
457 },
458 HelpEntry {
459 keys: "Esc",
460 desc: "Back to Normal mode",
461 is_header: false,
462 },
463 HelpEntry {
464 keys: "v / V / Ctrl+V",
465 desc: "Visual / Visual Line / Visual Block",
466 is_header: false,
467 },
468 HelpEntry {
469 keys: "h j k l · ←↓↑→",
470 desc: "Move cursor",
471 is_header: false,
472 },
473 HelpEntry {
474 keys: "w b e · 0 $ · gg G",
475 desc: "Word / line / file motions",
476 is_header: false,
477 },
478 HelpEntry {
479 keys: "d / c / y + motion",
480 desc: "Delete / change / yank",
481 is_header: false,
482 },
483 HelpEntry {
484 keys: "diw ci\" dib …",
485 desc: "Text objects (inner / around)",
486 is_header: false,
487 },
488 HelpEntry {
489 keys: "u / Ctrl+R",
490 desc: "Undo / Redo",
491 is_header: false,
492 },
493 HelpEntry {
494 keys: ".",
495 desc: "Repeat last change",
496 is_header: false,
497 },
498 HelpEntry {
499 keys: "p P",
500 desc: "Paste after / before",
501 is_header: false,
502 },
503 HelpEntry {
504 keys: "x",
505 desc: "Delete character",
506 is_header: false,
507 },
508 HelpEntry {
509 keys: "\"a \"+ ",
510 desc: "Registers (named / clipboard)",
511 is_header: false,
512 },
513 HelpEntry {
514 keys: "ma 'a `a",
515 desc: "Set mark / jump",
516 is_header: false,
517 },
518 HelpEntry {
519 keys: "Ctrl+O / Ctrl+I",
520 desc: "Jumplist back / forward",
521 is_header: false,
522 },
523 HelpEntry {
524 keys: "f t ; ,",
525 desc: "Find char; repeat / reverse",
526 is_header: false,
527 },
528 HelpEntry {
529 keys: "qa … q · @a · @@",
530 desc: "Record / play / replay macro",
531 is_header: false,
532 },
533 HelpEntry {
534 keys: "",
535 desc: "Search",
536 is_header: true,
537 },
538 HelpEntry {
539 keys: "/ ?",
540 desc: "Search forward / reverse",
541 is_header: false,
542 },
543 HelpEntry {
544 keys: "n N",
545 desc: "Next / previous match",
546 is_header: false,
547 },
548 HelpEntry {
549 keys: "* #",
550 desc: "Word under cursor (fwd / back)",
551 is_header: false,
552 },
553 HelpEntry {
554 keys: "",
555 desc: "LSP & diagnostics",
556 is_header: true,
557 },
558 HelpEntry {
559 keys: "gd",
560 desc: "Go to definition",
561 is_header: false,
562 },
563 HelpEntry {
564 keys: "gp",
565 desc: "Peek definition",
566 is_header: false,
567 },
568 HelpEntry {
569 keys: "gr",
570 desc: "Find references",
571 is_header: false,
572 },
573 HelpEntry {
574 keys: "K",
575 desc: "Hover documentation",
576 is_header: false,
577 },
578 HelpEntry {
579 keys: "Ctrl+A (Insert)",
580 desc: "Completions (LSP + keywords)",
581 is_header: false,
582 },
583 HelpEntry {
584 keys: "]d [d",
585 desc: "Next / prev diagnostic",
586 is_header: false,
587 },
588 HelpEntry {
589 keys: ":Rename name",
590 desc: "LSP rename",
591 is_header: false,
592 },
593 HelpEntry {
594 keys: "",
595 desc: "Tabs & buffers",
596 is_header: true,
597 },
598 HelpEntry {
599 keys: "gt gT",
600 desc: "Next / previous tab",
601 is_header: false,
602 },
603 HelpEntry {
604 keys: ":e <file>",
605 desc: "Open file (new tab)",
606 is_header: false,
607 },
608 HelpEntry {
609 keys: ":bd",
610 desc: "Close current tab",
611 is_header: false,
612 },
613 HelpEntry {
614 keys: ":w :q :wq",
615 desc: "Save / quit / save+quit",
616 is_header: false,
617 },
618 HelpEntry {
619 keys: ":s/pat/repl/g",
620 desc: "Substitute on line / range",
621 is_header: false,
622 },
623 HelpEntry {
624 keys: ":theme <name>",
625 desc: "Switch theme (persists)",
626 is_header: false,
627 },
628 HelpEntry {
629 keys: ":help",
630 desc: "List XLC commands",
631 is_header: false,
632 },
633 HelpEntry {
634 keys: "",
635 desc: "Settings panel",
636 is_header: true,
637 },
638 HelpEntry {
639 keys: "Tab / Shift+Tab",
640 desc: "Next / previous page",
641 is_header: false,
642 },
643 HelpEntry {
644 keys: "1 2 3 4",
645 desc: "Jump About / Setting / Pet / Help",
646 is_header: false,
647 },
648 HelpEntry {
649 keys: "j k · Enter",
650 desc: "Move selection · activate",
651 is_header: false,
652 },
653 HelpEntry {
654 keys: "s",
655 desc: "Save ~/.xei.toml",
656 is_header: false,
657 },
658 HelpEntry {
659 keys: "Esc / q",
660 desc: "Close settings",
661 is_header: false,
662 },
663 ]
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668pub enum SettingRow {
669 ThemeHeader,
670 Theme(usize),
671 EditorHeader,
672 TabWidth,
673 RelativeNumber,
674 WrapLines,
675 UndoCaching,
676 ClipboardSync,
677 GpuAcc,
678 GpuGraphics,
679 GpuHyperlinks,
680 KeyHints,
681 LspHeader,
682 LspEnabled,
683 LspLang(usize),
685 GitHeader,
686 OpenWorkbench,
687 OpenScm,
688 PetEnabled,
690 PetPath,
691 PetX,
692 PetY,
693 PetWidth,
694 PetSpeed,
696 PetReload,
697}
698
699fn setting_rows() -> Vec<SettingRow> {
700 let mut rows = vec![SettingRow::ThemeHeader];
701 for i in 0..theme::all_themes().len() {
702 rows.push(SettingRow::Theme(i));
703 }
704 rows.push(SettingRow::EditorHeader);
705 rows.push(SettingRow::TabWidth);
706 rows.push(SettingRow::RelativeNumber);
707 rows.push(SettingRow::WrapLines);
708 rows.push(SettingRow::UndoCaching);
709 rows.push(SettingRow::ClipboardSync);
710 rows.push(SettingRow::GpuAcc);
711 rows.push(SettingRow::GpuGraphics);
712 rows.push(SettingRow::GpuHyperlinks);
713 rows.push(SettingRow::KeyHints);
714 rows.push(SettingRow::LspHeader);
715 rows.push(SettingRow::LspEnabled);
716 for i in 0..config::lsp_lang_catalog().len() {
717 rows.push(SettingRow::LspLang(i));
718 }
719 rows.push(SettingRow::GitHeader);
720 rows.push(SettingRow::OpenWorkbench);
721 rows.push(SettingRow::OpenScm);
722 rows
723}
724
725fn pet_rows() -> Vec<SettingRow> {
726 vec![
727 SettingRow::PetEnabled,
728 SettingRow::PetPath,
729 SettingRow::PetX,
730 SettingRow::PetY,
731 SettingRow::PetWidth,
732 SettingRow::PetSpeed,
733 SettingRow::PetReload,
734 ]
735}
736
737const PET_SPEED_STOPS: &[u16] = &[25, 50, 75, 100, 125, 150, 200, 300, 400];
739
740#[derive(Debug, Clone)]
741pub struct SettingsPanel {
742 pub open: bool,
743 pub page: SettingsPage,
744 pub selected: usize,
746 pub draft: Config,
748 pub dirty: bool,
749 pub status: Option<String>,
750}
751
752impl Default for SettingsPanel {
753 fn default() -> Self {
754 Self {
755 open: false,
756 page: SettingsPage::About,
757 selected: 0,
758 draft: Config::default(),
759 dirty: false,
760 status: None,
761 }
762 }
763}
764
765impl SettingsPanel {
766 pub fn new() -> Self {
767 Self::default()
768 }
769
770 pub fn open_panel(&mut self) {
771 self.open = true;
772 self.page = SettingsPage::About;
773 self.selected = 0;
774 self.draft = config::load();
775 self.dirty = false;
776 self.status = None;
777 }
778
779 pub fn close(&mut self) {
780 self.open = false;
781 self.status = None;
782 }
783
784 pub fn visible(&self) -> bool {
785 self.open
786 }
787
788 pub fn setting_rows(&self) -> Vec<SettingRow> {
789 setting_rows()
790 }
791
792 pub fn page_item_count(&self) -> usize {
793 match self.page {
794 SettingsPage::About => 0,
795 SettingsPage::Setting => setting_rows().len(),
796 SettingsPage::Pet => pet_rows().len(),
797 SettingsPage::Help => help_entries().len(),
798 }
799 }
800
801 pub fn pet_rows(&self) -> Vec<SettingRow> {
802 pet_rows()
803 }
804
805 pub fn move_sel(&mut self, delta: isize) {
807 let n = self.page_item_count();
808 if n == 0 {
809 self.selected = 0;
810 return;
811 }
812 if self.page == SettingsPage::Setting {
813 let rows = setting_rows();
814 let is_header = |r: &SettingRow| {
815 matches!(
816 r,
817 SettingRow::ThemeHeader
818 | SettingRow::EditorHeader
819 | SettingRow::LspHeader
820 | SettingRow::GitHeader
821 )
822 };
823 let mut cur = self.selected as isize;
824 let step = if delta >= 0 { 1 } else { -1 };
825 let mut left = delta.abs().max(1);
826 if cur >= 0 && (cur as usize) < rows.len() && is_header(&rows[cur as usize]) {
828 left = left.max(1);
829 }
830 while left > 0 {
831 let next = cur + step;
832 if next < 0 || next >= rows.len() as isize {
833 break;
835 }
836 cur = next;
837 if !is_header(&rows[cur as usize]) {
838 left -= 1;
839 }
840 }
841 if (cur as usize) < rows.len() && is_header(&rows[cur as usize]) {
843 if let Some((i, _)) = rows.iter().enumerate().find(|(_, r)| !is_header(r)) {
844 cur = i as isize;
845 }
846 }
847 self.selected = cur as usize;
848 return;
849 }
850 if self.page == SettingsPage::Help {
852 let entries = help_entries();
853 let mut cur = self.selected as isize;
854 let step = if delta >= 0 { 1 } else { -1 };
855 let mut left = delta.abs().max(1);
856 while left > 0 {
857 let next = cur + step;
858 if next < 0 || next >= entries.len() as isize {
859 break;
860 }
861 cur = next;
862 if !entries[cur as usize].is_header {
863 left -= 1;
864 }
865 }
866 if (cur as usize) < entries.len() && entries[cur as usize].is_header {
867 if let Some((i, _)) = entries.iter().enumerate().find(|e| !e.1.is_header) {
868 cur = i as isize;
869 }
870 }
871 self.selected = cur as usize;
872 return;
873 }
874 let cur = self.selected as isize + delta;
875 self.selected = cur.clamp(0, (n - 1) as isize) as usize;
876 }
877
878 pub fn next_page(&mut self) {
879 self.page = self.page.next();
880 self.selected = self.default_selected_for_page();
881 self.status = None;
882 }
883
884 pub fn prev_page(&mut self) {
885 self.page = self.page.prev();
886 self.selected = self.default_selected_for_page();
887 self.status = None;
888 }
889
890 fn default_selected_for_page(&self) -> usize {
891 match self.page {
892 SettingsPage::Setting => 1, SettingsPage::Pet => 0,
894 SettingsPage::Help => {
895 help_entries()
897 .iter()
898 .position(|e| !e.is_header)
899 .unwrap_or(0)
900 }
901 SettingsPage::About => 0,
902 }
903 }
904
905 pub fn activate(&mut self) -> SettingsAction {
907 match self.page {
908 SettingsPage::About | SettingsPage::Help => SettingsAction::None,
909 SettingsPage::Pet => {
910 let rows = pet_rows();
911 let Some(row) = rows.get(self.selected).copied() else {
912 return SettingsAction::None;
913 };
914 match row {
915 SettingRow::PetEnabled => {
916 self.draft.pet_enabled = !self.draft.pet_enabled;
917 self.dirty = true;
918 self.status = Some(if self.draft.pet_enabled {
919 "pet = on (requires gpu_acc + Kitty/Ghostty)".into()
920 } else {
921 "pet = off".into()
922 });
923 SettingsAction::ApplyPet
924 }
925 SettingRow::PetPath => {
926 self.status = Some(
927 "Set path: :pet ~/path/to/pet.gif then s to save".into(),
928 );
929 SettingsAction::None
930 }
931 SettingRow::PetX => {
932 self.draft.pet_x = self.draft.pet_x.saturating_add(2);
934 self.dirty = true;
935 self.status = Some(format!("pet_x = {}", self.draft.pet_x));
936 SettingsAction::ApplyPet
937 }
938 SettingRow::PetY => {
939 self.draft.pet_y = self.draft.pet_y.saturating_add(1);
940 self.dirty = true;
941 self.status = Some(format!("pet_y = {}", self.draft.pet_y));
942 SettingsAction::ApplyPet
943 }
944 SettingRow::PetWidth => {
945 self.draft.pet_width_cells = match self.draft.pet_width_cells {
946 4..=7 => 12,
947 8..=15 => 20,
948 _ => 8,
949 };
950 self.dirty = true;
951 self.status =
952 Some(format!("pet_width_cells = {}", self.draft.pet_width_cells));
953 SettingsAction::ApplyPet
954 }
955 SettingRow::PetSpeed => {
956 self.draft.pet_speed = next_pet_speed(self.draft.pet_speed);
957 self.dirty = true;
958 self.status = Some(format!(
959 "pet_speed = {} ({})",
960 self.draft.pet_speed,
961 crate::pet::PetState::speed_label(self.draft.pet_speed)
962 ));
963 SettingsAction::ApplyPet
964 }
965 SettingRow::PetReload => {
966 self.status = Some("Reloading pet…".into());
967 SettingsAction::ApplyPet
968 }
969 _ => SettingsAction::None,
970 }
971 }
972 SettingsPage::Setting => {
973 let rows = setting_rows();
974 let Some(row) = rows.get(self.selected).copied() else {
975 return SettingsAction::None;
976 };
977 match row {
978 SettingRow::Theme(i) => {
979 let themes = theme::all_themes();
980 if let Some(t) = themes.get(i) {
981 self.draft.theme = t.name.to_string();
982 self.dirty = true;
983 self.status = Some(format!("Theme → {}", t.name));
984 return SettingsAction::ApplyTheme;
985 }
986 }
987 SettingRow::TabWidth => {
988 self.draft.tab_width = match self.draft.tab_width {
989 2 => 4,
990 4 => 8,
991 _ => 2,
992 };
993 self.dirty = true;
994 self.status = Some(format!("tab_width = {}", self.draft.tab_width));
995 }
996 SettingRow::RelativeNumber => {
997 self.draft.relative_number = !self.draft.relative_number;
998 self.dirty = true;
999 self.status = Some(format!(
1000 "relative_number = {}",
1001 self.draft.relative_number
1002 ));
1003 }
1004 SettingRow::UndoCaching => {
1005 self.draft.undo_caching = !self.draft.undo_caching;
1006 self.dirty = true;
1007 self.status = Some(if self.draft.undo_caching {
1008 "undo_caching = true (history survives close · ~/.xei/undo)".into()
1009 } else {
1010 "undo_caching = false (history discarded on close)".into()
1011 });
1012 }
1013 SettingRow::WrapLines => {
1014 self.draft.wrap_lines = !self.draft.wrap_lines;
1015 self.dirty = true;
1016 self.status = Some(if self.draft.wrap_lines {
1017 "wrap_lines = true (soft-wrap long lines)".into()
1018 } else {
1019 "wrap_lines = false (horizontal scroll · zh/zl pan)".into()
1020 });
1021 }
1022 SettingRow::ClipboardSync => {
1023 self.draft.clipboard_sync = !self.draft.clipboard_sync;
1024 self.dirty = true;
1025 self.status = Some(format!(
1026 "clipboard_sync = {}",
1027 self.draft.clipboard_sync
1028 ));
1029 }
1030 SettingRow::GpuAcc => {
1031 self.draft.gpu_acc = !self.draft.gpu_acc;
1032 self.dirty = true;
1033 self.status = Some(if self.draft.gpu_acc {
1034 "gpu_acc = true (Ghostty/Kitty enhancements on)".into()
1035 } else {
1036 "gpu_acc = false (plain cell TUI)".into()
1037 });
1038 return SettingsAction::ApplyGpuAcc;
1039 }
1040 SettingRow::GpuGraphics => {
1041 self.draft.gpu_graphics = !self.draft.gpu_graphics;
1042 self.dirty = true;
1043 self.status = Some(format!("gpu_graphics = {}", self.draft.gpu_graphics));
1044 }
1045 SettingRow::GpuHyperlinks => {
1046 self.draft.gpu_hyperlinks = !self.draft.gpu_hyperlinks;
1047 self.dirty = true;
1048 self.status =
1049 Some(format!("gpu_hyperlinks = {}", self.draft.gpu_hyperlinks));
1050 }
1051 SettingRow::KeyHints => {
1052 self.draft.key_hints = !self.draft.key_hints;
1053 self.dirty = true;
1054 self.status = Some(format!(
1055 "key_hints = {}",
1056 if self.draft.key_hints { "true" } else { "false" }
1057 ));
1058 }
1059 SettingRow::LspEnabled => {
1060 self.draft.lsp_enabled = !self.draft.lsp_enabled;
1061 self.dirty = true;
1062 self.status = Some(format!(
1063 "lsp_enabled = {}",
1064 self.draft.lsp_enabled
1065 ));
1066 return SettingsAction::ApplyLsp;
1067 }
1068 SettingRow::LspLang(i) => {
1069 let catalog = config::lsp_lang_catalog();
1071 if let Some((key, _label, default_cmd)) = catalog.get(i) {
1072 let cur = self.draft.lsp_servers.get(*key).cloned();
1073 match cur.as_deref() {
1074 None => {
1075 self.draft
1077 .lsp_servers
1078 .insert((*key).to_string(), String::new());
1079 self.status = Some(format!("lsp.{key} = off"));
1080 }
1081 Some("") => {
1082 self.draft.lsp_servers.remove(*key);
1084 self.status =
1085 Some(format!("lsp.{key} = default ({default_cmd})"));
1086 }
1087 Some(_) => {
1088 self.draft
1090 .lsp_servers
1091 .insert((*key).to_string(), String::new());
1092 self.status = Some(format!("lsp.{key} = off"));
1093 }
1094 }
1095 self.dirty = true;
1096 return SettingsAction::ApplyLsp;
1097 }
1098 }
1099 SettingRow::OpenWorkbench => {
1100 return SettingsAction::OpenWorkbench;
1101 }
1102 SettingRow::OpenScm => {
1103 return SettingsAction::OpenScm;
1104 }
1105 SettingRow::ThemeHeader
1106 | SettingRow::EditorHeader
1107 | SettingRow::LspHeader
1108 | SettingRow::GitHeader
1109 | SettingRow::PetEnabled
1110 | SettingRow::PetPath
1111 | SettingRow::PetX
1112 | SettingRow::PetY
1113 | SettingRow::PetWidth
1114 | SettingRow::PetSpeed
1115 | SettingRow::PetReload => {}
1116 }
1117 SettingsAction::None
1118 }
1119 }
1120 }
1121
1122 pub fn save(&mut self) {
1123 config::save(&self.draft);
1124 self.dirty = false;
1125 self.status = Some("Saved ~/.xei.toml".into());
1126 }
1127
1128 pub fn version_string() -> String {
1129 format!("xei {}", env!("CARGO_PKG_VERSION"))
1130 }
1131}
1132
1133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1135pub enum SettingsAction {
1136 None,
1137 ApplyTheme,
1138 ApplyGpuAcc,
1139 ApplyLsp,
1140 ApplyPet,
1141 OpenWorkbench,
1142 OpenScm,
1143}
1144
1145fn next_pet_speed(cur: u16) -> u16 {
1146 for (i, &s) in PET_SPEED_STOPS.iter().enumerate() {
1147 if cur < s {
1148 return s;
1149 }
1150 if cur == s {
1151 return PET_SPEED_STOPS
1152 .get(i + 1)
1153 .copied()
1154 .unwrap_or(PET_SPEED_STOPS[0]);
1155 }
1156 }
1157 100
1158}
1159
1160fn prev_pet_speed(cur: u16) -> u16 {
1161 let mut prev = *PET_SPEED_STOPS.last().unwrap_or(&100);
1162 for &s in PET_SPEED_STOPS {
1163 if cur <= s {
1164 return prev;
1165 }
1166 prev = s;
1167 }
1168 prev
1169}
1170
1171impl SettingsPanel {
1172 pub fn nudge_pet(&mut self, dir: i16, max_x: u16, max_y: u16) -> SettingsAction {
1175 if self.page != SettingsPage::Pet {
1176 return SettingsAction::None;
1177 }
1178 let rows = pet_rows();
1179 let Some(row) = rows.get(self.selected).copied() else {
1180 return SettingsAction::None;
1181 };
1182 match row {
1183 SettingRow::PetX => {
1184 if dir < 0 {
1185 self.draft.pet_x = self.draft.pet_x.saturating_sub(1);
1186 } else {
1187 self.draft.pet_x = self.draft.pet_x.saturating_add(1).min(max_x);
1188 }
1189 self.dirty = true;
1190 self.status = Some(format!("pet_x = {} (0..{max_x})", self.draft.pet_x));
1191 SettingsAction::ApplyPet
1192 }
1193 SettingRow::PetY => {
1194 if dir < 0 {
1195 self.draft.pet_y = self.draft.pet_y.saturating_sub(1);
1196 } else {
1197 self.draft.pet_y = self.draft.pet_y.saturating_add(1).min(max_y);
1198 }
1199 self.dirty = true;
1200 self.status = Some(format!("pet_y = {} (0..{max_y})", self.draft.pet_y));
1201 SettingsAction::ApplyPet
1202 }
1203 SettingRow::PetWidth => {
1204 if dir < 0 {
1205 self.draft.pet_width_cells =
1206 self.draft.pet_width_cells.saturating_sub(1).max(4);
1207 } else {
1208 self.draft.pet_width_cells =
1209 self.draft.pet_width_cells.saturating_add(1).min(40);
1210 }
1211 self.dirty = true;
1212 self.status = Some(format!("pet_width_cells = {}", self.draft.pet_width_cells));
1213 SettingsAction::ApplyPet
1214 }
1215 SettingRow::PetSpeed => {
1216 self.draft.pet_speed = if dir < 0 {
1217 prev_pet_speed(self.draft.pet_speed)
1218 } else {
1219 next_pet_speed(self.draft.pet_speed)
1220 };
1221 self.dirty = true;
1222 self.status = Some(format!(
1223 "pet_speed = {} ({})",
1224 self.draft.pet_speed,
1225 crate::pet::PetState::speed_label(self.draft.pet_speed)
1226 ));
1227 SettingsAction::ApplyPet
1228 }
1229 _ => SettingsAction::None,
1230 }
1231 }
1232}