@kitlangton/ghui 0.2.1 → 0.3.2

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.
Files changed (53) hide show
  1. package/README.md +1 -4
  2. package/bin/ghui.js +6 -1
  3. package/dist/index.js +9419 -0
  4. package/package.json +7 -4
  5. package/src/App.tsx +0 -2604
  6. package/src/appCommands.ts +0 -384
  7. package/src/commands.ts +0 -73
  8. package/src/config.ts +0 -28
  9. package/src/date.ts +0 -20
  10. package/src/domain.ts +0 -139
  11. package/src/errors.ts +0 -10
  12. package/src/index.tsx +0 -50
  13. package/src/keyboard/opentuiAdapter.ts +0 -43
  14. package/src/keymap/all.ts +0 -103
  15. package/src/keymap/closeModal.ts +0 -13
  16. package/src/keymap/commandPalette.ts +0 -16
  17. package/src/keymap/commentModal.ts +0 -73
  18. package/src/keymap/commentThreadModal.ts +0 -24
  19. package/src/keymap/detailView.ts +0 -30
  20. package/src/keymap/diffView.ts +0 -91
  21. package/src/keymap/filterMode.ts +0 -13
  22. package/src/keymap/helpers.ts +0 -24
  23. package/src/keymap/labelModal.ts +0 -16
  24. package/src/keymap/listNav.ts +0 -119
  25. package/src/keymap/mergeModal.ts +0 -23
  26. package/src/keymap/openRepositoryModal.ts +0 -13
  27. package/src/keymap/themeModal.ts +0 -49
  28. package/src/mergeActions.ts +0 -88
  29. package/src/observability.ts +0 -46
  30. package/src/pullRequestCache.ts +0 -19
  31. package/src/pullRequestViews.ts +0 -45
  32. package/src/services/BrowserOpener.ts +0 -22
  33. package/src/services/Clipboard.ts +0 -46
  34. package/src/services/CommandRunner.ts +0 -91
  35. package/src/services/GitHubService.ts +0 -741
  36. package/src/services/MockGitHubService.ts +0 -181
  37. package/src/themeStore.ts +0 -60
  38. package/src/ui/CommandPalette.tsx +0 -216
  39. package/src/ui/DetailsPane.tsx +0 -656
  40. package/src/ui/FooterHints.tsx +0 -90
  41. package/src/ui/LoadingLogo.tsx +0 -75
  42. package/src/ui/PullRequestDiffPane.tsx +0 -248
  43. package/src/ui/PullRequestList.tsx +0 -210
  44. package/src/ui/colors.ts +0 -814
  45. package/src/ui/commentEditor.ts +0 -126
  46. package/src/ui/comments.tsx +0 -143
  47. package/src/ui/diff.ts +0 -650
  48. package/src/ui/diffStats.tsx +0 -25
  49. package/src/ui/modals.tsx +0 -614
  50. package/src/ui/primitives.tsx +0 -205
  51. package/src/ui/pullRequests.ts +0 -106
  52. package/src/ui/singleLineInput.ts +0 -26
  53. package/src/ui/spinner.ts +0 -1
