@caretive/caret-cli 0.0.1
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/.npmrc.tmp +2 -0
- package/README.md +72 -0
- package/cmd/cline/main.go +348 -0
- package/cmd/cline-host/main.go +71 -0
- package/e2e/default_update_test.go +154 -0
- package/e2e/helpers_test.go +378 -0
- package/e2e/main_test.go +47 -0
- package/e2e/mixed_stress_test.go +120 -0
- package/e2e/sqlite_helper.go +161 -0
- package/e2e/start_list_test.go +178 -0
- package/go.mod +64 -0
- package/go.sum +162 -0
- package/man/cline.1 +331 -0
- package/man/cline.1.md +332 -0
- package/package.json +54 -0
- package/pkg/cli/auth/auth_cline_provider.go +285 -0
- package/pkg/cli/auth/auth_menu.go +323 -0
- package/pkg/cli/auth/auth_subscription.go +130 -0
- package/pkg/cli/auth/byo_quick_setup.go +247 -0
- package/pkg/cli/auth/models_cline.go +141 -0
- package/pkg/cli/auth/models_list_fetch.go +156 -0
- package/pkg/cli/auth/models_list_static.go +69 -0
- package/pkg/cli/auth/providers_byo.go +184 -0
- package/pkg/cli/auth/providers_list.go +517 -0
- package/pkg/cli/auth/update_api_configurations.go +647 -0
- package/pkg/cli/auth/wizard_byo.go +764 -0
- package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
- package/pkg/cli/auth/wizard_byo_oca.go +366 -0
- package/pkg/cli/auth.go +43 -0
- package/pkg/cli/clerror/cline_error.go +187 -0
- package/pkg/cli/config/manager.go +208 -0
- package/pkg/cli/config/settings_renderer.go +198 -0
- package/pkg/cli/config.go +152 -0
- package/pkg/cli/display/ansi.go +27 -0
- package/pkg/cli/display/banner.go +211 -0
- package/pkg/cli/display/deduplicator.go +95 -0
- package/pkg/cli/display/markdown_renderer.go +139 -0
- package/pkg/cli/display/renderer.go +304 -0
- package/pkg/cli/display/segment_streamer.go +212 -0
- package/pkg/cli/display/streaming.go +134 -0
- package/pkg/cli/display/system_renderer.go +269 -0
- package/pkg/cli/display/tool_renderer.go +455 -0
- package/pkg/cli/display/tool_result_parser.go +371 -0
- package/pkg/cli/display/typewriter.go +210 -0
- package/pkg/cli/doctor.go +65 -0
- package/pkg/cli/global/cline-clients.go +501 -0
- package/pkg/cli/global/global.go +113 -0
- package/pkg/cli/global/registry.go +304 -0
- package/pkg/cli/handlers/ask_handlers.go +339 -0
- package/pkg/cli/handlers/handler.go +130 -0
- package/pkg/cli/handlers/say_handlers.go +521 -0
- package/pkg/cli/instances.go +506 -0
- package/pkg/cli/logs.go +382 -0
- package/pkg/cli/output/coordinator.go +167 -0
- package/pkg/cli/output/input_model.go +497 -0
- package/pkg/cli/sqlite/locks.go +366 -0
- package/pkg/cli/task/history_handler.go +72 -0
- package/pkg/cli/task/input_handler.go +577 -0
- package/pkg/cli/task/manager.go +1283 -0
- package/pkg/cli/task/settings_parser.go +754 -0
- package/pkg/cli/task/stream_coordinator.go +60 -0
- package/pkg/cli/task.go +675 -0
- package/pkg/cli/terminal/keyboard.go +695 -0
- package/pkg/cli/tui/HELP_WANTED.md +1 -0
- package/pkg/cli/types/history.go +17 -0
- package/pkg/cli/types/messages.go +329 -0
- package/pkg/cli/types/state.go +59 -0
- package/pkg/cli/updater/updater.go +409 -0
- package/pkg/cli/version.go +43 -0
- package/pkg/common/constants.go +6 -0
- package/pkg/common/schema.go +54 -0
- package/pkg/common/types.go +54 -0
- package/pkg/common/utils.go +185 -0
- package/pkg/generated/field_overrides.go +39 -0
- package/pkg/generated/providers.go +1584 -0
- package/pkg/hostbridge/diff.go +351 -0
- package/pkg/hostbridge/disabled/watch.go +39 -0
- package/pkg/hostbridge/disabled/window.go +63 -0
- package/pkg/hostbridge/disabled/workspace.go +66 -0
- package/pkg/hostbridge/env.go +166 -0
- package/pkg/hostbridge/grpc_server.go +113 -0
- package/pkg/hostbridge/simple.go +43 -0
- package/pkg/hostbridge/simple_workspace.go +85 -0
- package/pkg/hostbridge/window.go +129 -0
- package/scripts/publish-caret-cli.sh +39 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
package terminal
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"runtime"
|
|
9
|
+
"strings"
|
|
10
|
+
"sync"
|
|
11
|
+
|
|
12
|
+
"github.com/cline/cli/pkg/cli/display"
|
|
13
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// KeyboardProtocol manages enhanced keyboard protocol support for detecting
|
|
17
|
+
// modified keys like shift+enter across all major terminals.
|
|
18
|
+
type KeyboardProtocol struct {
|
|
19
|
+
enabled bool
|
|
20
|
+
mu sync.Mutex
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var globalProtocol = &KeyboardProtocol{}
|
|
24
|
+
|
|
25
|
+
// EnableEnhancedKeyboard enables enhanced keyboard protocols to support
|
|
26
|
+
// shift+enter and other modified keys across all major terminals:
|
|
27
|
+
// - VS Code integrated terminal
|
|
28
|
+
// - iTerm2
|
|
29
|
+
// - Terminal.app
|
|
30
|
+
// - Ghostty
|
|
31
|
+
// - Kitty
|
|
32
|
+
// - WezTerm
|
|
33
|
+
// - Alacritty
|
|
34
|
+
// - foot
|
|
35
|
+
// - xterm
|
|
36
|
+
//
|
|
37
|
+
// This function is safe to call multiple times and handles cleanup automatically.
|
|
38
|
+
// It enables both modifyOtherKeys (xterm protocol) and Kitty keyboard protocol
|
|
39
|
+
// for maximum compatibility.
|
|
40
|
+
func EnableEnhancedKeyboard() {
|
|
41
|
+
globalProtocol.mu.Lock()
|
|
42
|
+
defer globalProtocol.mu.Unlock()
|
|
43
|
+
|
|
44
|
+
if globalProtocol.enabled {
|
|
45
|
+
return // Already enabled
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if we're in a TTY (not piped/redirected)
|
|
49
|
+
if !isatty(os.Stdin.Fd()) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Enable modifyOtherKeys mode 2
|
|
54
|
+
// This tells xterm-compatible terminals (VS Code, iTerm2, Terminal.app, etc.)
|
|
55
|
+
// to send escape sequences for modified keys including shift+enter
|
|
56
|
+
// Format: CSI > 4 ; 2 m
|
|
57
|
+
// - Mode 2 enables for ALL keys including well-known ones
|
|
58
|
+
fmt.Print("\x1b[>4;2m")
|
|
59
|
+
|
|
60
|
+
// Also enable Kitty keyboard protocol for terminals that support it
|
|
61
|
+
// This is a more modern protocol supported by Kitty, Ghostty, WezTerm, foot, etc.
|
|
62
|
+
// Format: CSI = <flags> u where flags=1 means "disambiguate escape codes"
|
|
63
|
+
// This makes shift+enter distinguishable from plain enter
|
|
64
|
+
fmt.Print("\x1b[=1u")
|
|
65
|
+
|
|
66
|
+
globalProtocol.enabled = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// DisableEnhancedKeyboard restores the terminal to its default keyboard mode.
|
|
70
|
+
// This should be called on program exit to be a good citizen.
|
|
71
|
+
func DisableEnhancedKeyboard() {
|
|
72
|
+
globalProtocol.mu.Lock()
|
|
73
|
+
defer globalProtocol.mu.Unlock()
|
|
74
|
+
|
|
75
|
+
if !globalProtocol.enabled {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Disable modifyOtherKeys (restore to mode 0)
|
|
80
|
+
fmt.Print("\x1b[>4;0m")
|
|
81
|
+
|
|
82
|
+
// Disable Kitty keyboard protocol
|
|
83
|
+
fmt.Print("\x1b[<u")
|
|
84
|
+
|
|
85
|
+
globalProtocol.enabled = false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// isatty checks if a file descriptor is a terminal
|
|
89
|
+
func isatty(fd uintptr) bool {
|
|
90
|
+
// Use the standard library's terminal package
|
|
91
|
+
// This works across all platforms (Unix, Windows, etc.)
|
|
92
|
+
fileInfo, err := os.Stdin.Stat()
|
|
93
|
+
if err != nil {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
return (fileInfo.Mode() & os.ModeCharDevice) != 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// SetupKeyboard detects the current terminal and configures keybindings if needed.
|
|
100
|
+
// Runs in background and doesn't block. Prints status when configs are modified.
|
|
101
|
+
func SetupKeyboard() {
|
|
102
|
+
go func() {
|
|
103
|
+
renderer := display.NewRenderer(global.Config.OutputFormat)
|
|
104
|
+
setupKeyboardInternal(renderer)
|
|
105
|
+
}()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SetupKeyboardSync is the synchronous version used by doctor command.
|
|
109
|
+
// Blocks until complete and prints status for all terminals.
|
|
110
|
+
func SetupKeyboardSync() {
|
|
111
|
+
renderer := display.NewRenderer(global.Config.OutputFormat)
|
|
112
|
+
setupKeyboardInternal(renderer)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func setupKeyboardInternal(renderer *display.Renderer) {
|
|
116
|
+
terminalName := DetectTerminal()
|
|
117
|
+
|
|
118
|
+
switch terminalName {
|
|
119
|
+
case "vscode":
|
|
120
|
+
// VS Code and Cursor use the same TERM_PROGRAM value
|
|
121
|
+
modified, path := SetupVSCodeKeybindings()
|
|
122
|
+
if modified {
|
|
123
|
+
fmt.Printf("%s VS Code %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
124
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
125
|
+
} else if path != "" {
|
|
126
|
+
fmt.Printf("%s\n", renderer.Dim("✓ VS Code shift+enter already configured"))
|
|
127
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
modified, path = SetupCursorKeybindings()
|
|
131
|
+
if modified {
|
|
132
|
+
fmt.Printf("%s Cursor %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
133
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
134
|
+
} else if path != "" {
|
|
135
|
+
fmt.Printf("%s\n", renderer.Dim("✓ Cursor shift+enter already configured"))
|
|
136
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case "ghostty":
|
|
140
|
+
modified, path := SetupGhosttyKeybindings()
|
|
141
|
+
if modified {
|
|
142
|
+
fmt.Printf("%s Ghostty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
143
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
144
|
+
fmt.Printf("%s\n", renderer.Dim(" Fully restart Ghostty (quit all windows) for changes to take effect"))
|
|
145
|
+
} else if path != "" {
|
|
146
|
+
fmt.Printf("%s\n", renderer.Dim("✓ Ghostty shift+enter already configured"))
|
|
147
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "wezterm":
|
|
151
|
+
modified, path := SetupWezTermKeybindings()
|
|
152
|
+
if modified {
|
|
153
|
+
fmt.Printf("%s WezTerm %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
154
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
155
|
+
} else if path != "" {
|
|
156
|
+
fmt.Printf("%s\n", renderer.Dim("✓ WezTerm shift+enter already configured"))
|
|
157
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case "alacritty":
|
|
161
|
+
modified, path := SetupAlacrittyKeybindings()
|
|
162
|
+
if modified {
|
|
163
|
+
fmt.Printf("%s Alacritty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
164
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
165
|
+
} else if path != "" {
|
|
166
|
+
fmt.Printf("%s\n", renderer.Dim("✓ Alacritty shift+enter already configured"))
|
|
167
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "kitty":
|
|
171
|
+
modified, path := SetupKittyKeybindings()
|
|
172
|
+
if modified {
|
|
173
|
+
fmt.Printf("%s Kitty %s\n", renderer.Dim("Configured shift+enter for"), renderer.Dim("terminal"))
|
|
174
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
175
|
+
} else if path != "" {
|
|
176
|
+
fmt.Printf("%s\n", renderer.Dim("✓ Kitty shift+enter already configured"))
|
|
177
|
+
fmt.Printf("%s %s\n", renderer.Dim(" →"), path)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "iterm2":
|
|
181
|
+
fmt.Printf("%s\n", renderer.Dim("✓ iTerm2 shift+enter works by default (maps to alt+enter)"))
|
|
182
|
+
|
|
183
|
+
case "terminal.app":
|
|
184
|
+
fmt.Printf("%s\n", renderer.Dim("⚠ Terminal.app requires manual configuration"))
|
|
185
|
+
fmt.Printf("%s\n", renderer.Dim(" See: Terminal → Preferences → Profiles → Keyboard"))
|
|
186
|
+
|
|
187
|
+
case "unknown":
|
|
188
|
+
fmt.Printf("%s\n", renderer.Dim("ℹ Terminal not detected - use alt+enter or ctrl+j for newlines"))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// getVSCodeConfigPath returns the platform-specific path to VS Code's User directory
|
|
193
|
+
func getVSCodeConfigPath() (string, error) {
|
|
194
|
+
home, err := os.UserHomeDir()
|
|
195
|
+
if err != nil {
|
|
196
|
+
return "", err
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
switch runtime.GOOS {
|
|
200
|
+
case "darwin":
|
|
201
|
+
return filepath.Join(home, "Library", "Application Support", "Code", "User"), nil
|
|
202
|
+
case "windows":
|
|
203
|
+
appData := os.Getenv("APPDATA")
|
|
204
|
+
if appData == "" {
|
|
205
|
+
appData = filepath.Join(home, "AppData", "Roaming")
|
|
206
|
+
}
|
|
207
|
+
return filepath.Join(appData, "Code", "User"), nil
|
|
208
|
+
default: // linux, freebsd, etc.
|
|
209
|
+
return filepath.Join(home, ".config", "Code", "User"), nil
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// getCursorConfigPath returns the platform-specific path to Cursor's User directory
|
|
214
|
+
func getCursorConfigPath() (string, error) {
|
|
215
|
+
home, err := os.UserHomeDir()
|
|
216
|
+
if err != nil {
|
|
217
|
+
return "", err
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
switch runtime.GOOS {
|
|
221
|
+
case "darwin":
|
|
222
|
+
return filepath.Join(home, "Library", "Application Support", "Cursor", "User"), nil
|
|
223
|
+
case "windows":
|
|
224
|
+
appData := os.Getenv("APPDATA")
|
|
225
|
+
if appData == "" {
|
|
226
|
+
appData = filepath.Join(home, "AppData", "Roaming")
|
|
227
|
+
}
|
|
228
|
+
return filepath.Join(appData, "Cursor", "User"), nil
|
|
229
|
+
default: // linux, freebsd, etc.
|
|
230
|
+
return filepath.Join(home, ".config", "Cursor", "User"), nil
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// DetectTerminal identifies which terminal emulator is currently running
|
|
235
|
+
func DetectTerminal() string {
|
|
236
|
+
// Check TERM_PROGRAM (works for most terminals)
|
|
237
|
+
termProgram := os.Getenv("TERM_PROGRAM")
|
|
238
|
+
switch termProgram {
|
|
239
|
+
case "vscode":
|
|
240
|
+
return "vscode" // Also covers Cursor (uses same value)
|
|
241
|
+
case "WezTerm":
|
|
242
|
+
return "wezterm"
|
|
243
|
+
case "ghostty":
|
|
244
|
+
return "ghostty"
|
|
245
|
+
case "iTerm.app":
|
|
246
|
+
return "iterm2"
|
|
247
|
+
case "Apple_Terminal":
|
|
248
|
+
return "terminal.app"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Kitty doesn't set TERM_PROGRAM, check KITTY_WINDOW_ID
|
|
252
|
+
if os.Getenv("KITTY_WINDOW_ID") != "" {
|
|
253
|
+
return "kitty"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Alacritty doesn't set TERM_PROGRAM, check ALACRITTY_SOCKET
|
|
257
|
+
if os.Getenv("ALACRITTY_SOCKET") != "" {
|
|
258
|
+
return "alacritty"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Ghostty fallback (cross-platform - more reliable than TERM_PROGRAM)
|
|
262
|
+
if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
|
|
263
|
+
return "ghostty"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Alacritty fallback
|
|
267
|
+
if os.Getenv("ALACRITTY_LOG") != "" {
|
|
268
|
+
return "alacritty"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check TERM variable as last resort
|
|
272
|
+
term := os.Getenv("TERM")
|
|
273
|
+
if strings.Contains(term, "kitty") {
|
|
274
|
+
return "kitty"
|
|
275
|
+
}
|
|
276
|
+
if term == "alacritty" {
|
|
277
|
+
return "alacritty"
|
|
278
|
+
}
|
|
279
|
+
if term == "xterm-ghostty" {
|
|
280
|
+
return "ghostty"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return "unknown"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// VSCodeKeybinding represents a VS Code keyboard shortcut
|
|
287
|
+
type VSCodeKeybinding struct {
|
|
288
|
+
Key string `json:"key"`
|
|
289
|
+
Command string `json:"command"`
|
|
290
|
+
Args map[string]interface{} `json:"args,omitempty"`
|
|
291
|
+
When string `json:"when,omitempty"`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// SetupVSCodeKeybindings adds shift+enter support to VS Code's integrated terminal
|
|
295
|
+
// by modifying the user's keybindings.json file.
|
|
296
|
+
// Returns (wasModified, configPath) to allow caller to log the change.
|
|
297
|
+
func SetupVSCodeKeybindings() (bool, string) {
|
|
298
|
+
// Get platform-specific VS Code config path
|
|
299
|
+
configDir, err := getVSCodeConfigPath()
|
|
300
|
+
if err != nil {
|
|
301
|
+
return false, ""
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
keybindingsPath := filepath.Join(configDir, "keybindings.json")
|
|
305
|
+
|
|
306
|
+
// Check if VS Code is installed (keybindings file or parent dir exists)
|
|
307
|
+
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
|
|
308
|
+
// VS Code not installed, skip silently
|
|
309
|
+
return false, ""
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Read existing keybindings
|
|
313
|
+
var keybindings []VSCodeKeybinding
|
|
314
|
+
|
|
315
|
+
data, err := os.ReadFile(keybindingsPath)
|
|
316
|
+
if err != nil {
|
|
317
|
+
if !os.IsNotExist(err) {
|
|
318
|
+
return false, ""
|
|
319
|
+
}
|
|
320
|
+
// File doesn't exist, start with empty array
|
|
321
|
+
keybindings = []VSCodeKeybinding{}
|
|
322
|
+
} else {
|
|
323
|
+
// Parse existing keybindings
|
|
324
|
+
if err := json.Unmarshal(data, &keybindings); err != nil {
|
|
325
|
+
// If parse fails, don't modify the file
|
|
326
|
+
return false, ""
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if shift+enter binding already exists
|
|
331
|
+
for _, kb := range keybindings {
|
|
332
|
+
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
|
|
333
|
+
// Already configured
|
|
334
|
+
return false, keybindingsPath
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add shift+enter keybinding
|
|
339
|
+
newBinding := VSCodeKeybinding{
|
|
340
|
+
Key: "shift+enter",
|
|
341
|
+
Command: "workbench.action.terminal.sendSequence",
|
|
342
|
+
Args: map[string]interface{}{
|
|
343
|
+
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
|
|
344
|
+
},
|
|
345
|
+
When: "terminalFocus",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
keybindings = append(keybindings, newBinding)
|
|
349
|
+
|
|
350
|
+
// Create backup
|
|
351
|
+
if data != nil {
|
|
352
|
+
backupPath := keybindingsPath + ".backup"
|
|
353
|
+
_ = os.WriteFile(backupPath, data, 0644)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Write updated keybindings
|
|
357
|
+
updatedData, err := json.MarshalIndent(keybindings, "", " ")
|
|
358
|
+
if err != nil {
|
|
359
|
+
return false, ""
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Ensure parent directory exists
|
|
363
|
+
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
|
|
364
|
+
return false, ""
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
|
|
368
|
+
return false, ""
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return true, keybindingsPath
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// SetupCursorKeybindings adds shift+enter support to Cursor's integrated terminal
|
|
375
|
+
// by modifying the user's keybindings.json file.
|
|
376
|
+
// Cursor is a fork of VS Code, so it uses the same keybinding format.
|
|
377
|
+
// Returns (wasModified, configPath) to allow caller to log the change.
|
|
378
|
+
func SetupCursorKeybindings() (bool, string) {
|
|
379
|
+
// Get platform-specific Cursor config path
|
|
380
|
+
configDir, err := getCursorConfigPath()
|
|
381
|
+
if err != nil {
|
|
382
|
+
return false, ""
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
keybindingsPath := filepath.Join(configDir, "keybindings.json")
|
|
386
|
+
|
|
387
|
+
// Check if Cursor is installed (keybindings file or parent dir exists)
|
|
388
|
+
if _, err := os.Stat(filepath.Dir(keybindingsPath)); os.IsNotExist(err) {
|
|
389
|
+
// Cursor not installed, skip silently
|
|
390
|
+
return false, ""
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Read existing keybindings
|
|
394
|
+
var keybindings []VSCodeKeybinding
|
|
395
|
+
|
|
396
|
+
data, err := os.ReadFile(keybindingsPath)
|
|
397
|
+
if err != nil {
|
|
398
|
+
if !os.IsNotExist(err) {
|
|
399
|
+
return false, ""
|
|
400
|
+
}
|
|
401
|
+
// File doesn't exist, start with empty array
|
|
402
|
+
keybindings = []VSCodeKeybinding{}
|
|
403
|
+
} else {
|
|
404
|
+
// Parse existing keybindings
|
|
405
|
+
if err := json.Unmarshal(data, &keybindings); err != nil {
|
|
406
|
+
// If parse fails, don't modify the file
|
|
407
|
+
return false, ""
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check if shift+enter binding already exists
|
|
412
|
+
for _, kb := range keybindings {
|
|
413
|
+
if kb.Key == "shift+enter" && kb.Command == "workbench.action.terminal.sendSequence" {
|
|
414
|
+
// Already configured
|
|
415
|
+
return false, keybindingsPath
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Add shift+enter keybinding
|
|
420
|
+
newBinding := VSCodeKeybinding{
|
|
421
|
+
Key: "shift+enter",
|
|
422
|
+
Command: "workbench.action.terminal.sendSequence",
|
|
423
|
+
Args: map[string]interface{}{
|
|
424
|
+
"text": "\u001b\n", // ESC + newline (alt+enter sequence)
|
|
425
|
+
},
|
|
426
|
+
When: "terminalFocus",
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
keybindings = append(keybindings, newBinding)
|
|
430
|
+
|
|
431
|
+
// Create backup
|
|
432
|
+
if data != nil {
|
|
433
|
+
backupPath := keybindingsPath + ".backup"
|
|
434
|
+
_ = os.WriteFile(backupPath, data, 0644)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Write updated keybindings
|
|
438
|
+
updatedData, err := json.MarshalIndent(keybindings, "", " ")
|
|
439
|
+
if err != nil {
|
|
440
|
+
return false, ""
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Ensure parent directory exists
|
|
444
|
+
if err := os.MkdirAll(filepath.Dir(keybindingsPath), 0755); err != nil {
|
|
445
|
+
return false, ""
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if err := os.WriteFile(keybindingsPath, updatedData, 0644); err != nil {
|
|
449
|
+
return false, ""
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return true, keybindingsPath
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// SetupGhosttyKeybindings adds shift+enter support to Ghostty terminal
|
|
456
|
+
// by appending to the user's config file.
|
|
457
|
+
// Returns (wasModified, configPath) to allow caller to log the change.
|
|
458
|
+
func SetupGhosttyKeybindings() (bool, string) {
|
|
459
|
+
home, err := os.UserHomeDir()
|
|
460
|
+
if err != nil {
|
|
461
|
+
return false, ""
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Ghostty config location: ~/.config/ghostty/config
|
|
465
|
+
configPath := filepath.Join(home, ".config", "ghostty", "config")
|
|
466
|
+
|
|
467
|
+
// Check if config directory exists
|
|
468
|
+
configDir := filepath.Dir(configPath)
|
|
469
|
+
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
|
470
|
+
// Ghostty not installed, skip silently
|
|
471
|
+
return false, ""
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Read existing config if it exists
|
|
475
|
+
var existingContent []byte
|
|
476
|
+
if data, err := os.ReadFile(configPath); err == nil {
|
|
477
|
+
existingContent = data
|
|
478
|
+
// Check if shift+enter already configured
|
|
479
|
+
if strings.Contains(string(data), "keybind = shift+enter") {
|
|
480
|
+
return false, configPath
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Keybinding to add - send newline character (0x0a)
|
|
485
|
+
// Ghostty requires \x0a hex escape syntax, verified working
|
|
486
|
+
keybinding := "keybind = shift+enter=text:\\x0a\n"
|
|
487
|
+
|
|
488
|
+
// Append to config
|
|
489
|
+
newContent := append(existingContent, []byte(keybinding)...)
|
|
490
|
+
|
|
491
|
+
// Ensure directory exists
|
|
492
|
+
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
493
|
+
return false, ""
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Create backup if file exists
|
|
497
|
+
if existingContent != nil {
|
|
498
|
+
backupPath := configPath + ".backup"
|
|
499
|
+
_ = os.WriteFile(backupPath, existingContent, 0644)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Write updated config
|
|
503
|
+
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
|
504
|
+
return false, ""
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return true, configPath
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// SetupWezTermKeybindings adds shift+enter support to WezTerm
|
|
511
|
+
// by appending to the user's .wezterm.lua file.
|
|
512
|
+
// Returns (wasModified, configPath)
|
|
513
|
+
func SetupWezTermKeybindings() (bool, string) {
|
|
514
|
+
home, err := os.UserHomeDir()
|
|
515
|
+
if err != nil {
|
|
516
|
+
return false, ""
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
configPath := filepath.Join(home, ".wezterm.lua")
|
|
520
|
+
|
|
521
|
+
// Check if WezTerm config exists
|
|
522
|
+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
523
|
+
// WezTerm not configured, skip silently
|
|
524
|
+
return false, ""
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Read existing config
|
|
528
|
+
data, err := os.ReadFile(configPath)
|
|
529
|
+
if err != nil {
|
|
530
|
+
return false, ""
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Check if shift+enter already configured
|
|
534
|
+
if strings.Contains(string(data), "key = 'Enter'") && strings.Contains(string(data), "mods = 'SHIFT'") {
|
|
535
|
+
return false, configPath
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Create backup
|
|
539
|
+
backupPath := configPath + ".backup"
|
|
540
|
+
_ = os.WriteFile(backupPath, data, 0644)
|
|
541
|
+
|
|
542
|
+
// Keybinding to add (insert before final return statement)
|
|
543
|
+
keybinding := `
|
|
544
|
+
-- Shift+Enter for newlines (added by Cline CLI)
|
|
545
|
+
config.keys = config.keys or {}
|
|
546
|
+
table.insert(config.keys, {
|
|
547
|
+
key = 'Enter',
|
|
548
|
+
mods = 'SHIFT',
|
|
549
|
+
action = wezterm.action.SendString '\x1b\n',
|
|
550
|
+
})
|
|
551
|
+
`
|
|
552
|
+
|
|
553
|
+
content := string(data)
|
|
554
|
+
// Try to insert before the final return statement
|
|
555
|
+
if strings.Contains(content, "return config") {
|
|
556
|
+
content = strings.Replace(content, "return config", keybinding+"\nreturn config", 1)
|
|
557
|
+
} else {
|
|
558
|
+
// No return statement, append at end
|
|
559
|
+
content += keybinding
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Write updated config
|
|
563
|
+
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
|
564
|
+
return false, ""
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return true, configPath
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// SetupAlacrittyKeybindings adds shift+enter support to Alacritty
|
|
571
|
+
// by appending to the user's alacritty.yml file.
|
|
572
|
+
// Returns (wasModified, configPath)
|
|
573
|
+
func SetupAlacrittyKeybindings() (bool, string) {
|
|
574
|
+
home, err := os.UserHomeDir()
|
|
575
|
+
if err != nil {
|
|
576
|
+
return false, ""
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Try both possible locations
|
|
580
|
+
configPaths := []string{
|
|
581
|
+
filepath.Join(home, ".config", "alacritty", "alacritty.yml"),
|
|
582
|
+
filepath.Join(home, ".config", "alacritty", "alacritty.toml"),
|
|
583
|
+
filepath.Join(home, ".alacritty.yml"),
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
var configPath string
|
|
587
|
+
for _, path := range configPaths {
|
|
588
|
+
if _, err := os.Stat(path); err == nil {
|
|
589
|
+
configPath = path
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if configPath == "" {
|
|
595
|
+
// Alacritty not configured, skip silently
|
|
596
|
+
return false, ""
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Read existing config
|
|
600
|
+
data, err := os.ReadFile(configPath)
|
|
601
|
+
if err != nil {
|
|
602
|
+
return false, ""
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Check if shift+enter already configured
|
|
606
|
+
if strings.Contains(string(data), "key: Return") && strings.Contains(string(data), "mods: Shift") {
|
|
607
|
+
return false, configPath
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Create backup
|
|
611
|
+
backupPath := configPath + ".backup"
|
|
612
|
+
_ = os.WriteFile(backupPath, data, 0644)
|
|
613
|
+
|
|
614
|
+
// Keybinding to add
|
|
615
|
+
var keybinding string
|
|
616
|
+
if strings.HasSuffix(configPath, ".yml") || strings.HasSuffix(configPath, ".yaml") {
|
|
617
|
+
keybinding = `
|
|
618
|
+
# Shift+Enter for newlines (added by Cline CLI)
|
|
619
|
+
key_bindings:
|
|
620
|
+
- { key: Return, mods: Shift, chars: "\x1b\n" }
|
|
621
|
+
`
|
|
622
|
+
} else {
|
|
623
|
+
// TOML format
|
|
624
|
+
keybinding = `
|
|
625
|
+
# Shift+Enter for newlines (added by Cline CLI)
|
|
626
|
+
[[keyboard.bindings]]
|
|
627
|
+
key = "Return"
|
|
628
|
+
mods = "Shift"
|
|
629
|
+
chars = "\x1b\n"
|
|
630
|
+
`
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Append to config
|
|
634
|
+
newContent := append(data, []byte(keybinding)...)
|
|
635
|
+
|
|
636
|
+
// Write updated config
|
|
637
|
+
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
|
638
|
+
return false, ""
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return true, configPath
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// SetupKittyKeybindings adds shift+enter support to Kitty terminal
|
|
645
|
+
// by appending to the user's kitty.conf file.
|
|
646
|
+
// Returns (wasModified, configPath)
|
|
647
|
+
func SetupKittyKeybindings() (bool, string) {
|
|
648
|
+
home, err := os.UserHomeDir()
|
|
649
|
+
if err != nil {
|
|
650
|
+
return false, ""
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
configPath := filepath.Join(home, ".config", "kitty", "kitty.conf")
|
|
654
|
+
|
|
655
|
+
// Check if config directory exists
|
|
656
|
+
configDir := filepath.Dir(configPath)
|
|
657
|
+
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
|
658
|
+
// Kitty not installed, skip silently
|
|
659
|
+
return false, ""
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Read existing config if it exists
|
|
663
|
+
var existingContent []byte
|
|
664
|
+
if data, err := os.ReadFile(configPath); err == nil {
|
|
665
|
+
existingContent = data
|
|
666
|
+
// Check if shift+enter already configured
|
|
667
|
+
if strings.Contains(string(data), "map shift+enter") {
|
|
668
|
+
return false, configPath
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Keybinding to add
|
|
673
|
+
keybinding := "# Shift+Enter for newlines (added by Cline CLI)\nmap shift+enter send_text all \\x1b\\n\n"
|
|
674
|
+
|
|
675
|
+
// Append to config
|
|
676
|
+
newContent := append(existingContent, []byte(keybinding)...)
|
|
677
|
+
|
|
678
|
+
// Ensure directory exists
|
|
679
|
+
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
680
|
+
return false, ""
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Create backup if file exists
|
|
684
|
+
if existingContent != nil {
|
|
685
|
+
backupPath := configPath + ".backup"
|
|
686
|
+
_ = os.WriteFile(backupPath, existingContent, 0644)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Write updated config
|
|
690
|
+
if err := os.WriteFile(configPath, newContent, 0644); err != nil {
|
|
691
|
+
return false, ""
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return true, configPath
|
|
695
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
if you can make a beautiful tui in go, please help!
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package types
|
|
2
|
+
|
|
3
|
+
// HistoryItem represents a task history item from taskHistory.json
|
|
4
|
+
// This struct matches the JSON format stored on disk
|
|
5
|
+
type HistoryItem struct {
|
|
6
|
+
Id string `json:"id"`
|
|
7
|
+
Ulid string `json:"ulid,omitempty"`
|
|
8
|
+
Ts int64 `json:"ts"`
|
|
9
|
+
Task string `json:"task"`
|
|
10
|
+
TokensIn int32 `json:"tokensIn"`
|
|
11
|
+
TokensOut int32 `json:"tokensOut"`
|
|
12
|
+
CacheWrites int32 `json:"cacheWrites,omitempty"`
|
|
13
|
+
CacheReads int32 `json:"cacheReads,omitempty"`
|
|
14
|
+
TotalCost float64 `json:"totalCost"`
|
|
15
|
+
Size int64 `json:"size,omitempty"`
|
|
16
|
+
IsFavorited bool `json:"isFavorited,omitempty"`
|
|
17
|
+
}
|