|
1 | 1 | export * as ConfigKeybinds from "./keybinds" |
2 | 2 |
|
3 | | -import z from "zod" |
| 3 | +import { Effect, Schema } from "effect" |
| 4 | +import type z from "zod" |
| 5 | +import { zod } from "@/util/effect-zod" |
4 | 6 |
|
5 | | -export const Keybinds = z |
6 | | - .object({ |
7 | | - leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), |
8 | | - app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"), |
9 | | - editor_open: z.string().optional().default("<leader>e").describe("Open external editor"), |
10 | | - theme_list: z.string().optional().default("<leader>t").describe("List available themes"), |
11 | | - sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"), |
12 | | - scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), |
13 | | - username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), |
14 | | - status_view: z.string().optional().default("<leader>s").describe("View status"), |
15 | | - session_export: z.string().optional().default("<leader>x").describe("Export session to editor"), |
16 | | - session_new: z.string().optional().default("<leader>n").describe("Create a new session"), |
17 | | - session_list: z.string().optional().default("<leader>l").describe("List all sessions"), |
18 | | - session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"), |
19 | | - session_fork: z.string().optional().default("none").describe("Fork session from message"), |
20 | | - session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), |
21 | | - session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), |
22 | | - stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), |
23 | | - model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), |
24 | | - model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), |
25 | | - session_share: z.string().optional().default("none").describe("Share current session"), |
26 | | - session_unshare: z.string().optional().default("none").describe("Unshare current session"), |
27 | | - session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), |
28 | | - session_compact: z.string().optional().default("<leader>c").describe("Compact the session"), |
29 | | - messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), |
30 | | - messages_page_down: z |
31 | | - .string() |
32 | | - .optional() |
33 | | - .default("pagedown,ctrl+alt+f") |
34 | | - .describe("Scroll messages down by one page"), |
35 | | - messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), |
36 | | - messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), |
37 | | - messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), |
38 | | - messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"), |
39 | | - messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), |
40 | | - messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), |
41 | | - messages_next: z.string().optional().default("none").describe("Navigate to next message"), |
42 | | - messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), |
43 | | - messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), |
44 | | - messages_copy: z.string().optional().default("<leader>y").describe("Copy message"), |
45 | | - messages_undo: z.string().optional().default("<leader>u").describe("Undo message"), |
46 | | - messages_redo: z.string().optional().default("<leader>r").describe("Redo message"), |
47 | | - messages_toggle_conceal: z |
48 | | - .string() |
49 | | - .optional() |
50 | | - .default("<leader>h") |
51 | | - .describe("Toggle code block concealment in messages"), |
52 | | - tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), |
53 | | - model_list: z.string().optional().default("<leader>m").describe("List available models"), |
54 | | - model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), |
55 | | - model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), |
56 | | - model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), |
57 | | - model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), |
58 | | - command_list: z.string().optional().default("ctrl+p").describe("List available commands"), |
59 | | - agent_list: z.string().optional().default("<leader>a").describe("List agents"), |
60 | | - agent_cycle: z.string().optional().default("tab").describe("Next agent"), |
61 | | - agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), |
62 | | - variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), |
63 | | - variant_list: z.string().optional().default("none").describe("List model variants"), |
64 | | - input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), |
65 | | - input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), |
66 | | - input_submit: z.string().optional().default("return").describe("Submit input"), |
67 | | - input_newline: z |
68 | | - .string() |
69 | | - .optional() |
70 | | - .default("shift+return,ctrl+return,alt+return,ctrl+j") |
71 | | - .describe("Insert newline in input"), |
72 | | - input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), |
73 | | - input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), |
74 | | - input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), |
75 | | - input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), |
76 | | - input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), |
77 | | - input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), |
78 | | - input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), |
79 | | - input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), |
80 | | - input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), |
81 | | - input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), |
82 | | - input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"), |
83 | | - input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), |
84 | | - input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), |
85 | | - input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), |
86 | | - input_select_visual_line_home: z |
87 | | - .string() |
88 | | - .optional() |
89 | | - .default("alt+shift+a") |
90 | | - .describe("Select to start of visual line in input"), |
91 | | - input_select_visual_line_end: z |
92 | | - .string() |
93 | | - .optional() |
94 | | - .default("alt+shift+e") |
95 | | - .describe("Select to end of visual line in input"), |
96 | | - input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), |
97 | | - input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), |
98 | | - input_select_buffer_home: z |
99 | | - .string() |
100 | | - .optional() |
101 | | - .default("shift+home") |
102 | | - .describe("Select to start of buffer in input"), |
103 | | - input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), |
104 | | - input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), |
105 | | - input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), |
106 | | - input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), |
107 | | - input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), |
108 | | - input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), |
109 | | - input_undo: z |
110 | | - .string() |
111 | | - .optional() |
112 | | - // On Windows prepend ctrl+z since terminal_suspend releases the binding. |
113 | | - .default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z") |
114 | | - .describe("Undo in input"), |
115 | | - input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), |
116 | | - input_word_forward: z |
117 | | - .string() |
118 | | - .optional() |
119 | | - .default("alt+f,alt+right,ctrl+right") |
120 | | - .describe("Move word forward in input"), |
121 | | - input_word_backward: z |
122 | | - .string() |
123 | | - .optional() |
124 | | - .default("alt+b,alt+left,ctrl+left") |
125 | | - .describe("Move word backward in input"), |
126 | | - input_select_word_forward: z |
127 | | - .string() |
128 | | - .optional() |
129 | | - .default("alt+shift+f,alt+shift+right") |
130 | | - .describe("Select word forward in input"), |
131 | | - input_select_word_backward: z |
132 | | - .string() |
133 | | - .optional() |
134 | | - .default("alt+shift+b,alt+shift+left") |
135 | | - .describe("Select word backward in input"), |
136 | | - input_delete_word_forward: z |
137 | | - .string() |
138 | | - .optional() |
139 | | - .default("alt+d,alt+delete,ctrl+delete") |
140 | | - .describe("Delete word forward in input"), |
141 | | - input_delete_word_backward: z |
142 | | - .string() |
143 | | - .optional() |
144 | | - .default("ctrl+w,ctrl+backspace,alt+backspace") |
145 | | - .describe("Delete word backward in input"), |
146 | | - history_previous: z.string().optional().default("up").describe("Previous history item"), |
147 | | - history_next: z.string().optional().default("down").describe("Next history item"), |
148 | | - session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"), |
149 | | - session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), |
150 | | - session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), |
151 | | - session_parent: z.string().optional().default("up").describe("Go to parent session"), |
152 | | - terminal_suspend: z |
153 | | - .string() |
154 | | - .optional() |
155 | | - .default("ctrl+z") |
156 | | - .transform((v) => (process.platform === "win32" ? "none" : v)) |
157 | | - .describe("Suspend terminal"), |
158 | | - terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), |
159 | | - tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"), |
160 | | - plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), |
161 | | - display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), |
162 | | - }) |
163 | | - .strict() |
164 | | - .meta({ |
165 | | - ref: "KeybindsConfig", |
166 | | - }) |
| 7 | +// Every keybind field has the same shape: an optional string with a default |
| 8 | +// binding and a human description. `keybind()` keeps the declaration list |
| 9 | +// below dense and readable. |
| 10 | +const keybind = (value: string, description: string) => |
| 11 | + Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(value))).annotate({ description }) |
| 12 | + |
| 13 | +// Windows prepends ctrl+z to the undo binding because `terminal_suspend` |
| 14 | +// cannot consume ctrl+z on native Windows terminals (no POSIX suspend). |
| 15 | +const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z" |
| 16 | + |
| 17 | +const KeybindsSchema = Schema.Struct({ |
| 18 | + leader: keybind("ctrl+x", "Leader key for keybind combinations"), |
| 19 | + app_exit: keybind("ctrl+c,ctrl+d,<leader>q", "Exit the application"), |
| 20 | + editor_open: keybind("<leader>e", "Open external editor"), |
| 21 | + theme_list: keybind("<leader>t", "List available themes"), |
| 22 | + sidebar_toggle: keybind("<leader>b", "Toggle sidebar"), |
| 23 | + scrollbar_toggle: keybind("none", "Toggle session scrollbar"), |
| 24 | + username_toggle: keybind("none", "Toggle username visibility"), |
| 25 | + status_view: keybind("<leader>s", "View status"), |
| 26 | + session_export: keybind("<leader>x", "Export session to editor"), |
| 27 | + session_new: keybind("<leader>n", "Create a new session"), |
| 28 | + session_list: keybind("<leader>l", "List all sessions"), |
| 29 | + session_timeline: keybind("<leader>g", "Show session timeline"), |
| 30 | + session_fork: keybind("none", "Fork session from message"), |
| 31 | + session_rename: keybind("ctrl+r", "Rename session"), |
| 32 | + session_delete: keybind("ctrl+d", "Delete session"), |
| 33 | + stash_delete: keybind("ctrl+d", "Delete stash entry"), |
| 34 | + model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"), |
| 35 | + model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"), |
| 36 | + session_share: keybind("none", "Share current session"), |
| 37 | + session_unshare: keybind("none", "Unshare current session"), |
| 38 | + session_interrupt: keybind("escape", "Interrupt current session"), |
| 39 | + session_compact: keybind("<leader>c", "Compact the session"), |
| 40 | + messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"), |
| 41 | + messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"), |
| 42 | + messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"), |
| 43 | + messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"), |
| 44 | + messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"), |
| 45 | + messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"), |
| 46 | + messages_first: keybind("ctrl+g,home", "Navigate to first message"), |
| 47 | + messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"), |
| 48 | + messages_next: keybind("none", "Navigate to next message"), |
| 49 | + messages_previous: keybind("none", "Navigate to previous message"), |
| 50 | + messages_last_user: keybind("none", "Navigate to last user message"), |
| 51 | + messages_copy: keybind("<leader>y", "Copy message"), |
| 52 | + messages_undo: keybind("<leader>u", "Undo message"), |
| 53 | + messages_redo: keybind("<leader>r", "Redo message"), |
| 54 | + messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"), |
| 55 | + tool_details: keybind("none", "Toggle tool details visibility"), |
| 56 | + model_list: keybind("<leader>m", "List available models"), |
| 57 | + model_cycle_recent: keybind("f2", "Next recently used model"), |
| 58 | + model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"), |
| 59 | + model_cycle_favorite: keybind("none", "Next favorite model"), |
| 60 | + model_cycle_favorite_reverse: keybind("none", "Previous favorite model"), |
| 61 | + command_list: keybind("ctrl+p", "List available commands"), |
| 62 | + agent_list: keybind("<leader>a", "List agents"), |
| 63 | + agent_cycle: keybind("tab", "Next agent"), |
| 64 | + agent_cycle_reverse: keybind("shift+tab", "Previous agent"), |
| 65 | + variant_cycle: keybind("ctrl+t", "Cycle model variants"), |
| 66 | + variant_list: keybind("none", "List model variants"), |
| 67 | + input_clear: keybind("ctrl+c", "Clear input field"), |
| 68 | + input_paste: keybind("ctrl+v", "Paste from clipboard"), |
| 69 | + input_submit: keybind("return", "Submit input"), |
| 70 | + input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"), |
| 71 | + input_move_left: keybind("left,ctrl+b", "Move cursor left in input"), |
| 72 | + input_move_right: keybind("right,ctrl+f", "Move cursor right in input"), |
| 73 | + input_move_up: keybind("up", "Move cursor up in input"), |
| 74 | + input_move_down: keybind("down", "Move cursor down in input"), |
| 75 | + input_select_left: keybind("shift+left", "Select left in input"), |
| 76 | + input_select_right: keybind("shift+right", "Select right in input"), |
| 77 | + input_select_up: keybind("shift+up", "Select up in input"), |
| 78 | + input_select_down: keybind("shift+down", "Select down in input"), |
| 79 | + input_line_home: keybind("ctrl+a", "Move to start of line in input"), |
| 80 | + input_line_end: keybind("ctrl+e", "Move to end of line in input"), |
| 81 | + input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"), |
| 82 | + input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"), |
| 83 | + input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"), |
| 84 | + input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"), |
| 85 | + input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"), |
| 86 | + input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"), |
| 87 | + input_buffer_home: keybind("home", "Move to start of buffer in input"), |
| 88 | + input_buffer_end: keybind("end", "Move to end of buffer in input"), |
| 89 | + input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"), |
| 90 | + input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"), |
| 91 | + input_delete_line: keybind("ctrl+shift+d", "Delete line in input"), |
| 92 | + input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"), |
| 93 | + input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"), |
| 94 | + input_backspace: keybind("backspace,shift+backspace", "Backspace in input"), |
| 95 | + input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"), |
| 96 | + input_undo: keybind(inputUndoDefault, "Undo in input"), |
| 97 | + input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"), |
| 98 | + input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"), |
| 99 | + input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"), |
| 100 | + input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"), |
| 101 | + input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"), |
| 102 | + input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"), |
| 103 | + input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"), |
| 104 | + history_previous: keybind("up", "Previous history item"), |
| 105 | + history_next: keybind("down", "Next history item"), |
| 106 | + session_child_first: keybind("<leader>down", "Go to first child session"), |
| 107 | + session_child_cycle: keybind("right", "Go to next child session"), |
| 108 | + session_child_cycle_reverse: keybind("left", "Go to previous child session"), |
| 109 | + session_parent: keybind("up", "Go to parent session"), |
| 110 | + // `terminal_suspend` was formerly `.default("ctrl+z").transform((v) => win32 ? "none" : v)`, |
| 111 | + // but `tui.ts` already forces the binding to "none" on win32 before calling |
| 112 | + // `Keybinds.parse(...)`, so the schema-level transform was redundant. |
| 113 | + terminal_suspend: keybind("ctrl+z", "Suspend terminal"), |
| 114 | + terminal_title_toggle: keybind("none", "Toggle terminal title"), |
| 115 | + tips_toggle: keybind("<leader>h", "Toggle tips on home screen"), |
| 116 | + plugin_manager: keybind("none", "Open plugin manager dialog"), |
| 117 | + display_thinking: keybind("none", "Toggle thinking blocks visibility"), |
| 118 | +}).annotate({ identifier: "KeybindsConfig" }) |
| 119 | + |
| 120 | +export type Keybinds = Schema.Schema.Type<typeof KeybindsSchema> |
| 121 | + |
| 122 | +// Consumers access `Keybinds.shape` and `Keybinds.shape.X.parse(undefined)`, |
| 123 | +// which requires the runtime type to be a ZodObject, not just ZodType. Every |
| 124 | +// field is `string().optional().default(...)` at runtime, so widen to that. |
| 125 | +export const Keybinds = zod(KeybindsSchema) as unknown as z.ZodObject< |
| 126 | + Record<keyof Keybinds, z.ZodDefault<z.ZodOptional<z.ZodString>>> |
| 127 | +> |
0 commit comments