@kitlangton/ghui 0.1.20 → 0.1.21
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/package.json +13 -6
- package/src/App.tsx +312 -305
- package/src/commands.ts +5 -0
- package/src/index.tsx +7 -1
- package/src/ui/CommandPalette.tsx +18 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitlangton/ghui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Terminal UI for GitHub pull requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,19 +42,26 @@
|
|
|
42
42
|
"start": "bun run src/index.tsx",
|
|
43
43
|
"start:mock": "GHUI_MOCK_PR_COUNT=400 GHUI_MOCK_REPO_COUNT=4 bun run src/index.tsx",
|
|
44
44
|
"test": "bun test test",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "oxlint --tsconfig tsconfig.json src/ test/",
|
|
47
|
+
"format": "oxfmt src/ test/",
|
|
48
|
+
"format:check": "oxfmt --check src/ test/"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@effect/atom-react": "4.0.0-beta.
|
|
49
|
-
"@opentui/core": "0.1
|
|
50
|
-
"@opentui/
|
|
51
|
-
"
|
|
51
|
+
"@effect/atom-react": "4.0.0-beta.59",
|
|
52
|
+
"@opentui/core": "0.2.1",
|
|
53
|
+
"@opentui/keymap": "0.2.1",
|
|
54
|
+
"@opentui/react": "0.2.1",
|
|
55
|
+
"effect": "4.0.0-beta.59",
|
|
52
56
|
"react": "19.2.5",
|
|
53
57
|
"scheduler": "0.27.0"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
60
|
+
"@effect/language-service": "0.85.1",
|
|
56
61
|
"@types/bun": "1.3.12",
|
|
57
62
|
"@types/react": "19.2.14",
|
|
63
|
+
"oxfmt": "0.47.0",
|
|
64
|
+
"oxlint": "1.62.0",
|
|
58
65
|
"typescript": "6.0.2"
|
|
59
66
|
}
|
|
60
67
|
}
|
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
+
import { useBindings } from "@opentui/keymap/react"
|
|
3
4
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
5
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
5
6
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
@@ -8,7 +9,7 @@ import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
|
|
|
8
9
|
import { useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
9
10
|
import { buildAppCommands } from "./appCommands.js"
|
|
10
11
|
import type { AppCommand } from "./commands.js"
|
|
11
|
-
import { clampCommandIndex, commandEnabled, filterCommands } from "./commands.js"
|
|
12
|
+
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
12
13
|
import { config } from "./config.js"
|
|
13
14
|
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
14
15
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
@@ -108,6 +109,7 @@ const DIFF_STICKY_HEADER_LINES = 2
|
|
|
108
109
|
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
109
110
|
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
110
111
|
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
112
|
+
const LOAD_MORE_SCROLL_THRESHOLD = 3
|
|
111
113
|
const DETAIL_PREFETCH_BEHIND = 1
|
|
112
114
|
const DETAIL_PREFETCH_AHEAD = 3
|
|
113
115
|
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
@@ -904,6 +906,20 @@ export const App = () => {
|
|
|
904
906
|
if (selectedIndex >= thresholdIndex) loadMorePullRequests()
|
|
905
907
|
}, [selectedIndex, visiblePullRequests.length, filterMode, filterQuery, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
906
908
|
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
if (filterMode || filterQuery.length > 0 || visiblePullRequests.length === 0 || detailFullView || diffFullView) return
|
|
911
|
+
if (!hasMorePullRequests || isLoadingMorePullRequests) return
|
|
912
|
+
const checkScroll = () => {
|
|
913
|
+
const scroll = prListScrollRef.current
|
|
914
|
+
if (!scroll || scroll.viewport.height <= 0) return
|
|
915
|
+
const bottom = scroll.scrollTop + scroll.viewport.height
|
|
916
|
+
if (bottom >= scroll.scrollHeight - LOAD_MORE_SCROLL_THRESHOLD) loadMorePullRequests()
|
|
917
|
+
}
|
|
918
|
+
checkScroll()
|
|
919
|
+
const interval = globalThis.setInterval(checkScroll, 120)
|
|
920
|
+
return () => globalThis.clearInterval(interval)
|
|
921
|
+
}, [visiblePullRequests.length, filterMode, filterQuery, detailFullView, diffFullView, hasMorePullRequests, isLoadingMorePullRequests, currentQueueCacheKey])
|
|
922
|
+
|
|
907
923
|
useEffect(() => {
|
|
908
924
|
const scroll = prListScrollRef.current
|
|
909
925
|
if (!scroll || selectedPullRequestRowIndex === null) return
|
|
@@ -1739,69 +1755,324 @@ export const App = () => {
|
|
|
1739
1755
|
const command = appCommands.find((entry) => entry.id === id)
|
|
1740
1756
|
return command ? runCommand(command, options) : false
|
|
1741
1757
|
}
|
|
1742
|
-
const
|
|
1758
|
+
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1759
|
+
if (!commandPaletteActive) return []
|
|
1760
|
+
const repository = parseRepositoryInput(commandPalette.query)
|
|
1761
|
+
if (!repository || repository === selectedRepository) return []
|
|
1762
|
+
return [defineCommand({
|
|
1763
|
+
id: `view.repository.dynamic:${repository}`,
|
|
1764
|
+
title: `Open ${repository}`,
|
|
1765
|
+
scope: "View",
|
|
1766
|
+
subtitle: "Switch to this repository",
|
|
1767
|
+
run: () => switchViewTo({ _tag: "Repository", repository }),
|
|
1768
|
+
})]
|
|
1769
|
+
})()
|
|
1770
|
+
// Dynamic commands always pin to the top of the palette; they came directly from the
|
|
1771
|
+
// user's typed input so they shouldn't be filtered by fuzzy score against themselves.
|
|
1772
|
+
const commandPaletteCommands = commandPaletteActive
|
|
1773
|
+
? [
|
|
1774
|
+
...dynamicPaletteCommands,
|
|
1775
|
+
...sortCommandsByScope(filterCommands(appCommands.filter((command) => command.id !== "command.open" && commandEnabled(command)), commandPalette.query)),
|
|
1776
|
+
]
|
|
1777
|
+
: []
|
|
1743
1778
|
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1744
1779
|
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1745
1780
|
|
|
1781
|
+
// Keymap migration phase 2: simple cmd-id bindings move out of useKeyboard.
|
|
1782
|
+
// Gated to "global mode" — no modal active, no full-view, not in filter editing —
|
|
1783
|
+
// so these don't dispatch on top of modal-specific handlers below.
|
|
1784
|
+
const globalKeymapActiveRef = useRef(false)
|
|
1785
|
+
globalKeymapActiveRef.current = !commandPaletteActive
|
|
1786
|
+
&& !openRepositoryModalActive
|
|
1787
|
+
&& !labelModalActive
|
|
1788
|
+
&& !commentModalActive
|
|
1789
|
+
&& !commentThreadModalActive
|
|
1790
|
+
&& !closeModalActive
|
|
1791
|
+
&& !mergeModalActive
|
|
1792
|
+
&& !themeModalActive
|
|
1793
|
+
&& !diffFullView
|
|
1794
|
+
&& !detailFullView
|
|
1795
|
+
&& !filterMode
|
|
1796
|
+
const runCommandByIdRef = useRef(runCommandById)
|
|
1797
|
+
runCommandByIdRef.current = runCommandById
|
|
1798
|
+
useBindings(() => ({
|
|
1799
|
+
enabled: () => globalKeymapActiveRef.current,
|
|
1800
|
+
bindings: [
|
|
1801
|
+
{ key: "/", cmd: () => runCommandByIdRef.current("filter.open") },
|
|
1802
|
+
{ key: "r", cmd: () => runCommandByIdRef.current("pull.refresh") },
|
|
1803
|
+
{ key: "t", cmd: () => runCommandByIdRef.current("theme.open") },
|
|
1804
|
+
{ key: "d", cmd: () => runCommandByIdRef.current("diff.open") },
|
|
1805
|
+
{ key: "l", cmd: () => runCommandByIdRef.current("pull.labels") },
|
|
1806
|
+
{ key: "m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1807
|
+
{ key: "shift+m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1808
|
+
{ key: "x", cmd: () => runCommandByIdRef.current("pull.close") },
|
|
1809
|
+
{ key: "o", cmd: () => runCommandByIdRef.current("pull.open-browser") },
|
|
1810
|
+
{ key: "s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1811
|
+
{ key: "shift+s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1812
|
+
{ key: "y", cmd: () => runCommandByIdRef.current("pull.copy-metadata") },
|
|
1813
|
+
{ key: "return", cmd: () => runCommandByIdRef.current("detail.open") },
|
|
1814
|
+
],
|
|
1815
|
+
}), [])
|
|
1816
|
+
// Always-on bindings — work even while modals are open.
|
|
1817
|
+
useBindings(() => ({
|
|
1818
|
+
bindings: [
|
|
1819
|
+
{ key: "ctrl+p", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1820
|
+
{ key: "meta+k", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1821
|
+
],
|
|
1822
|
+
}), [])
|
|
1823
|
+
|
|
1824
|
+
// CloseModal: escape closes, enter confirms.
|
|
1825
|
+
const closeModalActiveRef = useRef(false)
|
|
1826
|
+
closeModalActiveRef.current = closeModalActive
|
|
1827
|
+
const closeActiveModalRef = useRef(closeActiveModal)
|
|
1828
|
+
closeActiveModalRef.current = closeActiveModal
|
|
1829
|
+
const confirmClosePullRequestRef = useRef(confirmClosePullRequest)
|
|
1830
|
+
confirmClosePullRequestRef.current = confirmClosePullRequest
|
|
1831
|
+
useBindings(() => ({
|
|
1832
|
+
enabled: () => closeModalActiveRef.current,
|
|
1833
|
+
bindings: [
|
|
1834
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1835
|
+
{ key: "return", cmd: () => confirmClosePullRequestRef.current() },
|
|
1836
|
+
],
|
|
1837
|
+
}), [])
|
|
1838
|
+
|
|
1839
|
+
// MergeModal: escape, enter (when options>0), up/down/j/k navigation.
|
|
1840
|
+
const mergeModalActiveRef = useRef(false)
|
|
1841
|
+
mergeModalActiveRef.current = mergeModalActive
|
|
1842
|
+
const mergeModalContextRef = useRef({ availableCount: 0, confirm: confirmMergeAction, setMergeModal })
|
|
1843
|
+
mergeModalContextRef.current = {
|
|
1844
|
+
availableCount: availableMergeActions(mergeModal.info).length,
|
|
1845
|
+
confirm: confirmMergeAction,
|
|
1846
|
+
setMergeModal,
|
|
1847
|
+
}
|
|
1848
|
+
const moveMergeSelection = (delta: -1 | 1) => mergeModalContextRef.current.setMergeModal((current) => {
|
|
1849
|
+
const max = Math.max(0, mergeModalContextRef.current.availableCount - 1)
|
|
1850
|
+
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1851
|
+
})
|
|
1852
|
+
useBindings(() => ({
|
|
1853
|
+
enabled: () => mergeModalActiveRef.current,
|
|
1854
|
+
bindings: [
|
|
1855
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1856
|
+
{ key: "return", cmd: () => {
|
|
1857
|
+
if (mergeModalContextRef.current.availableCount > 0) mergeModalContextRef.current.confirm()
|
|
1858
|
+
} },
|
|
1859
|
+
{ key: "up", cmd: () => moveMergeSelection(-1) },
|
|
1860
|
+
{ key: "k", cmd: () => moveMergeSelection(-1) },
|
|
1861
|
+
{ key: "down", cmd: () => moveMergeSelection(1) },
|
|
1862
|
+
{ key: "j", cmd: () => moveMergeSelection(1) },
|
|
1863
|
+
],
|
|
1864
|
+
}), [])
|
|
1865
|
+
|
|
1866
|
+
// CommentThreadModal: scroll the thread, shortcut to compose a reply.
|
|
1867
|
+
const commentThreadModalActiveRef = useRef(false)
|
|
1868
|
+
commentThreadModalActiveRef.current = commentThreadModalActive
|
|
1869
|
+
const commentThreadCtxRef = useRef({ openDiffCommentModal, setCommentThreadModal, halfPage })
|
|
1870
|
+
commentThreadCtxRef.current = { openDiffCommentModal, setCommentThreadModal, halfPage }
|
|
1871
|
+
const scrollCommentThread = (delta: number) => commentThreadCtxRef.current.setCommentThreadModal((current) => ({
|
|
1872
|
+
...current,
|
|
1873
|
+
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
1874
|
+
}))
|
|
1875
|
+
useBindings(() => ({
|
|
1876
|
+
enabled: () => commentThreadModalActiveRef.current,
|
|
1877
|
+
bindings: [
|
|
1878
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1879
|
+
{ key: "return", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1880
|
+
{ key: "a", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1881
|
+
{ key: "c", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1882
|
+
{ key: "up", cmd: () => scrollCommentThread(-1) },
|
|
1883
|
+
{ key: "k", cmd: () => scrollCommentThread(-1) },
|
|
1884
|
+
{ key: "down", cmd: () => scrollCommentThread(1) },
|
|
1885
|
+
{ key: "j", cmd: () => scrollCommentThread(1) },
|
|
1886
|
+
{ key: "pageup", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1887
|
+
{ key: "ctrl+u", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1888
|
+
{ key: "pagedown", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1889
|
+
{ key: "ctrl+d", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1890
|
+
{ key: "ctrl+v", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1891
|
+
],
|
|
1892
|
+
}), [])
|
|
1893
|
+
|
|
1894
|
+
// LabelModal: nav keys via keymap; text input stays in useKeyboard fallback.
|
|
1895
|
+
const labelModalActiveRef = useRef(false)
|
|
1896
|
+
labelModalActiveRef.current = labelModalActive
|
|
1897
|
+
const labelModalCtxRef = useRef({ toggleLabelAtIndex, setLabelModal, filteredCount: 0 })
|
|
1898
|
+
labelModalCtxRef.current = {
|
|
1899
|
+
toggleLabelAtIndex,
|
|
1900
|
+
setLabelModal,
|
|
1901
|
+
filteredCount: filterLabels(labelModal.availableLabels, labelModal.query).length,
|
|
1902
|
+
}
|
|
1903
|
+
const moveLabelSelection = (delta: -1 | 1) => labelModalCtxRef.current.setLabelModal((current) => {
|
|
1904
|
+
const max = Math.max(0, labelModalCtxRef.current.filteredCount - 1)
|
|
1905
|
+
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1906
|
+
})
|
|
1907
|
+
useBindings(() => ({
|
|
1908
|
+
enabled: () => labelModalActiveRef.current,
|
|
1909
|
+
bindings: [
|
|
1910
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1911
|
+
{ key: "return", cmd: () => labelModalCtxRef.current.toggleLabelAtIndex() },
|
|
1912
|
+
{ key: "up", cmd: () => moveLabelSelection(-1) },
|
|
1913
|
+
{ key: "k", cmd: () => moveLabelSelection(-1) },
|
|
1914
|
+
{ key: "down", cmd: () => moveLabelSelection(1) },
|
|
1915
|
+
{ key: "j", cmd: () => moveLabelSelection(1) },
|
|
1916
|
+
],
|
|
1917
|
+
}), [])
|
|
1918
|
+
|
|
1919
|
+
// ThemeModal: nav + filter-mode toggle. j/k only navigate when not in filter mode
|
|
1920
|
+
// (so users can type those letters into the query).
|
|
1921
|
+
const themeModalActiveRef = useRef(false)
|
|
1922
|
+
themeModalActiveRef.current = themeModalActive
|
|
1923
|
+
const themeModalCtxRef = useRef({
|
|
1924
|
+
filterMode: false,
|
|
1925
|
+
hasResults: true,
|
|
1926
|
+
closeThemeModal,
|
|
1927
|
+
updateThemeQuery,
|
|
1928
|
+
moveThemeSelection,
|
|
1929
|
+
})
|
|
1930
|
+
themeModalCtxRef.current = {
|
|
1931
|
+
filterMode: themeModal.filterMode,
|
|
1932
|
+
hasResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
1933
|
+
closeThemeModal,
|
|
1934
|
+
updateThemeQuery,
|
|
1935
|
+
moveThemeSelection,
|
|
1936
|
+
}
|
|
1937
|
+
useBindings(() => ({
|
|
1938
|
+
enabled: () => themeModalActiveRef.current,
|
|
1939
|
+
bindings: [
|
|
1940
|
+
{ key: "escape", cmd: () => {
|
|
1941
|
+
if (themeModalCtxRef.current.filterMode) themeModalCtxRef.current.updateThemeQuery("", { filterMode: false })
|
|
1942
|
+
else themeModalCtxRef.current.closeThemeModal(false)
|
|
1943
|
+
} },
|
|
1944
|
+
{ key: "/", cmd: () => themeModalCtxRef.current.updateThemeQuery("", { filterMode: true }) },
|
|
1945
|
+
{ key: "return", cmd: () => {
|
|
1946
|
+
if (themeModalCtxRef.current.filterMode && !themeModalCtxRef.current.hasResults) return
|
|
1947
|
+
themeModalCtxRef.current.closeThemeModal(true)
|
|
1948
|
+
} },
|
|
1949
|
+
{ key: "up", cmd: () => themeModalCtxRef.current.moveThemeSelection(-1) },
|
|
1950
|
+
{ key: "down", cmd: () => themeModalCtxRef.current.moveThemeSelection(1) },
|
|
1951
|
+
{ key: "k", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(-1) } },
|
|
1952
|
+
{ key: "j", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(1) } },
|
|
1953
|
+
],
|
|
1954
|
+
}), [])
|
|
1955
|
+
|
|
1956
|
+
// OpenRepositoryModal: escape closes, return submits.
|
|
1957
|
+
const openRepositoryModalActiveRef = useRef(false)
|
|
1958
|
+
openRepositoryModalActiveRef.current = openRepositoryModalActive
|
|
1959
|
+
const openRepositoryFromInputRef = useRef(openRepositoryFromInput)
|
|
1960
|
+
openRepositoryFromInputRef.current = openRepositoryFromInput
|
|
1961
|
+
useBindings(() => ({
|
|
1962
|
+
enabled: () => openRepositoryModalActiveRef.current,
|
|
1963
|
+
bindings: [
|
|
1964
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1965
|
+
{ key: "return", cmd: () => openRepositoryFromInputRef.current() },
|
|
1966
|
+
],
|
|
1967
|
+
}), [])
|
|
1968
|
+
|
|
1969
|
+
// CommentModal: full text editor — escape, submit, all the cursor/edit bindings.
|
|
1970
|
+
const commentModalActiveRef = useRef(false)
|
|
1971
|
+
commentModalActiveRef.current = commentModalActive
|
|
1972
|
+
const commentModalCtxRef = useRef({ submitDiffComment, editComment })
|
|
1973
|
+
commentModalCtxRef.current = { submitDiffComment, editComment }
|
|
1974
|
+
const editComm = (transform: Parameters<typeof editComment>[0]) => commentModalCtxRef.current.editComment(transform)
|
|
1975
|
+
useBindings(() => ({
|
|
1976
|
+
enabled: () => commentModalActiveRef.current,
|
|
1977
|
+
bindings: [
|
|
1978
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1979
|
+
{ key: "ctrl+s", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
1980
|
+
{ key: "ctrl+a", cmd: () => editComm(moveLineStart) },
|
|
1981
|
+
{ key: "ctrl+e", cmd: () => editComm(moveLineEnd) },
|
|
1982
|
+
{ key: "ctrl+b", cmd: () => editComm(editorMoveLeft) },
|
|
1983
|
+
{ key: "ctrl+f", cmd: () => editComm(editorMoveRight) },
|
|
1984
|
+
{ key: "ctrl+w", cmd: () => editComm(deleteWordBackward) },
|
|
1985
|
+
{ key: "ctrl+u", cmd: () => editComm(deleteToLineStart) },
|
|
1986
|
+
{ key: "ctrl+k", cmd: () => editComm(deleteToLineEnd) },
|
|
1987
|
+
{ key: "ctrl+d", cmd: () => editComm(editorDeleteForward) },
|
|
1988
|
+
{ key: "meta+b", cmd: () => editComm(moveWordBackward) },
|
|
1989
|
+
{ key: "meta+left", cmd: () => editComm(moveWordBackward) },
|
|
1990
|
+
{ key: "meta+f", cmd: () => editComm(moveWordForward) },
|
|
1991
|
+
{ key: "meta+right", cmd: () => editComm(moveWordForward) },
|
|
1992
|
+
{ key: "meta+backspace", cmd: () => editComm(deleteWordBackward) },
|
|
1993
|
+
{ key: "meta+delete", cmd: () => editComm(deleteWordForward) },
|
|
1994
|
+
{ key: "backspace", cmd: () => editComm(editorBackspace) },
|
|
1995
|
+
{ key: "delete", cmd: () => editComm(editorDeleteForward) },
|
|
1996
|
+
{ key: "left", cmd: () => editComm(editorMoveLeft) },
|
|
1997
|
+
{ key: "right", cmd: () => editComm(editorMoveRight) },
|
|
1998
|
+
{ key: "up", cmd: () => editComm((state) => moveVertically(state, -1)) },
|
|
1999
|
+
{ key: "down", cmd: () => editComm((state) => moveVertically(state, 1)) },
|
|
2000
|
+
{ key: "home", cmd: () => editComm(moveLineStart) },
|
|
2001
|
+
{ key: "end", cmd: () => editComm(moveLineEnd) },
|
|
2002
|
+
{ key: "shift+return", cmd: () => editComm((state) => insertText(state, "\n")) },
|
|
2003
|
+
{ key: "return", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
2004
|
+
],
|
|
2005
|
+
}), [])
|
|
2006
|
+
|
|
2007
|
+
// CommandPalette: escape closes, return runs, up/k & down/j navigate.
|
|
2008
|
+
const commandPaletteActiveRef = useRef(false)
|
|
2009
|
+
commandPaletteActiveRef.current = commandPaletteActive
|
|
2010
|
+
const commandPaletteCtxRef = useRef({
|
|
2011
|
+
runSelected: () => {},
|
|
2012
|
+
setCommandPalette,
|
|
2013
|
+
paletteCommands: commandPaletteCommands,
|
|
2014
|
+
})
|
|
2015
|
+
commandPaletteCtxRef.current = {
|
|
2016
|
+
runSelected: () => { if (selectedCommand) runCommand(selectedCommand, { notifyDisabled: true, closePalette: true }) },
|
|
2017
|
+
setCommandPalette,
|
|
2018
|
+
paletteCommands: commandPaletteCommands,
|
|
2019
|
+
}
|
|
2020
|
+
const moveCommandPaletteSelection = (delta: -1 | 1) => commandPaletteCtxRef.current.setCommandPalette((current) => {
|
|
2021
|
+
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCtxRef.current.paletteCommands)
|
|
2022
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
2023
|
+
})
|
|
2024
|
+
useBindings(() => ({
|
|
2025
|
+
enabled: () => commandPaletteActiveRef.current,
|
|
2026
|
+
bindings: [
|
|
2027
|
+
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
2028
|
+
{ key: "ctrl+c", cmd: () => closeActiveModalRef.current() },
|
|
2029
|
+
{ key: "return", cmd: () => commandPaletteCtxRef.current.runSelected() },
|
|
2030
|
+
{ key: "up", cmd: () => moveCommandPaletteSelection(-1) },
|
|
2031
|
+
{ key: "down", cmd: () => moveCommandPaletteSelection(1) },
|
|
2032
|
+
],
|
|
2033
|
+
}), [])
|
|
2034
|
+
|
|
2035
|
+
// FilterMode: escape cancels, return commits.
|
|
2036
|
+
const filterModeRef = useRef(false)
|
|
2037
|
+
filterModeRef.current = filterMode
|
|
2038
|
+
const filterCtxRef = useRef({ filterQuery, filterDraft, setFilterQuery, setFilterDraft, setFilterMode })
|
|
2039
|
+
filterCtxRef.current = { filterQuery, filterDraft, setFilterQuery, setFilterDraft, setFilterMode }
|
|
2040
|
+
useBindings(() => ({
|
|
2041
|
+
enabled: () => filterModeRef.current,
|
|
2042
|
+
bindings: [
|
|
2043
|
+
{ key: "escape", cmd: () => {
|
|
2044
|
+
filterCtxRef.current.setFilterDraft(filterCtxRef.current.filterQuery)
|
|
2045
|
+
filterCtxRef.current.setFilterMode(false)
|
|
2046
|
+
} },
|
|
2047
|
+
{ key: "return", cmd: () => {
|
|
2048
|
+
filterCtxRef.current.setFilterQuery(filterCtxRef.current.filterDraft)
|
|
2049
|
+
filterCtxRef.current.setFilterMode(false)
|
|
2050
|
+
} },
|
|
2051
|
+
],
|
|
2052
|
+
}), [])
|
|
2053
|
+
|
|
1746
2054
|
useKeyboard((key) => {
|
|
1747
2055
|
if (commandPaletteActive) {
|
|
1748
|
-
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
1749
|
-
closeActiveModal()
|
|
1750
|
-
return
|
|
1751
|
-
}
|
|
1752
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1753
|
-
if (selectedCommand) runCommand(selectedCommand, { notifyDisabled: true, closePalette: true })
|
|
1754
|
-
return
|
|
1755
|
-
}
|
|
1756
|
-
if (key.name === "up" || key.name === "k" && !key.ctrl && !key.meta) {
|
|
1757
|
-
setCommandPalette((current) => {
|
|
1758
|
-
const selectedIndex = clampCommandIndex(current.selectedIndex - 1, commandPaletteCommands)
|
|
1759
|
-
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1760
|
-
})
|
|
1761
|
-
return
|
|
1762
|
-
}
|
|
1763
|
-
if (key.name === "down" || key.name === "j" && !key.ctrl && !key.meta) {
|
|
1764
|
-
setCommandPalette((current) => {
|
|
1765
|
-
const selectedIndex = clampCommandIndex(current.selectedIndex + 1, commandPaletteCommands)
|
|
1766
|
-
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1767
|
-
})
|
|
1768
|
-
return
|
|
1769
|
-
}
|
|
1770
2056
|
if (isSingleLineInputKey(key)) {
|
|
1771
2057
|
setCommandPalette((current) => {
|
|
1772
2058
|
const query = editSingleLineInput(current.query, key) ?? current.query
|
|
1773
2059
|
return current.query === query && current.selectedIndex === 0 ? current : { ...current, query, selectedIndex: 0 }
|
|
1774
2060
|
})
|
|
1775
|
-
return
|
|
1776
2061
|
}
|
|
1777
2062
|
return
|
|
1778
2063
|
}
|
|
1779
2064
|
|
|
1780
2065
|
if (openRepositoryModalActive) {
|
|
1781
|
-
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
1782
|
-
closeActiveModal()
|
|
1783
|
-
return
|
|
1784
|
-
}
|
|
1785
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1786
|
-
openRepositoryFromInput()
|
|
1787
|
-
return
|
|
1788
|
-
}
|
|
1789
2066
|
if (isSingleLineInputKey(key)) {
|
|
1790
2067
|
setOpenRepositoryModal((current) => ({
|
|
1791
2068
|
...current,
|
|
1792
2069
|
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
1793
2070
|
error: null,
|
|
1794
2071
|
}))
|
|
1795
|
-
return
|
|
1796
2072
|
}
|
|
1797
2073
|
return
|
|
1798
2074
|
}
|
|
1799
2075
|
|
|
1800
|
-
if ((key.ctrl && key.name === "p") || (key.meta && key.name === "k")) {
|
|
1801
|
-
runCommandById("command.open")
|
|
1802
|
-
return
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
2076
|
if ((key.name === "q" && !commentModalActive && !(themeModalActive && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
1806
2077
|
if (themeModalActive) {
|
|
1807
2078
|
closeThemeModal(false)
|
|
@@ -1816,237 +2087,28 @@ export const App = () => {
|
|
|
1816
2087
|
}
|
|
1817
2088
|
|
|
1818
2089
|
if (themeModalActive) {
|
|
1819
|
-
if (key.name === "escape") {
|
|
1820
|
-
if (themeModal.filterMode) {
|
|
1821
|
-
updateThemeQuery("", { filterMode: false })
|
|
1822
|
-
return
|
|
1823
|
-
}
|
|
1824
|
-
closeThemeModal(false)
|
|
1825
|
-
return
|
|
1826
|
-
}
|
|
1827
|
-
if (key.name === "/") {
|
|
1828
|
-
updateThemeQuery("", { filterMode: true })
|
|
1829
|
-
return
|
|
1830
|
-
}
|
|
1831
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1832
|
-
if (themeModal.filterMode && filterThemeDefinitions(themeModal.query).length === 0) return
|
|
1833
|
-
closeThemeModal(true)
|
|
1834
|
-
return
|
|
1835
|
-
}
|
|
1836
|
-
if (key.name === "up" || (!themeModal.filterMode && key.name === "k")) {
|
|
1837
|
-
moveThemeSelection(-1)
|
|
1838
|
-
return
|
|
1839
|
-
}
|
|
1840
|
-
if (key.name === "down" || (!themeModal.filterMode && key.name === "j")) {
|
|
1841
|
-
moveThemeSelection(1)
|
|
1842
|
-
return
|
|
1843
|
-
}
|
|
1844
2090
|
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
1845
2091
|
editThemeQuery((query) => editSingleLineInput(query, key) ?? query)
|
|
1846
|
-
return
|
|
1847
2092
|
}
|
|
1848
2093
|
return
|
|
1849
2094
|
}
|
|
1850
2095
|
|
|
1851
2096
|
if (commentModalActive) {
|
|
1852
|
-
if (key.name === "escape") {
|
|
1853
|
-
closeActiveModal()
|
|
1854
|
-
return
|
|
1855
|
-
}
|
|
1856
|
-
if (key.ctrl && key.name === "s") {
|
|
1857
|
-
submitDiffComment()
|
|
1858
|
-
return
|
|
1859
|
-
}
|
|
1860
|
-
if (key.ctrl && key.name === "a") {
|
|
1861
|
-
editComment(moveLineStart)
|
|
1862
|
-
return
|
|
1863
|
-
}
|
|
1864
|
-
if (key.ctrl && key.name === "e") {
|
|
1865
|
-
editComment(moveLineEnd)
|
|
1866
|
-
return
|
|
1867
|
-
}
|
|
1868
|
-
if (key.ctrl && key.name === "b") {
|
|
1869
|
-
editComment(editorMoveLeft)
|
|
1870
|
-
return
|
|
1871
|
-
}
|
|
1872
|
-
if (key.ctrl && key.name === "f") {
|
|
1873
|
-
editComment(editorMoveRight)
|
|
1874
|
-
return
|
|
1875
|
-
}
|
|
1876
|
-
if (key.ctrl && key.name === "w") {
|
|
1877
|
-
editComment(deleteWordBackward)
|
|
1878
|
-
return
|
|
1879
|
-
}
|
|
1880
|
-
if (key.ctrl && key.name === "u") {
|
|
1881
|
-
editComment(deleteToLineStart)
|
|
1882
|
-
return
|
|
1883
|
-
}
|
|
1884
|
-
if (key.ctrl && key.name === "k") {
|
|
1885
|
-
editComment(deleteToLineEnd)
|
|
1886
|
-
return
|
|
1887
|
-
}
|
|
1888
|
-
if (key.ctrl && key.name === "d") {
|
|
1889
|
-
editComment(editorDeleteForward)
|
|
1890
|
-
return
|
|
1891
|
-
}
|
|
1892
|
-
if ((key.meta || key.option) && (key.name === "b" || key.name === "left")) {
|
|
1893
|
-
editComment(moveWordBackward)
|
|
1894
|
-
return
|
|
1895
|
-
}
|
|
1896
|
-
if ((key.meta || key.option) && (key.name === "f" || key.name === "right")) {
|
|
1897
|
-
editComment(moveWordForward)
|
|
1898
|
-
return
|
|
1899
|
-
}
|
|
1900
|
-
if ((key.meta || key.option) && (key.name === "backspace" || key.name === "delete")) {
|
|
1901
|
-
editComment(key.name === "delete" ? deleteWordForward : deleteWordBackward)
|
|
1902
|
-
return
|
|
1903
|
-
}
|
|
1904
|
-
if (key.name === "backspace") {
|
|
1905
|
-
editComment(editorBackspace)
|
|
1906
|
-
return
|
|
1907
|
-
}
|
|
1908
|
-
if (key.name === "delete") {
|
|
1909
|
-
editComment(editorDeleteForward)
|
|
1910
|
-
return
|
|
1911
|
-
}
|
|
1912
|
-
if (key.name === "left") {
|
|
1913
|
-
editComment(editorMoveLeft)
|
|
1914
|
-
return
|
|
1915
|
-
}
|
|
1916
|
-
if (key.name === "right") {
|
|
1917
|
-
editComment(editorMoveRight)
|
|
1918
|
-
return
|
|
1919
|
-
}
|
|
1920
|
-
if (key.name === "up") {
|
|
1921
|
-
editComment((state) => moveVertically(state, -1))
|
|
1922
|
-
return
|
|
1923
|
-
}
|
|
1924
|
-
if (key.name === "down") {
|
|
1925
|
-
editComment((state) => moveVertically(state, 1))
|
|
1926
|
-
return
|
|
1927
|
-
}
|
|
1928
|
-
if (key.name === "home") {
|
|
1929
|
-
editComment(moveLineStart)
|
|
1930
|
-
return
|
|
1931
|
-
}
|
|
1932
|
-
if (key.name === "end") {
|
|
1933
|
-
editComment(moveLineEnd)
|
|
1934
|
-
return
|
|
1935
|
-
}
|
|
1936
|
-
if ((key.name === "return" || key.name === "enter") && key.shift) {
|
|
1937
|
-
editComment((state) => insertText(state, "\n"))
|
|
1938
|
-
return
|
|
1939
|
-
}
|
|
1940
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1941
|
-
submitDiffComment()
|
|
1942
|
-
return
|
|
1943
|
-
}
|
|
1944
2097
|
const text = printableKeyText(key)
|
|
1945
|
-
if (text)
|
|
1946
|
-
editComment((state) => insertText(state, text))
|
|
1947
|
-
return
|
|
1948
|
-
}
|
|
2098
|
+
if (text) editComment((state) => insertText(state, text))
|
|
1949
2099
|
return
|
|
1950
2100
|
}
|
|
1951
2101
|
|
|
1952
|
-
if (commentThreadModalActive) {
|
|
1953
|
-
if (key.name === "escape") {
|
|
1954
|
-
closeActiveModal()
|
|
1955
|
-
return
|
|
1956
|
-
}
|
|
1957
|
-
if (key.name === "return" || key.name === "enter" || key.name === "a" || key.name === "c") {
|
|
1958
|
-
openDiffCommentModal()
|
|
1959
|
-
return
|
|
1960
|
-
}
|
|
1961
|
-
if (key.name === "up" || key.name === "k") {
|
|
1962
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - 1) }))
|
|
1963
|
-
return
|
|
1964
|
-
}
|
|
1965
|
-
if (key.name === "down" || key.name === "j") {
|
|
1966
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + 1 }))
|
|
1967
|
-
return
|
|
1968
|
-
}
|
|
1969
|
-
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
1970
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: Math.max(0, current.scrollOffset - halfPage) }))
|
|
1971
|
-
return
|
|
1972
|
-
}
|
|
1973
|
-
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
1974
|
-
setCommentThreadModal((current) => ({ ...current, scrollOffset: current.scrollOffset + halfPage }))
|
|
1975
|
-
return
|
|
1976
|
-
}
|
|
1977
|
-
return
|
|
1978
|
-
}
|
|
1979
2102
|
|
|
1980
|
-
if (closeModalActive) {
|
|
1981
|
-
if (key.name === "escape") {
|
|
1982
|
-
closeActiveModal()
|
|
1983
|
-
return
|
|
1984
|
-
}
|
|
1985
|
-
if (key.name === "return" || key.name === "enter") {
|
|
1986
|
-
confirmClosePullRequest()
|
|
1987
|
-
return
|
|
1988
|
-
}
|
|
1989
|
-
return
|
|
1990
|
-
}
|
|
1991
2103
|
|
|
1992
|
-
if (mergeModalActive) {
|
|
1993
|
-
const options = availableMergeActions(mergeModal.info)
|
|
1994
|
-
if (key.name === "escape") {
|
|
1995
|
-
closeActiveModal()
|
|
1996
|
-
return
|
|
1997
|
-
}
|
|
1998
|
-
if ((key.name === "return" || key.name === "enter") && options.length > 0) {
|
|
1999
|
-
confirmMergeAction()
|
|
2000
|
-
return
|
|
2001
|
-
}
|
|
2002
|
-
if (key.name === "up" || key.name === "k") {
|
|
2003
|
-
setMergeModal((current) => ({
|
|
2004
|
-
...current,
|
|
2005
|
-
selectedIndex: Math.max(0, current.selectedIndex - 1),
|
|
2006
|
-
}))
|
|
2007
|
-
return
|
|
2008
|
-
}
|
|
2009
|
-
if (key.name === "down" || key.name === "j") {
|
|
2010
|
-
setMergeModal((current) => ({
|
|
2011
|
-
...current,
|
|
2012
|
-
selectedIndex: Math.min(Math.max(0, options.length - 1), current.selectedIndex + 1),
|
|
2013
|
-
}))
|
|
2014
|
-
return
|
|
2015
|
-
}
|
|
2016
|
-
return
|
|
2017
|
-
}
|
|
2018
2104
|
|
|
2019
2105
|
if (labelModalActive) {
|
|
2020
|
-
if (key.name === "escape") {
|
|
2021
|
-
closeActiveModal()
|
|
2022
|
-
return
|
|
2023
|
-
}
|
|
2024
|
-
if (key.name === "return" || key.name === "enter") {
|
|
2025
|
-
toggleLabelAtIndex()
|
|
2026
|
-
return
|
|
2027
|
-
}
|
|
2028
|
-
if (key.name === "up" || key.name === "k") {
|
|
2029
|
-
setLabelModal((current) => ({
|
|
2030
|
-
...current,
|
|
2031
|
-
selectedIndex: Math.max(0, current.selectedIndex - 1),
|
|
2032
|
-
}))
|
|
2033
|
-
return
|
|
2034
|
-
}
|
|
2035
|
-
if (key.name === "down" || key.name === "j") {
|
|
2036
|
-
const filtered = filterLabels(labelModal.availableLabels, labelModal.query)
|
|
2037
|
-
setLabelModal((current) => ({
|
|
2038
|
-
...current,
|
|
2039
|
-
selectedIndex: Math.min(Math.max(0, filtered.length - 1), current.selectedIndex + 1),
|
|
2040
|
-
}))
|
|
2041
|
-
return
|
|
2042
|
-
}
|
|
2043
2106
|
if (isSingleLineInputKey(key)) {
|
|
2044
2107
|
setLabelModal((current) => ({
|
|
2045
2108
|
...current,
|
|
2046
2109
|
query: editSingleLineInput(current.query, key) ?? current.query,
|
|
2047
2110
|
selectedIndex: 0,
|
|
2048
2111
|
}))
|
|
2049
|
-
return
|
|
2050
2112
|
}
|
|
2051
2113
|
return
|
|
2052
2114
|
}
|
|
@@ -2271,20 +2333,10 @@ export const App = () => {
|
|
|
2271
2333
|
}
|
|
2272
2334
|
|
|
2273
2335
|
if (filterMode) {
|
|
2274
|
-
if (key.name === "escape") {
|
|
2275
|
-
setFilterDraft(filterQuery)
|
|
2276
|
-
setFilterMode(false)
|
|
2277
|
-
return
|
|
2278
|
-
}
|
|
2279
|
-
if (key.name === "return" || key.name === "enter") {
|
|
2280
|
-
setFilterQuery(filterDraft)
|
|
2281
|
-
setFilterMode(false)
|
|
2282
|
-
return
|
|
2283
|
-
}
|
|
2284
2336
|
if (isSingleLineInputKey(key)) {
|
|
2285
2337
|
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
2286
|
-
return
|
|
2287
2338
|
}
|
|
2339
|
+
return
|
|
2288
2340
|
}
|
|
2289
2341
|
|
|
2290
2342
|
if (key.name === "tab") {
|
|
@@ -2292,23 +2344,10 @@ export const App = () => {
|
|
|
2292
2344
|
return
|
|
2293
2345
|
}
|
|
2294
2346
|
|
|
2295
|
-
if (isThemeKey(key)) {
|
|
2296
|
-
runCommandById("theme.open")
|
|
2297
|
-
return
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
if (key.name === "/") {
|
|
2301
|
-
runCommandById("filter.open")
|
|
2302
|
-
return
|
|
2303
|
-
}
|
|
2304
2347
|
if (key.name === "escape" && filterQuery.length > 0) {
|
|
2305
2348
|
runCommandById("filter.clear")
|
|
2306
2349
|
return
|
|
2307
2350
|
}
|
|
2308
|
-
if (key.name === "r") {
|
|
2309
|
-
runCommandById("pull.refresh")
|
|
2310
|
-
return
|
|
2311
|
-
}
|
|
2312
2351
|
if (isWideLayout && selectedPullRequest && !detailFullView && !diffFullView) {
|
|
2313
2352
|
if (key.name === "home") {
|
|
2314
2353
|
scrollDetailPreviewTo(0)
|
|
@@ -2391,38 +2430,6 @@ export const App = () => {
|
|
|
2391
2430
|
() => setSelectedIndex(0),
|
|
2392
2431
|
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2393
2432
|
)) return
|
|
2394
|
-
if ((key.name === "return" || key.name === "enter") && !detailFullView) {
|
|
2395
|
-
runCommandById("detail.open")
|
|
2396
|
-
return
|
|
2397
|
-
}
|
|
2398
|
-
if (key.name === "d" && selectedPullRequest) {
|
|
2399
|
-
runCommandById("diff.open")
|
|
2400
|
-
return
|
|
2401
|
-
}
|
|
2402
|
-
if (key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2403
|
-
runCommandById("pull.close")
|
|
2404
|
-
return
|
|
2405
|
-
}
|
|
2406
|
-
if (key.name === "l" && selectedPullRequest) {
|
|
2407
|
-
runCommandById("pull.labels")
|
|
2408
|
-
return
|
|
2409
|
-
}
|
|
2410
|
-
if (key.name === "m" || key.name === "M") {
|
|
2411
|
-
if (selectedPullRequest) runCommandById("pull.merge")
|
|
2412
|
-
return
|
|
2413
|
-
}
|
|
2414
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
2415
|
-
runCommandById("pull.open-browser")
|
|
2416
|
-
return
|
|
2417
|
-
}
|
|
2418
|
-
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
2419
|
-
runCommandById("pull.toggle-draft")
|
|
2420
|
-
return
|
|
2421
|
-
}
|
|
2422
|
-
if (key.name === "y" && selectedPullRequest) {
|
|
2423
|
-
runCommandById("pull.copy-metadata")
|
|
2424
|
-
return
|
|
2425
|
-
}
|
|
2426
2433
|
})
|
|
2427
2434
|
|
|
2428
2435
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
package/src/commands.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export type CommandScope = "Global" | "View" | "Pull request" | "Diff" | "Navigation" | "System"
|
|
2
2
|
|
|
3
|
+
const SCOPE_ORDER: readonly CommandScope[] = ["Global", "View", "Pull request", "Diff", "Navigation", "System"]
|
|
4
|
+
|
|
5
|
+
export const sortCommandsByScope = (commands: readonly AppCommand[]) =>
|
|
6
|
+
[...commands].sort((left, right) => SCOPE_ORDER.indexOf(left.scope) - SCOPE_ORDER.indexOf(right.scope))
|
|
7
|
+
|
|
3
8
|
export interface AppCommand {
|
|
4
9
|
readonly id: string
|
|
5
10
|
readonly title: string
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { addDefaultParsers, createCliRenderer, createTerminalPalette } from "@opentui/core"
|
|
4
4
|
import { RegistryProvider } from "@effect/atom-react"
|
|
5
5
|
import { createRoot } from "@opentui/react"
|
|
6
|
+
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
|
7
|
+
import { KeymapProvider } from "@opentui/keymap/react"
|
|
6
8
|
|
|
7
9
|
process.env.OTUI_USE_ALTERNATE_SCREEN = "true"
|
|
8
10
|
|
|
@@ -43,8 +45,12 @@ const renderer = await createCliRenderer({
|
|
|
43
45
|
|
|
44
46
|
process.stdout.write(FOCUS_REPORTING_ENABLE)
|
|
45
47
|
|
|
48
|
+
const keymap = createDefaultOpenTuiKeymap(renderer)
|
|
49
|
+
|
|
46
50
|
createRoot(renderer).render(
|
|
47
51
|
<RegistryProvider>
|
|
48
|
-
<
|
|
52
|
+
<KeymapProvider keymap={keymap}>
|
|
53
|
+
<App />
|
|
54
|
+
</KeymapProvider>
|
|
49
55
|
</RegistryProvider>,
|
|
50
56
|
)
|
|
@@ -17,6 +17,7 @@ const scopeLabels = {
|
|
|
17
17
|
|
|
18
18
|
export type CommandPaletteRow =
|
|
19
19
|
| { readonly _tag: "section"; readonly scope: AppCommand["scope"] }
|
|
20
|
+
| { readonly _tag: "spacer" }
|
|
20
21
|
| { readonly _tag: "command"; readonly command: AppCommand; readonly commandIndex: number }
|
|
21
22
|
|
|
22
23
|
export const buildCommandPaletteRows = (commands: readonly AppCommand[]): readonly CommandPaletteRow[] => {
|
|
@@ -25,6 +26,7 @@ export const buildCommandPaletteRows = (commands: readonly AppCommand[]): readon
|
|
|
25
26
|
for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
|
|
26
27
|
const command = commands[commandIndex]!
|
|
27
28
|
if (command.scope !== previousScope) {
|
|
29
|
+
if (previousScope !== null) rows.push({ _tag: "spacer" })
|
|
28
30
|
rows.push({ _tag: "section", scope: command.scope })
|
|
29
31
|
previousScope = command.scope
|
|
30
32
|
}
|
|
@@ -112,24 +114,35 @@ export const CommandPalette = ({
|
|
|
112
114
|
<>
|
|
113
115
|
{visibleRows.map((row, index) => {
|
|
114
116
|
const rowIndex = scrollTop + index
|
|
117
|
+
if (row._tag === "spacer") {
|
|
118
|
+
return <PlainLine key={`spacer-${rowIndex}`} text="" />
|
|
119
|
+
}
|
|
115
120
|
if (row._tag === "section") {
|
|
116
|
-
return <PlainLine key={`section-${row.scope}-${rowIndex}`} text={fitCell(`
|
|
121
|
+
return <PlainLine key={`section-${row.scope}-${rowIndex}`} text={fitCell(` ${scopeLabels[row.scope].toUpperCase()}`, rowWidth)} fg={colors.muted} />
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
const { command, commandIndex } = row
|
|
120
125
|
const isSelected = commandIndex === clampedIndex
|
|
121
126
|
const shortcut = command.shortcut ? trimCell(command.shortcut, 16) : ""
|
|
122
|
-
const
|
|
127
|
+
const shortcutWidth = shortcut.length === 0 ? 0 : Math.min(18, Math.max(6, shortcut.length + 1))
|
|
123
128
|
const trailingPadding = shortcut.length === 0 ? 0 : 1
|
|
124
|
-
|
|
129
|
+
// Layout: "▸ " (2) + title + " " (2) + subtitle + filler + shortcut + " " (1)
|
|
130
|
+
const SELECTOR_WIDTH = 2
|
|
131
|
+
const titleAvailable = Math.max(8, rowWidth - SELECTOR_WIDTH - shortcutWidth - trailingPadding)
|
|
132
|
+
const titleText = trimCell(command.title, Math.min(titleAvailable, 36))
|
|
133
|
+
const subtitleSpace = Math.max(0, titleAvailable - titleText.length - 2)
|
|
134
|
+
const subtitleText = command.subtitle && subtitleSpace > 4 ? trimCell(command.subtitle, subtitleSpace) : ""
|
|
135
|
+
const fillerWidth = Math.max(0, titleAvailable - titleText.length - (subtitleText ? 2 + subtitleText.length : 0))
|
|
125
136
|
|
|
126
137
|
return (
|
|
127
138
|
<box key={command.id} height={1}>
|
|
128
139
|
<TextLine width={rowWidth} bg={isSelected ? colors.selectedBg : undefined} fg={isSelected ? colors.selectedText : colors.text}>
|
|
129
140
|
<span fg={isSelected ? colors.accent : colors.muted}>{isSelected ? "▸" : " "}</span>
|
|
130
141
|
<span> </span>
|
|
131
|
-
{isSelected ? <span attributes={TextAttributes.BOLD}>{
|
|
132
|
-
{
|
|
142
|
+
{isSelected ? <span attributes={TextAttributes.BOLD}>{titleText}</span> : <span>{titleText}</span>}
|
|
143
|
+
{subtitleText ? <span fg={colors.muted}>{` ${subtitleText}`}</span> : null}
|
|
144
|
+
{fillerWidth > 0 ? <span>{" ".repeat(fillerWidth)}</span> : null}
|
|
145
|
+
{shortcutWidth > 0 ? <span fg={colors.muted}>{fitCell(shortcut, shortcutWidth, "right")}</span> : null}
|
|
133
146
|
{trailingPadding > 0 ? <span> </span> : null}
|
|
134
147
|
</TextLine>
|
|
135
148
|
</box>
|