@kitlangton/ghui 0.3.0 → 0.3.3

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 (55) hide show
  1. package/README.md +13 -3
  2. package/bin/ghui.js +6 -1
  3. package/dist/index.js +9456 -0
  4. package/package.json +7 -4
  5. package/src/App.tsx +0 -2779
  6. package/src/appCommands.ts +0 -409
  7. package/src/commands.ts +0 -73
  8. package/src/config.ts +0 -20
  9. package/src/date.ts +0 -20
  10. package/src/domain.ts +0 -149
  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 -116
  15. package/src/keymap/changedFilesModal.ts +0 -23
  16. package/src/keymap/closeModal.ts +0 -13
  17. package/src/keymap/commandPalette.ts +0 -16
  18. package/src/keymap/commentModal.ts +0 -73
  19. package/src/keymap/commentThreadModal.ts +0 -24
  20. package/src/keymap/detailView.ts +0 -30
  21. package/src/keymap/diffView.ts +0 -93
  22. package/src/keymap/filterMode.ts +0 -13
  23. package/src/keymap/helpers.ts +0 -24
  24. package/src/keymap/labelModal.ts +0 -16
  25. package/src/keymap/listNav.ts +0 -119
  26. package/src/keymap/mergeModal.ts +0 -23
  27. package/src/keymap/openRepositoryModal.ts +0 -13
  28. package/src/keymap/submitReviewModal.ts +0 -48
  29. package/src/keymap/themeModal.ts +0 -49
  30. package/src/mergeActions.ts +0 -88
  31. package/src/observability.ts +0 -46
  32. package/src/pullRequestCache.ts +0 -19
  33. package/src/pullRequestViews.ts +0 -43
  34. package/src/services/BrowserOpener.ts +0 -22
  35. package/src/services/Clipboard.ts +0 -46
  36. package/src/services/CommandRunner.ts +0 -91
  37. package/src/services/GitHubService.ts +0 -756
  38. package/src/services/MockGitHubService.ts +0 -182
  39. package/src/themeStore.ts +0 -60
  40. package/src/ui/CommandPalette.tsx +0 -176
  41. package/src/ui/DetailsPane.tsx +0 -656
  42. package/src/ui/FooterHints.tsx +0 -90
  43. package/src/ui/LoadingLogo.tsx +0 -75
  44. package/src/ui/PullRequestDiffPane.tsx +0 -249
  45. package/src/ui/PullRequestList.tsx +0 -194
  46. package/src/ui/colors.ts +0 -821
  47. package/src/ui/commentEditor.ts +0 -126
  48. package/src/ui/comments.tsx +0 -143
  49. package/src/ui/diff.ts +0 -650
  50. package/src/ui/diffStats.tsx +0 -25
  51. package/src/ui/modals.tsx +0 -964
  52. package/src/ui/primitives.tsx +0 -350
  53. package/src/ui/pullRequests.ts +0 -106
  54. package/src/ui/singleLineInput.ts +0 -26
  55. 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,116 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
