@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
package/pkg/cli/logs.go
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"sort"
|
|
8
|
+
"strings"
|
|
9
|
+
"text/tabwriter"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/cline/cli/pkg/cli/display"
|
|
13
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
14
|
+
"github.com/spf13/cobra"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type logFileInfo struct {
|
|
18
|
+
name string
|
|
19
|
+
path string
|
|
20
|
+
size int64
|
|
21
|
+
created time.Time
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func NewLogsCommand() *cobra.Command {
|
|
25
|
+
cmd := &cobra.Command{
|
|
26
|
+
Use: "logs",
|
|
27
|
+
Aliases: []string{"log", "l"},
|
|
28
|
+
Short: "Manage Cline log files",
|
|
29
|
+
Long: `List and manage log files created by Cline instances.`,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cmd.AddCommand(newLogsListCommand())
|
|
33
|
+
cmd.AddCommand(newLogsCleanCommand())
|
|
34
|
+
cmd.AddCommand(newLogsPathCommand())
|
|
35
|
+
|
|
36
|
+
return cmd
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func newLogsListCommand() *cobra.Command {
|
|
40
|
+
cmd := &cobra.Command{
|
|
41
|
+
Use: "list",
|
|
42
|
+
Aliases: []string{"l", "ls"},
|
|
43
|
+
Short: "List all log files",
|
|
44
|
+
Long: `List all log files in the Cline logs directory with their sizes and ages.`,
|
|
45
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
46
|
+
if global.Config == nil {
|
|
47
|
+
return fmt.Errorf("config not initialized")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
|
|
51
|
+
logs, err := listLogFiles(logsDir)
|
|
52
|
+
if err != nil {
|
|
53
|
+
return fmt.Errorf("failed to list log files: %w", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if len(logs) == 0 {
|
|
57
|
+
fmt.Println("No log files found.")
|
|
58
|
+
fmt.Printf("Log files will be created in: %s\n", logsDir)
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return renderLogsTable(logs, false)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return cmd
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func newLogsCleanCommand() *cobra.Command {
|
|
70
|
+
var olderThan int
|
|
71
|
+
var all bool
|
|
72
|
+
var dryRun bool
|
|
73
|
+
|
|
74
|
+
cmd := &cobra.Command{
|
|
75
|
+
Use: "clean",
|
|
76
|
+
Aliases: []string{"c"},
|
|
77
|
+
Short: "Delete old log files",
|
|
78
|
+
Long: `Delete log files older than a specified number of days.`,
|
|
79
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
80
|
+
if global.Config == nil {
|
|
81
|
+
return fmt.Errorf("config not initialized")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
|
|
85
|
+
logs, err := listLogFiles(logsDir)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return fmt.Errorf("failed to list log files: %w", err)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var toDelete []logFileInfo
|
|
91
|
+
if all {
|
|
92
|
+
toDelete = logs
|
|
93
|
+
} else {
|
|
94
|
+
toDelete = filterOldLogs(logs, olderThan)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if len(toDelete) == 0 {
|
|
98
|
+
if all {
|
|
99
|
+
fmt.Println("No log files to delete.")
|
|
100
|
+
} else {
|
|
101
|
+
fmt.Printf("No log files older than %d days found.\n", olderThan)
|
|
102
|
+
}
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Calculate total size
|
|
107
|
+
var totalSize int64
|
|
108
|
+
for _, log := range toDelete {
|
|
109
|
+
totalSize += log.size
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if dryRun {
|
|
113
|
+
fmt.Println("The following log files will be deleted:\n")
|
|
114
|
+
if err := renderLogsTable(toDelete, true); err != nil {
|
|
115
|
+
return err
|
|
116
|
+
}
|
|
117
|
+
fileWord := "files"
|
|
118
|
+
if len(toDelete) == 1 {
|
|
119
|
+
fileWord = "file"
|
|
120
|
+
}
|
|
121
|
+
fmt.Printf("\nSummary: %d %s will be deleted (%s freed)\n", len(toDelete), fileWord, formatFileSize(totalSize))
|
|
122
|
+
fmt.Println("\nRun without --dry-run to actually delete these files.")
|
|
123
|
+
return nil
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Actually delete the files
|
|
127
|
+
count, bytesFreed, err := deleteLogFiles(toDelete)
|
|
128
|
+
if err != nil {
|
|
129
|
+
return fmt.Errorf("failed to delete log files: %w", err)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fileWord := "files"
|
|
133
|
+
if count == 1 {
|
|
134
|
+
fileWord = "file"
|
|
135
|
+
}
|
|
136
|
+
fmt.Printf("Deleted %d log %s (%s freed)\n", count, fileWord, formatFileSize(bytesFreed))
|
|
137
|
+
return nil
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
cmd.Flags().IntVar(&olderThan, "older-than", 7, "delete logs older than N days")
|
|
142
|
+
cmd.Flags().BoolVar(&all, "all", false, "delete all log files")
|
|
143
|
+
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be deleted without deleting")
|
|
144
|
+
|
|
145
|
+
return cmd
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func newLogsPathCommand() *cobra.Command {
|
|
149
|
+
cmd := &cobra.Command{
|
|
150
|
+
Use: "path",
|
|
151
|
+
Short: "Print the logs directory path",
|
|
152
|
+
Long: `Print the absolute path to the Cline logs directory.`,
|
|
153
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
154
|
+
if global.Config == nil {
|
|
155
|
+
return fmt.Errorf("config not initialized")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logsDir := filepath.Join(global.Config.ConfigPath, "logs")
|
|
159
|
+
fmt.Println(logsDir)
|
|
160
|
+
return nil
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return cmd
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Helper functions
|
|
168
|
+
|
|
169
|
+
func listLogFiles(logsDir string) ([]logFileInfo, error) {
|
|
170
|
+
// Check if logs directory exists
|
|
171
|
+
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
|
|
172
|
+
return []logFileInfo{}, nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
entries, err := os.ReadDir(logsDir)
|
|
176
|
+
if err != nil {
|
|
177
|
+
return nil, err
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var logs []logFileInfo
|
|
181
|
+
for _, entry := range entries {
|
|
182
|
+
if entry.IsDir() {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Only process .log files
|
|
187
|
+
if !strings.HasSuffix(entry.Name(), ".log") {
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Parse timestamp from filename
|
|
192
|
+
created, err := parseTimestampFromFilename(entry.Name())
|
|
193
|
+
if err != nil {
|
|
194
|
+
// Skip files we can't parse
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
info, err := entry.Info()
|
|
199
|
+
if err != nil {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logs = append(logs, logFileInfo{
|
|
204
|
+
name: entry.Name(),
|
|
205
|
+
path: filepath.Join(logsDir, entry.Name()),
|
|
206
|
+
size: info.Size(),
|
|
207
|
+
created: created,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Sort by created time (oldest first)
|
|
212
|
+
sort.Slice(logs, func(i, j int) bool {
|
|
213
|
+
return logs[i].created.Before(logs[j].created)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return logs, nil
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func parseTimestampFromFilename(filename string) (time.Time, error) {
|
|
220
|
+
// Expected format: cline-core-2025-10-12-21-30-45-localhost-51051.log
|
|
221
|
+
// or: cline-host-2025-10-12-21-30-45-localhost-52051.log
|
|
222
|
+
|
|
223
|
+
parts := strings.Split(filename, "-")
|
|
224
|
+
if len(parts) < 8 {
|
|
225
|
+
return time.Time{}, fmt.Errorf("invalid filename format")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Extract timestamp parts: YYYY-MM-DD-HH-mm-ss
|
|
229
|
+
// They should be at indices 2-7
|
|
230
|
+
timestampStr := strings.Join(parts[2:8], "-")
|
|
231
|
+
|
|
232
|
+
// Parse as local time since the filename timestamp is created in local time
|
|
233
|
+
parsedTime, err := time.ParseInLocation("2006-01-02-15-04-05", timestampStr, time.Local)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return time.Time{}, err
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return parsedTime, nil
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func filterOldLogs(logs []logFileInfo, olderThanDays int) []logFileInfo {
|
|
242
|
+
cutoff := time.Now().AddDate(0, 0, -olderThanDays)
|
|
243
|
+
var filtered []logFileInfo
|
|
244
|
+
|
|
245
|
+
for _, log := range logs {
|
|
246
|
+
if log.created.Before(cutoff) {
|
|
247
|
+
filtered = append(filtered, log)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return filtered
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func deleteLogFiles(files []logFileInfo) (int, int64, error) {
|
|
255
|
+
var count int
|
|
256
|
+
var bytesFreed int64
|
|
257
|
+
|
|
258
|
+
for _, file := range files {
|
|
259
|
+
if err := os.Remove(file.path); err != nil {
|
|
260
|
+
return count, bytesFreed, err
|
|
261
|
+
}
|
|
262
|
+
count++
|
|
263
|
+
bytesFreed += file.size
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return count, bytesFreed, nil
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func formatFileSize(bytes int64) string {
|
|
270
|
+
const unit = 1024
|
|
271
|
+
if bytes < unit {
|
|
272
|
+
return fmt.Sprintf("%d B", bytes)
|
|
273
|
+
}
|
|
274
|
+
div, exp := int64(unit), 0
|
|
275
|
+
for n := bytes / unit; n >= unit; n /= unit {
|
|
276
|
+
div *= unit
|
|
277
|
+
exp++
|
|
278
|
+
}
|
|
279
|
+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func formatAge(t time.Time) string {
|
|
283
|
+
duration := time.Since(t)
|
|
284
|
+
|
|
285
|
+
if duration < time.Hour {
|
|
286
|
+
minutes := int(duration.Minutes())
|
|
287
|
+
return fmt.Sprintf("%dm ago", minutes)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if duration < 24*time.Hour {
|
|
291
|
+
hours := int(duration.Hours())
|
|
292
|
+
return fmt.Sprintf("%dh ago", hours)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if duration < 7*24*time.Hour {
|
|
296
|
+
days := int(duration.Hours() / 24)
|
|
297
|
+
return fmt.Sprintf("%dd ago", days)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
weeks := int(duration.Hours() / 24 / 7)
|
|
301
|
+
return fmt.Sprintf("%dw ago", weeks)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func renderLogsTable(logs []logFileInfo, markForDeletion bool) error {
|
|
305
|
+
// Build table data
|
|
306
|
+
type tableRow struct {
|
|
307
|
+
filename string
|
|
308
|
+
size string
|
|
309
|
+
created string
|
|
310
|
+
age string
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
var rows []tableRow
|
|
314
|
+
for _, log := range logs {
|
|
315
|
+
rows = append(rows, tableRow{
|
|
316
|
+
filename: log.name,
|
|
317
|
+
size: formatFileSize(log.size),
|
|
318
|
+
created: log.created.Format("2006-01-02 15:04:05"),
|
|
319
|
+
age: formatAge(log.created),
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check output format
|
|
324
|
+
if global.Config.OutputFormat == "plain" {
|
|
325
|
+
// Use tabwriter for plain output
|
|
326
|
+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
327
|
+
fmt.Fprintln(w, "FILENAME\tSIZE\tCREATED\tAGE")
|
|
328
|
+
|
|
329
|
+
for _, row := range rows {
|
|
330
|
+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
|
|
331
|
+
row.filename,
|
|
332
|
+
row.size,
|
|
333
|
+
row.created,
|
|
334
|
+
row.age,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
w.Flush()
|
|
339
|
+
return nil
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Use markdown table for rich output
|
|
343
|
+
colorRenderer := display.NewRenderer(global.Config.OutputFormat)
|
|
344
|
+
var markdown strings.Builder
|
|
345
|
+
markdown.WriteString("| **FILENAME** | **SIZE** | **CREATED** | **AGE** |\n")
|
|
346
|
+
markdown.WriteString("|--------------|----------|-------------|---------|")
|
|
347
|
+
|
|
348
|
+
for _, row := range rows {
|
|
349
|
+
line := fmt.Sprintf("\n| %s | %s | %s | %s |",
|
|
350
|
+
row.filename,
|
|
351
|
+
row.size,
|
|
352
|
+
row.created,
|
|
353
|
+
row.age,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
// If marking for deletion, wrap in red
|
|
357
|
+
if markForDeletion {
|
|
358
|
+
line = colorRenderer.Red(line)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
markdown.WriteString(line)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Render the markdown table
|
|
365
|
+
renderer, err := display.NewMarkdownRendererForTerminal()
|
|
366
|
+
if err != nil {
|
|
367
|
+
// Fallback to plain markdown if renderer fails
|
|
368
|
+
fmt.Println(markdown.String())
|
|
369
|
+
return nil
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
rendered, err := renderer.Render(markdown.String())
|
|
373
|
+
if err != nil {
|
|
374
|
+
fmt.Println(markdown.String())
|
|
375
|
+
return nil
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fmt.Print(strings.TrimLeft(rendered, "\n"))
|
|
379
|
+
fmt.Println()
|
|
380
|
+
|
|
381
|
+
return nil
|
|
382
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
package output
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"sync"
|
|
6
|
+
"sync/atomic"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// SuspendInputMsg tells the input model to suspend and hide
|
|
13
|
+
type SuspendInputMsg struct{}
|
|
14
|
+
|
|
15
|
+
// ResumeInputMsg tells the input model to resume and show
|
|
16
|
+
type ResumeInputMsg struct{}
|
|
17
|
+
|
|
18
|
+
// OutputCoordinator manages terminal output and coordinates with interactive input
|
|
19
|
+
type OutputCoordinator struct {
|
|
20
|
+
mu sync.Mutex
|
|
21
|
+
program *tea.Program
|
|
22
|
+
inputVisible atomic.Bool
|
|
23
|
+
inputModel *InputModel // Reference to current input model for state restoration
|
|
24
|
+
restartCallback func(*InputModel) // Callback to restart the program with preserved state
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var (
|
|
28
|
+
globalCoordinator *OutputCoordinator
|
|
29
|
+
coordinatorMu sync.Mutex
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// GetCoordinator returns the global output coordinator instance
|
|
33
|
+
func GetCoordinator() *OutputCoordinator {
|
|
34
|
+
coordinatorMu.Lock()
|
|
35
|
+
defer coordinatorMu.Unlock()
|
|
36
|
+
|
|
37
|
+
if globalCoordinator == nil {
|
|
38
|
+
globalCoordinator = &OutputCoordinator{}
|
|
39
|
+
}
|
|
40
|
+
return globalCoordinator
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// SetProgram sets the bubbletea program for input coordination
|
|
44
|
+
func (oc *OutputCoordinator) SetProgram(program *tea.Program) {
|
|
45
|
+
oc.mu.Lock()
|
|
46
|
+
defer oc.mu.Unlock()
|
|
47
|
+
oc.program = program
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// SetInputModel sets the current input model reference for state preservation
|
|
51
|
+
func (oc *OutputCoordinator) SetInputModel(model *InputModel) {
|
|
52
|
+
oc.mu.Lock()
|
|
53
|
+
defer oc.mu.Unlock()
|
|
54
|
+
oc.inputModel = model
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// SetRestartCallback sets the callback for restarting the program
|
|
58
|
+
func (oc *OutputCoordinator) SetRestartCallback(callback func(*InputModel)) {
|
|
59
|
+
oc.mu.Lock()
|
|
60
|
+
defer oc.mu.Unlock()
|
|
61
|
+
oc.restartCallback = callback
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// SetInputVisible sets whether input is currently visible
|
|
65
|
+
func (oc *OutputCoordinator) SetInputVisible(visible bool) {
|
|
66
|
+
oc.inputVisible.Store(visible)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// IsInputVisible returns whether input is currently visible
|
|
70
|
+
func (oc *OutputCoordinator) IsInputVisible() bool {
|
|
71
|
+
return oc.inputVisible.Load()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Printf prints formatted output, suspending input if necessary
|
|
75
|
+
func (oc *OutputCoordinator) Printf(format string, args ...interface{}) {
|
|
76
|
+
oc.mu.Lock()
|
|
77
|
+
prog := oc.program
|
|
78
|
+
model := oc.inputModel
|
|
79
|
+
restart := oc.restartCallback
|
|
80
|
+
visible := oc.inputVisible.Load()
|
|
81
|
+
oc.mu.Unlock()
|
|
82
|
+
|
|
83
|
+
if visible && prog != nil && restart != nil && model != nil {
|
|
84
|
+
// Kill/restart approach: completely stop the program, print, restart with state
|
|
85
|
+
|
|
86
|
+
// 1. Save the current input state (text, cursor position, etc.)
|
|
87
|
+
savedModel := model.Clone()
|
|
88
|
+
|
|
89
|
+
// 2. Manually clear the form from terminal BEFORE quitting
|
|
90
|
+
clearCodes := model.ClearScreen()
|
|
91
|
+
if clearCodes != "" {
|
|
92
|
+
fmt.Print(clearCodes)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Quit the program
|
|
96
|
+
prog.Send(Quit())
|
|
97
|
+
|
|
98
|
+
// Small delay to let program actually quit
|
|
99
|
+
time.Sleep(20 * time.Millisecond)
|
|
100
|
+
|
|
101
|
+
// 4. Print the output
|
|
102
|
+
fmt.Printf(format, args...)
|
|
103
|
+
|
|
104
|
+
// 5. Restart with preserved state
|
|
105
|
+
restart(savedModel)
|
|
106
|
+
} else {
|
|
107
|
+
// No input showing, just print normally
|
|
108
|
+
fmt.Printf(format, args...)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Println prints a line with newline, suspending input if necessary
|
|
113
|
+
func (oc *OutputCoordinator) Println(args ...interface{}) {
|
|
114
|
+
oc.Printf("%s\n", fmt.Sprint(args...))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Print prints output, suspending input if necessary
|
|
118
|
+
func (oc *OutputCoordinator) Print(args ...interface{}) {
|
|
119
|
+
oc.Printf("%s", fmt.Sprint(args...))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Package-level convenience functions
|
|
123
|
+
|
|
124
|
+
// Printf prints formatted output via the global coordinator
|
|
125
|
+
func Printf(format string, args ...interface{}) {
|
|
126
|
+
GetCoordinator().Printf(format, args...)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Println prints a line with newline via the global coordinator
|
|
130
|
+
func Println(args ...interface{}) {
|
|
131
|
+
GetCoordinator().Println(args...)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Print prints output via the global coordinator
|
|
135
|
+
func Print(args ...interface{}) {
|
|
136
|
+
GetCoordinator().Print(args...)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// SetProgram sets the bubbletea program on the global coordinator
|
|
140
|
+
func SetProgram(program *tea.Program) {
|
|
141
|
+
GetCoordinator().SetProgram(program)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// SetInputVisible sets input visibility on the global coordinator
|
|
145
|
+
func SetInputVisible(visible bool) {
|
|
146
|
+
GetCoordinator().SetInputVisible(visible)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// IsInputVisible checks input visibility on the global coordinator
|
|
150
|
+
func IsInputVisible() bool {
|
|
151
|
+
return GetCoordinator().IsInputVisible()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// SetInputModel sets the input model on the global coordinator
|
|
155
|
+
func SetInputModel(model *InputModel) {
|
|
156
|
+
GetCoordinator().SetInputModel(model)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// SetRestartCallback sets the restart callback on the global coordinator
|
|
160
|
+
func SetRestartCallback(callback func(*InputModel)) {
|
|
161
|
+
GetCoordinator().SetRestartCallback(callback)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Quit returns a Bubble Tea quit message
|
|
165
|
+
func Quit() tea.Msg {
|
|
166
|
+
return tea.Quit()
|
|
167
|
+
}
|