@arthony/keybook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/cli.js +413 -0
- package/package.json +54 -0
- package/seed/claude.yaml +21 -0
- package/seed/edge.yaml +39 -0
- package/seed/finder.yaml +52 -0
- package/seed/fork.yaml +27 -0
- package/seed/macos.yaml +50 -0
- package/seed/nano.yaml +51 -0
- package/seed/terminal.yaml +71 -0
- package/seed/text-editing.yaml +44 -0
- package/seed/tmux.yaml +61 -0
- package/seed/vim.yaml +66 -0
- package/seed/vscode.yaml +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ariyapong W.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# keybook
|
|
2
|
+
|
|
3
|
+
A self-contained, open-source **macOS TUI** for searching the keyboard
|
|
4
|
+
shortcuts and short recipes of your most-used apps. Launch `keybook`,
|
|
5
|
+
fuzzy-search by intent ("finder new tab", "terminal here"), and learn your
|
|
6
|
+
shortcuts along the way.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @arthony/keybook
|
|
12
|
+
# or run without installing:
|
|
13
|
+
npx @arthony/keybook
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node 22+ and macOS. The package is published under the `@arthony`
|
|
17
|
+
scope, but the commands it installs are still `keybook` (and the `kb` alias).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
keybook # launch the TUI (alias: kb)
|
|
23
|
+
keybook path # print the data directory
|
|
24
|
+
keybook edit # open the data directory in $EDITOR
|
|
25
|
+
keybook check # validate your data files
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In the TUI: type to fuzzy-search, ↑/↓ to move, ⏎ to copy the shortcut (or a
|
|
29
|
+
recipe's command) to the clipboard, ⎋ to quit.
|
|
30
|
+
|
|
31
|
+
## Your data
|
|
32
|
+
|
|
33
|
+
Shortcuts live in plain YAML files you own — by default `~/.config/keybook/`
|
|
34
|
+
(override with `$KEYBOOK_DATA_DIR`, e.g. point it at a synced/git folder to
|
|
35
|
+
share across machines). On first run the directory is seeded from a bundled
|
|
36
|
+
starter set; your edits are never overwritten.
|
|
37
|
+
|
|
38
|
+
Each entry is a key combo **or** a short recipe:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
app: Finder
|
|
42
|
+
entries:
|
|
43
|
+
- action: Open a new tab
|
|
44
|
+
keys: "⌘T"
|
|
45
|
+
tags: [new tab]
|
|
46
|
+
- action: Open Terminal at the current folder
|
|
47
|
+
steps: [Right-click the folder, Services → "New Terminal at Folder"]
|
|
48
|
+
tags: [terminal here]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Add entries by hand, or ask an AI assistant to append them and run
|
|
52
|
+
`keybook check` before committing.
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
Requires Node 22+ and pnpm. One-shot setup after cloning:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/Ariyapong/keybook.git
|
|
60
|
+
cd keybook
|
|
61
|
+
./scripts/dev-setup.sh # checks Node, enables pnpm, installs, runs tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or manually:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
corepack enable # enables pnpm (or: npm i -g pnpm)
|
|
68
|
+
pnpm install
|
|
69
|
+
pnpm test # run the suite
|
|
70
|
+
pnpm build # bundle to dist/cli.js
|
|
71
|
+
pnpm dev # run the TUI from source (tsx)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Also available: `pnpm typecheck`, `pnpm lint`, `pnpm format`.
|
|
75
|
+
|
|
76
|
+
**Layout:** `src/data` (zod schema + YAML loader), `src/config.ts` (data dir +
|
|
77
|
+
first-run seeding), `src/search.ts` (fzf), `src/tui` (Ink components),
|
|
78
|
+
`src/cli.ts` (commander entry), `seed/` (bundled starter data). Design docs are
|
|
79
|
+
in `docs/superpowers/`.
|
|
80
|
+
|
|
81
|
+
If `pnpm lint` reports a missing Biome binary, run `pnpm approve-builds`
|
|
82
|
+
(pnpm skips package build scripts by default).
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { render } from "ink";
|
|
7
|
+
import { createElement } from "react";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import { cpSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var YAML_RE = /\.ya?ml$/;
|
|
15
|
+
function resolveDataDir(env = process.env) {
|
|
16
|
+
if (env.KEYBOOK_DATA_DIR) return { dir: env.KEYBOOK_DATA_DIR, source: "env" };
|
|
17
|
+
if (env.XDG_CONFIG_HOME) return { dir: join(env.XDG_CONFIG_HOME, "keybook"), source: "xdg" };
|
|
18
|
+
return { dir: join(homedir(), ".config", "keybook"), source: "default" };
|
|
19
|
+
}
|
|
20
|
+
function seedDir() {
|
|
21
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "seed");
|
|
22
|
+
}
|
|
23
|
+
function ensureDataDir(dir, seed = seedDir()) {
|
|
24
|
+
const hasYaml = existsSync(dir) && readdirSync(dir).some((f) => YAML_RE.test(f));
|
|
25
|
+
if (hasYaml) return { initialized: false, fileCount: 0 };
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
const seedFiles = readdirSync(seed).filter((f) => YAML_RE.test(f));
|
|
28
|
+
for (const f of seedFiles) cpSync(join(seed, f), join(dir, f));
|
|
29
|
+
return { initialized: true, fileCount: seedFiles.length };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/data/loader.ts
|
|
33
|
+
import { readFileSync, readdirSync as readdirSync2 } from "fs";
|
|
34
|
+
import { join as join2 } from "path";
|
|
35
|
+
import { parse as parseYaml } from "yaml";
|
|
36
|
+
|
|
37
|
+
// src/data/schema.ts
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
var entrySchema = z.object({
|
|
40
|
+
action: z.string().min(1),
|
|
41
|
+
keys: z.string().min(1).optional(),
|
|
42
|
+
steps: z.array(z.string().min(1)).min(1).optional(),
|
|
43
|
+
command: z.string().min(1).optional(),
|
|
44
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
45
|
+
notes: z.string().min(1).optional(),
|
|
46
|
+
source: z.string().min(1).optional()
|
|
47
|
+
}).strict().refine((e) => Boolean(e.keys) || Boolean(e.steps) || Boolean(e.command), {
|
|
48
|
+
message: "entry must have at least one of: keys, steps, command"
|
|
49
|
+
});
|
|
50
|
+
var fileShape = z.object({
|
|
51
|
+
app: z.string().min(1),
|
|
52
|
+
entries: z.array(z.unknown()).min(1)
|
|
53
|
+
}).strict();
|
|
54
|
+
|
|
55
|
+
// src/data/loader.ts
|
|
56
|
+
var YAML_RE2 = /\.ya?ml$/;
|
|
57
|
+
function loadEntries(dataDir) {
|
|
58
|
+
const entries = [];
|
|
59
|
+
const errors = [];
|
|
60
|
+
let files;
|
|
61
|
+
try {
|
|
62
|
+
files = readdirSync2(dataDir).filter((f) => YAML_RE2.test(f)).sort();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
errors.push({
|
|
65
|
+
file: dataDir,
|
|
66
|
+
entryIndex: null,
|
|
67
|
+
message: `cannot read data dir: ${err.message}`
|
|
68
|
+
});
|
|
69
|
+
return { entries, errors };
|
|
70
|
+
}
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = parseYaml(readFileSync(join2(dataDir, file), "utf8"));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
errors.push({
|
|
77
|
+
file,
|
|
78
|
+
entryIndex: null,
|
|
79
|
+
message: `YAML parse error: ${err.message}`
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const shape = fileShape.safeParse(raw);
|
|
84
|
+
if (!shape.success) {
|
|
85
|
+
errors.push({
|
|
86
|
+
file,
|
|
87
|
+
entryIndex: null,
|
|
88
|
+
message: shape.error.issues[0]?.message ?? "invalid file shape"
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const { app, entries: rawEntries } = shape.data;
|
|
93
|
+
rawEntries.forEach((rawEntry, i) => {
|
|
94
|
+
const parsed = entrySchema.safeParse(rawEntry);
|
|
95
|
+
if (!parsed.success) {
|
|
96
|
+
errors.push({
|
|
97
|
+
file,
|
|
98
|
+
entryIndex: i,
|
|
99
|
+
message: parsed.error.issues.map((s) => s.message).join("; ")
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
entries.push({ app, ...parsed.data });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { entries, errors };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/commands.ts
|
|
110
|
+
function runPath(env = process.env) {
|
|
111
|
+
const { dir, source } = resolveDataDir(env);
|
|
112
|
+
return `${dir} (source: ${source})`;
|
|
113
|
+
}
|
|
114
|
+
function runCheck(dir) {
|
|
115
|
+
const { entries, errors } = loadEntries(dir);
|
|
116
|
+
if (errors.length === 0) return { ok: true, lines: [`\u2713 ${entries.length} entries OK`] };
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
lines: errors.map(
|
|
120
|
+
(e) => `\u2717 ${e.file}${e.entryIndex !== null ? ` [entry ${e.entryIndex}]` : ""}: ${e.message}`
|
|
121
|
+
)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/tui/App.tsx
|
|
126
|
+
import { Box as Box6, useApp, useInput, useStdout } from "ink";
|
|
127
|
+
import { useMemo, useState } from "react";
|
|
128
|
+
|
|
129
|
+
// src/clipboard.ts
|
|
130
|
+
import { spawnSync } from "child_process";
|
|
131
|
+
function copyToClipboard(text) {
|
|
132
|
+
try {
|
|
133
|
+
const res = spawnSync("pbcopy", { input: text });
|
|
134
|
+
return res.status === 0;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/search.ts
|
|
141
|
+
import { Fzf, extendedMatch } from "fzf";
|
|
142
|
+
function haystack(e) {
|
|
143
|
+
return [e.action, (e.tags ?? []).join(" "), e.app, e.keys ?? "", e.notes ?? ""].filter(Boolean).join(" ");
|
|
144
|
+
}
|
|
145
|
+
function search(entries, query) {
|
|
146
|
+
const q = query.trim();
|
|
147
|
+
if (!q) {
|
|
148
|
+
return [...entries].sort(
|
|
149
|
+
(a, b) => a.app.localeCompare(b.app) || a.action.localeCompare(b.action)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const fzf = new Fzf(entries, { selector: haystack, match: extendedMatch });
|
|
153
|
+
return fzf.find(q).map((r) => r.item);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/tui/Footer.tsx
|
|
157
|
+
import { Box, Text } from "ink";
|
|
158
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
159
|
+
function Footer({
|
|
160
|
+
flash,
|
|
161
|
+
errorCount,
|
|
162
|
+
resultCount
|
|
163
|
+
}) {
|
|
164
|
+
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [
|
|
165
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
166
|
+
"\u2191\u2193 move \u23CE copy \u238B quit (",
|
|
167
|
+
resultCount,
|
|
168
|
+
")"
|
|
169
|
+
] }),
|
|
170
|
+
flash ? /* @__PURE__ */ jsx(Text, { color: "green", children: flash }) : errorCount > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
171
|
+
"\u26A0 ",
|
|
172
|
+
errorCount,
|
|
173
|
+
" skipped \u2014 run `keybook check`"
|
|
174
|
+
] }) : /* @__PURE__ */ jsx(Text, { children: " " })
|
|
175
|
+
] });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/tui/PreviewPane.tsx
|
|
179
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
180
|
+
|
|
181
|
+
// src/tui/keycaps.ts
|
|
182
|
+
var GLYPHS = /* @__PURE__ */ new Set(["\u2318", "\u2325", "\u2303", "\u21E7", "\u21EA", "\u23CE", "\u238B", "\u232B", "\u21E5", "\u2423", "\u2191", "\u2193", "\u2190", "\u2192"]);
|
|
183
|
+
function parseSegment(seg) {
|
|
184
|
+
const tokens = [];
|
|
185
|
+
let buf = "";
|
|
186
|
+
for (const ch of seg) {
|
|
187
|
+
if (GLYPHS.has(ch)) {
|
|
188
|
+
if (buf) {
|
|
189
|
+
tokens.push(buf);
|
|
190
|
+
buf = "";
|
|
191
|
+
}
|
|
192
|
+
tokens.push(ch);
|
|
193
|
+
} else if (ch === " ") {
|
|
194
|
+
if (buf) {
|
|
195
|
+
tokens.push(buf);
|
|
196
|
+
buf = "";
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
buf += ch;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (buf) tokens.push(buf);
|
|
203
|
+
return tokens.length ? tokens : [seg];
|
|
204
|
+
}
|
|
205
|
+
function parseKeys(input) {
|
|
206
|
+
return input.split(",").map((s) => s.trim()).filter(Boolean).map(parseSegment);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/tui/PreviewPane.tsx
|
|
210
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
211
|
+
function KeyCaps({ value }) {
|
|
212
|
+
const chords = parseKeys(value);
|
|
213
|
+
return /* @__PURE__ */ jsx2(Box2, { children: chords.map((seg, si) => (
|
|
214
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: token order is stable; tokens can repeat (e.g. ⌃X⌃E) so the value is not a unique key
|
|
215
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
216
|
+
si > 0 ? /* @__PURE__ */ jsx2(Text2, { children: " , " }) : null,
|
|
217
|
+
seg.map((tok, ti) => (
|
|
218
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: see above — repeated tokens make the value non-unique
|
|
219
|
+
/* @__PURE__ */ jsx2(Box2, { marginRight: 1, borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: tok }) }, ti)
|
|
220
|
+
))
|
|
221
|
+
] }, si)
|
|
222
|
+
)) });
|
|
223
|
+
}
|
|
224
|
+
function PreviewPane({ entry, width = "50%" }) {
|
|
225
|
+
if (!entry) return null;
|
|
226
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, paddingLeft: 2, children: [
|
|
227
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: entry.app }),
|
|
228
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: entry.action }),
|
|
229
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
230
|
+
entry.keys ? /* @__PURE__ */ jsx2(KeyCaps, { value: entry.keys }) : null,
|
|
231
|
+
entry.steps?.map((s, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
232
|
+
i + 1,
|
|
233
|
+
". ",
|
|
234
|
+
s
|
|
235
|
+
] }, s)),
|
|
236
|
+
entry.command ? /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
237
|
+
"$ ",
|
|
238
|
+
entry.command
|
|
239
|
+
] }) : null
|
|
240
|
+
] }),
|
|
241
|
+
entry.tags?.length ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
242
|
+
"tags: ",
|
|
243
|
+
entry.tags.join(", ")
|
|
244
|
+
] }) : null,
|
|
245
|
+
entry.notes ? /* @__PURE__ */ jsx2(Text2, { color: "gray", children: entry.notes }) : null
|
|
246
|
+
] });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/tui/ResultList.tsx
|
|
250
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
251
|
+
|
|
252
|
+
// src/tui/ResultRow.tsx
|
|
253
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
254
|
+
import { jsxs as jsxs3 } from "react/jsx-runtime";
|
|
255
|
+
function ResultRow({ entry, selected }) {
|
|
256
|
+
const right = entry.keys ?? "recipe";
|
|
257
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
258
|
+
/* @__PURE__ */ jsxs3(Text3, { color: selected ? "cyan" : void 0, inverse: selected, children: [
|
|
259
|
+
selected ? "\u25B8 " : " ",
|
|
260
|
+
entry.app,
|
|
261
|
+
" \xB7 ",
|
|
262
|
+
entry.action
|
|
263
|
+
] }),
|
|
264
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
265
|
+
" ",
|
|
266
|
+
right
|
|
267
|
+
] })
|
|
268
|
+
] });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/tui/ResultList.tsx
|
|
272
|
+
import { jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
273
|
+
function ResultList({
|
|
274
|
+
results,
|
|
275
|
+
selected,
|
|
276
|
+
query,
|
|
277
|
+
height = 12,
|
|
278
|
+
width = "50%"
|
|
279
|
+
}) {
|
|
280
|
+
if (results.length === 0) {
|
|
281
|
+
return /* @__PURE__ */ jsx3(Box4, { width, justifyContent: "center", children: /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
|
|
282
|
+
'No matches for "',
|
|
283
|
+
query,
|
|
284
|
+
'"'
|
|
285
|
+
] }) });
|
|
286
|
+
}
|
|
287
|
+
const start = Math.max(0, Math.min(selected - Math.floor(height / 2), results.length - height));
|
|
288
|
+
const visible = results.slice(Math.max(0, start), Math.max(0, start) + height);
|
|
289
|
+
return /* @__PURE__ */ jsx3(Box4, { flexDirection: "column", width, children: visible.map((e, i) => /* @__PURE__ */ jsx3(
|
|
290
|
+
ResultRow,
|
|
291
|
+
{
|
|
292
|
+
entry: e,
|
|
293
|
+
selected: Math.max(0, start) + i === selected
|
|
294
|
+
},
|
|
295
|
+
`${e.app}:${e.action}`
|
|
296
|
+
)) });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/tui/SearchInput.tsx
|
|
300
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
301
|
+
import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
302
|
+
function SearchInput({ query }) {
|
|
303
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
304
|
+
/* @__PURE__ */ jsx4(Text5, { color: "cyan", children: "search: " }),
|
|
305
|
+
/* @__PURE__ */ jsx4(Text5, { children: query }),
|
|
306
|
+
/* @__PURE__ */ jsx4(Text5, { inverse: true, children: " " })
|
|
307
|
+
] });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/tui/layout.ts
|
|
311
|
+
var RESERVED_ROWS = 4;
|
|
312
|
+
function visibleListHeight(terminalRows) {
|
|
313
|
+
const rows = terminalRows && terminalRows > 0 ? terminalRows : 24;
|
|
314
|
+
return Math.max(1, rows - RESERVED_ROWS);
|
|
315
|
+
}
|
|
316
|
+
function columnWidths(terminalCols) {
|
|
317
|
+
const cols = terminalCols && terminalCols > 0 ? terminalCols : 80;
|
|
318
|
+
const left = Math.floor(cols / 2);
|
|
319
|
+
return { left, right: cols - left };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/tui/App.tsx
|
|
323
|
+
import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
324
|
+
function App({ entries, errorCount = 0, onCopy = copyToClipboard }) {
|
|
325
|
+
const { exit } = useApp();
|
|
326
|
+
const { stdout } = useStdout();
|
|
327
|
+
const [query, setQuery] = useState("");
|
|
328
|
+
const [selected, setSelected] = useState(0);
|
|
329
|
+
const [flash, setFlash] = useState("");
|
|
330
|
+
const results = useMemo(() => search(entries, query), [entries, query]);
|
|
331
|
+
const sel = results.length ? Math.min(selected, results.length - 1) : 0;
|
|
332
|
+
const current = results[sel];
|
|
333
|
+
const listHeight = visibleListHeight(stdout?.rows);
|
|
334
|
+
const { left, right } = columnWidths(stdout?.columns);
|
|
335
|
+
useInput((input, key) => {
|
|
336
|
+
if (!key.return && flash) setFlash("");
|
|
337
|
+
if (key.escape || key.ctrl && input === "c") {
|
|
338
|
+
exit();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (key.downArrow || key.ctrl && input === "n") {
|
|
342
|
+
setSelected((s) => Math.min(s + 1, results.length - 1));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (key.upArrow || key.ctrl && input === "p") {
|
|
346
|
+
setSelected((s) => Math.max(s - 1, 0));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (key.return) {
|
|
350
|
+
if (current) {
|
|
351
|
+
const text = current.keys ?? current.command ?? `${current.app}: ${current.action}`;
|
|
352
|
+
setFlash(onCopy(text) ? "\u2713 copied!" : "\u2717 copy failed");
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (key.backspace || key.delete) {
|
|
357
|
+
setQuery((q) => q.slice(0, -1));
|
|
358
|
+
setSelected(0);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (input && !key.ctrl && !key.meta) {
|
|
362
|
+
setQuery((q) => q + input);
|
|
363
|
+
setSelected(0);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
367
|
+
/* @__PURE__ */ jsx5(SearchInput, { query }),
|
|
368
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
369
|
+
/* @__PURE__ */ jsx5(
|
|
370
|
+
ResultList,
|
|
371
|
+
{
|
|
372
|
+
results,
|
|
373
|
+
selected: sel,
|
|
374
|
+
query,
|
|
375
|
+
height: listHeight,
|
|
376
|
+
width: left
|
|
377
|
+
}
|
|
378
|
+
),
|
|
379
|
+
/* @__PURE__ */ jsx5(PreviewPane, { entry: current, width: right })
|
|
380
|
+
] }),
|
|
381
|
+
/* @__PURE__ */ jsx5(Footer, { flash, errorCount, resultCount: results.length })
|
|
382
|
+
] });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/cli.ts
|
|
386
|
+
var program = new Command();
|
|
387
|
+
program.name("keybook").description("Search keyboard shortcuts & recipes for your apps").version("0.1.0");
|
|
388
|
+
program.command("path").description("Print the data directory").action(() => {
|
|
389
|
+
console.log(runPath());
|
|
390
|
+
});
|
|
391
|
+
program.command("edit").description("Open the data directory in $EDITOR").action(() => {
|
|
392
|
+
const { dir } = resolveDataDir();
|
|
393
|
+
ensureDataDir(dir);
|
|
394
|
+
const editor = process.env.EDITOR || "code";
|
|
395
|
+
const child = spawn(editor, [dir], { stdio: "inherit" });
|
|
396
|
+
child.on("error", () => spawn("open", [dir], { stdio: "inherit" }));
|
|
397
|
+
});
|
|
398
|
+
program.command("check").description("Validate all data files").action(() => {
|
|
399
|
+
const { dir } = resolveDataDir();
|
|
400
|
+
ensureDataDir(dir);
|
|
401
|
+
const result = runCheck(dir);
|
|
402
|
+
for (const line of result.lines) (result.ok ? console.log : console.error)(line);
|
|
403
|
+
process.exit(result.ok ? 0 : 1);
|
|
404
|
+
});
|
|
405
|
+
program.action(() => {
|
|
406
|
+
const { dir } = resolveDataDir();
|
|
407
|
+
const init = ensureDataDir(dir);
|
|
408
|
+
if (init.initialized) console.log(`Initialized ${dir} from seed (${init.fileCount} files).`);
|
|
409
|
+
const { entries, errors } = loadEntries(dir);
|
|
410
|
+
for (const e of errors) console.error(`\u26A0 ${e.file}: ${e.message}`);
|
|
411
|
+
render(createElement(App, { entries, errorCount: errors.length }));
|
|
412
|
+
});
|
|
413
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arthony/keybook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"description": "A macOS TUI for searching keyboard shortcuts and recipes of your favorite apps",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"ink",
|
|
10
|
+
"keyboard-shortcuts",
|
|
11
|
+
"shortcuts",
|
|
12
|
+
"cheatsheet",
|
|
13
|
+
"macos",
|
|
14
|
+
"terminal",
|
|
15
|
+
"productivity"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/Ariyapong/keybook#readme",
|
|
18
|
+
"repository": { "type": "git", "url": "git+https://github.com/Ariyapong/keybook.git" },
|
|
19
|
+
"bugs": { "url": "https://github.com/Ariyapong/keybook/issues" },
|
|
20
|
+
"author": "Ariyapong Wimolnoch",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": { "keybook": "dist/cli.js", "kb": "dist/cli.js" },
|
|
23
|
+
"files": ["dist", "seed", "README.md", "LICENSE"],
|
|
24
|
+
"engines": { "node": ">=22" },
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsx src/cli.ts",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "biome check .",
|
|
32
|
+
"format": "biome format --write .",
|
|
33
|
+
"prepublishOnly": "pnpm build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"fzf": "^0.5.2",
|
|
38
|
+
"ink": "^5.0.1",
|
|
39
|
+
"react": "^18.3.1",
|
|
40
|
+
"yaml": "^2.5.0",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "^1.9.4",
|
|
45
|
+
"@types/node": "^22.7.0",
|
|
46
|
+
"@types/react": "^18.3.0",
|
|
47
|
+
"ink-testing-library": "^4.0.0",
|
|
48
|
+
"tsup": "^8.3.0",
|
|
49
|
+
"tsx": "^4.19.0",
|
|
50
|
+
"typescript": "^5.6.0",
|
|
51
|
+
"vitest": "^2.1.0"
|
|
52
|
+
},
|
|
53
|
+
"license": "MIT"
|
|
54
|
+
}
|
package/seed/claude.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
app: Claude
|
|
2
|
+
entries:
|
|
3
|
+
- action: Start a new chat
|
|
4
|
+
keys: "⌘N"
|
|
5
|
+
notes: Labeled "New Conversation" in the File menu.
|
|
6
|
+
- action: Send the message
|
|
7
|
+
keys: "⏎"
|
|
8
|
+
- action: Insert a newline in the message
|
|
9
|
+
keys: "⇧⏎"
|
|
10
|
+
tags: [new line]
|
|
11
|
+
- action: Search your chats
|
|
12
|
+
keys: "⌘K"
|
|
13
|
+
notes: In-app shortcut (not in the menu bar). Distinct from Edit → Find (⌘F).
|
|
14
|
+
tags: [search, find chats]
|
|
15
|
+
- action: Summon Claude from anywhere (global hotkey)
|
|
16
|
+
steps:
|
|
17
|
+
- Open Claude → Settings
|
|
18
|
+
- Set a global keyboard shortcut for the quick launcher
|
|
19
|
+
- Press that shortcut from any app to summon Claude
|
|
20
|
+
notes: The Claude desktop app supports a configurable global hotkey.
|
|
21
|
+
tags: [global, launcher, quick]
|
package/seed/edge.yaml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
app: Microsoft Edge
|
|
2
|
+
entries:
|
|
3
|
+
- action: Open a new tab
|
|
4
|
+
keys: "⌘T"
|
|
5
|
+
tags: [new tab]
|
|
6
|
+
- action: Reopen the last closed tab
|
|
7
|
+
keys: "⌘⇧T"
|
|
8
|
+
tags: [restore tab]
|
|
9
|
+
- action: Open a new window
|
|
10
|
+
keys: "⌘N"
|
|
11
|
+
- action: Open a new InPrivate window
|
|
12
|
+
keys: "⌘⇧N"
|
|
13
|
+
tags: [incognito, private]
|
|
14
|
+
- action: Close the current tab
|
|
15
|
+
keys: "⌘W"
|
|
16
|
+
- action: Next / previous tab
|
|
17
|
+
keys: "⌃⇥, ⌃⇧⇥"
|
|
18
|
+
- action: Jump to tab 1–8
|
|
19
|
+
keys: "⌘1"
|
|
20
|
+
notes: ⌘1–⌘8 jump to that tab; ⌘9 jumps to the last tab.
|
|
21
|
+
- action: Focus the address bar
|
|
22
|
+
keys: "⌘L"
|
|
23
|
+
tags: [omnibox, url]
|
|
24
|
+
- action: Find on the page
|
|
25
|
+
keys: "⌘F"
|
|
26
|
+
- action: Reload / hard reload
|
|
27
|
+
keys: "⌘R, ⌘⇧R"
|
|
28
|
+
- action: Open Developer Tools
|
|
29
|
+
keys: "⌘⌥I"
|
|
30
|
+
tags: [devtools, inspect]
|
|
31
|
+
- action: Open History
|
|
32
|
+
keys: "⌘Y"
|
|
33
|
+
- action: Bookmark this page
|
|
34
|
+
keys: "⌘D"
|
|
35
|
+
tags: [favorite]
|
|
36
|
+
- action: Zoom in / out / reset
|
|
37
|
+
keys: "⌘+, ⌘-, ⌘0"
|
|
38
|
+
- action: Back / forward
|
|
39
|
+
keys: "⌘[, ⌘]"
|
package/seed/finder.yaml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
app: Finder
|
|
2
|
+
entries:
|
|
3
|
+
- action: Open a new tab
|
|
4
|
+
keys: "⌘T"
|
|
5
|
+
tags: [new tab]
|
|
6
|
+
- action: Open a new Finder window
|
|
7
|
+
keys: "⌘N"
|
|
8
|
+
- action: Create a new folder
|
|
9
|
+
keys: "⌘⇧N"
|
|
10
|
+
tags: [new folder]
|
|
11
|
+
- action: Go to folder by path
|
|
12
|
+
keys: "⌘⇧G"
|
|
13
|
+
tags: [go to, path]
|
|
14
|
+
- action: Go up one folder
|
|
15
|
+
keys: "⌘↑"
|
|
16
|
+
- action: Open selected item
|
|
17
|
+
keys: "⌘↓"
|
|
18
|
+
- action: Back / Forward
|
|
19
|
+
keys: "⌘[, ⌘]"
|
|
20
|
+
tags: [history, navigate]
|
|
21
|
+
- action: Rename the selected item
|
|
22
|
+
keys: "⏎"
|
|
23
|
+
notes: In Finder, Return renames (it does not open).
|
|
24
|
+
- action: Quick Look the selected item
|
|
25
|
+
keys: "␣"
|
|
26
|
+
tags: [preview, space]
|
|
27
|
+
- action: Show or hide hidden files
|
|
28
|
+
keys: "⌘⇧."
|
|
29
|
+
tags: [dotfiles, hidden]
|
|
30
|
+
- action: Get Info
|
|
31
|
+
keys: "⌘I"
|
|
32
|
+
- action: Duplicate
|
|
33
|
+
keys: "⌘D"
|
|
34
|
+
- action: Move to Trash
|
|
35
|
+
keys: "⌘⌫"
|
|
36
|
+
tags: [delete]
|
|
37
|
+
- action: Move copied files here (cut/paste)
|
|
38
|
+
keys: "⌘⌥V"
|
|
39
|
+
tags: [move, cut, paste]
|
|
40
|
+
- action: Empty the Trash
|
|
41
|
+
keys: "⌘⇧⌫"
|
|
42
|
+
- action: View as list / columns / gallery
|
|
43
|
+
keys: "⌘2"
|
|
44
|
+
notes: ⌘1 icons, ⌘2 list, ⌘3 columns, ⌘4 gallery.
|
|
45
|
+
- action: Open Terminal at the current folder
|
|
46
|
+
steps:
|
|
47
|
+
- Right-click the folder (or its name in the path bar at the bottom)
|
|
48
|
+
- Choose Services → "New Terminal at Folder"
|
|
49
|
+
notes: >-
|
|
50
|
+
Enable once in System Settings → Keyboard → Keyboard Shortcuts →
|
|
51
|
+
Services → Files and Folders → "New Terminal at Folder".
|
|
52
|
+
tags: [terminal here, open terminal, cd]
|
package/seed/fork.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
app: Fork
|
|
2
|
+
entries:
|
|
3
|
+
- action: Open a repository
|
|
4
|
+
keys: "⌘O"
|
|
5
|
+
- action: Open a new tab
|
|
6
|
+
keys: "⌘T"
|
|
7
|
+
- action: Close the current tab
|
|
8
|
+
keys: "⌘W"
|
|
9
|
+
- action: Commit the staged changes
|
|
10
|
+
keys: "⌘⏎"
|
|
11
|
+
notes: From the commit message box. The Repository → Commit menu item is ⇧⌘C.
|
|
12
|
+
- action: Fetch
|
|
13
|
+
keys: "⇧⌘F"
|
|
14
|
+
notes: Add ⌥ for Quick Fetch (skips the dialog) — ⌥⇧⌘F.
|
|
15
|
+
tags: [fetch]
|
|
16
|
+
- action: Pull
|
|
17
|
+
keys: "⇧⌘L"
|
|
18
|
+
notes: Add ⌥ for Quick Pull — ⌥⇧⌘L.
|
|
19
|
+
tags: [pull]
|
|
20
|
+
- action: Push
|
|
21
|
+
keys: "⇧⌘P"
|
|
22
|
+
notes: Add ⌥ for Quick Push — ⌥⇧⌘P.
|
|
23
|
+
tags: [push]
|
|
24
|
+
- action: Open Quick Launch (command palette)
|
|
25
|
+
keys: "⌘P"
|
|
26
|
+
notes: Fork's fuzzy action launcher.
|
|
27
|
+
tags: [palette, actions]
|
package/seed/macos.yaml
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
app: macOS
|
|
2
|
+
entries:
|
|
3
|
+
- action: Open Spotlight search
|
|
4
|
+
keys: "⌘␣"
|
|
5
|
+
tags: [search, launch]
|
|
6
|
+
- action: Switch between open apps
|
|
7
|
+
keys: "⌘⇥"
|
|
8
|
+
tags: [app switcher]
|
|
9
|
+
- action: Switch between windows of the current app
|
|
10
|
+
keys: "⌘`"
|
|
11
|
+
- action: Screenshot a region to a file
|
|
12
|
+
keys: "⌘⇧4"
|
|
13
|
+
tags: [screenshot, capture]
|
|
14
|
+
- action: Screenshot the whole screen to a file
|
|
15
|
+
keys: "⌘⇧3"
|
|
16
|
+
tags: [screenshot]
|
|
17
|
+
- action: Screenshot a region to the clipboard
|
|
18
|
+
keys: "⌃⌘⇧4"
|
|
19
|
+
tags: [screenshot, copy]
|
|
20
|
+
- action: Open the screenshot / recording toolbar
|
|
21
|
+
keys: "⌘⇧5"
|
|
22
|
+
tags: [screenshot, record]
|
|
23
|
+
- action: Quit the current app
|
|
24
|
+
keys: "⌘Q"
|
|
25
|
+
- action: Hide the current app
|
|
26
|
+
keys: "⌘H"
|
|
27
|
+
- action: Minimize the window
|
|
28
|
+
keys: "⌘M"
|
|
29
|
+
- action: Close the window
|
|
30
|
+
keys: "⌘W"
|
|
31
|
+
- action: Toggle full screen
|
|
32
|
+
keys: "⌃⌘F"
|
|
33
|
+
- action: Open Mission Control
|
|
34
|
+
keys: "⌃↑"
|
|
35
|
+
- action: Move one space left / right
|
|
36
|
+
keys: "⌃←, ⌃→"
|
|
37
|
+
- action: Lock the screen
|
|
38
|
+
keys: "⌃⌘Q"
|
|
39
|
+
- action: Open the Force Quit dialog
|
|
40
|
+
keys: "⌥⌘⎋"
|
|
41
|
+
tags: [force quit]
|
|
42
|
+
- action: Open the emoji & symbols picker
|
|
43
|
+
keys: "⌃⌘␣"
|
|
44
|
+
tags: [emoji]
|
|
45
|
+
- action: Record your screen
|
|
46
|
+
steps:
|
|
47
|
+
- Press ⌘⇧5
|
|
48
|
+
- Choose "Record Entire Screen" or "Record Selected Portion", then click Record
|
|
49
|
+
- To stop, click the ⏹ stop button in the menu bar
|
|
50
|
+
tags: [screen recording]
|
package/seed/nano.yaml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
app: Nano
|
|
2
|
+
entries:
|
|
3
|
+
- action: Save the file (write out)
|
|
4
|
+
keys: "⌃O"
|
|
5
|
+
tags: [save, write]
|
|
6
|
+
- action: Exit nano
|
|
7
|
+
keys: "⌃X"
|
|
8
|
+
tags: [quit, exit]
|
|
9
|
+
- action: Show help
|
|
10
|
+
keys: "⌃G"
|
|
11
|
+
tags: [help]
|
|
12
|
+
- action: Cut the current line
|
|
13
|
+
keys: "⌃K"
|
|
14
|
+
tags: [cut, delete]
|
|
15
|
+
- action: Paste (uncut) the cut text
|
|
16
|
+
keys: "⌃U"
|
|
17
|
+
tags: [paste, uncut]
|
|
18
|
+
- action: Set a mark, then move to select
|
|
19
|
+
keys: "⌃6"
|
|
20
|
+
notes: ⌃6 (or ⌃^) starts a selection; move the cursor to extend it, then ⌃K cuts it.
|
|
21
|
+
tags: [select, mark]
|
|
22
|
+
- action: Search
|
|
23
|
+
keys: "⌃W"
|
|
24
|
+
notes: After ⌃W, press ⏎ again to repeat the last search.
|
|
25
|
+
tags: [find, search]
|
|
26
|
+
- action: Search and replace
|
|
27
|
+
keys: "⌃\\"
|
|
28
|
+
tags: [replace]
|
|
29
|
+
- action: Go to line / column
|
|
30
|
+
keys: "⌃_"
|
|
31
|
+
tags: [goto, line]
|
|
32
|
+
- action: Go to top / bottom of the file
|
|
33
|
+
keys: "⌥\\, ⌥/"
|
|
34
|
+
tags: [top, bottom]
|
|
35
|
+
- action: Page up / down
|
|
36
|
+
keys: "⌃Y, ⌃V"
|
|
37
|
+
tags: [scroll, page]
|
|
38
|
+
- action: Undo / redo
|
|
39
|
+
keys: "⌥U, ⌥E"
|
|
40
|
+
tags: [undo, redo]
|
|
41
|
+
- action: Show the current cursor position
|
|
42
|
+
keys: "⌃C"
|
|
43
|
+
tags: [position, line number]
|
|
44
|
+
- action: Notation note (Control and Meta keys)
|
|
45
|
+
steps:
|
|
46
|
+
- nano writes ⌃ as "^" (Control) and ⌥ as "M-" (Meta)
|
|
47
|
+
- "Example: ^O means Control-O; M-U means Meta-U"
|
|
48
|
+
notes: >-
|
|
49
|
+
On macOS, Meta = Option. If ⌥ shortcuts don't work in Terminal, enable
|
|
50
|
+
Terminal → Settings → Profiles → Keyboard → "Use Option as Meta key".
|
|
51
|
+
tags: [notation, meta, help]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
app: Terminal
|
|
2
|
+
entries:
|
|
3
|
+
- action: Open a new tab
|
|
4
|
+
keys: "⌘T"
|
|
5
|
+
tags: [new tab]
|
|
6
|
+
- action: Open a new window
|
|
7
|
+
keys: "⌘N"
|
|
8
|
+
- action: Close the current tab
|
|
9
|
+
keys: "⌘W"
|
|
10
|
+
- action: Next / previous tab
|
|
11
|
+
keys: "⌃⇥, ⌃⇧⇥"
|
|
12
|
+
tags: [switch tab]
|
|
13
|
+
- action: Clear the screen and scrollback
|
|
14
|
+
keys: "⌘K"
|
|
15
|
+
tags: [clear]
|
|
16
|
+
- action: Find in the terminal
|
|
17
|
+
keys: "⌘F"
|
|
18
|
+
- action: Increase / decrease font size
|
|
19
|
+
keys: "⌘+, ⌘-"
|
|
20
|
+
- action: Jump to start / end of line
|
|
21
|
+
keys: "⌃A, ⌃E"
|
|
22
|
+
tags: [cursor, beginning, end, readline]
|
|
23
|
+
- action: Move back / forward one word
|
|
24
|
+
keys: "⌥B, ⌥F"
|
|
25
|
+
tags: [cursor, word, readline]
|
|
26
|
+
- action: Delete to end of line
|
|
27
|
+
keys: "⌃K"
|
|
28
|
+
tags: [readline, kill]
|
|
29
|
+
- action: Delete the previous word
|
|
30
|
+
keys: "⌃W"
|
|
31
|
+
tags: [readline]
|
|
32
|
+
- action: Clear the screen (keep the current line)
|
|
33
|
+
keys: "⌃L"
|
|
34
|
+
tags: [readline]
|
|
35
|
+
- action: Reverse-search command history
|
|
36
|
+
keys: "⌃R"
|
|
37
|
+
tags: [history, search, readline]
|
|
38
|
+
- action: Cancel the current command
|
|
39
|
+
keys: "⌃C"
|
|
40
|
+
tags: [interrupt]
|
|
41
|
+
- action: Delete to the start of the line
|
|
42
|
+
keys: "⌃U"
|
|
43
|
+
tags: [readline, kill]
|
|
44
|
+
- action: Delete the character under the cursor (or EOF / logout)
|
|
45
|
+
keys: "⌃D"
|
|
46
|
+
notes: On an empty line, ⌃D signals end-of-input and can close the shell.
|
|
47
|
+
tags: [readline, delete, eof]
|
|
48
|
+
- action: Paste the last killed (deleted) text
|
|
49
|
+
keys: "⌃Y"
|
|
50
|
+
tags: [readline, yank, paste]
|
|
51
|
+
- action: Delete the next word
|
|
52
|
+
keys: "⌥D"
|
|
53
|
+
tags: [readline, word]
|
|
54
|
+
- action: Swap (transpose) the last two characters
|
|
55
|
+
keys: "⌃T"
|
|
56
|
+
tags: [readline, transpose]
|
|
57
|
+
- action: Suspend the running program, then resume it
|
|
58
|
+
keys: "⌃Z"
|
|
59
|
+
command: fg
|
|
60
|
+
notes: ⌃Z suspends to the background; type `fg` to resume (or `bg` to run it in the background).
|
|
61
|
+
tags: [suspend, background, job]
|
|
62
|
+
- action: Repeat the last command / reuse its last argument
|
|
63
|
+
keys: "!!, !$"
|
|
64
|
+
notes: >-
|
|
65
|
+
!! re-runs the previous command (e.g. `sudo !!`); !$ expands to the last
|
|
66
|
+
argument of the previous command.
|
|
67
|
+
tags: [history, tip, bang]
|
|
68
|
+
- action: Edit the current command line in your $EDITOR
|
|
69
|
+
keys: "⌃X⌃E"
|
|
70
|
+
notes: Opens the half-typed command in $EDITOR; save and quit to run it (bash/zsh).
|
|
71
|
+
tags: [readline, editor, tip]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
app: Text editing
|
|
2
|
+
entries:
|
|
3
|
+
- action: Move one word left / right
|
|
4
|
+
keys: "⌥←, ⌥→"
|
|
5
|
+
tags: [word, navigation, cursor]
|
|
6
|
+
- action: Move to start / end of the line
|
|
7
|
+
keys: "⌘←, ⌘→"
|
|
8
|
+
tags: [line, home, end]
|
|
9
|
+
- action: Move to start / end of the document
|
|
10
|
+
keys: "⌘↑, ⌘↓"
|
|
11
|
+
tags: [top, bottom, document]
|
|
12
|
+
- action: Delete the previous word
|
|
13
|
+
keys: "⌥⌫"
|
|
14
|
+
tags: [delete, word, backspace]
|
|
15
|
+
- action: Delete to the start of the line
|
|
16
|
+
keys: "⌘⌫"
|
|
17
|
+
tags: [delete, line]
|
|
18
|
+
- action: Delete the next word (forward)
|
|
19
|
+
keys: "fn⌥⌫"
|
|
20
|
+
notes: ⌫ is Delete (Backspace); fn turns it into Forward Delete (⌦).
|
|
21
|
+
tags: [delete, word, forward]
|
|
22
|
+
- action: Select one word left / right
|
|
23
|
+
keys: "⌥⇧←, ⌥⇧→"
|
|
24
|
+
tags: [select, word]
|
|
25
|
+
- action: Select to start / end of the line
|
|
26
|
+
keys: "⌘⇧←, ⌘⇧→"
|
|
27
|
+
tags: [select, line]
|
|
28
|
+
- action: Select to start / end of the document
|
|
29
|
+
keys: "⌘⇧↑, ⌘⇧↓"
|
|
30
|
+
tags: [select, document]
|
|
31
|
+
- action: Select all
|
|
32
|
+
keys: "⌘A"
|
|
33
|
+
tags: [select, all]
|
|
34
|
+
- action: Undo / redo
|
|
35
|
+
keys: "⌘Z, ⇧⌘Z"
|
|
36
|
+
tags: [undo, redo]
|
|
37
|
+
- action: Jump to start / end of line (emacs-style)
|
|
38
|
+
keys: "⌃A, ⌃E"
|
|
39
|
+
notes: Works in most macOS native (Cocoa) text fields, not every app.
|
|
40
|
+
tags: [cursor, readline]
|
|
41
|
+
- action: Delete to end of line (emacs-style)
|
|
42
|
+
keys: "⌃K"
|
|
43
|
+
notes: Cocoa text fields; pairs with ⌃Y to paste it back.
|
|
44
|
+
tags: [delete, kill]
|
package/seed/tmux.yaml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
app: tmux
|
|
2
|
+
entries:
|
|
3
|
+
- action: Prefix key (press before each command below)
|
|
4
|
+
keys: "⌃B"
|
|
5
|
+
notes: Default tmux prefix.
|
|
6
|
+
- action: Create a new window
|
|
7
|
+
keys: "⌃B, c"
|
|
8
|
+
- action: Next / previous window
|
|
9
|
+
keys: "⌃B, n"
|
|
10
|
+
notes: n next, p previous.
|
|
11
|
+
- action: Go to window by number
|
|
12
|
+
keys: "⌃B, 0"
|
|
13
|
+
notes: 0–9.
|
|
14
|
+
- action: Split the pane left / right
|
|
15
|
+
keys: "⌃B, %"
|
|
16
|
+
tags: [vertical split]
|
|
17
|
+
- action: Split the pane top / bottom
|
|
18
|
+
keys: '⌃B, "'
|
|
19
|
+
tags: [horizontal split]
|
|
20
|
+
- action: Move between panes
|
|
21
|
+
keys: "⌃B, →"
|
|
22
|
+
notes: Use ↑ ↓ ← → to move.
|
|
23
|
+
- action: Toggle pane zoom
|
|
24
|
+
keys: "⌃B, z"
|
|
25
|
+
tags: [fullscreen pane, maximize]
|
|
26
|
+
- action: Close the current pane
|
|
27
|
+
keys: "⌃B, x"
|
|
28
|
+
- action: Detach from the session
|
|
29
|
+
keys: "⌃B, d"
|
|
30
|
+
- action: List sessions
|
|
31
|
+
keys: "⌃B, s"
|
|
32
|
+
- action: Rename the current window
|
|
33
|
+
keys: "⌃B, ,"
|
|
34
|
+
- action: Enter copy mode (scroll)
|
|
35
|
+
keys: "⌃B, ["
|
|
36
|
+
tags: [scroll, copy]
|
|
37
|
+
- action: Open the command prompt
|
|
38
|
+
keys: "⌃B, :"
|
|
39
|
+
- action: Reload the tmux config
|
|
40
|
+
command: tmux source-file ~/.tmux.conf
|
|
41
|
+
tags: [reload, config]
|
|
42
|
+
- action: Break the current pane into its own window
|
|
43
|
+
keys: "⌃B, !"
|
|
44
|
+
tags: [pane, window, break]
|
|
45
|
+
- action: Swap the current pane left / right
|
|
46
|
+
keys: "⌃B, {"
|
|
47
|
+
notes: "{ swaps with the previous pane; } swaps with the next."
|
|
48
|
+
tags: [pane, swap, move]
|
|
49
|
+
- action: Cycle through the preset pane layouts
|
|
50
|
+
keys: "⌃B, ␣"
|
|
51
|
+
tags: [layout, arrange]
|
|
52
|
+
- action: Show pane numbers (then press one to jump)
|
|
53
|
+
keys: "⌃B, q"
|
|
54
|
+
tags: [pane, jump]
|
|
55
|
+
- action: Kill the current window
|
|
56
|
+
keys: "⌃B, &"
|
|
57
|
+
notes: Asks for confirmation.
|
|
58
|
+
tags: [window, close, kill]
|
|
59
|
+
- action: Paste the most recent copy-mode buffer
|
|
60
|
+
keys: "⌃B, ]"
|
|
61
|
+
tags: [paste, copy]
|
package/seed/vim.yaml
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
app: Vim
|
|
2
|
+
entries:
|
|
3
|
+
- action: Enter Insert mode (before / after the cursor)
|
|
4
|
+
keys: "i, a"
|
|
5
|
+
tags: [insert, edit]
|
|
6
|
+
- action: Return to Normal mode
|
|
7
|
+
keys: "⎋"
|
|
8
|
+
tags: [normal, escape]
|
|
9
|
+
- action: Enter Visual / Visual-Block mode
|
|
10
|
+
keys: "v, ⌃V"
|
|
11
|
+
tags: [select, block]
|
|
12
|
+
- action: Save the file
|
|
13
|
+
keys: ":w⏎"
|
|
14
|
+
tags: [write, save]
|
|
15
|
+
- action: Save and quit
|
|
16
|
+
keys: ":wq⏎"
|
|
17
|
+
notes: ZZ does the same from Normal mode.
|
|
18
|
+
tags: [save, quit, exit]
|
|
19
|
+
- action: Quit without saving
|
|
20
|
+
keys: ":q!⏎"
|
|
21
|
+
tags: [quit, discard, exit]
|
|
22
|
+
- action: Go to start / end of line
|
|
23
|
+
keys: "0, $"
|
|
24
|
+
notes: "^ jumps to the first non-blank character."
|
|
25
|
+
tags: [line, motion]
|
|
26
|
+
- action: Go to top / bottom of file
|
|
27
|
+
keys: "gg, G"
|
|
28
|
+
notes: '":42⏎" jumps to line 42.'
|
|
29
|
+
tags: [motion, goto]
|
|
30
|
+
- action: Move word forward / back
|
|
31
|
+
keys: "w, b"
|
|
32
|
+
tags: [word, motion]
|
|
33
|
+
- action: Jump to the matching bracket
|
|
34
|
+
keys: "%"
|
|
35
|
+
tags: [bracket, paren, match]
|
|
36
|
+
- action: Delete / yank (copy) the current line
|
|
37
|
+
keys: "dd, yy"
|
|
38
|
+
tags: [delete, copy, cut]
|
|
39
|
+
- action: Paste after / before the cursor
|
|
40
|
+
keys: "p, P"
|
|
41
|
+
tags: [paste]
|
|
42
|
+
- action: Delete the character under the cursor
|
|
43
|
+
keys: "x"
|
|
44
|
+
tags: [delete, char]
|
|
45
|
+
- action: Undo / redo
|
|
46
|
+
keys: "u, ⌃R"
|
|
47
|
+
tags: [undo, redo]
|
|
48
|
+
- action: Repeat the last change
|
|
49
|
+
keys: "."
|
|
50
|
+
tags: [repeat, dot]
|
|
51
|
+
- action: Search, then next / previous match
|
|
52
|
+
keys: "/text⏎, n, N"
|
|
53
|
+
tags: [search, find]
|
|
54
|
+
- action: Replace every match in the file
|
|
55
|
+
keys: ":%s/old/new/g⏎"
|
|
56
|
+
notes: Add c at the end (…/gc) to confirm each replacement.
|
|
57
|
+
tags: [replace, substitute, tip]
|
|
58
|
+
- action: Change inside word / quotes / parens
|
|
59
|
+
keys: 'ciw, ci", ci('
|
|
60
|
+
notes: >-
|
|
61
|
+
Text objects — "change inner". Swap c for d to delete instead (diw, di").
|
|
62
|
+
Works with [], {}, <> too.
|
|
63
|
+
tags: [text object, tip, change]
|
|
64
|
+
- action: Indent / outdent the line or selection
|
|
65
|
+
keys: ">>, <<"
|
|
66
|
+
tags: [indent]
|
package/seed/vscode.yaml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
app: VS Code
|
|
2
|
+
entries:
|
|
3
|
+
- action: Show the Command Palette
|
|
4
|
+
keys: "⌘⇧P"
|
|
5
|
+
tags: [commands, palette]
|
|
6
|
+
- action: Quick Open a file by name
|
|
7
|
+
keys: "⌘P"
|
|
8
|
+
tags: [open file, goto]
|
|
9
|
+
- action: Toggle the integrated terminal
|
|
10
|
+
keys: "⌃`"
|
|
11
|
+
tags: [terminal]
|
|
12
|
+
- action: Toggle the sidebar
|
|
13
|
+
keys: "⌘B"
|
|
14
|
+
- action: Add a cursor below
|
|
15
|
+
keys: "⌘⌥↓"
|
|
16
|
+
tags: [multi cursor]
|
|
17
|
+
- action: Select the next occurrence of the selection
|
|
18
|
+
keys: "⌘D"
|
|
19
|
+
tags: [multi cursor, select]
|
|
20
|
+
- action: Go to a line number
|
|
21
|
+
keys: "⌃G"
|
|
22
|
+
- action: Go to definition
|
|
23
|
+
keys: "F12"
|
|
24
|
+
- action: Rename the symbol
|
|
25
|
+
keys: "F2"
|
|
26
|
+
tags: [refactor]
|
|
27
|
+
- action: Find / Replace in the file
|
|
28
|
+
keys: "⌘F, ⌥⌘F"
|
|
29
|
+
- action: Find in all files
|
|
30
|
+
keys: "⌘⇧F"
|
|
31
|
+
tags: [search]
|
|
32
|
+
- action: Toggle line comment
|
|
33
|
+
keys: "⌘/"
|
|
34
|
+
- action: Move the current line up / down
|
|
35
|
+
keys: "⌥↑, ⌥↓"
|
|
36
|
+
- action: Copy the current line up / down
|
|
37
|
+
keys: "⇧⌥↑, ⇧⌥↓"
|
|
38
|
+
- action: Format the document
|
|
39
|
+
keys: "⇧⌥F"
|
|
40
|
+
- action: Split the editor
|
|
41
|
+
keys: "⌘\\"
|
|
42
|
+
- action: Trigger a Quick Fix
|
|
43
|
+
keys: "⌘."
|
|
44
|
+
tags: [lightbulb]
|
|
45
|
+
- action: Toggle Zen Mode
|
|
46
|
+
keys: "⌘K, Z"
|
|
47
|
+
tags: [focus, fullscreen]
|
|
48
|
+
- action: Delete the current line
|
|
49
|
+
keys: "⌘⇧K"
|
|
50
|
+
tags: [delete line]
|
|
51
|
+
- action: Insert a line below / above
|
|
52
|
+
keys: "⌘⏎, ⇧⌘⏎"
|
|
53
|
+
tags: [new line]
|
|
54
|
+
- action: Select all occurrences of the current selection
|
|
55
|
+
keys: "⌘⇧L"
|
|
56
|
+
tags: [multi cursor, select]
|
|
57
|
+
- action: Go to the next problem (error / warning)
|
|
58
|
+
keys: "F8"
|
|
59
|
+
tags: [errors, diagnostics]
|
|
60
|
+
- action: Navigate back / forward (cursor history)
|
|
61
|
+
keys: "⌃-, ⌃⇧-"
|
|
62
|
+
tags: [navigate, history, goto]
|
|
63
|
+
- action: Open Keyboard Shortcuts
|
|
64
|
+
keys: "⌘K, ⌘S"
|
|
65
|
+
tags: [keybindings, shortcuts]
|