- import { changedFilesModalKeymap, type ChangedFilesModalCtx } from "./changedFilesModal.ts"
3
- import { closeModalKeymap, type CloseModalCtx } from "./closeModal.ts"
4
- import { commandPaletteKeymap, type CommandPaletteCtx } from "./commandPalette.ts"
5
- import { commentModalKeymap, type CommentModalCtx } from "./commentModal.ts"
6
- import { commentThreadModalKeymap, type CommentThreadModalCtx } from "./commentThreadModal.ts"
7
- import { detailViewKeymap, type DetailViewCtx } from "./detailView.ts"
8
- import { diffViewKeymap, type DiffViewCtx } from "./diffView.ts"
9
- import { filterModeKeymap, type FilterModeCtx } from "./filterMode.ts"
10
- import { labelModalKeymap, type LabelModalCtx } from "./labelModal.ts"
11
- import { listNavKeymap, type ListNavCtx } from "./listNav.ts"
12
- import { mergeModalKeymap, type MergeModalCtx } from "./mergeModal.ts"
13
- import { openRepositoryModalKeymap, type OpenRepositoryModalCtx } from "./openRepositoryModal.ts"
14
- import { submitReviewModalKeymap, type SubmitReviewModalCtx } from "./submitReviewModal.ts"
15
- import { themeModalKeymap, type ThemeModalCtx } from "./themeModal.ts"
16
-
17
- export interface AppCtx {
18
- // Active flags
19
- readonly closeModalActive: boolean
20
- readonly mergeModalActive: boolean
21
- readonly commentThreadModalActive: boolean
22
- readonly changedFilesModalActive: boolean
23
- readonly submitReviewModalActive: boolean
24
- readonly labelModalActive: boolean
25
- readonly themeModalActive: boolean
26
- readonly openRepositoryModalActive: boolean
27
- readonly commentModalActive: boolean
28
- readonly commandPaletteActive: boolean
29
- readonly filterMode: boolean
30
- readonly diffFullView: boolean
31
- readonly detailFullView: boolean
32
-
33
- // True whenever a modal/mode swallows raw text input (so q-quit, etc. are
34
- // disabled inside text-editing contexts).
35
- readonly textInputActive: boolean
36
-
37
- // Per-layer narrow contexts
38
- readonly closeModal: CloseModalCtx
39
- readonly mergeModal: MergeModalCtx
40
- readonly commentThreadModal: CommentThreadModalCtx
41
- readonly changedFilesModal: ChangedFilesModalCtx
42
- readonly submitReviewModal: SubmitReviewModalCtx
43
- readonly labelModal: LabelModalCtx
44
- readonly themeModal: ThemeModalCtx
45
- readonly openRepositoryModal: OpenRepositoryModalCtx
46
- readonly commentModal: CommentModalCtx
47
- readonly commandPalette: CommandPaletteCtx
48
- readonly filterModeCtx: FilterModeCtx
49
- readonly diff: DiffViewCtx
50
- readonly detail: DetailViewCtx
51
- readonly listNav: ListNavCtx
52
-
53
- // Always-on / app-level
54
- readonly openCommandPalette: () => void
55
- readonly handleQuitOrClose: () => void
56
- }
57
-
58
- const App = context<AppCtx>()
59
-
60
- const modalActive = (a: AppCtx): boolean =>
61
- a.closeModalActive
62
- || a.mergeModalActive
63
- || a.commentThreadModalActive
64
- || a.changedFilesModalActive
65
- || a.submitReviewModalActive
66
- || a.labelModalActive
67
- || a.themeModalActive
68
- || a.openRepositoryModalActive
69
- || a.commentModalActive
70
- || a.commandPaletteActive
71
-
72
- const inListMode = (a: AppCtx): boolean =>
73
- !modalActive(a)
74
- && !a.filterMode
75
- && !a.diffFullView
76
- && !a.detailFullView
77
-
78
- export const appKeymap = App(
79
- // Always-on: command palette opener
80
- { id: "command.open", title: "Open command palette", keys: ["ctrl+p", "meta+k"], run: (s) => s.openCommandPalette() },
81
-
82
- // Quit / close-active-modal — gated to "not editing text"
83
- {
84
- id: "app.quit-or-close",
85
- title: "Quit / close modal",
86
- keys: ["ctrl+c"],
87
- run: (s) => s.handleQuitOrClose(),
88
- },
89
- {
90
- id: "app.quit-or-close-q",
91
- title: "Quit / close modal",
92
- keys: ["q"],
93
- when: (s) => !s.textInputActive,
94
- run: (s) => s.handleQuitOrClose(),
95
- },
96
-
97
- // Modal layers
98
- closeModalKeymap.scope((a) => a.closeModalActive && a.closeModal),
99
- mergeModalKeymap.scope((a) => a.mergeModalActive && a.mergeModal),
100
- commentThreadModalKeymap.scope((a) => a.commentThreadModalActive && a.commentThreadModal),
101
- changedFilesModalKeymap.scope((a) => a.changedFilesModalActive && a.changedFilesModal),
102
- submitReviewModalKeymap.scope((a) => a.submitReviewModalActive && a.submitReviewModal),
103
- labelModalKeymap.scope((a) => a.labelModalActive && a.labelModal),
104
- themeModalKeymap.scope((a) => a.themeModalActive && a.themeModal),
105
- openRepositoryModalKeymap.scope((a) => a.openRepositoryModalActive && a.openRepositoryModal),
106
- commentModalKeymap.scope((a) => a.commentModalActive && a.commentModal),
107
- commandPaletteKeymap.scope((a) => a.commandPaletteActive && a.commandPalette),
108
- filterModeKeymap.scope((a) => a.filterMode && a.filterModeCtx),
109
-
110
- // Full-view layers (only when no modal is on top)
111
- diffViewKeymap.scope((a) => a.diffFullView && !modalActive(a) && a.diff),
112
- detailViewKeymap.scope((a) => a.detailFullView && !modalActive(a) && a.detail),
113
-
114
- // PR list nav
115
- listNavKeymap.scope((a) => inListMode(a) && a.listNav),
116
- )
@@ -1,23 +0,0 @@
1
- import { context } from "@ghui/keymap"
2
-
3
- export interface ChangedFilesModalCtx {
4
- readonly hasResults: boolean
5
- readonly closeModal: () => void
6
- readonly selectFile: () => void
7
- readonly moveSelection: (delta: -1 | 1) => void
8
- }
9
-
10
- const ChangedFiles = context<ChangedFilesModalCtx>()
11
-
12
- export const changedFilesModalKeymap = ChangedFiles(
13
- { id: "changed-files.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
14
- {
15
- id: "changed-files.select",
16
- title: "Jump to file",
17
- keys: ["return"],
18
- enabled: (s) => s.hasResults ? true : "No matching files.",
19
- run: (s) => s.selectFile(),
20
- },
21
- { id: "changed-files.up", title: "Up", keys: ["k", "up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
22
- { id: "changed-files.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
23
- )
@@ -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", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
15
- { id: "palette.down", title: "Down", keys: ["down", "ctrl+n", "ctrl+j"], 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"], 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,93 +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 toggleRange: () => void
12
- readonly toggleView: () => void
13
- readonly toggleWrap: () => void
14
- readonly reload: () => void
15
- readonly nextThread: () => void
16
- readonly previousThread: () => void
17
- readonly moveAnchor: (delta: number, opts?: { preserveViewportRow?: boolean }) => void
18
- readonly moveAnchorToBoundary: (boundary: "first" | "last") => void
19
- readonly alignAnchor: (align: DiffAlign) => void
20
- readonly selectSide: (side: DiffSide) => void
21
- readonly openChangedFiles: () => void
22
- readonly openSubmitReview: () => void
23
- readonly nextFile: () => void
24
- readonly previousFile: () => void
25
- readonly openInBrowser: () => void
26
- }
27
-
28
- const Diff = context<DiffViewCtx>()
29
-
30
- export const diffViewKeymap = Diff(
31
- { id: "diff.escape", title: "Close diff / clear range", keys: ["escape"], run: (s) => s.handleEscape() },
32
- { id: "diff.open-comment", title: "Open / add comment", keys: ["return"], run: (s) => s.openSelectedComment() },
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.changed-files", title: "Changed files", keys: ["f"], run: (s) => s.openChangedFiles() },
81
- { id: "diff.next-file", title: "Next file", keys: ["]"], run: (s) => s.nextFile() },
82
- { id: "diff.previous-file", title: "Previous file", keys: ["["], run: (s) => s.previousFile() },
83
- { id: "diff.submit-review", title: "Submit review", keys: ["shift+r"], run: (s) => s.openSubmitReview() },
84
-
85
- // Boundary jumps + align
86
- { id: "diff.first", title: "First comment", keys: ["g g"], run: (s) => s.moveAnchorToBoundary("first") },
87
- { id: "diff.last", title: "Last comment", keys: ["shift+g"], run: (s) => s.moveAnchorToBoundary("last") },
88
- { id: "diff.align-center", title: "Align center", keys: ["z z"], run: (s) => s.alignAnchor("center") },
89
- { id: "diff.align-top", title: "Align top", keys: ["z t"], run: (s) => s.alignAnchor("top") },
90
- { id: "diff.align-bottom", title: "Align bottom", keys: ["z b"], run: (s) => s.alignAnchor("bottom") },
91
-
92
- { id: "diff.open-browser", title: "Open in browser", keys: ["o"], run: (s) => s.openInBrowser() },
93
- )
@@ -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", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
15
- { id: "label-modal.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], 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
- )