@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.
- package/README.md +1 -4
- package/bin/ghui.js +6 -1
- package/dist/index.js +9419 -0
- package/package.json +7 -4
- package/src/App.tsx +0 -2604
- package/src/appCommands.ts +0 -384
- package/src/commands.ts +0 -73
- package/src/config.ts +0 -28
- package/src/date.ts +0 -20
- package/src/domain.ts +0 -139
- package/src/errors.ts +0 -10
- package/src/index.tsx +0 -50
- package/src/keyboard/opentuiAdapter.ts +0 -43
- package/src/keymap/all.ts +0 -103
- package/src/keymap/closeModal.ts +0 -13
- package/src/keymap/commandPalette.ts +0 -16
- package/src/keymap/commentModal.ts +0 -73
- package/src/keymap/commentThreadModal.ts +0 -24
- package/src/keymap/detailView.ts +0 -30
- package/src/keymap/diffView.ts +0 -91
- package/src/keymap/filterMode.ts +0 -13
- package/src/keymap/helpers.ts +0 -24
- package/src/keymap/labelModal.ts +0 -16
- package/src/keymap/listNav.ts +0 -119
- package/src/keymap/mergeModal.ts +0 -23
- package/src/keymap/openRepositoryModal.ts +0 -13
- package/src/keymap/themeModal.ts +0 -49
- package/src/mergeActions.ts +0 -88
- package/src/observability.ts +0 -46
- package/src/pullRequestCache.ts +0 -19
- package/src/pullRequestViews.ts +0 -45
- package/src/services/BrowserOpener.ts +0 -22
- package/src/services/Clipboard.ts +0 -46
- package/src/services/CommandRunner.ts +0 -91
- package/src/services/GitHubService.ts +0 -741
- package/src/services/MockGitHubService.ts +0 -181
- package/src/themeStore.ts +0 -60
- package/src/ui/CommandPalette.tsx +0 -216
- package/src/ui/DetailsPane.tsx +0 -656
- package/src/ui/FooterHints.tsx +0 -90
- package/src/ui/LoadingLogo.tsx +0 -75
- package/src/ui/PullRequestDiffPane.tsx +0 -248
- package/src/ui/PullRequestList.tsx +0 -210
- package/src/ui/colors.ts +0 -814
- package/src/ui/commentEditor.ts +0 -126
- package/src/ui/comments.tsx +0 -143
- package/src/ui/diff.ts +0 -650
- package/src/ui/diffStats.tsx +0 -25
- package/src/ui/modals.tsx +0 -614
- package/src/ui/primitives.tsx +0 -205
- package/src/ui/pullRequests.ts +0 -106
- package/src/ui/singleLineInput.ts +0 -26
- 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
|
-
)
|
package/src/keymap/closeModal.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/detailView.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/diffView.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/filterMode.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/helpers.ts
DELETED
|
@@ -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
|
-
}
|
package/src/keymap/labelModal.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/listNav.ts
DELETED
|
@@ -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
|
-
)
|
package/src/keymap/mergeModal.ts
DELETED
|
@@ -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
|
-
)
|