@kitlangton/ghui 0.2.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1 -4
  2. package/bin/ghui.js +6 -1
  3. package/dist/index.js +9419 -0
  4. package/package.json +7 -4
  5. package/src/App.tsx +0 -2604
  6. package/src/appCommands.ts +0 -384
  7. package/src/commands.ts +0 -73
  8. package/src/config.ts +0 -28
  9. package/src/date.ts +0 -20
  10. package/src/domain.ts +0 -139
  11. package/src/errors.ts +0 -10
  12. package/src/index.tsx +0 -50
  13. package/src/keyboard/opentuiAdapter.ts +0 -43
  14. package/src/keymap/all.ts +0 -103
  15. package/src/keymap/closeModal.ts +0 -13
  16. package/src/keymap/commandPalette.ts +0 -16
  17. package/src/keymap/commentModal.ts +0 -73
  18. package/src/keymap/commentThreadModal.ts +0 -24
  19. package/src/keymap/detailView.ts +0 -30
  20. package/src/keymap/diffView.ts +0 -91
  21. package/src/keymap/filterMode.ts +0 -13
  22. package/src/keymap/helpers.ts +0 -24
  23. package/src/keymap/labelModal.ts +0 -16
  24. package/src/keymap/listNav.ts +0 -119
  25. package/src/keymap/mergeModal.ts +0 -23
  26. package/src/keymap/openRepositoryModal.ts +0 -13
  27. package/src/keymap/themeModal.ts +0 -49
  28. package/src/mergeActions.ts +0 -88
  29. package/src/observability.ts +0 -46
  30. package/src/pullRequestCache.ts +0 -19
  31. package/src/pullRequestViews.ts +0 -45
  32. package/src/services/BrowserOpener.ts +0 -22
  33. package/src/services/Clipboard.ts +0 -46
  34. package/src/services/CommandRunner.ts +0 -91
  35. package/src/services/GitHubService.ts +0 -741
  36. package/src/services/MockGitHubService.ts +0 -181
  37. package/src/themeStore.ts +0 -60
  38. package/src/ui/CommandPalette.tsx +0 -216
  39. package/src/ui/DetailsPane.tsx +0 -656
  40. package/src/ui/FooterHints.tsx +0 -90
  41. package/src/ui/LoadingLogo.tsx +0 -75
  42. package/src/ui/PullRequestDiffPane.tsx +0 -248
  43. package/src/ui/PullRequestList.tsx +0 -210
  44. package/src/ui/colors.ts +0 -814
  45. package/src/ui/commentEditor.ts +0 -126
  46. package/src/ui/comments.tsx +0 -143
  47. package/src/ui/diff.ts +0 -650
  48. package/src/ui/diffStats.tsx +0 -25
  49. package/src/ui/modals.tsx +0 -614
  50. package/src/ui/primitives.tsx +0 -205
  51. package/src/ui/pullRequests.ts +0 -106
  52. package/src/ui/singleLineInput.ts +0 -26
  53. package/src/ui/spinner.ts +0 -1
