stage-tui 1.0.7
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 +112 -0
- package/bin/stage +51 -0
- package/index.ts +22 -0
- package/package.json +46 -0
- package/src/ai-commit.ts +706 -0
- package/src/app.tsx +127 -0
- package/src/config-file.ts +40 -0
- package/src/config.ts +283 -0
- package/src/git-branch-name.ts +13 -0
- package/src/git-process.ts +25 -0
- package/src/git-status-parser.ts +103 -0
- package/src/git.ts +298 -0
- package/src/hooks/use-branch-dialog-controller.ts +188 -0
- package/src/hooks/use-commit-history-controller.ts +130 -0
- package/src/hooks/use-git-tui-controller.ts +310 -0
- package/src/hooks/use-git-tui-effects.ts +168 -0
- package/src/hooks/use-git-tui-keyboard.ts +293 -0
- package/src/ui/components/branch-dialog.tsx +107 -0
- package/src/ui/components/commit-dialog.tsx +68 -0
- package/src/ui/components/commit-history-dialog.tsx +87 -0
- package/src/ui/components/diff-workspace.tsx +108 -0
- package/src/ui/components/footer-bar.tsx +65 -0
- package/src/ui/components/shortcuts-dialog.tsx +53 -0
- package/src/ui/components/top-bar.tsx +36 -0
- package/src/ui/diff-style.ts +33 -0
- package/src/ui/theme.ts +151 -0
- package/src/ui/types.ts +21 -0
- package/src/ui/utils.ts +99 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react"
|
|
2
|
+
import type { Dispatch, SetStateAction } from "react"
|
|
3
|
+
|
|
4
|
+
import { COMMIT_FOCUS_ORDER, MAIN_FOCUS_ORDER, type FocusTarget, type TopAction } from "../ui/types"
|
|
5
|
+
|
|
6
|
+
type UseGitTuiKeyboardParams = {
|
|
7
|
+
renderer: {
|
|
8
|
+
destroy: () => void
|
|
9
|
+
getSelection?: () => { getSelectedText: () => string } | null
|
|
10
|
+
copyToClipboardOSC52?: (text: string) => boolean
|
|
11
|
+
}
|
|
12
|
+
commitDialogOpen: boolean
|
|
13
|
+
branchDialogOpen: boolean
|
|
14
|
+
branchDialogMode: "select" | "create" | "confirm"
|
|
15
|
+
historyDialogOpen: boolean
|
|
16
|
+
historyDialogMode: "list" | "action"
|
|
17
|
+
shortcutsDialogOpen: boolean
|
|
18
|
+
setCommitDialogOpen: Dispatch<SetStateAction<boolean>>
|
|
19
|
+
setFocus: Dispatch<SetStateAction<FocusTarget>>
|
|
20
|
+
focus: FocusTarget
|
|
21
|
+
fileCount: number
|
|
22
|
+
moveToPreviousFile: () => void
|
|
23
|
+
moveToNextFile: () => void
|
|
24
|
+
openBranchDialog: () => void
|
|
25
|
+
closeBranchDialog: () => void
|
|
26
|
+
showBranchDialogList: () => void
|
|
27
|
+
submitBranchSelection: () => Promise<void>
|
|
28
|
+
submitBranchStrategy: () => Promise<void>
|
|
29
|
+
openHistoryDialog: () => Promise<void>
|
|
30
|
+
closeHistoryDialog: () => void
|
|
31
|
+
backToHistoryCommitList: () => void
|
|
32
|
+
submitHistoryCommitSelection: () => Promise<void>
|
|
33
|
+
submitHistoryAction: () => Promise<void>
|
|
34
|
+
commitChanges: () => Promise<void>
|
|
35
|
+
createBranchAndCheckout: () => Promise<void>
|
|
36
|
+
openCommitDialog: () => void
|
|
37
|
+
openShortcutsDialog: () => void
|
|
38
|
+
closeShortcutsDialog: () => void
|
|
39
|
+
runTopAction: (action: TopAction) => Promise<void>
|
|
40
|
+
toggleSelectedFileInCommit: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useGitTuiKeyboard({
|
|
44
|
+
renderer,
|
|
45
|
+
commitDialogOpen,
|
|
46
|
+
branchDialogOpen,
|
|
47
|
+
branchDialogMode,
|
|
48
|
+
historyDialogOpen,
|
|
49
|
+
historyDialogMode,
|
|
50
|
+
shortcutsDialogOpen,
|
|
51
|
+
setCommitDialogOpen,
|
|
52
|
+
setFocus,
|
|
53
|
+
focus,
|
|
54
|
+
fileCount,
|
|
55
|
+
moveToPreviousFile,
|
|
56
|
+
moveToNextFile,
|
|
57
|
+
openBranchDialog,
|
|
58
|
+
closeBranchDialog,
|
|
59
|
+
showBranchDialogList,
|
|
60
|
+
submitBranchSelection,
|
|
61
|
+
submitBranchStrategy,
|
|
62
|
+
openHistoryDialog,
|
|
63
|
+
closeHistoryDialog,
|
|
64
|
+
backToHistoryCommitList,
|
|
65
|
+
submitHistoryCommitSelection,
|
|
66
|
+
submitHistoryAction,
|
|
67
|
+
commitChanges,
|
|
68
|
+
createBranchAndCheckout,
|
|
69
|
+
openCommitDialog,
|
|
70
|
+
openShortcutsDialog,
|
|
71
|
+
closeShortcutsDialog,
|
|
72
|
+
runTopAction,
|
|
73
|
+
toggleSelectedFileInCommit,
|
|
74
|
+
}: UseGitTuiKeyboardParams) {
|
|
75
|
+
useKeyboard((key) => {
|
|
76
|
+
const hasNonShiftModifier = key.ctrl || key.meta || key.option || key.super || key.hyper
|
|
77
|
+
const isPlainShortcutKey = !hasNonShiftModifier
|
|
78
|
+
const isMetaCopy = (key.meta || key.super) && !key.ctrl && !key.option && !key.hyper && key.name === "c"
|
|
79
|
+
const isHelpKey = isPlainShortcutKey && (key.name === "?" || ((key.name === "/" || key.name === "slash") && key.shift))
|
|
80
|
+
const isSpaceKey = key.name === "space" || key.name === " "
|
|
81
|
+
const isEnter = key.name === "return" || key.name === "linefeed"
|
|
82
|
+
const isDialogOpen = commitDialogOpen || branchDialogOpen || historyDialogOpen || shortcutsDialogOpen
|
|
83
|
+
|
|
84
|
+
if (isMetaCopy) {
|
|
85
|
+
const selectedText = renderer.getSelection?.()?.getSelectedText?.()
|
|
86
|
+
if (selectedText && selectedText.length > 0) {
|
|
87
|
+
renderer.copyToClipboardOSC52?.(selectedText)
|
|
88
|
+
key.preventDefault()
|
|
89
|
+
key.stopPropagation()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!commitDialogOpen && !branchDialogOpen && !historyDialogOpen && isHelpKey) {
|
|
95
|
+
key.preventDefault()
|
|
96
|
+
key.stopPropagation()
|
|
97
|
+
if (shortcutsDialogOpen) {
|
|
98
|
+
closeShortcutsDialog()
|
|
99
|
+
} else {
|
|
100
|
+
openShortcutsDialog()
|
|
101
|
+
}
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (shortcutsDialogOpen) {
|
|
106
|
+
if (key.name === "escape") {
|
|
107
|
+
key.preventDefault()
|
|
108
|
+
key.stopPropagation()
|
|
109
|
+
closeShortcutsDialog()
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (historyDialogOpen && isEnter) {
|
|
115
|
+
key.preventDefault()
|
|
116
|
+
key.stopPropagation()
|
|
117
|
+
if (historyDialogMode === "action") {
|
|
118
|
+
void submitHistoryAction()
|
|
119
|
+
} else {
|
|
120
|
+
void submitHistoryCommitSelection()
|
|
121
|
+
}
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (branchDialogOpen && isEnter) {
|
|
126
|
+
key.preventDefault()
|
|
127
|
+
key.stopPropagation()
|
|
128
|
+
if (branchDialogMode === "create") {
|
|
129
|
+
void createBranchAndCheckout()
|
|
130
|
+
} else if (branchDialogMode === "confirm") {
|
|
131
|
+
void submitBranchStrategy()
|
|
132
|
+
} else {
|
|
133
|
+
void submitBranchSelection()
|
|
134
|
+
}
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (commitDialogOpen && isEnter) {
|
|
139
|
+
key.preventDefault()
|
|
140
|
+
key.stopPropagation()
|
|
141
|
+
void commitChanges()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (key.name === "escape") {
|
|
146
|
+
if (historyDialogOpen) {
|
|
147
|
+
if (historyDialogMode === "action") {
|
|
148
|
+
backToHistoryCommitList()
|
|
149
|
+
} else {
|
|
150
|
+
closeHistoryDialog()
|
|
151
|
+
}
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (branchDialogOpen) {
|
|
155
|
+
if (branchDialogMode === "select") {
|
|
156
|
+
closeBranchDialog()
|
|
157
|
+
} else {
|
|
158
|
+
showBranchDialogList()
|
|
159
|
+
}
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if (commitDialogOpen) {
|
|
163
|
+
setCommitDialogOpen(false)
|
|
164
|
+
setFocus("files")
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
renderer.destroy()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (key.name === "tab") {
|
|
172
|
+
key.preventDefault()
|
|
173
|
+
key.stopPropagation()
|
|
174
|
+
if (branchDialogOpen) {
|
|
175
|
+
setFocus(branchDialogMode === "create" ? "branch-create" : "branch-dialog-list")
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (historyDialogOpen) {
|
|
179
|
+
setFocus(historyDialogMode === "action" ? "history-actions" : "history-commits")
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const order = commitDialogOpen ? COMMIT_FOCUS_ORDER : MAIN_FOCUS_ORDER
|
|
183
|
+
setFocus((current) => {
|
|
184
|
+
const currentIndex = order.findIndex((item) => item === current)
|
|
185
|
+
if (currentIndex < 0) return order[0] ?? "files"
|
|
186
|
+
const nextIndex = key.shift ? (currentIndex - 1 + order.length) % order.length : (currentIndex + 1) % order.length
|
|
187
|
+
return order[nextIndex] ?? "files"
|
|
188
|
+
})
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "c") {
|
|
193
|
+
key.preventDefault()
|
|
194
|
+
key.stopPropagation()
|
|
195
|
+
openCommitDialog()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "b") {
|
|
200
|
+
key.preventDefault()
|
|
201
|
+
key.stopPropagation()
|
|
202
|
+
openBranchDialog()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "h") {
|
|
207
|
+
key.preventDefault()
|
|
208
|
+
key.stopPropagation()
|
|
209
|
+
void openHistoryDialog()
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!isDialogOpen && isPlainShortcutKey && focus === "files" && fileCount > 0 && isSpaceKey) {
|
|
214
|
+
key.preventDefault()
|
|
215
|
+
key.stopPropagation()
|
|
216
|
+
toggleSelectedFileInCommit()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!isDialogOpen && isPlainShortcutKey && focus === "files" && fileCount > 0 && key.name === "up") {
|
|
221
|
+
key.preventDefault()
|
|
222
|
+
key.stopPropagation()
|
|
223
|
+
moveToPreviousFile()
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (!isDialogOpen && isPlainShortcutKey && focus === "files" && fileCount > 0 && key.name === "down") {
|
|
227
|
+
key.preventDefault()
|
|
228
|
+
key.stopPropagation()
|
|
229
|
+
moveToNextFile()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!isDialogOpen && key.ctrl && key.name === "r") {
|
|
234
|
+
key.preventDefault()
|
|
235
|
+
key.stopPropagation()
|
|
236
|
+
void runTopAction("refresh")
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (!isDialogOpen && key.ctrl && key.name === "f") {
|
|
240
|
+
key.preventDefault()
|
|
241
|
+
key.stopPropagation()
|
|
242
|
+
void runTopAction("fetch")
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (!isDialogOpen && key.ctrl && key.name === "l") {
|
|
246
|
+
key.preventDefault()
|
|
247
|
+
key.stopPropagation()
|
|
248
|
+
void runTopAction("pull")
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (!isDialogOpen && key.ctrl && key.name === "p") {
|
|
252
|
+
key.preventDefault()
|
|
253
|
+
key.stopPropagation()
|
|
254
|
+
void runTopAction("push")
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "r") {
|
|
259
|
+
key.preventDefault()
|
|
260
|
+
key.stopPropagation()
|
|
261
|
+
void runTopAction("refresh")
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "f") {
|
|
265
|
+
key.preventDefault()
|
|
266
|
+
key.stopPropagation()
|
|
267
|
+
void runTopAction("fetch")
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "l") {
|
|
271
|
+
key.preventDefault()
|
|
272
|
+
key.stopPropagation()
|
|
273
|
+
void runTopAction("pull")
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (!isDialogOpen && isPlainShortcutKey && key.name === "p") {
|
|
277
|
+
key.preventDefault()
|
|
278
|
+
key.stopPropagation()
|
|
279
|
+
void runTopAction("push")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (key.ctrl && key.name === "return") {
|
|
284
|
+
key.preventDefault()
|
|
285
|
+
key.stopPropagation()
|
|
286
|
+
if (commitDialogOpen) {
|
|
287
|
+
void commitChanges()
|
|
288
|
+
} else {
|
|
289
|
+
openCommitDialog()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { InputRenderable, SelectOption } from "@opentui/core"
|
|
2
|
+
import type { RefObject } from "react"
|
|
3
|
+
|
|
4
|
+
import type { UiTheme } from "../theme"
|
|
5
|
+
import type { FocusTarget } from "../types"
|
|
6
|
+
|
|
7
|
+
type BranchDialogProps = {
|
|
8
|
+
open: boolean
|
|
9
|
+
mode: "select" | "create" | "confirm"
|
|
10
|
+
focus: FocusTarget
|
|
11
|
+
currentBranch: string
|
|
12
|
+
branchOptions: SelectOption[]
|
|
13
|
+
branchOptionsKey: string
|
|
14
|
+
branchIndex: number
|
|
15
|
+
onBranchChange: (index: number) => void
|
|
16
|
+
branchStrategyOptions: SelectOption[]
|
|
17
|
+
branchStrategyIndex: number
|
|
18
|
+
onBranchStrategyChange: (index: number) => void
|
|
19
|
+
branchName: string
|
|
20
|
+
branchNameRef: RefObject<InputRenderable | null>
|
|
21
|
+
onBranchNameInput: (value: string) => void
|
|
22
|
+
theme: UiTheme
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function BranchDialog({
|
|
26
|
+
open,
|
|
27
|
+
mode,
|
|
28
|
+
focus,
|
|
29
|
+
currentBranch,
|
|
30
|
+
branchOptions,
|
|
31
|
+
branchOptionsKey,
|
|
32
|
+
branchIndex,
|
|
33
|
+
onBranchChange,
|
|
34
|
+
branchStrategyOptions,
|
|
35
|
+
branchStrategyIndex,
|
|
36
|
+
onBranchStrategyChange,
|
|
37
|
+
branchName,
|
|
38
|
+
branchNameRef,
|
|
39
|
+
onBranchNameInput,
|
|
40
|
+
theme,
|
|
41
|
+
}: BranchDialogProps) {
|
|
42
|
+
if (!open) return null
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box
|
|
46
|
+
style={{
|
|
47
|
+
width: "100%",
|
|
48
|
+
flexGrow: 1,
|
|
49
|
+
paddingLeft: 6,
|
|
50
|
+
paddingRight: 6,
|
|
51
|
+
paddingTop: 8,
|
|
52
|
+
paddingBottom: 4,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<box style={{ width: "100%", maxWidth: 88, gap: 1, flexDirection: "column", flexGrow: 1 }}>
|
|
56
|
+
<text fg={theme.colors.title}>change branch</text>
|
|
57
|
+
<text fg={theme.colors.subtleText}>current: {currentBranch}</text>
|
|
58
|
+
{mode === "select" ? (
|
|
59
|
+
<>
|
|
60
|
+
<text fg={theme.colors.subtleText}>enter to checkout | select & enter to create branch | esc to close</text>
|
|
61
|
+
<select
|
|
62
|
+
key={branchOptionsKey}
|
|
63
|
+
style={{ width: "100%", height: "100%", textColor: theme.colors.selectText }}
|
|
64
|
+
options={branchOptions}
|
|
65
|
+
selectedIndex={branchIndex}
|
|
66
|
+
showDescription={true}
|
|
67
|
+
focused={focus === "branch-dialog-list"}
|
|
68
|
+
selectedBackgroundColor={theme.colors.selectSelectedBackground}
|
|
69
|
+
selectedTextColor={theme.colors.selectSelectedText}
|
|
70
|
+
focusedTextColor={theme.colors.selectFocusedText}
|
|
71
|
+
onChange={onBranchChange}
|
|
72
|
+
/>
|
|
73
|
+
</>
|
|
74
|
+
) : mode === "confirm" ? (
|
|
75
|
+
<>
|
|
76
|
+
<text fg={theme.colors.subtleText}>what should happen to your working changes? | enter to continue | esc to go back</text>
|
|
77
|
+
<select
|
|
78
|
+
key={`${branchOptionsKey}-strategy`}
|
|
79
|
+
style={{ width: "100%", height: "100%", textColor: theme.colors.selectText }}
|
|
80
|
+
options={branchStrategyOptions}
|
|
81
|
+
selectedIndex={branchStrategyIndex}
|
|
82
|
+
showDescription={true}
|
|
83
|
+
focused={focus === "branch-dialog-list"}
|
|
84
|
+
selectedBackgroundColor={theme.colors.selectSelectedBackground}
|
|
85
|
+
selectedTextColor={theme.colors.selectSelectedText}
|
|
86
|
+
focusedTextColor={theme.colors.selectFocusedText}
|
|
87
|
+
onChange={onBranchStrategyChange}
|
|
88
|
+
/>
|
|
89
|
+
</>
|
|
90
|
+
) : (
|
|
91
|
+
<>
|
|
92
|
+
<text fg={theme.colors.subtleText}>enter to create and checkout | invalid chars become "-" | esc to go back</text>
|
|
93
|
+
<input
|
|
94
|
+
ref={branchNameRef}
|
|
95
|
+
value={branchName}
|
|
96
|
+
onInput={onBranchNameInput}
|
|
97
|
+
placeholder="new branch name"
|
|
98
|
+
focused={focus === "branch-create"}
|
|
99
|
+
textColor={theme.colors.inputText}
|
|
100
|
+
focusedTextColor={theme.colors.inputFocusedText}
|
|
101
|
+
/>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</box>
|
|
105
|
+
</box>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { InputRenderable, TextareaRenderable } from "@opentui/core"
|
|
2
|
+
import type { RefObject } from "react"
|
|
3
|
+
|
|
4
|
+
import type { UiTheme } from "../theme"
|
|
5
|
+
import type { FocusTarget } from "../types"
|
|
6
|
+
|
|
7
|
+
type CommitDialogProps = {
|
|
8
|
+
open: boolean
|
|
9
|
+
focus: FocusTarget
|
|
10
|
+
summary: string
|
|
11
|
+
descriptionRenderKey: number
|
|
12
|
+
summaryRef: RefObject<InputRenderable | null>
|
|
13
|
+
descriptionRef: RefObject<TextareaRenderable | null>
|
|
14
|
+
onSummaryInput: (value: string) => void
|
|
15
|
+
theme: UiTheme
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CommitDialog({
|
|
19
|
+
open,
|
|
20
|
+
focus,
|
|
21
|
+
summary,
|
|
22
|
+
descriptionRenderKey,
|
|
23
|
+
summaryRef,
|
|
24
|
+
descriptionRef,
|
|
25
|
+
onSummaryInput,
|
|
26
|
+
theme,
|
|
27
|
+
}: CommitDialogProps) {
|
|
28
|
+
if (!open) return null
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<box
|
|
32
|
+
style={{
|
|
33
|
+
width: "100%",
|
|
34
|
+
flexGrow: 1,
|
|
35
|
+
paddingLeft: 6,
|
|
36
|
+
paddingRight: 6,
|
|
37
|
+
paddingTop: 4,
|
|
38
|
+
paddingBottom: 3,
|
|
39
|
+
gap: 1,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<text fg={theme.colors.title}>commit changes</text>
|
|
43
|
+
<text fg={theme.colors.subtleText}>enter to commit | esc to cancel</text>
|
|
44
|
+
<box style={{ width: "100%", height: 3, flexDirection: "column", marginTop: 1 }}>
|
|
45
|
+
<input
|
|
46
|
+
ref={summaryRef}
|
|
47
|
+
value={summary}
|
|
48
|
+
onInput={onSummaryInput}
|
|
49
|
+
placeholder="summary (required)"
|
|
50
|
+
focused={focus === "commit-summary"}
|
|
51
|
+
textColor={theme.colors.inputText}
|
|
52
|
+
focusedTextColor={theme.colors.inputFocusedText}
|
|
53
|
+
/>
|
|
54
|
+
</box>
|
|
55
|
+
<box style={{ width: "100%", flexGrow: 1 }}>
|
|
56
|
+
<textarea
|
|
57
|
+
key={descriptionRenderKey}
|
|
58
|
+
ref={descriptionRef}
|
|
59
|
+
initialValue=""
|
|
60
|
+
placeholder="description (optional)"
|
|
61
|
+
focused={focus === "commit-description"}
|
|
62
|
+
textColor={theme.colors.secondaryInputText}
|
|
63
|
+
focusedTextColor={theme.colors.inputText}
|
|
64
|
+
/>
|
|
65
|
+
</box>
|
|
66
|
+
</box>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core"
|
|
2
|
+
|
|
3
|
+
import type { UiTheme } from "../theme"
|
|
4
|
+
import type { FocusTarget } from "../types"
|
|
5
|
+
|
|
6
|
+
type CommitHistoryDialogProps = {
|
|
7
|
+
open: boolean
|
|
8
|
+
mode: "list" | "action"
|
|
9
|
+
focus: FocusTarget
|
|
10
|
+
currentBranch: string
|
|
11
|
+
commitOptions: SelectOption[]
|
|
12
|
+
commitIndex: number
|
|
13
|
+
onCommitChange: (index: number) => void
|
|
14
|
+
actionOptions: SelectOption[]
|
|
15
|
+
actionIndex: number
|
|
16
|
+
onActionChange: (index: number) => void
|
|
17
|
+
selectedCommitTitle: string
|
|
18
|
+
theme: UiTheme
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CommitHistoryDialog({
|
|
22
|
+
open,
|
|
23
|
+
mode,
|
|
24
|
+
focus,
|
|
25
|
+
currentBranch,
|
|
26
|
+
commitOptions,
|
|
27
|
+
commitIndex,
|
|
28
|
+
onCommitChange,
|
|
29
|
+
actionOptions,
|
|
30
|
+
actionIndex,
|
|
31
|
+
onActionChange,
|
|
32
|
+
selectedCommitTitle,
|
|
33
|
+
theme,
|
|
34
|
+
}: CommitHistoryDialogProps) {
|
|
35
|
+
if (!open) return null
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<box
|
|
39
|
+
style={{
|
|
40
|
+
width: "100%",
|
|
41
|
+
flexGrow: 1,
|
|
42
|
+
paddingLeft: 6,
|
|
43
|
+
paddingRight: 6,
|
|
44
|
+
paddingTop: 8,
|
|
45
|
+
paddingBottom: 4,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<box style={{ width: "100%", maxWidth: 96, flexDirection: "column", gap: 1, flexGrow: 1 }}>
|
|
49
|
+
<text fg={theme.colors.title}>commit history</text>
|
|
50
|
+
<text fg={theme.colors.subtleText}>current branch: {currentBranch}</text>
|
|
51
|
+
|
|
52
|
+
{mode === "list" ? (
|
|
53
|
+
<>
|
|
54
|
+
<text fg={theme.colors.subtleText}>enter to choose action for selected commit | esc to close</text>
|
|
55
|
+
<select
|
|
56
|
+
style={{ width: "100%", height: "100%", textColor: theme.colors.selectText }}
|
|
57
|
+
options={commitOptions}
|
|
58
|
+
selectedIndex={commitIndex}
|
|
59
|
+
showDescription={true}
|
|
60
|
+
focused={focus === "history-commits"}
|
|
61
|
+
selectedBackgroundColor={theme.colors.selectSelectedBackground}
|
|
62
|
+
selectedTextColor={theme.colors.selectSelectedText}
|
|
63
|
+
focusedTextColor={theme.colors.selectFocusedText}
|
|
64
|
+
onChange={onCommitChange}
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
<text fg={theme.colors.text}>{selectedCommitTitle}</text>
|
|
70
|
+
<text fg={theme.colors.subtleText}>choose action | enter confirm | esc back</text>
|
|
71
|
+
<select
|
|
72
|
+
style={{ width: "100%", height: 4, textColor: theme.colors.selectText }}
|
|
73
|
+
options={actionOptions}
|
|
74
|
+
selectedIndex={actionIndex}
|
|
75
|
+
showDescription={true}
|
|
76
|
+
focused={focus === "history-actions"}
|
|
77
|
+
selectedBackgroundColor={theme.colors.selectSelectedBackground}
|
|
78
|
+
selectedTextColor={theme.colors.selectSelectedText}
|
|
79
|
+
focusedTextColor={theme.colors.selectFocusedText}
|
|
80
|
+
onChange={onActionChange}
|
|
81
|
+
/>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
</box>
|
|
85
|
+
</box>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { UiTheme } from "../theme"
|
|
2
|
+
import type { FileRow, FocusTarget } from "../types"
|
|
3
|
+
|
|
4
|
+
type DiffWorkspaceProps = {
|
|
5
|
+
fileRows: FileRow[]
|
|
6
|
+
fileIndex: number
|
|
7
|
+
selectedFilePath: string | null
|
|
8
|
+
focus: FocusTarget
|
|
9
|
+
terminalWidth: number
|
|
10
|
+
terminalHeight: number
|
|
11
|
+
diffText: string
|
|
12
|
+
diffMessage: string | null
|
|
13
|
+
diffFiletype: string | undefined
|
|
14
|
+
diffView: "unified" | "split"
|
|
15
|
+
theme: UiTheme
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DiffWorkspace({
|
|
19
|
+
fileRows,
|
|
20
|
+
fileIndex,
|
|
21
|
+
selectedFilePath,
|
|
22
|
+
focus,
|
|
23
|
+
terminalWidth,
|
|
24
|
+
terminalHeight,
|
|
25
|
+
diffText,
|
|
26
|
+
diffMessage,
|
|
27
|
+
diffFiletype,
|
|
28
|
+
diffView,
|
|
29
|
+
theme,
|
|
30
|
+
}: DiffWorkspaceProps) {
|
|
31
|
+
const visibleRows = Math.max(1, terminalHeight - 6)
|
|
32
|
+
const changesPaneWidth = getChangesPaneWidth(terminalWidth)
|
|
33
|
+
const { start, end } = getVisibleRange(fileRows.length, fileIndex, visibleRows)
|
|
34
|
+
const rows = fileRows.slice(start, end)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<box style={{ flexDirection: "row", flexGrow: 1, gap: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
38
|
+
<box style={{ width: changesPaneWidth, flexDirection: "column" }}>
|
|
39
|
+
<text fg={theme.colors.mutedText}>changes ({fileRows.length})</text>
|
|
40
|
+
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
41
|
+
{rows.map((row, rowIndex) => {
|
|
42
|
+
const absoluteIndex = start + rowIndex
|
|
43
|
+
const selected = absoluteIndex === fileIndex
|
|
44
|
+
return (
|
|
45
|
+
<box key={row.path} style={{ height: 1, flexDirection: "row", ...(selected ? { backgroundColor: theme.colors.selectedRowBackground } : {}) }}>
|
|
46
|
+
<text fg={row.included ? theme.colors.text : theme.colors.subtleText}>{row.included ? "[x]" : "[ ]"}</text>
|
|
47
|
+
<text fg={theme.colors.subtleText}> </text>
|
|
48
|
+
<text fg={row.statusColor}>{row.statusSymbol}</text>
|
|
49
|
+
<text fg={theme.colors.subtleText}> </text>
|
|
50
|
+
{row.directory ? <text fg={theme.colors.subtleText}>{row.directory}</text> : null}
|
|
51
|
+
<text fg={selected || focus === "files" ? theme.colors.inputFocusedText : theme.colors.text}>{row.filename}</text>
|
|
52
|
+
</box>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</box>
|
|
56
|
+
</box>
|
|
57
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
58
|
+
<text fg={theme.colors.mutedText}>{selectedFilePath ?? "no file selected"}</text>
|
|
59
|
+
{diffMessage ? (
|
|
60
|
+
<box style={{ width: "100%", height: "100%", paddingLeft: 1, paddingTop: 1 }}>
|
|
61
|
+
<text fg={theme.colors.subtleText}>{diffMessage}</text>
|
|
62
|
+
</box>
|
|
63
|
+
) : (
|
|
64
|
+
<diff
|
|
65
|
+
key={selectedFilePath ?? "no-selection"}
|
|
66
|
+
diff={diffText}
|
|
67
|
+
view={diffView}
|
|
68
|
+
filetype={diffFiletype}
|
|
69
|
+
syntaxStyle={theme.diffSyntaxStyle}
|
|
70
|
+
showLineNumbers={true}
|
|
71
|
+
wrapMode="none"
|
|
72
|
+
lineNumberFg={theme.colors.diffLineNumber}
|
|
73
|
+
addedBg={theme.colors.diffAddedBackground}
|
|
74
|
+
removedBg={theme.colors.diffRemovedBackground}
|
|
75
|
+
addedContentBg={theme.colors.diffAddedBackground}
|
|
76
|
+
removedContentBg={theme.colors.diffRemovedBackground}
|
|
77
|
+
addedLineNumberBg={theme.colors.diffAddedLineNumberBackground}
|
|
78
|
+
removedLineNumberBg={theme.colors.diffRemovedLineNumberBackground}
|
|
79
|
+
fg={theme.colors.diffForeground}
|
|
80
|
+
style={{ width: "100%", height: "100%" }}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</box>
|
|
84
|
+
</box>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getChangesPaneWidth(terminalWidth: number): number {
|
|
89
|
+
if (!Number.isFinite(terminalWidth) || terminalWidth <= 0) return 42
|
|
90
|
+
const minimumWidth = 24
|
|
91
|
+
const minimumDiffWidth = 48
|
|
92
|
+
const preferredWidth = Math.floor(terminalWidth * 0.28)
|
|
93
|
+
const maxAllowedWidth = Math.max(minimumWidth, terminalWidth - minimumDiffWidth)
|
|
94
|
+
return clamp(preferredWidth, minimumWidth, maxAllowedWidth)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function clamp(value: number, min: number, max: number): number {
|
|
98
|
+
return Math.max(min, Math.min(max, value))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getVisibleRange(total: number, selectedIndex: number, windowSize: number): { start: number; end: number } {
|
|
102
|
+
if (total <= 0) return { start: 0, end: 0 }
|
|
103
|
+
const safeSize = Math.max(windowSize, 1)
|
|
104
|
+
const maxStart = Math.max(total - safeSize, 0)
|
|
105
|
+
const centeredStart = Math.max(selectedIndex - Math.floor(safeSize / 2), 0)
|
|
106
|
+
const start = Math.min(centeredStart, maxStart)
|
|
107
|
+
return { start, end: Math.min(start + safeSize, total) }
|
|
108
|
+
}
|