• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 22514309419

28 Feb 2026 05:29AM UTC coverage: 8.888% (+4.6%) from 4.278%
22514309419

push

github

smallnest
replace with custiomized cli

0 of 492 new or added lines in 4 files covered. (0.0%)

3864 existing lines in 35 files now uncovered.

2864 of 32222 relevant lines covered (8.89%)

0.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cli/input/line_editor.go
1
package input
2

3
import (
4
        "bufio"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "os"
9
        "strings"
10
        "unicode/utf8"
11

12
        "golang.org/x/sys/unix"
13
)
14

15
var ErrInterrupt = errors.New("interrupt")
16

17
type lineEditor struct {
18
        fd     int
19
        reader *bufio.Reader
20
        state  *unix.Termios
21
        prompt string
22

23
        buf    []rune
24
        cursor int
25

26
        history []string
27
        histPos int
28
        draft   []rune
29

30
        pasteMode    bool
31
        pasteBuilder strings.Builder
32
        pendingPaste string
33
        tabHandler   func() string
34
}
35

NEW
36
func NewLineEditor(prompt string, tabHandler func() string) (*lineEditor, error) {
×
NEW
37
        fd := int(os.Stdin.Fd())
×
NEW
38
        st, err := makeRaw(fd)
×
NEW
39
        if err != nil {
×
NEW
40
                return nil, err
×
NEW
41
        }
×
NEW
42
        e := &lineEditor{
×
NEW
43
                fd:         fd,
×
NEW
44
                reader:     bufio.NewReader(os.Stdin),
×
NEW
45
                state:      st,
×
NEW
46
                prompt:     prompt,
×
NEW
47
                histPos:    0,
×
NEW
48
                tabHandler: tabHandler,
×
NEW
49
        }
×
NEW
50
        e.histPos = len(e.history)
×
NEW
51
        e.enableBracketedPaste()
×
NEW
52
        return e, nil
×
53
}
54

55
// Suspend 暂时退出原始模式,用于输出多行内容
NEW
56
func (e *lineEditor) Suspend() error {
×
NEW
57
        // 清除当前行并换行
×
NEW
58
        _, _ = os.Stdout.WriteString("\r\n")
×
NEW
59
        // 退出原始模式
×
NEW
60
        return restoreTerminal(e.fd, e.state)
×
NEW
61
}
×
62

63
// Resume 恢复原始模式
NEW
64
func (e *lineEditor) Resume() error {
×
NEW
65
        // 重新进入原始模式
×
NEW
66
        _, err := makeRaw(e.fd)
×
NEW
67
        if err != nil {
×
NEW
68
                return err
×
NEW
69
        }
×
70
        // 重新启用 bracketed paste
NEW
71
        e.enableBracketedPaste()
×
NEW
72
        return nil
×
73
}
74

NEW
75
func (e *lineEditor) Close() error {
×
NEW
76
        e.disableBracketedPaste()
×
NEW
77
        _, _ = os.Stdout.WriteString("\r\n")
×
NEW
78
        return restoreTerminal(e.fd, e.state)
×
NEW
79
}
×
80

NEW
81
func (e *lineEditor) InitHistory(history []string) {
×
NEW
82
        e.history = e.history[:0]
×
NEW
83
        for _, h := range history {
×
NEW
84
                if h != "" {
×
NEW
85
                        e.history = append(e.history, h)
×
NEW
86
                }
×
87
        }
NEW
88
        e.histPos = len(e.history)
×
89
}
90

NEW
91
func (e *lineEditor) SaveToHistory(line string) {
×
NEW
92
        if strings.TrimSpace(line) == "" {
×
NEW
93
                return
×
NEW
94
        }
×
NEW
95
        if len(e.history) > 0 && e.history[len(e.history)-1] == line {
×
NEW
96
                return
×
NEW
97
        }
×
NEW
98
        e.history = append(e.history, line)
×
NEW
99
        e.histPos = len(e.history)
×
100
}
101

NEW
102
func (e *lineEditor) Refresh() {
×
NEW
103
        e.render()
×
NEW
104
}
×
105

