use super::keymap::Action;
use nucleo_matcher::{
pattern::{CaseMatching, Normalization, Pattern},
Config as NucleoConfig, Matcher, Utf32Str,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PaletteEntry {
pub action: Action,
pub name: &'static str,
pub description: &'static str,
}
pub const fn palette_entries() -> &'static [PaletteEntry] {
&[
PaletteEntry {
action: Action::Create,
name: "create",
description: "new worktree (form opens)",
},
PaletteEntry {
action: Action::DeleteConfirm,
name: "delete",
description: "delete the selected worktree (with confirm)",
},
PaletteEntry {
action: Action::Bootstrap,
name: "bootstrap",
description: "re-run bootstrap on the selected worktree",
},
PaletteEntry {
action: Action::Refresh,
name: "refresh",
description: "refresh the worktree list",
},
PaletteEntry {
action: Action::Sync,
name: "sync",
description: "sync the selected worktree onto its upstream (rebase)",
},
PaletteEntry {
action: Action::Pull,
name: "pull",
description: "pull the selected worktree's branch from its upstream",
},
PaletteEntry {
action: Action::Push,
name: "push",
description: "push the selected worktree's branch to its remote",
},
PaletteEntry {
action: Action::EditWorktree,
name: "edit-worktree",
description: "rename the selected worktree's branch",
},
PaletteEntry {
action: Action::ExitToWorktree,
name: "exit-to-worktree",
description: "quit the TUI and cd to the selected worktree path",
},
PaletteEntry {
action: Action::TerminalFullscreen,
name: "terminal-fullscreen",
description: "open a native $SHELL fullscreen (suspend TUI)",
},
PaletteEntry {
action: Action::BrowseLinks,
name: "browse-links",
description: "open the issue/PR URL browser menu",
},
PaletteEntry {
action: Action::OpenDocs,
name: "open-docs",
description: "open the gwm documentation in the browser",
},
PaletteEntry {
action: Action::LinkPrompt,
name: "link",
description: "link the selected worktree to an issue or PR",
},
PaletteEntry {
action: Action::FetchGithub,
name: "fetch-github",
description: "refresh GitHub issue/PR status via `gh`",
},
PaletteEntry {
action: Action::LazyGitFullscreen,
name: "lazygit-fullscreen",
description: "open lazygit fullscreen (suspends TUI)",
},
PaletteEntry {
action: Action::LazyGitPty,
name: "lazygit-pty",
description: "open lazygit in an embedded PTY overlay",
},
PaletteEntry {
action: Action::TerminalPty,
name: "terminal-pty",
description: "open a native $SHELL in an embedded PTY overlay",
},
PaletteEntry {
action: Action::ReviewFullscreen,
name: "review-fullscreen",
description: "run the [review] launcher fullscreen",
},
PaletteEntry {
action: Action::ReviewPty,
name: "review-pty",
description: "run the [review] launcher in an embedded PTY overlay",
},
PaletteEntry {
action: Action::YankPath,
name: "yank-path",
description: "yank the selected worktree path to the clipboard",
},
PaletteEntry {
action: Action::YankBranchName,
name: "yank-branch-name",
description: "yank the selected worktree's branch name to the clipboard",
},
PaletteEntry {
action: Action::YankWorktreeName,
name: "yank-worktree-name",
description: "yank the selected worktree's slug/name to the clipboard",
},
PaletteEntry {
action: Action::Filter,
name: "filter",
description: "open the fuzzy filter bar",
},
PaletteEntry {
action: Action::ToggleSidebar,
name: "toggle-sidebar",
description: "toggle the git preview sidebar",
},
PaletteEntry {
action: Action::ToggleSidebarMode,
name: "toggle-sidebar-mode",
description: "cycle the sidebar between commits and stashes",
},
PaletteEntry {
action: Action::CycleSidebarLayout,
name: "cycle-sidebar-layout",
description: "cycle the sidebar layout (auto / side-by-side / stacked)",
},
PaletteEntry {
action: Action::ToggleSidebarPosition,
name: "toggle-sidebar-position",
description: "toggle the sidebar position (left / right)",
},
PaletteEntry {
action: Action::ToggleDeleteBranch,
name: "toggle-delete-branch",
description: "toggle whether `delete` also drops the branch",
},
PaletteEntry {
action: Action::FocusSwap,
name: "focus-swap",
description: "swap focus between worktree list and sidebar",
},
PaletteEntry {
action: Action::FocusWorktrees,
name: "focus-worktrees",
description: "focus the worktrees pane",
},
PaletteEntry {
action: Action::FocusStatus,
name: "focus-status",
description: "focus the status pane (opens it if hidden)",
},
PaletteEntry {
action: Action::CiChecks,
name: "ci-checks",
description: "list the linked PR's CI checks (Enter opens in browser)",
},
PaletteEntry {
action: Action::WtScrollDown,
name: "wt-scroll-down",
description: "scroll the Working Tree pane down (status focus)",
},
PaletteEntry {
action: Action::WtScrollUp,
name: "wt-scroll-up",
description: "scroll the Working Tree pane up (status focus)",
},
PaletteEntry {
action: Action::Top,
name: "top",
description: "jump to the first worktree",
},
PaletteEntry {
action: Action::Bottom,
name: "bottom",
description: "jump to the last worktree",
},
PaletteEntry {
action: Action::Down,
name: "down",
description: "select the next worktree",
},
PaletteEntry {
action: Action::Up,
name: "up",
description: "select the previous worktree",
},
PaletteEntry {
action: Action::CommandLogs,
name: "command-logs",
description: "show the command logs overlay",
},
PaletteEntry {
action: Action::ConfigPanel,
name: "config-panel",
description: "show the resolved configuration panel",
},
PaletteEntry {
action: Action::ExecOverlay,
name: "exec",
description: "pick an [exec.profiles] profile and run it in a PTY",
},
PaletteEntry {
action: Action::CleanOverlay,
name: "clean",
description: "preview and reclaim build artifacts in the selected worktree",
},
PaletteEntry {
action: Action::AgentSessions,
name: "agents",
description: "show the agent sessions attached to the selected worktree",
},
PaletteEntry {
action: Action::MuxPane,
name: "mux-pane",
description: "open the selected worktree in a new multiplexer pane/tab",
},
PaletteEntry {
action: Action::Macro1,
name: "macro_one",
description: "run the user-configured [tui.macro1] command",
},
PaletteEntry {
action: Action::Macro2,
name: "macro_two",
description: "run the user-configured [tui.macro2] command",
},
PaletteEntry {
action: Action::Help,
name: "help",
description: "show the help overlay",
},
PaletteEntry {
action: Action::Quit,
name: "quit",
description: "quit the TUI",
},
PaletteEntry {
action: Action::CommandPalette,
name: "command-palette",
description: "this command palette",
},
]
}
#[derive(Debug)]
pub struct PaletteState {
pub open: bool,
buffer: String,
matches: Vec<usize>,
highlight: usize,
}
impl PaletteState {
pub fn new() -> Self {
let mut s = Self {
open: false,
buffer: String::new(),
matches: Vec::new(),
highlight: 0,
};
s.recompute_matches();
s
}
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn highlight(&self) -> usize {
self.highlight
}
pub fn matches(&self) -> Vec<&'static PaletteEntry> {
self.matches.iter().map(|&i| &palette_entries()[i]).collect()
}
pub fn open(&mut self) {
self.open = true;
self.buffer.clear();
self.highlight = 0;
self.recompute_matches();
}
pub fn close(&mut self) {
self.open = false;
self.buffer.clear();
self.highlight = 0;
self.recompute_matches();
}
pub fn push_char(&mut self, c: char) {
self.buffer.push(c);
self.recompute_matches();
self.highlight = 0;
}
pub fn pop_char(&mut self) {
self.buffer.pop();
self.recompute_matches();
self.highlight = 0;
}
pub fn cycle_highlight_down(&mut self) {
if self.matches.is_empty() {
return;
}
self.highlight = (self.highlight + 1) % self.matches.len();
}
pub fn cycle_highlight_up(&mut self) {
if self.matches.is_empty() {
return;
}
self.highlight = if self.highlight == 0 {
self.matches.len() - 1
} else {
self.highlight - 1
};
}
pub fn accept(&mut self) -> Option<Action> {
if self.matches.is_empty() {
return None;
}
let idx = *self.matches.get(self.highlight)?;
let action = palette_entries()[idx].action;
self.close();
Some(action)
}
fn recompute_matches(&mut self) {
let registry = palette_entries();
if self.buffer.is_empty() {
self.matches = (0..registry.len()).collect();
return;
}
let pattern = Pattern::parse(&self.buffer, CaseMatching::Smart, Normalization::Smart);
let mut matcher = Matcher::new(NucleoConfig::DEFAULT);
let mut buf: Vec<char> = Vec::new();
let mut scored: Vec<(u32, usize)> = Vec::with_capacity(registry.len());
for (i, entry) in registry.iter().enumerate() {
let hay = Utf32Str::new(entry.name, &mut buf);
if let Some(score) = pattern.score(hay, &mut matcher) {
scored.push((score, i));
}
}
scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
self.matches = scored.into_iter().map(|(_, i)| i).collect();
}
}
impl Default for PaletteState {
fn default() -> Self {
Self::new()
}
}