Skip to content

Commit fdfb80b

Browse files
committedMar 10, 2025·
fix: interactive console commands that span multiple lines
fixes: #4809
1 parent f697414 commit fdfb80b

File tree

2 files changed

+44
-27
lines changed

2 files changed

+44
-27
lines changed
 

‎internal/terminal/interactive.go

+16-19
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,28 @@ type KongContextBinder func(ctx context.Context, kctx *kong.Context) context.Con
2929
type exitPanic struct{}
3030

3131
type interactiveConsole struct {
32-
projectConfig projectconfig.Config
33-
l *readline.Instance
34-
binder KongContextBinder
35-
k *kong.Kong
36-
closeWait sync.WaitGroup
37-
closed bool
32+
projectConfig projectconfig.Config
33+
l *readline.Instance
34+
binder KongContextBinder
35+
k *kong.Kong
36+
closeWait sync.WaitGroup
37+
closed bool
38+
currentLineCallback func(string)
3839
}
3940

4041
func newInteractiveConsole(k *kong.Kong, binder KongContextBinder, projectConfig projectconfig.Config, eventSource *schemaeventsource.EventSource) (*interactiveConsole, error) {
42+
it := &interactiveConsole{k: k, binder: binder, projectConfig: projectConfig}
4143
l, err := readline.NewEx(&readline.Config{
4244
Prompt: interactivePrompt,
4345
InterruptPrompt: "^C",
4446
AutoComplete: &FTLCompletion{app: k, view: eventSource.ViewOnly()},
45-
Listener: &ExitListener{cancel: func() {
46-
_ = syscall.Kill(-syscall.Getpid(), syscall.SIGINT) //nolint:forcetypeassert,errcheck // best effort
47-
}},
47+
Listener: it,
4848
})
49+
it.l = l
4950
if err != nil {
5051
return nil, fmt.Errorf("init readline: %w", err)
5152
}
5253

53-
it := &interactiveConsole{k: k, binder: binder, l: l, projectConfig: projectConfig}
5454
it.closeWait.Add(1)
5555
return it, nil
5656
}
@@ -98,7 +98,6 @@ func (r *interactiveConsole) run(ctx context.Context) error {
9898
if tsm, ok = sm.(*terminalStatusManager); ok {
9999
tsm.statusLock.Lock()
100100
tsm.clearStatusMessages()
101-
tsm.console = true
102101
tsm.consoleRefresh = r.l.Refresh
103102
tsm.recalculateLines()
104103
tsm.statusLock.Unlock()
@@ -117,6 +116,7 @@ func (r *interactiveConsole) run(ctx context.Context) error {
117116
}
118117

119118
for {
119+
l.Operation.String()
120120
line, err := l.Readline()
121121
if errors.Is(err, readline.ErrInterrupt) {
122122

@@ -193,15 +193,12 @@ func (r *interactiveConsole) run(ctx context.Context) error {
193193
return nil
194194
}
195195

196-
var _ readline.Listener = &ExitListener{}
197-
198-
type ExitListener struct {
199-
cancel func()
200-
}
201-
202-
func (e ExitListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
196+
func (e *interactiveConsole) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
203197
if key == readline.CharInterrupt {
204-
e.cancel()
198+
_ = syscall.Kill(-syscall.Getpid(), syscall.SIGINT) //nolint:forcetypeassert,errcheck // best effort
199+
}
200+
if e.currentLineCallback != nil {
201+
e.currentLineCallback(string(line))
205202
}
206203
return line, pos, true
207204
}

‎internal/terminal/status.go

+28-8
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ type terminalStatusManager struct {
112112
height int
113113
width int
114114
exitWait sync.WaitGroup
115-
console bool
116115
consoleRefresh func()
117116
spinnerCount int
118117
interactiveConsole optional.Option[*interactiveConsole]
118+
interactiveLines int
119119
}
120120

121121
type statusKey struct{}
@@ -283,11 +283,14 @@ func (r *terminalStatusManager) clearStatusMessages() {
283283
return
284284
}
285285
count := r.totalStatusLines
286-
if r.console {
287-
count--
286+
if r.interactiveConsole.Ok() {
287+
// With the interactive console the cursor sits on the last line, so we just clear, we don't move up
288+
r.underlyingWrite(ansiClearLine)
289+
} else {
290+
// Without the interactive console the cursor sits on an empty line by itself
291+
r.underlyingWrite(ansiUpOneLine + ansiClearLine)
288292
}
289-
r.underlyingWrite(ansiClearLine)
290-
for range count {
293+
for range count - 1 {
291294
r.underlyingWrite(ansiUpOneLine + ansiClearLine)
292295
}
293296
}
@@ -421,6 +424,10 @@ func (r *terminalStatusManager) redrawStatus() {
421424
}
422425
}
423426
if r.consoleRefresh != nil {
427+
// If there is more than one line we need to actually make space for it to render
428+
for range r.interactiveLines - 1 {
429+
r.underlyingWrite("\n")
430+
}
424431
r.consoleRefresh()
425432
}
426433
}
@@ -471,9 +478,7 @@ func (r *terminalStatusManager) recalculateLines() {
471478
total += countLines(i.prefix+i.message+i.suffix, r.width)
472479
}
473480
}
474-
if r.console {
475-
total++
476-
}
481+
total += r.interactiveLines
477482
r.totalStatusLines = total
478483
r.redrawStatus()
479484
}
@@ -540,6 +545,21 @@ func LaunchEmbeddedConsole(ctx context.Context, k *kong.Kong, binder KongContext
540545
fmt.Printf("\033[31mError: %s\033[0m\n", err)
541546
return
542547
}
548+
it.currentLineCallback = func(s string) {
549+
lines := countLines("> "+s, tsm.width) + 1
550+
if lines != tsm.interactiveLines {
551+
tsm.statusLock.Lock()
552+
defer tsm.statusLock.Unlock()
553+
if lines > tsm.interactiveLines {
554+
// Enter has already been pressed, so we need to add the lines to the total
555+
// This is so that it actually gets cleared
556+
tsm.totalStatusLines += lines - tsm.interactiveLines
557+
}
558+
tsm.interactiveLines = lines
559+
tsm.recalculateLines()
560+
}
561+
}
562+
tsm.interactiveLines = 1
543563
tsm.interactiveConsole = optional.Some(it)
544564
go func() {
545565
err := it.run(ctx)

0 commit comments

Comments
 (0)
Please sign in to comment.