NEW
106
func (e *lineEditor) ReadLine() (string, error) {
×
NEW
107
        e.render()
×
NEW
108
        for {
×
NEW
109
                k, r, err := e.readKey()
×
NEW
110
                if err != nil {
×
NEW
111
                        if errors.Is(err, io.EOF) {
×
NEW
112
                                return "", io.EOF
×
NEW
113
                        }
×
NEW
114
                        return "", err
×
115
                }
116

NEW
117
                switch k {
×
NEW
118
                case keyCtrlC:
×
NEW
119
                        return "", ErrInterrupt
×
NEW
120
                case keyCtrlD:
×
NEW
121
                        if len(e.buf) == 0 {
×
NEW
122
                                return "", io.EOF
×
NEW
123
                        }
×
NEW
124
                        if e.cursor < len(e.buf) {
×
NEW
125
                                e.buf = append(e.buf[:e.cursor], e.buf[e.cursor+1:]...)
×
NEW
126
                                e.render()
×
NEW
127
                        }
×
NEW
128
                        continue
×
NEW
129
                case keyTab:
×
NEW
130
                        if e.tabHandler != nil {
×
NEW
131
                                cmd := e.tabHandler()
×
NEW
132
                                if cmd != "" {
×
NEW
133
                                        e.buf = []rune(cmd)
×
NEW
134
                                        e.cursor = len(e.buf)
×
NEW
135
                                        e.render()
×
NEW
136
                                }
×
137
                        }
NEW
138
                        continue
×
NEW
139
                case keyEnter:
×
NEW
140
                        if e.pasteMode {
×
NEW
141
                                e.pasteBuilder.WriteRune('\n')
×
NEW
142
                                continue
×
143
                        }
NEW
144
                        line := string(e.buf)
×
NEW
145
                        if e.pendingPaste != "" && line == e.pastePlaceholder() {
×
NEW
146
                                line = e.pendingPaste
×
NEW
147
                                e.pendingPaste = ""
×
NEW
148
                                _, _ = os.Stdout.WriteString("\r\n" + line + "\r\n")
×
NEW
149
                        } else {
×
NEW
150
                                _, _ = os.Stdout.WriteString("\r\n")
×
NEW
151
                        }
×
NEW
152
                        e.buf = e.buf[:0]
×
NEW
153
                        e.cursor = 0
×
NEW
154
                        e.histPos = len(e.history)
×
NEW
155
                        e.draft = e.draft[:0]
×
NEW
156
                        return line, nil
×
NEW
157
                case keyBackspace:
×
NEW
158
                        if e.cursor > 0 {
×
NEW
159
                                e.buf = append(e.buf[:e.cursor-1], e.buf[e.cursor:]...)
×
NEW
160
                                e.cursor--
×
NEW
161
                                e.render()
×
NEW
162
                        }
×
NEW
163
                case keyLeft:
×
NEW
164
                        if e.cursor > 0 {
×
NEW
165
                                e.cursor--
×
NEW
166
                                e.render()
×
NEW
167
                        }
×
NEW
168
                case keyRight:
×
NEW
169
                        if e.cursor < len(e.buf) {
×
NEW
170
                                e.cursor++
×
NEW
171
                                e.render()
×
NEW
172
                        }
×
NEW
173
                case keyUp:
×
NEW
174
                        e.historyUp()
×
NEW
175
                        e.render()
×
NEW
176
                case keyDown:
×
NEW
177
                        e.historyDown()
×
NEW
178
                        e.render()
×
NEW
179
                case keyPasteStart:
×
NEW
180
                        e.pasteMode = true
×
NEW
181
                        e.pasteBuilder.Reset()
×
NEW
182
                case keyPasteEnd:
×
NEW
183
                        e.pasteMode = false
×
NEW
184
                        chunk := normalizePastedContent(e.pasteBuilder.String())
×
NEW
185
                        e.pasteBuilder.Reset()
×
NEW
186
                        if chunk != "" {
×
NEW
187
                                newlines := strings.Count(chunk, "\n")
×
NEW
188
                                if newlines > 0 {
×
NEW
189
                                        clearLines(newlines)
×
NEW
190
                                }
×
NEW
191
                                if e.pendingPaste == "" {
×
NEW
192
                                        e.pendingPaste = chunk
×
NEW
193
                                } else {
×
NEW
194
                                        e.pendingPaste += "\n" + chunk
×
NEW
195
                                }
×
NEW
196
                                e.buf = []rune(e.pastePlaceholder())
×
NEW
197
                                e.cursor = len(e.buf)
×
NEW
198
                                e.render()
×
199
                        }
NEW
200
                case keyRune:
×
NEW
201
                        if e.pasteMode {
×
NEW
202
                                e.pasteBuilder.WriteRune(r)
×
NEW
203
                                continue
×
204
                        }
NEW
205
                        if e.pendingPaste != "" {
×
NEW
206
                                // If user starts typing, cancel the pending pasted block.
×
NEW
207
                                e.pendingPaste = ""
×
NEW
208
                                e.buf = e.buf[:0]
×
NEW
209
                                e.cursor = 0
×
NEW
210
                        }
×
NEW
211
                        e.insertRune(r)
×
NEW
212
                        e.render()
×
NEW
213
                case keyUnknown:
×
NEW
214
                        if e.pasteMode {
×
NEW
215
                                e.pasteBuilder.WriteRune(r)
×
NEW
216
                        }
×
217
                }
218
        }
219
}
220