@@ -1,181 +0,0 @@
1
- import { Effect, Layer } from "effect"
2
- import type { CheckItem, CreatePullRequestCommentInput, Mergeable, PullRequestConversationItem, PullRequestItem, PullRequestLabel, PullRequestMergeInfo, PullRequestPage, PullRequestQueueMode, PullRequestReviewComment, ReviewStatus } from "../domain.js"
3
- import { GitHubService } from "./GitHubService.js"
4
-
5
- export interface MockOptions {
6
- readonly prCount: number
7
- readonly repoCount?: number
8
- readonly username?: string
9
- readonly seed?: number
10
- }
11
-
12
- const REVIEW_CYCLE: readonly ReviewStatus[] = ["approved", "changes", "review", "none", "draft"]
13
- const MERGEABLE_CYCLE: readonly Mergeable[] = ["mergeable", "conflicting", "unknown"]
14
-
15
- const synthCheckSummary = (passed: number, total: number): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> => {
16
- const checks: readonly CheckItem[] = Array.from({ length: total }, (_, index) => ({
17
- name: `check-${index}`,
18
- status: "completed",
19
- conclusion: index < passed ? "success" : "failure",
20
- }))
21
- if (total === 0) return { checkStatus: "none", checkSummary: null, checks: [] }
22
- if (passed === total) return { checkStatus: "passing", checkSummary: `${passed}/${total}`, checks }
23
- return { checkStatus: "failing", checkSummary: `${passed}/${total}`, checks }
24
- }
25
-
26
- const synthLabels = (index: number): readonly PullRequestLabel[] => {
27
- if (index % 5 === 0) return [{ name: "bug", color: "#d73a4a" }]
28
- if (index % 7 === 0) return [{ name: "enhancement", color: "#a2eeef" }, { name: "tests", color: "#0e8a16" }]
29
- return []
30
- }
31
-
32
- const buildPullRequest = (index: number, options: Required<MockOptions>): PullRequestItem => {
33
- const repoIndex = index % options.repoCount
34
- const repository = `mock-org/repo-${repoIndex}`
35
- const number = 1000 + index
36
- const total = 8 + (index % 5)
37
- const passed = total - (index % 3 === 0 ? 1 : 0)
38
- const review = REVIEW_CYCLE[index % REVIEW_CYCLE.length]!
39
- const createdAt = new Date(Date.now() - index * 86_400_000)
40
-
41
- return {
42
- repository,
43
- author: options.username,
44
- headRefOid: `deadbeef${index.toString(16).padStart(8, "0")}`,
45
- number,
46
- title: `Mock PR ${number}: example change ${index}`,
47
- body: `This is mock pull request #${number}.\n\nLine A.\nLine B.`,
48
- labels: synthLabels(index),
49
- additions: 10 + index,
50
- deletions: 5 + (index % 11),
51
- changedFiles: 1 + (index % 7),
52
- state: "open",
53
- reviewStatus: review,
54
- ...synthCheckSummary(passed, total),
55
- autoMergeEnabled: index % 11 === 0,
56
- detailLoaded: true,
57
- createdAt,
58
- closedAt: null,
59
- url: `https://github.com/${repository}/pull/${number}`,
60
- }
61
- }
62
-
63
- export const buildMockPullRequests = (options: MockOptions): readonly PullRequestItem[] => {
64
- const resolved: Required<MockOptions> = {
65
- prCount: options.prCount,
66
- repoCount: options.repoCount ?? 4,
67
- username: options.username ?? "mock-user",
68
- seed: options.seed ?? 0,
69
- }
70
- return Array.from({ length: resolved.prCount }, (_, index) => buildPullRequest(index, resolved))
71
- }
72
-
73
- const filterByView = (mode: PullRequestQueueMode, repository: string | null, source: readonly PullRequestItem[]) => {
74
- if (mode === "repository") return repository ? source.filter((item) => item.repository === repository) : []
75
- return source
76
- }
77
-
78
- const pageItems = (source: readonly PullRequestItem[], cursor: string | null, pageSize: number): PullRequestPage => {
79
- const start = cursor ? Number.parseInt(cursor, 10) : 0
80
- const safeStart = Number.isFinite(start) && start >= 0 ? start : 0
81
- const safePageSize = Math.max(1, Math.min(100, pageSize))
82
- const end = Math.min(source.length, safeStart + safePageSize)
83
- return {
84
- items: source.slice(safeStart, end),
85
- endCursor: end > safeStart ? String(end) : null,
86
- hasNextPage: end < source.length,
87
- }
88
- }
89
-
90
- const mockDiff = `diff --git a/src/mockDiff.ts b/src/mockDiff.ts
91
- --- a/src/mockDiff.ts
92
- +++ b/src/mockDiff.ts
93
- @@ -1,6 +1,6 @@
94
- export const before = true
95
- -const oldOne = 1
96
- +const newOne = 1
97
- - sameName()
98
- + sameName()
99
- -const oldTwo = 2
100
- +const newTwo = 2
101
- export const after = true`
102
-
103
- export const MockGitHubService = {
104
- layer: (options: MockOptions) => {
105
- const items = buildMockPullRequests(options)
106
- const username = options.username ?? "mock-user"
107
- const summaryItems = items.map((item) => ({
108
- ...item,
109
- body: "",
110
- labels: [],
111
- additions: 0,
112
- deletions: 0,
113
- changedFiles: 0,
114
- detailLoaded: false,
115
- } satisfies PullRequestItem))
116
- const findPullRequest = (repository: string, number: number) => items.find((item) => item.repository === repository && item.number === number) ?? items[0]!
117
- const conversationItems = (repository: string, number: number): readonly PullRequestConversationItem[] => [
118
- {
119
- _tag: "comment",
120
- id: `mock-comment:${repository}:${number}:1`,
121
- author: "mock-reviewer",
122
- body: `Top-level discussion for #${number}. This should appear after the summary with its own separator.`,
123
- createdAt: new Date(Date.now() - 3_600_000),
124
- url: null,
125
- },
126
- {
127
- _tag: "review-comment",
128
- id: `mock-review:${repository}:${number}:1`,
129
- author: "mock-reviewer",
130
- body: "Inline review comment rendered in the same conversation stream.",
131
- createdAt: new Date(Date.now() - 1_800_000),
132
- url: null,
133
- path: "src/App.tsx",
134
- line: 42,
135
- side: "RIGHT",
136
- },
137
- ]
138
-
139
- return Layer.succeed(
140
- GitHubService,
141
- GitHubService.of({
142
- listOpenPullRequests: (mode: PullRequestQueueMode, repository: string | null) => Effect.succeed(filterByView(mode, repository, summaryItems)),
143
- listOpenPullRequestPage: (input) => Effect.succeed(pageItems(filterByView(input.mode, input.repository, summaryItems), input.cursor, input.pageSize)),
144
- listOpenPullRequestDetails: (mode: PullRequestQueueMode, repository: string | null) => Effect.succeed(filterByView(mode, repository, items)),
145
- getPullRequestDetails: (repository, number) => Effect.succeed(findPullRequest(repository, number)),
146
- getAuthenticatedUser: () => Effect.succeed(username),
147
- getPullRequestDiff: (_repo, _number) => Effect.succeed(mockDiff),
148
- listPullRequestComments: (_repo, _number) => Effect.succeed([] as readonly PullRequestReviewComment[]),
149
- listPullRequestConversation: (repository, number) => Effect.succeed(conversationItems(repository, number)),
150
- getPullRequestMergeInfo: (repository, number) => Effect.succeed({
151
- repository,
152
- number,
153
- title: `Mock PR ${number}`,
154
- state: "open",
155
- isDraft: false,
156
- mergeable: MERGEABLE_CYCLE[number % MERGEABLE_CYCLE.length]!,
157
- reviewStatus: "approved",
158
- checkStatus: "passing",
159
- checkSummary: "10/10",
160
- autoMergeEnabled: false,
161
- } satisfies PullRequestMergeInfo),
162
- mergePullRequest: () => Effect.void,
163
- closePullRequest: () => Effect.void,
164
- createPullRequestComment: (input: CreatePullRequestCommentInput) => Effect.succeed({
165
- id: `mock:${Date.now()}`,
166
- path: input.path,
167
- line: input.line,
168
- side: input.side,
169
- author: username,
170
- body: input.body,
171
- createdAt: new Date(),
172
- url: null,
173
- } satisfies PullRequestReviewComment),
174
- toggleDraftStatus: () => Effect.void,
175
- listRepoLabels: () => Effect.succeed([]),
176
- addPullRequestLabel: () => Effect.void,
177
- removePullRequestLabel: () => Effect.void,
178
- }),
179
- )
180
- },
181
- }
package/src/themeStore.ts DELETED
@@ -1,60 +0,0 @@
1
- import { mkdir } from "node:fs/promises"
2
- import { homedir } from "node:os"
3
- import { dirname, join } from "node:path"
4
- import { Effect, Schema } from "effect"
5
- import { isThemeId, type ThemeId } from "./ui/colors.js"
6
- import { DiffWhitespaceMode } from "./ui/diff.js"
7
-
8
- interface StoredConfig {
9
- readonly theme?: unknown
10
- readonly diffWhitespaceMode?: unknown
11
- }
12
-
13
- const configDirectory = () => {
14
- if (process.env.GHUI_CONFIG_DIR) return process.env.GHUI_CONFIG_DIR
15
- if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "ghui")
16
- if (process.platform === "win32" && process.env.APPDATA) return join(process.env.APPDATA, "ghui")
17
- return join(homedir(), ".config", "ghui")
18
- }
19
-
20
- export const configPath = () => join(configDirectory(), "config.json")
21
-
22
- const parseConfig = (text: string): StoredConfig => {
23
- const value = JSON.parse(text) as unknown
24
- return value && typeof value === "object" ? value : {}
25
- }
26
-
27
- const readStoredConfig = async () => {
28
- const file = Bun.file(configPath())
29
- return await file.exists() ? parseConfig(await file.text()) : {}
30
- }
31
-
32
- const writeStoredConfig = async (config: StoredConfig) => {
33
- const path = configPath()
34
- await mkdir(dirname(path), { recursive: true })
35
- await Bun.write(path, `${JSON.stringify(config, null, "\t")}\n`)
36
- }
37
-
38
- export const loadStoredThemeId: Effect.Effect<ThemeId> = Effect.catchCause(Effect.tryPromise(async () => {
39
- const config = await readStoredConfig()
40
- return isThemeId(config.theme) ? config.theme : "ghui"
41
- }), () => Effect.succeed("ghui" satisfies ThemeId))
42
-
43
- export const loadStoredDiffWhitespaceMode: Effect.Effect<DiffWhitespaceMode> = Effect.catchCause(Effect.tryPromise(async () => {
44
- const config = await readStoredConfig()
45
- return Schema.is(DiffWhitespaceMode)(config.diffWhitespaceMode) ? config.diffWhitespaceMode : "ignore"
46
- }), () => Effect.succeed("ignore" satisfies DiffWhitespaceMode))
47
-
48
- export const saveStoredThemeId = (theme: ThemeId): Effect.Effect<void> => Effect.tryPromise(async () => {
49
- const config = await readStoredConfig()
50
- if (config.theme === theme) return
51
-
52
- await writeStoredConfig({ ...config, theme })
53
- })
54
-
55
- export const saveStoredDiffWhitespaceMode = (diffWhitespaceMode: DiffWhitespaceMode): Effect.Effect<void> => Effect.tryPromise(async () => {
56
- const config = await readStoredConfig()
57
- if (config.diffWhitespaceMode === diffWhitespaceMode) return
58
-
59
- await writeStoredConfig({ ...config, diffWhitespaceMode })
60
- })
@@ -1,216 +0,0 @@
1
- import { TextAttributes, type MouseEvent } from "@opentui/core"
2
- import { useEffect, useMemo, useState } from "react"
3
- import type { AppCommand } from "../commands.js"
4
- import { clampCommandIndex } from "../commands.js"
5
- import { colors } from "./colors.js"
6
- import { scrollTopForVisibleLine } from "./diff.js"
7
- import { centerCell, Divider, Filler, fitCell, HintRow, ModalFrame, PaddedRow, PlainLine, standardModalDims, TextLine, trimCell } from "./primitives.js"
8
-
9
- const scopeLabels = {
10
- Global: "App",
11
- View: "View",
12
- "Pull request": "Pull Request",
13
- Diff: "Diff",
14
- Navigation: "Navigation",
15
- System: "System",
16
- } as const satisfies Record<AppCommand["scope"], string>
17
-
18
- export type CommandPaletteRow =
19
- | { readonly _tag: "section"; readonly scope: AppCommand["scope"] }
20
- | { readonly _tag: "spacer" }
21
- | { readonly _tag: "command"; readonly command: AppCommand; readonly commandIndex: number }
22
-
23
- export const buildCommandPaletteRows = (commands: readonly AppCommand[]): readonly CommandPaletteRow[] => {
24
- const rows: CommandPaletteRow[] = []
25
- let previousScope: AppCommand["scope"] | null = null
26
- for (let commandIndex = 0; commandIndex < commands.length; commandIndex++) {
27
- const command = commands[commandIndex]!
28
- if (command.scope !== previousScope) {
29
- if (previousScope !== null) rows.push({ _tag: "spacer" })
30
- rows.push({ _tag: "section", scope: command.scope })
31
- previousScope = command.scope
32
- }
33
- rows.push({ _tag: "command", command, commandIndex })
34
- }
35
- return rows
36
- }
37
-
38
- export const commandPaletteSelectedRowIndex = (rows: readonly CommandPaletteRow[], selectedCommandIndex: number) =>
39
- Math.max(0, rows.findIndex((row) => row._tag === "command" && row.commandIndex === selectedCommandIndex))
40
-
41
- export const commandPaletteScrollTop = ({
42
- current,
43
- rowsLength,
44
- listHeight,
45
- selectedRowIndex,
46
- }: {
47
- readonly current: number
48
- readonly rowsLength: number
49
- readonly listHeight: number
50
- readonly selectedRowIndex: number
51
- }) => {
52
- if (rowsLength <= listHeight) return 0
53
- const maxScrollTop = Math.max(0, rowsLength - listHeight)
54
- const clampScrollTop = (value: number) => Math.max(0, Math.min(value, maxScrollTop))
55
- return clampScrollTop(scrollTopForVisibleLine(current, listHeight, selectedRowIndex, 0))
56
- }
57
-
58
- export const commandPaletteClampScrollTop = (rowsLength: number, listHeight: number, value: number) =>
59
- Math.max(0, Math.min(value, Math.max(0, rowsLength - listHeight)))
60
-
61
- export const CommandPalette = ({
62
- commands,
63
- query,
64
- selectedIndex,
65
- modalWidth,
66
- modalHeight,
67
- offsetLeft,
68
- offsetTop,
69
- onSelectCommandIndex,
70
- onRunCommand,
71
- }: {
72
- commands: readonly AppCommand[]
73
- query: string
74
- selectedIndex: number
75
- modalWidth: number
76
- modalHeight: number
77
- offsetLeft: number
78
- offsetTop: number
79
- onSelectCommandIndex: (index: number) => void
80
- onRunCommand: (command: AppCommand) => void
81
- }) => {
82
- const { innerWidth, contentWidth, rowWidth } = standardModalDims(modalWidth, modalHeight)
83
- const listHeight = Math.max(1, modalHeight - 6)
84
- const clampedIndex = clampCommandIndex(selectedIndex, commands)
85
- const [scrollTop, setScrollTop] = useState(0)
86
- const rows = useMemo(() => buildCommandPaletteRows(commands), [commands])
87
- const selectedRowIndex = commandPaletteSelectedRowIndex(rows, clampedIndex)
88
- const visibleRows = rows.slice(scrollTop, scrollTop + listHeight)
89
- const bottomPaddingRows = Math.max(0, listHeight - visibleRows.length)
90
- const countText = commands.length === 1 ? "1 command" : `${commands.length} commands`
91
- const placeholder = "Search"
92
- const titleText = "Commands"
93
- const headerGap = 1
94
- const headerDivider = "│"
95
- const searchGap = 1
96
- const dividerColumn = 1 + titleText.length + headerGap
97
- const searchStart = titleText.length + headerGap + headerDivider.length + searchGap
98
- const countGap = countText.length > 0 ? 2 : 0
99
- const searchWidth = Math.max(1, contentWidth - searchStart - countGap - countText.length)
100
- const queryText = trimCell(query, Math.max(0, searchWidth - 1))
101
- const queryPadding = Math.max(0, searchWidth - queryText.length - 1)
102
- const caretFg = colors.background === "transparent" ? colors.text : colors.background
103
- const emptyTopRows = Math.max(0, Math.floor((listHeight - 1) / 2))
104
- const emptyBottomRows = Math.max(0, listHeight - emptyTopRows - 1)
105
- const runCommandOnMouseDown = (command: AppCommand) => (event: MouseEvent) => {
106
- if (event.button !== 0) return
107
- event.preventDefault()
108
- event.stopPropagation()
109
- onRunCommand(command)
110
- }
111
- const selectCommandOnMouse = (commandIndex: number) => (event: MouseEvent) => {
112
- onSelectCommandIndex(commandIndex)
113
- event.stopPropagation()
114
- }
115
- const handleMouseScroll = (event: MouseEvent) => {
116
- if (!event.scroll || rows.length <= listHeight) return
117
- const delta = Math.max(1, Math.ceil(event.scroll.delta))
118
- const direction = event.scroll.direction === "down" || event.scroll.direction === "right" ? 1 : -1
119
- setScrollTop((current) => commandPaletteClampScrollTop(rows.length, listHeight, current + direction * delta))
120
- event.preventDefault()
121
- event.stopPropagation()
122
- }
123
- const content = rows.length === 0 ? (
124
- <>
125
- <Filler rows={emptyTopRows} prefix="top" />
126
- <PlainLine text={centerCell("No matching command", rowWidth)} fg={colors.muted} />
127
- <Filler rows={emptyBottomRows} prefix="bottom" />
128
- </>
129
- ) : (
130
- <>
131
- {visibleRows.map((row, index) => {
132
- const rowIndex = scrollTop + index
133
- if (row._tag === "spacer") {
134
- return <PlainLine key={`spacer-${rowIndex}`} text="" />
135
- }
136
- if (row._tag === "section") {
137
- return <PlainLine key={`section-${row.scope}-${rowIndex}`} text={fitCell(` ${scopeLabels[row.scope].toUpperCase()}`, rowWidth)} fg={colors.muted} />
138
- }
139
-
140
- const { command, commandIndex } = row
141
- const isSelected = commandIndex === clampedIndex
142
- const shortcut = command.shortcut ? trimCell(command.shortcut, 16) : ""
143
- const shortcutWidth = shortcut.length === 0 ? 0 : Math.min(18, Math.max(6, shortcut.length + 1))
144
- const trailingPadding = shortcut.length === 0 ? 0 : 1
145
- // Layout: "▸ " (2) + title + " " (2) + subtitle + filler + shortcut + " " (1)
146
- const SELECTOR_WIDTH = 2
147
- const titleAvailable = Math.max(8, rowWidth - SELECTOR_WIDTH - shortcutWidth - trailingPadding)
148
- const titleText = trimCell(command.title, Math.min(titleAvailable, 36))
149
- const subtitleSpace = Math.max(0, titleAvailable - titleText.length - 2)
150
- const subtitleText = command.subtitle && subtitleSpace > 4 ? trimCell(command.subtitle, subtitleSpace) : ""
151
- const fillerWidth = Math.max(0, titleAvailable - titleText.length - (subtitleText ? 2 + subtitleText.length : 0))
152
-
153
- return (
154
- <box key={command.id} height={1} onMouseDown={runCommandOnMouseDown(command)} onMouseMove={selectCommandOnMouse(commandIndex)} onMouseOver={selectCommandOnMouse(commandIndex)}>
155
- <TextLine width={rowWidth} bg={isSelected ? colors.selectedBg : undefined} fg={isSelected ? colors.selectedText : colors.text}>
156
- <span fg={isSelected ? colors.accent : colors.muted}>{isSelected ? "▸" : " "}</span>
157
- <span> </span>
158
- {isSelected ? <span attributes={TextAttributes.BOLD}>{titleText}</span> : <span>{titleText}</span>}
159
- {subtitleText ? <span fg={colors.muted}>{` ${subtitleText}`}</span> : null}
160
- {fillerWidth > 0 ? <span>{" ".repeat(fillerWidth)}</span> : null}
161
- {shortcutWidth > 0 ? <span fg={colors.muted}>{fitCell(shortcut, shortcutWidth, "right")}</span> : null}
162
- {trailingPadding > 0 ? <span> </span> : null}
163
- </TextLine>
164
- </box>
165
- )
166
- })}
167
- <Filler rows={bottomPaddingRows} prefix="pad" />
168
- </>
169
- )
170
- useEffect(() => {
171
- setScrollTop((current) => commandPaletteScrollTop({ current, rowsLength: rows.length, listHeight, selectedRowIndex }))
172
- }, [listHeight, rows.length, selectedRowIndex])
173
- return (
174
- <ModalFrame
175
- left={offsetLeft}
176
- top={offsetTop}
177
- width={modalWidth}
178
- height={modalHeight}
179
- junctionRows={[1, modalHeight - 4]}
180
- topJunctionColumns={[dividerColumn]}
181
- >
182
- <PaddedRow>
183
- <TextLine>
184
- <span fg={colors.accent} attributes={TextAttributes.BOLD}>{titleText}</span>
185
- <span>{" ".repeat(headerGap)}</span>
186
- <span fg={colors.separator}>{headerDivider}</span>
187
- <span>{" ".repeat(searchGap)}</span>
188
- {query.length > 0 ? (
189
- <>
190
- <span fg={colors.text}>{queryText}</span>
191
- <span bg={colors.muted} fg={caretFg}> </span>
192
- {queryPadding > 0 ? <span>{" ".repeat(queryPadding)}</span> : null}
193
- </>
194
- ) : (
195
- <>
196
- <span bg={colors.muted} fg={caretFg}>{placeholder[0]}</span>
197
- <span fg={colors.muted}>{fitCell(placeholder.slice(1), Math.max(0, searchWidth - 1))}</span>
198
- </>
199
- )}
200
- {countText.length > 0 && searchWidth > placeholder.length ? (
201
- <>
202
- <span>{" ".repeat(countGap)}</span>
203
- <span fg={colors.muted}>{countText}</span>
204
- </>
205
- ) : null}
206
- </TextLine>
207
- </PaddedRow>
208
- <Divider width={innerWidth} junctionAt={dividerColumn} junctionChar="┴" />
209
- <box height={listHeight} flexDirection="column" onMouseScroll={handleMouseScroll}>{content}</box>
210
- <Divider width={innerWidth} />
211
- <PaddedRow>
212
- <HintRow items={[{ key: "↑↓", label: "select" }, { key: "enter", label: "run" }, { key: "ctrl-u", label: "clear" }, { key: "ctrl-w", label: "word" }, { key: "esc", label: "close" }]} />
213
- </PaddedRow>
214
- </ModalFrame>
215
- )
216
- }