├── .check-gofmt.sh ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── complete.go ├── complete_helper.go ├── docs ├── CHANGELOG.md ├── MIGRATING.md └── shortcut.md ├── example ├── readline-demo │ └── readline-demo.go ├── readline-im │ └── README.md ├── readline-multiline │ └── readline-multiline.go ├── readline-paged-completion │ └── readline-paged-completion.go └── readline-pass-strength │ └── readline-pass-strength.go ├── go.mod ├── go.sum ├── history.go ├── internal ├── ansi │ ├── ansi.go │ └── ansi_windows.go ├── platform │ ├── utils_unix.go │ └── utils_windows.go ├── ringbuf │ ├── ringbuf.go │ └── ringbuf_test.go ├── runes │ ├── runes.go │ └── runes_test.go └── term │ ├── term.go │ ├── term_plan9.go │ ├── term_unix.go │ ├── term_unix_bsd.go │ ├── term_unix_other.go │ ├── term_unsupported.go │ ├── term_windows.go │ ├── terminal.go │ └── terminal_test.go ├── operation.go ├── readline.go ├── readline_test.go ├── runebuf.go ├── search.go ├── terminal.go ├── undo.go ├── utils.go └── vim.go /.check-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # exclude vendor/ 4 | SOURCES="*.go internal/ example/" 5 | 6 | if [ "$1" = "--fix" ]; then 7 | exec gofmt -s -w $SOURCES 8 | fi 9 | 10 | if [ -n "$(gofmt -s -l $SOURCES)" ]; then 11 | echo "Go code is not formatted correctly with \`gofmt -s\`:" 12 | gofmt -s -d $SOURCES 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "build" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "master" 7 | push: 8 | branches: 9 | - "master" 10 | 11 | jobs: 12 | build: 13 | runs-on: "ubuntu-22.04" 14 | steps: 15 | - name: "checkout repository" 16 | uses: "actions/checkout@v3" 17 | - name: "setup go" 18 | uses: "actions/setup-go@v3" 19 | with: 20 | go-version: "1.20" 21 | - name: "make ci" 22 | run: "make ci" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | *.swp 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chzyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test examples ci gofmt 2 | 3 | all: ci 4 | 5 | ci: test examples 6 | 7 | test: 8 | go test -race ./... 9 | go vet ./... 10 | ./.check-gofmt.sh 11 | 12 | examples: 13 | GOOS=linux go build -o /dev/null example/readline-demo/readline-demo.go 14 | GOOS=windows go build -o /dev/null example/readline-demo/readline-demo.go 15 | GOOS=darwin go build -o /dev/null example/readline-demo/readline-demo.go 16 | 17 | gofmt: 18 | ./.check-gofmt.sh --fix 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | readline 2 | ======== 3 | 4 | [![Godoc](https://godoc.org/github.com/ergochat/readline?status.svg)](https://godoc.org/github.com/ergochat/readline) 5 | 6 | This is a pure Go implementation of functionality comparable to [GNU Readline](https://en.wikipedia.org/wiki/GNU_Readline), i.e. line editing and command history for simple TUI programs. 7 | 8 | It is a fork of [chzyer/readline](https://github.com/chzyer/readline). 9 | 10 | * Relative to the upstream repository, it is actively maintained and has numerous bug fixes 11 | - See our [changelog](docs/CHANGELOG.md) for details on fixes and improvements 12 | - See our [migration guide](docs/MIGRATING.md) for advice on how to migrate from upstream 13 | * Relative to [x/term](https://pkg.go.dev/golang.org/x/term), it has more features (e.g. tab-completion) 14 | * In use by multiple projects: [gopass](https://github.com/gopasspw/gopass), [fq](https://github.com/wader/fq), and [ircdog](https://github.com/ergochat/ircdog) 15 | 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "log" 23 | 24 | "github.com/ergochat/readline" 25 | ) 26 | 27 | func main() { 28 | // see readline.NewFromConfig for advanced options: 29 | rl, err := readline.New("> ") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | defer rl.Close() 34 | log.SetOutput(rl.Stderr()) // redraw the prompt correctly after log output 35 | 36 | for { 37 | line, err := rl.ReadLine() 38 | // `err` is either nil, io.EOF, readline.ErrInterrupt, or an unexpected 39 | // condition in stdin: 40 | if err != nil { 41 | return 42 | } 43 | // `line` is returned without the terminating \n or CRLF: 44 | fmt.Fprintf(rl, "you wrote: %s\n", line) 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /complete.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "sync/atomic" 8 | 9 | "github.com/ergochat/readline/internal/platform" 10 | "github.com/ergochat/readline/internal/runes" 11 | ) 12 | 13 | type AutoCompleter interface { 14 | // Readline will pass the whole line and current offset to it 15 | // Completer need to pass all the candidates, and how long they shared the same characters in line 16 | // Example: 17 | // [go, git, git-shell, grep] 18 | // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1 19 | // Do("gi", 2) => ["t", "t-shell"], 2 20 | // Do("git", 3) => ["", "-shell"], 3 21 | Do(line []rune, pos int) (newLine [][]rune, length int) 22 | } 23 | 24 | type opCompleter struct { 25 | w *terminal 26 | op *operation 27 | 28 | inCompleteMode atomic.Uint32 // this is read asynchronously from wrapWriter 29 | inSelectMode bool 30 | 31 | candidate [][]rune // list of candidates 32 | candidateSource []rune // buffer string when tab was pressed 33 | candidateOff int // num runes in common from buf where candidate start 34 | candidateChoice int // absolute index of the chosen candidate (indexing the candidate array which might not all display in current page) 35 | candidateColNum int // num columns candidates take 0..wraps, 1 col, 2 cols etc. 36 | candidateColWidth int // width of candidate columns 37 | linesAvail int // number of lines available below the user's prompt which could be used for rendering the completion 38 | pageStartIdx []int // start index in the candidate array on each page (candidatePageStart[i] = absolute idx of the first candidate on page i) 39 | curPage int // index of the current page 40 | } 41 | 42 | func newOpCompleter(w *terminal, op *operation) *opCompleter { 43 | return &opCompleter{ 44 | w: w, 45 | op: op, 46 | } 47 | } 48 | 49 | func (o *opCompleter) doSelect() { 50 | if len(o.candidate) == 1 { 51 | o.op.buf.WriteRunes(o.candidate[0]) 52 | o.ExitCompleteMode(false) 53 | return 54 | } 55 | o.nextCandidate() 56 | o.CompleteRefresh() 57 | } 58 | 59 | // Convert absolute index of the chosen candidate to a page-relative index 60 | func (o *opCompleter) candidateChoiceWithinPage() int { 61 | return o.candidateChoice - o.pageStartIdx[o.curPage] 62 | } 63 | 64 | // Given a page relative index of the chosen candidate, update the absolute index 65 | func (o *opCompleter) updateAbsolutechoice(choiceWithinPage int) { 66 | o.candidateChoice = choiceWithinPage + o.pageStartIdx[o.curPage] 67 | } 68 | 69 | // Move selection to the next candidate, updating page if necessary 70 | // Note: we don't allow passing arbitrary offset to this function because, e.g., 71 | // we don't have the 3rd page offset initialized when the user is just seeing the first page, 72 | // so we only allow users to navigate into the 2nd page but not to an arbirary page as a result 73 | // of calling this method 74 | func (o *opCompleter) nextCandidate() { 75 | o.candidateChoice = (o.candidateChoice + 1) % len(o.candidate) 76 | // Wrapping around 77 | if o.candidateChoice == 0 { 78 | o.curPage = 0 79 | return 80 | } 81 | // Going to next page 82 | if o.candidateChoice == o.pageStartIdx[o.curPage+1] { 83 | o.curPage += 1 84 | } 85 | } 86 | 87 | // Move selection to the next ith col in the current line, wrapping to the line start/end if needed 88 | func (o *opCompleter) nextCol(i int) { 89 | // If o.candidateColNum == 1 or 0, there is only one col per line and this is a noop 90 | if o.candidateColNum > 1 { 91 | idxWithinPage := o.candidateChoiceWithinPage() 92 | curLine := idxWithinPage / o.candidateColNum 93 | offsetInLine := idxWithinPage % o.candidateColNum 94 | nextOffset := offsetInLine + i 95 | nextOffset %= o.candidateColNum 96 | if nextOffset < 0 { 97 | nextOffset += o.candidateColNum 98 | } 99 | 100 | nextIdxWithinPage := curLine*o.candidateColNum + nextOffset 101 | o.updateAbsolutechoice(nextIdxWithinPage) 102 | } 103 | } 104 | 105 | // Move selection to the line below 106 | func (o *opCompleter) nextLine() { 107 | colNum := 1 108 | if o.candidateColNum > 1 { 109 | colNum = o.candidateColNum 110 | } 111 | 112 | idxWithinPage := o.candidateChoiceWithinPage() 113 | 114 | idxWithinPage += colNum 115 | if idxWithinPage >= o.getMatrixSize() { 116 | idxWithinPage -= o.getMatrixSize() 117 | } else if idxWithinPage >= o.numCandidateCurPage() { 118 | idxWithinPage += colNum 119 | idxWithinPage -= o.getMatrixSize() 120 | } 121 | 122 | o.updateAbsolutechoice(idxWithinPage) 123 | } 124 | 125 | // Move selection to the line above 126 | func (o *opCompleter) prevLine() { 127 | colNum := 1 128 | if o.candidateColNum > 1 { 129 | colNum = o.candidateColNum 130 | } 131 | 132 | idxWithinPage := o.candidateChoiceWithinPage() 133 | 134 | idxWithinPage -= colNum 135 | if idxWithinPage < 0 { 136 | idxWithinPage += o.getMatrixSize() 137 | if idxWithinPage >= o.numCandidateCurPage() { 138 | idxWithinPage -= colNum 139 | } 140 | } 141 | 142 | o.updateAbsolutechoice(idxWithinPage) 143 | } 144 | 145 | // Move selection to the start of the current line 146 | func (o *opCompleter) lineStart() { 147 | if o.candidateColNum > 1 { 148 | idxWithinPage := o.candidateChoiceWithinPage() 149 | lineOffset := idxWithinPage % o.candidateColNum 150 | idxWithinPage -= lineOffset 151 | o.updateAbsolutechoice(idxWithinPage) 152 | } 153 | } 154 | 155 | // Move selection to the end of the current line 156 | func (o *opCompleter) lineEnd() { 157 | if o.candidateColNum > 1 { 158 | idxWithinPage := o.candidateChoiceWithinPage() 159 | offsetToLineEnd := o.candidateColNum - idxWithinPage%o.candidateColNum - 1 160 | idxWithinPage += offsetToLineEnd 161 | o.updateAbsolutechoice(idxWithinPage) 162 | if o.candidateChoice >= len(o.candidate) { 163 | o.candidateChoice = len(o.candidate) - 1 164 | } 165 | } 166 | } 167 | 168 | // Move to the next page if possible, returning selection to the first item in the page 169 | func (o *opCompleter) nextPage() { 170 | // Check that this is not the last page already 171 | nextPageStart := o.pageStartIdx[o.curPage+1] 172 | if nextPageStart < len(o.candidate) { 173 | o.curPage += 1 174 | o.candidateChoice = o.pageStartIdx[o.curPage] 175 | } 176 | } 177 | 178 | // Move to the previous page if possible, returning selection to the first item in the page 179 | func (o *opCompleter) prevPage() { 180 | if o.curPage > 0 { 181 | o.curPage -= 1 182 | o.candidateChoice = o.pageStartIdx[o.curPage] 183 | } 184 | } 185 | 186 | // OnComplete returns true if complete mode is available. Used to ring bell 187 | // when tab pressed if cannot do complete for reason such as width unknown 188 | // or no candidates available. 189 | func (o *opCompleter) OnComplete() (ringBell bool) { 190 | tWidth, tHeight := o.w.GetWidthHeight() 191 | if tWidth == 0 || tHeight < 3 { 192 | return false 193 | } 194 | if o.IsInCompleteSelectMode() { 195 | o.doSelect() 196 | return true 197 | } 198 | 199 | buf := o.op.buf 200 | rs := buf.Runes() 201 | 202 | // If in complete mode and nothing else typed then we must be entering select mode 203 | if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) { 204 | if len(o.candidate) > 1 { 205 | same, size := runes.Aggregate(o.candidate) 206 | if size > 0 { 207 | buf.WriteRunes(same) 208 | o.ExitCompleteMode(false) 209 | return false // partial completion so ring the bell 210 | } 211 | } 212 | o.EnterCompleteSelectMode() 213 | o.doSelect() 214 | return true 215 | } 216 | 217 | newLines, offset := o.op.GetConfig().AutoComplete.Do(rs, buf.idx) 218 | if len(newLines) == 0 || (len(newLines) == 1 && len(newLines[0]) == 0) { 219 | o.ExitCompleteMode(false) 220 | return false // will ring bell on initial tab press 221 | } 222 | if o.candidateOff > offset { 223 | // part of buffer we are completing has changed. Example might be that we were completing "ls" and 224 | // user typed space so we are no longer completing "ls" but now we are completing an argument of 225 | // the ls command. Instead of continuing in complete mode, we exit. 226 | o.ExitCompleteMode(false) 227 | return true 228 | } 229 | o.candidateSource = rs 230 | 231 | // only Aggregate candidates in non-complete mode 232 | if !o.IsInCompleteMode() { 233 | if len(newLines) == 1 { 234 | // not yet in complete mode but only 1 candidate so complete it 235 | buf.WriteRunes(newLines[0]) 236 | o.ExitCompleteMode(false) 237 | return true 238 | } 239 | 240 | // check if all candidates have common prefix and return it and its size 241 | same, size := runes.Aggregate(newLines) 242 | if size > 0 { 243 | buf.WriteRunes(same) 244 | o.ExitCompleteMode(false) 245 | return false // partial completion so ring the bell 246 | } 247 | } 248 | 249 | // otherwise, we just enter complete mode (which does a refresh) 250 | o.EnterCompleteMode(offset, newLines) 251 | return true 252 | } 253 | 254 | func (o *opCompleter) IsInCompleteSelectMode() bool { 255 | return o.inSelectMode 256 | } 257 | 258 | func (o *opCompleter) IsInCompleteMode() bool { 259 | return o.inCompleteMode.Load() == 1 260 | } 261 | 262 | func (o *opCompleter) HandleCompleteSelect(r rune) (stayInMode bool) { 263 | next := true 264 | switch r { 265 | case CharEnter, CharCtrlJ: 266 | next = false 267 | o.op.buf.WriteRunes(o.candidate[o.candidateChoice]) 268 | o.ExitCompleteMode(false) 269 | case CharLineStart: 270 | o.lineStart() 271 | case CharLineEnd: 272 | o.lineEnd() 273 | case CharBackspace: 274 | o.ExitCompleteSelectMode() 275 | next = false 276 | case CharTab: 277 | o.nextCandidate() 278 | case CharForward: 279 | o.nextCol(1) 280 | case CharBell, CharInterrupt: 281 | o.ExitCompleteMode(true) 282 | next = false 283 | case CharNext: 284 | o.nextLine() 285 | case CharBackward, MetaShiftTab: 286 | o.nextCol(-1) 287 | case CharPrev: 288 | o.prevLine() 289 | case 'j', 'J': 290 | o.prevPage() 291 | case 'k', 'K': 292 | o.nextPage() 293 | default: 294 | next = false 295 | o.ExitCompleteSelectMode() 296 | } 297 | if next { 298 | o.CompleteRefresh() 299 | return true 300 | } 301 | return false 302 | } 303 | 304 | func (o *opCompleter) getMatrixSize() int { 305 | colNum := 1 306 | if o.candidateColNum > 1 { 307 | colNum = o.candidateColNum 308 | } 309 | line := o.getMatrixNumRows() 310 | return line * colNum 311 | } 312 | 313 | // Number of candidate that could fit on current page 314 | func (o *opCompleter) numCandidateCurPage() int { 315 | // Safety: we will always render the first page, and whenever we finished rendering page i, 316 | // we always populate o.candidatePageStart through at least i + 1, so when this is called, we 317 | // always know the start of the next page 318 | return o.pageStartIdx[o.curPage+1] - o.pageStartIdx[o.curPage] 319 | } 320 | 321 | // Get number of rows of current page viewed as a matrix of candidates 322 | func (o *opCompleter) getMatrixNumRows() int { 323 | candidateCurPage := o.numCandidateCurPage() 324 | // Normal case where there is no wrap 325 | if o.candidateColNum > 1 { 326 | numLine := candidateCurPage / o.candidateColNum 327 | if candidateCurPage%o.candidateColNum != 0 { 328 | numLine++ 329 | } 330 | return numLine 331 | } 332 | 333 | // Now since there are wraps, each candidate will be put on its own line, so the number of lines is just the number of candidate 334 | return candidateCurPage 335 | } 336 | 337 | // setColumnInfo calculates column width and number of columns required 338 | // to present the list of candidates on the terminal. 339 | func (o *opCompleter) setColumnInfo() { 340 | same := o.op.buf.RuneSlice(-o.candidateOff) 341 | sameWidth := runes.WidthAll(same) 342 | 343 | colWidth := 0 344 | for _, c := range o.candidate { 345 | w := sameWidth + runes.WidthAll(c) 346 | if w > colWidth { 347 | colWidth = w 348 | } 349 | } 350 | colWidth++ // whitespace between cols 351 | 352 | tWidth, _ := o.w.GetWidthHeight() 353 | 354 | // -1 to avoid end of line issues 355 | width := tWidth - 1 356 | colNum := width / colWidth 357 | if colNum != 0 { 358 | colWidth += (width - (colWidth * colNum)) / colNum 359 | } 360 | 361 | o.candidateColNum = colNum 362 | o.candidateColWidth = colWidth 363 | } 364 | 365 | // CompleteRefresh is used for completemode and selectmode 366 | func (o *opCompleter) CompleteRefresh() { 367 | if !o.IsInCompleteMode() { 368 | return 369 | } 370 | 371 | buf := bufio.NewWriter(o.w) 372 | // calculate num lines from cursor pos to where choices should be written 373 | lineCnt := o.op.buf.CursorLineCount() 374 | buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) // move down from cursor to start of candidates 375 | buf.WriteString("\033[J") 376 | 377 | same := o.op.buf.RuneSlice(-o.candidateOff) 378 | tWidth, _ := o.w.GetWidthHeight() 379 | 380 | colIdx := 0 381 | lines := 0 382 | sameWidth := runes.WidthAll(same) 383 | 384 | // Show completions for the current page 385 | idx := o.pageStartIdx[o.curPage] 386 | for ; idx < len(o.candidate); idx++ { 387 | // If writing the current candidate would overflow the page, 388 | // we know that it is the start of the next page. 389 | if colIdx == 0 && lines == o.linesAvail { 390 | if o.curPage == len(o.pageStartIdx)-1 { 391 | o.pageStartIdx = append(o.pageStartIdx, idx) 392 | } 393 | break 394 | } 395 | 396 | c := o.candidate[idx] 397 | inSelect := idx == o.candidateChoice && o.IsInCompleteSelectMode() 398 | cWidth := sameWidth + runes.WidthAll(c) 399 | cLines := 1 400 | if tWidth > 0 { 401 | sWidth := 0 402 | if platform.IsWindows && inSelect { 403 | sWidth = 1 // adjust for hightlighting on Windows 404 | } 405 | cLines = (cWidth + sWidth) / tWidth 406 | if (cWidth+sWidth)%tWidth > 0 { 407 | cLines++ 408 | } 409 | } 410 | 411 | if lines > 0 && colIdx == 0 { 412 | // After line 1, if we're printing to the first column 413 | // goto a new line. We do it here, instead of at the end 414 | // of the loop, to avoid the last \n taking up a blank 415 | // line at the end and stealing realestate. 416 | buf.WriteString("\n") 417 | } 418 | 419 | if inSelect { 420 | buf.WriteString("\033[30;47m") 421 | } 422 | 423 | buf.WriteString(string(same)) 424 | buf.WriteString(string(c)) 425 | if o.candidateColNum >= 1 { 426 | // only output spaces between columns if everything fits 427 | buf.Write(bytes.Repeat([]byte(" "), o.candidateColWidth-cWidth)) 428 | } 429 | 430 | if inSelect { 431 | buf.WriteString("\033[0m") 432 | } 433 | 434 | colIdx++ 435 | if colIdx >= o.candidateColNum { 436 | lines += cLines 437 | colIdx = 0 438 | if platform.IsWindows { 439 | // Windows EOL edge-case. 440 | buf.WriteString("\b") 441 | } 442 | } 443 | } 444 | 445 | if idx == len(o.candidate) { 446 | // Book-keeping for the last page. 447 | o.pageStartIdx = append(o.pageStartIdx, len(o.candidate)) 448 | } 449 | 450 | if colIdx > 0 { 451 | lines++ // mid-line so count it. 452 | } 453 | 454 | // Show the guidance if there are more pages 455 | if idx != len(o.candidate) || o.curPage > 0 { 456 | buf.WriteString("\n-- (j: prev page) (k: next page) --") 457 | lines++ 458 | } 459 | 460 | // wrote out choices over "lines", move back to cursor (positioned at index) 461 | fmt.Fprintf(buf, "\033[%dA", lines) 462 | buf.Write(o.op.buf.getBackspaceSequence()) 463 | buf.Flush() 464 | } 465 | 466 | func (o *opCompleter) EnterCompleteSelectMode() { 467 | o.inSelectMode = true 468 | o.candidateChoice = -1 469 | } 470 | 471 | func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) { 472 | o.inCompleteMode.Store(1) 473 | o.candidate = candidate 474 | o.candidateOff = offset 475 | o.setColumnInfo() 476 | o.initPage() 477 | o.CompleteRefresh() 478 | } 479 | 480 | func (o *opCompleter) initPage() { 481 | _, tHeight := o.w.GetWidthHeight() 482 | buflineCnt := o.op.buf.LineCount() // lines taken by buffer content 483 | o.linesAvail = tHeight - buflineCnt - 1 // lines available without scrolling buffer off screen, reserve one line for the guidance message 484 | o.pageStartIdx = []int{0} // first page always start at 0 485 | o.curPage = 0 486 | } 487 | 488 | func (o *opCompleter) ExitCompleteSelectMode() { 489 | o.inSelectMode = false 490 | o.candidateChoice = -1 491 | } 492 | 493 | func (o *opCompleter) ExitCompleteMode(revent bool) { 494 | o.inCompleteMode.Store(0) 495 | o.candidate = nil 496 | o.candidateOff = -1 497 | o.candidateSource = nil 498 | o.ExitCompleteSelectMode() 499 | } 500 | -------------------------------------------------------------------------------- /complete_helper.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/ergochat/readline/internal/runes" 8 | ) 9 | 10 | // PrefixCompleter implements AutoCompleter via a recursive tree. 11 | type PrefixCompleter struct { 12 | // Name is the name of a command, subcommand, or argument eligible for completion. 13 | Name string 14 | // Callback is optional; if defined, it takes the current line and returns 15 | // a list of possible completions associated with the current node (i.e. 16 | // in place of Name). 17 | Callback func(string) []string 18 | // Children is a list of possible completions that can follow the current node. 19 | Children []*PrefixCompleter 20 | 21 | nameRunes []rune // just a cache 22 | } 23 | 24 | var _ AutoCompleter = (*PrefixCompleter)(nil) 25 | 26 | func (p *PrefixCompleter) Tree(prefix string) string { 27 | buf := bytes.NewBuffer(nil) 28 | p.print(prefix, 0, buf) 29 | return buf.String() 30 | } 31 | 32 | func prefixPrint(p *PrefixCompleter, prefix string, level int, buf *bytes.Buffer) { 33 | if strings.TrimSpace(p.Name) != "" { 34 | buf.WriteString(prefix) 35 | if level > 0 { 36 | buf.WriteString("├") 37 | buf.WriteString(strings.Repeat("─", (level*4)-2)) 38 | buf.WriteString(" ") 39 | } 40 | buf.WriteString(p.Name) 41 | buf.WriteByte('\n') 42 | level++ 43 | } 44 | for _, ch := range p.Children { 45 | ch.print(prefix, level, buf) 46 | } 47 | } 48 | 49 | func (p *PrefixCompleter) print(prefix string, level int, buf *bytes.Buffer) { 50 | prefixPrint(p, prefix, level, buf) 51 | } 52 | 53 | func (p *PrefixCompleter) getName() []rune { 54 | if p.nameRunes == nil { 55 | if p.Name != "" { 56 | p.nameRunes = []rune(p.Name) 57 | } else { 58 | p.nameRunes = make([]rune, 0) 59 | } 60 | } 61 | return p.nameRunes 62 | } 63 | 64 | func (p *PrefixCompleter) getDynamicNames(line []rune) [][]rune { 65 | var result [][]rune 66 | for _, name := range p.Callback(string(line)) { 67 | nameRunes := []rune(name) 68 | nameRunes = append(nameRunes, ' ') 69 | result = append(result, nameRunes) 70 | } 71 | return result 72 | } 73 | 74 | func (p *PrefixCompleter) SetChildren(children []*PrefixCompleter) { 75 | p.Children = children 76 | } 77 | 78 | func NewPrefixCompleter(pc ...*PrefixCompleter) *PrefixCompleter { 79 | return PcItem("", pc...) 80 | } 81 | 82 | func PcItem(name string, pc ...*PrefixCompleter) *PrefixCompleter { 83 | name += " " 84 | result := &PrefixCompleter{ 85 | Name: name, 86 | Children: pc, 87 | } 88 | result.getName() // initialize nameRunes member 89 | return result 90 | } 91 | 92 | func PcItemDynamic(callback func(string) []string, pc ...*PrefixCompleter) *PrefixCompleter { 93 | return &PrefixCompleter{ 94 | Callback: callback, 95 | Children: pc, 96 | } 97 | } 98 | 99 | func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { 100 | return doInternal(p, line, pos, line) 101 | } 102 | 103 | func doInternal(p *PrefixCompleter, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) { 104 | line = runes.TrimSpaceLeft(line[:pos]) 105 | goNext := false 106 | var lineCompleter *PrefixCompleter 107 | for _, child := range p.Children { 108 | var childNames [][]rune 109 | if child.Callback != nil { 110 | childNames = child.getDynamicNames(origLine) 111 | } else { 112 | childNames = make([][]rune, 1) 113 | childNames[0] = child.getName() 114 | } 115 | 116 | for _, childName := range childNames { 117 | if len(line) >= len(childName) { 118 | if runes.HasPrefix(line, childName) { 119 | if len(line) == len(childName) { 120 | newLine = append(newLine, []rune{' '}) 121 | } else { 122 | newLine = append(newLine, childName) 123 | } 124 | offset = len(childName) 125 | lineCompleter = child 126 | goNext = true 127 | } 128 | } else { 129 | if runes.HasPrefix(childName, line) { 130 | newLine = append(newLine, childName[len(line):]) 131 | offset = len(line) 132 | lineCompleter = child 133 | } 134 | } 135 | } 136 | } 137 | 138 | if len(newLine) != 1 { 139 | return 140 | } 141 | 142 | tmpLine := make([]rune, 0, len(line)) 143 | for i := offset; i < len(line); i++ { 144 | if line[i] == ' ' { 145 | continue 146 | } 147 | 148 | tmpLine = append(tmpLine, line[i:]...) 149 | return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine) 150 | } 151 | 152 | if goNext { 153 | return doInternal(lineCompleter, nil, 0, origLine) 154 | } 155 | return 156 | } 157 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.3] - 2024-09-02 4 | 5 | * It is now possible to select and navigate through tab-completion candidates in pager mode (#65, #66, thanks [@YangchenYe323](https://github.com/YangchenYe323)!) 6 | * Fixed Home and End keys in certain terminals and multiplexers (#67, #68) 7 | * Fixed crashing edge cases in the Ctrl-T "transpose characters" operation (#69, #70) 8 | * Removed `(Config).ForceUseInteractive`; instead `(Config).FuncIsTerminal` can be set to `func() bool { return true }` 9 | 10 | ## [0.1.2] - 2024-07-04 11 | 12 | * Fixed skipping between words with Alt+{Left,Right} and Alt+{b,f} (#59, #63) 13 | * Fixed `FuncFilterInputRune` support (#61, thanks [@sohomdatta1](https://github.com/sohomdatta1)!) 14 | 15 | ## [0.1.1] - 2024-05-06 16 | 17 | * Fixed zos support (#55) 18 | * Added support for the Home and End keys (#53) 19 | * Removed some internal enums related to Vim mode from the public API (#57) 20 | 21 | ## [0.1.0] - 2024-01-14 22 | 23 | * Added optional undo support with Ctrl+_ ; this must be enabled manually by setting `(Config).Undo` to `true` 24 | * Removed `PrefixCompleterInterface` in favor of the concrete type `*PrefixCompleter` (most client code that explicitly uses `PrefixCompleterInterface` can simply substitute `*PrefixCompleter`) 25 | * Fixed a Windows-specific bug where backspace from the screen edge erased an extra line from the screen (#35) 26 | * Removed `(PrefixCompleter).Dynamic`, which was redundant with `(PrefixCompleter).Callback` 27 | * Removed `SegmentCompleter` and related APIs (users can still define their own `AutoCompleter` implementations, including by vendoring `SegmentCompleter`) 28 | * Removed `(Config).UniqueEditLine` 29 | * Removed public `Do` and `Print` functions 30 | * Fixed a case where the search menu remained visible after exiting search mode (#38, #40) 31 | * Fixed a data race on async writes in complete mode (#30) 32 | 33 | ## [0.0.6] - 2023-11-06 34 | 35 | * Added `(*Instance).ClearScreen` (#36, #37) 36 | * Removed `(*Instance).Clean` (#37) 37 | 38 | ## [0.0.5] -- 2023-06-02 39 | 40 | No public API changes. 41 | 42 | ## [v0.0.4] -- 2023-06-02 43 | 44 | * Fixed panic on Ctrl-S followed by Ctrl-C (#32) 45 | * Fixed data races around history search (#29) 46 | * Added `(*Instance).ReadLine` as the preferred name (`Readline` is still accepted as an alias) (#29) 47 | * `Listener` and `Painter` are now function types instead of interfaces (#29) 48 | * Cleanups and renames for some relatively obscure APIs (#28, #29) 49 | 50 | ## [v0.0.3] -- 2023-04-17 51 | 52 | * Added `(*Instance).SetDefault` to replace `FillStdin` and `WriteStdin` (#24) 53 | * Fixed Delete key on an empty line causing the prompt to exit (#14) 54 | * Fixed double draw of prompt on `ReadlineWithDefault` (#24) 55 | * Hide `Operation`, `Terminal`, `RuneBuffer`, and others from the public API (#18) 56 | 57 | ## [v0.0.2] -- 2023-03-27 58 | 59 | * Fixed overwriting existing text on the same line as the prompt (d9af5677814a) 60 | * Fixed wide character handling, including emoji (d9af5677814a) 61 | * Fixed numerous UI race conditions (62ab2cfd1794, 3bfb569368b4, 4d842a2fe366) 62 | * Added a pager for completion candidates (76ae9696abd5) 63 | * Removed ANSI translation layer on Windows, instead enabling native ANSI support; this fixes a crash (#2) 64 | * Fixed Ctrl-Z suspend and resume (#17) 65 | * Fixed handling of Shift-Tab (#16) 66 | * Fixed word deletion at the beginning of the line deleting the entire line (#11) 67 | * Fixed a nil dereference from `SetConfig` (#3) 68 | * Added zos support (#10) 69 | * Cleanups and renames for many relatively obscure APIs (#3, #9) 70 | 71 | ## [v0.0.1] 72 | 73 | v0.0.1 is the upstream repository [chzyer/readline](https://github.com/chzyer/readline/)'s final public release [v1.5.1](https://github.com/chzyer/readline/releases/tag/v1.5.1). 74 | -------------------------------------------------------------------------------- /docs/MIGRATING.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | ergochat/readline is largely API-compatible with the most commonly used functionality of chzyer/readline. See our [godoc page](https://pkg.go.dev/github.com/ergochat/readline) for the current state of the public API; if an API you were using has been removed, its replacement may be readily apparent. 4 | 5 | Here are some guidelines for APIs that have been removed or changed: 6 | 7 | * readline used to expose most of `golang.org/x/term`, e.g. `readline.IsTerminal` and `readline.GetSize`, as part of its public API; these functions are no longer exposed. We recommend importing `golang.org/x/term` itself as a replacement. 8 | * Various APIs that allowed manipulating the instance's configuration directly (e.g. `(*Instance).SetMaskRune`) have been removed. We recommend using `(*Instance).SetConfig` instead. 9 | * The preferred name for `NewEx` is now `NewFromConfig` (`NewEx` is provided as a compatibility alias). 10 | * The preferred name for `(*Instance).Readline` is now `ReadLine` (`Readline` is provided as a compatibility alias). 11 | * `PrefixCompleterInterface` was removed in favor of exposing `PrefixCompleter` as a concrete struct type. In general, references to `PrefixCompleterInterface` can be changed to `*PrefixCompleter`. 12 | * `(Config).ForceUseInteractive` has been removed. Instead, set `(Config).FuncIsTerminal` to `func() bool { return true }`. 13 | -------------------------------------------------------------------------------- /docs/shortcut.md: -------------------------------------------------------------------------------- 1 | ## Readline Shortcut 2 | 3 | `Meta`+`B` means press `Esc` and `n` separately. 4 | Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` 5 | Notice: `Meta`+`B` is equals with `Alt`+`B` in windows. 6 | 7 | * Shortcut in normal mode 8 | 9 | | Shortcut | Comment | 10 | | ------------------ | --------------------------------- | 11 | | `Ctrl`+`A` | Beginning of line | 12 | | `Ctrl`+`B` / `←` | Backward one character | 13 | | `Meta`+`B` | Backward one word | 14 | | `Ctrl`+`C` | Send io.EOF | 15 | | `Ctrl`+`D` | Delete one character | 16 | | `Meta`+`D` | Delete one word | 17 | | `Ctrl`+`E` | End of line | 18 | | `Ctrl`+`F` / `→` | Forward one character | 19 | | `Meta`+`F` | Forward one word | 20 | | `Ctrl`+`G` | Cancel | 21 | | `Ctrl`+`H` | Delete previous character | 22 | | `Ctrl`+`I` / `Tab` | Command line completion | 23 | | `Ctrl`+`J` | Line feed | 24 | | `Ctrl`+`K` | Cut text to the end of line | 25 | | `Ctrl`+`L` | Clear screen | 26 | | `Ctrl`+`M` | Same as Enter key | 27 | | `Ctrl`+`N` / `↓` | Next line (in history) | 28 | | `Ctrl`+`P` / `↑` | Prev line (in history) | 29 | | `Ctrl`+`R` | Search backwards in history | 30 | | `Ctrl`+`S` | Search forwards in history | 31 | | `Ctrl`+`T` | Transpose characters | 32 | | `Meta`+`T` | Transpose words (TODO) | 33 | | `Ctrl`+`U` | Cut text to the beginning of line | 34 | | `Ctrl`+`W` | Cut previous word | 35 | | `Backspace` | Delete previous character | 36 | | `Meta`+`Backspace` | Cut previous word | 37 | | `Enter` | Line feed | 38 | 39 | 40 | * Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode) 41 | 42 | | Shortcut | Comment | 43 | | ----------------------- | --------------------------------------- | 44 | | `Ctrl`+`S` | Search forwards in history | 45 | | `Ctrl`+`R` | Search backwards in history | 46 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history | 47 | | `Backspace` | Delete previous character | 48 | | Other | Exit Search Mode | 49 | 50 | * Shortcut in Complete Select Mode (double `Tab` to enter this mode) 51 | 52 | | Shortcut | Comment | 53 | | ----------------------- | ---------------------------------------- | 54 | | `Ctrl`+`F` | Move Forward | 55 | | `Ctrl`+`B` | Move Backward | 56 | | `Ctrl`+`N` | Move to next line | 57 | | `Ctrl`+`P` | Move to previous line | 58 | | `Ctrl`+`A` | Move to the first candicate in current line | 59 | | `Ctrl`+`E` | Move to the last candicate in current line | 60 | | `Tab` / `Enter` | Use the word on cursor to complete | 61 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode | 62 | | Other | Exit Complete Select Mode | -------------------------------------------------------------------------------- /example/readline-demo/readline-demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "math/rand" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/ergochat/readline" 14 | ) 15 | 16 | func usage(w io.Writer) { 17 | io.WriteString(w, "commands:\n") 18 | io.WriteString(w, completer.Tree(" ")) 19 | } 20 | 21 | // Function constructor - constructs new function for listing given directory 22 | func listFiles(path string) func(string) []string { 23 | return func(line string) []string { 24 | names := make([]string, 0) 25 | files, _ := ioutil.ReadDir(path) 26 | for _, f := range files { 27 | names = append(names, f.Name()) 28 | } 29 | return names 30 | } 31 | } 32 | 33 | var completer = readline.NewPrefixCompleter( 34 | readline.PcItem("mode", 35 | readline.PcItem("vi"), 36 | readline.PcItem("emacs"), 37 | ), 38 | readline.PcItem("login"), 39 | readline.PcItem("say", 40 | readline.PcItemDynamic(listFiles("./"), 41 | readline.PcItem("with", 42 | readline.PcItem("following"), 43 | readline.PcItem("items"), 44 | ), 45 | ), 46 | readline.PcItem("hello"), 47 | readline.PcItem("bye"), 48 | ), 49 | readline.PcItem("setprompt"), 50 | readline.PcItem("setpassword"), 51 | readline.PcItem("bye"), 52 | readline.PcItem("help"), 53 | readline.PcItem("go", 54 | readline.PcItem("build", readline.PcItem("-o"), readline.PcItem("-v")), 55 | readline.PcItem("install", 56 | readline.PcItem("-v"), 57 | readline.PcItem("-vv"), 58 | readline.PcItem("-vvv"), 59 | ), 60 | readline.PcItem("test"), 61 | ), 62 | readline.PcItem("sleep"), 63 | readline.PcItem("async"), 64 | readline.PcItem("clear"), 65 | ) 66 | 67 | func filterInput(r rune) (rune, bool) { 68 | switch r { 69 | // block CtrlZ feature 70 | case readline.CharCtrlZ: 71 | return r, false 72 | } 73 | return r, true 74 | } 75 | 76 | func asyncSleep(instance *readline.Instance, min, max time.Duration) { 77 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 78 | for { 79 | duration := min + time.Duration(int64(float64(max-min)*r.Float64())) 80 | time.Sleep(duration) 81 | fmt.Fprintf(instance, "Slept for %v\n", duration) 82 | } 83 | } 84 | 85 | func asyncClose(instance *readline.Instance, line string) { 86 | duration := 3 * time.Second 87 | if fields := strings.Fields(line); len(fields) >= 2 { 88 | if dur, err := time.ParseDuration(fields[1]); err == nil { 89 | duration = dur 90 | } 91 | } 92 | time.Sleep(duration) 93 | instance.Close() 94 | } 95 | 96 | func main() { 97 | l, err := readline.NewFromConfig(&readline.Config{ 98 | Prompt: "\033[31m»\033[0m ", 99 | HistoryFile: "/tmp/readline.tmp", 100 | AutoComplete: completer, 101 | InterruptPrompt: "^C", 102 | EOFPrompt: "exit", 103 | 104 | HistorySearchFold: true, 105 | FuncFilterInputRune: filterInput, 106 | 107 | Undo: true, 108 | }) 109 | if err != nil { 110 | panic(err) 111 | } 112 | defer l.Close() 113 | l.CaptureExitSignal() 114 | 115 | setPasswordCfg := l.GeneratePasswordConfig() 116 | setPasswordCfg.Listener = func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 117 | l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line))) 118 | l.Refresh() 119 | return nil, 0, false 120 | } 121 | 122 | log.SetOutput(l.Stderr()) 123 | asyncStarted := false 124 | for { 125 | line, err := l.Readline() 126 | if err == readline.ErrInterrupt { 127 | if len(line) == 0 { 128 | break 129 | } else { 130 | continue 131 | } 132 | } else if err == io.EOF { 133 | break 134 | } 135 | 136 | line = strings.TrimSpace(line) 137 | switch { 138 | case strings.HasPrefix(line, "mode "): 139 | switch line[5:] { 140 | case "vi": 141 | l.SetVimMode(true) 142 | case "emacs": 143 | l.SetVimMode(false) 144 | default: 145 | println("invalid mode:", line[5:]) 146 | } 147 | case line == "mode": 148 | if l.IsVimMode() { 149 | println("current mode: vim") 150 | } else { 151 | println("current mode: emacs") 152 | } 153 | case line == "login": 154 | pswd, err := l.ReadPassword("please enter your password: ") 155 | if err != nil { 156 | break 157 | } 158 | println("you enter:", strconv.Quote(string(pswd))) 159 | case line == "help": 160 | usage(l.Stderr()) 161 | case line == "setpassword": 162 | pswd, err := l.ReadLineWithConfig(setPasswordCfg) 163 | if err == nil { 164 | println("you set:", strconv.Quote(string(pswd))) 165 | } 166 | case strings.HasPrefix(line, "setprompt"): 167 | if len(line) <= 10 { 168 | log.Println("setprompt ") 169 | break 170 | } 171 | l.SetPrompt(line[10:]) 172 | case strings.HasPrefix(line, "say"): 173 | line := strings.TrimSpace(line[3:]) 174 | if len(line) == 0 { 175 | log.Println("say what?") 176 | break 177 | } 178 | go func() { 179 | for range time.Tick(time.Second) { 180 | log.Println(line) 181 | } 182 | }() 183 | case strings.HasPrefix(line, "echo "): 184 | preArg := strings.TrimSpace(strings.TrimPrefix(line, "echo ")) 185 | arg := strings.TrimPrefix(preArg, "-n ") 186 | out := []byte(arg) 187 | if preArg == arg { 188 | out = append(out, '\n') 189 | } 190 | l.Write(out) 191 | case line == "bye": 192 | goto exit 193 | case line == "sleep": 194 | log.Println("sleep 4 second") 195 | time.Sleep(4 * time.Second) 196 | case line == "async": 197 | if !asyncStarted { 198 | asyncStarted = true 199 | log.Println("initialize async sleep/write") 200 | go asyncSleep(l, time.Second, 3*time.Second) 201 | } else { 202 | log.Println("async writes already started") 203 | } 204 | case line == "clear": 205 | l.ClearScreen() 206 | case strings.HasPrefix(line, "close"): 207 | go asyncClose(l, line) 208 | case line == "": 209 | default: 210 | log.Println("you said:", strconv.Quote(line)) 211 | } 212 | } 213 | exit: 214 | } 215 | -------------------------------------------------------------------------------- /example/readline-im/README.md: -------------------------------------------------------------------------------- 1 | # readline-im 2 | 3 | ![readline-im](https://dl.dropboxusercontent.com/s/52hc7bo92g3pgi5/03F93B8D-9B4B-4D35-BBAA-22FBDAC7F299-26173-000164AA33980001.gif?dl=0) 4 | -------------------------------------------------------------------------------- /example/readline-multiline/readline-multiline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/ergochat/readline" 7 | ) 8 | 9 | func main() { 10 | rl, err := readline.NewEx(&readline.Config{ 11 | Prompt: "> ", 12 | HistoryFile: "/tmp/readline-multiline", 13 | DisableAutoSaveHistory: true, 14 | }) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer rl.Close() 19 | 20 | var cmds []string 21 | for { 22 | line, err := rl.Readline() 23 | if err != nil { 24 | break 25 | } 26 | line = strings.TrimSpace(line) 27 | if len(line) == 0 { 28 | continue 29 | } 30 | cmds = append(cmds, line) 31 | if !strings.HasSuffix(line, ";") { 32 | rl.SetPrompt(">>> ") 33 | continue 34 | } 35 | cmd := strings.Join(cmds, " ") 36 | cmds = cmds[:0] 37 | rl.SetPrompt("> ") 38 | rl.SaveToHistory(cmd) 39 | println(cmd) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/readline-paged-completion/readline-paged-completion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "math/rand" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/ergochat/readline" 12 | ) 13 | 14 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 15 | 16 | func randSeq(n int) string { 17 | b := make([]rune, n) 18 | for i := range b { 19 | b[i] = letters[rand.Intn(len(letters))] 20 | } 21 | return string(b) 22 | } 23 | 24 | // A completor that will give a lot of completions for showcasing the paging functionality 25 | type Completor struct{} 26 | 27 | func (c *Completor) Do(line []rune, pos int) ([][]rune, int) { 28 | completion := make([][]rune, 0, 10000) 29 | for i := 0; i < 1000; i += 1 { 30 | var s string 31 | if i%2 == 0 { 32 | s = fmt.Sprintf("%s%05d", randSeq(1), i) 33 | } else if i%3 == 0 { 34 | s = fmt.Sprintf("%s%010d", randSeq(1), i) 35 | } else { 36 | s = fmt.Sprintf("%s%07d", randSeq(1), i) 37 | } 38 | completion = append(completion, []rune(s)) 39 | } 40 | return completion, pos 41 | } 42 | 43 | func main() { 44 | c := Completor{} 45 | l, err := readline.NewEx(&readline.Config{ 46 | Prompt: "\033[31m»\033[0m ", 47 | AutoComplete: &c, 48 | InterruptPrompt: "^C", 49 | EOFPrompt: "exit", 50 | }) 51 | if err != nil { 52 | panic(err) 53 | } 54 | defer l.Close() 55 | for { 56 | line, err := l.Readline() 57 | if err == readline.ErrInterrupt { 58 | if len(line) == 0 { 59 | break 60 | } else { 61 | continue 62 | } 63 | } else if err == io.EOF { 64 | break 65 | } 66 | 67 | line = strings.TrimSpace(line) 68 | switch { 69 | default: 70 | log.Println("you said:", strconv.Quote(line)) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/readline-pass-strength/readline-pass-strength.go: -------------------------------------------------------------------------------- 1 | // This is a small example using readline to read a password 2 | // and check it's strength while typing using the zxcvbn library. 3 | // Depending on the strength the prompt is colored nicely to indicate strength. 4 | // 5 | // This file is licensed under the WTFPL: 6 | // 7 | // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 8 | // Version 2, December 2004 9 | // 10 | // Copyright (C) 2004 Sam Hocevar 11 | // 12 | // Everyone is permitted to copy and distribute verbatim or modified 13 | // copies of this license document, and changing it is allowed as long 14 | // as the name is changed. 15 | // 16 | // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 17 | // TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 18 | // 19 | // 0. You just DO WHAT THE FUCK YOU WANT TO. 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/ergochat/readline" 26 | ) 27 | 28 | const ( 29 | Cyan = 36 30 | Green = 32 31 | Magenta = 35 32 | Red = 31 33 | Yellow = 33 34 | BackgroundRed = 41 35 | ) 36 | 37 | // Reset sequence 38 | var ColorResetEscape = "\033[0m" 39 | 40 | // ColorResetEscape translates a ANSI color number to a color escape. 41 | func ColorEscape(color int) string { 42 | return fmt.Sprintf("\033[0;%dm", color) 43 | } 44 | 45 | // Colorize the msg using ANSI color escapes 46 | func Colorize(msg string, color int) string { 47 | return ColorEscape(color) + msg + ColorResetEscape 48 | } 49 | 50 | func createStrengthPrompt(password []rune) string { 51 | symbol, color := "", Red 52 | 53 | switch { 54 | case len(password) <= 1: 55 | symbol = "✗" 56 | color = Red 57 | case len(password) <= 3: 58 | symbol = "⚡" 59 | color = Magenta 60 | case len(password) <= 5: 61 | symbol = "⚠" 62 | color = Yellow 63 | default: 64 | symbol = "✔" 65 | color = Green 66 | } 67 | 68 | prompt := Colorize(symbol, color) 69 | prompt += Colorize(" ENT", Cyan) 70 | 71 | prompt += Colorize(" New Password: ", color) 72 | return prompt 73 | } 74 | 75 | func main() { 76 | rl, err := readline.New("") 77 | if err != nil { 78 | return 79 | } 80 | defer rl.Close() 81 | 82 | setPasswordCfg := rl.GeneratePasswordConfig() 83 | setPasswordCfg.Listener = func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 84 | rl.SetPrompt(createStrengthPrompt(line)) 85 | rl.Refresh() 86 | return nil, 0, false 87 | } 88 | 89 | pswd, err := rl.ReadLineWithConfig(setPasswordCfg) 90 | if err != nil { 91 | return 92 | } 93 | 94 | fmt.Println("Your password was:", string(pswd)) 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ergochat/readline 2 | 3 | go 1.19 4 | 5 | require ( 6 | golang.org/x/sys v0.15.0 7 | golang.org/x/text v0.9.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 2 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 3 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 4 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 5 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 6 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 7 | -------------------------------------------------------------------------------- /history.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "container/list" 6 | "os" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/ergochat/readline/internal/runes" 11 | ) 12 | 13 | type hisItem struct { 14 | Source []rune 15 | Version int64 16 | Tmp []rune 17 | } 18 | 19 | func (h *hisItem) Clean() { 20 | h.Source = nil 21 | h.Tmp = nil 22 | } 23 | 24 | type opHistory struct { 25 | operation *operation 26 | history *list.List 27 | historyVer int64 28 | current *list.Element 29 | fd *os.File 30 | fdLock sync.Mutex 31 | enable bool 32 | } 33 | 34 | func newOpHistory(operation *operation) (o *opHistory) { 35 | o = &opHistory{ 36 | operation: operation, 37 | history: list.New(), 38 | enable: true, 39 | } 40 | o.initHistory() 41 | return o 42 | } 43 | 44 | func (o *opHistory) isEnabled() bool { 45 | return o.enable && o.operation.GetConfig().HistoryLimit > 0 46 | } 47 | 48 | func (o *opHistory) Reset() { 49 | o.history = list.New() 50 | o.current = nil 51 | } 52 | 53 | func (o *opHistory) initHistory() { 54 | cfg := o.operation.GetConfig() 55 | if cfg.HistoryFile != "" { 56 | o.historyUpdatePath(cfg) 57 | } 58 | } 59 | 60 | // only called by newOpHistory 61 | func (o *opHistory) historyUpdatePath(cfg *Config) { 62 | o.fdLock.Lock() 63 | defer o.fdLock.Unlock() 64 | f, err := os.OpenFile(cfg.HistoryFile, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) 65 | if err != nil { 66 | return 67 | } 68 | o.fd = f 69 | r := bufio.NewReader(o.fd) 70 | total := 0 71 | for ; ; total++ { 72 | line, err := r.ReadString('\n') 73 | if err != nil { 74 | break 75 | } 76 | // ignore the empty line 77 | line = strings.TrimSpace(line) 78 | if len(line) == 0 { 79 | continue 80 | } 81 | o.Push([]rune(line)) 82 | o.Compact() 83 | } 84 | if total > cfg.HistoryLimit { 85 | o.rewriteLocked() 86 | } 87 | o.historyVer++ 88 | o.Push(nil) 89 | return 90 | } 91 | 92 | func (o *opHistory) Compact() { 93 | cfg := o.operation.GetConfig() 94 | for o.history.Len() > cfg.HistoryLimit && o.history.Len() > 0 { 95 | o.history.Remove(o.history.Front()) 96 | } 97 | } 98 | 99 | func (o *opHistory) Rewrite() { 100 | o.fdLock.Lock() 101 | defer o.fdLock.Unlock() 102 | o.rewriteLocked() 103 | } 104 | 105 | func (o *opHistory) rewriteLocked() { 106 | cfg := o.operation.GetConfig() 107 | if cfg.HistoryFile == "" { 108 | return 109 | } 110 | 111 | tmpFile := cfg.HistoryFile + ".tmp" 112 | fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666) 113 | if err != nil { 114 | return 115 | } 116 | 117 | buf := bufio.NewWriter(fd) 118 | for elem := o.history.Front(); elem != nil; elem = elem.Next() { 119 | buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n") 120 | } 121 | buf.Flush() 122 | 123 | // replace history file 124 | if err = os.Rename(tmpFile, cfg.HistoryFile); err != nil { 125 | fd.Close() 126 | return 127 | } 128 | 129 | if o.fd != nil { 130 | o.fd.Close() 131 | } 132 | // fd is write only, just satisfy what we need. 133 | o.fd = fd 134 | } 135 | 136 | func (o *opHistory) Close() { 137 | o.fdLock.Lock() 138 | defer o.fdLock.Unlock() 139 | if o.fd != nil { 140 | o.fd.Close() 141 | } 142 | } 143 | 144 | func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) { 145 | for elem := o.current; elem != nil; elem = elem.Prev() { 146 | item := o.showItem(elem.Value) 147 | if isNewSearch { 148 | start += len(rs) 149 | } 150 | if elem == o.current { 151 | if len(item) >= start { 152 | item = item[:start] 153 | } 154 | } 155 | idx := runes.IndexAllBckEx(item, rs, o.operation.GetConfig().HistorySearchFold) 156 | if idx < 0 { 157 | continue 158 | } 159 | return idx, elem 160 | } 161 | return -1, nil 162 | } 163 | 164 | func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) { 165 | for elem := o.current; elem != nil; elem = elem.Next() { 166 | item := o.showItem(elem.Value) 167 | if isNewSearch { 168 | start -= len(rs) 169 | if start < 0 { 170 | start = 0 171 | } 172 | } 173 | if elem == o.current { 174 | if len(item)-1 >= start { 175 | item = item[start:] 176 | } else { 177 | continue 178 | } 179 | } 180 | idx := runes.IndexAllEx(item, rs, o.operation.GetConfig().HistorySearchFold) 181 | if idx < 0 { 182 | continue 183 | } 184 | if elem == o.current { 185 | idx += start 186 | } 187 | return idx, elem 188 | } 189 | return -1, nil 190 | } 191 | 192 | func (o *opHistory) showItem(obj interface{}) []rune { 193 | item := obj.(*hisItem) 194 | if item.Version == o.historyVer { 195 | return item.Tmp 196 | } 197 | return item.Source 198 | } 199 | 200 | func (o *opHistory) Prev() []rune { 201 | if o.current == nil { 202 | return nil 203 | } 204 | current := o.current.Prev() 205 | if current == nil { 206 | return nil 207 | } 208 | o.current = current 209 | return runes.Copy(o.showItem(current.Value)) 210 | } 211 | 212 | func (o *opHistory) Next() ([]rune, bool) { 213 | if o.current == nil { 214 | return nil, false 215 | } 216 | current := o.current.Next() 217 | if current == nil { 218 | return nil, false 219 | } 220 | 221 | o.current = current 222 | return runes.Copy(o.showItem(current.Value)), true 223 | } 224 | 225 | // Disable the current history 226 | func (o *opHistory) Disable() { 227 | o.enable = false 228 | } 229 | 230 | // Enable the current history 231 | func (o *opHistory) Enable() { 232 | o.enable = true 233 | } 234 | 235 | func (o *opHistory) debug() { 236 | debugPrint("-------") 237 | for item := o.history.Front(); item != nil; item = item.Next() { 238 | debugPrint("%+v", item.Value) 239 | } 240 | } 241 | 242 | // save history 243 | func (o *opHistory) New(current []rune) (err error) { 244 | if !o.isEnabled() { 245 | return nil 246 | } 247 | 248 | current = runes.Copy(current) 249 | 250 | // if just use last command without modify 251 | // just clean lastest history 252 | if back := o.history.Back(); back != nil { 253 | prev := back.Prev() 254 | if prev != nil { 255 | if runes.Equal(current, prev.Value.(*hisItem).Source) { 256 | o.current = o.history.Back() 257 | o.current.Value.(*hisItem).Clean() 258 | o.historyVer++ 259 | return nil 260 | } 261 | } 262 | } 263 | 264 | if len(current) == 0 { 265 | o.current = o.history.Back() 266 | if o.current != nil { 267 | o.current.Value.(*hisItem).Clean() 268 | o.historyVer++ 269 | return nil 270 | } 271 | } 272 | 273 | if o.current != o.history.Back() { 274 | // move history item to current command 275 | currentItem := o.current.Value.(*hisItem) 276 | // set current to last item 277 | o.current = o.history.Back() 278 | 279 | current = runes.Copy(currentItem.Tmp) 280 | } 281 | 282 | // err only can be a IO error, just report 283 | err = o.Update(current, true) 284 | 285 | // push a new one to commit current command 286 | o.historyVer++ 287 | o.Push(nil) 288 | return 289 | } 290 | 291 | func (o *opHistory) Revert() { 292 | o.historyVer++ 293 | o.current = o.history.Back() 294 | } 295 | 296 | func (o *opHistory) Update(s []rune, commit bool) (err error) { 297 | if !o.isEnabled() { 298 | return nil 299 | } 300 | 301 | o.fdLock.Lock() 302 | defer o.fdLock.Unlock() 303 | s = runes.Copy(s) 304 | if o.current == nil { 305 | o.Push(s) 306 | o.Compact() 307 | return 308 | } 309 | r := o.current.Value.(*hisItem) 310 | r.Version = o.historyVer 311 | if commit { 312 | r.Source = s 313 | if o.fd != nil { 314 | // just report the error 315 | _, err = o.fd.Write([]byte(string(r.Source) + "\n")) 316 | } 317 | } else { 318 | r.Tmp = append(r.Tmp[:0], s...) 319 | } 320 | o.current.Value = r 321 | o.Compact() 322 | return 323 | } 324 | 325 | func (o *opHistory) Push(s []rune) { 326 | s = runes.Copy(s) 327 | elem := o.history.PushBack(&hisItem{Source: s}) 328 | o.current = elem 329 | } 330 | -------------------------------------------------------------------------------- /internal/ansi/ansi.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package ansi 4 | 5 | func EnableANSI() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /internal/ansi/ansi_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | Copyright (c) Jason Walton (https://www.thedreaming.org) 5 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 6 | 7 | Released under the MIT License: 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | */ 27 | 28 | package ansi 29 | 30 | import ( 31 | "sync" 32 | 33 | "golang.org/x/sys/windows" 34 | ) 35 | 36 | var ( 37 | ansiErr error 38 | ansiOnce sync.Once 39 | ) 40 | 41 | func EnableANSI() error { 42 | ansiOnce.Do(func() { 43 | ansiErr = realEnableANSI() 44 | }) 45 | return ansiErr 46 | } 47 | 48 | func realEnableANSI() error { 49 | // We want to enable the following modes, if they are not already set: 50 | // ENABLE_VIRTUAL_TERMINAL_PROCESSING on stdout (color support) 51 | // ENABLE_VIRTUAL_TERMINAL_INPUT on stdin (ansi input sequences) 52 | // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences 53 | if err := windowsSetMode(windows.STD_OUTPUT_HANDLE, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { 54 | return err 55 | } 56 | if err := windowsSetMode(windows.STD_INPUT_HANDLE, windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | func windowsSetMode(stdhandle uint32, modeFlag uint32) (err error) { 63 | handle, err := windows.GetStdHandle(stdhandle) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // Get the existing console mode. 69 | var mode uint32 70 | err = windows.GetConsoleMode(handle, &mode) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Enable the mode if it is not currently set 76 | if mode&modeFlag != modeFlag { 77 | mode = mode | modeFlag 78 | err = windows.SetConsoleMode(handle, mode) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/platform/utils_unix.go: -------------------------------------------------------------------------------- 1 | //go:build aix || darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd || os400 || solaris || zos 2 | 3 | package platform 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | 12 | "github.com/ergochat/readline/internal/term" 13 | ) 14 | 15 | const ( 16 | IsWindows = false 17 | ) 18 | 19 | // SuspendProcess suspends the process with SIGTSTP, 20 | // then blocks until it is resumed. 21 | func SuspendProcess() { 22 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGCONT) 23 | defer stop() 24 | 25 | p, err := os.FindProcess(os.Getpid()) 26 | if err != nil { 27 | panic(err) 28 | } 29 | p.Signal(syscall.SIGTSTP) 30 | // wait for SIGCONT 31 | <-ctx.Done() 32 | } 33 | 34 | // getWidthHeight of the terminal using given file descriptor 35 | func getWidthHeight(stdoutFd int) (width int, height int) { 36 | width, height, err := term.GetSize(stdoutFd) 37 | if err != nil { 38 | return -1, -1 39 | } 40 | return 41 | } 42 | 43 | // GetScreenSize returns the width/height of the terminal or -1,-1 or error 44 | func GetScreenSize() (width int, height int) { 45 | width, height = getWidthHeight(syscall.Stdout) 46 | if width < 0 { 47 | width, height = getWidthHeight(syscall.Stderr) 48 | } 49 | return 50 | } 51 | 52 | func DefaultIsTerminal() bool { 53 | return term.IsTerminal(syscall.Stdin) && (term.IsTerminal(syscall.Stdout) || term.IsTerminal(syscall.Stderr)) 54 | } 55 | 56 | // ----------------------------------------------------------------------------- 57 | 58 | var ( 59 | sizeChange sync.Once 60 | sizeChangeCallback func() 61 | ) 62 | 63 | func DefaultOnWidthChanged(f func()) { 64 | DefaultOnSizeChanged(f) 65 | } 66 | 67 | func DefaultOnSizeChanged(f func()) { 68 | sizeChangeCallback = f 69 | sizeChange.Do(func() { 70 | ch := make(chan os.Signal, 1) 71 | signal.Notify(ch, syscall.SIGWINCH) 72 | 73 | go func() { 74 | for { 75 | _, ok := <-ch 76 | if !ok { 77 | break 78 | } 79 | sizeChangeCallback() 80 | } 81 | }() 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /internal/platform/utils_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package platform 4 | 5 | import ( 6 | "syscall" 7 | 8 | "github.com/ergochat/readline/internal/term" 9 | ) 10 | 11 | const ( 12 | IsWindows = true 13 | ) 14 | 15 | func SuspendProcess() { 16 | } 17 | 18 | // GetScreenSize returns the width, height of the terminal or -1,-1 19 | func GetScreenSize() (width int, height int) { 20 | width, height, err := term.GetSize(int(syscall.Stdout)) 21 | if err == nil { 22 | return width, height 23 | } else { 24 | return 0, 0 25 | } 26 | } 27 | 28 | func DefaultIsTerminal() bool { 29 | return term.IsTerminal(int(syscall.Stdin)) && term.IsTerminal(int(syscall.Stdout)) 30 | } 31 | 32 | func DefaultOnWidthChanged(f func()) { 33 | DefaultOnSizeChanged(f) 34 | } 35 | 36 | func DefaultOnSizeChanged(f func()) { 37 | // TODO: does Windows have a SIGWINCH analogue? 38 | } 39 | -------------------------------------------------------------------------------- /internal/ringbuf/ringbuf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Shivaram Lingamneni 2 | // released under the MIT license 3 | 4 | package ringbuf 5 | 6 | type Buffer[T any] struct { 7 | // three possible states: 8 | // empty: start == end == -1 9 | // partially full: start != end 10 | // full: start == end > 0 11 | // if entries exist, they go from `start` to `(end - 1) % length` 12 | buffer []T 13 | start int 14 | end int 15 | maximumSize int 16 | } 17 | 18 | func NewExpandableBuffer[T any](initialSize, maximumSize int) (result *Buffer[T]) { 19 | result = new(Buffer[T]) 20 | result.Initialize(initialSize, maximumSize) 21 | return 22 | } 23 | 24 | func (hist *Buffer[T]) Initialize(initialSize, maximumSize int) { 25 | if maximumSize == 0 { 26 | panic("maximum size cannot be 0") 27 | } 28 | hist.buffer = make([]T, initialSize) 29 | hist.start = -1 30 | hist.end = -1 31 | hist.maximumSize = maximumSize 32 | } 33 | 34 | // Add adds an item to the buffer 35 | func (list *Buffer[T]) Add(item T) { 36 | list.maybeExpand() 37 | 38 | var pos int 39 | if list.start == -1 { // empty 40 | pos = 0 41 | list.start = 0 42 | list.end = 1 % len(list.buffer) 43 | } else if list.start != list.end { // partially full 44 | pos = list.end 45 | list.end = (list.end + 1) % len(list.buffer) 46 | } else if list.start == list.end { // full 47 | pos = list.end 48 | list.end = (list.end + 1) % len(list.buffer) 49 | list.start = list.end // advance start as well, overwriting first entry 50 | } 51 | 52 | list.buffer[pos] = item 53 | } 54 | 55 | func (list *Buffer[T]) Pop() (item T, success bool) { 56 | length := list.Length() 57 | if length == 0 { 58 | return item, false 59 | } else { 60 | pos := list.prev(list.end) 61 | item = list.buffer[pos] 62 | list.buffer[pos] = *new(T) // TODO verify that this doesn't allocate 63 | if length > 1 { 64 | list.end = pos 65 | } else { 66 | // reset to empty buffer 67 | list.start = -1 68 | list.end = -1 69 | } 70 | return item, true 71 | } 72 | } 73 | 74 | func (list *Buffer[T]) Range(ascending bool, rangeFunction func(item *T) (stopIteration bool)) { 75 | if list.start == -1 || len(list.buffer) == 0 { 76 | return 77 | } 78 | 79 | var pos, stop int 80 | if ascending { 81 | pos = list.start 82 | stop = list.prev(list.end) 83 | } else { 84 | pos = list.prev(list.end) 85 | stop = list.start 86 | } 87 | 88 | for { 89 | if shouldStop := rangeFunction(&list.buffer[pos]); shouldStop { 90 | return 91 | } 92 | 93 | if pos == stop { 94 | return 95 | } 96 | 97 | if ascending { 98 | pos = list.next(pos) 99 | } else { 100 | pos = list.prev(pos) 101 | } 102 | } 103 | } 104 | 105 | type Predicate[T any] func(item *T) (matches bool) 106 | 107 | func (list *Buffer[T]) Match(ascending bool, predicate Predicate[T], limit int) []T { 108 | var results []T 109 | rangeFunc := func(item *T) (stopIteration bool) { 110 | if predicate(item) { 111 | results = append(results, *item) 112 | return limit > 0 && len(results) >= limit 113 | } else { 114 | return false 115 | } 116 | } 117 | list.Range(ascending, rangeFunc) 118 | return results 119 | } 120 | 121 | func (list *Buffer[T]) prev(index int) int { 122 | switch index { 123 | case 0: 124 | return len(list.buffer) - 1 125 | default: 126 | return index - 1 127 | } 128 | } 129 | 130 | func (list *Buffer[T]) next(index int) int { 131 | switch index { 132 | case len(list.buffer) - 1: 133 | return 0 134 | default: 135 | return index + 1 136 | } 137 | } 138 | 139 | func (list *Buffer[T]) maybeExpand() { 140 | length := list.Length() 141 | if length < len(list.buffer) { 142 | return // we have spare capacity already 143 | } 144 | 145 | if len(list.buffer) == list.maximumSize { 146 | return // cannot expand any further 147 | } 148 | 149 | newSize := roundUpToPowerOfTwo(length + 1) 150 | if list.maximumSize < newSize { 151 | newSize = list.maximumSize 152 | } 153 | list.resize(newSize) 154 | } 155 | 156 | // return n such that v <= n and n == 2**i for some i 157 | func roundUpToPowerOfTwo(v int) int { 158 | // http://graphics.stanford.edu/~seander/bithacks.html 159 | v -= 1 160 | v |= v >> 1 161 | v |= v >> 2 162 | v |= v >> 4 163 | v |= v >> 8 164 | v |= v >> 16 165 | return v + 1 166 | } 167 | 168 | func (hist *Buffer[T]) Length() int { 169 | if hist.start == -1 { 170 | return 0 171 | } else if hist.start < hist.end { 172 | return hist.end - hist.start 173 | } else { 174 | return len(hist.buffer) - (hist.start - hist.end) 175 | } 176 | } 177 | 178 | func (list *Buffer[T]) resize(size int) { 179 | newbuffer := make([]T, size) 180 | 181 | if list.start == -1 { 182 | // indices are already correct and nothing needs to be copied 183 | } else if size == 0 { 184 | // this is now the empty list 185 | list.start = -1 186 | list.end = -1 187 | } else { 188 | currentLength := list.Length() 189 | start := list.start 190 | end := list.end 191 | // if we're truncating, keep the latest entries, not the earliest 192 | if size < currentLength { 193 | start = list.end - size 194 | if start < 0 { 195 | start += len(list.buffer) 196 | } 197 | } 198 | if start < end { 199 | copied := copy(newbuffer, list.buffer[start:end]) 200 | list.start = 0 201 | list.end = copied % size 202 | } else { 203 | lenInitial := len(list.buffer) - start 204 | copied := copy(newbuffer, list.buffer[start:]) 205 | copied += copy(newbuffer[lenInitial:], list.buffer[:end]) 206 | list.start = 0 207 | list.end = copied % size 208 | } 209 | } 210 | 211 | list.buffer = newbuffer 212 | } 213 | 214 | func (hist *Buffer[T]) Clear() { 215 | hist.Range(true, func(item *T) bool { 216 | var zero T 217 | *item = zero 218 | return false 219 | }) 220 | hist.start = -1 221 | hist.end = -1 222 | } 223 | -------------------------------------------------------------------------------- /internal/ringbuf/ringbuf_test.go: -------------------------------------------------------------------------------- 1 | package ringbuf 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func assertEqual(found, expected interface{}) { 10 | if !reflect.DeepEqual(found, expected) { 11 | panic(fmt.Sprintf("found %#v, expected %#v", found, expected)) 12 | } 13 | } 14 | 15 | func testRange(low, hi int) []int { 16 | result := make([]int, hi-low) 17 | for i := low; i < hi; i++ { 18 | result[i-low] = i 19 | } 20 | return result 21 | } 22 | 23 | func extractContents[T any](buf *Buffer[T]) (result []T) { 24 | buf.Range(true, func(i *T) bool { 25 | result = append(result, *i) 26 | return false 27 | }) 28 | return result 29 | } 30 | 31 | func TestRingbuf(t *testing.T) { 32 | b := NewExpandableBuffer[int](16, 32) 33 | numItems := 0 34 | for i := 0; i < 32; i++ { 35 | assertEqual(b.Length(), numItems) 36 | b.Add(i) 37 | numItems++ 38 | } 39 | 40 | assertEqual(b.Length(), 32) 41 | assertEqual(extractContents(b), testRange(0, 32)) 42 | 43 | for i := 32; i < 40; i++ { 44 | b.Add(i) 45 | assertEqual(b.Length(), 32) 46 | } 47 | 48 | assertEqual(b.Length(), 32) 49 | assertEqual(extractContents(b), testRange(8, 40)) 50 | assertEqual(b.Length(), 32) 51 | 52 | for i := 39; i >= 8; i-- { 53 | assertEqual(b.Length(), i-7) 54 | val, success := b.Pop() 55 | assertEqual(success, true) 56 | assertEqual(val, i) 57 | } 58 | 59 | _, success := b.Pop() 60 | assertEqual(success, false) 61 | } 62 | 63 | func TestClear(t *testing.T) { 64 | b := NewExpandableBuffer[int](8, 8) 65 | for i := 1; i <= 4; i++ { 66 | b.Add(i) 67 | } 68 | assertEqual(extractContents(b), testRange(1, 5)) 69 | b.Clear() 70 | assertEqual(b.Length(), 0) 71 | assertEqual(len(extractContents(b)), 0) 72 | // verify that the internal storage was cleared (important for GC) 73 | for i := 0; i < len(b.buffer); i++ { 74 | assertEqual(b.buffer[i], 0) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/runes/runes.go: -------------------------------------------------------------------------------- 1 | package runes 2 | 3 | import ( 4 | "bytes" 5 | "golang.org/x/text/width" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | var TabWidth = 4 11 | 12 | func EqualRune(a, b rune, fold bool) bool { 13 | if a == b { 14 | return true 15 | } 16 | if !fold { 17 | return false 18 | } 19 | if a > b { 20 | a, b = b, a 21 | } 22 | if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' { 23 | if b == a+'a'-'A' { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func EqualRuneFold(a, b rune) bool { 31 | return EqualRune(a, b, true) 32 | } 33 | 34 | func EqualFold(a, b []rune) bool { 35 | if len(a) != len(b) { 36 | return false 37 | } 38 | for i := 0; i < len(a); i++ { 39 | if EqualRuneFold(a[i], b[i]) { 40 | continue 41 | } 42 | return false 43 | } 44 | 45 | return true 46 | } 47 | 48 | func Equal(a, b []rune) bool { 49 | if len(a) != len(b) { 50 | return false 51 | } 52 | for i := 0; i < len(a); i++ { 53 | if a[i] != b[i] { 54 | return false 55 | } 56 | } 57 | return true 58 | } 59 | 60 | func IndexAllBckEx(r, sub []rune, fold bool) int { 61 | for i := len(r) - len(sub); i >= 0; i-- { 62 | found := true 63 | for j := 0; j < len(sub); j++ { 64 | if !EqualRune(r[i+j], sub[j], fold) { 65 | found = false 66 | break 67 | } 68 | } 69 | if found { 70 | return i 71 | } 72 | } 73 | return -1 74 | } 75 | 76 | // Search in runes from end to front 77 | func IndexAllBck(r, sub []rune) int { 78 | return IndexAllBckEx(r, sub, false) 79 | } 80 | 81 | // Search in runes from front to end 82 | func IndexAll(r, sub []rune) int { 83 | return IndexAllEx(r, sub, false) 84 | } 85 | 86 | func IndexAllEx(r, sub []rune, fold bool) int { 87 | for i := 0; i < len(r); i++ { 88 | found := true 89 | if len(r[i:]) < len(sub) { 90 | return -1 91 | } 92 | for j := 0; j < len(sub); j++ { 93 | if !EqualRune(r[i+j], sub[j], fold) { 94 | found = false 95 | break 96 | } 97 | } 98 | if found { 99 | return i 100 | } 101 | } 102 | return -1 103 | } 104 | 105 | func Index(r rune, rs []rune) int { 106 | for i := 0; i < len(rs); i++ { 107 | if rs[i] == r { 108 | return i 109 | } 110 | } 111 | return -1 112 | } 113 | 114 | func ColorFilter(r []rune) []rune { 115 | newr := make([]rune, 0, len(r)) 116 | for pos := 0; pos < len(r); pos++ { 117 | if r[pos] == '\033' && r[pos+1] == '[' { 118 | idx := Index('m', r[pos+2:]) 119 | if idx == -1 { 120 | continue 121 | } 122 | pos += idx + 2 123 | continue 124 | } 125 | newr = append(newr, r[pos]) 126 | } 127 | return newr 128 | } 129 | 130 | var zeroWidth = []*unicode.RangeTable{ 131 | unicode.Mn, 132 | unicode.Me, 133 | unicode.Cc, 134 | unicode.Cf, 135 | } 136 | 137 | var doubleWidth = []*unicode.RangeTable{ 138 | unicode.Han, 139 | unicode.Hangul, 140 | unicode.Hiragana, 141 | unicode.Katakana, 142 | } 143 | 144 | func Width(r rune) int { 145 | if r == '\t' { 146 | return TabWidth 147 | } 148 | if unicode.IsOneOf(zeroWidth, r) { 149 | return 0 150 | } 151 | switch width.LookupRune(r).Kind() { 152 | case width.EastAsianWide, width.EastAsianFullwidth: 153 | return 2 154 | default: 155 | return 1 156 | } 157 | } 158 | 159 | func WidthAll(r []rune) (length int) { 160 | for i := 0; i < len(r); i++ { 161 | length += Width(r[i]) 162 | } 163 | return 164 | } 165 | 166 | func Backspace(r []rune) []byte { 167 | return bytes.Repeat([]byte{'\b'}, WidthAll(r)) 168 | } 169 | 170 | func Copy(r []rune) []rune { 171 | n := make([]rune, len(r)) 172 | copy(n, r) 173 | return n 174 | } 175 | 176 | func HasPrefixFold(r, prefix []rune) bool { 177 | if len(r) < len(prefix) { 178 | return false 179 | } 180 | return EqualFold(r[:len(prefix)], prefix) 181 | } 182 | 183 | func HasPrefix(r, prefix []rune) bool { 184 | if len(r) < len(prefix) { 185 | return false 186 | } 187 | return Equal(r[:len(prefix)], prefix) 188 | } 189 | 190 | func Aggregate(candicate [][]rune) (same []rune, size int) { 191 | for i := 0; i < len(candicate[0]); i++ { 192 | for j := 0; j < len(candicate)-1; j++ { 193 | if i >= len(candicate[j]) || i >= len(candicate[j+1]) { 194 | goto aggregate 195 | } 196 | if candicate[j][i] != candicate[j+1][i] { 197 | goto aggregate 198 | } 199 | } 200 | size = i + 1 201 | } 202 | aggregate: 203 | if size > 0 { 204 | same = Copy(candicate[0][:size]) 205 | for i := 0; i < len(candicate); i++ { 206 | n := Copy(candicate[i]) 207 | copy(n, n[size:]) 208 | candicate[i] = n[:len(n)-size] 209 | } 210 | } 211 | return 212 | } 213 | 214 | func TrimSpaceLeft(in []rune) []rune { 215 | firstIndex := len(in) 216 | for i, r := range in { 217 | if unicode.IsSpace(r) == false { 218 | firstIndex = i 219 | break 220 | } 221 | } 222 | return in[firstIndex:] 223 | } 224 | 225 | func IsWordBreak(i rune) bool { 226 | switch { 227 | case i >= 'a' && i <= 'z': 228 | case i >= 'A' && i <= 'Z': 229 | case i >= '0' && i <= '9': 230 | default: 231 | return true 232 | } 233 | return false 234 | } 235 | 236 | // split prompt + runes into lines by screenwidth starting from an offset. 237 | // the prompt should be filtered before passing to only its display runes. 238 | // if you know the width of the next character, pass it in as it is used 239 | // to decide if we generate an extra empty rune array to show next is new 240 | // line. 241 | func SplitByLine(prompt, rs []rune, offset, screenWidth, nextWidth int) [][]rune { 242 | ret := make([][]rune, 0) 243 | prs := append(prompt, rs...) 244 | si := 0 245 | currentWidth := offset 246 | for i, r := range prs { 247 | w := Width(r) 248 | if r == '\n' { 249 | ret = append(ret, prs[si:i+1]) 250 | si = i + 1 251 | currentWidth = 0 252 | } else if currentWidth+w > screenWidth { 253 | ret = append(ret, prs[si:i]) 254 | si = i 255 | currentWidth = 0 256 | } 257 | currentWidth += w 258 | } 259 | ret = append(ret, prs[si:]) 260 | if currentWidth+nextWidth > screenWidth { 261 | ret = append(ret, []rune{}) 262 | } 263 | return ret 264 | } 265 | -------------------------------------------------------------------------------- /internal/runes/runes_test.go: -------------------------------------------------------------------------------- 1 | package runes 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type twidth struct { 9 | r []rune 10 | length int 11 | } 12 | 13 | func TestSingleRuneWidth(t *testing.T) { 14 | type test struct { 15 | r rune 16 | w int 17 | } 18 | 19 | tests := []test{ 20 | {0, 0}, // default rune is 0 - default mask 21 | {'a', 1}, 22 | {'☭', 1}, 23 | {'你', 2}, 24 | {'日', 2}, // kanji 25 | {'カ', 1}, // half-width katakana 26 | {'カ', 2}, // full-width katakana 27 | {'ひ', 2}, // full-width hiragana 28 | {'W', 2}, // full-width romanji 29 | {')', 2}, // full-width symbols 30 | {'😅', 2}, // emoji 31 | } 32 | 33 | for _, test := range tests { 34 | if w := Width(test.r); w != test.w { 35 | t.Error("result is not expected", string(test.r), test.w, w) 36 | } 37 | } 38 | } 39 | 40 | func TestRuneWidth(t *testing.T) { 41 | rs := []twidth{ 42 | {[]rune(""), 0}, 43 | {[]rune("☭"), 1}, 44 | {[]rune("a"), 1}, 45 | {[]rune("你"), 2}, 46 | {ColorFilter([]rune("☭\033[13;1m你")), 3}, 47 | {[]rune("漢字"), 4}, // kanji 48 | {[]rune("カタカナ"), 4}, // half-width katakana 49 | {[]rune("カタカナ"), 8}, // full-width katakana 50 | {[]rune("ひらがな"), 8}, // full-width hiragana 51 | {[]rune("WIDE"), 8}, // full-width romanji 52 | {[]rune("ー。"), 4}, // full-width symbols 53 | {[]rune("안녕하세요"), 10}, // full-width Hangul 54 | {[]rune("😅"), 2}, // emoji 55 | } 56 | for _, r := range rs { 57 | if w := WidthAll(r.r); w != r.length { 58 | t.Error("result is not expected", string(r.r), r.length, w) 59 | } 60 | } 61 | } 62 | 63 | type tagg struct { 64 | r [][]rune 65 | e [][]rune 66 | length int 67 | } 68 | 69 | func TestAggRunes(t *testing.T) { 70 | rs := []tagg{ 71 | { 72 | [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, 73 | [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 74 | 1, 75 | }, 76 | { 77 | [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, 78 | [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 79 | 1, 80 | }, 81 | { 82 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 83 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 84 | 0, 85 | }, 86 | { 87 | [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, 88 | [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 89 | 2, 90 | }, 91 | } 92 | for _, r := range rs { 93 | same, off := Aggregate(r.r) 94 | if off != r.length { 95 | t.Fatal("result not expect", off) 96 | } 97 | if len(same) != off { 98 | t.Fatal("result not expect", same) 99 | } 100 | if !reflect.DeepEqual(r.r, r.e) { 101 | t.Fatal("result not expect") 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/term/term.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package term provides support functions for dealing with terminals, as 6 | // commonly found on UNIX systems. 7 | // 8 | // Putting a terminal into raw mode is the most common requirement: 9 | // 10 | // oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 11 | // if err != nil { 12 | // panic(err) 13 | // } 14 | // defer term.Restore(int(os.Stdin.Fd()), oldState) 15 | // 16 | // Note that on non-Unix systems os.Stdin.Fd() may not be 0. 17 | package term 18 | 19 | // State contains the state of a terminal. 20 | type State struct { 21 | state 22 | } 23 | 24 | // IsTerminal returns whether the given file descriptor is a terminal. 25 | func IsTerminal(fd int) bool { 26 | return isTerminal(fd) 27 | } 28 | 29 | // MakeRaw puts the terminal connected to the given file descriptor into raw 30 | // mode and returns the previous state of the terminal so that it can be 31 | // restored. 32 | func MakeRaw(fd int) (*State, error) { 33 | return makeRaw(fd) 34 | } 35 | 36 | // GetState returns the current state of a terminal which may be useful to 37 | // restore the terminal after a signal. 38 | func GetState(fd int) (*State, error) { 39 | return getState(fd) 40 | } 41 | 42 | // Restore restores the terminal connected to the given file descriptor to a 43 | // previous state. 44 | func Restore(fd int, oldState *State) error { 45 | return restore(fd, oldState) 46 | } 47 | 48 | // GetSize returns the visible dimensions of the given terminal. 49 | // 50 | // These dimensions don't include any scrollback buffer height. 51 | func GetSize(fd int) (width, height int, err error) { 52 | return getSize(fd) 53 | } 54 | 55 | // ReadPassword reads a line of input from a terminal without local echo. This 56 | // is commonly used for inputting passwords and other sensitive data. The slice 57 | // returned does not include the \n. 58 | func ReadPassword(fd int) ([]byte, error) { 59 | return readPassword(fd) 60 | } 61 | -------------------------------------------------------------------------------- /internal/term/term_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package term 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | 11 | "golang.org/x/sys/plan9" 12 | ) 13 | 14 | type state struct{} 15 | 16 | func isTerminal(fd int) bool { 17 | path, err := plan9.Fd2path(fd) 18 | if err != nil { 19 | return false 20 | } 21 | return path == "/dev/cons" || path == "/mnt/term/dev/cons" 22 | } 23 | 24 | func makeRaw(fd int) (*State, error) { 25 | return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 26 | } 27 | 28 | func getState(fd int) (*State, error) { 29 | return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 30 | } 31 | 32 | func restore(fd int, state *State) error { 33 | return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 34 | } 35 | 36 | func getSize(fd int) (width, height int, err error) { 37 | return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 38 | } 39 | 40 | func readPassword(fd int) ([]byte, error) { 41 | return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 42 | } 43 | -------------------------------------------------------------------------------- /internal/term/term_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos 6 | 7 | package term 8 | 9 | import ( 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | type state struct { 14 | termios unix.Termios 15 | } 16 | 17 | func isTerminal(fd int) bool { 18 | _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 19 | return err == nil 20 | } 21 | 22 | func makeRaw(fd int) (*State, error) { 23 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | oldState := State{state{termios: *termios}} 29 | 30 | // This attempts to replicate the behaviour documented for cfmakeraw in 31 | // the termios(3) manpage. 32 | termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON 33 | //termios.Oflag &^= unix.OPOST 34 | termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN 35 | termios.Cflag &^= unix.CSIZE | unix.PARENB 36 | termios.Cflag |= unix.CS8 37 | termios.Cc[unix.VMIN] = 1 38 | termios.Cc[unix.VTIME] = 0 39 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { 40 | return nil, err 41 | } 42 | 43 | return &oldState, nil 44 | } 45 | 46 | func getState(fd int) (*State, error) { 47 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &State{state{termios: *termios}}, nil 53 | } 54 | 55 | func restore(fd int, state *State) error { 56 | return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) 57 | } 58 | 59 | func getSize(fd int) (width, height int, err error) { 60 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 61 | if err != nil { 62 | return 0, 0, err 63 | } 64 | return int(ws.Col), int(ws.Row), nil 65 | } 66 | 67 | // passwordReader is an io.Reader that reads from a specific file descriptor. 68 | type passwordReader int 69 | 70 | func (r passwordReader) Read(buf []byte) (int, error) { 71 | return unix.Read(int(r), buf) 72 | } 73 | 74 | func readPassword(fd int) ([]byte, error) { 75 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | newState := *termios 81 | newState.Lflag &^= unix.ECHO 82 | newState.Lflag |= unix.ICANON | unix.ISIG 83 | newState.Iflag |= unix.ICRNL 84 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { 85 | return nil, err 86 | } 87 | 88 | defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) 89 | 90 | return readPasswordLine(passwordReader(fd)) 91 | } 92 | -------------------------------------------------------------------------------- /internal/term/term_unix_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 6 | 7 | package term 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | const ioctlReadTermios = unix.TIOCGETA 12 | const ioctlWriteTermios = unix.TIOCSETA 13 | -------------------------------------------------------------------------------- /internal/term/term_unix_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build aix || linux || solaris || zos 6 | 7 | package term 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | const ioctlReadTermios = unix.TCGETS 12 | const ioctlWriteTermios = unix.TCSETS 13 | -------------------------------------------------------------------------------- /internal/term/term_unsupported.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9 6 | 7 | package term 8 | 9 | import ( 10 | "fmt" 11 | "runtime" 12 | ) 13 | 14 | type state struct{} 15 | 16 | func isTerminal(fd int) bool { 17 | return false 18 | } 19 | 20 | func makeRaw(fd int) (*State, error) { 21 | return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 22 | } 23 | 24 | func getState(fd int) (*State, error) { 25 | return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 26 | } 27 | 28 | func restore(fd int, state *State) error { 29 | return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 30 | } 31 | 32 | func getSize(fd int) (width, height int, err error) { 33 | return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 34 | } 35 | 36 | func readPassword(fd int) ([]byte, error) { 37 | return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 38 | } 39 | -------------------------------------------------------------------------------- /internal/term/term_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package term 6 | 7 | import ( 8 | "os" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | type state struct { 14 | mode uint32 15 | } 16 | 17 | func isTerminal(fd int) bool { 18 | var st uint32 19 | err := windows.GetConsoleMode(windows.Handle(fd), &st) 20 | return err == nil 21 | } 22 | 23 | func makeRaw(fd int) (*State, error) { 24 | var st uint32 25 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 26 | return nil, err 27 | } 28 | raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) 29 | if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { 30 | return nil, err 31 | } 32 | return &State{state{st}}, nil 33 | } 34 | 35 | func getState(fd int) (*State, error) { 36 | var st uint32 37 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 38 | return nil, err 39 | } 40 | return &State{state{st}}, nil 41 | } 42 | 43 | func restore(fd int, state *State) error { 44 | return windows.SetConsoleMode(windows.Handle(fd), state.mode) 45 | } 46 | 47 | func getSize(fd int) (width, height int, err error) { 48 | var info windows.ConsoleScreenBufferInfo 49 | if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { 50 | return 0, 0, err 51 | } 52 | return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil 53 | } 54 | 55 | func readPassword(fd int) ([]byte, error) { 56 | var st uint32 57 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 58 | return nil, err 59 | } 60 | old := st 61 | 62 | st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) 63 | st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) 64 | if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { 65 | return nil, err 66 | } 67 | 68 | defer windows.SetConsoleMode(windows.Handle(fd), old) 69 | 70 | var h windows.Handle 71 | p, _ := windows.GetCurrentProcess() 72 | if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { 73 | return nil, err 74 | } 75 | 76 | f := os.NewFile(uintptr(h), "stdin") 77 | defer f.Close() 78 | return readPasswordLine(f) 79 | } 80 | -------------------------------------------------------------------------------- /internal/term/terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package term 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "runtime" 11 | "strconv" 12 | "sync" 13 | "unicode/utf8" 14 | ) 15 | 16 | // EscapeCodes contains escape sequences that can be written to the terminal in 17 | // order to achieve different styles of text. 18 | type EscapeCodes struct { 19 | // Foreground colors 20 | Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte 21 | 22 | // Reset all attributes 23 | Reset []byte 24 | } 25 | 26 | var vt100EscapeCodes = EscapeCodes{ 27 | Black: []byte{keyEscape, '[', '3', '0', 'm'}, 28 | Red: []byte{keyEscape, '[', '3', '1', 'm'}, 29 | Green: []byte{keyEscape, '[', '3', '2', 'm'}, 30 | Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, 31 | Blue: []byte{keyEscape, '[', '3', '4', 'm'}, 32 | Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, 33 | Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, 34 | White: []byte{keyEscape, '[', '3', '7', 'm'}, 35 | 36 | Reset: []byte{keyEscape, '[', '0', 'm'}, 37 | } 38 | 39 | // Terminal contains the state for running a VT100 terminal that is capable of 40 | // reading lines of input. 41 | type Terminal struct { 42 | // AutoCompleteCallback, if non-null, is called for each keypress with 43 | // the full input line and the current position of the cursor (in 44 | // bytes, as an index into |line|). If it returns ok=false, the key 45 | // press is processed normally. Otherwise it returns a replacement line 46 | // and the new cursor position. 47 | AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) 48 | 49 | // Escape contains a pointer to the escape codes for this terminal. 50 | // It's always a valid pointer, although the escape codes themselves 51 | // may be empty if the terminal doesn't support them. 52 | Escape *EscapeCodes 53 | 54 | // lock protects the terminal and the state in this object from 55 | // concurrent processing of a key press and a Write() call. 56 | lock sync.Mutex 57 | 58 | c io.ReadWriter 59 | prompt []rune 60 | 61 | // line is the current line being entered. 62 | line []rune 63 | // pos is the logical position of the cursor in line 64 | pos int 65 | // echo is true if local echo is enabled 66 | echo bool 67 | // pasteActive is true iff there is a bracketed paste operation in 68 | // progress. 69 | pasteActive bool 70 | 71 | // cursorX contains the current X value of the cursor where the left 72 | // edge is 0. cursorY contains the row number where the first row of 73 | // the current line is 0. 74 | cursorX, cursorY int 75 | // maxLine is the greatest value of cursorY so far. 76 | maxLine int 77 | 78 | termWidth, termHeight int 79 | 80 | // outBuf contains the terminal data to be sent. 81 | outBuf []byte 82 | // remainder contains the remainder of any partial key sequences after 83 | // a read. It aliases into inBuf. 84 | remainder []byte 85 | inBuf [256]byte 86 | 87 | // history contains previously entered commands so that they can be 88 | // accessed with the up and down keys. 89 | history stRingBuffer 90 | // historyIndex stores the currently accessed history entry, where zero 91 | // means the immediately previous entry. 92 | historyIndex int 93 | // When navigating up and down the history it's possible to return to 94 | // the incomplete, initial line. That value is stored in 95 | // historyPending. 96 | historyPending string 97 | } 98 | 99 | // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is 100 | // a local terminal, that terminal must first have been put into raw mode. 101 | // prompt is a string that is written at the start of each input line (i.e. 102 | // "> "). 103 | func NewTerminal(c io.ReadWriter, prompt string) *Terminal { 104 | return &Terminal{ 105 | Escape: &vt100EscapeCodes, 106 | c: c, 107 | prompt: []rune(prompt), 108 | termWidth: 80, 109 | termHeight: 24, 110 | echo: true, 111 | historyIndex: -1, 112 | } 113 | } 114 | 115 | const ( 116 | keyCtrlC = 3 117 | keyCtrlD = 4 118 | keyCtrlU = 21 119 | keyEnter = '\r' 120 | keyEscape = 27 121 | keyBackspace = 127 122 | keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota 123 | keyUp 124 | keyDown 125 | keyLeft 126 | keyRight 127 | keyAltLeft 128 | keyAltRight 129 | keyHome 130 | keyEnd 131 | keyDeleteWord 132 | keyDeleteLine 133 | keyClearScreen 134 | keyPasteStart 135 | keyPasteEnd 136 | ) 137 | 138 | var ( 139 | crlf = []byte{'\r', '\n'} 140 | pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} 141 | pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} 142 | ) 143 | 144 | // bytesToKey tries to parse a key sequence from b. If successful, it returns 145 | // the key and the remainder of the input. Otherwise it returns utf8.RuneError. 146 | func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { 147 | if len(b) == 0 { 148 | return utf8.RuneError, nil 149 | } 150 | 151 | if !pasteActive { 152 | switch b[0] { 153 | case 1: // ^A 154 | return keyHome, b[1:] 155 | case 2: // ^B 156 | return keyLeft, b[1:] 157 | case 5: // ^E 158 | return keyEnd, b[1:] 159 | case 6: // ^F 160 | return keyRight, b[1:] 161 | case 8: // ^H 162 | return keyBackspace, b[1:] 163 | case 11: // ^K 164 | return keyDeleteLine, b[1:] 165 | case 12: // ^L 166 | return keyClearScreen, b[1:] 167 | case 23: // ^W 168 | return keyDeleteWord, b[1:] 169 | case 14: // ^N 170 | return keyDown, b[1:] 171 | case 16: // ^P 172 | return keyUp, b[1:] 173 | } 174 | } 175 | 176 | if b[0] != keyEscape { 177 | if !utf8.FullRune(b) { 178 | return utf8.RuneError, b 179 | } 180 | r, l := utf8.DecodeRune(b) 181 | return r, b[l:] 182 | } 183 | 184 | if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { 185 | switch b[2] { 186 | case 'A': 187 | return keyUp, b[3:] 188 | case 'B': 189 | return keyDown, b[3:] 190 | case 'C': 191 | return keyRight, b[3:] 192 | case 'D': 193 | return keyLeft, b[3:] 194 | case 'H': 195 | return keyHome, b[3:] 196 | case 'F': 197 | return keyEnd, b[3:] 198 | } 199 | } 200 | 201 | if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { 202 | switch b[5] { 203 | case 'C': 204 | return keyAltRight, b[6:] 205 | case 'D': 206 | return keyAltLeft, b[6:] 207 | } 208 | } 209 | 210 | if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { 211 | return keyPasteStart, b[6:] 212 | } 213 | 214 | if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { 215 | return keyPasteEnd, b[6:] 216 | } 217 | 218 | // If we get here then we have a key that we don't recognise, or a 219 | // partial sequence. It's not clear how one should find the end of a 220 | // sequence without knowing them all, but it seems that [a-zA-Z~] only 221 | // appears at the end of a sequence. 222 | for i, c := range b[0:] { 223 | if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { 224 | return keyUnknown, b[i+1:] 225 | } 226 | } 227 | 228 | return utf8.RuneError, b 229 | } 230 | 231 | // queue appends data to the end of t.outBuf 232 | func (t *Terminal) queue(data []rune) { 233 | t.outBuf = append(t.outBuf, []byte(string(data))...) 234 | } 235 | 236 | var space = []rune{' '} 237 | 238 | func isPrintable(key rune) bool { 239 | isInSurrogateArea := key >= 0xd800 && key <= 0xdbff 240 | return key >= 32 && !isInSurrogateArea 241 | } 242 | 243 | // moveCursorToPos appends data to t.outBuf which will move the cursor to the 244 | // given, logical position in the text. 245 | func (t *Terminal) moveCursorToPos(pos int) { 246 | if !t.echo { 247 | return 248 | } 249 | 250 | x := visualLength(t.prompt) + pos 251 | y := x / t.termWidth 252 | x = x % t.termWidth 253 | 254 | up := 0 255 | if y < t.cursorY { 256 | up = t.cursorY - y 257 | } 258 | 259 | down := 0 260 | if y > t.cursorY { 261 | down = y - t.cursorY 262 | } 263 | 264 | left := 0 265 | if x < t.cursorX { 266 | left = t.cursorX - x 267 | } 268 | 269 | right := 0 270 | if x > t.cursorX { 271 | right = x - t.cursorX 272 | } 273 | 274 | t.cursorX = x 275 | t.cursorY = y 276 | t.move(up, down, left, right) 277 | } 278 | 279 | func (t *Terminal) move(up, down, left, right int) { 280 | m := []rune{} 281 | 282 | // 1 unit up can be expressed as ^[[A or ^[A 283 | // 5 units up can be expressed as ^[[5A 284 | 285 | if up == 1 { 286 | m = append(m, keyEscape, '[', 'A') 287 | } else if up > 1 { 288 | m = append(m, keyEscape, '[') 289 | m = append(m, []rune(strconv.Itoa(up))...) 290 | m = append(m, 'A') 291 | } 292 | 293 | if down == 1 { 294 | m = append(m, keyEscape, '[', 'B') 295 | } else if down > 1 { 296 | m = append(m, keyEscape, '[') 297 | m = append(m, []rune(strconv.Itoa(down))...) 298 | m = append(m, 'B') 299 | } 300 | 301 | if right == 1 { 302 | m = append(m, keyEscape, '[', 'C') 303 | } else if right > 1 { 304 | m = append(m, keyEscape, '[') 305 | m = append(m, []rune(strconv.Itoa(right))...) 306 | m = append(m, 'C') 307 | } 308 | 309 | if left == 1 { 310 | m = append(m, keyEscape, '[', 'D') 311 | } else if left > 1 { 312 | m = append(m, keyEscape, '[') 313 | m = append(m, []rune(strconv.Itoa(left))...) 314 | m = append(m, 'D') 315 | } 316 | 317 | t.queue(m) 318 | } 319 | 320 | func (t *Terminal) clearLineToRight() { 321 | op := []rune{keyEscape, '[', 'K'} 322 | t.queue(op) 323 | } 324 | 325 | const maxLineLength = 4096 326 | 327 | func (t *Terminal) setLine(newLine []rune, newPos int) { 328 | if t.echo { 329 | t.moveCursorToPos(0) 330 | t.writeLine(newLine) 331 | for i := len(newLine); i < len(t.line); i++ { 332 | t.writeLine(space) 333 | } 334 | t.moveCursorToPos(newPos) 335 | } 336 | t.line = newLine 337 | t.pos = newPos 338 | } 339 | 340 | func (t *Terminal) advanceCursor(places int) { 341 | t.cursorX += places 342 | t.cursorY += t.cursorX / t.termWidth 343 | if t.cursorY > t.maxLine { 344 | t.maxLine = t.cursorY 345 | } 346 | t.cursorX = t.cursorX % t.termWidth 347 | 348 | if places > 0 && t.cursorX == 0 { 349 | // Normally terminals will advance the current position 350 | // when writing a character. But that doesn't happen 351 | // for the last character in a line. However, when 352 | // writing a character (except a new line) that causes 353 | // a line wrap, the position will be advanced two 354 | // places. 355 | // 356 | // So, if we are stopping at the end of a line, we 357 | // need to write a newline so that our cursor can be 358 | // advanced to the next line. 359 | t.outBuf = append(t.outBuf, '\r', '\n') 360 | } 361 | } 362 | 363 | func (t *Terminal) eraseNPreviousChars(n int) { 364 | if n == 0 { 365 | return 366 | } 367 | 368 | if t.pos < n { 369 | n = t.pos 370 | } 371 | t.pos -= n 372 | t.moveCursorToPos(t.pos) 373 | 374 | copy(t.line[t.pos:], t.line[n+t.pos:]) 375 | t.line = t.line[:len(t.line)-n] 376 | if t.echo { 377 | t.writeLine(t.line[t.pos:]) 378 | for i := 0; i < n; i++ { 379 | t.queue(space) 380 | } 381 | t.advanceCursor(n) 382 | t.moveCursorToPos(t.pos) 383 | } 384 | } 385 | 386 | // countToLeftWord returns then number of characters from the cursor to the 387 | // start of the previous word. 388 | func (t *Terminal) countToLeftWord() int { 389 | if t.pos == 0 { 390 | return 0 391 | } 392 | 393 | pos := t.pos - 1 394 | for pos > 0 { 395 | if t.line[pos] != ' ' { 396 | break 397 | } 398 | pos-- 399 | } 400 | for pos > 0 { 401 | if t.line[pos] == ' ' { 402 | pos++ 403 | break 404 | } 405 | pos-- 406 | } 407 | 408 | return t.pos - pos 409 | } 410 | 411 | // countToRightWord returns then number of characters from the cursor to the 412 | // start of the next word. 413 | func (t *Terminal) countToRightWord() int { 414 | pos := t.pos 415 | for pos < len(t.line) { 416 | if t.line[pos] == ' ' { 417 | break 418 | } 419 | pos++ 420 | } 421 | for pos < len(t.line) { 422 | if t.line[pos] != ' ' { 423 | break 424 | } 425 | pos++ 426 | } 427 | return pos - t.pos 428 | } 429 | 430 | // visualLength returns the number of visible glyphs in s. 431 | func visualLength(runes []rune) int { 432 | inEscapeSeq := false 433 | length := 0 434 | 435 | for _, r := range runes { 436 | switch { 437 | case inEscapeSeq: 438 | if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { 439 | inEscapeSeq = false 440 | } 441 | case r == '\x1b': 442 | inEscapeSeq = true 443 | default: 444 | length++ 445 | } 446 | } 447 | 448 | return length 449 | } 450 | 451 | // handleKey processes the given key and, optionally, returns a line of text 452 | // that the user has entered. 453 | func (t *Terminal) handleKey(key rune) (line string, ok bool) { 454 | if t.pasteActive && key != keyEnter { 455 | t.addKeyToLine(key) 456 | return 457 | } 458 | 459 | switch key { 460 | case keyBackspace: 461 | if t.pos == 0 { 462 | return 463 | } 464 | t.eraseNPreviousChars(1) 465 | case keyAltLeft: 466 | // move left by a word. 467 | t.pos -= t.countToLeftWord() 468 | t.moveCursorToPos(t.pos) 469 | case keyAltRight: 470 | // move right by a word. 471 | t.pos += t.countToRightWord() 472 | t.moveCursorToPos(t.pos) 473 | case keyLeft: 474 | if t.pos == 0 { 475 | return 476 | } 477 | t.pos-- 478 | t.moveCursorToPos(t.pos) 479 | case keyRight: 480 | if t.pos == len(t.line) { 481 | return 482 | } 483 | t.pos++ 484 | t.moveCursorToPos(t.pos) 485 | case keyHome: 486 | if t.pos == 0 { 487 | return 488 | } 489 | t.pos = 0 490 | t.moveCursorToPos(t.pos) 491 | case keyEnd: 492 | if t.pos == len(t.line) { 493 | return 494 | } 495 | t.pos = len(t.line) 496 | t.moveCursorToPos(t.pos) 497 | case keyUp: 498 | entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) 499 | if !ok { 500 | return "", false 501 | } 502 | if t.historyIndex == -1 { 503 | t.historyPending = string(t.line) 504 | } 505 | t.historyIndex++ 506 | runes := []rune(entry) 507 | t.setLine(runes, len(runes)) 508 | case keyDown: 509 | switch t.historyIndex { 510 | case -1: 511 | return 512 | case 0: 513 | runes := []rune(t.historyPending) 514 | t.setLine(runes, len(runes)) 515 | t.historyIndex-- 516 | default: 517 | entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) 518 | if ok { 519 | t.historyIndex-- 520 | runes := []rune(entry) 521 | t.setLine(runes, len(runes)) 522 | } 523 | } 524 | case keyEnter: 525 | t.moveCursorToPos(len(t.line)) 526 | t.queue([]rune("\r\n")) 527 | line = string(t.line) 528 | ok = true 529 | t.line = t.line[:0] 530 | t.pos = 0 531 | t.cursorX = 0 532 | t.cursorY = 0 533 | t.maxLine = 0 534 | case keyDeleteWord: 535 | // Delete zero or more spaces and then one or more characters. 536 | t.eraseNPreviousChars(t.countToLeftWord()) 537 | case keyDeleteLine: 538 | // Delete everything from the current cursor position to the 539 | // end of line. 540 | for i := t.pos; i < len(t.line); i++ { 541 | t.queue(space) 542 | t.advanceCursor(1) 543 | } 544 | t.line = t.line[:t.pos] 545 | t.moveCursorToPos(t.pos) 546 | case keyCtrlD: 547 | // Erase the character under the current position. 548 | // The EOF case when the line is empty is handled in 549 | // readLine(). 550 | if t.pos < len(t.line) { 551 | t.pos++ 552 | t.eraseNPreviousChars(1) 553 | } 554 | case keyCtrlU: 555 | t.eraseNPreviousChars(t.pos) 556 | case keyClearScreen: 557 | // Erases the screen and moves the cursor to the home position. 558 | t.queue([]rune("\x1b[2J\x1b[H")) 559 | t.queue(t.prompt) 560 | t.cursorX, t.cursorY = 0, 0 561 | t.advanceCursor(visualLength(t.prompt)) 562 | t.setLine(t.line, t.pos) 563 | default: 564 | if t.AutoCompleteCallback != nil { 565 | prefix := string(t.line[:t.pos]) 566 | suffix := string(t.line[t.pos:]) 567 | 568 | t.lock.Unlock() 569 | newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) 570 | t.lock.Lock() 571 | 572 | if completeOk { 573 | t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) 574 | return 575 | } 576 | } 577 | if !isPrintable(key) { 578 | return 579 | } 580 | if len(t.line) == maxLineLength { 581 | return 582 | } 583 | t.addKeyToLine(key) 584 | } 585 | return 586 | } 587 | 588 | // addKeyToLine inserts the given key at the current position in the current 589 | // line. 590 | func (t *Terminal) addKeyToLine(key rune) { 591 | if len(t.line) == cap(t.line) { 592 | newLine := make([]rune, len(t.line), 2*(1+len(t.line))) 593 | copy(newLine, t.line) 594 | t.line = newLine 595 | } 596 | t.line = t.line[:len(t.line)+1] 597 | copy(t.line[t.pos+1:], t.line[t.pos:]) 598 | t.line[t.pos] = key 599 | if t.echo { 600 | t.writeLine(t.line[t.pos:]) 601 | } 602 | t.pos++ 603 | t.moveCursorToPos(t.pos) 604 | } 605 | 606 | func (t *Terminal) writeLine(line []rune) { 607 | for len(line) != 0 { 608 | remainingOnLine := t.termWidth - t.cursorX 609 | todo := len(line) 610 | if todo > remainingOnLine { 611 | todo = remainingOnLine 612 | } 613 | t.queue(line[:todo]) 614 | t.advanceCursor(visualLength(line[:todo])) 615 | line = line[todo:] 616 | } 617 | } 618 | 619 | // writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. 620 | func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { 621 | for len(buf) > 0 { 622 | i := bytes.IndexByte(buf, '\n') 623 | todo := len(buf) 624 | if i >= 0 { 625 | todo = i 626 | } 627 | 628 | var nn int 629 | nn, err = w.Write(buf[:todo]) 630 | n += nn 631 | if err != nil { 632 | return n, err 633 | } 634 | buf = buf[todo:] 635 | 636 | if i >= 0 { 637 | if _, err = w.Write(crlf); err != nil { 638 | return n, err 639 | } 640 | n++ 641 | buf = buf[1:] 642 | } 643 | } 644 | 645 | return n, nil 646 | } 647 | 648 | func (t *Terminal) Write(buf []byte) (n int, err error) { 649 | t.lock.Lock() 650 | defer t.lock.Unlock() 651 | 652 | if t.cursorX == 0 && t.cursorY == 0 { 653 | // This is the easy case: there's nothing on the screen that we 654 | // have to move out of the way. 655 | return writeWithCRLF(t.c, buf) 656 | } 657 | 658 | // We have a prompt and possibly user input on the screen. We 659 | // have to clear it first. 660 | t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) 661 | t.cursorX = 0 662 | t.clearLineToRight() 663 | 664 | for t.cursorY > 0 { 665 | t.move(1 /* up */, 0, 0, 0) 666 | t.cursorY-- 667 | t.clearLineToRight() 668 | } 669 | 670 | if _, err = t.c.Write(t.outBuf); err != nil { 671 | return 672 | } 673 | t.outBuf = t.outBuf[:0] 674 | 675 | if n, err = writeWithCRLF(t.c, buf); err != nil { 676 | return 677 | } 678 | 679 | t.writeLine(t.prompt) 680 | if t.echo { 681 | t.writeLine(t.line) 682 | } 683 | 684 | t.moveCursorToPos(t.pos) 685 | 686 | if _, err = t.c.Write(t.outBuf); err != nil { 687 | return 688 | } 689 | t.outBuf = t.outBuf[:0] 690 | return 691 | } 692 | 693 | // ReadPassword temporarily changes the prompt and reads a password, without 694 | // echo, from the terminal. 695 | func (t *Terminal) ReadPassword(prompt string) (line string, err error) { 696 | t.lock.Lock() 697 | defer t.lock.Unlock() 698 | 699 | oldPrompt := t.prompt 700 | t.prompt = []rune(prompt) 701 | t.echo = false 702 | 703 | line, err = t.readLine() 704 | 705 | t.prompt = oldPrompt 706 | t.echo = true 707 | 708 | return 709 | } 710 | 711 | // ReadLine returns a line of input from the terminal. 712 | func (t *Terminal) ReadLine() (line string, err error) { 713 | t.lock.Lock() 714 | defer t.lock.Unlock() 715 | 716 | return t.readLine() 717 | } 718 | 719 | func (t *Terminal) readLine() (line string, err error) { 720 | // t.lock must be held at this point 721 | 722 | if t.cursorX == 0 && t.cursorY == 0 { 723 | t.writeLine(t.prompt) 724 | t.c.Write(t.outBuf) 725 | t.outBuf = t.outBuf[:0] 726 | } 727 | 728 | lineIsPasted := t.pasteActive 729 | 730 | for { 731 | rest := t.remainder 732 | lineOk := false 733 | for !lineOk { 734 | var key rune 735 | key, rest = bytesToKey(rest, t.pasteActive) 736 | if key == utf8.RuneError { 737 | break 738 | } 739 | if !t.pasteActive { 740 | if key == keyCtrlD { 741 | if len(t.line) == 0 { 742 | return "", io.EOF 743 | } 744 | } 745 | if key == keyCtrlC { 746 | return "", io.EOF 747 | } 748 | if key == keyPasteStart { 749 | t.pasteActive = true 750 | if len(t.line) == 0 { 751 | lineIsPasted = true 752 | } 753 | continue 754 | } 755 | } else if key == keyPasteEnd { 756 | t.pasteActive = false 757 | continue 758 | } 759 | if !t.pasteActive { 760 | lineIsPasted = false 761 | } 762 | line, lineOk = t.handleKey(key) 763 | } 764 | if len(rest) > 0 { 765 | n := copy(t.inBuf[:], rest) 766 | t.remainder = t.inBuf[:n] 767 | } else { 768 | t.remainder = nil 769 | } 770 | t.c.Write(t.outBuf) 771 | t.outBuf = t.outBuf[:0] 772 | if lineOk { 773 | if t.echo { 774 | t.historyIndex = -1 775 | t.history.Add(line) 776 | } 777 | if lineIsPasted { 778 | err = ErrPasteIndicator 779 | } 780 | return 781 | } 782 | 783 | // t.remainder is a slice at the beginning of t.inBuf 784 | // containing a partial key sequence 785 | readBuf := t.inBuf[len(t.remainder):] 786 | var n int 787 | 788 | t.lock.Unlock() 789 | n, err = t.c.Read(readBuf) 790 | t.lock.Lock() 791 | 792 | if err != nil { 793 | return 794 | } 795 | 796 | t.remainder = t.inBuf[:n+len(t.remainder)] 797 | } 798 | } 799 | 800 | // SetPrompt sets the prompt to be used when reading subsequent lines. 801 | func (t *Terminal) SetPrompt(prompt string) { 802 | t.lock.Lock() 803 | defer t.lock.Unlock() 804 | 805 | t.prompt = []rune(prompt) 806 | } 807 | 808 | func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { 809 | // Move cursor to column zero at the start of the line. 810 | t.move(t.cursorY, 0, t.cursorX, 0) 811 | t.cursorX, t.cursorY = 0, 0 812 | t.clearLineToRight() 813 | for t.cursorY < numPrevLines { 814 | // Move down a line 815 | t.move(0, 1, 0, 0) 816 | t.cursorY++ 817 | t.clearLineToRight() 818 | } 819 | // Move back to beginning. 820 | t.move(t.cursorY, 0, 0, 0) 821 | t.cursorX, t.cursorY = 0, 0 822 | 823 | t.queue(t.prompt) 824 | t.advanceCursor(visualLength(t.prompt)) 825 | t.writeLine(t.line) 826 | t.moveCursorToPos(t.pos) 827 | } 828 | 829 | func (t *Terminal) SetSize(width, height int) error { 830 | t.lock.Lock() 831 | defer t.lock.Unlock() 832 | 833 | if width == 0 { 834 | width = 1 835 | } 836 | 837 | oldWidth := t.termWidth 838 | t.termWidth, t.termHeight = width, height 839 | 840 | switch { 841 | case width == oldWidth: 842 | // If the width didn't change then nothing else needs to be 843 | // done. 844 | return nil 845 | case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: 846 | // If there is nothing on current line and no prompt printed, 847 | // just do nothing 848 | return nil 849 | case width < oldWidth: 850 | // Some terminals (e.g. xterm) will truncate lines that were 851 | // too long when shinking. Others, (e.g. gnome-terminal) will 852 | // attempt to wrap them. For the former, repainting t.maxLine 853 | // works great, but that behaviour goes badly wrong in the case 854 | // of the latter because they have doubled every full line. 855 | 856 | // We assume that we are working on a terminal that wraps lines 857 | // and adjust the cursor position based on every previous line 858 | // wrapping and turning into two. This causes the prompt on 859 | // xterms to move upwards, which isn't great, but it avoids a 860 | // huge mess with gnome-terminal. 861 | if t.cursorX >= t.termWidth { 862 | t.cursorX = t.termWidth - 1 863 | } 864 | t.cursorY *= 2 865 | t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) 866 | case width > oldWidth: 867 | // If the terminal expands then our position calculations will 868 | // be wrong in the future because we think the cursor is 869 | // |t.pos| chars into the string, but there will be a gap at 870 | // the end of any wrapped line. 871 | // 872 | // But the position will actually be correct until we move, so 873 | // we can move back to the beginning and repaint everything. 874 | t.clearAndRepaintLinePlusNPrevious(t.maxLine) 875 | } 876 | 877 | _, err := t.c.Write(t.outBuf) 878 | t.outBuf = t.outBuf[:0] 879 | return err 880 | } 881 | 882 | type pasteIndicatorError struct{} 883 | 884 | func (pasteIndicatorError) Error() string { 885 | return "terminal: ErrPasteIndicator not correctly handled" 886 | } 887 | 888 | // ErrPasteIndicator may be returned from ReadLine as the error, in addition 889 | // to valid line data. It indicates that bracketed paste mode is enabled and 890 | // that the returned line consists only of pasted data. Programs may wish to 891 | // interpret pasted data more literally than typed data. 892 | var ErrPasteIndicator = pasteIndicatorError{} 893 | 894 | // SetBracketedPasteMode requests that the terminal bracket paste operations 895 | // with markers. Not all terminals support this but, if it is supported, then 896 | // enabling this mode will stop any autocomplete callback from running due to 897 | // pastes. Additionally, any lines that are completely pasted will be returned 898 | // from ReadLine with the error set to ErrPasteIndicator. 899 | func (t *Terminal) SetBracketedPasteMode(on bool) { 900 | if on { 901 | io.WriteString(t.c, "\x1b[?2004h") 902 | } else { 903 | io.WriteString(t.c, "\x1b[?2004l") 904 | } 905 | } 906 | 907 | // stRingBuffer is a ring buffer of strings. 908 | type stRingBuffer struct { 909 | // entries contains max elements. 910 | entries []string 911 | max int 912 | // head contains the index of the element most recently added to the ring. 913 | head int 914 | // size contains the number of elements in the ring. 915 | size int 916 | } 917 | 918 | func (s *stRingBuffer) Add(a string) { 919 | if s.entries == nil { 920 | const defaultNumEntries = 100 921 | s.entries = make([]string, defaultNumEntries) 922 | s.max = defaultNumEntries 923 | } 924 | 925 | s.head = (s.head + 1) % s.max 926 | s.entries[s.head] = a 927 | if s.size < s.max { 928 | s.size++ 929 | } 930 | } 931 | 932 | // NthPreviousEntry returns the value passed to the nth previous call to Add. 933 | // If n is zero then the immediately prior value is returned, if one, then the 934 | // next most recent, and so on. If such an element doesn't exist then ok is 935 | // false. 936 | func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { 937 | if n < 0 || n >= s.size { 938 | return "", false 939 | } 940 | index := s.head - n 941 | if index < 0 { 942 | index += s.max 943 | } 944 | return s.entries[index], true 945 | } 946 | 947 | // readPasswordLine reads from reader until it finds \n or io.EOF. 948 | // The slice returned does not include the \n. 949 | // readPasswordLine also ignores any \r it finds. 950 | // Windows uses \r as end of line. So, on Windows, readPasswordLine 951 | // reads until it finds \r and ignores any \n it finds during processing. 952 | func readPasswordLine(reader io.Reader) ([]byte, error) { 953 | var buf [1]byte 954 | var ret []byte 955 | 956 | for { 957 | n, err := reader.Read(buf[:]) 958 | if n > 0 { 959 | switch buf[0] { 960 | case '\b': 961 | if len(ret) > 0 { 962 | ret = ret[:len(ret)-1] 963 | } 964 | case '\n': 965 | if runtime.GOOS != "windows" { 966 | return ret, nil 967 | } 968 | // otherwise ignore \n 969 | case '\r': 970 | if runtime.GOOS == "windows" { 971 | return ret, nil 972 | } 973 | // otherwise ignore \r 974 | default: 975 | ret = append(ret, buf[0]) 976 | } 977 | continue 978 | } 979 | if err != nil { 980 | if err == io.EOF && len(ret) > 0 { 981 | return ret, nil 982 | } 983 | return ret, err 984 | } 985 | } 986 | } 987 | -------------------------------------------------------------------------------- /internal/term/terminal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package term 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "os" 11 | "runtime" 12 | "testing" 13 | ) 14 | 15 | type MockTerminal struct { 16 | toSend []byte 17 | bytesPerRead int 18 | received []byte 19 | } 20 | 21 | func (c *MockTerminal) Read(data []byte) (n int, err error) { 22 | n = len(data) 23 | if n == 0 { 24 | return 25 | } 26 | if n > len(c.toSend) { 27 | n = len(c.toSend) 28 | } 29 | if n == 0 { 30 | return 0, io.EOF 31 | } 32 | if c.bytesPerRead > 0 && n > c.bytesPerRead { 33 | n = c.bytesPerRead 34 | } 35 | copy(data, c.toSend[:n]) 36 | c.toSend = c.toSend[n:] 37 | return 38 | } 39 | 40 | func (c *MockTerminal) Write(data []byte) (n int, err error) { 41 | c.received = append(c.received, data...) 42 | return len(data), nil 43 | } 44 | 45 | func TestClose(t *testing.T) { 46 | c := &MockTerminal{} 47 | ss := NewTerminal(c, "> ") 48 | line, err := ss.ReadLine() 49 | if line != "" { 50 | t.Errorf("Expected empty line but got: %s", line) 51 | } 52 | if err != io.EOF { 53 | t.Errorf("Error should have been EOF but got: %s", err) 54 | } 55 | } 56 | 57 | var keyPressTests = []struct { 58 | in string 59 | line string 60 | err error 61 | throwAwayLines int 62 | }{ 63 | { 64 | err: io.EOF, 65 | }, 66 | { 67 | in: "\r", 68 | line: "", 69 | }, 70 | { 71 | in: "foo\r", 72 | line: "foo", 73 | }, 74 | { 75 | in: "a\x1b[Cb\r", // right 76 | line: "ab", 77 | }, 78 | { 79 | in: "a\x1b[Db\r", // left 80 | line: "ba", 81 | }, 82 | { 83 | in: "a\006b\r", // ^F 84 | line: "ab", 85 | }, 86 | { 87 | in: "a\002b\r", // ^B 88 | line: "ba", 89 | }, 90 | { 91 | in: "a\177b\r", // backspace 92 | line: "b", 93 | }, 94 | { 95 | in: "\x1b[A\r", // up 96 | }, 97 | { 98 | in: "\x1b[B\r", // down 99 | }, 100 | { 101 | in: "\016\r", // ^P 102 | }, 103 | { 104 | in: "\014\r", // ^N 105 | }, 106 | { 107 | in: "line\x1b[A\x1b[B\r", // up then down 108 | line: "line", 109 | }, 110 | { 111 | in: "line1\rline2\x1b[A\r", // recall previous line. 112 | line: "line1", 113 | throwAwayLines: 1, 114 | }, 115 | { 116 | // recall two previous lines and append. 117 | in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r", 118 | line: "line1xxx", 119 | throwAwayLines: 2, 120 | }, 121 | { 122 | // Ctrl-A to move to beginning of line followed by ^K to kill 123 | // line. 124 | in: "a b \001\013\r", 125 | line: "", 126 | }, 127 | { 128 | // Ctrl-A to move to beginning of line, Ctrl-E to move to end, 129 | // finally ^K to kill nothing. 130 | in: "a b \001\005\013\r", 131 | line: "a b ", 132 | }, 133 | { 134 | in: "\027\r", 135 | line: "", 136 | }, 137 | { 138 | in: "a\027\r", 139 | line: "", 140 | }, 141 | { 142 | in: "a \027\r", 143 | line: "", 144 | }, 145 | { 146 | in: "a b\027\r", 147 | line: "a ", 148 | }, 149 | { 150 | in: "a b \027\r", 151 | line: "a ", 152 | }, 153 | { 154 | in: "one two thr\x1b[D\027\r", 155 | line: "one two r", 156 | }, 157 | { 158 | in: "\013\r", 159 | line: "", 160 | }, 161 | { 162 | in: "a\013\r", 163 | line: "a", 164 | }, 165 | { 166 | in: "ab\x1b[D\013\r", 167 | line: "a", 168 | }, 169 | { 170 | in: "Ξεσκεπάζω\r", 171 | line: "Ξεσκεπάζω", 172 | }, 173 | { 174 | in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace. 175 | line: "", 176 | throwAwayLines: 1, 177 | }, 178 | { 179 | in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter. 180 | line: "£", 181 | throwAwayLines: 1, 182 | }, 183 | { 184 | // Ctrl-D at the end of the line should be ignored. 185 | in: "a\004\r", 186 | line: "a", 187 | }, 188 | { 189 | // a, b, left, Ctrl-D should erase the b. 190 | in: "ab\x1b[D\004\r", 191 | line: "a", 192 | }, 193 | { 194 | // a, b, c, d, left, left, ^U should erase to the beginning of 195 | // the line. 196 | in: "abcd\x1b[D\x1b[D\025\r", 197 | line: "cd", 198 | }, 199 | { 200 | // Bracketed paste mode: control sequences should be returned 201 | // verbatim in paste mode. 202 | in: "abc\x1b[200~de\177f\x1b[201~\177\r", 203 | line: "abcde\177", 204 | }, 205 | { 206 | // Enter in bracketed paste mode should still work. 207 | in: "abc\x1b[200~d\refg\x1b[201~h\r", 208 | line: "efgh", 209 | throwAwayLines: 1, 210 | }, 211 | { 212 | // Lines consisting entirely of pasted data should be indicated as such. 213 | in: "\x1b[200~a\r", 214 | line: "a", 215 | err: ErrPasteIndicator, 216 | }, 217 | { 218 | // Ctrl-C terminates readline 219 | in: "\003", 220 | err: io.EOF, 221 | }, 222 | { 223 | // Ctrl-C at the end of line also terminates readline 224 | in: "a\003\r", 225 | err: io.EOF, 226 | }, 227 | } 228 | 229 | func TestKeyPresses(t *testing.T) { 230 | for i, test := range keyPressTests { 231 | for j := 1; j < len(test.in); j++ { 232 | c := &MockTerminal{ 233 | toSend: []byte(test.in), 234 | bytesPerRead: j, 235 | } 236 | ss := NewTerminal(c, "> ") 237 | for k := 0; k < test.throwAwayLines; k++ { 238 | _, err := ss.ReadLine() 239 | if err != nil { 240 | t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err) 241 | } 242 | } 243 | line, err := ss.ReadLine() 244 | if line != test.line { 245 | t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) 246 | break 247 | } 248 | if err != test.err { 249 | t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) 250 | break 251 | } 252 | } 253 | } 254 | } 255 | 256 | var renderTests = []struct { 257 | in string 258 | received string 259 | err error 260 | }{ 261 | { 262 | // Cursor move after keyHome (left 4) then enter (right 4, newline) 263 | in: "abcd\x1b[H\r", 264 | received: "> abcd\x1b[4D\x1b[4C\r\n", 265 | }, 266 | { 267 | // Write, home, prepend, enter. Prepends rewrites the line. 268 | in: "cdef\x1b[Hab\r", 269 | received: "> cdef" + // Initial input 270 | "\x1b[4Da" + // Move cursor back, insert first char 271 | "cdef" + // Copy over original string 272 | "\x1b[4Dbcdef" + // Repeat for second char with copy 273 | "\x1b[4D" + // Put cursor back in position to insert again 274 | "\x1b[4C\r\n", // Put cursor at the end of the line and newline. 275 | }, 276 | } 277 | 278 | func TestRender(t *testing.T) { 279 | for i, test := range renderTests { 280 | for j := 1; j < len(test.in); j++ { 281 | c := &MockTerminal{ 282 | toSend: []byte(test.in), 283 | bytesPerRead: j, 284 | } 285 | ss := NewTerminal(c, "> ") 286 | _, err := ss.ReadLine() 287 | if err != test.err { 288 | t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) 289 | break 290 | } 291 | if test.received != string(c.received) { 292 | t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received) 293 | break 294 | } 295 | } 296 | } 297 | } 298 | 299 | func TestPasswordNotSaved(t *testing.T) { 300 | c := &MockTerminal{ 301 | toSend: []byte("password\r\x1b[A\r"), 302 | bytesPerRead: 1, 303 | } 304 | ss := NewTerminal(c, "> ") 305 | pw, _ := ss.ReadPassword("> ") 306 | if pw != "password" { 307 | t.Fatalf("failed to read password, got %s", pw) 308 | } 309 | line, _ := ss.ReadLine() 310 | if len(line) > 0 { 311 | t.Fatalf("password was saved in history") 312 | } 313 | } 314 | 315 | var setSizeTests = []struct { 316 | width, height int 317 | }{ 318 | {40, 13}, 319 | {80, 24}, 320 | {132, 43}, 321 | } 322 | 323 | func TestTerminalSetSize(t *testing.T) { 324 | for _, setSize := range setSizeTests { 325 | c := &MockTerminal{ 326 | toSend: []byte("password\r\x1b[A\r"), 327 | bytesPerRead: 1, 328 | } 329 | ss := NewTerminal(c, "> ") 330 | ss.SetSize(setSize.width, setSize.height) 331 | pw, _ := ss.ReadPassword("Password: ") 332 | if pw != "password" { 333 | t.Fatalf("failed to read password, got %s", pw) 334 | } 335 | if string(c.received) != "Password: \r\n" { 336 | t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received) 337 | } 338 | } 339 | } 340 | 341 | func TestReadPasswordLineEnd(t *testing.T) { 342 | type testType struct { 343 | input string 344 | want string 345 | } 346 | var tests = []testType{ 347 | {"\r\n", ""}, 348 | {"test\r\n", "test"}, 349 | {"test\r", "test"}, 350 | {"test\n", "test"}, 351 | {"testtesttesttes\n", "testtesttesttes"}, 352 | {"testtesttesttes\r\n", "testtesttesttes"}, 353 | {"testtesttesttesttest\n", "testtesttesttesttest"}, 354 | {"testtesttesttesttest\r\n", "testtesttesttesttest"}, 355 | {"\btest", "test"}, 356 | {"t\best", "est"}, 357 | {"te\bst", "tst"}, 358 | {"test\b", "tes"}, 359 | {"test\b\r\n", "tes"}, 360 | {"test\b\n", "tes"}, 361 | {"test\b\r", "tes"}, 362 | } 363 | eol := "\n" 364 | if runtime.GOOS == "windows" { 365 | eol = "\r" 366 | } 367 | tests = append(tests, testType{eol, ""}) 368 | for _, test := range tests { 369 | buf := new(bytes.Buffer) 370 | if _, err := buf.WriteString(test.input); err != nil { 371 | t.Fatal(err) 372 | } 373 | 374 | have, err := readPasswordLine(buf) 375 | if err != nil { 376 | t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) 377 | continue 378 | } 379 | if string(have) != test.want { 380 | t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) 381 | continue 382 | } 383 | 384 | if _, err = buf.WriteString(test.input); err != nil { 385 | t.Fatal(err) 386 | } 387 | have, err = readPasswordLine(buf) 388 | if err != nil { 389 | t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) 390 | continue 391 | } 392 | if string(have) != test.want { 393 | t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) 394 | continue 395 | } 396 | } 397 | } 398 | 399 | func TestMakeRawState(t *testing.T) { 400 | fd := int(os.Stdout.Fd()) 401 | if !IsTerminal(fd) { 402 | t.Skip("stdout is not a terminal; skipping test") 403 | } 404 | 405 | st, err := GetState(fd) 406 | if err != nil { 407 | t.Fatalf("failed to get terminal state from GetState: %s", err) 408 | } 409 | 410 | if runtime.GOOS == "ios" { 411 | t.Skip("MakeRaw not allowed on iOS; skipping test") 412 | } 413 | 414 | defer Restore(fd, st) 415 | raw, err := MakeRaw(fd) 416 | if err != nil { 417 | t.Fatalf("failed to get terminal state from MakeRaw: %s", err) 418 | } 419 | 420 | if *st != *raw { 421 | t.Errorf("states do not match; was %v, expected %v", raw, st) 422 | } 423 | } 424 | 425 | func TestOutputNewlines(t *testing.T) { 426 | // \n should be changed to \r\n in terminal output. 427 | buf := new(bytes.Buffer) 428 | term := NewTerminal(buf, ">") 429 | 430 | term.Write([]byte("1\n2\n")) 431 | output := buf.String() 432 | const expected = "1\r\n2\r\n" 433 | 434 | if output != expected { 435 | t.Errorf("incorrect output: was %q, expected %q", output, expected) 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /operation.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/ergochat/readline/internal/platform" 10 | "github.com/ergochat/readline/internal/runes" 11 | ) 12 | 13 | var ( 14 | ErrInterrupt = errors.New("Interrupt") 15 | ) 16 | 17 | type operation struct { 18 | m sync.Mutex 19 | t *terminal 20 | buf *runeBuffer 21 | wrapOut atomic.Pointer[wrapWriter] 22 | wrapErr atomic.Pointer[wrapWriter] 23 | 24 | isPrompting bool // true when prompt written and waiting for input 25 | 26 | history *opHistory 27 | search *opSearch 28 | completer *opCompleter 29 | vim *opVim 30 | undo *opUndo 31 | } 32 | 33 | func (o *operation) SetBuffer(what string) { 34 | o.buf.SetNoRefresh([]rune(what)) 35 | } 36 | 37 | type wrapWriter struct { 38 | o *operation 39 | target io.Writer 40 | } 41 | 42 | func (w *wrapWriter) Write(b []byte) (int, error) { 43 | return w.o.write(w.target, b) 44 | } 45 | 46 | func (o *operation) write(target io.Writer, b []byte) (int, error) { 47 | o.m.Lock() 48 | defer o.m.Unlock() 49 | 50 | if !o.isPrompting { 51 | return target.Write(b) 52 | } 53 | 54 | var ( 55 | n int 56 | err error 57 | ) 58 | o.buf.Refresh(func() { 59 | n, err = target.Write(b) 60 | // Adjust the prompt start position by b 61 | rout := runes.ColorFilter([]rune(string(b[:]))) 62 | tWidth, _ := o.t.GetWidthHeight() 63 | sp := runes.SplitByLine(rout, []rune{}, o.buf.ppos, tWidth, 1) 64 | if len(sp) > 1 { 65 | o.buf.ppos = len(sp[len(sp)-1]) 66 | } else { 67 | o.buf.ppos += len(rout) 68 | } 69 | }) 70 | 71 | o.search.RefreshIfNeeded() 72 | if o.completer.IsInCompleteMode() { 73 | o.completer.CompleteRefresh() 74 | } 75 | return n, err 76 | } 77 | 78 | func newOperation(t *terminal) *operation { 79 | cfg := t.GetConfig() 80 | op := &operation{ 81 | t: t, 82 | buf: newRuneBuffer(t), 83 | } 84 | op.SetConfig(cfg) 85 | op.vim = newVimMode(op) 86 | op.completer = newOpCompleter(op.buf.w, op) 87 | cfg.FuncOnWidthChanged(t.OnSizeChange) 88 | return op 89 | } 90 | 91 | func (o *operation) GetConfig() *Config { 92 | return o.t.GetConfig() 93 | } 94 | 95 | func (o *operation) readline(deadline chan struct{}) ([]rune, error) { 96 | isTyping := false // don't add new undo entries during normal typing 97 | 98 | for { 99 | keepInSearchMode := false 100 | keepInCompleteMode := false 101 | r, err := o.t.GetRune(deadline) 102 | 103 | if cfg := o.GetConfig(); cfg.FuncFilterInputRune != nil && err == nil { 104 | var process bool 105 | r, process = cfg.FuncFilterInputRune(r) 106 | if !process { 107 | o.buf.Refresh(nil) // to refresh the line 108 | continue // ignore this rune 109 | } 110 | } 111 | 112 | if err == io.EOF { 113 | if o.buf.Len() == 0 { 114 | o.buf.Clean() 115 | return nil, io.EOF 116 | } else { 117 | // if stdin got io.EOF and there is something left in buffer, 118 | // let's flush them by sending CharEnter. 119 | // And we will got io.EOF int next loop. 120 | r = CharEnter 121 | } 122 | } else if err != nil { 123 | return nil, err 124 | } 125 | isUpdateHistory := true 126 | 127 | if o.completer.IsInCompleteSelectMode() { 128 | keepInCompleteMode = o.completer.HandleCompleteSelect(r) 129 | if keepInCompleteMode { 130 | continue 131 | } 132 | 133 | o.buf.Refresh(nil) 134 | switch r { 135 | case CharEnter, CharCtrlJ: 136 | o.history.Update(o.buf.Runes(), false) 137 | fallthrough 138 | case CharInterrupt: 139 | fallthrough 140 | case CharBell: 141 | continue 142 | } 143 | } 144 | 145 | if o.vim.IsEnableVimMode() { 146 | r = o.vim.HandleVim(r, func() rune { 147 | r, err := o.t.GetRune(deadline) 148 | if err == nil { 149 | return r 150 | } else { 151 | return 0 152 | } 153 | }) 154 | if r == 0 { 155 | continue 156 | } 157 | } 158 | 159 | var result []rune 160 | 161 | isTypingRune := false 162 | 163 | switch r { 164 | case CharBell: 165 | if o.search.IsSearchMode() { 166 | o.search.ExitSearchMode(true) 167 | o.buf.Refresh(nil) 168 | } 169 | if o.completer.IsInCompleteMode() { 170 | o.completer.ExitCompleteMode(true) 171 | o.buf.Refresh(nil) 172 | } 173 | case CharBckSearch: 174 | if !o.search.SearchMode(searchDirectionBackward) { 175 | o.t.Bell() 176 | break 177 | } 178 | keepInSearchMode = true 179 | case CharCtrlU: 180 | o.undo.add() 181 | o.buf.KillFront() 182 | case CharFwdSearch: 183 | if !o.search.SearchMode(searchDirectionForward) { 184 | o.t.Bell() 185 | break 186 | } 187 | keepInSearchMode = true 188 | case CharKill: 189 | o.undo.add() 190 | o.buf.Kill() 191 | keepInCompleteMode = true 192 | case MetaForward: 193 | o.buf.MoveToNextWord() 194 | case CharTranspose: 195 | o.undo.add() 196 | o.buf.Transpose() 197 | case MetaBackward: 198 | o.buf.MoveToPrevWord() 199 | case MetaDelete: 200 | o.undo.add() 201 | o.buf.DeleteWord() 202 | case CharLineStart: 203 | o.buf.MoveToLineStart() 204 | case CharLineEnd: 205 | o.buf.MoveToLineEnd() 206 | case CharBackspace, CharCtrlH: 207 | o.undo.add() 208 | if o.search.IsSearchMode() { 209 | o.search.SearchBackspace() 210 | keepInSearchMode = true 211 | break 212 | } 213 | 214 | if o.buf.Len() == 0 { 215 | o.t.Bell() 216 | break 217 | } 218 | o.buf.Backspace() 219 | case CharCtrlZ: 220 | if !platform.IsWindows { 221 | o.buf.Clean() 222 | o.t.SleepToResume() 223 | o.Refresh() 224 | } 225 | case CharCtrlL: 226 | clearScreen(o.t) 227 | o.buf.SetOffset(cursorPosition{1, 1}) 228 | o.Refresh() 229 | case MetaBackspace, CharCtrlW: 230 | o.undo.add() 231 | o.buf.BackEscapeWord() 232 | case MetaShiftTab: 233 | // no-op 234 | case CharCtrlY: 235 | o.buf.Yank() 236 | case CharCtrl_: 237 | o.undo.undo() 238 | case CharEnter, CharCtrlJ: 239 | if o.search.IsSearchMode() { 240 | o.search.ExitSearchMode(false) 241 | } 242 | if o.completer.IsInCompleteMode() { 243 | o.completer.ExitCompleteMode(true) 244 | o.buf.Refresh(nil) 245 | } 246 | o.buf.MoveToLineEnd() 247 | var data []rune 248 | o.buf.WriteRune('\n') 249 | data = o.buf.Reset() 250 | data = data[:len(data)-1] // trim \n 251 | result = data 252 | if !o.GetConfig().DisableAutoSaveHistory { 253 | // ignore IO error 254 | _ = o.history.New(data) 255 | } else { 256 | isUpdateHistory = false 257 | } 258 | o.undo.init() 259 | case CharBackward: 260 | o.buf.MoveBackward() 261 | case CharForward: 262 | o.buf.MoveForward() 263 | case CharPrev: 264 | buf := o.history.Prev() 265 | if buf != nil { 266 | o.buf.Set(buf) 267 | o.undo.init() 268 | } else { 269 | o.t.Bell() 270 | } 271 | case CharNext: 272 | buf, ok := o.history.Next() 273 | if ok { 274 | o.buf.Set(buf) 275 | o.undo.init() 276 | } else { 277 | o.t.Bell() 278 | } 279 | case MetaDeleteKey, CharEOT: 280 | o.undo.add() 281 | // on Delete key or Ctrl-D, attempt to delete a character: 282 | if o.buf.Len() > 0 || !o.IsNormalMode() { 283 | if !o.buf.Delete() { 284 | o.t.Bell() 285 | } 286 | break 287 | } 288 | if r != CharEOT { 289 | break 290 | } 291 | // Ctrl-D on an empty buffer: treated as EOF 292 | o.buf.WriteString(o.GetConfig().EOFPrompt + "\n") 293 | o.buf.Reset() 294 | isUpdateHistory = false 295 | o.history.Revert() 296 | o.buf.Clean() 297 | return nil, io.EOF 298 | case CharInterrupt: 299 | if o.search.IsSearchMode() { 300 | o.search.ExitSearchMode(true) 301 | break 302 | } 303 | if o.completer.IsInCompleteMode() { 304 | o.completer.ExitCompleteMode(true) 305 | o.buf.Refresh(nil) 306 | break 307 | } 308 | o.buf.MoveToLineEnd() 309 | o.buf.Refresh(nil) 310 | hint := o.GetConfig().InterruptPrompt + "\n" 311 | o.buf.WriteString(hint) 312 | remain := o.buf.Reset() 313 | remain = remain[:len(remain)-len([]rune(hint))] 314 | isUpdateHistory = false 315 | o.history.Revert() 316 | return nil, ErrInterrupt 317 | case CharTab: 318 | if o.GetConfig().AutoComplete != nil { 319 | if o.completer.OnComplete() { 320 | if o.completer.IsInCompleteMode() { 321 | keepInCompleteMode = true 322 | continue // redraw is done, loop 323 | } 324 | } else { 325 | o.t.Bell() 326 | } 327 | o.buf.Refresh(nil) 328 | break 329 | } // else: process as a normal input character 330 | fallthrough 331 | default: 332 | isTypingRune = true 333 | if !isTyping { 334 | o.undo.add() 335 | } 336 | if o.search.IsSearchMode() { 337 | o.search.SearchChar(r) 338 | keepInSearchMode = true 339 | break 340 | } 341 | o.buf.WriteRune(r) 342 | if o.completer.IsInCompleteMode() { 343 | o.completer.OnComplete() 344 | if o.completer.IsInCompleteMode() { 345 | keepInCompleteMode = true 346 | } else { 347 | o.buf.Refresh(nil) 348 | } 349 | } 350 | } 351 | 352 | isTyping = isTypingRune 353 | 354 | // suppress the Listener callback if we received Enter or similar and are 355 | // submitting the result, since the buffer has already been cleared: 356 | if result == nil { 357 | if listener := o.GetConfig().Listener; listener != nil { 358 | newLine, newPos, ok := listener(o.buf.Runes(), o.buf.Pos(), r) 359 | if ok { 360 | o.buf.SetWithIdx(newPos, newLine) 361 | } 362 | } 363 | } 364 | 365 | o.m.Lock() 366 | if !keepInSearchMode && o.search.IsSearchMode() { 367 | o.search.ExitSearchMode(false) 368 | o.buf.Refresh(nil) 369 | o.undo.init() 370 | } else if o.completer.IsInCompleteMode() { 371 | if !keepInCompleteMode { 372 | o.completer.ExitCompleteMode(false) 373 | o.refresh() 374 | o.undo.init() 375 | } else { 376 | o.buf.Refresh(nil) 377 | o.completer.CompleteRefresh() 378 | } 379 | } 380 | if isUpdateHistory && !o.search.IsSearchMode() { 381 | // it will cause null history 382 | o.history.Update(o.buf.Runes(), false) 383 | } 384 | o.m.Unlock() 385 | 386 | if result != nil { 387 | return result, nil 388 | } 389 | } 390 | } 391 | 392 | func (o *operation) Stderr() io.Writer { 393 | return o.wrapErr.Load() 394 | } 395 | 396 | func (o *operation) Stdout() io.Writer { 397 | return o.wrapOut.Load() 398 | } 399 | 400 | func (o *operation) String() (string, error) { 401 | r, err := o.Runes() 402 | return string(r), err 403 | } 404 | 405 | func (o *operation) Runes() ([]rune, error) { 406 | o.t.EnterRawMode() 407 | defer o.t.ExitRawMode() 408 | 409 | cfg := o.GetConfig() 410 | listener := cfg.Listener 411 | if listener != nil { 412 | listener(nil, 0, 0) 413 | } 414 | 415 | // Before writing the prompt and starting to read, get a lock 416 | // so we don't race with wrapWriter trying to write and refresh. 417 | o.m.Lock() 418 | o.isPrompting = true 419 | // Query cursor position before printing the prompt as there 420 | // may be existing text on the same line that ideally we don't 421 | // want to overwrite and cause prompt to jump left. 422 | o.getAndSetOffset(nil) 423 | o.buf.Print() // print prompt & buffer contents 424 | // Prompt written safely, unlock until read completes and then 425 | // lock again to unset. 426 | o.m.Unlock() 427 | 428 | if cfg.Undo { 429 | o.undo = newOpUndo(o) 430 | } 431 | 432 | defer func() { 433 | o.m.Lock() 434 | o.isPrompting = false 435 | o.buf.SetOffset(cursorPosition{1, 1}) 436 | o.m.Unlock() 437 | }() 438 | 439 | return o.readline(nil) 440 | } 441 | 442 | func (o *operation) getAndSetOffset(deadline chan struct{}) { 443 | if !o.GetConfig().isInteractive { 444 | return 445 | } 446 | 447 | // Handle lineedge cases where existing text before before 448 | // the prompt is printed would leave us at the right edge of 449 | // the screen but the next character would actually be printed 450 | // at the beginning of the next line. 451 | // TODO ??? 452 | o.t.Write([]byte(" \b")) 453 | 454 | if offset, err := o.t.GetCursorPosition(deadline); err == nil { 455 | o.buf.SetOffset(offset) 456 | } 457 | } 458 | 459 | func (o *operation) GenPasswordConfig() *Config { 460 | baseConfig := o.GetConfig() 461 | return &Config{ 462 | EnableMask: true, 463 | InterruptPrompt: "\n", 464 | EOFPrompt: "\n", 465 | HistoryLimit: -1, 466 | 467 | Stdin: baseConfig.Stdin, 468 | Stdout: baseConfig.Stdout, 469 | Stderr: baseConfig.Stderr, 470 | 471 | FuncIsTerminal: baseConfig.FuncIsTerminal, 472 | FuncMakeRaw: baseConfig.FuncMakeRaw, 473 | FuncExitRaw: baseConfig.FuncExitRaw, 474 | FuncOnWidthChanged: baseConfig.FuncOnWidthChanged, 475 | } 476 | } 477 | 478 | func (o *operation) ReadLineWithConfig(cfg *Config) (string, error) { 479 | backupCfg, err := o.SetConfig(cfg) 480 | if err != nil { 481 | return "", err 482 | } 483 | defer func() { 484 | o.SetConfig(backupCfg) 485 | }() 486 | return o.String() 487 | } 488 | 489 | func (o *operation) SetTitle(t string) { 490 | o.t.Write([]byte("\033[2;" + t + "\007")) 491 | } 492 | 493 | func (o *operation) Slice() ([]byte, error) { 494 | r, err := o.Runes() 495 | if err != nil { 496 | return nil, err 497 | } 498 | return []byte(string(r)), nil 499 | } 500 | 501 | func (o *operation) Close() { 502 | o.history.Close() 503 | } 504 | 505 | func (o *operation) IsNormalMode() bool { 506 | return !o.completer.IsInCompleteMode() && !o.search.IsSearchMode() 507 | } 508 | 509 | func (op *operation) SetConfig(cfg *Config) (*Config, error) { 510 | op.m.Lock() 511 | defer op.m.Unlock() 512 | old := op.t.GetConfig() 513 | if err := cfg.init(); err != nil { 514 | return old, err 515 | } 516 | 517 | // install the config in its canonical location (inside terminal): 518 | op.t.SetConfig(cfg) 519 | 520 | op.wrapOut.Store(&wrapWriter{target: cfg.Stdout, o: op}) 521 | op.wrapErr.Store(&wrapWriter{target: cfg.Stderr, o: op}) 522 | 523 | if op.history == nil { 524 | op.history = newOpHistory(op) 525 | } 526 | if op.search == nil { 527 | op.search = newOpSearch(op.buf.w, op.buf, op.history) 528 | } 529 | 530 | if cfg.AutoComplete != nil && op.completer == nil { 531 | op.completer = newOpCompleter(op.buf.w, op) 532 | } 533 | 534 | return old, nil 535 | } 536 | 537 | func (o *operation) ResetHistory() { 538 | o.history.Reset() 539 | } 540 | 541 | func (o *operation) SaveToHistory(content string) error { 542 | return o.history.New([]rune(content)) 543 | } 544 | 545 | func (o *operation) Refresh() { 546 | o.m.Lock() 547 | defer o.m.Unlock() 548 | o.refresh() 549 | } 550 | 551 | func (o *operation) refresh() { 552 | if o.isPrompting { 553 | o.buf.Refresh(nil) 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /readline.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | 10 | "github.com/ergochat/readline/internal/platform" 11 | ) 12 | 13 | type Instance struct { 14 | terminal *terminal 15 | operation *operation 16 | 17 | closeOnce sync.Once 18 | closeErr error 19 | } 20 | 21 | type Config struct { 22 | // Prompt is the input prompt (ANSI escape sequences are supported on all platforms) 23 | Prompt string 24 | 25 | // HistoryFile is the path to the file where persistent history will be stored 26 | // (empty string disables). 27 | HistoryFile string 28 | // HistoryLimit is the maximum number of history entries to store. If it is 0 29 | // or unset, the default value is 500; set to -1 to disable. 30 | HistoryLimit int 31 | DisableAutoSaveHistory bool 32 | // HistorySearchFold enables case-insensitive history searching. 33 | HistorySearchFold bool 34 | 35 | // AutoComplete defines the tab-completion behavior. See the documentation for 36 | // the AutoCompleter interface for details. 37 | AutoComplete AutoCompleter 38 | 39 | // Listener is an optional callback to intercept keypresses. 40 | Listener Listener 41 | 42 | // Painter is an optional callback to rewrite the buffer for display. 43 | Painter Painter 44 | 45 | // FuncFilterInputRune is an optional callback to translate keyboard inputs; 46 | // it takes in the input rune and returns (translation, ok). If ok is false, 47 | // the rune is skipped. 48 | FuncFilterInputRune func(rune) (rune, bool) 49 | 50 | // VimMode enables Vim-style insert mode by default. 51 | VimMode bool 52 | 53 | InterruptPrompt string 54 | EOFPrompt string 55 | 56 | EnableMask bool 57 | MaskRune rune 58 | 59 | // Undo controls whether to maintain an undo buffer (if enabled, 60 | // Ctrl+_ will undo the previous action). 61 | Undo bool 62 | 63 | // These fields allow customizing terminal handling. Most clients should ignore them. 64 | Stdin io.Reader 65 | Stdout io.Writer 66 | Stderr io.Writer 67 | FuncIsTerminal func() bool 68 | FuncMakeRaw func() error 69 | FuncExitRaw func() error 70 | FuncGetSize func() (width int, height int) 71 | FuncOnWidthChanged func(func()) 72 | 73 | // private fields 74 | inited bool 75 | isInteractive bool 76 | } 77 | 78 | func (c *Config) init() error { 79 | if c.inited { 80 | return nil 81 | } 82 | c.inited = true 83 | if c.Stdin == nil { 84 | c.Stdin = os.Stdin 85 | } 86 | 87 | if c.Stdout == nil { 88 | c.Stdout = os.Stdout 89 | } 90 | if c.Stderr == nil { 91 | c.Stderr = os.Stderr 92 | } 93 | if c.HistoryLimit == 0 { 94 | c.HistoryLimit = 500 95 | } 96 | 97 | if c.InterruptPrompt == "" { 98 | c.InterruptPrompt = "^C" 99 | } else if c.InterruptPrompt == "\n" { 100 | c.InterruptPrompt = "" 101 | } 102 | if c.EOFPrompt == "" { 103 | c.EOFPrompt = "^D" 104 | } else if c.EOFPrompt == "\n" { 105 | c.EOFPrompt = "" 106 | } 107 | 108 | if c.FuncGetSize == nil { 109 | c.FuncGetSize = platform.GetScreenSize 110 | } 111 | if c.FuncIsTerminal == nil { 112 | c.FuncIsTerminal = platform.DefaultIsTerminal 113 | } 114 | rm := new(rawModeHandler) 115 | if c.FuncMakeRaw == nil { 116 | c.FuncMakeRaw = rm.Enter 117 | } 118 | if c.FuncExitRaw == nil { 119 | c.FuncExitRaw = rm.Exit 120 | } 121 | if c.FuncOnWidthChanged == nil { 122 | c.FuncOnWidthChanged = platform.DefaultOnSizeChanged 123 | } 124 | if c.Painter == nil { 125 | c.Painter = defaultPainter 126 | } 127 | 128 | c.isInteractive = c.FuncIsTerminal() 129 | 130 | return nil 131 | } 132 | 133 | // NewFromConfig creates a readline instance from the specified configuration. 134 | func NewFromConfig(cfg *Config) (*Instance, error) { 135 | if err := cfg.init(); err != nil { 136 | return nil, err 137 | } 138 | t, err := newTerminal(cfg) 139 | if err != nil { 140 | return nil, err 141 | } 142 | o := newOperation(t) 143 | return &Instance{ 144 | terminal: t, 145 | operation: o, 146 | }, nil 147 | } 148 | 149 | // NewEx is an alias for NewFromConfig, for compatibility. 150 | var NewEx = NewFromConfig 151 | 152 | // New creates a readline instance with default configuration. 153 | func New(prompt string) (*Instance, error) { 154 | return NewFromConfig(&Config{Prompt: prompt}) 155 | } 156 | 157 | func (i *Instance) ResetHistory() { 158 | i.operation.ResetHistory() 159 | } 160 | 161 | func (i *Instance) SetPrompt(s string) { 162 | cfg := i.GetConfig() 163 | cfg.Prompt = s 164 | i.SetConfig(cfg) 165 | } 166 | 167 | // readline will refresh automatic when write through Stdout() 168 | func (i *Instance) Stdout() io.Writer { 169 | return i.operation.Stdout() 170 | } 171 | 172 | // readline will refresh automatic when write through Stdout() 173 | func (i *Instance) Stderr() io.Writer { 174 | return i.operation.Stderr() 175 | } 176 | 177 | // switch VimMode in runtime 178 | func (i *Instance) SetVimMode(on bool) { 179 | cfg := i.GetConfig() 180 | cfg.VimMode = on 181 | i.SetConfig(cfg) 182 | } 183 | 184 | func (i *Instance) IsVimMode() bool { 185 | return i.operation.vim.IsEnableVimMode() 186 | } 187 | 188 | // GeneratePasswordConfig generates a suitable Config for reading passwords; 189 | // this config can be modified and then used with ReadLineWithConfig, or 190 | // SetConfig. 191 | func (i *Instance) GeneratePasswordConfig() *Config { 192 | return i.operation.GenPasswordConfig() 193 | } 194 | 195 | func (i *Instance) ReadLineWithConfig(cfg *Config) (string, error) { 196 | return i.operation.ReadLineWithConfig(cfg) 197 | } 198 | 199 | func (i *Instance) ReadPassword(prompt string) ([]byte, error) { 200 | if result, err := i.ReadLineWithConfig(i.GeneratePasswordConfig()); err == nil { 201 | return []byte(result), nil 202 | } else { 203 | return nil, err 204 | } 205 | } 206 | 207 | // ReadLine reads a line from the configured input source, allowing inline editing. 208 | // The returned error is either nil, io.EOF, or readline.ErrInterrupt. 209 | func (i *Instance) ReadLine() (string, error) { 210 | return i.operation.String() 211 | } 212 | 213 | // Readline is an alias for ReadLine, for compatibility. 214 | func (i *Instance) Readline() (string, error) { 215 | return i.ReadLine() 216 | } 217 | 218 | // SetDefault prefills a default value for the next call to Readline() 219 | // or related methods. The value will appear after the prompt for the user 220 | // to edit, with the cursor at the end of the line. 221 | func (i *Instance) SetDefault(defaultValue string) { 222 | i.operation.SetBuffer(defaultValue) 223 | } 224 | 225 | func (i *Instance) ReadLineWithDefault(defaultValue string) (string, error) { 226 | i.SetDefault(defaultValue) 227 | return i.operation.String() 228 | } 229 | 230 | // SaveToHistory adds a string to the instance's stored history. This is particularly 231 | // relevant when DisableAutoSaveHistory is configured. 232 | func (i *Instance) SaveToHistory(content string) error { 233 | return i.operation.SaveToHistory(content) 234 | } 235 | 236 | // same as readline 237 | func (i *Instance) ReadSlice() ([]byte, error) { 238 | return i.operation.Slice() 239 | } 240 | 241 | // Close() closes the readline instance, cleaning up state changes to the 242 | // terminal. It interrupts any concurrent Readline() operation, so it can be 243 | // asynchronously or from a signal handler. It is concurrency-safe and 244 | // idempotent, so it can be called multiple times. 245 | func (i *Instance) Close() error { 246 | i.closeOnce.Do(func() { 247 | // TODO reorder these? 248 | i.operation.Close() 249 | i.closeErr = i.terminal.Close() 250 | }) 251 | return i.closeErr 252 | } 253 | 254 | // CaptureExitSignal registers handlers for common exit signals that will 255 | // close the readline instance. 256 | func (i *Instance) CaptureExitSignal() { 257 | cSignal := make(chan os.Signal, 1) 258 | // TODO handle other signals in a portable way? 259 | signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 260 | go func() { 261 | for range cSignal { 262 | i.Close() 263 | } 264 | }() 265 | } 266 | 267 | // Write writes output to the screen, redrawing the prompt and buffer 268 | // as needed. 269 | func (i *Instance) Write(b []byte) (int, error) { 270 | return i.Stdout().Write(b) 271 | } 272 | 273 | // GetConfig returns a copy of the current config. 274 | func (i *Instance) GetConfig() *Config { 275 | cfg := i.operation.GetConfig() 276 | result := new(Config) 277 | *result = *cfg 278 | return result 279 | } 280 | 281 | // SetConfig modifies the current instance's config. 282 | func (i *Instance) SetConfig(cfg *Config) error { 283 | _, err := i.operation.SetConfig(cfg) 284 | return err 285 | } 286 | 287 | // Refresh redraws the input buffer on screen. 288 | func (i *Instance) Refresh() { 289 | i.operation.Refresh() 290 | } 291 | 292 | // DisableHistory disables the saving of input lines in history. 293 | func (i *Instance) DisableHistory() { 294 | i.operation.history.Disable() 295 | } 296 | 297 | // EnableHistory enables the saving of input lines in history. 298 | func (i *Instance) EnableHistory() { 299 | i.operation.history.Enable() 300 | } 301 | 302 | // ClearScreen clears the screen. 303 | func (i *Instance) ClearScreen() { 304 | clearScreen(i.operation.Stdout()) 305 | } 306 | 307 | // Painter is a callback type to allow modifying the buffer before it is rendered 308 | // on screen, for example, to implement real-time syntax highlighting. 309 | type Painter func(line []rune, pos int) []rune 310 | 311 | func defaultPainter(line []rune, _ int) []rune { 312 | return line 313 | } 314 | 315 | // Listener is a callback type to listen for keypresses while the line is being 316 | // edited. It is invoked initially with (nil, 0, 0), and then subsequently for 317 | // any keypress until (but not including) the newline/enter keypress that completes 318 | // the input. 319 | type Listener func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) 320 | -------------------------------------------------------------------------------- /readline_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRace(t *testing.T) { 9 | rl, err := NewFromConfig(&Config{}) 10 | if err != nil { 11 | t.Fatal(err) 12 | return 13 | } 14 | 15 | go func() { 16 | for range time.Tick(time.Millisecond) { 17 | rl.SetPrompt("hello") 18 | } 19 | }() 20 | 21 | go func() { 22 | time.Sleep(100 * time.Millisecond) 23 | rl.Close() 24 | }() 25 | 26 | rl.Readline() 27 | } 28 | 29 | func TestParseCPRResponse(t *testing.T) { 30 | badResponses := []string{ 31 | "", 32 | ";", 33 | "\x00", 34 | "\x00;", 35 | ";\x00", 36 | "x", 37 | "1;a", 38 | "a;1", 39 | "a;1;", 40 | "1;1;", 41 | "1;1;1", 42 | } 43 | for _, response := range badResponses { 44 | if _, err := parseCPRResponse([]byte(response)); err == nil { 45 | t.Fatalf("expected parsing of `%s` to fail, but did not", response) 46 | } 47 | } 48 | 49 | goodResponses := []struct { 50 | input string 51 | output cursorPosition 52 | }{ 53 | {"1;2", cursorPosition{1, 2}}, 54 | {"0;2", cursorPosition{0, 2}}, 55 | {"0;0", cursorPosition{0, 0}}, 56 | {"48378;9999999", cursorPosition{48378, 9999999}}, 57 | } 58 | 59 | for _, response := range goodResponses { 60 | got, err := parseCPRResponse([]byte(response.input)) 61 | if err != nil { 62 | t.Fatalf("could not parse `%s`: %v", response.input, err) 63 | } 64 | if got != response.output { 65 | t.Fatalf("expected %s to parse to %#v, got %#v", response.input, response.output, got) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /runebuf.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/ergochat/readline/internal/runes" 12 | ) 13 | 14 | type runeBuffer struct { 15 | buf []rune 16 | idx int 17 | w *terminal 18 | 19 | cpos cursorPosition 20 | ppos int // prompt start position (0 == column 1) 21 | 22 | lastKill []rune 23 | 24 | sync.Mutex 25 | } 26 | 27 | func (r *runeBuffer) pushKill(text []rune) { 28 | r.lastKill = append([]rune{}, text...) 29 | } 30 | 31 | func newRuneBuffer(w *terminal) *runeBuffer { 32 | rb := &runeBuffer{ 33 | w: w, 34 | } 35 | return rb 36 | } 37 | 38 | func (r *runeBuffer) CurrentWidth(x int) int { 39 | r.Lock() 40 | defer r.Unlock() 41 | return runes.WidthAll(r.buf[:x]) 42 | } 43 | 44 | func (r *runeBuffer) PromptLen() int { 45 | r.Lock() 46 | defer r.Unlock() 47 | return r.promptLen() 48 | } 49 | 50 | func (r *runeBuffer) promptLen() int { 51 | return runes.WidthAll(runes.ColorFilter([]rune(r.prompt()))) 52 | } 53 | 54 | func (r *runeBuffer) RuneSlice(i int) []rune { 55 | r.Lock() 56 | defer r.Unlock() 57 | 58 | if i > 0 { 59 | rs := make([]rune, i) 60 | copy(rs, r.buf[r.idx:r.idx+i]) 61 | return rs 62 | } 63 | rs := make([]rune, -i) 64 | copy(rs, r.buf[r.idx+i:r.idx]) 65 | return rs 66 | } 67 | 68 | func (r *runeBuffer) Runes() []rune { 69 | r.Lock() 70 | newr := make([]rune, len(r.buf)) 71 | copy(newr, r.buf) 72 | r.Unlock() 73 | return newr 74 | } 75 | 76 | func (r *runeBuffer) Pos() int { 77 | r.Lock() 78 | defer r.Unlock() 79 | return r.idx 80 | } 81 | 82 | func (r *runeBuffer) Len() int { 83 | r.Lock() 84 | defer r.Unlock() 85 | return len(r.buf) 86 | } 87 | 88 | func (r *runeBuffer) MoveToLineStart() { 89 | r.Refresh(func() { 90 | r.idx = 0 91 | }) 92 | } 93 | 94 | func (r *runeBuffer) MoveBackward() { 95 | r.Refresh(func() { 96 | if r.idx == 0 { 97 | return 98 | } 99 | r.idx-- 100 | }) 101 | } 102 | 103 | func (r *runeBuffer) WriteString(s string) { 104 | r.WriteRunes([]rune(s)) 105 | } 106 | 107 | func (r *runeBuffer) WriteRune(s rune) { 108 | r.WriteRunes([]rune{s}) 109 | } 110 | 111 | func (r *runeBuffer) getConfig() *Config { 112 | return r.w.GetConfig() 113 | } 114 | 115 | func (r *runeBuffer) isInteractive() bool { 116 | return r.getConfig().isInteractive 117 | } 118 | 119 | func (r *runeBuffer) prompt() string { 120 | return r.getConfig().Prompt 121 | } 122 | 123 | func (r *runeBuffer) WriteRunes(s []rune) { 124 | r.Lock() 125 | defer r.Unlock() 126 | 127 | if r.idx == len(r.buf) { 128 | // cursor is already at end of buf data so just call 129 | // append instead of refesh to save redrawing. 130 | r.buf = append(r.buf, s...) 131 | r.idx += len(s) 132 | if r.isInteractive() { 133 | r.append(s) 134 | } 135 | } else { 136 | // writing into the data somewhere so do a refresh 137 | r.refresh(func() { 138 | tail := append(s, r.buf[r.idx:]...) 139 | r.buf = append(r.buf[:r.idx], tail...) 140 | r.idx += len(s) 141 | }) 142 | } 143 | } 144 | 145 | func (r *runeBuffer) MoveForward() { 146 | r.Refresh(func() { 147 | if r.idx == len(r.buf) { 148 | return 149 | } 150 | r.idx++ 151 | }) 152 | } 153 | 154 | func (r *runeBuffer) IsCursorInEnd() bool { 155 | r.Lock() 156 | defer r.Unlock() 157 | return r.idx == len(r.buf) 158 | } 159 | 160 | func (r *runeBuffer) Replace(ch rune) { 161 | r.Refresh(func() { 162 | r.buf[r.idx] = ch 163 | }) 164 | } 165 | 166 | func (r *runeBuffer) Erase() { 167 | r.Refresh(func() { 168 | r.idx = 0 169 | r.pushKill(r.buf[:]) 170 | r.buf = r.buf[:0] 171 | }) 172 | } 173 | 174 | func (r *runeBuffer) Delete() (success bool) { 175 | r.Refresh(func() { 176 | if r.idx == len(r.buf) { 177 | return 178 | } 179 | r.pushKill(r.buf[r.idx : r.idx+1]) 180 | r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) 181 | success = true 182 | }) 183 | return 184 | } 185 | 186 | func (r *runeBuffer) DeleteWord() { 187 | if r.idx == len(r.buf) { 188 | return 189 | } 190 | init := r.idx 191 | for init < len(r.buf) && runes.IsWordBreak(r.buf[init]) { 192 | init++ 193 | } 194 | for i := init + 1; i < len(r.buf); i++ { 195 | if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { 196 | r.pushKill(r.buf[r.idx : i-1]) 197 | r.Refresh(func() { 198 | r.buf = append(r.buf[:r.idx], r.buf[i-1:]...) 199 | }) 200 | return 201 | } 202 | } 203 | r.Kill() 204 | } 205 | 206 | func (r *runeBuffer) MoveToPrevWord() (success bool) { 207 | r.Refresh(func() { 208 | if r.idx == 0 { 209 | return 210 | } 211 | 212 | for i := r.idx - 1; i > 0; i-- { 213 | if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { 214 | r.idx = i 215 | success = true 216 | return 217 | } 218 | } 219 | r.idx = 0 220 | success = true 221 | }) 222 | return 223 | } 224 | 225 | func (r *runeBuffer) KillFront() { 226 | r.Refresh(func() { 227 | if r.idx == 0 { 228 | return 229 | } 230 | 231 | length := len(r.buf) - r.idx 232 | r.pushKill(r.buf[:r.idx]) 233 | copy(r.buf[:length], r.buf[r.idx:]) 234 | r.idx = 0 235 | r.buf = r.buf[:length] 236 | }) 237 | } 238 | 239 | func (r *runeBuffer) Kill() { 240 | r.Refresh(func() { 241 | r.pushKill(r.buf[r.idx:]) 242 | r.buf = r.buf[:r.idx] 243 | }) 244 | } 245 | 246 | func (r *runeBuffer) Transpose() { 247 | r.Refresh(func() { 248 | if r.idx == 0 { 249 | // match the GNU Readline behavior, Ctrl-T at the start of the line 250 | // is a no-op: 251 | return 252 | } 253 | 254 | // OK, we have at least one character behind us: 255 | if r.idx < len(r.buf) { 256 | // swap the character in front of us with the one behind us 257 | r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx] 258 | // advance the cursor 259 | r.idx++ 260 | } else if r.idx == len(r.buf) && len(r.buf) >= 2 { 261 | // swap the two characters behind us 262 | r.buf[r.idx-2], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx-2] 263 | // leave the cursor in place since there's nowhere to go 264 | } 265 | }) 266 | } 267 | 268 | func (r *runeBuffer) MoveToNextWord() { 269 | r.Refresh(func() { 270 | for i := r.idx + 1; i < len(r.buf); i++ { 271 | if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { 272 | r.idx = i 273 | return 274 | } 275 | } 276 | 277 | r.idx = len(r.buf) 278 | }) 279 | } 280 | 281 | func (r *runeBuffer) MoveToEndWord() { 282 | r.Refresh(func() { 283 | // already at the end, so do nothing 284 | if r.idx == len(r.buf) { 285 | return 286 | } 287 | // if we are at the end of a word already, go to next 288 | if !runes.IsWordBreak(r.buf[r.idx]) && runes.IsWordBreak(r.buf[r.idx+1]) { 289 | r.idx++ 290 | } 291 | 292 | // keep going until at the end of a word 293 | for i := r.idx + 1; i < len(r.buf); i++ { 294 | if runes.IsWordBreak(r.buf[i]) && !runes.IsWordBreak(r.buf[i-1]) { 295 | r.idx = i - 1 296 | return 297 | } 298 | } 299 | r.idx = len(r.buf) 300 | }) 301 | } 302 | 303 | func (r *runeBuffer) BackEscapeWord() { 304 | r.Refresh(func() { 305 | if r.idx == 0 { 306 | return 307 | } 308 | for i := r.idx - 1; i >= 0; i-- { 309 | if i == 0 || (runes.IsWordBreak(r.buf[i-1])) && !runes.IsWordBreak(r.buf[i]) { 310 | r.pushKill(r.buf[i:r.idx]) 311 | r.buf = append(r.buf[:i], r.buf[r.idx:]...) 312 | r.idx = i 313 | return 314 | } 315 | } 316 | 317 | r.buf = r.buf[:0] 318 | r.idx = 0 319 | }) 320 | } 321 | 322 | func (r *runeBuffer) Yank() { 323 | if len(r.lastKill) == 0 { 324 | return 325 | } 326 | r.Refresh(func() { 327 | buf := make([]rune, 0, len(r.buf)+len(r.lastKill)) 328 | buf = append(buf, r.buf[:r.idx]...) 329 | buf = append(buf, r.lastKill...) 330 | buf = append(buf, r.buf[r.idx:]...) 331 | r.buf = buf 332 | r.idx += len(r.lastKill) 333 | }) 334 | } 335 | 336 | func (r *runeBuffer) Backspace() { 337 | r.Refresh(func() { 338 | if r.idx == 0 { 339 | return 340 | } 341 | 342 | r.idx-- 343 | r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) 344 | }) 345 | } 346 | 347 | func (r *runeBuffer) MoveToLineEnd() { 348 | r.Lock() 349 | defer r.Unlock() 350 | if r.idx == len(r.buf) { 351 | return 352 | } 353 | r.refresh(func() { 354 | r.idx = len(r.buf) 355 | }) 356 | } 357 | 358 | // LineCount returns number of lines the buffer takes as it appears in the terminal. 359 | func (r *runeBuffer) LineCount() int { 360 | sp := r.getSplitByLine(r.buf, 1) 361 | return len(sp) 362 | } 363 | 364 | func (r *runeBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { 365 | r.Refresh(func() { 366 | if reverse { 367 | for i := r.idx - 1; i >= 0; i-- { 368 | if r.buf[i] == ch { 369 | r.idx = i 370 | if prevChar { 371 | r.idx++ 372 | } 373 | success = true 374 | return 375 | } 376 | } 377 | return 378 | } 379 | for i := r.idx + 1; i < len(r.buf); i++ { 380 | if r.buf[i] == ch { 381 | r.idx = i 382 | if prevChar { 383 | r.idx-- 384 | } 385 | success = true 386 | return 387 | } 388 | } 389 | }) 390 | return 391 | } 392 | 393 | func (r *runeBuffer) isInLineEdge() bool { 394 | sp := r.getSplitByLine(r.buf, 1) 395 | return len(sp[len(sp)-1]) == 0 // last line is 0 len 396 | } 397 | 398 | func (r *runeBuffer) getSplitByLine(rs []rune, nextWidth int) [][]rune { 399 | tWidth, _ := r.w.GetWidthHeight() 400 | cfg := r.getConfig() 401 | if cfg.EnableMask { 402 | w := runes.Width(cfg.MaskRune) 403 | masked := []rune(strings.Repeat(string(cfg.MaskRune), len(rs))) 404 | return runes.SplitByLine(runes.ColorFilter([]rune(r.prompt())), masked, r.ppos, tWidth, w) 405 | } else { 406 | return runes.SplitByLine(runes.ColorFilter([]rune(r.prompt())), rs, r.ppos, tWidth, nextWidth) 407 | } 408 | } 409 | 410 | func (r *runeBuffer) IdxLine(width int) int { 411 | r.Lock() 412 | defer r.Unlock() 413 | return r.idxLine(width) 414 | } 415 | 416 | func (r *runeBuffer) idxLine(width int) int { 417 | if width == 0 { 418 | return 0 419 | } 420 | nextWidth := 1 421 | if r.idx < len(r.buf) { 422 | nextWidth = runes.Width(r.buf[r.idx]) 423 | } 424 | sp := r.getSplitByLine(r.buf[:r.idx], nextWidth) 425 | return len(sp) - 1 426 | } 427 | 428 | func (r *runeBuffer) CursorLineCount() int { 429 | tWidth, _ := r.w.GetWidthHeight() 430 | return r.LineCount() - r.IdxLine(tWidth) 431 | } 432 | 433 | func (r *runeBuffer) Refresh(f func()) { 434 | r.Lock() 435 | defer r.Unlock() 436 | r.refresh(f) 437 | } 438 | 439 | func (r *runeBuffer) refresh(f func()) { 440 | if !r.isInteractive() { 441 | if f != nil { 442 | f() 443 | } 444 | return 445 | } 446 | 447 | r.clean() 448 | if f != nil { 449 | f() 450 | } 451 | r.print() 452 | } 453 | 454 | func (r *runeBuffer) SetOffset(position cursorPosition) { 455 | r.Lock() 456 | defer r.Unlock() 457 | r.setOffset(position) 458 | } 459 | 460 | func (r *runeBuffer) setOffset(cpos cursorPosition) { 461 | r.cpos = cpos 462 | tWidth, _ := r.w.GetWidthHeight() 463 | if cpos.col > 0 && cpos.col < tWidth { 464 | r.ppos = cpos.col - 1 // c should be 1..tWidth 465 | } else { 466 | r.ppos = 0 467 | } 468 | } 469 | 470 | // append s to the end of the current output. append is called in 471 | // place of print() when clean() was avoided. As output is appended on 472 | // the end, the cursor also needs no extra adjustment. 473 | // NOTE: assumes len(s) >= 1 which should always be true for append. 474 | func (r *runeBuffer) append(s []rune) { 475 | buf := bytes.NewBuffer(nil) 476 | slen := len(s) 477 | cfg := r.getConfig() 478 | if cfg.EnableMask { 479 | if slen > 1 && cfg.MaskRune != 0 { 480 | // write a mask character for all runes except the last rune 481 | buf.WriteString(strings.Repeat(string(cfg.MaskRune), slen-1)) 482 | } 483 | // for the last rune, write \n or mask it otherwise. 484 | if s[slen-1] == '\n' { 485 | buf.WriteRune('\n') 486 | } else if cfg.MaskRune != 0 { 487 | buf.WriteRune(cfg.MaskRune) 488 | } 489 | } else { 490 | for _, e := range cfg.Painter(s, slen) { 491 | if e == '\t' { 492 | buf.WriteString(strings.Repeat(" ", runes.TabWidth)) 493 | } else { 494 | buf.WriteRune(e) 495 | } 496 | } 497 | } 498 | if r.isInLineEdge() { 499 | buf.WriteString(" \b") 500 | } 501 | r.w.Write(buf.Bytes()) 502 | } 503 | 504 | // Print writes out the prompt and buffer contents at the current cursor position 505 | func (r *runeBuffer) Print() { 506 | r.Lock() 507 | defer r.Unlock() 508 | if !r.isInteractive() { 509 | return 510 | } 511 | r.print() 512 | } 513 | 514 | func (r *runeBuffer) print() { 515 | r.w.Write(r.output()) 516 | } 517 | 518 | func (r *runeBuffer) output() []byte { 519 | buf := bytes.NewBuffer(nil) 520 | buf.WriteString(r.prompt()) 521 | buf.WriteString("\x1b[0K") // VT100 "Clear line from cursor right", see #38 522 | cfg := r.getConfig() 523 | if cfg.EnableMask && len(r.buf) > 0 { 524 | if cfg.MaskRune != 0 { 525 | buf.WriteString(strings.Repeat(string(cfg.MaskRune), len(r.buf)-1)) 526 | } 527 | if r.buf[len(r.buf)-1] == '\n' { 528 | buf.WriteRune('\n') 529 | } else if cfg.MaskRune != 0 { 530 | buf.WriteRune(cfg.MaskRune) 531 | } 532 | } else { 533 | for _, e := range cfg.Painter(r.buf, r.idx) { 534 | if e == '\t' { 535 | buf.WriteString(strings.Repeat(" ", runes.TabWidth)) 536 | } else { 537 | buf.WriteRune(e) 538 | } 539 | } 540 | } 541 | if r.isInLineEdge() { 542 | buf.WriteString(" \b") 543 | } 544 | // cursor position 545 | if len(r.buf) > r.idx { 546 | buf.Write(r.getBackspaceSequence()) 547 | } 548 | return buf.Bytes() 549 | } 550 | 551 | func (r *runeBuffer) getBackspaceSequence() []byte { 552 | bcnt := len(r.buf) - r.idx // backwards count to index 553 | sp := r.getSplitByLine(r.buf, 1) 554 | 555 | // Calculate how many lines up to the index line 556 | up := 0 557 | spi := len(sp) - 1 558 | for spi >= 0 { 559 | bcnt -= len(sp[spi]) 560 | if bcnt <= 0 { 561 | break 562 | } 563 | up++ 564 | spi-- 565 | } 566 | 567 | // Calculate what column the index should be set to 568 | column := 1 569 | if spi == 0 { 570 | column += r.ppos 571 | } 572 | for _, rune := range sp[spi] { 573 | if bcnt >= 0 { 574 | break 575 | } 576 | column += runes.Width(rune) 577 | bcnt++ 578 | } 579 | 580 | buf := bytes.NewBuffer(nil) 581 | if up > 0 { 582 | fmt.Fprintf(buf, "\033[%dA", up) // move cursor up to index line 583 | } 584 | fmt.Fprintf(buf, "\033[%dG", column) // move cursor to column 585 | 586 | return buf.Bytes() 587 | } 588 | 589 | func (r *runeBuffer) CopyForUndo(prev []rune) (cur []rune, idx int, changed bool) { 590 | if runes.Equal(r.buf, prev) { 591 | return prev, r.idx, false 592 | } else { 593 | return runes.Copy(r.buf), r.idx, true 594 | } 595 | } 596 | 597 | func (r *runeBuffer) Restore(buf []rune, idx int) { 598 | r.buf = buf 599 | r.idx = idx 600 | } 601 | 602 | func (r *runeBuffer) Reset() []rune { 603 | ret := runes.Copy(r.buf) 604 | r.buf = r.buf[:0] 605 | r.idx = 0 606 | return ret 607 | } 608 | 609 | func (r *runeBuffer) calWidth(m int) int { 610 | if m > 0 { 611 | return runes.WidthAll(r.buf[r.idx : r.idx+m]) 612 | } 613 | return runes.WidthAll(r.buf[r.idx+m : r.idx]) 614 | } 615 | 616 | func (r *runeBuffer) SetStyle(start, end int, style string) { 617 | if end < start { 618 | panic("end < start") 619 | } 620 | 621 | // goto start 622 | move := start - r.idx 623 | if move > 0 { 624 | r.w.Write([]byte(string(r.buf[r.idx : r.idx+move]))) 625 | } else { 626 | r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move))) 627 | } 628 | r.w.Write([]byte("\033[" + style + "m")) 629 | r.w.Write([]byte(string(r.buf[start:end]))) 630 | r.w.Write([]byte("\033[0m")) 631 | // TODO: move back 632 | } 633 | 634 | func (r *runeBuffer) SetWithIdx(idx int, buf []rune) { 635 | r.Refresh(func() { 636 | r.buf = buf 637 | r.idx = idx 638 | }) 639 | } 640 | 641 | func (r *runeBuffer) Set(buf []rune) { 642 | r.SetWithIdx(len(buf), buf) 643 | } 644 | 645 | func (r *runeBuffer) SetNoRefresh(buf []rune) { 646 | r.buf = buf 647 | r.idx = len(buf) 648 | } 649 | 650 | func (r *runeBuffer) cleanOutput(w io.Writer, idxLine int) { 651 | buf := bufio.NewWriter(w) 652 | 653 | tWidth, _ := r.w.GetWidthHeight() 654 | if tWidth == 0 { 655 | buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen())) 656 | buf.Write([]byte("\033[J")) 657 | } else { 658 | if idxLine > 0 { 659 | fmt.Fprintf(buf, "\033[%dA", idxLine) // move cursor up by idxLine 660 | } 661 | fmt.Fprintf(buf, "\033[%dG", r.ppos+1) // move cursor back to initial ppos position 662 | buf.Write([]byte("\033[J")) // clear from cursor to end of screen 663 | } 664 | buf.Flush() 665 | return 666 | } 667 | 668 | func (r *runeBuffer) Clean() { 669 | r.Lock() 670 | r.clean() 671 | r.Unlock() 672 | } 673 | 674 | func (r *runeBuffer) clean() { 675 | tWidth, _ := r.w.GetWidthHeight() 676 | r.cleanWithIdxLine(r.idxLine(tWidth)) 677 | } 678 | 679 | func (r *runeBuffer) cleanWithIdxLine(idxLine int) { 680 | if !r.isInteractive() { 681 | return 682 | } 683 | r.cleanOutput(r.w, idxLine) 684 | } 685 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "fmt" 7 | "sync" 8 | ) 9 | 10 | type searchState uint 11 | 12 | const ( 13 | searchStateFound searchState = iota 14 | searchStateFailing 15 | ) 16 | 17 | type searchDirection uint 18 | 19 | const ( 20 | searchDirectionForward searchDirection = iota 21 | searchDirectionBackward 22 | ) 23 | 24 | type opSearch struct { 25 | mutex sync.Mutex 26 | inMode bool 27 | state searchState 28 | dir searchDirection 29 | source *list.Element 30 | w *terminal 31 | buf *runeBuffer 32 | data []rune 33 | history *opHistory 34 | markStart int 35 | markEnd int 36 | } 37 | 38 | func newOpSearch(w *terminal, buf *runeBuffer, history *opHistory) *opSearch { 39 | return &opSearch{ 40 | w: w, 41 | buf: buf, 42 | history: history, 43 | } 44 | } 45 | 46 | func (o *opSearch) IsSearchMode() bool { 47 | o.mutex.Lock() 48 | defer o.mutex.Unlock() 49 | return o.inMode 50 | } 51 | 52 | func (o *opSearch) SearchBackspace() { 53 | o.mutex.Lock() 54 | defer o.mutex.Unlock() 55 | if len(o.data) > 0 { 56 | o.data = o.data[:len(o.data)-1] 57 | o.search(true) 58 | } 59 | } 60 | 61 | func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) { 62 | if o.dir == searchDirectionBackward { 63 | return o.history.FindBck(isNewSearch, o.data, o.buf.idx) 64 | } 65 | return o.history.FindFwd(isNewSearch, o.data, o.buf.idx) 66 | } 67 | 68 | func (o *opSearch) search(isChange bool) bool { 69 | if len(o.data) == 0 { 70 | o.state = searchStateFound 71 | o.searchRefresh(-1) 72 | return true 73 | } 74 | idx, elem := o.findHistoryBy(isChange) 75 | if elem == nil { 76 | o.searchRefresh(-2) 77 | return false 78 | } 79 | o.history.current = elem 80 | 81 | item := o.history.showItem(o.history.current.Value) 82 | start, end := 0, 0 83 | if o.dir == searchDirectionBackward { 84 | start, end = idx, idx+len(o.data) 85 | } else { 86 | start, end = idx, idx+len(o.data) 87 | idx += len(o.data) 88 | } 89 | o.buf.SetWithIdx(idx, item) 90 | o.markStart, o.markEnd = start, end 91 | o.searchRefresh(idx) 92 | return true 93 | } 94 | 95 | func (o *opSearch) SearchChar(r rune) { 96 | o.mutex.Lock() 97 | defer o.mutex.Unlock() 98 | 99 | o.data = append(o.data, r) 100 | o.search(true) 101 | } 102 | 103 | func (o *opSearch) SearchMode(dir searchDirection) bool { 104 | o.mutex.Lock() 105 | defer o.mutex.Unlock() 106 | 107 | tWidth, _ := o.w.GetWidthHeight() 108 | if tWidth == 0 { 109 | return false 110 | } 111 | alreadyInMode := o.inMode 112 | o.inMode = true 113 | o.dir = dir 114 | o.source = o.history.current 115 | if alreadyInMode { 116 | o.search(false) 117 | } else { 118 | o.searchRefresh(-1) 119 | } 120 | return true 121 | } 122 | 123 | func (o *opSearch) ExitSearchMode(revert bool) { 124 | o.mutex.Lock() 125 | defer o.mutex.Unlock() 126 | 127 | if revert { 128 | o.history.current = o.source 129 | var redrawValue []rune 130 | if o.history.current != nil { 131 | redrawValue = o.history.showItem(o.history.current.Value) 132 | } 133 | o.buf.Set(redrawValue) 134 | } 135 | o.markStart, o.markEnd = 0, 0 136 | o.state = searchStateFound 137 | o.inMode = false 138 | o.source = nil 139 | o.data = nil 140 | } 141 | 142 | func (o *opSearch) searchRefresh(x int) { 143 | tWidth, _ := o.w.GetWidthHeight() 144 | if x == -2 { 145 | o.state = searchStateFailing 146 | } else if x >= 0 { 147 | o.state = searchStateFound 148 | } 149 | if x < 0 { 150 | x = o.buf.idx 151 | } 152 | x = o.buf.CurrentWidth(x) 153 | x += o.buf.PromptLen() 154 | x = x % tWidth 155 | 156 | if o.markStart > 0 { 157 | o.buf.SetStyle(o.markStart, o.markEnd, "4") 158 | } 159 | 160 | lineCnt := o.buf.CursorLineCount() 161 | buf := bytes.NewBuffer(nil) 162 | buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) 163 | buf.WriteString("\033[J") 164 | if o.state == searchStateFailing { 165 | buf.WriteString("failing ") 166 | } 167 | if o.dir == searchDirectionBackward { 168 | buf.WriteString("bck") 169 | } else if o.dir == searchDirectionForward { 170 | buf.WriteString("fwd") 171 | } 172 | buf.WriteString("-i-search: ") 173 | buf.WriteString(string(o.data)) // keyword 174 | buf.WriteString("\033[4m \033[0m") // _ 175 | fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev 176 | if x > 0 { 177 | fmt.Fprintf(buf, "\033[%dC", x) // move forward 178 | } 179 | o.w.Write(buf.Bytes()) 180 | } 181 | 182 | func (o *opSearch) RefreshIfNeeded() { 183 | o.mutex.Lock() 184 | defer o.mutex.Unlock() 185 | 186 | if o.inMode { 187 | o.searchRefresh(-1) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strconv" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/ergochat/readline/internal/ansi" 15 | "github.com/ergochat/readline/internal/platform" 16 | ) 17 | 18 | const ( 19 | // see waitForDSR 20 | dsrTimeout = 250 * time.Millisecond 21 | 22 | maxAnsiLen = 32 23 | 24 | // how many non-CPR reads to buffer while waiting for a CPR response 25 | maxCPRBufferLen = 128 * 1024 26 | ) 27 | 28 | var ( 29 | deadlineExceeded = errors.New("deadline exceeded") 30 | concurrentReads = errors.New("concurrent read operations detected") 31 | invalidCPR = errors.New("invalid CPR response") 32 | ) 33 | 34 | /* 35 | terminal manages terminal input. The design constraints here are somewhat complex: 36 | 37 | 1. Calls to (*Instance).Readline() must always be preemptible by (*Instance).Close. 38 | This could be handled at the Operation layer instead; however, it's cleaner 39 | to provide an API in terminal itself that can interrupt attempts to read. 40 | 2. In between calls to Readline(), or *after* a call to (*Instance).Close(), 41 | stdin must be available for code outside of this library to read from. The 42 | problem is that reads from stdin in Go are not preemptible (see, for example, 43 | https://github.com/golang/go/issues/24842 ). In the worst case, an 44 | interrupted read will leave (*terminal).ioloop() running, and it will 45 | consume one more user keystroke before it exits. However, it is a design goal 46 | to read as little as possible at a time. 47 | 3. We have to handle the DSR ("device status report") query and the 48 | CPR ("cursor position report") response: 49 | https://vt100.net/docs/vt510-rm/DSR-CPR.html 50 | This involves writing an ANSI escape sequence to stdout, then waiting 51 | for the terminal to asynchronously write an ANSI escape sequence to stdin. 52 | We have to pick this value out of the stream and process it without 53 | disrupting the handling of actual user input. Moreover, concurrent Close() 54 | while a CPR query is in flight should ensure (if possible) that the 55 | response is actually read; otherwise the response may be printed to the 56 | screen, disrupting the user experience. 57 | 58 | Accordingly, the concurrency design is as follows: 59 | 60 | 1. ioloop() runs asynchronously. It operates in lockstep with the read methods: 61 | each synchronous receive from kickChan is matched with a synchronous send to 62 | outChan. It does blocking reads from stdin, reading as little as possible at 63 | a time, and passing the results back over outChan. 64 | 2. The read methods ("internal public API") GetRune() and GetCursorPosition() 65 | are not concurrency-safe and must be called in serial. They are backed by 66 | readFromStdin, which wakes ioloop() if necessary and waits for a response. 67 | If GetCursorPosition() reads non-CPR data, it will buffer it for GetRune() 68 | to read later. 69 | 3. Close() can be called asynchronously. It interrupts ioloop() (unless ioloop() 70 | is actually reading from stdin, in which case it interrupts it after the next 71 | keystroke), and also interrupts any in-progress GetRune() call. If 72 | GetCursorPosition() is in progress, it tries to wait until the CPR response 73 | has been received. It is idempotent and can be called multiple times. 74 | */ 75 | 76 | type terminal struct { 77 | cfg atomic.Pointer[Config] 78 | dimensions atomic.Pointer[termDimensions] 79 | closeOnce sync.Once 80 | closeErr error 81 | outChan chan readResult 82 | kickChan chan struct{} 83 | stopChan chan struct{} 84 | buffer []rune // actual input that we saw while waiting for the CPR 85 | inFlight bool // tracks whether we initiated a read and then gave up waiting 86 | sleeping int32 87 | 88 | // asynchronously receive DSR messages from the terminal, 89 | // ensuring at most one query is in flight at a time 90 | dsrLock sync.Mutex 91 | dsrDone chan struct{} // nil if there is no DSR query in flight 92 | } 93 | 94 | // termDimensions stores the terminal width and height (-1 means unknown) 95 | type termDimensions struct { 96 | width int 97 | height int 98 | } 99 | 100 | type cursorPosition struct { 101 | row int 102 | col int 103 | } 104 | 105 | // readResult represents the result of a single "read operation" from the 106 | // perspective of terminal. it may be a pure no-op. the consumer needs to 107 | // read again if it didn't get what it wanted 108 | type readResult struct { 109 | r rune 110 | ok bool // is `r` valid user input? if not, we may need to read again 111 | // other data that can be conveyed in a single read operation; 112 | // currently only the CPR: 113 | pos *cursorPosition 114 | } 115 | 116 | func newTerminal(cfg *Config) (*terminal, error) { 117 | if cfg.isInteractive { 118 | if ansiErr := ansi.EnableANSI(); ansiErr != nil { 119 | return nil, fmt.Errorf("Could not enable ANSI escapes: %w", ansiErr) 120 | } 121 | } 122 | t := &terminal{ 123 | kickChan: make(chan struct{}), 124 | outChan: make(chan readResult), 125 | stopChan: make(chan struct{}), 126 | } 127 | t.SetConfig(cfg) 128 | // Get and cache the current terminal size. 129 | t.OnSizeChange() 130 | 131 | go t.ioloop() 132 | return t, nil 133 | } 134 | 135 | // SleepToResume will sleep myself, and return only if I'm resumed. 136 | func (t *terminal) SleepToResume() { 137 | if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) { 138 | return 139 | } 140 | defer atomic.StoreInt32(&t.sleeping, 0) 141 | 142 | t.ExitRawMode() 143 | platform.SuspendProcess() 144 | t.EnterRawMode() 145 | } 146 | 147 | func (t *terminal) EnterRawMode() (err error) { 148 | return t.GetConfig().FuncMakeRaw() 149 | } 150 | 151 | func (t *terminal) ExitRawMode() (err error) { 152 | return t.GetConfig().FuncExitRaw() 153 | } 154 | 155 | func (t *terminal) Write(b []byte) (int, error) { 156 | return t.GetConfig().Stdout.Write(b) 157 | } 158 | 159 | // getOffset sends a DSR query to get the current offset, then blocks 160 | // until the query returns. 161 | func (t *terminal) GetCursorPosition(deadline chan struct{}) (cursorPosition, error) { 162 | // ensure there is no in-flight query, set up a waiter 163 | ok := func() (ok bool) { 164 | t.dsrLock.Lock() 165 | defer t.dsrLock.Unlock() 166 | if t.dsrDone == nil { 167 | t.dsrDone = make(chan struct{}) 168 | ok = true 169 | } 170 | return 171 | }() 172 | 173 | if !ok { 174 | return cursorPosition{-1, -1}, concurrentReads 175 | } 176 | 177 | defer func() { 178 | t.dsrLock.Lock() 179 | defer t.dsrLock.Unlock() 180 | close(t.dsrDone) 181 | t.dsrDone = nil 182 | }() 183 | 184 | // send the DSR Cursor Position Report request to terminal stdout: 185 | // https://vt100.net/docs/vt510-rm/DSR-CPR.html 186 | _, err := t.Write([]byte("\x1b[6n")) 187 | if err != nil { 188 | return cursorPosition{-1, -1}, err 189 | } 190 | 191 | for { 192 | result, err := t.readFromStdin(deadline) 193 | if err != nil { 194 | return cursorPosition{-1, -1}, err 195 | } 196 | if result.ok { 197 | // non-CPR input, save it to be read later: 198 | t.buffer = append(t.buffer, result.r) 199 | if len(t.buffer) > maxCPRBufferLen { 200 | panic("did not receive DSR CPR response") 201 | } 202 | } 203 | if result.pos != nil { 204 | return *result.pos, nil 205 | } 206 | } 207 | } 208 | 209 | // waitForDSR waits for any in-flight DSR query to complete. this prevents 210 | // garbage from being written to the terminal when Close() interrupts an 211 | // in-flight query. 212 | func (t *terminal) waitForDSR() { 213 | t.dsrLock.Lock() 214 | dsrDone := t.dsrDone 215 | t.dsrLock.Unlock() 216 | if dsrDone != nil { 217 | // tradeoffs: if the timeout is too high, we risk slowing down Close(); 218 | // if it's too low, we risk writing the CPR to the terminal, which is bad UX, 219 | // but neither of these outcomes is catastrophic 220 | timer := time.NewTimer(dsrTimeout) 221 | select { 222 | case <-dsrDone: 223 | case <-timer.C: 224 | } 225 | timer.Stop() 226 | } 227 | } 228 | 229 | func (t *terminal) GetRune(deadline chan struct{}) (rune, error) { 230 | if len(t.buffer) > 0 { 231 | result := t.buffer[0] 232 | t.buffer = t.buffer[1:] 233 | return result, nil 234 | } 235 | return t.getRuneFromStdin(deadline) 236 | } 237 | 238 | func (t *terminal) getRuneFromStdin(deadline chan struct{}) (rune, error) { 239 | for { 240 | result, err := t.readFromStdin(deadline) 241 | if err != nil { 242 | return 0, err 243 | } else if result.ok { 244 | return result.r, nil 245 | } // else: CPR or something else we didn't understand, read again 246 | } 247 | } 248 | 249 | func (t *terminal) readFromStdin(deadline chan struct{}) (result readResult, err error) { 250 | // we may have sent a kick previously and given up on the response; 251 | // if so, don't kick again (we will try again to read the pending response) 252 | if !t.inFlight { 253 | select { 254 | case t.kickChan <- struct{}{}: 255 | t.inFlight = true 256 | case <-t.stopChan: 257 | return result, io.EOF 258 | case <-deadline: 259 | return result, deadlineExceeded 260 | } 261 | } 262 | 263 | select { 264 | case result = <-t.outChan: 265 | t.inFlight = false 266 | return result, nil 267 | case <-t.stopChan: 268 | return result, io.EOF 269 | case <-deadline: 270 | return result, deadlineExceeded 271 | } 272 | } 273 | 274 | func (t *terminal) ioloop() { 275 | // ensure close if we get an error from stdio 276 | defer t.Close() 277 | 278 | buf := bufio.NewReader(t.GetConfig().Stdin) 279 | var ansiBuf bytes.Buffer 280 | 281 | for { 282 | select { 283 | case <-t.kickChan: 284 | case <-t.stopChan: 285 | return 286 | } 287 | 288 | r, _, err := buf.ReadRune() 289 | if err != nil { 290 | return 291 | } 292 | 293 | var result readResult 294 | if r == '\x1b' { 295 | // we're starting an ANSI escape sequence: 296 | // keep reading until we reach the end of the sequence 297 | result, err = t.consumeANSIEscape(buf, &ansiBuf) 298 | if err != nil { 299 | return 300 | } 301 | } else { 302 | result = readResult{r: r, ok: true} 303 | } 304 | 305 | select { 306 | case t.outChan <- result: 307 | case <-t.stopChan: 308 | return 309 | } 310 | } 311 | } 312 | 313 | func (t *terminal) consumeANSIEscape(buf *bufio.Reader, ansiBuf *bytes.Buffer) (result readResult, err error) { 314 | ansiBuf.Reset() 315 | initial, _, err := buf.ReadRune() 316 | if err != nil { 317 | return 318 | } 319 | // we already read one \x1b. this can indicate either the start of an ANSI 320 | // escape sequence, or a keychord with Alt (e.g. Alt+f produces `\x1bf` in 321 | // a typical xterm). 322 | switch initial { 323 | case 'f': 324 | // Alt-f in xterm, or Option+RightArrow in iTerm2 with "Natural text editing" 325 | return readResult{r: MetaForward, ok: true}, nil // Alt-f 326 | case 'b': 327 | // Alt-b in xterm, or Option+LeftArrow in iTerm2 with "Natural text editing" 328 | return readResult{r: MetaBackward, ok: true}, nil // Alt-b 329 | case '[', 'O': 330 | // this is a real ANSI escape sequence, read the rest of the sequence below: 331 | case '\x1b': 332 | // Alt plus a real ANSI escape sequence. Handle this specially since 333 | // right now the only cases we want to handle are the arrow keys: 334 | return consumeAltSequence(buf) 335 | default: 336 | return // invalid, ignore 337 | } 338 | 339 | // data consists of ; and 0-9 , anything else terminates the sequence 340 | var type_ rune 341 | for { 342 | r, _, err := buf.ReadRune() 343 | if err != nil { 344 | return result, err 345 | } 346 | if r == ';' || ('0' <= r && r <= '9') { 347 | ansiBuf.WriteRune(r) 348 | } else { 349 | type_ = r 350 | break 351 | } 352 | } 353 | 354 | var r rune 355 | switch type_ { 356 | case 'R': 357 | if initial == '[' { 358 | // DSR CPR response; if we can't parse it, just ignore it 359 | // (do not return an error here because that would stop ioloop()) 360 | if cpos, err := parseCPRResponse(ansiBuf.Bytes()); err == nil { 361 | return readResult{r: 0, ok: false, pos: &cpos}, nil 362 | } 363 | } 364 | case 'D': 365 | if altModifierEnabled(ansiBuf.Bytes()) { 366 | r = MetaBackward 367 | } else { 368 | r = CharBackward 369 | } 370 | case 'C': 371 | if altModifierEnabled(ansiBuf.Bytes()) { 372 | r = MetaForward 373 | } else { 374 | r = CharForward 375 | } 376 | case 'A': 377 | r = CharPrev 378 | case 'B': 379 | r = CharNext 380 | case 'H': 381 | r = CharLineStart 382 | case 'F': 383 | r = CharLineEnd 384 | case '~': 385 | if initial == '[' { 386 | switch string(ansiBuf.Bytes()) { 387 | case "3": 388 | r = MetaDeleteKey // this is the key typically labeled "Delete" 389 | case "1", "7": 390 | r = CharLineStart // "Home" key 391 | case "4", "8": 392 | r = CharLineEnd // "End" key 393 | } 394 | } 395 | case 'Z': 396 | if initial == '[' { 397 | r = MetaShiftTab 398 | } 399 | } 400 | 401 | if r != 0 { 402 | return readResult{r: r, ok: true}, nil 403 | } 404 | return // default: no interpretable rune value 405 | } 406 | 407 | func consumeAltSequence(buf *bufio.Reader) (result readResult, err error) { 408 | initial, _, err := buf.ReadRune() 409 | if err != nil { 410 | return 411 | } 412 | if initial != '[' { 413 | return 414 | } 415 | second, _, err := buf.ReadRune() 416 | if err != nil { 417 | return 418 | } 419 | switch second { 420 | case 'D': 421 | return readResult{r: MetaBackward, ok: true}, nil 422 | case 'C': 423 | return readResult{r: MetaForward, ok: true}, nil 424 | default: 425 | return 426 | } 427 | } 428 | 429 | func altModifierEnabled(payload []byte) bool { 430 | // https://www.xfree86.org/current/ctlseqs.html ; modifier keycodes 431 | // go after the semicolon, e.g. Alt-LeftArrow is `\x1b[1;3D` in VTE 432 | // terminals, where 3 indicates Alt 433 | if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 { 434 | if string(payload[semicolonIdx+1:]) == "3" { 435 | return true 436 | } 437 | } 438 | return false 439 | } 440 | 441 | func parseCPRResponse(payload []byte) (cursorPosition, error) { 442 | if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 { 443 | if row, err := strconv.Atoi(string(payload[:semicolonIdx])); err == nil { 444 | if col, err := strconv.Atoi(string(payload[semicolonIdx+1:])); err == nil { 445 | return cursorPosition{row: row, col: col}, nil 446 | } 447 | } 448 | } 449 | return cursorPosition{-1, -1}, invalidCPR 450 | } 451 | 452 | func (t *terminal) Bell() { 453 | t.Write([]byte{CharBell}) 454 | } 455 | 456 | func (t *terminal) Close() error { 457 | t.closeOnce.Do(func() { 458 | t.waitForDSR() 459 | close(t.stopChan) 460 | // don't close outChan; outChan results should always be valid. 461 | // instead we always select on both outChan and stopChan 462 | t.closeErr = t.ExitRawMode() 463 | }) 464 | return t.closeErr 465 | } 466 | 467 | func (t *terminal) SetConfig(c *Config) error { 468 | t.cfg.Store(c) 469 | return nil 470 | } 471 | 472 | func (t *terminal) GetConfig() *Config { 473 | return t.cfg.Load() 474 | } 475 | 476 | // OnSizeChange gets the current terminal size and caches it 477 | func (t *terminal) OnSizeChange() { 478 | cfg := t.GetConfig() 479 | width, height := cfg.FuncGetSize() 480 | t.dimensions.Store(&termDimensions{ 481 | width: width, 482 | height: height, 483 | }) 484 | } 485 | 486 | // GetWidthHeight returns the cached width, height values from the terminal 487 | func (t *terminal) GetWidthHeight() (width, height int) { 488 | dimensions := t.dimensions.Load() 489 | return dimensions.width, dimensions.height 490 | } 491 | -------------------------------------------------------------------------------- /undo.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "github.com/ergochat/readline/internal/ringbuf" 5 | ) 6 | 7 | type undoEntry struct { 8 | pos int 9 | buf []rune 10 | } 11 | 12 | // nil receiver is a valid no-op object 13 | type opUndo struct { 14 | op *operation 15 | stack ringbuf.Buffer[undoEntry] 16 | } 17 | 18 | func newOpUndo(op *operation) *opUndo { 19 | o := &opUndo{op: op} 20 | o.stack.Initialize(32, 64) 21 | o.init() 22 | return o 23 | } 24 | 25 | func (o *opUndo) add() { 26 | if o == nil { 27 | return 28 | } 29 | 30 | top, success := o.stack.Pop() 31 | buf, pos, changed := o.op.buf.CopyForUndo(top.buf) // if !success, top.buf is nil 32 | newEntry := undoEntry{pos: pos, buf: buf} 33 | if !success { 34 | o.stack.Add(newEntry) 35 | } else if !changed { 36 | o.stack.Add(newEntry) // update cursor position 37 | } else { 38 | o.stack.Add(top) 39 | o.stack.Add(newEntry) 40 | } 41 | } 42 | 43 | func (o *opUndo) undo() { 44 | if o == nil { 45 | return 46 | } 47 | 48 | top, success := o.stack.Pop() 49 | if !success { 50 | return 51 | } 52 | o.op.buf.Restore(top.buf, top.pos) 53 | o.op.buf.Refresh(nil) 54 | } 55 | 56 | func (o *opUndo) init() { 57 | if o == nil { 58 | return 59 | } 60 | 61 | buf, pos, _ := o.op.buf.CopyForUndo(nil) 62 | initialEntry := undoEntry{ 63 | pos: pos, 64 | buf: buf, 65 | } 66 | o.stack.Clear() 67 | o.stack.Add(initialEntry) 68 | } 69 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | "io" 7 | "os" 8 | "sync" 9 | "syscall" 10 | 11 | "github.com/ergochat/readline/internal/term" 12 | ) 13 | 14 | const ( 15 | CharLineStart = 1 16 | CharBackward = 2 17 | CharInterrupt = 3 18 | CharEOT = 4 19 | CharLineEnd = 5 20 | CharForward = 6 21 | CharBell = 7 22 | CharCtrlH = 8 23 | CharTab = 9 24 | CharCtrlJ = 10 25 | CharKill = 11 26 | CharCtrlL = 12 27 | CharEnter = 13 28 | CharNext = 14 29 | CharPrev = 16 30 | CharBckSearch = 18 31 | CharFwdSearch = 19 32 | CharTranspose = 20 33 | CharCtrlU = 21 34 | CharCtrlW = 23 35 | CharCtrlY = 25 36 | CharCtrlZ = 26 37 | CharEsc = 27 38 | CharCtrl_ = 31 39 | CharO = 79 40 | CharEscapeEx = 91 41 | CharBackspace = 127 42 | ) 43 | 44 | const ( 45 | MetaBackward rune = -iota - 1 46 | MetaForward 47 | MetaDelete 48 | MetaBackspace 49 | MetaTranspose 50 | MetaShiftTab 51 | MetaDeleteKey 52 | ) 53 | 54 | type rawModeHandler struct { 55 | sync.Mutex 56 | state *term.State 57 | } 58 | 59 | func (r *rawModeHandler) Enter() (err error) { 60 | r.Lock() 61 | defer r.Unlock() 62 | r.state, err = term.MakeRaw(int(syscall.Stdin)) 63 | return err 64 | } 65 | 66 | func (r *rawModeHandler) Exit() error { 67 | r.Lock() 68 | defer r.Unlock() 69 | if r.state == nil { 70 | return nil 71 | } 72 | err := term.Restore(int(syscall.Stdin), r.state) 73 | if err == nil { 74 | r.state = nil 75 | } 76 | return err 77 | } 78 | 79 | func clearScreen(w io.Writer) error { 80 | _, err := w.Write([]byte("\x1b[H\x1b[J")) 81 | return err 82 | } 83 | 84 | // ----------------------------------------------------------------------------- 85 | 86 | // print a linked list to Debug() 87 | func debugList(l *list.List) { 88 | idx := 0 89 | for e := l.Front(); e != nil; e = e.Next() { 90 | debugPrint("%d %+v", idx, e.Value) 91 | idx++ 92 | } 93 | } 94 | 95 | // append log info to another file 96 | func debugPrint(fmtStr string, o ...interface{}) { 97 | f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 98 | fmt.Fprintf(f, fmtStr, o...) 99 | fmt.Fprintln(f) 100 | f.Close() 101 | } 102 | -------------------------------------------------------------------------------- /vim.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | const ( 4 | vim_NORMAL = iota 5 | vim_INSERT 6 | vim_VISUAL 7 | ) 8 | 9 | type opVim struct { 10 | op *operation 11 | vimMode int 12 | } 13 | 14 | func newVimMode(op *operation) *opVim { 15 | ov := &opVim{ 16 | op: op, 17 | vimMode: vim_INSERT, 18 | } 19 | return ov 20 | } 21 | 22 | func (o *opVim) IsEnableVimMode() bool { 23 | return o.op.GetConfig().VimMode 24 | } 25 | 26 | func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { 27 | rb := o.op.buf 28 | handled = true 29 | switch r { 30 | case 'h': 31 | t = CharBackward 32 | case 'j': 33 | t = CharNext 34 | case 'k': 35 | t = CharPrev 36 | case 'l': 37 | t = CharForward 38 | case '0', '^': 39 | rb.MoveToLineStart() 40 | case '$': 41 | rb.MoveToLineEnd() 42 | case 'x': 43 | rb.Delete() 44 | if rb.IsCursorInEnd() { 45 | rb.MoveBackward() 46 | } 47 | case 'r': 48 | rb.Replace(readNext()) 49 | case 'd': 50 | next := readNext() 51 | switch next { 52 | case 'd': 53 | rb.Erase() 54 | case 'w': 55 | rb.DeleteWord() 56 | case 'h': 57 | rb.Backspace() 58 | case 'l': 59 | rb.Delete() 60 | } 61 | case 'p': 62 | rb.Yank() 63 | case 'b', 'B': 64 | rb.MoveToPrevWord() 65 | case 'w', 'W': 66 | rb.MoveToNextWord() 67 | case 'e', 'E': 68 | rb.MoveToEndWord() 69 | case 'f', 'F', 't', 'T': 70 | next := readNext() 71 | prevChar := r == 't' || r == 'T' 72 | reverse := r == 'F' || r == 'T' 73 | switch next { 74 | case CharEsc: 75 | default: 76 | rb.MoveTo(next, prevChar, reverse) 77 | } 78 | default: 79 | return r, false 80 | } 81 | return t, true 82 | } 83 | 84 | func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { 85 | rb := o.op.buf 86 | handled = true 87 | switch r { 88 | case 'i': 89 | case 'I': 90 | rb.MoveToLineStart() 91 | case 'a': 92 | rb.MoveForward() 93 | case 'A': 94 | rb.MoveToLineEnd() 95 | case 's': 96 | rb.Delete() 97 | case 'S': 98 | rb.Erase() 99 | case 'c': 100 | next := readNext() 101 | switch next { 102 | case 'c': 103 | rb.Erase() 104 | case 'w': 105 | rb.DeleteWord() 106 | case 'h': 107 | rb.Backspace() 108 | case 'l': 109 | rb.Delete() 110 | } 111 | default: 112 | return r, false 113 | } 114 | 115 | o.EnterVimInsertMode() 116 | return 117 | } 118 | 119 | func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { 120 | switch r { 121 | case CharEnter, CharInterrupt: 122 | o.vimMode = vim_INSERT // ??? 123 | return r 124 | } 125 | 126 | if r, handled := o.handleVimNormalMovement(r, readNext); handled { 127 | return r 128 | } 129 | 130 | if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { 131 | return r 132 | } 133 | 134 | // invalid operation 135 | o.op.t.Bell() 136 | return 0 137 | } 138 | 139 | func (o *opVim) EnterVimInsertMode() { 140 | o.vimMode = vim_INSERT 141 | } 142 | 143 | func (o *opVim) ExitVimInsertMode() { 144 | o.vimMode = vim_NORMAL 145 | } 146 | 147 | func (o *opVim) HandleVim(r rune, readNext func() rune) rune { 148 | if o.vimMode == vim_NORMAL { 149 | return o.HandleVimNormal(r, readNext) 150 | } 151 | if r == CharEsc { 152 | o.ExitVimInsertMode() 153 | return 0 154 | } 155 | 156 | switch o.vimMode { 157 | case vim_INSERT: 158 | return r 159 | case vim_VISUAL: 160 | } 161 | return r 162 | } 163 | --------------------------------------------------------------------------------