NEW
221
func (e *lineEditor) insertRune(r rune) {
×
NEW
222
        if e.cursor == len(e.buf) {
×
NEW
223
                e.buf = append(e.buf, r)
×
NEW
224
                e.cursor++
×
NEW
225
                return
×
NEW
226
        }
×
NEW
227
        e.buf = append(e.buf, 0)
×
NEW
228
        copy(e.buf[e.cursor+1:], e.buf[e.cursor:])
×
NEW
229
        e.buf[e.cursor] = r
×
NEW
230
        e.cursor++
×
231
}
232

NEW
233
func (e *lineEditor) historyUp() {
×
NEW
234
        if len(e.history) == 0 {
×
NEW
235
                return
×
NEW
236
        }
×
NEW
237
        if e.histPos == len(e.history) {
×
NEW
238
                e.draft = append(e.draft[:0], e.buf...)
×
NEW
239
        }
×
NEW
240
        if e.histPos > 0 {
×
NEW
241
                e.histPos--
×
NEW
242
                e.buf = []rune(e.history[e.histPos])
×
NEW
243
                e.cursor = len(e.buf)
×
NEW
244
        }
×
245
}
246

NEW
247
func (e *lineEditor) historyDown() {
×
NEW
248
        if len(e.history) == 0 {
×
NEW
249
                return
×
NEW
250
        }
×
NEW
251
        if e.histPos < len(e.history)-1 {
×
NEW
252
                e.histPos++
×
NEW
253
                e.buf = []rune(e.history[e.histPos])
×
NEW
254
                e.cursor = len(e.buf)
×
NEW
255
                return
×
NEW
256
        }
×
NEW
257
        if e.histPos == len(e.history)-1 {
×
NEW
258
                e.histPos = len(e.history)
×
NEW
259
                e.buf = append(e.buf[:0], e.draft...)
×
NEW
260
                e.cursor = len(e.buf)
×
NEW
261
        }
×
262
}
263

NEW
264
func (e *lineEditor) render() {
×
NEW
265
        line := string(e.buf)
×
NEW
266
        _, _ = os.Stdout.WriteString("\r\x1b[2K" + e.prompt + line)
×
NEW
267
        tail := len(e.buf) - e.cursor
×
NEW
268
        if tail > 0 {
×
NEW
269
                _, _ = os.Stdout.WriteString(fmt.Sprintf("\x1b[%dD", tail))
×
NEW
270
        }
×
271
}
272

NEW
273
func (e *lineEditor) ClearCurrentLine() {
×
NEW
274
        _, _ = os.Stdout.WriteString("\r\x1b[2K")
×
NEW
275
}
×
276

NEW
277
func normalizePastedContent(s string) string {
×
NEW
278
        s = strings.ReplaceAll(s, "\r\n", "\n")
×
NEW
279
        s = strings.ReplaceAll(s, "\r", "\n")
×
NEW
280
        return strings.TrimRight(s, "\n")
×
NEW
281
}
×
282

NEW
283
func clearLines(count int) {
×
NEW
284
        if count <= 0 {
×
NEW
285
                return
×
NEW
286
        }
×
NEW
287
        fmt.Printf("\x1b[%dA\x1b[J", count)
×
288
}
289

NEW
290
func (e *lineEditor) pastePlaceholder() string {
×
NEW
291
        return fmt.Sprintf("[Pasted Content %d chars]", utf8.RuneCountInString(e.pendingPaste))
×
NEW
292
}
×
293

NEW
294
func (e *lineEditor) enableBracketedPaste() {
×
NEW
295
        _, _ = os.Stdout.WriteString("\x1b[?2004h")
×
NEW
296
}
×
297

NEW
298
func (e *lineEditor) disableBracketedPaste() {
×
NEW
299
        _, _ = os.Stdout.WriteString("\x1b[?2004l")
×
NEW
300
}
×
301