package/src/index.tsx DELETED
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { addDefaultParsers, createCliRenderer, createTerminalPalette } from "@opentui/core"
4
- import { RegistryProvider } from "@effect/atom-react"
5
- import { createRoot } from "@opentui/react"
6
-
7
- process.env.OTUI_USE_ALTERNATE_SCREEN = "true"
8
-
9
- addDefaultParsers([
10
- {
11
- filetype: "bash",
12
- aliases: ["sh", "shell", "zsh", "ksh"],
13
- wasm: "https://github.com/tree-sitter/tree-sitter-bash/releases/download/v0.25.1/tree-sitter-bash.wasm",
14
- queries: {
15
- highlights: ["https://raw.githubusercontent.com/tree-sitter/tree-sitter-bash/v0.25.1/queries/highlights.scm"],
16
- },
17
- },
18
- ])
19
-
20
- const FOCUS_REPORTING_ENABLE = "\x1b[?1004h"
21
- const FOCUS_REPORTING_DISABLE = "\x1b[?1004l"
22
-
23
- const paletteDetector = createTerminalPalette(process.stdin, process.stdout)
24
- const [terminalColors, { setSystemThemeColors }, { App }] = await Promise.all([
25
- paletteDetector.detect({ timeout: 150 }).catch(() => null).finally(() => paletteDetector.cleanup()),
26
- import("./ui/colors.js"),
27
- import("./App.js"),
28
- ])
29
-
30
- if (terminalColors) {
31
- setSystemThemeColors(terminalColors)
32
- }
33
-
34
- const renderer = await createCliRenderer({
35
- exitOnCtrlC: false,
36
- screenMode: "alternate-screen",
37
- externalOutputMode: "passthrough",
38
- onDestroy: () => {
39
- process.stdout.write(FOCUS_REPORTING_DISABLE)
40
- process.exit(0)
41
- },
42
- })
43
-
44
- process.stdout.write(FOCUS_REPORTING_ENABLE)
45
-
46
- createRoot(renderer).render(
47
- <RegistryProvider>
48
- <App />
49
- </RegistryProvider>,
50
- )
@@ -1,43 +0,0 @@
1
- import type { KeyEvent } from "@opentui/core"
2
- import { useKeyboard } from "@opentui/react"
3
- import type { ParsedStroke } from "@ghui/keymap"
4
- import type { KeySubscribe } from "@ghui/keymap/react"
5
- import { useMemo, useRef } from "react"
6
-
7
- /**
8
- * Map an opentui KeyEvent into @ghui/keymap's ParsedStroke.
9
- * `option` (alt key on Linux/Windows) is folded into `meta` to keep one
10
- * cross-platform modifier surface.
11
- */
12
- export const normalizeOpenTuiKey = (event: KeyEvent): ParsedStroke => ({
13
- key: event.name.toLowerCase(),
14
- ctrl: event.ctrl,
15
- shift: event.shift,
16
- meta: event.meta || event.option,
17
- })
18
-
19
- /**
20
- * React hook that returns a stable `KeySubscribe` driven by opentui's keyboard.
21
- *
22
- * Multiple subscribers (e.g. the `@ghui/keymap` dispatcher and any text-input
23
- * fallback) can each register their own handler. Internally we attach a single
24
- * `useKeyboard` and fan out to every registered handler, so we don't accidentally
25
- * stack two `useKeyboard` listeners on the same component.
26
- */
27
- export const useOpenTuiSubscribe = (): KeySubscribe => {
28
- const handlersRef = useRef<Set<(stroke: ParsedStroke) => void>>(new Set())
29
-
30
- useKeyboard((event) => {
31
- const keyEvent = event as KeyEvent
32
- if (keyEvent.defaultPrevented) return
33
- const stroke = normalizeOpenTuiKey(keyEvent)
34
- for (const handler of handlersRef.current) handler(stroke)
35
- })
36
-
37
- return useMemo<KeySubscribe>(() => (handler) => {
38
- handlersRef.current.add(handler)
39
- return () => {
40
- handlersRef.current.delete(handler)
41
- }
42
- }, [])
43
- }
package/src/keymap/all.ts DELETED
@@ -1,103 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
- import { closeModalKeymap, type CloseModalCtx } from "./closeModal.ts"
3
- import { commandPaletteKeymap, type CommandPaletteCtx } from "./commandPalette.ts"
4
- import { commentModalKeymap, type CommentModalCtx } from "./commentModal.ts"
5
- import { commentThreadModalKeymap, type CommentThreadModalCtx } from "./commentThreadModal.ts"
6
- import { detailViewKeymap, type DetailViewCtx } from "./detailView.ts"
7
- import { diffViewKeymap, type DiffViewCtx } from "./diffView.ts"
8
- import { filterModeKeymap, type FilterModeCtx } from "./filterMode.ts"
9
- import { labelModalKeymap, type LabelModalCtx } from "./labelModal.ts"
10
- import { listNavKeymap, type ListNavCtx } from "./listNav.ts"
11
- import { mergeModalKeymap, type MergeModalCtx } from "./mergeModal.ts"
12
- import { openRepositoryModalKeymap, type OpenRepositoryModalCtx } from "./openRepositoryModal.ts"
13
- import { themeModalKeymap, type ThemeModalCtx } from "./themeModal.ts"
14
-
15
- export interface AppCtx {
16
- // Active flags
17
- readonly closeModalActive: boolean
18
- readonly mergeModalActive: boolean
19
- readonly commentThreadModalActive: boolean
20
- readonly labelModalActive: boolean
21
- readonly themeModalActive: boolean
22
- readonly openRepositoryModalActive: boolean
23
- readonly commentModalActive: boolean
24
- readonly commandPaletteActive: boolean
25
- readonly filterMode: boolean
26
- readonly diffFullView: boolean
27
- readonly detailFullView: boolean
28
-
29
- // True whenever a modal/mode swallows raw text input (so q-quit, etc. are
30
- // disabled inside text-editing contexts).
31
- readonly textInputActive: boolean
32
-
33
- // Per-layer narrow contexts
34
- readonly closeModal: CloseModalCtx
35
- readonly mergeModal: MergeModalCtx
36
- readonly commentThreadModal: CommentThreadModalCtx
37
- readonly labelModal: LabelModalCtx
38
- readonly themeModal: ThemeModalCtx
39
- readonly openRepositoryModal: OpenRepositoryModalCtx
40
- readonly commentModal: CommentModalCtx
41
- readonly commandPalette: CommandPaletteCtx
42
- readonly filterModeCtx: FilterModeCtx
43
- readonly diff: DiffViewCtx
44
- readonly detail: DetailViewCtx
45
- readonly listNav: ListNavCtx
46
-
47
- // Always-on / app-level
48
- readonly openCommandPalette: () => void
49
- readonly handleQuitOrClose: () => void
50
- }
51
-
52
- const App = context<AppCtx>()
53
-
54
- const inListMode = (a: AppCtx): boolean =>
55
- !a.closeModalActive
56
- && !a.mergeModalActive
57
- && !a.commentThreadModalActive
58
- && !a.labelModalActive
59
- && !a.themeModalActive
60
- && !a.openRepositoryModalActive
61
- && !a.commentModalActive
62
- && !a.commandPaletteActive
63
- && !a.filterMode
64
- && !a.diffFullView
65
- && !a.detailFullView
66
-
67
- export const appKeymap = App(
68
- // Always-on: command palette opener
69
- { id: "command.open", title: "Open command palette", keys: ["ctrl+p", "meta+k"], run: (s) => s.openCommandPalette() },
70
-
71
- // Quit / close-active-modal — gated to "not editing text"
72
- {
73
- id: "app.quit-or-close",
74
- title: "Quit / close modal",
75
- keys: ["ctrl+c"],
76
- run: (s) => s.handleQuitOrClose(),
77
- },
78
- {
79
- id: "app.quit-or-close-q",
80
- title: "Quit / close modal",
81
- keys: ["q"],
82
- when: (s) => !s.textInputActive,
83
- run: (s) => s.handleQuitOrClose(),
84
- },
85
-
86
- // Modal layers
87
- closeModalKeymap.scope((a) => a.closeModalActive && a.closeModal),
88
- mergeModalKeymap.scope((a) => a.mergeModalActive && a.mergeModal),
89
- commentThreadModalKeymap.scope((a) => a.commentThreadModalActive && a.commentThreadModal),
90
- labelModalKeymap.scope((a) => a.labelModalActive && a.labelModal),
91
- themeModalKeymap.scope((a) => a.themeModalActive && a.themeModal),
92
- openRepositoryModalKeymap.scope((a) => a.openRepositoryModalActive && a.openRepositoryModal),
93
- commentModalKeymap.scope((a) => a.commentModalActive && a.commentModal),
94
- commandPaletteKeymap.scope((a) => a.commandPaletteActive && a.commandPalette),
95
- filterModeKeymap.scope((a) => a.filterMode && a.filterModeCtx),
96
-
97
- // Full-view layers (only when no modal is on top)
98
- diffViewKeymap.scope((a) => a.diffFullView && !a.commandPaletteActive && a.diff),
99
- detailViewKeymap.scope((a) => a.detailFullView && !a.commandPaletteActive && a.detail),
100
-
101
- // PR list nav
102
- listNavKeymap.scope((a) => inListMode(a) && a.listNav),
103
- )
@@ -1,13 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface CloseModalCtx {
4
- readonly closeModal: () => void
5
- readonly confirmClose: () => void
6
- }
7
-
8
- const Close = context<CloseModalCtx>()
9
-
10
- export const closeModalKeymap = Close(
11
- { id: "close-modal.cancel", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
12
- { id: "close-modal.confirm", title: "Close pull request", keys: ["return"], run: (s) => s.confirmClose() },
13
- )
@@ -1,16 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface CommandPaletteCtx {
4
- readonly closeModal: () => void
5
- readonly runSelected: () => void
6
- readonly moveSelection: (delta: -1 | 1) => void
7
- }
8
-
9
- const Palette = context<CommandPaletteCtx>()
10
-
11
- export const commandPaletteKeymap = Palette(
12
- { id: "palette.close", title: "Close palette", keys: ["escape", "ctrl+c"], run: (s) => s.closeModal() },
13
- { id: "palette.run", title: "Run command", keys: ["return"], run: (s) => s.runSelected() },
14
- { id: "palette.up", title: "Up", keys: ["up"], run: (s) => s.moveSelection(-1) },
15
- { id: "palette.down", title: "Down", keys: ["down"], run: (s) => s.moveSelection(1) },
16
- )
@@ -1,73 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- /**
4
- * Single-method-per-binding interface. Each `move*` / `delete*` is a discrete
5
- * editor command; the App constructs the context so each method applies the
6
- * appropriate `editorState → editorState` transform via the existing
7
- * `editComment` helper.
8
- */
9
- export interface CommentModalCtx {
10
- readonly closeModal: () => void
11
- readonly submit: () => void
12
- readonly insertNewline: () => void
13
- readonly moveLeft: () => void
14
- readonly moveRight: () => void
15
- readonly moveUp: () => void
16
- readonly moveDown: () => void
17
- readonly moveLineStart: () => void
18
- readonly moveLineEnd: () => void
19
- readonly moveWordBackward: () => void
20
- readonly moveWordForward: () => void
21
- readonly backspace: () => void
22
- readonly deleteForward: () => void
23
- readonly deleteWordBackward: () => void
24
- readonly deleteWordForward: () => void
25
- readonly deleteToLineStart: () => void
26
- readonly deleteToLineEnd: () => void
27
- }
28
-
29
- const Comment = context<CommentModalCtx>()
30
-
31
- export const commentModalKeymap = Comment(
32
- { id: "comment.escape", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
33
- { id: "comment.submit", title: "Submit", keys: ["ctrl+s", "return"], run: (s) => s.submit() },
34
- { id: "comment.newline", title: "Insert newline", keys: ["shift+return"], run: (s) => s.insertNewline() },
35
-
36
- // Cursor movement
37
- { id: "comment.move-left", title: "Cursor left", keys: ["left", "ctrl+b"], run: (s) => s.moveLeft() },
38
- { id: "comment.move-right", title: "Cursor right", keys: ["right", "ctrl+f"], run: (s) => s.moveRight() },
39
- { id: "comment.move-up", title: "Cursor up", keys: ["up"], run: (s) => s.moveUp() },
40
- { id: "comment.move-down", title: "Cursor down", keys: ["down"], run: (s) => s.moveDown() },
41
- { id: "comment.line-start", title: "Line start", keys: ["home", "ctrl+a"], run: (s) => s.moveLineStart() },
42
- { id: "comment.line-end", title: "Line end", keys: ["end", "ctrl+e"], run: (s) => s.moveLineEnd() },
43
- {
44
- id: "comment.word-back",
45
- title: "Word backward",
46
- keys: ["meta+b", "meta+left"],
47
- run: (s) => s.moveWordBackward(),
48
- },
49
- {
50
- id: "comment.word-forward",
51
- title: "Word forward",
52
- keys: ["meta+f", "meta+right"],
53
- run: (s) => s.moveWordForward(),
54
- },
55
-
56
- // Deletion
57
- { id: "comment.backspace", title: "Backspace", keys: ["backspace"], run: (s) => s.backspace() },
58
- { id: "comment.delete", title: "Delete", keys: ["delete", "ctrl+d"], run: (s) => s.deleteForward() },
59
- {
60
- id: "comment.delete-word-back",
61
- title: "Delete word backward",
62
- keys: ["ctrl+w", "meta+backspace"],
63
- run: (s) => s.deleteWordBackward(),
64
- },
65
- {
66
- id: "comment.delete-word-forward",
67
- title: "Delete word forward",
68
- keys: ["meta+delete"],
69
- run: (s) => s.deleteWordForward(),
70
- },
71
- { id: "comment.delete-to-line-start", title: "Delete to line start", keys: ["ctrl+u"], run: (s) => s.deleteToLineStart() },
72
- { id: "comment.delete-to-line-end", title: "Delete to line end", keys: ["ctrl+k"], run: (s) => s.deleteToLineEnd() },
73
- )
@@ -1,24 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface CommentThreadModalCtx {
4
- readonly halfPage: number
5
- readonly closeModal: () => void
6
- readonly openInlineComment: () => void
7
- readonly scrollBy: (delta: number) => void
8
- }
9
-
10
- const Thread = context<CommentThreadModalCtx>()
11
-
12
- export const commentThreadModalKeymap = Thread(
13
- { id: "comment-thread.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
14
- { id: "comment-thread.reply", title: "Reply", keys: ["return", "a", "c"], run: (s) => s.openInlineComment() },
15
- { id: "comment-thread.up", title: "Up", keys: ["k", "up"], run: (s) => s.scrollBy(-1) },
16
- { id: "comment-thread.down", title: "Down", keys: ["j", "down"], run: (s) => s.scrollBy(1) },
17
- { id: "comment-thread.half-up", title: "Half page up", keys: ["pageup", "ctrl+u"], run: (s) => s.scrollBy(-s.halfPage) },
18
- {
19
- id: "comment-thread.half-down",
20
- title: "Half page down",
21
- keys: ["pagedown", "ctrl+d", "ctrl+v"],
22
- run: (s) => s.scrollBy(s.halfPage),
23
- },
24
- )
@@ -1,30 +0,0 @@
1
- import { context, type Scrollable, scrollCommands } from "@ghui/keymap"
2
-
3
- export interface DetailViewCtx extends Scrollable {
4
- readonly closeDetail: () => void
5
- readonly openTheme: () => void
6
- readonly openDiff: () => void
7
- readonly closePullRequest: () => void
8
- readonly openLabels: () => void
9
- readonly openMerge: () => void
10
- readonly toggleDraft: () => void
11
- readonly refresh: () => void
12
- readonly openInBrowser: () => void
13
- readonly copyMetadata: () => void
14
- }
15
-
16
- const Detail = context<DetailViewCtx>()
17
-
18
- export const detailViewKeymap = Detail(
19
- scrollCommands<DetailViewCtx>(),
20
- { id: "detail.close", title: "Close detail", keys: ["escape", "return"], run: (s) => s.closeDetail() },
21
- { id: "detail.theme", title: "Open theme", keys: ["t"], run: (s) => s.openTheme() },
22
- { id: "detail.diff", title: "Open diff", keys: ["d"], run: (s) => s.openDiff() },
23
- { id: "detail.close-pr", title: "Close pull request", keys: ["x"], run: (s) => s.closePullRequest() },
24
- { id: "detail.labels", title: "Manage labels", keys: ["l"], run: (s) => s.openLabels() },
25
- { id: "detail.merge", title: "Merge", keys: ["m", "shift+m"], run: (s) => s.openMerge() },
26
- { id: "detail.toggle-draft", title: "Toggle draft", keys: ["s", "shift+s"], run: (s) => s.toggleDraft() },
27
- { id: "detail.refresh", title: "Refresh", keys: ["r"], run: (s) => s.refresh() },
28
- { id: "detail.open-browser", title: "Open in browser", keys: ["o"], run: (s) => s.openInBrowser() },
29
- { id: "detail.copy", title: "Copy metadata", keys: ["y"], run: (s) => s.copyMetadata() },
30
- )
@@ -1,91 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
- import { countedVerticalBindings } from "./helpers.ts"
3
-
4
- export type DiffSide = "LEFT" | "RIGHT"
5
- export type DiffAlign = "center" | "top" | "bottom"
6
-
7
- export interface DiffViewCtx {
8
- readonly halfPage: number
9
- readonly handleEscape: () => void // closes diff, or clears comment range if active
10
- readonly openSelectedComment: () => void
11
- readonly addComment: () => void
12
- readonly toggleRange: () => void
13
- readonly toggleView: () => void
14
- readonly toggleWrap: () => void
15
- readonly reload: () => void
16
- readonly nextThread: () => void
17
- readonly previousThread: () => void
18
- readonly moveAnchor: (delta: number, opts?: { preserveViewportRow?: boolean }) => void
19
- readonly moveAnchorToBoundary: (boundary: "first" | "last") => void
20
- readonly alignAnchor: (align: DiffAlign) => void
21
- readonly selectSide: (side: DiffSide) => void
22
- readonly nextFile: () => void
23
- readonly previousFile: () => void
24
- readonly openInBrowser: () => void
25
- }
26
-
27
- const Diff = context<DiffViewCtx>()
28
-
29
- export const diffViewKeymap = Diff(
30
- { id: "diff.escape", title: "Close diff / clear range", keys: ["escape"], run: (s) => s.handleEscape() },
31
- { id: "diff.open-comment", title: "Open / add comment", keys: ["return"], run: (s) => s.openSelectedComment() },
32
- { id: "diff.add-comment", title: "Add comment on line", keys: ["a"], run: (s) => s.addComment() },
33
- { id: "diff.toggle-range", title: "Toggle comment range", keys: ["v"], run: (s) => s.toggleRange() },
34
- { id: "diff.toggle-view", title: "Toggle split/unified", keys: ["shift+v"], run: (s) => s.toggleView() },
35
- { id: "diff.toggle-wrap", title: "Toggle wrap", keys: ["w"], run: (s) => s.toggleWrap() },
36
- { id: "diff.reload", title: "Reload diff", keys: ["r"], run: (s) => s.reload() },
37
- { id: "diff.next-thread", title: "Next thread", keys: ["n"], run: (s) => s.nextThread() },
38
- { id: "diff.previous-thread", title: "Previous thread", keys: ["p"], run: (s) => s.previousThread() },
39
-
40
- // Half-page anchor moves preserve viewport row (true vim semantics)
41
- {
42
- id: "diff.half-up",
43
- title: "Half page up",
44
- keys: ["pageup", "ctrl+u"],
45
- run: (s) => s.moveAnchor(-s.halfPage, { preserveViewportRow: true }),
46
- },
47
- {
48
- id: "diff.half-down",
49
- title: "Half page down",
50
- keys: ["pagedown", "ctrl+d", "ctrl+v"],
51
- run: (s) => s.moveAnchor(s.halfPage, { preserveViewportRow: true }),
52
- },
53
-
54
- // Jump-by-8 (shift+arrow / meta+arrow / shift+letter / meta+letter)
55
- {
56
- id: "diff.jump-up",
57
- title: "Jump up",
58
- keys: ["shift+up", "shift+k", "meta+up", "meta+k"],
59
- run: (s) => s.moveAnchor(-8),
60
- },
61
- {
62
- id: "diff.jump-down",
63
- title: "Jump down",
64
- keys: ["shift+down", "shift+j", "meta+down", "meta+j"],
65
- run: (s) => s.moveAnchor(8),
66
- },
67
-
68
- // Vim count prefixes ("2 k", "15 j", etc.)
69
- ...countedVerticalBindings<DiffViewCtx>((s, delta) => s.moveAnchor(delta)),
70
-
71
- // Single-step
72
- { id: "diff.up", title: "Up", keys: ["up", "k"], run: (s) => s.moveAnchor(-1) },
73
- { id: "diff.down", title: "Down", keys: ["down", "j"], run: (s) => s.moveAnchor(1) },
74
-
75
- // Side selection
76
- { id: "diff.side-left", title: "Old side", keys: ["left", "h"], run: (s) => s.selectSide("LEFT") },
77
- { id: "diff.side-right", title: "New side", keys: ["right", "l"], run: (s) => s.selectSide("RIGHT") },
78
-
79
- // File nav
80
- { id: "diff.next-file", title: "Next file", keys: ["]"], run: (s) => s.nextFile() },
81
- { id: "diff.previous-file", title: "Previous file", keys: ["["], run: (s) => s.previousFile() },
82
-
83
- // Boundary jumps + align
84
- { id: "diff.first", title: "First comment", keys: ["g g"], run: (s) => s.moveAnchorToBoundary("first") },
85
- { id: "diff.last", title: "Last comment", keys: ["shift+g"], run: (s) => s.moveAnchorToBoundary("last") },
86
- { id: "diff.align-center", title: "Align center", keys: ["z z"], run: (s) => s.alignAnchor("center") },
87
- { id: "diff.align-top", title: "Align top", keys: ["z t"], run: (s) => s.alignAnchor("top") },
88
- { id: "diff.align-bottom", title: "Align bottom", keys: ["z b"], run: (s) => s.alignAnchor("bottom") },
89
-
90
- { id: "diff.open-browser", title: "Open in browser", keys: ["o"], run: (s) => s.openInBrowser() },
91
- )
@@ -1,13 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface FilterModeCtx {
4
- readonly cancel: () => void
5
- readonly commit: () => void
6
- }
7
-
8
- const Filter = context<FilterModeCtx>()
9
-
10
- export const filterModeKeymap = Filter(
11
- { id: "filter-mode.cancel", title: "Cancel filter", keys: ["escape"], run: (s) => s.cancel() },
12
- { id: "filter-mode.commit", title: "Apply filter", keys: ["return"], run: (s) => s.commit() },
13
- )
@@ -1,24 +0,0 @@
1
- import type { CommandConfig } from "@ghui/keymap"
2
-
3
- const MAX_COUNT_PREFIX = 99
4
-
5
- const countSequence = (count: number, key: string) =>
6
- `${String(count).split("").join(" ")} ${key}`
7
-
8
- /**
9
- * Vim-style "count prefix" navigation: `2 k` moves by 2, `15 j` by 15, etc.
10
- * Generates bindings for `count` × {k, up, j, down} for count ∈ [1, 99].
11
- *
12
- * Each generated binding is anonymous (no meta) — they don't belong in the
13
- * palette. Only `moveBy` semantics matter.
14
- */
15
- export const countedVerticalBindings = <C>(
16
- moveBy: (ctx: C, delta: number) => void,
17
- ): readonly CommandConfig<C>[] => {
18
- const out: CommandConfig<C>[] = []
19
- for (let count = 1; count <= MAX_COUNT_PREFIX; count++) {
20
- out.push({ keys: [countSequence(count, "k"), countSequence(count, "up")], run: (s) => moveBy(s, -count) })
21
- out.push({ keys: [countSequence(count, "j"), countSequence(count, "down")], run: (s) => moveBy(s, count) })
22
- }
23
- return out
24
- }
@@ -1,16 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface LabelModalCtx {
4
- readonly closeModal: () => void
5
- readonly toggleSelected: () => void
6
- readonly moveSelection: (delta: -1 | 1) => void
7
- }
8
-
9
- const Label = context<LabelModalCtx>()
10
-
11
- export const labelModalKeymap = Label(
12
- { id: "label-modal.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
13
- { id: "label-modal.toggle", title: "Toggle label", keys: ["return"], run: (s) => s.toggleSelected() },
14
- { id: "label-modal.up", title: "Up", keys: ["k", "up"], run: (s) => s.moveSelection(-1) },
15
- { id: "label-modal.down", title: "Down", keys: ["j", "down"], run: (s) => s.moveSelection(1) },
16
- )
@@ -1,119 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
- import { countedVerticalBindings } from "./helpers.ts"
3
-
4
- export interface ListNavCtx {
5
- readonly halfPage: number
6
- readonly visibleCount: number
7
- readonly hasFilter: boolean
8
- readonly canScrollDetailPreview: boolean
9
- readonly runCommandById: (id: string) => void
10
- readonly switchQueueMode: (delta: 1 | -1) => void
11
- readonly scrollDetailPreviewBy: (delta: number) => void
12
- readonly scrollDetailPreviewTo: (line: number) => void
13
- readonly clearFilter: () => void
14
- readonly stepSelected: (delta: number) => void
15
- readonly stepSelectedUp: (count?: number) => void
16
- readonly stepSelectedDown: (count?: number) => void
17
- readonly stepSelectedUpWrap: () => void
18
- readonly stepSelectedDownWithLoadMore: () => void
19
- readonly moveSelectedToPreviousGroup: () => void
20
- readonly moveSelectedToNextGroup: () => void
21
- readonly setSelected: (index: number) => void
22
- }
23
-
24
- const List = context<ListNavCtx>()
25
-
26
- export const listNavKeymap = List(
27
- // Single-key command shortcuts (delegate to existing AppCommand registry)
28
- { id: "list.filter", title: "Filter", keys: ["/"], run: (s) => s.runCommandById("filter.open") },
29
- { id: "list.refresh", title: "Refresh", keys: ["r"], run: (s) => s.runCommandById("pull.refresh") },
30
- { id: "list.theme", title: "Theme", keys: ["t"], run: (s) => s.runCommandById("theme.open") },
31
- { id: "list.diff", title: "Open diff", keys: ["d"], run: (s) => s.runCommandById("diff.open") },
32
- { id: "list.labels", title: "Labels", keys: ["l"], run: (s) => s.runCommandById("pull.labels") },
33
- { id: "list.merge", title: "Merge", keys: ["m", "shift+m"], run: (s) => s.runCommandById("pull.merge") },
34
- { id: "list.close-pr", title: "Close PR", keys: ["x"], run: (s) => s.runCommandById("pull.close") },
35
- { id: "list.open-browser", title: "Open in browser", keys: ["o"], run: (s) => s.runCommandById("pull.open-browser") },
36
- { id: "list.toggle-draft", title: "Toggle draft", keys: ["s", "shift+s"], run: (s) => s.runCommandById("pull.toggle-draft") },
37
- { id: "list.copy", title: "Copy metadata", keys: ["y"], run: (s) => s.runCommandById("pull.copy-metadata") },
38
- { id: "list.detail.open", title: "Open details", keys: ["return"], run: (s) => s.runCommandById("detail.open") },
39
-
40
- // Queue mode tabs
41
- { id: "list.next-tab", title: "Next view", keys: ["tab"], run: (s) => s.switchQueueMode(1) },
42
- { id: "list.prev-tab", title: "Previous view", keys: ["shift+tab"], run: (s) => s.switchQueueMode(-1) },
43
-
44
- // Escape clears filter only when one is set
45
- {
46
- id: "list.clear-filter",
47
- title: "Clear filter",
48
- keys: ["escape"],
49
- enabled: (s) => s.hasFilter ? true : "No filter to clear.",
50
- run: (s) => s.clearFilter(),
51
- },
52
-
53
- // Wide-layout detail preview scroll
54
- {
55
- id: "list.preview.top",
56
- title: "Detail preview top",
57
- keys: ["home"],
58
- enabled: (s) => s.canScrollDetailPreview ? true : "Detail preview not visible.",
59
- run: (s) => s.scrollDetailPreviewTo(0),
60
- },
61
- {
62
- id: "list.preview.bottom",
63
- title: "Detail preview bottom",
64
- keys: ["end"],
65
- enabled: (s) => s.canScrollDetailPreview ? true : "Detail preview not visible.",
66
- run: (s) => s.scrollDetailPreviewTo(Number.MAX_SAFE_INTEGER),
67
- },
68
- {
69
- id: "list.preview.half-up",
70
- title: "Detail preview ½ up",
71
- keys: ["pageup"],
72
- enabled: (s) => s.canScrollDetailPreview ? true : "Detail preview not visible.",
73
- run: (s) => s.scrollDetailPreviewBy(-s.halfPage),
74
- },
75
- {
76
- id: "list.preview.half-down",
77
- title: "Detail preview ½ down",
78
- keys: ["pagedown"],
79
- enabled: (s) => s.canScrollDetailPreview ? true : "Detail preview not visible.",
80
- run: (s) => s.scrollDetailPreviewBy(s.halfPage),
81
- },
82
-
83
- // Group jumps
84
- {
85
- id: "list.group-prev",
86
- title: "Previous group",
87
- keys: ["[", "meta+up", "meta+k", "shift+k"],
88
- run: (s) => s.moveSelectedToPreviousGroup(),
89
- },
90
- {
91
- id: "list.group-next",
92
- title: "Next group",
93
- keys: ["]", "meta+down", "meta+j", "shift+j"],
94
- run: (s) => s.moveSelectedToNextGroup(),
95
- },
96
-
97
- // Half-page steps
98
- { id: "list.half-up", title: "Half page up", keys: ["ctrl+u"], run: (s) => s.stepSelected(-s.halfPage) },
99
- { id: "list.half-down", title: "Half page down", keys: ["ctrl+d"], run: (s) => s.stepSelected(s.halfPage) },
100
-
101
- // Vim count prefixes
102
- ...countedVerticalBindings<ListNavCtx>((s, delta) => {
103
- if (delta < 0) s.stepSelectedUp(-delta)
104
- else s.stepSelectedDown(delta)
105
- }),
106
-
107
- // Single-step (with wrap up, load-more on down)
108
- { id: "list.up", title: "Up", keys: ["up", "k"], run: (s) => s.stepSelectedUpWrap() },
109
- { id: "list.down", title: "Down", keys: ["down", "j"], run: (s) => s.stepSelectedDownWithLoadMore() },
110
-
111
- // Top / bottom
112
- { id: "list.top", title: "Top", keys: ["g g"], run: (s) => s.setSelected(0) },
113
- {
114
- id: "list.bottom",
115
- title: "Bottom",
116
- keys: ["shift+g"],
117
- run: (s) => s.setSelected(s.visibleCount === 0 ? 0 : s.visibleCount - 1),
118
- },
119
- )
@@ -1,23 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface MergeModalCtx {
4
- readonly availableActionCount: number
5
- readonly closeModal: () => void
6
- readonly confirmMerge: () => void
7
- readonly moveSelection: (delta: -1 | 1) => void
8
- }
9
-
10
- const Merge = context<MergeModalCtx>()
11
-
12
- export const mergeModalKeymap = Merge(
13
- { id: "merge-modal.cancel", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
14
- {
15
- id: "merge-modal.confirm",
16
- title: "Merge pull request",
17
- keys: ["return"],
18
- enabled: (s) => s.availableActionCount > 0 ? true : "No merge actions available.",
19
- run: (s) => s.confirmMerge(),
20
- },
21
- { id: "merge-modal.up", title: "Up", keys: ["k", "up"], run: (s) => s.moveSelection(-1) },
22
- { id: "merge-modal.down", title: "Down", keys: ["j", "down"], run: (s) => s.moveSelection(1) },
23
- )
@@ -1,13 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface OpenRepositoryModalCtx {
4
- readonly closeModal: () => void
5
- readonly openFromInput: () => void
6
- }
7
-
8
- const OpenRepo = context<OpenRepositoryModalCtx>()
9
-
10
- export const openRepositoryModalKeymap = OpenRepo(
11
- { id: "open-repo.close", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
12
- { id: "open-repo.open", title: "Open repository", keys: ["return"], run: (s) => s.openFromInput() },
13
- )