@kitlangton/ghui 0.2.1 → 0.3.0

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.
@@ -0,0 +1,23 @@
1
+ import { context } from "@ghui/keymap"
2
+
3
+ export interface ChangedFilesModalCtx {
4
+ readonly hasResults: boolean
5
+ readonly closeModal: () => void
6
+ readonly selectFile: () => void
7
+ readonly moveSelection: (delta: -1 | 1) => void
8
+ }
9
+
10
+ const ChangedFiles = context<ChangedFilesModalCtx>()
11
+
12
+ export const changedFilesModalKeymap = ChangedFiles(
13
+ { id: "changed-files.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
14
+ {
15
+ id: "changed-files.select",
16
+ title: "Jump to file",
17
+ keys: ["return"],
18
+ enabled: (s) => s.hasResults ? true : "No matching files.",
19
+ run: (s) => s.selectFile(),
20
+ },
21
+ { id: "changed-files.up", title: "Up", keys: ["k", "up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
22
+ { id: "changed-files.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
23
+ )
@@ -11,6 +11,6 @@ const Palette = context<CommandPaletteCtx>()
11
11
  export const commandPaletteKeymap = Palette(
12
12
  { id: "palette.close", title: "Close palette", keys: ["escape", "ctrl+c"], run: (s) => s.closeModal() },
13
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) },
14
+ { id: "palette.up", title: "Up", keys: ["up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
15
+ { id: "palette.down", title: "Down", keys: ["down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
16
16
  )
@@ -11,7 +11,7 @@ const Thread = context<CommentThreadModalCtx>()
11
11
 
12
12
  export const commentThreadModalKeymap = Thread(
13
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() },
14
+ { id: "comment-thread.reply", title: "Reply", keys: ["return"], run: (s) => s.openInlineComment() },
15
15
  { id: "comment-thread.up", title: "Up", keys: ["k", "up"], run: (s) => s.scrollBy(-1) },
16
16
  { id: "comment-thread.down", title: "Down", keys: ["j", "down"], run: (s) => s.scrollBy(1) },
17
17
  { id: "comment-thread.half-up", title: "Half page up", keys: ["pageup", "ctrl+u"], run: (s) => s.scrollBy(-s.halfPage) },
@@ -8,7 +8,6 @@ export interface DiffViewCtx {
8
8
  readonly halfPage: number
9
9
  readonly handleEscape: () => void // closes diff, or clears comment range if active
10
10
  readonly openSelectedComment: () => void
11
- readonly addComment: () => void
12
11
  readonly toggleRange: () => void
13
12
  readonly toggleView: () => void
14
13
  readonly toggleWrap: () => void
@@ -19,6 +18,8 @@ export interface DiffViewCtx {
19
18
  readonly moveAnchorToBoundary: (boundary: "first" | "last") => void
20
19
  readonly alignAnchor: (align: DiffAlign) => void
21
20
  readonly selectSide: (side: DiffSide) => void
21
+ readonly openChangedFiles: () => void
22
+ readonly openSubmitReview: () => void
22
23
  readonly nextFile: () => void
23
24
  readonly previousFile: () => void
24
25
  readonly openInBrowser: () => void
@@ -29,7 +30,6 @@ const Diff = context<DiffViewCtx>()
29
30
  export const diffViewKeymap = Diff(
30
31
  { id: "diff.escape", title: "Close diff / clear range", keys: ["escape"], run: (s) => s.handleEscape() },
31
32
  { 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
33
  { id: "diff.toggle-range", title: "Toggle comment range", keys: ["v"], run: (s) => s.toggleRange() },
34
34
  { id: "diff.toggle-view", title: "Toggle split/unified", keys: ["shift+v"], run: (s) => s.toggleView() },
35
35
  { id: "diff.toggle-wrap", title: "Toggle wrap", keys: ["w"], run: (s) => s.toggleWrap() },
@@ -77,8 +77,10 @@ export const diffViewKeymap = Diff(
77
77
  { id: "diff.side-right", title: "New side", keys: ["right", "l"], run: (s) => s.selectSide("RIGHT") },
78
78
 
79
79
  // File nav
80
+ { id: "diff.changed-files", title: "Changed files", keys: ["f"], run: (s) => s.openChangedFiles() },
80
81
  { id: "diff.next-file", title: "Next file", keys: ["]"], run: (s) => s.nextFile() },
81
82
  { id: "diff.previous-file", title: "Previous file", keys: ["["], run: (s) => s.previousFile() },
83
+ { id: "diff.submit-review", title: "Submit review", keys: ["shift+r"], run: (s) => s.openSubmitReview() },
82
84
 
83
85
  // Boundary jumps + align
84
86
  { id: "diff.first", title: "First comment", keys: ["g g"], run: (s) => s.moveAnchorToBoundary("first") },
@@ -11,6 +11,6 @@ const Label = context<LabelModalCtx>()
11
11
  export const labelModalKeymap = Label(
12
12
  { id: "label-modal.close", title: "Close", keys: ["escape"], run: (s) => s.closeModal() },
13
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) },
14
+ { id: "label-modal.up", title: "Up", keys: ["k", "up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
15
+ { id: "label-modal.down", title: "Down", keys: ["j", "down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
16
16
  )
@@ -0,0 +1,48 @@
1
+ import { context } from "@ghui/keymap"
2
+
3
+ export interface SubmitReviewModalCtx {
4
+ readonly closeModal: () => void
5
+ readonly submit: () => void
6
+ readonly insertNewline: () => void
7
+ readonly moveActionSelection: (delta: -1 | 1) => void
8
+ readonly moveLeft: () => void
9
+ readonly moveRight: () => void
10
+ readonly moveUp: () => void
11
+ readonly moveDown: () => void
12
+ readonly moveLineStart: () => void
13
+ readonly moveLineEnd: () => void
14
+ readonly moveWordBackward: () => void
15
+ readonly moveWordForward: () => void
16
+ readonly backspace: () => void
17
+ readonly deleteForward: () => void
18
+ readonly deleteWordBackward: () => void
19
+ readonly deleteWordForward: () => void
20
+ readonly deleteToLineStart: () => void
21
+ readonly deleteToLineEnd: () => void
22
+ }
23
+
24
+ const SubmitReview = context<SubmitReviewModalCtx>()
25
+
26
+ export const submitReviewModalKeymap = SubmitReview(
27
+ { id: "submit-review.escape", title: "Cancel", keys: ["escape"], run: (s) => s.closeModal() },
28
+ { id: "submit-review.submit", title: "Submit", keys: ["ctrl+s", "return"], run: (s) => s.submit() },
29
+ { id: "submit-review.newline", title: "Insert newline", keys: ["shift+return"], run: (s) => s.insertNewline() },
30
+ { id: "submit-review.next-action", title: "Next action", keys: ["tab"], run: (s) => s.moveActionSelection(1) },
31
+ { id: "submit-review.previous-action", title: "Previous action", keys: ["shift+tab"], run: (s) => s.moveActionSelection(-1) },
32
+
33
+ { id: "submit-review.move-left", title: "Cursor left", keys: ["left", "ctrl+b"], run: (s) => s.moveLeft() },
34
+ { id: "submit-review.move-right", title: "Cursor right", keys: ["right", "ctrl+f"], run: (s) => s.moveRight() },
35
+ { id: "submit-review.move-up", title: "Cursor up", keys: ["up"], run: (s) => s.moveUp() },
36
+ { id: "submit-review.move-down", title: "Cursor down", keys: ["down"], run: (s) => s.moveDown() },
37
+ { id: "submit-review.line-start", title: "Line start", keys: ["home", "ctrl+a"], run: (s) => s.moveLineStart() },
38
+ { id: "submit-review.line-end", title: "Line end", keys: ["end", "ctrl+e"], run: (s) => s.moveLineEnd() },
39
+ { id: "submit-review.word-back", title: "Word backward", keys: ["meta+b", "meta+left"], run: (s) => s.moveWordBackward() },
40
+ { id: "submit-review.word-forward", title: "Word forward", keys: ["meta+f", "meta+right"], run: (s) => s.moveWordForward() },
41
+
42
+ { id: "submit-review.backspace", title: "Backspace", keys: ["backspace"], run: (s) => s.backspace() },
43
+ { id: "submit-review.delete", title: "Delete", keys: ["delete", "ctrl+d"], run: (s) => s.deleteForward() },
44
+ { id: "submit-review.delete-word-back", title: "Delete word backward", keys: ["ctrl+w", "meta+backspace"], run: (s) => s.deleteWordBackward() },
45
+ { id: "submit-review.delete-word-forward", title: "Delete word forward", keys: ["meta+delete"], run: (s) => s.deleteWordForward() },
46
+ { id: "submit-review.delete-to-line-start", title: "Delete to line start", keys: ["ctrl+u"], run: (s) => s.deleteToLineStart() },
47
+ { id: "submit-review.delete-to-line-end", title: "Delete to line end", keys: ["ctrl+k"], run: (s) => s.deleteToLineEnd() },
48
+ )
@@ -30,8 +30,8 @@ export const themeModalKeymap = Theme(
30
30
  enabled: (s) => s.filterMode && !s.hasFilteredResults ? "No matching themes." : true,
31
31
  run: (s) => s.confirmSelection(),
32
32
  },
33
- { id: "theme-modal.up-arrow", title: "Up", keys: ["up"], run: (s) => s.moveSelection(-1) },
34
- { id: "theme-modal.down-arrow", title: "Down", keys: ["down"], run: (s) => s.moveSelection(1) },
33
+ { id: "theme-modal.up-arrow", title: "Up", keys: ["up", "ctrl+p", "ctrl+k"], run: (s) => s.moveSelection(-1) },
34
+ { id: "theme-modal.down-arrow", title: "Down", keys: ["down", "ctrl+n", "ctrl+j"], run: (s) => s.moveSelection(1) },
35
35
  {
36
36
  id: "theme-modal.up-letter",
37
37
  title: "Up",
@@ -4,9 +4,7 @@ export type PullRequestView =
4
4
  | { readonly _tag: "Repository"; readonly repository: string }
5
5
  | { readonly _tag: "Queue"; readonly mode: PullRequestUserQueueMode; readonly repository: string | null }
6
6
 
7
- export const initialPullRequestView = (repository: string | null): PullRequestView => repository
8
- ? { _tag: "Repository", repository }
9
- : { _tag: "Queue", mode: "authored", repository: null }
7
+ export const initialPullRequestView = (): PullRequestView => ({ _tag: "Queue", mode: "authored", repository: null })
10
8
 
11
9
  export const viewMode = (view: PullRequestView): PullRequestQueueMode => view._tag === "Repository" ? "repository" : view.mode
12
10
 
@@ -1,6 +1,6 @@
1
1
  import { Context, Effect, Layer, Schema } from "effect"
2
2
  import { config } from "../config.js"
3
- import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type ListPullRequestPageInput, type Mergeable, type PullRequestConversationItem, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestPage, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus } from "../domain.js"
3
+ import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type ListPullRequestPageInput, type Mergeable, type PullRequestConversationItem, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestPage, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus, type SubmitPullRequestReviewInput } from "../domain.js"
4
4
  import { getMergeActionDefinition } from "../mergeActions.js"
5
5
  import { CommandError, CommandRunner, type JsonParseError } from "./CommandRunner.js"
6
6
 
@@ -413,9 +413,9 @@ const parsePullRequest = (item: RawPullRequestNode): PullRequestItem => {
413
413
  }
414
414
  }
415
415
 
416
- const searchQuery = (mode: PullRequestQueueMode, author: string, repository: string | null) => {
416
+ const searchQuery = (mode: PullRequestQueueMode, repository: string | null) => {
417
417
  const sort = mode === "repository" ? "sort:updated-desc" : "sort:created-desc"
418
- return `${pullRequestQueueSearchQualifier(mode, author, repository)} is:pr is:open ${sort}`
418
+ return `${pullRequestQueueSearchQualifier(mode, repository)} is:pr is:open ${sort}`
419
419
  }
420
420
 
421
421
  const pullRequestPage = <Item>(connection: PullRequestConnection<Item>, parse: (node: Item) => PullRequestItem): PullRequestPage => ({
@@ -506,7 +506,7 @@ const fallbackCreatedComment = (input: CreatePullRequestCommentInput): PullReque
506
506
  path: input.path,
507
507
  line: input.line,
508
508
  side: input.side,
509
- author: config.author.replace(/^@/, "") || "you",
509
+ author: "you",
510
510
  body: input.body,
511
511
  createdAt: new Date(),
512
512
  url: null,
@@ -522,6 +522,12 @@ const MERGEABLE_BY_RAW: Record<string, Mergeable> = {
522
522
  const normalizeMergeable = (value: string): Mergeable =>
523
523
  MERGEABLE_BY_RAW[value] ?? "unknown"
524
524
 
525
+ const REVIEW_EVENT_CLI_FLAG = {
526
+ COMMENT: "--comment",
527
+ APPROVE: "--approve",
528
+ REQUEST_CHANGES: "--request-changes",
529
+ } as const satisfies Record<SubmitPullRequestReviewInput["event"], string>
530
+
525
531
  export class GitHubService extends Context.Service<GitHubService, {
526
532
  readonly listOpenPullRequests: (mode: PullRequestQueueMode, repository: string | null) => Effect.Effect<readonly PullRequestItem[], GitHubError>
527
533
  readonly listOpenPullRequestPage: (input: ListPullRequestPageInput) => Effect.Effect<PullRequestPage, GitHubError>
@@ -535,6 +541,7 @@ export class GitHubService extends Context.Service<GitHubService, {
535
541
  readonly mergePullRequest: (repository: string, number: number, action: PullRequestMergeAction) => Effect.Effect<void, CommandError>
536
542
  readonly closePullRequest: (repository: string, number: number) => Effect.Effect<void, CommandError>
537
543
  readonly createPullRequestComment: (input: CreatePullRequestCommentInput) => Effect.Effect<PullRequestReviewComment, GitHubError>
544
+ readonly submitPullRequestReview: (input: SubmitPullRequestReviewInput) => Effect.Effect<void, CommandError>
538
545
  readonly toggleDraftStatus: (repository: string, number: number, isDraft: boolean) => Effect.Effect<void, CommandError>
539
546
  readonly listRepoLabels: (repository: string) => Effect.Effect<readonly { readonly name: string; readonly color: string | null }[], GitHubError>
540
547
  readonly addPullRequestLabel: (repository: string, number: number, label: string) => Effect.Effect<void, CommandError>
@@ -557,7 +564,7 @@ export class GitHubService extends Context.Service<GitHubService, {
557
564
  const response: SearchResponse<Item["Type"]> = yield* command.runSchema(responseSchema, "gh", [
558
565
  "api", "graphql",
559
566
  "-f", `query=${query}`,
560
- "-F", `searchQuery=${searchQuery(input.mode, config.author, input.repository)}`,
567
+ "-F", `searchQuery=${searchQuery(input.mode, input.repository)}`,
561
568
  "-F", `first=${input.pageSize}`,
562
569
  ...(input.cursor ? ["-F", `after=${input.cursor}`] : []),
563
570
  ])
@@ -702,6 +709,13 @@ export class GitHubService extends Context.Service<GitHubService, {
702
709
  return parsePullRequestComment(response) ?? fallbackCreatedComment(input)
703
710
  })
704
711
 
712
+ const submitPullRequestReview = (input: SubmitPullRequestReviewInput) =>
713
+ ghVoid("submitPullRequestReview", [
714
+ "pr", "review", String(input.number), "--repo", input.repository,
715
+ REVIEW_EVENT_CLI_FLAG[input.event],
716
+ "--body", input.body,
717
+ ])
718
+
705
719
  const toggleDraftStatus = (repository: string, number: number, isDraft: boolean) =>
706
720
  ghVoid("toggleDraftStatus", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
707
721
 
@@ -729,6 +743,7 @@ export class GitHubService extends Context.Service<GitHubService, {
729
743
  mergePullRequest,
730
744
  closePullRequest,
731
745
  createPullRequestComment,
746
+ submitPullRequestReview,
732
747
  toggleDraftStatus,
733
748
  listRepoLabels,
734
749
  addPullRequestLabel,
@@ -171,6 +171,7 @@ export const MockGitHubService = {
171
171
  createdAt: new Date(),
172
172
  url: null,
173
173
  } satisfies PullRequestReviewComment),
174
+ submitPullRequestReview: () => Effect.void,
174
175
  toggleDraftStatus: () => Effect.void,
175
176
  listRepoLabels: () => Effect.succeed([]),
176
177
  addPullRequestLabel: () => Effect.void,
@@ -4,7 +4,7 @@ import type { AppCommand } from "../commands.js"
4
4
  import { clampCommandIndex } from "../commands.js"
5
5
  import { colors } from "./colors.js"
6
6
  import { scrollTopForVisibleLine } from "./diff.js"
7
- import { centerCell, Divider, Filler, fitCell, HintRow, ModalFrame, PaddedRow, PlainLine, standardModalDims, TextLine, trimCell } from "./primitives.js"
7
+ import { centerCell, Filler, fitCell, HintRow, PlainLine, searchModalDims, SearchModalFrame, TextLine, trimCell } from "./primitives.js"
8
8
 
9
9
  const scopeLabels = {
10
10
  Global: "App",
@@ -79,8 +79,7 @@ export const CommandPalette = ({
79
79
  onSelectCommandIndex: (index: number) => void
80
80
  onRunCommand: (command: AppCommand) => void
81
81
  }) => {
82
- const { innerWidth, contentWidth, rowWidth } = standardModalDims(modalWidth, modalHeight)
83
- const listHeight = Math.max(1, modalHeight - 6)
82
+ const { bodyHeight: listHeight, rowWidth } = searchModalDims(modalWidth, modalHeight)
84
83
  const clampedIndex = clampCommandIndex(selectedIndex, commands)
85
84
  const [scrollTop, setScrollTop] = useState(0)
86
85
  const rows = useMemo(() => buildCommandPaletteRows(commands), [commands])
@@ -88,18 +87,6 @@ export const CommandPalette = ({
88
87
  const visibleRows = rows.slice(scrollTop, scrollTop + listHeight)
89
88
  const bottomPaddingRows = Math.max(0, listHeight - visibleRows.length)
90
89
  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
90
  const emptyTopRows = Math.max(0, Math.floor((listHeight - 1) / 2))
104
91
  const emptyBottomRows = Math.max(0, listHeight - emptyTopRows - 1)
105
92
  const runCommandOnMouseDown = (command: AppCommand) => (event: MouseEvent) => {
@@ -171,46 +158,19 @@ export const CommandPalette = ({
171
158
  setScrollTop((current) => commandPaletteScrollTop({ current, rowsLength: rows.length, listHeight, selectedRowIndex }))
172
159
  }, [listHeight, rows.length, selectedRowIndex])
173
160
  return (
174
- <ModalFrame
161
+ <SearchModalFrame
175
162
  left={offsetLeft}
176
163
  top={offsetTop}
177
164
  width={modalWidth}
178
165
  height={modalHeight}
179
- junctionRows={[1, modalHeight - 4]}
180
- topJunctionColumns={[dividerColumn]}
166
+ title="Commands"
167
+ query={query}
168
+ placeholder="Search"
169
+ countText={countText}
170
+ onBodyMouseScroll={handleMouseScroll}
171
+ footer={<HintRow items={[{ key: "↑↓", label: "select" }, { key: "enter", label: "run" }, { key: "esc", label: "close" }]} />}
181
172
  >
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>
173
+ {content}
174
+ </SearchModalFrame>
215
175
  )
216
176
  }
@@ -1,7 +1,7 @@
1
1
  import type { DiffRenderable, MouseEvent, ScrollBoxRenderable } from "@opentui/core"
2
2
  import { useMemo, type Ref } from "react"
3
3
  import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
4
- import { colors, type ThemeId } from "./colors.js"
4
+ import { colors, lineNumberTextColor, type ThemeId } from "./colors.js"
5
5
  import { CommentBodyLine, commentCountText, commentMetaSegments, CommentSegmentsLine } from "./comments.js"
6
6
  import { createDiffSyntaxStyle, diffCommentAnchorLabel, diffCommentLineLabel, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileIndexAtLine, type DiffFileStats, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
7
7
  import { LoadingPane, StatusCard } from "./DetailsPane.js"
@@ -162,6 +162,7 @@ export const PullRequestDiffPane = ({
162
162
  return ` ${selectedCommentLabel ?? diffCommentAnchorLabel(selectedCommentAnchor)}`
163
163
  }
164
164
  const stickyCommentColor = selectedCommentAnchor?.side === "LEFT" ? colors.status.failing : colors.status.passing
165
+ const diffLineNumberFg = lineNumberTextColor(colors.diff.lineNumberBg, colors.text)
165
166
  const handleDiffMouseDown = function (this: ScrollBoxRenderable, event: MouseEvent) {
166
167
  if (event.button !== 0) return
167
168
  const localY = event.y - this.viewport.y
@@ -201,7 +202,7 @@ export const PullRequestDiffPane = ({
201
202
  contextBg={colors.diff.contextBg}
202
203
  addedSignColor={colors.status.passing}
203
204
  removedSignColor={colors.status.failing}
204
- lineNumberFg={colors.muted}
205
+ lineNumberFg={diffLineNumberFg}
205
206
  lineNumberBg={colors.diff.lineNumberBg}
206
207
  addedLineNumberBg={colors.diff.addedLineNumberBg}
207
208
  removedLineNumberBg={colors.diff.removedLineNumberBg}
@@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core"
2
2
  import type { LoadStatus, PullRequestItem } from "../domain.js"
3
3
  import { daysOpen } from "../date.js"
4
4
  import { colors } from "./colors.js"
5
- import { fitCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
5
+ import { fitCell, MatchedCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
6
6
  import { pullRequestRowDisplay, repoColor, reviewIcon } from "./pullRequests.js"
7
7
 
8
8
  export type PullRequestGroups = Array<[string, PullRequestItem[]]>
@@ -37,22 +37,6 @@ const groupAgeWidth = (pullRequests: readonly PullRequestItem[]) => {
37
37
  return Math.max(4, maxLen + 1)
38
38
  }
39
39
 
40
- const MatchedCell = ({ text, width, query, align = "left" }: { text: string; width: number; query: string; align?: "left" | "right" }) => {
41
- const fitted = fitCell(text, width, align)
42
- const needle = query.trim().toLowerCase()
43
- const index = needle.length > 0 ? fitted.toLowerCase().indexOf(needle) : -1
44
- if (index < 0) return <span>{fitted}</span>
45
-
46
- const end = Math.min(fitted.length, index + needle.length)
47
- return (
48
- <>
49
- {index > 0 ? <span>{fitted.slice(0, index)}</span> : null}
50
- <span fg={colors.accent} attributes={TextAttributes.BOLD}>{fitted.slice(index, end)}</span>
51
- {end < fitted.length ? <span>{fitted.slice(end)}</span> : null}
52
- </>
53
- )
54
- }
55
-
56
40
  const GroupTitle = ({ label, color, filterText }: { label: string; color: string; filterText: string }) => (
57
41
  <TextLine>
58
42
  <span fg={color}>{GROUP_ICON} </span>
package/src/ui/colors.ts CHANGED
@@ -154,6 +154,13 @@ const mutedTextColor = (background: string) => {
154
154
 
155
155
  const contrastText = (background: string) => luminance(background) > 128 ? "#000000" : "#ffffff"
156
156
 
157
+ export const lineNumberTextColor = (background: string, foreground: string) => {
158
+ const bg = readableHex(background, foreground)
159
+ const fg = readableHex(foreground, contrastText(bg))
160
+ const contrast = Math.abs(luminance(bg) - luminance(fg))
161
+ return mixHex(bg, fg, contrast < 90 ? 0.62 : 0.5)
162
+ }
163
+
157
164
  const ghuiColors: ColorPalette = {
158
165
  background: "#111018",
159
166
  modalBackground: "#1a1a2e",