302
type keyType int
303

304
const (
305
        keyUnknown keyType = iota
306
        keyRune
307
        keyEnter
308
        keyBackspace
309
        keyCtrlC
310
        keyCtrlD
311
        keyLeft
312
        keyRight
313
        keyUp
314
        keyDown
315
        keyPasteStart
316
        keyPasteEnd
317
        keyTab
318
)
319

NEW
320
func (e *lineEditor) readKey() (keyType, rune, error) {
×
NEW
321
        r, _, err := e.reader.ReadRune()
×
NEW
322
        if err != nil {
×
NEW
323
                return keyUnknown, 0, err
×
NEW
324
        }
×
325

NEW
326
        switch r {
×
NEW
327
        case 3:
×
NEW
328
                return keyCtrlC, 0, nil
×
NEW
329
        case 4:
×
NEW
330
                return keyCtrlD, 0, nil
×
NEW
331
        case '\r', '\n':
×
NEW
332
                return keyEnter, 0, nil
×
NEW
333
        case 127, 8:
×
NEW
334
                return keyBackspace, 0, nil
×
NEW
335
        case '\t':
×
NEW
336
                return keyTab, 0, nil
×
NEW
337
        case 0x1b:
×
NEW
338
                return e.readEscapeKey()
×
NEW
339
        default:
×
NEW
340
                return keyRune, r, nil
×
341
        }
342
}
343

NEW
344
func (e *lineEditor) readEscapeKey() (keyType, rune, error) {
×
NEW
345
        r, _, err := e.reader.ReadRune()
×
NEW
346
        if err != nil {
×
NEW
347
                return keyUnknown, 0, err
×
NEW
348
        }
×
NEW
349
        if r != '[' {
×
NEW
350
                return keyUnknown, 0, nil
×
NEW
351
        }
×
352

NEW
353
        var sb strings.Builder
×
NEW
354
        for {
×
NEW
355
                rn, _, err := e.reader.ReadRune()
×
NEW
356
                if err != nil {
×
NEW
357
                        return keyUnknown, 0, err
×
NEW
358
                }
×
NEW
359
                sb.WriteRune(rn)
×
NEW
360
                if (rn >= 'A' && rn <= 'Z') || rn == '~' {
×
NEW
361
                        break
×
362
                }
363
        }
364

NEW
365
        switch sb.String() {
×
NEW
366
        case "A":
×
NEW
367
                return keyUp, 0, nil
×
NEW
368
        case "B":
×
NEW
369
                return keyDown, 0, nil
×
NEW
370
        case "C":
×
NEW
371
                return keyRight, 0, nil
×
NEW
372
        case "D":
×
NEW
373
                return keyLeft, 0, nil
×
NEW
374
        case "200~":
×
NEW
375
                return keyPasteStart, 0, nil
×
NEW
376
        case "201~":
×
NEW
377
                return keyPasteEnd, 0, nil
×
NEW
378
        default:
×
NEW
379
                return keyUnknown, 0, nil
×
380
        }
381
}
382

NEW
383
func makeRaw(fd int) (*unix.Termios, error) {
×
NEW
384
        getReq, setReq := termiosRequests()
×
NEW
385
        termios, err := unix.IoctlGetTermios(fd, getReq)
×
NEW
386
        if err != nil {
×
NEW
387
                return nil, err
×
NEW
388
        }
×
389

NEW
390
        oldState := *termios
×
NEW
391
        // cfmakeraw equivalent
×
NEW
392
        termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
×
NEW
393
        termios.Oflag &^= unix.OPOST
×
NEW
394
        termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
×
NEW
395
        termios.Cflag &^= unix.CSIZE | unix.PARENB
×
NEW
396
        termios.Cflag |= unix.CS8
×
NEW
397
        termios.Cc[unix.VMIN] = 1
×
NEW
398
        termios.Cc[unix.VTIME] = 0
×
NEW
399

×
NEW
400
        if err := unix.IoctlSetTermios(fd, setReq, termios); err != nil {
×
NEW
401
                return nil, err
×
NEW
402
        }
×
NEW
403
        return &oldState, nil
×
404
}
405

NEW
406
func restoreTerminal(fd int, state *unix.Termios) error {
×
NEW
407
        _, setReq := termiosRequests()
×
NEW
408
        return unix.IoctlSetTermios(fd, setReq, state)
×
NEW
409
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc