1use crate::config::{Config, ConfigRow, ConfigSource};
23use crate::tui::keymap::{Action, KeyStroke, Keymap};
24use crate::tui::modal_keymap::{ModalAction, ModalKeymap};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum SettingsTab {
31 #[default]
33 Theme,
34 Worktree,
36 Tui,
39 Keys,
43 All,
45}
46
47impl SettingsTab {
48 pub const ALL: [SettingsTab; 5] = [
50 SettingsTab::Theme,
51 SettingsTab::Worktree,
52 SettingsTab::Tui,
53 SettingsTab::Keys,
54 SettingsTab::All,
55 ];
56
57 pub fn label(self) -> &'static str {
59 match self {
60 SettingsTab::Theme => "Theme",
61 SettingsTab::Worktree => "Worktree",
62 SettingsTab::Tui => "TUI",
63 SettingsTab::Keys => "Keys",
64 SettingsTab::All => "All",
65 }
66 }
67
68 pub fn fields(self) -> &'static [SettingField] {
71 match self {
72 SettingsTab::Theme => &[SettingField::ThemePreset],
73 SettingsTab::Worktree => &[
74 SettingField::WorktreeBase,
75 SettingField::WorktreePathPattern,
76 SettingField::WorktreeBranchPattern,
77 ],
78 SettingsTab::Tui => &[
79 SettingField::SidebarPosition,
80 SettingField::OpenMode,
81 SettingField::ConfirmCountdown,
82 SettingField::AutoRefreshSecs,
83 SettingField::OpenShellCmd,
84 SettingField::OpenEditorCmd,
85 ],
86 SettingsTab::Keys | SettingsTab::All => &[],
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum KeyTarget {
97 Global(Action),
99 Modal(ModalAction),
101}
102
103impl KeyTarget {
104 pub fn config_key(self) -> String {
106 match self {
107 KeyTarget::Global(a) => format!("tui.keys.{}", a.slug()),
108 KeyTarget::Modal(m) => format!("tui.keys.modal.{}.{}", m.context().config_path(), m.verb()),
109 }
110 }
111
112 pub fn single_only(self) -> bool {
115 matches!(self, KeyTarget::Modal(_))
116 }
117
118 pub fn compat_alias_keys(self) -> Vec<String> {
123 match self {
124 KeyTarget::Global(a) => a.compat_alias_slugs().map(|s| format!("tui.keys.{s}")).collect(),
125 KeyTarget::Modal(_) => Vec::new(),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct KeyRow {
135 pub target: KeyTarget,
137 pub scope: String,
139 pub label: String,
141 pub keys: String,
143 pub source: ConfigSource,
145}
146
147pub fn build_key_rows(keymap: &Keymap, modal: &ModalKeymap, source_of: impl Fn(&str) -> ConfigSource) -> Vec<KeyRow> {
153 let mut rows = Vec::new();
154 for action in Action::all() {
155 let target = KeyTarget::Global(action);
156 rows.push(KeyRow {
157 target,
158 scope: "global".to_string(),
159 label: action.slug().to_string(),
160 keys: keymap.keys_display(action),
161 source: source_of(&target.config_key()),
162 });
163 }
164 for action in ModalAction::all() {
165 let target = KeyTarget::Modal(action);
166 rows.push(KeyRow {
167 target,
168 scope: format!("modal.{}", action.context().config_path()),
169 label: action.verb().to_string(),
170 keys: modal.keys_display(action),
171 source: source_of(&target.config_key()),
172 });
173 }
174 rows
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct KeyCapture {
184 pub row: usize,
186 pub single_only: bool,
188 pub pending: Vec<KeyStroke>,
190}
191
192impl KeyCapture {
193 pub fn as_config_items(&self) -> Vec<String> {
198 if self.pending.is_empty() {
199 return Vec::new();
200 }
201 vec![self.pending.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(" ")]
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
209pub enum SettingsLayer {
210 #[default]
212 Project,
213 Global,
215}
216
217impl SettingsLayer {
218 pub fn label(self) -> &'static str {
220 match self {
221 SettingsLayer::Project => "project (.gwm.toml)",
222 SettingsLayer::Global => "global (~/.config/gwm)",
223 }
224 }
225
226 pub fn source(self) -> ConfigSource {
230 match self {
231 SettingsLayer::Project => ConfigSource::Repo,
232 SettingsLayer::Global => ConfigSource::User,
233 }
234 }
235
236 pub fn toggled(self) -> Self {
238 match self {
239 SettingsLayer::Project => SettingsLayer::Global,
240 SettingsLayer::Global => SettingsLayer::Project,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum FieldKind {
248 Choice,
250 Uint,
252 Text,
254}
255
256const SIDEBAR_CHOICES: &[&str] = &["right", "left"];
257const OPEN_MODE_CHOICES: &[&str] = &["shell", "editor", "finder"];
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum SettingField {
262 ThemePreset,
264 WorktreeBase,
266 WorktreePathPattern,
268 WorktreeBranchPattern,
270 SidebarPosition,
272 OpenMode,
274 ConfirmCountdown,
276 AutoRefreshSecs,
278 OpenShellCmd,
280 OpenEditorCmd,
282}
283
284impl SettingField {
285 pub fn label(self) -> &'static str {
287 match self {
288 SettingField::ThemePreset => "theme preset",
289 SettingField::WorktreeBase => "base directory",
290 SettingField::WorktreePathPattern => "path pattern",
291 SettingField::WorktreeBranchPattern => "branch pattern",
292 SettingField::SidebarPosition => "sidebar position",
293 SettingField::OpenMode => "open mode",
294 SettingField::ConfirmCountdown => "confirm countdown (s)",
295 SettingField::AutoRefreshSecs => "auto refresh (s)",
296 SettingField::OpenShellCmd => "open shell cmd",
297 SettingField::OpenEditorCmd => "open editor cmd",
298 }
299 }
300
301 pub fn key_path(self) -> &'static str {
303 match self {
304 SettingField::ThemePreset => "theme.preset",
305 SettingField::WorktreeBase => "worktree.base",
306 SettingField::WorktreePathPattern => "worktree.path_pattern",
307 SettingField::WorktreeBranchPattern => "worktree.branch_pattern",
308 SettingField::SidebarPosition => "tui.sidebar_position",
309 SettingField::OpenMode => "tui.open.mode",
310 SettingField::ConfirmCountdown => "tui.confirm_countdown_secs",
311 SettingField::AutoRefreshSecs => "tui.auto_refresh_secs",
312 SettingField::OpenShellCmd => "tui.open.shell_cmd",
313 SettingField::OpenEditorCmd => "tui.open.editor_cmd",
314 }
315 }
316
317 pub fn kind(self) -> FieldKind {
319 match self {
320 SettingField::ThemePreset | SettingField::SidebarPosition | SettingField::OpenMode => FieldKind::Choice,
321 SettingField::ConfirmCountdown | SettingField::AutoRefreshSecs => FieldKind::Uint,
322 SettingField::WorktreeBase
323 | SettingField::WorktreePathPattern
324 | SettingField::WorktreeBranchPattern
325 | SettingField::OpenShellCmd
326 | SettingField::OpenEditorCmd => FieldKind::Text,
327 }
328 }
329
330 fn edit_char_limit(self) -> usize {
331 match self {
332 SettingField::AutoRefreshSecs => 20,
333 SettingField::ConfirmCountdown => 3,
334 _ => 256,
335 }
336 }
337
338 pub fn choices(self) -> &'static [&'static str] {
341 match self {
342 SettingField::ThemePreset => crate::tui::theme::preset_names(),
343 SettingField::SidebarPosition => SIDEBAR_CHOICES,
344 SettingField::OpenMode => OPEN_MODE_CHOICES,
345 _ => &[],
346 }
347 }
348
349 pub fn current(self, cfg: &Config) -> String {
351 match self {
352 SettingField::ThemePreset => cfg.theme.preset.clone().unwrap_or_else(|| "default".into()),
353 SettingField::WorktreeBase => cfg.worktree.base.clone(),
354 SettingField::WorktreePathPattern => cfg.worktree.path_pattern.clone(),
355 SettingField::WorktreeBranchPattern => cfg.worktree.branch_pattern.clone(),
356 SettingField::SidebarPosition => cfg.tui.sidebar_position.label().into(),
357 SettingField::OpenMode => match cfg.tui.open.mode {
358 crate::config::TuiOpenMode::Shell => "shell".into(),
359 crate::config::TuiOpenMode::Editor => "editor".into(),
360 crate::config::TuiOpenMode::Finder => "finder".into(),
361 },
362 SettingField::ConfirmCountdown => cfg.tui.confirm_countdown_secs.to_string(),
363 SettingField::AutoRefreshSecs => cfg.tui.auto_refresh_secs.to_string(),
364 SettingField::OpenShellCmd => cfg.tui.open.shell_cmd.clone().unwrap_or_default(),
365 SettingField::OpenEditorCmd => cfg.tui.open.editor_cmd.clone().unwrap_or_default(),
366 }
367 }
368
369 pub fn next_choice(self, cfg: &Config) -> Option<String> {
373 let choices = self.choices();
374 if choices.is_empty() {
375 return None;
376 }
377 let current = self.current(cfg);
378 let idx = choices.iter().position(|c| *c == current);
379 let next = match idx {
380 Some(i) => choices[(i + 1) % choices.len()],
381 None => choices[0],
382 };
383 Some(next.to_string())
384 }
385}
386
387#[derive(Debug, Default)]
392pub struct ConfigPanel {
393 pub rows: Vec<ConfigRow>,
398 pub tab: SettingsTab,
400 pub layer: SettingsLayer,
402 pub selected: usize,
404 pub editing: Option<String>,
407 pub key_rows: Vec<KeyRow>,
411 pub capture: Option<KeyCapture>,
414 pub scroll: u16,
416 pub max_scroll: u16,
419 pub x_scroll: u16,
421 pub max_x_scroll: u16,
423}
424
425impl ConfigPanel {
426 pub fn new() -> Self {
428 Self::default()
429 }
430
431 pub fn fields(&self) -> &'static [SettingField] {
433 self.tab.fields()
434 }
435
436 pub fn selected_field(&self) -> Option<SettingField> {
438 self.fields().get(self.selected).copied()
439 }
440
441 pub fn selected_key_row(&self) -> Option<&KeyRow> {
443 if self.tab == SettingsTab::Keys {
444 self.key_rows.get(self.selected)
445 } else {
446 None
447 }
448 }
449
450 fn selectable_count(&self) -> usize {
453 if self.tab == SettingsTab::Keys {
454 self.key_rows.len()
455 } else {
456 self.fields().len()
457 }
458 }
459
460 pub fn next_tab(&mut self) {
463 let idx = SettingsTab::ALL.iter().position(|t| *t == self.tab).unwrap_or(0);
464 self.tab = SettingsTab::ALL[(idx + 1) % SettingsTab::ALL.len()];
465 self.selected = 0;
466 self.editing = None;
467 self.capture = None;
468 self.scroll = 0;
469 }
470
471 pub fn prev_tab(&mut self) {
473 let idx = SettingsTab::ALL.iter().position(|t| *t == self.tab).unwrap_or(0);
474 let len = SettingsTab::ALL.len();
475 self.tab = SettingsTab::ALL[(idx + len - 1) % len];
476 self.selected = 0;
477 self.editing = None;
478 self.capture = None;
479 self.scroll = 0;
480 }
481
482 pub fn toggle_layer(&mut self) {
484 self.layer = self.layer.toggled();
485 }
486
487 pub fn select_prev(&mut self) {
490 if self.editing.is_some() || self.capture.is_some() {
491 return;
492 }
493 self.selected = self.selected.saturating_sub(1);
494 }
495
496 pub fn select_next(&mut self) {
498 if self.editing.is_some() || self.capture.is_some() {
499 return;
500 }
501 let count = self.selectable_count();
502 if count > 0 {
503 self.selected = (self.selected + 1).min(count - 1);
504 }
505 }
506
507 pub fn begin_edit(&mut self, current: &str) {
511 if matches!(
512 self.selected_field().map(SettingField::kind),
513 Some(FieldKind::Uint | FieldKind::Text)
514 ) {
515 self.editing = Some(current.to_string());
516 }
517 }
518
519 pub fn push_edit_char(&mut self, c: char) {
523 let field = self.selected_field();
524 let uint = matches!(field.map(SettingField::kind), Some(FieldKind::Uint));
525 let limit = field.map(SettingField::edit_char_limit).unwrap_or(256);
526 if let Some(buf) = self.editing.as_mut() {
527 if uint {
528 if c.is_ascii_digit() && buf.len() < limit {
529 buf.push(c);
530 }
531 } else if !c.is_control() && buf.len() < limit {
532 buf.push(c);
533 }
534 }
535 }
536
537 pub fn pop_edit_char(&mut self) {
539 if let Some(buf) = self.editing.as_mut() {
540 buf.pop();
541 }
542 }
543
544 pub fn cancel_edit(&mut self) {
546 self.editing = None;
547 }
548
549 pub fn take_edit(&mut self) -> Option<String> {
553 self.editing.take()
554 }
555
556 pub fn field_source(&self, field: SettingField) -> Option<ConfigSource> {
561 self.rows.iter().find(|r| r.key == field.key_path()).map(|r| r.source)
562 }
563
564 pub fn begin_capture(&mut self) {
571 if self.tab != SettingsTab::Keys {
572 return;
573 }
574 if let Some(row) = self.key_rows.get(self.selected) {
575 self.capture = Some(KeyCapture {
576 row: self.selected,
577 single_only: row.target.single_only(),
578 pending: Vec::new(),
579 });
580 }
581 }
582
583 pub fn capture_push(&mut self, stroke: KeyStroke) {
585 if let Some(cap) = self.capture.as_mut() {
586 cap.pending.push(stroke);
587 }
588 }
589
590 pub fn capture_pop(&mut self) {
592 if let Some(cap) = self.capture.as_mut() {
593 cap.pending.pop();
594 }
595 }
596
597 pub fn cancel_capture(&mut self) {
599 self.capture = None;
600 }
601
602 pub fn take_capture(&mut self) -> Option<KeyCapture> {
604 self.capture.take()
605 }
606
607 pub fn scroll_down(&mut self) {
609 self.scroll = (self.scroll + 1).min(self.max_scroll);
610 }
611
612 pub fn scroll_up(&mut self) {
614 self.scroll = self.scroll.saturating_sub(1);
615 }
616
617 pub fn scroll_right(&mut self) {
619 self.x_scroll = (self.x_scroll + 1).min(self.max_x_scroll);
620 }
621
622 pub fn scroll_left(&mut self) {
624 self.x_scroll = self.x_scroll.saturating_sub(1);
625 }
626
627 pub fn scroll_to_top(&mut self) {
629 self.scroll = 0;
630 }
631
632 pub fn scroll_to_bottom(&mut self) {
634 self.scroll = self.max_scroll;
635 }
636
637 pub fn reset(&mut self) {
640 self.scroll = 0;
641 self.x_scroll = 0;
642 self.selected = 0;
643 self.editing = None;
644 self.capture = None;
645 }
646}