pax_global_header00006660000000000000000000000064146675011230014517gustar00rootroot0000000000000052 comment=01a3cc25c0bb62563f867b0e1dbc838c20fe37fd readline-0.1.3/000077500000000000000000000000001466750112300133035ustar00rootroot00000000000000readline-0.1.3/.check-gofmt.sh000077500000000000000000000004061466750112300161070ustar00rootroot00000000000000#!/bin/bash # exclude vendor/ SOURCES="*.go internal/ example/" if [ "$1" = "--fix" ]; then exec gofmt -s -w $SOURCES fi if [ -n "$(gofmt -s -l $SOURCES)" ]; then echo "Go code is not formatted correctly with \`gofmt -s\`:" gofmt -s -d $SOURCES exit 1 fi readline-0.1.3/.github/000077500000000000000000000000001466750112300146435ustar00rootroot00000000000000readline-0.1.3/.github/workflows/000077500000000000000000000000001466750112300167005ustar00rootroot00000000000000readline-0.1.3/.github/workflows/build.yml000066400000000000000000000005771466750112300205330ustar00rootroot00000000000000name: "build" on: pull_request: branches: - "master" push: branches: - "master" jobs: build: runs-on: "ubuntu-22.04" steps: - name: "checkout repository" uses: "actions/checkout@v3" - name: "setup go" uses: "actions/setup-go@v3" with: go-version: "1.20" - name: "make ci" run: "make ci" readline-0.1.3/.gitignore000066400000000000000000000000201466750112300152630ustar00rootroot00000000000000.vscode/* *.swp readline-0.1.3/LICENSE000066400000000000000000000020621466750112300143100ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Chzyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. readline-0.1.3/Makefile000066400000000000000000000006061466750112300147450ustar00rootroot00000000000000.PHONY: all test examples ci gofmt all: ci ci: test examples test: go test -race ./... go vet ./... ./.check-gofmt.sh examples: GOOS=linux go build -o /dev/null example/readline-demo/readline-demo.go GOOS=windows go build -o /dev/null example/readline-demo/readline-demo.go GOOS=darwin go build -o /dev/null example/readline-demo/readline-demo.go gofmt: ./.check-gofmt.sh --fix readline-0.1.3/README.md000066400000000000000000000027611466750112300145700ustar00rootroot00000000000000readline ======== [![Godoc](https://godoc.org/github.com/ergochat/readline?status.svg)](https://godoc.org/github.com/ergochat/readline) 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. It is a fork of [chzyer/readline](https://github.com/chzyer/readline). * Relative to the upstream repository, it is actively maintained and has numerous bug fixes - See our [changelog](docs/CHANGELOG.md) for details on fixes and improvements - See our [migration guide](docs/MIGRATING.md) for advice on how to migrate from upstream * Relative to [x/term](https://pkg.go.dev/golang.org/x/term), it has more features (e.g. tab-completion) * In use by multiple projects: [gopass](https://github.com/gopasspw/gopass), [fq](https://github.com/wader/fq), and [ircdog](https://github.com/ergochat/ircdog) ```go package main import ( "fmt" "log" "github.com/ergochat/readline" ) func main() { // see readline.NewFromConfig for advanced options: rl, err := readline.New("> ") if err != nil { log.Fatal(err) } defer rl.Close() log.SetOutput(rl.Stderr()) // redraw the prompt correctly after log output for { line, err := rl.ReadLine() // `err` is either nil, io.EOF, readline.ErrInterrupt, or an unexpected // condition in stdin: if err != nil { return } // `line` is returned without the terminating \n or CRLF: fmt.Fprintf(rl, "you wrote: %s\n", line) } } ``` readline-0.1.3/complete.go000066400000000000000000000334411466750112300154470ustar00rootroot00000000000000package readline import ( "bufio" "bytes" "fmt" "sync/atomic" "github.com/ergochat/readline/internal/platform" "github.com/ergochat/readline/internal/runes" ) type AutoCompleter interface { // Readline will pass the whole line and current offset to it // Completer need to pass all the candidates, and how long they shared the same characters in line // Example: // [go, git, git-shell, grep] // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1 // Do("gi", 2) => ["t", "t-shell"], 2 // Do("git", 3) => ["", "-shell"], 3 Do(line []rune, pos int) (newLine [][]rune, length int) } type opCompleter struct { w *terminal op *operation inCompleteMode atomic.Uint32 // this is read asynchronously from wrapWriter inSelectMode bool candidate [][]rune // list of candidates candidateSource []rune // buffer string when tab was pressed candidateOff int // num runes in common from buf where candidate start candidateChoice int // absolute index of the chosen candidate (indexing the candidate array which might not all display in current page) candidateColNum int // num columns candidates take 0..wraps, 1 col, 2 cols etc. candidateColWidth int // width of candidate columns linesAvail int // number of lines available below the user's prompt which could be used for rendering the completion pageStartIdx []int // start index in the candidate array on each page (candidatePageStart[i] = absolute idx of the first candidate on page i) curPage int // index of the current page } func newOpCompleter(w *terminal, op *operation) *opCompleter { return &opCompleter{ w: w, op: op, } } func (o *opCompleter) doSelect() { if len(o.candidate) == 1 { o.op.buf.WriteRunes(o.candidate[0]) o.ExitCompleteMode(false) return } o.nextCandidate() o.CompleteRefresh() } // Convert absolute index of the chosen candidate to a page-relative index func (o *opCompleter) candidateChoiceWithinPage() int { return o.candidateChoice - o.pageStartIdx[o.curPage] } // Given a page relative index of the chosen candidate, update the absolute index func (o *opCompleter) updateAbsolutechoice(choiceWithinPage int) { o.candidateChoice = choiceWithinPage + o.pageStartIdx[o.curPage] } // Move selection to the next candidate, updating page if necessary // Note: we don't allow passing arbitrary offset to this function because, e.g., // we don't have the 3rd page offset initialized when the user is just seeing the first page, // so we only allow users to navigate into the 2nd page but not to an arbirary page as a result // of calling this method func (o *opCompleter) nextCandidate() { o.candidateChoice = (o.candidateChoice + 1) % len(o.candidate) // Wrapping around if o.candidateChoice == 0 { o.curPage = 0 return } // Going to next page if o.candidateChoice == o.pageStartIdx[o.curPage+1] { o.curPage += 1 } } // Move selection to the next ith col in the current line, wrapping to the line start/end if needed func (o *opCompleter) nextCol(i int) { // If o.candidateColNum == 1 or 0, there is only one col per line and this is a noop if o.candidateColNum > 1 { idxWithinPage := o.candidateChoiceWithinPage() curLine := idxWithinPage / o.candidateColNum offsetInLine := idxWithinPage % o.candidateColNum nextOffset := offsetInLine + i nextOffset %= o.candidateColNum if nextOffset < 0 { nextOffset += o.candidateColNum } nextIdxWithinPage := curLine*o.candidateColNum + nextOffset o.updateAbsolutechoice(nextIdxWithinPage) } } // Move selection to the line below func (o *opCompleter) nextLine() { colNum := 1 if o.candidateColNum > 1 { colNum = o.candidateColNum } idxWithinPage := o.candidateChoiceWithinPage() idxWithinPage += colNum if idxWithinPage >= o.getMatrixSize() { idxWithinPage -= o.getMatrixSize() } else if idxWithinPage >= o.numCandidateCurPage() { idxWithinPage += colNum idxWithinPage -= o.getMatrixSize() } o.updateAbsolutechoice(idxWithinPage) } // Move selection to the line above func (o *opCompleter) prevLine() { colNum := 1 if o.candidateColNum > 1 { colNum = o.candidateColNum } idxWithinPage := o.candidateChoiceWithinPage() idxWithinPage -= colNum if idxWithinPage < 0 { idxWithinPage += o.getMatrixSize() if idxWithinPage >= o.numCandidateCurPage() { idxWithinPage -= colNum } } o.updateAbsolutechoice(idxWithinPage) } // Move selection to the start of the current line func (o *opCompleter) lineStart() { if o.candidateColNum > 1 { idxWithinPage := o.candidateChoiceWithinPage() lineOffset := idxWithinPage % o.candidateColNum idxWithinPage -= lineOffset o.updateAbsolutechoice(idxWithinPage) } } // Move selection to the end of the current line func (o *opCompleter) lineEnd() { if o.candidateColNum > 1 { idxWithinPage := o.candidateChoiceWithinPage() offsetToLineEnd := o.candidateColNum - idxWithinPage%o.candidateColNum - 1 idxWithinPage += offsetToLineEnd o.updateAbsolutechoice(idxWithinPage) if o.candidateChoice >= len(o.candidate) { o.candidateChoice = len(o.candidate) - 1 } } } // Move to the next page if possible, returning selection to the first item in the page func (o *opCompleter) nextPage() { // Check that this is not the last page already nextPageStart := o.pageStartIdx[o.curPage+1] if nextPageStart < len(o.candidate) { o.curPage += 1 o.candidateChoice = o.pageStartIdx[o.curPage] } } // Move to the previous page if possible, returning selection to the first item in the page func (o *opCompleter) prevPage() { if o.curPage > 0 { o.curPage -= 1 o.candidateChoice = o.pageStartIdx[o.curPage] } } // OnComplete returns true if complete mode is available. Used to ring bell // when tab pressed if cannot do complete for reason such as width unknown // or no candidates available. func (o *opCompleter) OnComplete() (ringBell bool) { tWidth, tHeight := o.w.GetWidthHeight() if tWidth == 0 || tHeight < 3 { return false } if o.IsInCompleteSelectMode() { o.doSelect() return true } buf := o.op.buf rs := buf.Runes() // If in complete mode and nothing else typed then we must be entering select mode if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) { if len(o.candidate) > 1 { same, size := runes.Aggregate(o.candidate) if size > 0 { buf.WriteRunes(same) o.ExitCompleteMode(false) return false // partial completion so ring the bell } } o.EnterCompleteSelectMode() o.doSelect() return true } newLines, offset := o.op.GetConfig().AutoComplete.Do(rs, buf.idx) if len(newLines) == 0 || (len(newLines) == 1 && len(newLines[0]) == 0) { o.ExitCompleteMode(false) return false // will ring bell on initial tab press } if o.candidateOff > offset { // part of buffer we are completing has changed. Example might be that we were completing "ls" and // user typed space so we are no longer completing "ls" but now we are completing an argument of // the ls command. Instead of continuing in complete mode, we exit. o.ExitCompleteMode(false) return true } o.candidateSource = rs // only Aggregate candidates in non-complete mode if !o.IsInCompleteMode() { if len(newLines) == 1 { // not yet in complete mode but only 1 candidate so complete it buf.WriteRunes(newLines[0]) o.ExitCompleteMode(false) return true } // check if all candidates have common prefix and return it and its size same, size := runes.Aggregate(newLines) if size > 0 { buf.WriteRunes(same) o.ExitCompleteMode(false) return false // partial completion so ring the bell } } // otherwise, we just enter complete mode (which does a refresh) o.EnterCompleteMode(offset, newLines) return true } func (o *opCompleter) IsInCompleteSelectMode() bool { return o.inSelectMode } func (o *opCompleter) IsInCompleteMode() bool { return o.inCompleteMode.Load() == 1 } func (o *opCompleter) HandleCompleteSelect(r rune) (stayInMode bool) { next := true switch r { case CharEnter, CharCtrlJ: next = false o.op.buf.WriteRunes(o.candidate[o.candidateChoice]) o.ExitCompleteMode(false) case CharLineStart: o.lineStart() case CharLineEnd: o.lineEnd() case CharBackspace: o.ExitCompleteSelectMode() next = false case CharTab: o.nextCandidate() case CharForward: o.nextCol(1) case CharBell, CharInterrupt: o.ExitCompleteMode(true) next = false case CharNext: o.nextLine() case CharBackward, MetaShiftTab: o.nextCol(-1) case CharPrev: o.prevLine() case 'j', 'J': o.prevPage() case 'k', 'K': o.nextPage() default: next = false o.ExitCompleteSelectMode() } if next { o.CompleteRefresh() return true } return false } func (o *opCompleter) getMatrixSize() int { colNum := 1 if o.candidateColNum > 1 { colNum = o.candidateColNum } line := o.getMatrixNumRows() return line * colNum } // Number of candidate that could fit on current page func (o *opCompleter) numCandidateCurPage() int { // Safety: we will always render the first page, and whenever we finished rendering page i, // we always populate o.candidatePageStart through at least i + 1, so when this is called, we // always know the start of the next page return o.pageStartIdx[o.curPage+1] - o.pageStartIdx[o.curPage] } // Get number of rows of current page viewed as a matrix of candidates func (o *opCompleter) getMatrixNumRows() int { candidateCurPage := o.numCandidateCurPage() // Normal case where there is no wrap if o.candidateColNum > 1 { numLine := candidateCurPage / o.candidateColNum if candidateCurPage%o.candidateColNum != 0 { numLine++ } return numLine } // 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 return candidateCurPage } // setColumnInfo calculates column width and number of columns required // to present the list of candidates on the terminal. func (o *opCompleter) setColumnInfo() { same := o.op.buf.RuneSlice(-o.candidateOff) sameWidth := runes.WidthAll(same) colWidth := 0 for _, c := range o.candidate { w := sameWidth + runes.WidthAll(c) if w > colWidth { colWidth = w } } colWidth++ // whitespace between cols tWidth, _ := o.w.GetWidthHeight() // -1 to avoid end of line issues width := tWidth - 1 colNum := width / colWidth if colNum != 0 { colWidth += (width - (colWidth * colNum)) / colNum } o.candidateColNum = colNum o.candidateColWidth = colWidth } // CompleteRefresh is used for completemode and selectmode func (o *opCompleter) CompleteRefresh() { if !o.IsInCompleteMode() { return } buf := bufio.NewWriter(o.w) // calculate num lines from cursor pos to where choices should be written lineCnt := o.op.buf.CursorLineCount() buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) // move down from cursor to start of candidates buf.WriteString("\033[J") same := o.op.buf.RuneSlice(-o.candidateOff) tWidth, _ := o.w.GetWidthHeight() colIdx := 0 lines := 0 sameWidth := runes.WidthAll(same) // Show completions for the current page idx := o.pageStartIdx[o.curPage] for ; idx < len(o.candidate); idx++ { // If writing the current candidate would overflow the page, // we know that it is the start of the next page. if colIdx == 0 && lines == o.linesAvail { if o.curPage == len(o.pageStartIdx)-1 { o.pageStartIdx = append(o.pageStartIdx, idx) } break } c := o.candidate[idx] inSelect := idx == o.candidateChoice && o.IsInCompleteSelectMode() cWidth := sameWidth + runes.WidthAll(c) cLines := 1 if tWidth > 0 { sWidth := 0 if platform.IsWindows && inSelect { sWidth = 1 // adjust for hightlighting on Windows } cLines = (cWidth + sWidth) / tWidth if (cWidth+sWidth)%tWidth > 0 { cLines++ } } if lines > 0 && colIdx == 0 { // After line 1, if we're printing to the first column // goto a new line. We do it here, instead of at the end // of the loop, to avoid the last \n taking up a blank // line at the end and stealing realestate. buf.WriteString("\n") } if inSelect { buf.WriteString("\033[30;47m") } buf.WriteString(string(same)) buf.WriteString(string(c)) if o.candidateColNum >= 1 { // only output spaces between columns if everything fits buf.Write(bytes.Repeat([]byte(" "), o.candidateColWidth-cWidth)) } if inSelect { buf.WriteString("\033[0m") } colIdx++ if colIdx >= o.candidateColNum { lines += cLines colIdx = 0 if platform.IsWindows { // Windows EOL edge-case. buf.WriteString("\b") } } } if idx == len(o.candidate) { // Book-keeping for the last page. o.pageStartIdx = append(o.pageStartIdx, len(o.candidate)) } if colIdx > 0 { lines++ // mid-line so count it. } // Show the guidance if there are more pages if idx != len(o.candidate) || o.curPage > 0 { buf.WriteString("\n-- (j: prev page) (k: next page) --") lines++ } // wrote out choices over "lines", move back to cursor (positioned at index) fmt.Fprintf(buf, "\033[%dA", lines) buf.Write(o.op.buf.getBackspaceSequence()) buf.Flush() } func (o *opCompleter) EnterCompleteSelectMode() { o.inSelectMode = true o.candidateChoice = -1 } func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) { o.inCompleteMode.Store(1) o.candidate = candidate o.candidateOff = offset o.setColumnInfo() o.initPage() o.CompleteRefresh() } func (o *opCompleter) initPage() { _, tHeight := o.w.GetWidthHeight() buflineCnt := o.op.buf.LineCount() // lines taken by buffer content o.linesAvail = tHeight - buflineCnt - 1 // lines available without scrolling buffer off screen, reserve one line for the guidance message o.pageStartIdx = []int{0} // first page always start at 0 o.curPage = 0 } func (o *opCompleter) ExitCompleteSelectMode() { o.inSelectMode = false o.candidateChoice = -1 } func (o *opCompleter) ExitCompleteMode(revent bool) { o.inCompleteMode.Store(0) o.candidate = nil o.candidateOff = -1 o.candidateSource = nil o.ExitCompleteSelectMode() } readline-0.1.3/complete_helper.go000066400000000000000000000073211466750112300170040ustar00rootroot00000000000000package readline import ( "bytes" "strings" "github.com/ergochat/readline/internal/runes" ) // PrefixCompleter implements AutoCompleter via a recursive tree. type PrefixCompleter struct { // Name is the name of a command, subcommand, or argument eligible for completion. Name string // Callback is optional; if defined, it takes the current line and returns // a list of possible completions associated with the current node (i.e. // in place of Name). Callback func(string) []string // Children is a list of possible completions that can follow the current node. Children []*PrefixCompleter nameRunes []rune // just a cache } var _ AutoCompleter = (*PrefixCompleter)(nil) func (p *PrefixCompleter) Tree(prefix string) string { buf := bytes.NewBuffer(nil) p.print(prefix, 0, buf) return buf.String() } func prefixPrint(p *PrefixCompleter, prefix string, level int, buf *bytes.Buffer) { if strings.TrimSpace(p.Name) != "" { buf.WriteString(prefix) if level > 0 { buf.WriteString("├") buf.WriteString(strings.Repeat("─", (level*4)-2)) buf.WriteString(" ") } buf.WriteString(p.Name) buf.WriteByte('\n') level++ } for _, ch := range p.Children { ch.print(prefix, level, buf) } } func (p *PrefixCompleter) print(prefix string, level int, buf *bytes.Buffer) { prefixPrint(p, prefix, level, buf) } func (p *PrefixCompleter) getName() []rune { if p.nameRunes == nil { if p.Name != "" { p.nameRunes = []rune(p.Name) } else { p.nameRunes = make([]rune, 0) } } return p.nameRunes } func (p *PrefixCompleter) getDynamicNames(line []rune) [][]rune { var result [][]rune for _, name := range p.Callback(string(line)) { nameRunes := []rune(name) nameRunes = append(nameRunes, ' ') result = append(result, nameRunes) } return result } func (p *PrefixCompleter) SetChildren(children []*PrefixCompleter) { p.Children = children } func NewPrefixCompleter(pc ...*PrefixCompleter) *PrefixCompleter { return PcItem("", pc...) } func PcItem(name string, pc ...*PrefixCompleter) *PrefixCompleter { name += " " result := &PrefixCompleter{ Name: name, Children: pc, } result.getName() // initialize nameRunes member return result } func PcItemDynamic(callback func(string) []string, pc ...*PrefixCompleter) *PrefixCompleter { return &PrefixCompleter{ Callback: callback, Children: pc, } } func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { return doInternal(p, line, pos, line) } func doInternal(p *PrefixCompleter, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) { line = runes.TrimSpaceLeft(line[:pos]) goNext := false var lineCompleter *PrefixCompleter for _, child := range p.Children { var childNames [][]rune if child.Callback != nil { childNames = child.getDynamicNames(origLine) } else { childNames = make([][]rune, 1) childNames[0] = child.getName() } for _, childName := range childNames { if len(line) >= len(childName) { if runes.HasPrefix(line, childName) { if len(line) == len(childName) { newLine = append(newLine, []rune{' '}) } else { newLine = append(newLine, childName) } offset = len(childName) lineCompleter = child goNext = true } } else { if runes.HasPrefix(childName, line) { newLine = append(newLine, childName[len(line):]) offset = len(line) lineCompleter = child } } } } if len(newLine) != 1 { return } tmpLine := make([]rune, 0, len(line)) for i := offset; i < len(line); i++ { if line[i] == ' ' { continue } tmpLine = append(tmpLine, line[i:]...) return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine) } if goNext { return doInternal(lineCompleter, nil, 0, origLine) } return } readline-0.1.3/docs/000077500000000000000000000000001466750112300142335ustar00rootroot00000000000000readline-0.1.3/docs/CHANGELOG.md000066400000000000000000000067171466750112300160570ustar00rootroot00000000000000# Changelog ## [0.1.3] - 2024-09-02 * It is now possible to select and navigate through tab-completion candidates in pager mode (#65, #66, thanks [@YangchenYe323](https://github.com/YangchenYe323)!) * Fixed Home and End keys in certain terminals and multiplexers (#67, #68) * Fixed crashing edge cases in the Ctrl-T "transpose characters" operation (#69, #70) * Removed `(Config).ForceUseInteractive`; instead `(Config).FuncIsTerminal` can be set to `func() bool { return true }` ## [0.1.2] - 2024-07-04 * Fixed skipping between words with Alt+{Left,Right} and Alt+{b,f} (#59, #63) * Fixed `FuncFilterInputRune` support (#61, thanks [@sohomdatta1](https://github.com/sohomdatta1)!) ## [0.1.1] - 2024-05-06 * Fixed zos support (#55) * Added support for the Home and End keys (#53) * Removed some internal enums related to Vim mode from the public API (#57) ## [0.1.0] - 2024-01-14 * Added optional undo support with Ctrl+_ ; this must be enabled manually by setting `(Config).Undo` to `true` * Removed `PrefixCompleterInterface` in favor of the concrete type `*PrefixCompleter` (most client code that explicitly uses `PrefixCompleterInterface` can simply substitute `*PrefixCompleter`) * Fixed a Windows-specific bug where backspace from the screen edge erased an extra line from the screen (#35) * Removed `(PrefixCompleter).Dynamic`, which was redundant with `(PrefixCompleter).Callback` * Removed `SegmentCompleter` and related APIs (users can still define their own `AutoCompleter` implementations, including by vendoring `SegmentCompleter`) * Removed `(Config).UniqueEditLine` * Removed public `Do` and `Print` functions * Fixed a case where the search menu remained visible after exiting search mode (#38, #40) * Fixed a data race on async writes in complete mode (#30) ## [0.0.6] - 2023-11-06 * Added `(*Instance).ClearScreen` (#36, #37) * Removed `(*Instance).Clean` (#37) ## [0.0.5] -- 2023-06-02 No public API changes. ## [v0.0.4] -- 2023-06-02 * Fixed panic on Ctrl-S followed by Ctrl-C (#32) * Fixed data races around history search (#29) * Added `(*Instance).ReadLine` as the preferred name (`Readline` is still accepted as an alias) (#29) * `Listener` and `Painter` are now function types instead of interfaces (#29) * Cleanups and renames for some relatively obscure APIs (#28, #29) ## [v0.0.3] -- 2023-04-17 * Added `(*Instance).SetDefault` to replace `FillStdin` and `WriteStdin` (#24) * Fixed Delete key on an empty line causing the prompt to exit (#14) * Fixed double draw of prompt on `ReadlineWithDefault` (#24) * Hide `Operation`, `Terminal`, `RuneBuffer`, and others from the public API (#18) ## [v0.0.2] -- 2023-03-27 * Fixed overwriting existing text on the same line as the prompt (d9af5677814a) * Fixed wide character handling, including emoji (d9af5677814a) * Fixed numerous UI race conditions (62ab2cfd1794, 3bfb569368b4, 4d842a2fe366) * Added a pager for completion candidates (76ae9696abd5) * Removed ANSI translation layer on Windows, instead enabling native ANSI support; this fixes a crash (#2) * Fixed Ctrl-Z suspend and resume (#17) * Fixed handling of Shift-Tab (#16) * Fixed word deletion at the beginning of the line deleting the entire line (#11) * Fixed a nil dereference from `SetConfig` (#3) * Added zos support (#10) * Cleanups and renames for many relatively obscure APIs (#3, #9) ## [v0.0.1] 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). readline-0.1.3/docs/MIGRATING.md000066400000000000000000000025071466750112300161020ustar00rootroot00000000000000# Migrating 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. Here are some guidelines for APIs that have been removed or changed: * 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. * Various APIs that allowed manipulating the instance's configuration directly (e.g. `(*Instance).SetMaskRune`) have been removed. We recommend using `(*Instance).SetConfig` instead. * The preferred name for `NewEx` is now `NewFromConfig` (`NewEx` is provided as a compatibility alias). * The preferred name for `(*Instance).Readline` is now `ReadLine` (`Readline` is provided as a compatibility alias). * `PrefixCompleterInterface` was removed in favor of exposing `PrefixCompleter` as a concrete struct type. In general, references to `PrefixCompleterInterface` can be changed to `*PrefixCompleter`. * `(Config).ForceUseInteractive` has been removed. Instead, set `(Config).FuncIsTerminal` to `func() bool { return true }`. readline-0.1.3/docs/shortcut.md000066400000000000000000000064511466750112300164360ustar00rootroot00000000000000## Readline Shortcut `Meta`+`B` means press `Esc` and `n` separately. Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` Notice: `Meta`+`B` is equals with `Alt`+`B` in windows. * Shortcut in normal mode | Shortcut | Comment | | ------------------ | --------------------------------- | | `Ctrl`+`A` | Beginning of line | | `Ctrl`+`B` / `←` | Backward one character | | `Meta`+`B` | Backward one word | | `Ctrl`+`C` | Send io.EOF | | `Ctrl`+`D` | Delete one character | | `Meta`+`D` | Delete one word | | `Ctrl`+`E` | End of line | | `Ctrl`+`F` / `→` | Forward one character | | `Meta`+`F` | Forward one word | | `Ctrl`+`G` | Cancel | | `Ctrl`+`H` | Delete previous character | | `Ctrl`+`I` / `Tab` | Command line completion | | `Ctrl`+`J` | Line feed | | `Ctrl`+`K` | Cut text to the end of line | | `Ctrl`+`L` | Clear screen | | `Ctrl`+`M` | Same as Enter key | | `Ctrl`+`N` / `↓` | Next line (in history) | | `Ctrl`+`P` / `↑` | Prev line (in history) | | `Ctrl`+`R` | Search backwards in history | | `Ctrl`+`S` | Search forwards in history | | `Ctrl`+`T` | Transpose characters | | `Meta`+`T` | Transpose words (TODO) | | `Ctrl`+`U` | Cut text to the beginning of line | | `Ctrl`+`W` | Cut previous word | | `Backspace` | Delete previous character | | `Meta`+`Backspace` | Cut previous word | | `Enter` | Line feed | * Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode) | Shortcut | Comment | | ----------------------- | --------------------------------------- | | `Ctrl`+`S` | Search forwards in history | | `Ctrl`+`R` | Search backwards in history | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history | | `Backspace` | Delete previous character | | Other | Exit Search Mode | * Shortcut in Complete Select Mode (double `Tab` to enter this mode) | Shortcut | Comment | | ----------------------- | ---------------------------------------- | | `Ctrl`+`F` | Move Forward | | `Ctrl`+`B` | Move Backward | | `Ctrl`+`N` | Move to next line | | `Ctrl`+`P` | Move to previous line | | `Ctrl`+`A` | Move to the first candicate in current line | | `Ctrl`+`E` | Move to the last candicate in current line | | `Tab` / `Enter` | Use the word on cursor to complete | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode | | Other | Exit Complete Select Mode |readline-0.1.3/example/000077500000000000000000000000001466750112300147365ustar00rootroot00000000000000readline-0.1.3/example/readline-demo/000077500000000000000000000000001466750112300174435ustar00rootroot00000000000000readline-0.1.3/example/readline-demo/readline-demo.go000066400000000000000000000114231466750112300225000ustar00rootroot00000000000000package main import ( "fmt" "io" "io/ioutil" "log" "math/rand" "strconv" "strings" "time" "github.com/ergochat/readline" ) func usage(w io.Writer) { io.WriteString(w, "commands:\n") io.WriteString(w, completer.Tree(" ")) } // Function constructor - constructs new function for listing given directory func listFiles(path string) func(string) []string { return func(line string) []string { names := make([]string, 0) files, _ := ioutil.ReadDir(path) for _, f := range files { names = append(names, f.Name()) } return names } } var completer = readline.NewPrefixCompleter( readline.PcItem("mode", readline.PcItem("vi"), readline.PcItem("emacs"), ), readline.PcItem("login"), readline.PcItem("say", readline.PcItemDynamic(listFiles("./"), readline.PcItem("with", readline.PcItem("following"), readline.PcItem("items"), ), ), readline.PcItem("hello"), readline.PcItem("bye"), ), readline.PcItem("setprompt"), readline.PcItem("setpassword"), readline.PcItem("bye"), readline.PcItem("help"), readline.PcItem("go", readline.PcItem("build", readline.PcItem("-o"), readline.PcItem("-v")), readline.PcItem("install", readline.PcItem("-v"), readline.PcItem("-vv"), readline.PcItem("-vvv"), ), readline.PcItem("test"), ), readline.PcItem("sleep"), readline.PcItem("async"), readline.PcItem("clear"), ) func filterInput(r rune) (rune, bool) { switch r { // block CtrlZ feature case readline.CharCtrlZ: return r, false } return r, true } func asyncSleep(instance *readline.Instance, min, max time.Duration) { r := rand.New(rand.NewSource(time.Now().UnixNano())) for { duration := min + time.Duration(int64(float64(max-min)*r.Float64())) time.Sleep(duration) fmt.Fprintf(instance, "Slept for %v\n", duration) } } func asyncClose(instance *readline.Instance, line string) { duration := 3 * time.Second if fields := strings.Fields(line); len(fields) >= 2 { if dur, err := time.ParseDuration(fields[1]); err == nil { duration = dur } } time.Sleep(duration) instance.Close() } func main() { l, err := readline.NewFromConfig(&readline.Config{ Prompt: "\033[31m»\033[0m ", HistoryFile: "/tmp/readline.tmp", AutoComplete: completer, InterruptPrompt: "^C", EOFPrompt: "exit", HistorySearchFold: true, FuncFilterInputRune: filterInput, Undo: true, }) if err != nil { panic(err) } defer l.Close() l.CaptureExitSignal() setPasswordCfg := l.GeneratePasswordConfig() setPasswordCfg.Listener = func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line))) l.Refresh() return nil, 0, false } log.SetOutput(l.Stderr()) asyncStarted := false for { line, err := l.Readline() if err == readline.ErrInterrupt { if len(line) == 0 { break } else { continue } } else if err == io.EOF { break } line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "mode "): switch line[5:] { case "vi": l.SetVimMode(true) case "emacs": l.SetVimMode(false) default: println("invalid mode:", line[5:]) } case line == "mode": if l.IsVimMode() { println("current mode: vim") } else { println("current mode: emacs") } case line == "login": pswd, err := l.ReadPassword("please enter your password: ") if err != nil { break } println("you enter:", strconv.Quote(string(pswd))) case line == "help": usage(l.Stderr()) case line == "setpassword": pswd, err := l.ReadLineWithConfig(setPasswordCfg) if err == nil { println("you set:", strconv.Quote(string(pswd))) } case strings.HasPrefix(line, "setprompt"): if len(line) <= 10 { log.Println("setprompt ") break } l.SetPrompt(line[10:]) case strings.HasPrefix(line, "say"): line := strings.TrimSpace(line[3:]) if len(line) == 0 { log.Println("say what?") break } go func() { for range time.Tick(time.Second) { log.Println(line) } }() case strings.HasPrefix(line, "echo "): preArg := strings.TrimSpace(strings.TrimPrefix(line, "echo ")) arg := strings.TrimPrefix(preArg, "-n ") out := []byte(arg) if preArg == arg { out = append(out, '\n') } l.Write(out) case line == "bye": goto exit case line == "sleep": log.Println("sleep 4 second") time.Sleep(4 * time.Second) case line == "async": if !asyncStarted { asyncStarted = true log.Println("initialize async sleep/write") go asyncSleep(l, time.Second, 3*time.Second) } else { log.Println("async writes already started") } case line == "clear": l.ClearScreen() case strings.HasPrefix(line, "close"): go asyncClose(l, line) case line == "": default: log.Println("you said:", strconv.Quote(line)) } } exit: } readline-0.1.3/example/readline-im/000077500000000000000000000000001466750112300171245ustar00rootroot00000000000000readline-0.1.3/example/readline-im/README.md000066400000000000000000000002301466750112300203760ustar00rootroot00000000000000# readline-im ![readline-im](https://dl.dropboxusercontent.com/s/52hc7bo92g3pgi5/03F93B8D-9B4B-4D35-BBAA-22FBDAC7F299-26173-000164AA33980001.gif?dl=0) readline-0.1.3/example/readline-multiline/000077500000000000000000000000001466750112300205215ustar00rootroot00000000000000readline-0.1.3/example/readline-multiline/readline-multiline.go000066400000000000000000000012501466750112300246310ustar00rootroot00000000000000package main import ( "strings" "github.com/ergochat/readline" ) func main() { rl, err := readline.NewEx(&readline.Config{ Prompt: "> ", HistoryFile: "/tmp/readline-multiline", DisableAutoSaveHistory: true, }) if err != nil { panic(err) } defer rl.Close() var cmds []string for { line, err := rl.Readline() if err != nil { break } line = strings.TrimSpace(line) if len(line) == 0 { continue } cmds = append(cmds, line) if !strings.HasSuffix(line, ";") { rl.SetPrompt(">>> ") continue } cmd := strings.Join(cmds, " ") cmds = cmds[:0] rl.SetPrompt("> ") rl.SaveToHistory(cmd) println(cmd) } } readline-0.1.3/example/readline-paged-completion/000077500000000000000000000000001466750112300217465ustar00rootroot00000000000000readline-0.1.3/example/readline-paged-completion/readline-paged-completion.go000066400000000000000000000025601466750112300273100ustar00rootroot00000000000000package main import ( "fmt" "io" "log" "math/rand" "strconv" "strings" "github.com/ergochat/readline" ) var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randSeq(n int) string { b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } // A completor that will give a lot of completions for showcasing the paging functionality type Completor struct{} func (c *Completor) Do(line []rune, pos int) ([][]rune, int) { completion := make([][]rune, 0, 10000) for i := 0; i < 1000; i += 1 { var s string if i%2 == 0 { s = fmt.Sprintf("%s%05d", randSeq(1), i) } else if i%3 == 0 { s = fmt.Sprintf("%s%010d", randSeq(1), i) } else { s = fmt.Sprintf("%s%07d", randSeq(1), i) } completion = append(completion, []rune(s)) } return completion, pos } func main() { c := Completor{} l, err := readline.NewEx(&readline.Config{ Prompt: "\033[31m»\033[0m ", AutoComplete: &c, InterruptPrompt: "^C", EOFPrompt: "exit", }) if err != nil { panic(err) } defer l.Close() for { line, err := l.Readline() if err == readline.ErrInterrupt { if len(line) == 0 { break } else { continue } } else if err == io.EOF { break } line = strings.TrimSpace(line) switch { default: log.Println("you said:", strconv.Quote(line)) } } } readline-0.1.3/example/readline-pass-strength/000077500000000000000000000000001466750112300213215ustar00rootroot00000000000000readline-0.1.3/example/readline-pass-strength/readline-pass-strength.go000066400000000000000000000042461466750112300262410ustar00rootroot00000000000000// This is a small example using readline to read a password // and check it's strength while typing using the zxcvbn library. // Depending on the strength the prompt is colored nicely to indicate strength. // // This file is licensed under the WTFPL: // // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE // Version 2, December 2004 // // Copyright (C) 2004 Sam Hocevar // // Everyone is permitted to copy and distribute verbatim or modified // copies of this license document, and changing it is allowed as long // as the name is changed. // // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE // TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION // // 0. You just DO WHAT THE FUCK YOU WANT TO. package main import ( "fmt" "github.com/ergochat/readline" ) const ( Cyan = 36 Green = 32 Magenta = 35 Red = 31 Yellow = 33 BackgroundRed = 41 ) // Reset sequence var ColorResetEscape = "\033[0m" // ColorResetEscape translates a ANSI color number to a color escape. func ColorEscape(color int) string { return fmt.Sprintf("\033[0;%dm", color) } // Colorize the msg using ANSI color escapes func Colorize(msg string, color int) string { return ColorEscape(color) + msg + ColorResetEscape } func createStrengthPrompt(password []rune) string { symbol, color := "", Red switch { case len(password) <= 1: symbol = "✗" color = Red case len(password) <= 3: symbol = "⚡" color = Magenta case len(password) <= 5: symbol = "⚠" color = Yellow default: symbol = "✔" color = Green } prompt := Colorize(symbol, color) prompt += Colorize(" ENT", Cyan) prompt += Colorize(" New Password: ", color) return prompt } func main() { rl, err := readline.New("") if err != nil { return } defer rl.Close() setPasswordCfg := rl.GeneratePasswordConfig() setPasswordCfg.Listener = func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { rl.SetPrompt(createStrengthPrompt(line)) rl.Refresh() return nil, 0, false } pswd, err := rl.ReadLineWithConfig(setPasswordCfg) if err != nil { return } fmt.Println("Your password was:", string(pswd)) } readline-0.1.3/go.mod000066400000000000000000000001561466750112300144130ustar00rootroot00000000000000module github.com/ergochat/readline go 1.19 require ( golang.org/x/sys v0.15.0 golang.org/x/text v0.9.0 ) readline-0.1.3/go.sum000066400000000000000000000007131466750112300144370ustar00rootroot00000000000000golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= readline-0.1.3/history.go000066400000000000000000000142041466750112300153340ustar00rootroot00000000000000package readline import ( "bufio" "container/list" "fmt" "os" "strings" "sync" "github.com/ergochat/readline/internal/runes" ) type hisItem struct { Source []rune Version int64 Tmp []rune } func (h *hisItem) Clean() { h.Source = nil h.Tmp = nil } type opHistory struct { operation *operation history *list.List historyVer int64 current *list.Element fd *os.File fdLock sync.Mutex enable bool } func newOpHistory(operation *operation) (o *opHistory) { o = &opHistory{ operation: operation, history: list.New(), enable: true, } o.initHistory() return o } func (o *opHistory) isEnabled() bool { return o.enable && o.operation.GetConfig().HistoryLimit > 0 } func (o *opHistory) Reset() { o.history = list.New() o.current = nil } func (o *opHistory) initHistory() { cfg := o.operation.GetConfig() if cfg.HistoryFile != "" { o.historyUpdatePath(cfg) } } // only called by newOpHistory func (o *opHistory) historyUpdatePath(cfg *Config) { o.fdLock.Lock() defer o.fdLock.Unlock() f, err := os.OpenFile(cfg.HistoryFile, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { return } o.fd = f r := bufio.NewReader(o.fd) total := 0 for ; ; total++ { line, err := r.ReadString('\n') if err != nil { break } // ignore the empty line line = strings.TrimSpace(line) if len(line) == 0 { continue } o.Push([]rune(line)) o.Compact() } if total > cfg.HistoryLimit { o.rewriteLocked() } o.historyVer++ o.Push(nil) return } func (o *opHistory) Compact() { cfg := o.operation.GetConfig() for o.history.Len() > cfg.HistoryLimit && o.history.Len() > 0 { o.history.Remove(o.history.Front()) } } func (o *opHistory) Rewrite() { o.fdLock.Lock() defer o.fdLock.Unlock() o.rewriteLocked() } func (o *opHistory) rewriteLocked() { cfg := o.operation.GetConfig() if cfg.HistoryFile == "" { return } tmpFile := cfg.HistoryFile + ".tmp" fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666) if err != nil { return } buf := bufio.NewWriter(fd) for elem := o.history.Front(); elem != nil; elem = elem.Next() { buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n") } buf.Flush() // replace history file if err = os.Rename(tmpFile, cfg.HistoryFile); err != nil { fd.Close() return } if o.fd != nil { o.fd.Close() } // fd is write only, just satisfy what we need. o.fd = fd } func (o *opHistory) Close() { o.fdLock.Lock() defer o.fdLock.Unlock() if o.fd != nil { o.fd.Close() } } func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Prev() { item := o.showItem(elem.Value) if isNewSearch { start += len(rs) } if elem == o.current { if len(item) >= start { item = item[:start] } } idx := runes.IndexAllBckEx(item, rs, o.operation.GetConfig().HistorySearchFold) if idx < 0 { continue } return idx, elem } return -1, nil } func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Next() { item := o.showItem(elem.Value) if isNewSearch { start -= len(rs) if start < 0 { start = 0 } } if elem == o.current { if len(item)-1 >= start { item = item[start:] } else { continue } } idx := runes.IndexAllEx(item, rs, o.operation.GetConfig().HistorySearchFold) if idx < 0 { continue } if elem == o.current { idx += start } return idx, elem } return -1, nil } func (o *opHistory) showItem(obj interface{}) []rune { item := obj.(*hisItem) if item.Version == o.historyVer { return item.Tmp } return item.Source } func (o *opHistory) Prev() []rune { if o.current == nil { return nil } current := o.current.Prev() if current == nil { return nil } o.current = current return runes.Copy(o.showItem(current.Value)) } func (o *opHistory) Next() ([]rune, bool) { if o.current == nil { return nil, false } current := o.current.Next() if current == nil { return nil, false } o.current = current return runes.Copy(o.showItem(current.Value)), true } // Disable the current history func (o *opHistory) Disable() { o.enable = false } // Enable the current history func (o *opHistory) Enable() { o.enable = true } func (o *opHistory) debug() { debugPrint("-------") for item := o.history.Front(); item != nil; item = item.Next() { debugPrint(fmt.Sprintf("%+v", item.Value)) } } // save history func (o *opHistory) New(current []rune) (err error) { if !o.isEnabled() { return nil } current = runes.Copy(current) // if just use last command without modify // just clean lastest history if back := o.history.Back(); back != nil { prev := back.Prev() if prev != nil { if runes.Equal(current, prev.Value.(*hisItem).Source) { o.current = o.history.Back() o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } } if len(current) == 0 { o.current = o.history.Back() if o.current != nil { o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } if o.current != o.history.Back() { // move history item to current command currentItem := o.current.Value.(*hisItem) // set current to last item o.current = o.history.Back() current = runes.Copy(currentItem.Tmp) } // err only can be a IO error, just report err = o.Update(current, true) // push a new one to commit current command o.historyVer++ o.Push(nil) return } func (o *opHistory) Revert() { o.historyVer++ o.current = o.history.Back() } func (o *opHistory) Update(s []rune, commit bool) (err error) { if !o.isEnabled() { return nil } o.fdLock.Lock() defer o.fdLock.Unlock() s = runes.Copy(s) if o.current == nil { o.Push(s) o.Compact() return } r := o.current.Value.(*hisItem) r.Version = o.historyVer if commit { r.Source = s if o.fd != nil { // just report the error _, err = o.fd.Write([]byte(string(r.Source) + "\n")) } } else { r.Tmp = append(r.Tmp[:0], s...) } o.current.Value = r o.Compact() return } func (o *opHistory) Push(s []rune) { s = runes.Copy(s) elem := o.history.PushBack(&hisItem{Source: s}) o.current = elem } readline-0.1.3/internal/000077500000000000000000000000001466750112300151175ustar00rootroot00000000000000readline-0.1.3/internal/ansi/000077500000000000000000000000001466750112300160515ustar00rootroot00000000000000readline-0.1.3/internal/ansi/ansi.go000066400000000000000000000001131466750112300173250ustar00rootroot00000000000000//go:build !windows package ansi func EnableANSI() error { return nil } readline-0.1.3/internal/ansi/ansi_windows.go000066400000000000000000000047421466750112300211130ustar00rootroot00000000000000//go:build windows /* Copyright (c) Jason Walton (https://www.thedreaming.org) Copyright (c) Sindre Sorhus (https://sindresorhus.com) Released under the MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package ansi import ( "sync" "golang.org/x/sys/windows" ) var ( ansiErr error ansiOnce sync.Once ) func EnableANSI() error { ansiOnce.Do(func() { ansiErr = realEnableANSI() }) return ansiErr } func realEnableANSI() error { // We want to enable the following modes, if they are not already set: // ENABLE_VIRTUAL_TERMINAL_PROCESSING on stdout (color support) // ENABLE_VIRTUAL_TERMINAL_INPUT on stdin (ansi input sequences) // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences if err := windowsSetMode(windows.STD_OUTPUT_HANDLE, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { return err } if err := windowsSetMode(windows.STD_INPUT_HANDLE, windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil { return err } return nil } func windowsSetMode(stdhandle uint32, modeFlag uint32) (err error) { handle, err := windows.GetStdHandle(stdhandle) if err != nil { return err } // Get the existing console mode. var mode uint32 err = windows.GetConsoleMode(handle, &mode) if err != nil { return err } // Enable the mode if it is not currently set if mode&modeFlag != modeFlag { mode = mode | modeFlag err = windows.SetConsoleMode(handle, mode) if err != nil { return err } } return nil } readline-0.1.3/internal/platform/000077500000000000000000000000001466750112300167435ustar00rootroot00000000000000readline-0.1.3/internal/platform/utils_unix.go000066400000000000000000000032621466750112300215000ustar00rootroot00000000000000//go:build aix || darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd || os400 || solaris || zos package platform import ( "context" "os" "os/signal" "sync" "syscall" "github.com/ergochat/readline/internal/term" ) const ( IsWindows = false ) // SuspendProcess suspends the process with SIGTSTP, // then blocks until it is resumed. func SuspendProcess() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGCONT) defer stop() p, err := os.FindProcess(os.Getpid()) if err != nil { panic(err) } p.Signal(syscall.SIGTSTP) // wait for SIGCONT <-ctx.Done() } // getWidthHeight of the terminal using given file descriptor func getWidthHeight(stdoutFd int) (width int, height int) { width, height, err := term.GetSize(stdoutFd) if err != nil { return -1, -1 } return } // GetScreenSize returns the width/height of the terminal or -1,-1 or error func GetScreenSize() (width int, height int) { width, height = getWidthHeight(syscall.Stdout) if width < 0 { width, height = getWidthHeight(syscall.Stderr) } return } func DefaultIsTerminal() bool { return term.IsTerminal(syscall.Stdin) && (term.IsTerminal(syscall.Stdout) || term.IsTerminal(syscall.Stderr)) } // ----------------------------------------------------------------------------- var ( sizeChange sync.Once sizeChangeCallback func() ) func DefaultOnWidthChanged(f func()) { DefaultOnSizeChanged(f) } func DefaultOnSizeChanged(f func()) { sizeChangeCallback = f sizeChange.Do(func() { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGWINCH) go func() { for { _, ok := <-ch if !ok { break } sizeChangeCallback() } }() }) } readline-0.1.3/internal/platform/utils_windows.go000066400000000000000000000012511466750112300222030ustar00rootroot00000000000000//go:build windows package platform import ( "syscall" "github.com/ergochat/readline/internal/term" ) const ( IsWindows = true ) func SuspendProcess() { } // GetScreenSize returns the width, height of the terminal or -1,-1 func GetScreenSize() (width int, height int) { width, height, err := term.GetSize(int(syscall.Stdout)) if err == nil { return width, height } else { return 0, 0 } } func DefaultIsTerminal() bool { return term.IsTerminal(int(syscall.Stdin)) && term.IsTerminal(int(syscall.Stdout)) } func DefaultOnWidthChanged(f func()) { DefaultOnSizeChanged(f) } func DefaultOnSizeChanged(f func()) { // TODO: does Windows have a SIGWINCH analogue? } readline-0.1.3/internal/ringbuf/000077500000000000000000000000001466750112300165535ustar00rootroot00000000000000readline-0.1.3/internal/ringbuf/ringbuf.go000066400000000000000000000112051466750112300205350ustar00rootroot00000000000000// Copyright (c) 2023 Shivaram Lingamneni // released under the MIT license package ringbuf type Buffer[T any] struct { // three possible states: // empty: start == end == -1 // partially full: start != end // full: start == end > 0 // if entries exist, they go from `start` to `(end - 1) % length` buffer []T start int end int maximumSize int } func NewExpandableBuffer[T any](initialSize, maximumSize int) (result *Buffer[T]) { result = new(Buffer[T]) result.Initialize(initialSize, maximumSize) return } func (hist *Buffer[T]) Initialize(initialSize, maximumSize int) { if maximumSize == 0 { panic("maximum size cannot be 0") } hist.buffer = make([]T, initialSize) hist.start = -1 hist.end = -1 hist.maximumSize = maximumSize } // Add adds an item to the buffer func (list *Buffer[T]) Add(item T) { list.maybeExpand() var pos int if list.start == -1 { // empty pos = 0 list.start = 0 list.end = 1 % len(list.buffer) } else if list.start != list.end { // partially full pos = list.end list.end = (list.end + 1) % len(list.buffer) } else if list.start == list.end { // full pos = list.end list.end = (list.end + 1) % len(list.buffer) list.start = list.end // advance start as well, overwriting first entry } list.buffer[pos] = item } func (list *Buffer[T]) Pop() (item T, success bool) { length := list.Length() if length == 0 { return item, false } else { pos := list.prev(list.end) item = list.buffer[pos] list.buffer[pos] = *new(T) // TODO verify that this doesn't allocate if length > 1 { list.end = pos } else { // reset to empty buffer list.start = -1 list.end = -1 } return item, true } } func (list *Buffer[T]) Range(ascending bool, rangeFunction func(item *T) (stopIteration bool)) { if list.start == -1 || len(list.buffer) == 0 { return } var pos, stop int if ascending { pos = list.start stop = list.prev(list.end) } else { pos = list.prev(list.end) stop = list.start } for { if shouldStop := rangeFunction(&list.buffer[pos]); shouldStop { return } if pos == stop { return } if ascending { pos = list.next(pos) } else { pos = list.prev(pos) } } } type Predicate[T any] func(item *T) (matches bool) func (list *Buffer[T]) Match(ascending bool, predicate Predicate[T], limit int) []T { var results []T rangeFunc := func(item *T) (stopIteration bool) { if predicate(item) { results = append(results, *item) return limit > 0 && len(results) >= limit } else { return false } } list.Range(ascending, rangeFunc) return results } func (list *Buffer[T]) prev(index int) int { switch index { case 0: return len(list.buffer) - 1 default: return index - 1 } } func (list *Buffer[T]) next(index int) int { switch index { case len(list.buffer) - 1: return 0 default: return index + 1 } } func (list *Buffer[T]) maybeExpand() { length := list.Length() if length < len(list.buffer) { return // we have spare capacity already } if len(list.buffer) == list.maximumSize { return // cannot expand any further } newSize := roundUpToPowerOfTwo(length + 1) if list.maximumSize < newSize { newSize = list.maximumSize } list.resize(newSize) } // return n such that v <= n and n == 2**i for some i func roundUpToPowerOfTwo(v int) int { // http://graphics.stanford.edu/~seander/bithacks.html v -= 1 v |= v >> 1 v |= v >> 2 v |= v >> 4 v |= v >> 8 v |= v >> 16 return v + 1 } func (hist *Buffer[T]) Length() int { if hist.start == -1 { return 0 } else if hist.start < hist.end { return hist.end - hist.start } else { return len(hist.buffer) - (hist.start - hist.end) } } func (list *Buffer[T]) resize(size int) { newbuffer := make([]T, size) if list.start == -1 { // indices are already correct and nothing needs to be copied } else if size == 0 { // this is now the empty list list.start = -1 list.end = -1 } else { currentLength := list.Length() start := list.start end := list.end // if we're truncating, keep the latest entries, not the earliest if size < currentLength { start = list.end - size if start < 0 { start += len(list.buffer) } } if start < end { copied := copy(newbuffer, list.buffer[start:end]) list.start = 0 list.end = copied % size } else { lenInitial := len(list.buffer) - start copied := copy(newbuffer, list.buffer[start:]) copied += copy(newbuffer[lenInitial:], list.buffer[:end]) list.start = 0 list.end = copied % size } } list.buffer = newbuffer } func (hist *Buffer[T]) Clear() { hist.Range(true, func(item *T) bool { var zero T *item = zero return false }) hist.start = -1 hist.end = -1 } readline-0.1.3/internal/ringbuf/ringbuf_test.go000066400000000000000000000027761466750112300216110ustar00rootroot00000000000000package ringbuf import ( "fmt" "reflect" "testing" ) func assertEqual(found, expected interface{}) { if !reflect.DeepEqual(found, expected) { panic(fmt.Sprintf("found %#v, expected %#v", found, expected)) } } func testRange(low, hi int) []int { result := make([]int, hi-low) for i := low; i < hi; i++ { result[i-low] = i } return result } func extractContents[T any](buf *Buffer[T]) (result []T) { buf.Range(true, func(i *T) bool { result = append(result, *i) return false }) return result } func TestRingbuf(t *testing.T) { b := NewExpandableBuffer[int](16, 32) numItems := 0 for i := 0; i < 32; i++ { assertEqual(b.Length(), numItems) b.Add(i) numItems++ } assertEqual(b.Length(), 32) assertEqual(extractContents(b), testRange(0, 32)) for i := 32; i < 40; i++ { b.Add(i) assertEqual(b.Length(), 32) } assertEqual(b.Length(), 32) assertEqual(extractContents(b), testRange(8, 40)) assertEqual(b.Length(), 32) for i := 39; i >= 8; i-- { assertEqual(b.Length(), i-7) val, success := b.Pop() assertEqual(success, true) assertEqual(val, i) } _, success := b.Pop() assertEqual(success, false) } func TestClear(t *testing.T) { b := NewExpandableBuffer[int](8, 8) for i := 1; i <= 4; i++ { b.Add(i) } assertEqual(extractContents(b), testRange(1, 5)) b.Clear() assertEqual(b.Length(), 0) assertEqual(len(extractContents(b)), 0) // verify that the internal storage was cleared (important for GC) for i := 0; i < len(b.buffer); i++ { assertEqual(b.buffer[i], 0) } } readline-0.1.3/internal/runes/000077500000000000000000000000001466750112300162535ustar00rootroot00000000000000readline-0.1.3/internal/runes/runes.go000066400000000000000000000111251466750112300177360ustar00rootroot00000000000000package runes import ( "bytes" "golang.org/x/text/width" "unicode" "unicode/utf8" ) var TabWidth = 4 func EqualRune(a, b rune, fold bool) bool { if a == b { return true } if !fold { return false } if a > b { a, b = b, a } if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' { if b == a+'a'-'A' { return true } } return false } func EqualRuneFold(a, b rune) bool { return EqualRune(a, b, true) } func EqualFold(a, b []rune) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if EqualRuneFold(a[i], b[i]) { continue } return false } return true } func Equal(a, b []rune) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func IndexAllBckEx(r, sub []rune, fold bool) int { for i := len(r) - len(sub); i >= 0; i-- { found := true for j := 0; j < len(sub); j++ { if !EqualRune(r[i+j], sub[j], fold) { found = false break } } if found { return i } } return -1 } // Search in runes from end to front func IndexAllBck(r, sub []rune) int { return IndexAllBckEx(r, sub, false) } // Search in runes from front to end func IndexAll(r, sub []rune) int { return IndexAllEx(r, sub, false) } func IndexAllEx(r, sub []rune, fold bool) int { for i := 0; i < len(r); i++ { found := true if len(r[i:]) < len(sub) { return -1 } for j := 0; j < len(sub); j++ { if !EqualRune(r[i+j], sub[j], fold) { found = false break } } if found { return i } } return -1 } func Index(r rune, rs []rune) int { for i := 0; i < len(rs); i++ { if rs[i] == r { return i } } return -1 } func ColorFilter(r []rune) []rune { newr := make([]rune, 0, len(r)) for pos := 0; pos < len(r); pos++ { if r[pos] == '\033' && r[pos+1] == '[' { idx := Index('m', r[pos+2:]) if idx == -1 { continue } pos += idx + 2 continue } newr = append(newr, r[pos]) } return newr } var zeroWidth = []*unicode.RangeTable{ unicode.Mn, unicode.Me, unicode.Cc, unicode.Cf, } var doubleWidth = []*unicode.RangeTable{ unicode.Han, unicode.Hangul, unicode.Hiragana, unicode.Katakana, } func Width(r rune) int { if r == '\t' { return TabWidth } if unicode.IsOneOf(zeroWidth, r) { return 0 } switch width.LookupRune(r).Kind() { case width.EastAsianWide, width.EastAsianFullwidth: return 2 default: return 1 } } func WidthAll(r []rune) (length int) { for i := 0; i < len(r); i++ { length += Width(r[i]) } return } func Backspace(r []rune) []byte { return bytes.Repeat([]byte{'\b'}, WidthAll(r)) } func Copy(r []rune) []rune { n := make([]rune, len(r)) copy(n, r) return n } func HasPrefixFold(r, prefix []rune) bool { if len(r) < len(prefix) { return false } return EqualFold(r[:len(prefix)], prefix) } func HasPrefix(r, prefix []rune) bool { if len(r) < len(prefix) { return false } return Equal(r[:len(prefix)], prefix) } func Aggregate(candicate [][]rune) (same []rune, size int) { for i := 0; i < len(candicate[0]); i++ { for j := 0; j < len(candicate)-1; j++ { if i >= len(candicate[j]) || i >= len(candicate[j+1]) { goto aggregate } if candicate[j][i] != candicate[j+1][i] { goto aggregate } } size = i + 1 } aggregate: if size > 0 { same = Copy(candicate[0][:size]) for i := 0; i < len(candicate); i++ { n := Copy(candicate[i]) copy(n, n[size:]) candicate[i] = n[:len(n)-size] } } return } func TrimSpaceLeft(in []rune) []rune { firstIndex := len(in) for i, r := range in { if unicode.IsSpace(r) == false { firstIndex = i break } } return in[firstIndex:] } func IsWordBreak(i rune) bool { switch { case i >= 'a' && i <= 'z': case i >= 'A' && i <= 'Z': case i >= '0' && i <= '9': default: return true } return false } // split prompt + runes into lines by screenwidth starting from an offset. // the prompt should be filtered before passing to only its display runes. // if you know the width of the next character, pass it in as it is used // to decide if we generate an extra empty rune array to show next is new // line. func SplitByLine(prompt, rs []rune, offset, screenWidth, nextWidth int) [][]rune { ret := make([][]rune, 0) prs := append(prompt, rs...) si := 0 currentWidth := offset for i, r := range prs { w := Width(r) if r == '\n' { ret = append(ret, prs[si:i+1]) si = i + 1 currentWidth = 0 } else if currentWidth+w > screenWidth { ret = append(ret, prs[si:i]) si = i currentWidth = 0 } currentWidth += w } ret = append(ret, prs[si:]) if currentWidth+nextWidth > screenWidth { ret = append(ret, []rune{}) } return ret } readline-0.1.3/internal/runes/runes_test.go000066400000000000000000000044371466750112300210050ustar00rootroot00000000000000package runes import ( "reflect" "testing" ) type twidth struct { r []rune length int } func TestSingleRuneWidth(t *testing.T) { type test struct { r rune w int } tests := []test{ {0, 0}, // default rune is 0 - default mask {'a', 1}, {'☭', 1}, {'你', 2}, {'日', 2}, // kanji {'カ', 1}, // half-width katakana {'カ', 2}, // full-width katakana {'ひ', 2}, // full-width hiragana {'W', 2}, // full-width romanji {')', 2}, // full-width symbols {'😅', 2}, // emoji } for _, test := range tests { if w := Width(test.r); w != test.w { t.Error("result is not expected", string(test.r), test.w, w) } } } func TestRuneWidth(t *testing.T) { rs := []twidth{ {[]rune(""), 0}, {[]rune("☭"), 1}, {[]rune("a"), 1}, {[]rune("你"), 2}, {ColorFilter([]rune("☭\033[13;1m你")), 3}, {[]rune("漢字"), 4}, // kanji {[]rune("カタカナ"), 4}, // half-width katakana {[]rune("カタカナ"), 8}, // full-width katakana {[]rune("ひらがな"), 8}, // full-width hiragana {[]rune("WIDE"), 8}, // full-width romanji {[]rune("ー。"), 4}, // full-width symbols {[]rune("안녕하세요"), 10}, // full-width Hangul {[]rune("😅"), 2}, // emoji } for _, r := range rs { if w := WidthAll(r.r); w != r.length { t.Error("result is not expected", string(r.r), r.length, w) } } } type tagg struct { r [][]rune e [][]rune length int } func TestAggRunes(t *testing.T) { rs := []tagg{ { [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 1, }, { [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 1, }, { [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 0, }, { [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 2, }, } for _, r := range rs { same, off := Aggregate(r.r) if off != r.length { t.Fatal("result not expect", off) } if len(same) != off { t.Fatal("result not expect", same) } if !reflect.DeepEqual(r.r, r.e) { t.Fatal("result not expect") } } } readline-0.1.3/internal/term/000077500000000000000000000000001466750112300160665ustar00rootroot00000000000000readline-0.1.3/internal/term/term.go000066400000000000000000000034451466750112300173720ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package term provides support functions for dealing with terminals, as // commonly found on UNIX systems. // // Putting a terminal into raw mode is the most common requirement: // // oldState, err := term.MakeRaw(int(os.Stdin.Fd())) // if err != nil { // panic(err) // } // defer term.Restore(int(os.Stdin.Fd()), oldState) // // Note that on non-Unix systems os.Stdin.Fd() may not be 0. package term // State contains the state of a terminal. type State struct { state } // IsTerminal returns whether the given file descriptor is a terminal. func IsTerminal(fd int) bool { return isTerminal(fd) } // MakeRaw puts the terminal connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be // restored. func MakeRaw(fd int) (*State, error) { return makeRaw(fd) } // GetState returns the current state of a terminal which may be useful to // restore the terminal after a signal. func GetState(fd int) (*State, error) { return getState(fd) } // Restore restores the terminal connected to the given file descriptor to a // previous state. func Restore(fd int, oldState *State) error { return restore(fd, oldState) } // GetSize returns the visible dimensions of the given terminal. // // These dimensions don't include any scrollback buffer height. func GetSize(fd int) (width, height int, err error) { return getSize(fd) } // ReadPassword reads a line of input from a terminal without local echo. This // is commonly used for inputting passwords and other sensitive data. The slice // returned does not include the \n. func ReadPassword(fd int) ([]byte, error) { return readPassword(fd) } readline-0.1.3/internal/term/term_plan9.go000066400000000000000000000021751466750112300204740ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package term import ( "fmt" "runtime" "golang.org/x/sys/plan9" ) type state struct{} func isTerminal(fd int) bool { path, err := plan9.Fd2path(fd) if err != nil { return false } return path == "/dev/cons" || path == "/mnt/term/dev/cons" } func makeRaw(fd int) (*State, error) { return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func getState(fd int) (*State, error) { return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func restore(fd int, state *State) error { return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func getSize(fd int) (width, height int, err error) { return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func readPassword(fd int) ([]byte, error) { return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } readline-0.1.3/internal/term/term_unix.go000066400000000000000000000045361466750112300204370ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos package term import ( "golang.org/x/sys/unix" ) type state struct { termios unix.Termios } func isTerminal(fd int) bool { _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) return err == nil } func makeRaw(fd int) (*State, error) { termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) if err != nil { return nil, err } oldState := State{state{termios: *termios}} // This attempts to replicate the behaviour documented for cfmakeraw in // the termios(3) manpage. termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON //termios.Oflag &^= unix.OPOST termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN termios.Cflag &^= unix.CSIZE | unix.PARENB termios.Cflag |= unix.CS8 termios.Cc[unix.VMIN] = 1 termios.Cc[unix.VTIME] = 0 if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { return nil, err } return &oldState, nil } func getState(fd int) (*State, error) { termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) if err != nil { return nil, err } return &State{state{termios: *termios}}, nil } func restore(fd int, state *State) error { return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) } func getSize(fd int) (width, height int, err error) { ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) if err != nil { return 0, 0, err } return int(ws.Col), int(ws.Row), nil } // passwordReader is an io.Reader that reads from a specific file descriptor. type passwordReader int func (r passwordReader) Read(buf []byte) (int, error) { return unix.Read(int(r), buf) } func readPassword(fd int) ([]byte, error) { termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) if err != nil { return nil, err } newState := *termios newState.Lflag &^= unix.ECHO newState.Lflag |= unix.ICANON | unix.ISIG newState.Iflag |= unix.ICRNL if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { return nil, err } defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) return readPasswordLine(passwordReader(fd)) } readline-0.1.3/internal/term/term_unix_bsd.go000066400000000000000000000005351466750112300212620ustar00rootroot00000000000000// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd package term import "golang.org/x/sys/unix" const ioctlReadTermios = unix.TIOCGETA const ioctlWriteTermios = unix.TIOCSETA readline-0.1.3/internal/term/term_unix_other.go000066400000000000000000000005041466750112300216270ustar00rootroot00000000000000// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build aix || linux || solaris || zos package term import "golang.org/x/sys/unix" const ioctlReadTermios = unix.TCGETS const ioctlWriteTermios = unix.TCSETS readline-0.1.3/internal/term/term_unsupported.go000066400000000000000000000021621466750112300220350ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9 package term import ( "fmt" "runtime" ) type state struct{} func isTerminal(fd int) bool { return false } func makeRaw(fd int) (*State, error) { return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func getState(fd int) (*State, error) { return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func restore(fd int, state *State) error { return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func getSize(fd int) (width, height int, err error) { return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } func readPassword(fd int) ([]byte, error) { return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) } readline-0.1.3/internal/term/term_windows.go000066400000000000000000000041331466750112300211370ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package term import ( "os" "golang.org/x/sys/windows" ) type state struct { mode uint32 } func isTerminal(fd int) bool { var st uint32 err := windows.GetConsoleMode(windows.Handle(fd), &st) return err == nil } func makeRaw(fd int) (*State, error) { var st uint32 if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { return nil, err } raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { return nil, err } return &State{state{st}}, nil } func getState(fd int) (*State, error) { var st uint32 if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { return nil, err } return &State{state{st}}, nil } func restore(fd int, state *State) error { return windows.SetConsoleMode(windows.Handle(fd), state.mode) } func getSize(fd int) (width, height int, err error) { var info windows.ConsoleScreenBufferInfo if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { return 0, 0, err } return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil } func readPassword(fd int) ([]byte, error) { var st uint32 if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { return nil, err } old := st st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { return nil, err } defer windows.SetConsoleMode(windows.Handle(fd), old) var h windows.Handle p, _ := windows.GetCurrentProcess() if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { return nil, err } f := os.NewFile(uintptr(h), "stdin") defer f.Close() return readPasswordLine(f) } readline-0.1.3/internal/term/terminal.go000066400000000000000000000547561466750112300202510ustar00rootroot00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package term import ( "bytes" "io" "runtime" "strconv" "sync" "unicode/utf8" ) // EscapeCodes contains escape sequences that can be written to the terminal in // order to achieve different styles of text. type EscapeCodes struct { // Foreground colors Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte // Reset all attributes Reset []byte } var vt100EscapeCodes = EscapeCodes{ Black: []byte{keyEscape, '[', '3', '0', 'm'}, Red: []byte{keyEscape, '[', '3', '1', 'm'}, Green: []byte{keyEscape, '[', '3', '2', 'm'}, Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, Blue: []byte{keyEscape, '[', '3', '4', 'm'}, Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, White: []byte{keyEscape, '[', '3', '7', 'm'}, Reset: []byte{keyEscape, '[', '0', 'm'}, } // Terminal contains the state for running a VT100 terminal that is capable of // reading lines of input. type Terminal struct { // AutoCompleteCallback, if non-null, is called for each keypress with // the full input line and the current position of the cursor (in // bytes, as an index into |line|). If it returns ok=false, the key // press is processed normally. Otherwise it returns a replacement line // and the new cursor position. AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) // Escape contains a pointer to the escape codes for this terminal. // It's always a valid pointer, although the escape codes themselves // may be empty if the terminal doesn't support them. Escape *EscapeCodes // lock protects the terminal and the state in this object from // concurrent processing of a key press and a Write() call. lock sync.Mutex c io.ReadWriter prompt []rune // line is the current line being entered. line []rune // pos is the logical position of the cursor in line pos int // echo is true if local echo is enabled echo bool // pasteActive is true iff there is a bracketed paste operation in // progress. pasteActive bool // cursorX contains the current X value of the cursor where the left // edge is 0. cursorY contains the row number where the first row of // the current line is 0. cursorX, cursorY int // maxLine is the greatest value of cursorY so far. maxLine int termWidth, termHeight int // outBuf contains the terminal data to be sent. outBuf []byte // remainder contains the remainder of any partial key sequences after // a read. It aliases into inBuf. remainder []byte inBuf [256]byte // history contains previously entered commands so that they can be // accessed with the up and down keys. history stRingBuffer // historyIndex stores the currently accessed history entry, where zero // means the immediately previous entry. historyIndex int // When navigating up and down the history it's possible to return to // the incomplete, initial line. That value is stored in // historyPending. historyPending string } // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is // a local terminal, that terminal must first have been put into raw mode. // prompt is a string that is written at the start of each input line (i.e. // "> "). func NewTerminal(c io.ReadWriter, prompt string) *Terminal { return &Terminal{ Escape: &vt100EscapeCodes, c: c, prompt: []rune(prompt), termWidth: 80, termHeight: 24, echo: true, historyIndex: -1, } } const ( keyCtrlC = 3 keyCtrlD = 4 keyCtrlU = 21 keyEnter = '\r' keyEscape = 27 keyBackspace = 127 keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota keyUp keyDown keyLeft keyRight keyAltLeft keyAltRight keyHome keyEnd keyDeleteWord keyDeleteLine keyClearScreen keyPasteStart keyPasteEnd ) var ( crlf = []byte{'\r', '\n'} pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} ) // bytesToKey tries to parse a key sequence from b. If successful, it returns // the key and the remainder of the input. Otherwise it returns utf8.RuneError. func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { if len(b) == 0 { return utf8.RuneError, nil } if !pasteActive { switch b[0] { case 1: // ^A return keyHome, b[1:] case 2: // ^B return keyLeft, b[1:] case 5: // ^E return keyEnd, b[1:] case 6: // ^F return keyRight, b[1:] case 8: // ^H return keyBackspace, b[1:] case 11: // ^K return keyDeleteLine, b[1:] case 12: // ^L return keyClearScreen, b[1:] case 23: // ^W return keyDeleteWord, b[1:] case 14: // ^N return keyDown, b[1:] case 16: // ^P return keyUp, b[1:] } } if b[0] != keyEscape { if !utf8.FullRune(b) { return utf8.RuneError, b } r, l := utf8.DecodeRune(b) return r, b[l:] } if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { switch b[2] { case 'A': return keyUp, b[3:] case 'B': return keyDown, b[3:] case 'C': return keyRight, b[3:] case 'D': return keyLeft, b[3:] case 'H': return keyHome, b[3:] case 'F': return keyEnd, b[3:] } } if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { switch b[5] { case 'C': return keyAltRight, b[6:] case 'D': return keyAltLeft, b[6:] } } if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { return keyPasteStart, b[6:] } if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { return keyPasteEnd, b[6:] } // If we get here then we have a key that we don't recognise, or a // partial sequence. It's not clear how one should find the end of a // sequence without knowing them all, but it seems that [a-zA-Z~] only // appears at the end of a sequence. for i, c := range b[0:] { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { return keyUnknown, b[i+1:] } } return utf8.RuneError, b } // queue appends data to the end of t.outBuf func (t *Terminal) queue(data []rune) { t.outBuf = append(t.outBuf, []byte(string(data))...) } var space = []rune{' '} func isPrintable(key rune) bool { isInSurrogateArea := key >= 0xd800 && key <= 0xdbff return key >= 32 && !isInSurrogateArea } // moveCursorToPos appends data to t.outBuf which will move the cursor to the // given, logical position in the text. func (t *Terminal) moveCursorToPos(pos int) { if !t.echo { return } x := visualLength(t.prompt) + pos y := x / t.termWidth x = x % t.termWidth up := 0 if y < t.cursorY { up = t.cursorY - y } down := 0 if y > t.cursorY { down = y - t.cursorY } left := 0 if x < t.cursorX { left = t.cursorX - x } right := 0 if x > t.cursorX { right = x - t.cursorX } t.cursorX = x t.cursorY = y t.move(up, down, left, right) } func (t *Terminal) move(up, down, left, right int) { m := []rune{} // 1 unit up can be expressed as ^[[A or ^[A // 5 units up can be expressed as ^[[5A if up == 1 { m = append(m, keyEscape, '[', 'A') } else if up > 1 { m = append(m, keyEscape, '[') m = append(m, []rune(strconv.Itoa(up))...) m = append(m, 'A') } if down == 1 { m = append(m, keyEscape, '[', 'B') } else if down > 1 { m = append(m, keyEscape, '[') m = append(m, []rune(strconv.Itoa(down))...) m = append(m, 'B') } if right == 1 { m = append(m, keyEscape, '[', 'C') } else if right > 1 { m = append(m, keyEscape, '[') m = append(m, []rune(strconv.Itoa(right))...) m = append(m, 'C') } if left == 1 { m = append(m, keyEscape, '[', 'D') } else if left > 1 { m = append(m, keyEscape, '[') m = append(m, []rune(strconv.Itoa(left))...) m = append(m, 'D') } t.queue(m) } func (t *Terminal) clearLineToRight() { op := []rune{keyEscape, '[', 'K'} t.queue(op) } const maxLineLength = 4096 func (t *Terminal) setLine(newLine []rune, newPos int) { if t.echo { t.moveCursorToPos(0) t.writeLine(newLine) for i := len(newLine); i < len(t.line); i++ { t.writeLine(space) } t.moveCursorToPos(newPos) } t.line = newLine t.pos = newPos } func (t *Terminal) advanceCursor(places int) { t.cursorX += places t.cursorY += t.cursorX / t.termWidth if t.cursorY > t.maxLine { t.maxLine = t.cursorY } t.cursorX = t.cursorX % t.termWidth if places > 0 && t.cursorX == 0 { // Normally terminals will advance the current position // when writing a character. But that doesn't happen // for the last character in a line. However, when // writing a character (except a new line) that causes // a line wrap, the position will be advanced two // places. // // So, if we are stopping at the end of a line, we // need to write a newline so that our cursor can be // advanced to the next line. t.outBuf = append(t.outBuf, '\r', '\n') } } func (t *Terminal) eraseNPreviousChars(n int) { if n == 0 { return } if t.pos < n { n = t.pos } t.pos -= n t.moveCursorToPos(t.pos) copy(t.line[t.pos:], t.line[n+t.pos:]) t.line = t.line[:len(t.line)-n] if t.echo { t.writeLine(t.line[t.pos:]) for i := 0; i < n; i++ { t.queue(space) } t.advanceCursor(n) t.moveCursorToPos(t.pos) } } // countToLeftWord returns then number of characters from the cursor to the // start of the previous word. func (t *Terminal) countToLeftWord() int { if t.pos == 0 { return 0 } pos := t.pos - 1 for pos > 0 { if t.line[pos] != ' ' { break } pos-- } for pos > 0 { if t.line[pos] == ' ' { pos++ break } pos-- } return t.pos - pos } // countToRightWord returns then number of characters from the cursor to the // start of the next word. func (t *Terminal) countToRightWord() int { pos := t.pos for pos < len(t.line) { if t.line[pos] == ' ' { break } pos++ } for pos < len(t.line) { if t.line[pos] != ' ' { break } pos++ } return pos - t.pos } // visualLength returns the number of visible glyphs in s. func visualLength(runes []rune) int { inEscapeSeq := false length := 0 for _, r := range runes { switch { case inEscapeSeq: if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { inEscapeSeq = false } case r == '\x1b': inEscapeSeq = true default: length++ } } return length } // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key rune) (line string, ok bool) { if t.pasteActive && key != keyEnter { t.addKeyToLine(key) return } switch key { case keyBackspace: if t.pos == 0 { return } t.eraseNPreviousChars(1) case keyAltLeft: // move left by a word. t.pos -= t.countToLeftWord() t.moveCursorToPos(t.pos) case keyAltRight: // move right by a word. t.pos += t.countToRightWord() t.moveCursorToPos(t.pos) case keyLeft: if t.pos == 0 { return } t.pos-- t.moveCursorToPos(t.pos) case keyRight: if t.pos == len(t.line) { return } t.pos++ t.moveCursorToPos(t.pos) case keyHome: if t.pos == 0 { return } t.pos = 0 t.moveCursorToPos(t.pos) case keyEnd: if t.pos == len(t.line) { return } t.pos = len(t.line) t.moveCursorToPos(t.pos) case keyUp: entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) if !ok { return "", false } if t.historyIndex == -1 { t.historyPending = string(t.line) } t.historyIndex++ runes := []rune(entry) t.setLine(runes, len(runes)) case keyDown: switch t.historyIndex { case -1: return case 0: runes := []rune(t.historyPending) t.setLine(runes, len(runes)) t.historyIndex-- default: entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) if ok { t.historyIndex-- runes := []rune(entry) t.setLine(runes, len(runes)) } } case keyEnter: t.moveCursorToPos(len(t.line)) t.queue([]rune("\r\n")) line = string(t.line) ok = true t.line = t.line[:0] t.pos = 0 t.cursorX = 0 t.cursorY = 0 t.maxLine = 0 case keyDeleteWord: // Delete zero or more spaces and then one or more characters. t.eraseNPreviousChars(t.countToLeftWord()) case keyDeleteLine: // Delete everything from the current cursor position to the // end of line. for i := t.pos; i < len(t.line); i++ { t.queue(space) t.advanceCursor(1) } t.line = t.line[:t.pos] t.moveCursorToPos(t.pos) case keyCtrlD: // Erase the character under the current position. // The EOF case when the line is empty is handled in // readLine(). if t.pos < len(t.line) { t.pos++ t.eraseNPreviousChars(1) } case keyCtrlU: t.eraseNPreviousChars(t.pos) case keyClearScreen: // Erases the screen and moves the cursor to the home position. t.queue([]rune("\x1b[2J\x1b[H")) t.queue(t.prompt) t.cursorX, t.cursorY = 0, 0 t.advanceCursor(visualLength(t.prompt)) t.setLine(t.line, t.pos) default: if t.AutoCompleteCallback != nil { prefix := string(t.line[:t.pos]) suffix := string(t.line[t.pos:]) t.lock.Unlock() newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) t.lock.Lock() if completeOk { t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) return } } if !isPrintable(key) { return } if len(t.line) == maxLineLength { return } t.addKeyToLine(key) } return } // addKeyToLine inserts the given key at the current position in the current // line. func (t *Terminal) addKeyToLine(key rune) { if len(t.line) == cap(t.line) { newLine := make([]rune, len(t.line), 2*(1+len(t.line))) copy(newLine, t.line) t.line = newLine } t.line = t.line[:len(t.line)+1] copy(t.line[t.pos+1:], t.line[t.pos:]) t.line[t.pos] = key if t.echo { t.writeLine(t.line[t.pos:]) } t.pos++ t.moveCursorToPos(t.pos) } func (t *Terminal) writeLine(line []rune) { for len(line) != 0 { remainingOnLine := t.termWidth - t.cursorX todo := len(line) if todo > remainingOnLine { todo = remainingOnLine } t.queue(line[:todo]) t.advanceCursor(visualLength(line[:todo])) line = line[todo:] } } // writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { for len(buf) > 0 { i := bytes.IndexByte(buf, '\n') todo := len(buf) if i >= 0 { todo = i } var nn int nn, err = w.Write(buf[:todo]) n += nn if err != nil { return n, err } buf = buf[todo:] if i >= 0 { if _, err = w.Write(crlf); err != nil { return n, err } n++ buf = buf[1:] } } return n, nil } func (t *Terminal) Write(buf []byte) (n int, err error) { t.lock.Lock() defer t.lock.Unlock() if t.cursorX == 0 && t.cursorY == 0 { // This is the easy case: there's nothing on the screen that we // have to move out of the way. return writeWithCRLF(t.c, buf) } // We have a prompt and possibly user input on the screen. We // have to clear it first. t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) t.cursorX = 0 t.clearLineToRight() for t.cursorY > 0 { t.move(1 /* up */, 0, 0, 0) t.cursorY-- t.clearLineToRight() } if _, err = t.c.Write(t.outBuf); err != nil { return } t.outBuf = t.outBuf[:0] if n, err = writeWithCRLF(t.c, buf); err != nil { return } t.writeLine(t.prompt) if t.echo { t.writeLine(t.line) } t.moveCursorToPos(t.pos) if _, err = t.c.Write(t.outBuf); err != nil { return } t.outBuf = t.outBuf[:0] return } // ReadPassword temporarily changes the prompt and reads a password, without // echo, from the terminal. func (t *Terminal) ReadPassword(prompt string) (line string, err error) { t.lock.Lock() defer t.lock.Unlock() oldPrompt := t.prompt t.prompt = []rune(prompt) t.echo = false line, err = t.readLine() t.prompt = oldPrompt t.echo = true return } // ReadLine returns a line of input from the terminal. func (t *Terminal) ReadLine() (line string, err error) { t.lock.Lock() defer t.lock.Unlock() return t.readLine() } func (t *Terminal) readLine() (line string, err error) { // t.lock must be held at this point if t.cursorX == 0 && t.cursorY == 0 { t.writeLine(t.prompt) t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] } lineIsPasted := t.pasteActive for { rest := t.remainder lineOk := false for !lineOk { var key rune key, rest = bytesToKey(rest, t.pasteActive) if key == utf8.RuneError { break } if !t.pasteActive { if key == keyCtrlD { if len(t.line) == 0 { return "", io.EOF } } if key == keyCtrlC { return "", io.EOF } if key == keyPasteStart { t.pasteActive = true if len(t.line) == 0 { lineIsPasted = true } continue } } else if key == keyPasteEnd { t.pasteActive = false continue } if !t.pasteActive { lineIsPasted = false } line, lineOk = t.handleKey(key) } if len(rest) > 0 { n := copy(t.inBuf[:], rest) t.remainder = t.inBuf[:n] } else { t.remainder = nil } t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] if lineOk { if t.echo { t.historyIndex = -1 t.history.Add(line) } if lineIsPasted { err = ErrPasteIndicator } return } // t.remainder is a slice at the beginning of t.inBuf // containing a partial key sequence readBuf := t.inBuf[len(t.remainder):] var n int t.lock.Unlock() n, err = t.c.Read(readBuf) t.lock.Lock() if err != nil { return } t.remainder = t.inBuf[:n+len(t.remainder)] } } // SetPrompt sets the prompt to be used when reading subsequent lines. func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() defer t.lock.Unlock() t.prompt = []rune(prompt) } func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { // Move cursor to column zero at the start of the line. t.move(t.cursorY, 0, t.cursorX, 0) t.cursorX, t.cursorY = 0, 0 t.clearLineToRight() for t.cursorY < numPrevLines { // Move down a line t.move(0, 1, 0, 0) t.cursorY++ t.clearLineToRight() } // Move back to beginning. t.move(t.cursorY, 0, 0, 0) t.cursorX, t.cursorY = 0, 0 t.queue(t.prompt) t.advanceCursor(visualLength(t.prompt)) t.writeLine(t.line) t.moveCursorToPos(t.pos) } func (t *Terminal) SetSize(width, height int) error { t.lock.Lock() defer t.lock.Unlock() if width == 0 { width = 1 } oldWidth := t.termWidth t.termWidth, t.termHeight = width, height switch { case width == oldWidth: // If the width didn't change then nothing else needs to be // done. return nil case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: // If there is nothing on current line and no prompt printed, // just do nothing return nil case width < oldWidth: // Some terminals (e.g. xterm) will truncate lines that were // too long when shinking. Others, (e.g. gnome-terminal) will // attempt to wrap them. For the former, repainting t.maxLine // works great, but that behaviour goes badly wrong in the case // of the latter because they have doubled every full line. // We assume that we are working on a terminal that wraps lines // and adjust the cursor position based on every previous line // wrapping and turning into two. This causes the prompt on // xterms to move upwards, which isn't great, but it avoids a // huge mess with gnome-terminal. if t.cursorX >= t.termWidth { t.cursorX = t.termWidth - 1 } t.cursorY *= 2 t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) case width > oldWidth: // If the terminal expands then our position calculations will // be wrong in the future because we think the cursor is // |t.pos| chars into the string, but there will be a gap at // the end of any wrapped line. // // But the position will actually be correct until we move, so // we can move back to the beginning and repaint everything. t.clearAndRepaintLinePlusNPrevious(t.maxLine) } _, err := t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] return err } type pasteIndicatorError struct{} func (pasteIndicatorError) Error() string { return "terminal: ErrPasteIndicator not correctly handled" } // ErrPasteIndicator may be returned from ReadLine as the error, in addition // to valid line data. It indicates that bracketed paste mode is enabled and // that the returned line consists only of pasted data. Programs may wish to // interpret pasted data more literally than typed data. var ErrPasteIndicator = pasteIndicatorError{} // SetBracketedPasteMode requests that the terminal bracket paste operations // with markers. Not all terminals support this but, if it is supported, then // enabling this mode will stop any autocomplete callback from running due to // pastes. Additionally, any lines that are completely pasted will be returned // from ReadLine with the error set to ErrPasteIndicator. func (t *Terminal) SetBracketedPasteMode(on bool) { if on { io.WriteString(t.c, "\x1b[?2004h") } else { io.WriteString(t.c, "\x1b[?2004l") } } // stRingBuffer is a ring buffer of strings. type stRingBuffer struct { // entries contains max elements. entries []string max int // head contains the index of the element most recently added to the ring. head int // size contains the number of elements in the ring. size int } func (s *stRingBuffer) Add(a string) { if s.entries == nil { const defaultNumEntries = 100 s.entries = make([]string, defaultNumEntries) s.max = defaultNumEntries } s.head = (s.head + 1) % s.max s.entries[s.head] = a if s.size < s.max { s.size++ } } // NthPreviousEntry returns the value passed to the nth previous call to Add. // If n is zero then the immediately prior value is returned, if one, then the // next most recent, and so on. If such an element doesn't exist then ok is // false. func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { if n < 0 || n >= s.size { return "", false } index := s.head - n if index < 0 { index += s.max } return s.entries[index], true } // readPasswordLine reads from reader until it finds \n or io.EOF. // The slice returned does not include the \n. // readPasswordLine also ignores any \r it finds. // Windows uses \r as end of line. So, on Windows, readPasswordLine // reads until it finds \r and ignores any \n it finds during processing. func readPasswordLine(reader io.Reader) ([]byte, error) { var buf [1]byte var ret []byte for { n, err := reader.Read(buf[:]) if n > 0 { switch buf[0] { case '\b': if len(ret) > 0 { ret = ret[:len(ret)-1] } case '\n': if runtime.GOOS != "windows" { return ret, nil } // otherwise ignore \n case '\r': if runtime.GOOS == "windows" { return ret, nil } // otherwise ignore \r default: ret = append(ret, buf[0]) } continue } if err != nil { if err == io.EOF && len(ret) > 0 { return ret, nil } return ret, err } } } readline-0.1.3/internal/term/terminal_test.go000066400000000000000000000220711466750112300212710ustar00rootroot00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package term import ( "bytes" "io" "os" "runtime" "testing" ) type MockTerminal struct { toSend []byte bytesPerRead int received []byte } func (c *MockTerminal) Read(data []byte) (n int, err error) { n = len(data) if n == 0 { return } if n > len(c.toSend) { n = len(c.toSend) } if n == 0 { return 0, io.EOF } if c.bytesPerRead > 0 && n > c.bytesPerRead { n = c.bytesPerRead } copy(data, c.toSend[:n]) c.toSend = c.toSend[n:] return } func (c *MockTerminal) Write(data []byte) (n int, err error) { c.received = append(c.received, data...) return len(data), nil } func TestClose(t *testing.T) { c := &MockTerminal{} ss := NewTerminal(c, "> ") line, err := ss.ReadLine() if line != "" { t.Errorf("Expected empty line but got: %s", line) } if err != io.EOF { t.Errorf("Error should have been EOF but got: %s", err) } } var keyPressTests = []struct { in string line string err error throwAwayLines int }{ { err: io.EOF, }, { in: "\r", line: "", }, { in: "foo\r", line: "foo", }, { in: "a\x1b[Cb\r", // right line: "ab", }, { in: "a\x1b[Db\r", // left line: "ba", }, { in: "a\006b\r", // ^F line: "ab", }, { in: "a\002b\r", // ^B line: "ba", }, { in: "a\177b\r", // backspace line: "b", }, { in: "\x1b[A\r", // up }, { in: "\x1b[B\r", // down }, { in: "\016\r", // ^P }, { in: "\014\r", // ^N }, { in: "line\x1b[A\x1b[B\r", // up then down line: "line", }, { in: "line1\rline2\x1b[A\r", // recall previous line. line: "line1", throwAwayLines: 1, }, { // recall two previous lines and append. in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r", line: "line1xxx", throwAwayLines: 2, }, { // Ctrl-A to move to beginning of line followed by ^K to kill // line. in: "a b \001\013\r", line: "", }, { // Ctrl-A to move to beginning of line, Ctrl-E to move to end, // finally ^K to kill nothing. in: "a b \001\005\013\r", line: "a b ", }, { in: "\027\r", line: "", }, { in: "a\027\r", line: "", }, { in: "a \027\r", line: "", }, { in: "a b\027\r", line: "a ", }, { in: "a b \027\r", line: "a ", }, { in: "one two thr\x1b[D\027\r", line: "one two r", }, { in: "\013\r", line: "", }, { in: "a\013\r", line: "a", }, { in: "ab\x1b[D\013\r", line: "a", }, { in: "Ξεσκεπάζω\r", line: "Ξεσκεπάζω", }, { in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace. line: "", throwAwayLines: 1, }, { in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter. line: "£", throwAwayLines: 1, }, { // Ctrl-D at the end of the line should be ignored. in: "a\004\r", line: "a", }, { // a, b, left, Ctrl-D should erase the b. in: "ab\x1b[D\004\r", line: "a", }, { // a, b, c, d, left, left, ^U should erase to the beginning of // the line. in: "abcd\x1b[D\x1b[D\025\r", line: "cd", }, { // Bracketed paste mode: control sequences should be returned // verbatim in paste mode. in: "abc\x1b[200~de\177f\x1b[201~\177\r", line: "abcde\177", }, { // Enter in bracketed paste mode should still work. in: "abc\x1b[200~d\refg\x1b[201~h\r", line: "efgh", throwAwayLines: 1, }, { // Lines consisting entirely of pasted data should be indicated as such. in: "\x1b[200~a\r", line: "a", err: ErrPasteIndicator, }, { // Ctrl-C terminates readline in: "\003", err: io.EOF, }, { // Ctrl-C at the end of line also terminates readline in: "a\003\r", err: io.EOF, }, } func TestKeyPresses(t *testing.T) { for i, test := range keyPressTests { for j := 1; j < len(test.in); j++ { c := &MockTerminal{ toSend: []byte(test.in), bytesPerRead: j, } ss := NewTerminal(c, "> ") for k := 0; k < test.throwAwayLines; k++ { _, err := ss.ReadLine() if err != nil { t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err) } } line, err := ss.ReadLine() if line != test.line { t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) break } if err != test.err { t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) break } } } } var renderTests = []struct { in string received string err error }{ { // Cursor move after keyHome (left 4) then enter (right 4, newline) in: "abcd\x1b[H\r", received: "> abcd\x1b[4D\x1b[4C\r\n", }, { // Write, home, prepend, enter. Prepends rewrites the line. in: "cdef\x1b[Hab\r", received: "> cdef" + // Initial input "\x1b[4Da" + // Move cursor back, insert first char "cdef" + // Copy over original string "\x1b[4Dbcdef" + // Repeat for second char with copy "\x1b[4D" + // Put cursor back in position to insert again "\x1b[4C\r\n", // Put cursor at the end of the line and newline. }, } func TestRender(t *testing.T) { for i, test := range renderTests { for j := 1; j < len(test.in); j++ { c := &MockTerminal{ toSend: []byte(test.in), bytesPerRead: j, } ss := NewTerminal(c, "> ") _, err := ss.ReadLine() if err != test.err { t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) break } if test.received != string(c.received) { t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received) break } } } } func TestPasswordNotSaved(t *testing.T) { c := &MockTerminal{ toSend: []byte("password\r\x1b[A\r"), bytesPerRead: 1, } ss := NewTerminal(c, "> ") pw, _ := ss.ReadPassword("> ") if pw != "password" { t.Fatalf("failed to read password, got %s", pw) } line, _ := ss.ReadLine() if len(line) > 0 { t.Fatalf("password was saved in history") } } var setSizeTests = []struct { width, height int }{ {40, 13}, {80, 24}, {132, 43}, } func TestTerminalSetSize(t *testing.T) { for _, setSize := range setSizeTests { c := &MockTerminal{ toSend: []byte("password\r\x1b[A\r"), bytesPerRead: 1, } ss := NewTerminal(c, "> ") ss.SetSize(setSize.width, setSize.height) pw, _ := ss.ReadPassword("Password: ") if pw != "password" { t.Fatalf("failed to read password, got %s", pw) } if string(c.received) != "Password: \r\n" { t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received) } } } func TestReadPasswordLineEnd(t *testing.T) { type testType struct { input string want string } var tests = []testType{ {"\r\n", ""}, {"test\r\n", "test"}, {"test\r", "test"}, {"test\n", "test"}, {"testtesttesttes\n", "testtesttesttes"}, {"testtesttesttes\r\n", "testtesttesttes"}, {"testtesttesttesttest\n", "testtesttesttesttest"}, {"testtesttesttesttest\r\n", "testtesttesttesttest"}, {"\btest", "test"}, {"t\best", "est"}, {"te\bst", "tst"}, {"test\b", "tes"}, {"test\b\r\n", "tes"}, {"test\b\n", "tes"}, {"test\b\r", "tes"}, } eol := "\n" if runtime.GOOS == "windows" { eol = "\r" } tests = append(tests, testType{eol, ""}) for _, test := range tests { buf := new(bytes.Buffer) if _, err := buf.WriteString(test.input); err != nil { t.Fatal(err) } have, err := readPasswordLine(buf) if err != nil { t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) continue } if string(have) != test.want { t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) continue } if _, err = buf.WriteString(test.input); err != nil { t.Fatal(err) } have, err = readPasswordLine(buf) if err != nil { t.Errorf("readPasswordLine(%q) failed: %v", test.input, err) continue } if string(have) != test.want { t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want) continue } } } func TestMakeRawState(t *testing.T) { fd := int(os.Stdout.Fd()) if !IsTerminal(fd) { t.Skip("stdout is not a terminal; skipping test") } st, err := GetState(fd) if err != nil { t.Fatalf("failed to get terminal state from GetState: %s", err) } if runtime.GOOS == "ios" { t.Skip("MakeRaw not allowed on iOS; skipping test") } defer Restore(fd, st) raw, err := MakeRaw(fd) if err != nil { t.Fatalf("failed to get terminal state from MakeRaw: %s", err) } if *st != *raw { t.Errorf("states do not match; was %v, expected %v", raw, st) } } func TestOutputNewlines(t *testing.T) { // \n should be changed to \r\n in terminal output. buf := new(bytes.Buffer) term := NewTerminal(buf, ">") term.Write([]byte("1\n2\n")) output := buf.String() const expected = "1\r\n2\r\n" if output != expected { t.Errorf("incorrect output: was %q, expected %q", output, expected) } } readline-0.1.3/operation.go000066400000000000000000000272271466750112300156440ustar00rootroot00000000000000package readline import ( "errors" "io" "sync" "sync/atomic" "github.com/ergochat/readline/internal/platform" "github.com/ergochat/readline/internal/runes" ) var ( ErrInterrupt = errors.New("Interrupt") ) type operation struct { m sync.Mutex t *terminal buf *runeBuffer wrapOut atomic.Pointer[wrapWriter] wrapErr atomic.Pointer[wrapWriter] isPrompting bool // true when prompt written and waiting for input history *opHistory search *opSearch completer *opCompleter vim *opVim undo *opUndo } func (o *operation) SetBuffer(what string) { o.buf.SetNoRefresh([]rune(what)) } type wrapWriter struct { o *operation target io.Writer } func (w *wrapWriter) Write(b []byte) (int, error) { return w.o.write(w.target, b) } func (o *operation) write(target io.Writer, b []byte) (int, error) { o.m.Lock() defer o.m.Unlock() if !o.isPrompting { return target.Write(b) } var ( n int err error ) o.buf.Refresh(func() { n, err = target.Write(b) // Adjust the prompt start position by b rout := runes.ColorFilter([]rune(string(b[:]))) tWidth, _ := o.t.GetWidthHeight() sp := runes.SplitByLine(rout, []rune{}, o.buf.ppos, tWidth, 1) if len(sp) > 1 { o.buf.ppos = len(sp[len(sp)-1]) } else { o.buf.ppos += len(rout) } }) o.search.RefreshIfNeeded() if o.completer.IsInCompleteMode() { o.completer.CompleteRefresh() } return n, err } func newOperation(t *terminal) *operation { cfg := t.GetConfig() op := &operation{ t: t, buf: newRuneBuffer(t), } op.SetConfig(cfg) op.vim = newVimMode(op) op.completer = newOpCompleter(op.buf.w, op) cfg.FuncOnWidthChanged(t.OnSizeChange) return op } func (o *operation) GetConfig() *Config { return o.t.GetConfig() } func (o *operation) readline(deadline chan struct{}) ([]rune, error) { isTyping := false // don't add new undo entries during normal typing for { keepInSearchMode := false keepInCompleteMode := false r, err := o.t.GetRune(deadline) if cfg := o.GetConfig(); cfg.FuncFilterInputRune != nil && err == nil { var process bool r, process = cfg.FuncFilterInputRune(r) if !process { o.buf.Refresh(nil) // to refresh the line continue // ignore this rune } } if err == io.EOF { if o.buf.Len() == 0 { o.buf.Clean() return nil, io.EOF } else { // if stdin got io.EOF and there is something left in buffer, // let's flush them by sending CharEnter. // And we will got io.EOF int next loop. r = CharEnter } } else if err != nil { return nil, err } isUpdateHistory := true if o.completer.IsInCompleteSelectMode() { keepInCompleteMode = o.completer.HandleCompleteSelect(r) if keepInCompleteMode { continue } o.buf.Refresh(nil) switch r { case CharEnter, CharCtrlJ: o.history.Update(o.buf.Runes(), false) fallthrough case CharInterrupt: fallthrough case CharBell: continue } } if o.vim.IsEnableVimMode() { r = o.vim.HandleVim(r, func() rune { r, err := o.t.GetRune(deadline) if err == nil { return r } else { return 0 } }) if r == 0 { continue } } var result []rune isTypingRune := false switch r { case CharBell: if o.search.IsSearchMode() { o.search.ExitSearchMode(true) o.buf.Refresh(nil) } if o.completer.IsInCompleteMode() { o.completer.ExitCompleteMode(true) o.buf.Refresh(nil) } case CharBckSearch: if !o.search.SearchMode(searchDirectionBackward) { o.t.Bell() break } keepInSearchMode = true case CharCtrlU: o.undo.add() o.buf.KillFront() case CharFwdSearch: if !o.search.SearchMode(searchDirectionForward) { o.t.Bell() break } keepInSearchMode = true case CharKill: o.undo.add() o.buf.Kill() keepInCompleteMode = true case MetaForward: o.buf.MoveToNextWord() case CharTranspose: o.undo.add() o.buf.Transpose() case MetaBackward: o.buf.MoveToPrevWord() case MetaDelete: o.undo.add() o.buf.DeleteWord() case CharLineStart: o.buf.MoveToLineStart() case CharLineEnd: o.buf.MoveToLineEnd() case CharBackspace, CharCtrlH: o.undo.add() if o.search.IsSearchMode() { o.search.SearchBackspace() keepInSearchMode = true break } if o.buf.Len() == 0 { o.t.Bell() break } o.buf.Backspace() case CharCtrlZ: if !platform.IsWindows { o.buf.Clean() o.t.SleepToResume() o.Refresh() } case CharCtrlL: clearScreen(o.t) o.buf.SetOffset(cursorPosition{1, 1}) o.Refresh() case MetaBackspace, CharCtrlW: o.undo.add() o.buf.BackEscapeWord() case MetaShiftTab: // no-op case CharCtrlY: o.buf.Yank() case CharCtrl_: o.undo.undo() case CharEnter, CharCtrlJ: if o.search.IsSearchMode() { o.search.ExitSearchMode(false) } if o.completer.IsInCompleteMode() { o.completer.ExitCompleteMode(true) o.buf.Refresh(nil) } o.buf.MoveToLineEnd() var data []rune o.buf.WriteRune('\n') data = o.buf.Reset() data = data[:len(data)-1] // trim \n result = data if !o.GetConfig().DisableAutoSaveHistory { // ignore IO error _ = o.history.New(data) } else { isUpdateHistory = false } o.undo.init() case CharBackward: o.buf.MoveBackward() case CharForward: o.buf.MoveForward() case CharPrev: buf := o.history.Prev() if buf != nil { o.buf.Set(buf) o.undo.init() } else { o.t.Bell() } case CharNext: buf, ok := o.history.Next() if ok { o.buf.Set(buf) o.undo.init() } else { o.t.Bell() } case MetaDeleteKey, CharEOT: o.undo.add() // on Delete key or Ctrl-D, attempt to delete a character: if o.buf.Len() > 0 || !o.IsNormalMode() { if !o.buf.Delete() { o.t.Bell() } break } if r != CharEOT { break } // Ctrl-D on an empty buffer: treated as EOF o.buf.WriteString(o.GetConfig().EOFPrompt + "\n") o.buf.Reset() isUpdateHistory = false o.history.Revert() o.buf.Clean() return nil, io.EOF case CharInterrupt: if o.search.IsSearchMode() { o.search.ExitSearchMode(true) break } if o.completer.IsInCompleteMode() { o.completer.ExitCompleteMode(true) o.buf.Refresh(nil) break } o.buf.MoveToLineEnd() o.buf.Refresh(nil) hint := o.GetConfig().InterruptPrompt + "\n" o.buf.WriteString(hint) remain := o.buf.Reset() remain = remain[:len(remain)-len([]rune(hint))] isUpdateHistory = false o.history.Revert() return nil, ErrInterrupt case CharTab: if o.GetConfig().AutoComplete != nil { if o.completer.OnComplete() { if o.completer.IsInCompleteMode() { keepInCompleteMode = true continue // redraw is done, loop } } else { o.t.Bell() } o.buf.Refresh(nil) break } // else: process as a normal input character fallthrough default: isTypingRune = true if !isTyping { o.undo.add() } if o.search.IsSearchMode() { o.search.SearchChar(r) keepInSearchMode = true break } o.buf.WriteRune(r) if o.completer.IsInCompleteMode() { o.completer.OnComplete() if o.completer.IsInCompleteMode() { keepInCompleteMode = true } else { o.buf.Refresh(nil) } } } isTyping = isTypingRune // suppress the Listener callback if we received Enter or similar and are // submitting the result, since the buffer has already been cleared: if result == nil { if listener := o.GetConfig().Listener; listener != nil { newLine, newPos, ok := listener(o.buf.Runes(), o.buf.Pos(), r) if ok { o.buf.SetWithIdx(newPos, newLine) } } } o.m.Lock() if !keepInSearchMode && o.search.IsSearchMode() { o.search.ExitSearchMode(false) o.buf.Refresh(nil) o.undo.init() } else if o.completer.IsInCompleteMode() { if !keepInCompleteMode { o.completer.ExitCompleteMode(false) o.refresh() o.undo.init() } else { o.buf.Refresh(nil) o.completer.CompleteRefresh() } } if isUpdateHistory && !o.search.IsSearchMode() { // it will cause null history o.history.Update(o.buf.Runes(), false) } o.m.Unlock() if result != nil { return result, nil } } } func (o *operation) Stderr() io.Writer { return o.wrapErr.Load() } func (o *operation) Stdout() io.Writer { return o.wrapOut.Load() } func (o *operation) String() (string, error) { r, err := o.Runes() return string(r), err } func (o *operation) Runes() ([]rune, error) { o.t.EnterRawMode() defer o.t.ExitRawMode() cfg := o.GetConfig() listener := cfg.Listener if listener != nil { listener(nil, 0, 0) } // Before writing the prompt and starting to read, get a lock // so we don't race with wrapWriter trying to write and refresh. o.m.Lock() o.isPrompting = true // Query cursor position before printing the prompt as there // may be existing text on the same line that ideally we don't // want to overwrite and cause prompt to jump left. o.getAndSetOffset(nil) o.buf.Print() // print prompt & buffer contents // Prompt written safely, unlock until read completes and then // lock again to unset. o.m.Unlock() if cfg.Undo { o.undo = newOpUndo(o) } defer func() { o.m.Lock() o.isPrompting = false o.buf.SetOffset(cursorPosition{1, 1}) o.m.Unlock() }() return o.readline(nil) } func (o *operation) getAndSetOffset(deadline chan struct{}) { if !o.GetConfig().isInteractive { return } // Handle lineedge cases where existing text before before // the prompt is printed would leave us at the right edge of // the screen but the next character would actually be printed // at the beginning of the next line. // TODO ??? o.t.Write([]byte(" \b")) if offset, err := o.t.GetCursorPosition(deadline); err == nil { o.buf.SetOffset(offset) } } func (o *operation) GenPasswordConfig() *Config { baseConfig := o.GetConfig() return &Config{ EnableMask: true, InterruptPrompt: "\n", EOFPrompt: "\n", HistoryLimit: -1, Stdin: baseConfig.Stdin, Stdout: baseConfig.Stdout, Stderr: baseConfig.Stderr, FuncIsTerminal: baseConfig.FuncIsTerminal, FuncMakeRaw: baseConfig.FuncMakeRaw, FuncExitRaw: baseConfig.FuncExitRaw, FuncOnWidthChanged: baseConfig.FuncOnWidthChanged, } } func (o *operation) ReadLineWithConfig(cfg *Config) (string, error) { backupCfg, err := o.SetConfig(cfg) if err != nil { return "", err } defer func() { o.SetConfig(backupCfg) }() return o.String() } func (o *operation) SetTitle(t string) { o.t.Write([]byte("\033[2;" + t + "\007")) } func (o *operation) Slice() ([]byte, error) { r, err := o.Runes() if err != nil { return nil, err } return []byte(string(r)), nil } func (o *operation) Close() { o.history.Close() } func (o *operation) IsNormalMode() bool { return !o.completer.IsInCompleteMode() && !o.search.IsSearchMode() } func (op *operation) SetConfig(cfg *Config) (*Config, error) { op.m.Lock() defer op.m.Unlock() old := op.t.GetConfig() if err := cfg.init(); err != nil { return old, err } // install the config in its canonical location (inside terminal): op.t.SetConfig(cfg) op.wrapOut.Store(&wrapWriter{target: cfg.Stdout, o: op}) op.wrapErr.Store(&wrapWriter{target: cfg.Stderr, o: op}) if op.history == nil { op.history = newOpHistory(op) } if op.search == nil { op.search = newOpSearch(op.buf.w, op.buf, op.history) } if cfg.AutoComplete != nil && op.completer == nil { op.completer = newOpCompleter(op.buf.w, op) } return old, nil } func (o *operation) ResetHistory() { o.history.Reset() } func (o *operation) SaveToHistory(content string) error { return o.history.New([]rune(content)) } func (o *operation) Refresh() { o.m.Lock() defer o.m.Unlock() o.refresh() } func (o *operation) refresh() { if o.isPrompting { o.buf.Refresh(nil) } } readline-0.1.3/readline.go000066400000000000000000000202441466750112300154170ustar00rootroot00000000000000package readline import ( "io" "os" "os/signal" "sync" "syscall" "github.com/ergochat/readline/internal/platform" ) type Instance struct { terminal *terminal operation *operation closeOnce sync.Once closeErr error } type Config struct { // Prompt is the input prompt (ANSI escape sequences are supported on all platforms) Prompt string // HistoryFile is the path to the file where persistent history will be stored // (empty string disables). HistoryFile string // HistoryLimit is the maximum number of history entries to store. If it is 0 // or unset, the default value is 500; set to -1 to disable. HistoryLimit int DisableAutoSaveHistory bool // HistorySearchFold enables case-insensitive history searching. HistorySearchFold bool // AutoComplete defines the tab-completion behavior. See the documentation for // the AutoCompleter interface for details. AutoComplete AutoCompleter // Listener is an optional callback to intercept keypresses. Listener Listener // Painter is an optional callback to rewrite the buffer for display. Painter Painter // FuncFilterInputRune is an optional callback to translate keyboard inputs; // it takes in the input rune and returns (translation, ok). If ok is false, // the rune is skipped. FuncFilterInputRune func(rune) (rune, bool) // VimMode enables Vim-style insert mode by default. VimMode bool InterruptPrompt string EOFPrompt string EnableMask bool MaskRune rune // Undo controls whether to maintain an undo buffer (if enabled, // Ctrl+_ will undo the previous action). Undo bool // These fields allow customizing terminal handling. Most clients should ignore them. Stdin io.Reader Stdout io.Writer Stderr io.Writer FuncIsTerminal func() bool FuncMakeRaw func() error FuncExitRaw func() error FuncGetSize func() (width int, height int) FuncOnWidthChanged func(func()) // private fields inited bool isInteractive bool } func (c *Config) init() error { if c.inited { return nil } c.inited = true if c.Stdin == nil { c.Stdin = os.Stdin } if c.Stdout == nil { c.Stdout = os.Stdout } if c.Stderr == nil { c.Stderr = os.Stderr } if c.HistoryLimit == 0 { c.HistoryLimit = 500 } if c.InterruptPrompt == "" { c.InterruptPrompt = "^C" } else if c.InterruptPrompt == "\n" { c.InterruptPrompt = "" } if c.EOFPrompt == "" { c.EOFPrompt = "^D" } else if c.EOFPrompt == "\n" { c.EOFPrompt = "" } if c.FuncGetSize == nil { c.FuncGetSize = platform.GetScreenSize } if c.FuncIsTerminal == nil { c.FuncIsTerminal = platform.DefaultIsTerminal } rm := new(rawModeHandler) if c.FuncMakeRaw == nil { c.FuncMakeRaw = rm.Enter } if c.FuncExitRaw == nil { c.FuncExitRaw = rm.Exit } if c.FuncOnWidthChanged == nil { c.FuncOnWidthChanged = platform.DefaultOnSizeChanged } if c.Painter == nil { c.Painter = defaultPainter } c.isInteractive = c.FuncIsTerminal() return nil } // NewFromConfig creates a readline instance from the specified configuration. func NewFromConfig(cfg *Config) (*Instance, error) { if err := cfg.init(); err != nil { return nil, err } t, err := newTerminal(cfg) if err != nil { return nil, err } o := newOperation(t) return &Instance{ terminal: t, operation: o, }, nil } // NewEx is an alias for NewFromConfig, for compatibility. var NewEx = NewFromConfig // New creates a readline instance with default configuration. func New(prompt string) (*Instance, error) { return NewFromConfig(&Config{Prompt: prompt}) } func (i *Instance) ResetHistory() { i.operation.ResetHistory() } func (i *Instance) SetPrompt(s string) { cfg := i.GetConfig() cfg.Prompt = s i.SetConfig(cfg) } // readline will refresh automatic when write through Stdout() func (i *Instance) Stdout() io.Writer { return i.operation.Stdout() } // readline will refresh automatic when write through Stdout() func (i *Instance) Stderr() io.Writer { return i.operation.Stderr() } // switch VimMode in runtime func (i *Instance) SetVimMode(on bool) { cfg := i.GetConfig() cfg.VimMode = on i.SetConfig(cfg) } func (i *Instance) IsVimMode() bool { return i.operation.vim.IsEnableVimMode() } // GeneratePasswordConfig generates a suitable Config for reading passwords; // this config can be modified and then used with ReadLineWithConfig, or // SetConfig. func (i *Instance) GeneratePasswordConfig() *Config { return i.operation.GenPasswordConfig() } func (i *Instance) ReadLineWithConfig(cfg *Config) (string, error) { return i.operation.ReadLineWithConfig(cfg) } func (i *Instance) ReadPassword(prompt string) ([]byte, error) { if result, err := i.ReadLineWithConfig(i.GeneratePasswordConfig()); err == nil { return []byte(result), nil } else { return nil, err } } // ReadLine reads a line from the configured input source, allowing inline editing. // The returned error is either nil, io.EOF, or readline.ErrInterrupt. func (i *Instance) ReadLine() (string, error) { return i.operation.String() } // Readline is an alias for ReadLine, for compatibility. func (i *Instance) Readline() (string, error) { return i.ReadLine() } // SetDefault prefills a default value for the next call to Readline() // or related methods. The value will appear after the prompt for the user // to edit, with the cursor at the end of the line. func (i *Instance) SetDefault(defaultValue string) { i.operation.SetBuffer(defaultValue) } func (i *Instance) ReadLineWithDefault(defaultValue string) (string, error) { i.SetDefault(defaultValue) return i.operation.String() } // SaveToHistory adds a string to the instance's stored history. This is particularly // relevant when DisableAutoSaveHistory is configured. func (i *Instance) SaveToHistory(content string) error { return i.operation.SaveToHistory(content) } // same as readline func (i *Instance) ReadSlice() ([]byte, error) { return i.operation.Slice() } // Close() closes the readline instance, cleaning up state changes to the // terminal. It interrupts any concurrent Readline() operation, so it can be // asynchronously or from a signal handler. It is concurrency-safe and // idempotent, so it can be called multiple times. func (i *Instance) Close() error { i.closeOnce.Do(func() { // TODO reorder these? i.operation.Close() i.closeErr = i.terminal.Close() }) return i.closeErr } // CaptureExitSignal registers handlers for common exit signals that will // close the readline instance. func (i *Instance) CaptureExitSignal() { cSignal := make(chan os.Signal, 1) // TODO handle other signals in a portable way? signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { for range cSignal { i.Close() } }() } // Write writes output to the screen, redrawing the prompt and buffer // as needed. func (i *Instance) Write(b []byte) (int, error) { return i.Stdout().Write(b) } // GetConfig returns a copy of the current config. func (i *Instance) GetConfig() *Config { cfg := i.operation.GetConfig() result := new(Config) *result = *cfg return result } // SetConfig modifies the current instance's config. func (i *Instance) SetConfig(cfg *Config) error { _, err := i.operation.SetConfig(cfg) return err } // Refresh redraws the input buffer on screen. func (i *Instance) Refresh() { i.operation.Refresh() } // DisableHistory disables the saving of input lines in history. func (i *Instance) DisableHistory() { i.operation.history.Disable() } // EnableHistory enables the saving of input lines in history. func (i *Instance) EnableHistory() { i.operation.history.Enable() } // ClearScreen clears the screen. func (i *Instance) ClearScreen() { clearScreen(i.operation.Stdout()) } // Painter is a callback type to allow modifying the buffer before it is rendered // on screen, for example, to implement real-time syntax highlighting. type Painter func(line []rune, pos int) []rune func defaultPainter(line []rune, _ int) []rune { return line } // Listener is a callback type to listen for keypresses while the line is being // edited. It is invoked initially with (nil, 0, 0), and then subsequently for // any keypress until (but not including) the newline/enter keypress that completes // the input. type Listener func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) readline-0.1.3/readline_test.go000066400000000000000000000023211466750112300164520ustar00rootroot00000000000000package readline import ( "testing" "time" ) func TestRace(t *testing.T) { rl, err := NewFromConfig(&Config{}) if err != nil { t.Fatal(err) return } go func() { for range time.Tick(time.Millisecond) { rl.SetPrompt("hello") } }() go func() { time.Sleep(100 * time.Millisecond) rl.Close() }() rl.Readline() } func TestParseCPRResponse(t *testing.T) { badResponses := []string{ "", ";", "\x00", "\x00;", ";\x00", "x", "1;a", "a;1", "a;1;", "1;1;", "1;1;1", } for _, response := range badResponses { if _, err := parseCPRResponse([]byte(response)); err == nil { t.Fatalf("expected parsing of `%s` to fail, but did not", response) } } goodResponses := []struct { input string output cursorPosition }{ {"1;2", cursorPosition{1, 2}}, {"0;2", cursorPosition{0, 2}}, {"0;0", cursorPosition{0, 0}}, {"48378;9999999", cursorPosition{48378, 9999999}}, } for _, response := range goodResponses { got, err := parseCPRResponse([]byte(response.input)) if err != nil { t.Fatalf("could not parse `%s`: %v", response.input, err) } if got != response.output { t.Fatalf("expected %s to parse to %#v, got %#v", response.input, response.output, got) } } } readline-0.1.3/runebuf.go000066400000000000000000000322661466750112300153110ustar00rootroot00000000000000package readline import ( "bufio" "bytes" "fmt" "io" "strings" "sync" "github.com/ergochat/readline/internal/runes" ) type runeBuffer struct { buf []rune idx int w *terminal cpos cursorPosition ppos int // prompt start position (0 == column 1) lastKill []rune sync.Mutex } func (r *runeBuffer) pushKill(text []rune) { r.lastKill = append([]rune{}, text...) } func newRuneBuffer(w *terminal) *runeBuffer { rb := &runeBuffer{ w: w, } return rb } func (r *runeBuffer) CurrentWidth(x int) int { r.Lock() defer r.Unlock() return runes.WidthAll(r.buf[:x]) } func (r *runeBuffer) PromptLen() int { r.Lock() defer r.Unlock() return r.promptLen() } func (r *runeBuffer) promptLen() int { return runes.WidthAll(runes.ColorFilter([]rune(r.prompt()))) } func (r *runeBuffer) RuneSlice(i int) []rune { r.Lock() defer r.Unlock() if i > 0 { rs := make([]rune, i) copy(rs, r.buf[r.idx:r.idx+i]) return rs } rs := make([]rune, -i) copy(rs, r.buf[r.idx+i:r.idx]) return rs } func (r *runeBuffer) Runes() []rune { r.Lock() newr := make([]rune, len(r.buf)) copy(newr, r.buf) r.Unlock() return newr } func (r *runeBuffer) Pos() int { r.Lock() defer r.Unlock() return r.idx } func (r *runeBuffer) Len() int { r.Lock() defer r.Unlock() return len(r.buf) } func (r *runeBuffer) MoveToLineStart() { r.Refresh(func() { r.idx = 0 }) } func (r *runeBuffer) MoveBackward() { r.Refresh(func() { if r.idx == 0 { return } r.idx-- }) } func (r *runeBuffer) WriteString(s string) { r.WriteRunes([]rune(s)) } func (r *runeBuffer) WriteRune(s rune) { r.WriteRunes([]rune{s}) } func (r *runeBuffer) getConfig() *Config { return r.w.GetConfig() } func (r *runeBuffer) isInteractive() bool { return r.getConfig().isInteractive } func (r *runeBuffer) prompt() string { return r.getConfig().Prompt } func (r *runeBuffer) WriteRunes(s []rune) { r.Lock() defer r.Unlock() if r.idx == len(r.buf) { // cursor is already at end of buf data so just call // append instead of refesh to save redrawing. r.buf = append(r.buf, s...) r.idx += len(s) if r.isInteractive() { r.append(s) } } else { // writing into the data somewhere so do a refresh r.refresh(func() { tail := append(s, r.buf[r.idx:]...) r.buf = append(r.buf[:r.idx], tail...) r.idx += len(s) }) } } func (r *runeBuffer) MoveForward() { r.Refresh(func() { if r.idx == len(r.buf) { return } r.idx++ }) } func (r *runeBuffer) IsCursorInEnd() bool { r.Lock() defer r.Unlock() return r.idx == len(r.buf) } func (r *runeBuffer) Replace(ch rune) { r.Refresh(func() { r.buf[r.idx] = ch }) } func (r *runeBuffer) Erase() { r.Refresh(func() { r.idx = 0 r.pushKill(r.buf[:]) r.buf = r.buf[:0] }) } func (r *runeBuffer) Delete() (success bool) { r.Refresh(func() { if r.idx == len(r.buf) { return } r.pushKill(r.buf[r.idx : r.idx+1]) r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) success = true }) return } func (r *runeBuffer) DeleteWord() { if r.idx == len(r.buf) { return } init := r.idx for init < len(r.buf) && runes.IsWordBreak(r.buf[init]) { init++ } for i := init + 1; i < len(r.buf); i++ { if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { r.pushKill(r.buf[r.idx : i-1]) r.Refresh(func() { r.buf = append(r.buf[:r.idx], r.buf[i-1:]...) }) return } } r.Kill() } func (r *runeBuffer) MoveToPrevWord() (success bool) { r.Refresh(func() { if r.idx == 0 { return } for i := r.idx - 1; i > 0; i-- { if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { r.idx = i success = true return } } r.idx = 0 success = true }) return } func (r *runeBuffer) KillFront() { r.Refresh(func() { if r.idx == 0 { return } length := len(r.buf) - r.idx r.pushKill(r.buf[:r.idx]) copy(r.buf[:length], r.buf[r.idx:]) r.idx = 0 r.buf = r.buf[:length] }) } func (r *runeBuffer) Kill() { r.Refresh(func() { r.pushKill(r.buf[r.idx:]) r.buf = r.buf[:r.idx] }) } func (r *runeBuffer) Transpose() { r.Refresh(func() { if r.idx == 0 { // match the GNU Readline behavior, Ctrl-T at the start of the line // is a no-op: return } // OK, we have at least one character behind us: if r.idx < len(r.buf) { // swap the character in front of us with the one behind us r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx] // advance the cursor r.idx++ } else if r.idx == len(r.buf) && len(r.buf) >= 2 { // swap the two characters behind us r.buf[r.idx-2], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx-2] // leave the cursor in place since there's nowhere to go } }) } func (r *runeBuffer) MoveToNextWord() { r.Refresh(func() { for i := r.idx + 1; i < len(r.buf); i++ { if !runes.IsWordBreak(r.buf[i]) && runes.IsWordBreak(r.buf[i-1]) { r.idx = i return } } r.idx = len(r.buf) }) } func (r *runeBuffer) MoveToEndWord() { r.Refresh(func() { // already at the end, so do nothing if r.idx == len(r.buf) { return } // if we are at the end of a word already, go to next if !runes.IsWordBreak(r.buf[r.idx]) && runes.IsWordBreak(r.buf[r.idx+1]) { r.idx++ } // keep going until at the end of a word for i := r.idx + 1; i < len(r.buf); i++ { if runes.IsWordBreak(r.buf[i]) && !runes.IsWordBreak(r.buf[i-1]) { r.idx = i - 1 return } } r.idx = len(r.buf) }) } func (r *runeBuffer) BackEscapeWord() { r.Refresh(func() { if r.idx == 0 { return } for i := r.idx - 1; i >= 0; i-- { if i == 0 || (runes.IsWordBreak(r.buf[i-1])) && !runes.IsWordBreak(r.buf[i]) { r.pushKill(r.buf[i:r.idx]) r.buf = append(r.buf[:i], r.buf[r.idx:]...) r.idx = i return } } r.buf = r.buf[:0] r.idx = 0 }) } func (r *runeBuffer) Yank() { if len(r.lastKill) == 0 { return } r.Refresh(func() { buf := make([]rune, 0, len(r.buf)+len(r.lastKill)) buf = append(buf, r.buf[:r.idx]...) buf = append(buf, r.lastKill...) buf = append(buf, r.buf[r.idx:]...) r.buf = buf r.idx += len(r.lastKill) }) } func (r *runeBuffer) Backspace() { r.Refresh(func() { if r.idx == 0 { return } r.idx-- r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) }) } func (r *runeBuffer) MoveToLineEnd() { r.Lock() defer r.Unlock() if r.idx == len(r.buf) { return } r.refresh(func() { r.idx = len(r.buf) }) } // LineCount returns number of lines the buffer takes as it appears in the terminal. func (r *runeBuffer) LineCount() int { sp := r.getSplitByLine(r.buf, 1) return len(sp) } func (r *runeBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { r.Refresh(func() { if reverse { for i := r.idx - 1; i >= 0; i-- { if r.buf[i] == ch { r.idx = i if prevChar { r.idx++ } success = true return } } return } for i := r.idx + 1; i < len(r.buf); i++ { if r.buf[i] == ch { r.idx = i if prevChar { r.idx-- } success = true return } } }) return } func (r *runeBuffer) isInLineEdge() bool { sp := r.getSplitByLine(r.buf, 1) return len(sp[len(sp)-1]) == 0 // last line is 0 len } func (r *runeBuffer) getSplitByLine(rs []rune, nextWidth int) [][]rune { tWidth, _ := r.w.GetWidthHeight() cfg := r.getConfig() if cfg.EnableMask { w := runes.Width(cfg.MaskRune) masked := []rune(strings.Repeat(string(cfg.MaskRune), len(rs))) return runes.SplitByLine(runes.ColorFilter([]rune(r.prompt())), masked, r.ppos, tWidth, w) } else { return runes.SplitByLine(runes.ColorFilter([]rune(r.prompt())), rs, r.ppos, tWidth, nextWidth) } } func (r *runeBuffer) IdxLine(width int) int { r.Lock() defer r.Unlock() return r.idxLine(width) } func (r *runeBuffer) idxLine(width int) int { if width == 0 { return 0 } nextWidth := 1 if r.idx < len(r.buf) { nextWidth = runes.Width(r.buf[r.idx]) } sp := r.getSplitByLine(r.buf[:r.idx], nextWidth) return len(sp) - 1 } func (r *runeBuffer) CursorLineCount() int { tWidth, _ := r.w.GetWidthHeight() return r.LineCount() - r.IdxLine(tWidth) } func (r *runeBuffer) Refresh(f func()) { r.Lock() defer r.Unlock() r.refresh(f) } func (r *runeBuffer) refresh(f func()) { if !r.isInteractive() { if f != nil { f() } return } r.clean() if f != nil { f() } r.print() } func (r *runeBuffer) SetOffset(position cursorPosition) { r.Lock() defer r.Unlock() r.setOffset(position) } func (r *runeBuffer) setOffset(cpos cursorPosition) { r.cpos = cpos tWidth, _ := r.w.GetWidthHeight() if cpos.col > 0 && cpos.col < tWidth { r.ppos = cpos.col - 1 // c should be 1..tWidth } else { r.ppos = 0 } } // append s to the end of the current output. append is called in // place of print() when clean() was avoided. As output is appended on // the end, the cursor also needs no extra adjustment. // NOTE: assumes len(s) >= 1 which should always be true for append. func (r *runeBuffer) append(s []rune) { buf := bytes.NewBuffer(nil) slen := len(s) cfg := r.getConfig() if cfg.EnableMask { if slen > 1 && cfg.MaskRune != 0 { // write a mask character for all runes except the last rune buf.WriteString(strings.Repeat(string(cfg.MaskRune), slen-1)) } // for the last rune, write \n or mask it otherwise. if s[slen-1] == '\n' { buf.WriteRune('\n') } else if cfg.MaskRune != 0 { buf.WriteRune(cfg.MaskRune) } } else { for _, e := range cfg.Painter(s, slen) { if e == '\t' { buf.WriteString(strings.Repeat(" ", runes.TabWidth)) } else { buf.WriteRune(e) } } } if r.isInLineEdge() { buf.WriteString(" \b") } r.w.Write(buf.Bytes()) } // Print writes out the prompt and buffer contents at the current cursor position func (r *runeBuffer) Print() { r.Lock() defer r.Unlock() if !r.isInteractive() { return } r.print() } func (r *runeBuffer) print() { r.w.Write(r.output()) } func (r *runeBuffer) output() []byte { buf := bytes.NewBuffer(nil) buf.WriteString(r.prompt()) buf.WriteString("\x1b[0K") // VT100 "Clear line from cursor right", see #38 cfg := r.getConfig() if cfg.EnableMask && len(r.buf) > 0 { if cfg.MaskRune != 0 { buf.WriteString(strings.Repeat(string(cfg.MaskRune), len(r.buf)-1)) } if r.buf[len(r.buf)-1] == '\n' { buf.WriteRune('\n') } else if cfg.MaskRune != 0 { buf.WriteRune(cfg.MaskRune) } } else { for _, e := range cfg.Painter(r.buf, r.idx) { if e == '\t' { buf.WriteString(strings.Repeat(" ", runes.TabWidth)) } else { buf.WriteRune(e) } } } if r.isInLineEdge() { buf.WriteString(" \b") } // cursor position if len(r.buf) > r.idx { buf.Write(r.getBackspaceSequence()) } return buf.Bytes() } func (r *runeBuffer) getBackspaceSequence() []byte { bcnt := len(r.buf) - r.idx // backwards count to index sp := r.getSplitByLine(r.buf, 1) // Calculate how many lines up to the index line up := 0 spi := len(sp) - 1 for spi >= 0 { bcnt -= len(sp[spi]) if bcnt <= 0 { break } up++ spi-- } // Calculate what column the index should be set to column := 1 if spi == 0 { column += r.ppos } for _, rune := range sp[spi] { if bcnt >= 0 { break } column += runes.Width(rune) bcnt++ } buf := bytes.NewBuffer(nil) if up > 0 { fmt.Fprintf(buf, "\033[%dA", up) // move cursor up to index line } fmt.Fprintf(buf, "\033[%dG", column) // move cursor to column return buf.Bytes() } func (r *runeBuffer) CopyForUndo(prev []rune) (cur []rune, idx int, changed bool) { if runes.Equal(r.buf, prev) { return prev, r.idx, false } else { return runes.Copy(r.buf), r.idx, true } } func (r *runeBuffer) Restore(buf []rune, idx int) { r.buf = buf r.idx = idx } func (r *runeBuffer) Reset() []rune { ret := runes.Copy(r.buf) r.buf = r.buf[:0] r.idx = 0 return ret } func (r *runeBuffer) calWidth(m int) int { if m > 0 { return runes.WidthAll(r.buf[r.idx : r.idx+m]) } return runes.WidthAll(r.buf[r.idx+m : r.idx]) } func (r *runeBuffer) SetStyle(start, end int, style string) { if end < start { panic("end < start") } // goto start move := start - r.idx if move > 0 { r.w.Write([]byte(string(r.buf[r.idx : r.idx+move]))) } else { r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move))) } r.w.Write([]byte("\033[" + style + "m")) r.w.Write([]byte(string(r.buf[start:end]))) r.w.Write([]byte("\033[0m")) // TODO: move back } func (r *runeBuffer) SetWithIdx(idx int, buf []rune) { r.Refresh(func() { r.buf = buf r.idx = idx }) } func (r *runeBuffer) Set(buf []rune) { r.SetWithIdx(len(buf), buf) } func (r *runeBuffer) SetNoRefresh(buf []rune) { r.buf = buf r.idx = len(buf) } func (r *runeBuffer) cleanOutput(w io.Writer, idxLine int) { buf := bufio.NewWriter(w) tWidth, _ := r.w.GetWidthHeight() if tWidth == 0 { buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen())) buf.Write([]byte("\033[J")) } else { if idxLine > 0 { fmt.Fprintf(buf, "\033[%dA", idxLine) // move cursor up by idxLine } fmt.Fprintf(buf, "\033[%dG", r.ppos+1) // move cursor back to initial ppos position buf.Write([]byte("\033[J")) // clear from cursor to end of screen } buf.Flush() return } func (r *runeBuffer) Clean() { r.Lock() r.clean() r.Unlock() } func (r *runeBuffer) clean() { tWidth, _ := r.w.GetWidthHeight() r.cleanWithIdxLine(r.idxLine(tWidth)) } func (r *runeBuffer) cleanWithIdxLine(idxLine int) { if !r.isInteractive() { return } r.cleanOutput(r.w, idxLine) } readline-0.1.3/search.go000066400000000000000000000072061466750112300151040ustar00rootroot00000000000000package readline import ( "bytes" "container/list" "fmt" "sync" ) type searchState uint const ( searchStateFound searchState = iota searchStateFailing ) type searchDirection uint const ( searchDirectionForward searchDirection = iota searchDirectionBackward ) type opSearch struct { mutex sync.Mutex inMode bool state searchState dir searchDirection source *list.Element w *terminal buf *runeBuffer data []rune history *opHistory markStart int markEnd int } func newOpSearch(w *terminal, buf *runeBuffer, history *opHistory) *opSearch { return &opSearch{ w: w, buf: buf, history: history, } } func (o *opSearch) IsSearchMode() bool { o.mutex.Lock() defer o.mutex.Unlock() return o.inMode } func (o *opSearch) SearchBackspace() { o.mutex.Lock() defer o.mutex.Unlock() if len(o.data) > 0 { o.data = o.data[:len(o.data)-1] o.search(true) } } func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) { if o.dir == searchDirectionBackward { return o.history.FindBck(isNewSearch, o.data, o.buf.idx) } return o.history.FindFwd(isNewSearch, o.data, o.buf.idx) } func (o *opSearch) search(isChange bool) bool { if len(o.data) == 0 { o.state = searchStateFound o.searchRefresh(-1) return true } idx, elem := o.findHistoryBy(isChange) if elem == nil { o.searchRefresh(-2) return false } o.history.current = elem item := o.history.showItem(o.history.current.Value) start, end := 0, 0 if o.dir == searchDirectionBackward { start, end = idx, idx+len(o.data) } else { start, end = idx, idx+len(o.data) idx += len(o.data) } o.buf.SetWithIdx(idx, item) o.markStart, o.markEnd = start, end o.searchRefresh(idx) return true } func (o *opSearch) SearchChar(r rune) { o.mutex.Lock() defer o.mutex.Unlock() o.data = append(o.data, r) o.search(true) } func (o *opSearch) SearchMode(dir searchDirection) bool { o.mutex.Lock() defer o.mutex.Unlock() tWidth, _ := o.w.GetWidthHeight() if tWidth == 0 { return false } alreadyInMode := o.inMode o.inMode = true o.dir = dir o.source = o.history.current if alreadyInMode { o.search(false) } else { o.searchRefresh(-1) } return true } func (o *opSearch) ExitSearchMode(revert bool) { o.mutex.Lock() defer o.mutex.Unlock() if revert { o.history.current = o.source var redrawValue []rune if o.history.current != nil { redrawValue = o.history.showItem(o.history.current.Value) } o.buf.Set(redrawValue) } o.markStart, o.markEnd = 0, 0 o.state = searchStateFound o.inMode = false o.source = nil o.data = nil } func (o *opSearch) searchRefresh(x int) { tWidth, _ := o.w.GetWidthHeight() if x == -2 { o.state = searchStateFailing } else if x >= 0 { o.state = searchStateFound } if x < 0 { x = o.buf.idx } x = o.buf.CurrentWidth(x) x += o.buf.PromptLen() x = x % tWidth if o.markStart > 0 { o.buf.SetStyle(o.markStart, o.markEnd, "4") } lineCnt := o.buf.CursorLineCount() buf := bytes.NewBuffer(nil) buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) buf.WriteString("\033[J") if o.state == searchStateFailing { buf.WriteString("failing ") } if o.dir == searchDirectionBackward { buf.WriteString("bck") } else if o.dir == searchDirectionForward { buf.WriteString("fwd") } buf.WriteString("-i-search: ") buf.WriteString(string(o.data)) // keyword buf.WriteString("\033[4m \033[0m") // _ fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev if x > 0 { fmt.Fprintf(buf, "\033[%dC", x) // move forward } o.w.Write(buf.Bytes()) } func (o *opSearch) RefreshIfNeeded() { o.mutex.Lock() defer o.mutex.Unlock() if o.inMode { o.searchRefresh(-1) } } readline-0.1.3/terminal.go000066400000000000000000000317731466750112300154600ustar00rootroot00000000000000package readline import ( "bufio" "bytes" "errors" "fmt" "io" "strconv" "sync" "sync/atomic" "time" "github.com/ergochat/readline/internal/ansi" "github.com/ergochat/readline/internal/platform" ) const ( // see waitForDSR dsrTimeout = 250 * time.Millisecond maxAnsiLen = 32 // how many non-CPR reads to buffer while waiting for a CPR response maxCPRBufferLen = 128 * 1024 ) var ( deadlineExceeded = errors.New("deadline exceeded") concurrentReads = errors.New("concurrent read operations detected") invalidCPR = errors.New("invalid CPR response") ) /* terminal manages terminal input. The design constraints here are somewhat complex: 1. Calls to (*Instance).Readline() must always be preemptible by (*Instance).Close. This could be handled at the Operation layer instead; however, it's cleaner to provide an API in terminal itself that can interrupt attempts to read. 2. In between calls to Readline(), or *after* a call to (*Instance).Close(), stdin must be available for code outside of this library to read from. The problem is that reads from stdin in Go are not preemptible (see, for example, https://github.com/golang/go/issues/24842 ). In the worst case, an interrupted read will leave (*terminal).ioloop() running, and it will consume one more user keystroke before it exits. However, it is a design goal to read as little as possible at a time. 3. We have to handle the DSR ("device status report") query and the CPR ("cursor position report") response: https://vt100.net/docs/vt510-rm/DSR-CPR.html This involves writing an ANSI escape sequence to stdout, then waiting for the terminal to asynchronously write an ANSI escape sequence to stdin. We have to pick this value out of the stream and process it without disrupting the handling of actual user input. Moreover, concurrent Close() while a CPR query is in flight should ensure (if possible) that the response is actually read; otherwise the response may be printed to the screen, disrupting the user experience. Accordingly, the concurrency design is as follows: 1. ioloop() runs asynchronously. It operates in lockstep with the read methods: each synchronous receive from kickChan is matched with a synchronous send to outChan. It does blocking reads from stdin, reading as little as possible at a time, and passing the results back over outChan. 2. The read methods ("internal public API") GetRune() and GetCursorPosition() are not concurrency-safe and must be called in serial. They are backed by readFromStdin, which wakes ioloop() if necessary and waits for a response. If GetCursorPosition() reads non-CPR data, it will buffer it for GetRune() to read later. 3. Close() can be called asynchronously. It interrupts ioloop() (unless ioloop() is actually reading from stdin, in which case it interrupts it after the next keystroke), and also interrupts any in-progress GetRune() call. If GetCursorPosition() is in progress, it tries to wait until the CPR response has been received. It is idempotent and can be called multiple times. */ type terminal struct { cfg atomic.Pointer[Config] dimensions atomic.Pointer[termDimensions] closeOnce sync.Once closeErr error outChan chan readResult kickChan chan struct{} stopChan chan struct{} buffer []rune // actual input that we saw while waiting for the CPR inFlight bool // tracks whether we initiated a read and then gave up waiting sleeping int32 // asynchronously receive DSR messages from the terminal, // ensuring at most one query is in flight at a time dsrLock sync.Mutex dsrDone chan struct{} // nil if there is no DSR query in flight } // termDimensions stores the terminal width and height (-1 means unknown) type termDimensions struct { width int height int } type cursorPosition struct { row int col int } // readResult represents the result of a single "read operation" from the // perspective of terminal. it may be a pure no-op. the consumer needs to // read again if it didn't get what it wanted type readResult struct { r rune ok bool // is `r` valid user input? if not, we may need to read again // other data that can be conveyed in a single read operation; // currently only the CPR: pos *cursorPosition } func newTerminal(cfg *Config) (*terminal, error) { if cfg.isInteractive { if ansiErr := ansi.EnableANSI(); ansiErr != nil { return nil, fmt.Errorf("Could not enable ANSI escapes: %w", ansiErr) } } t := &terminal{ kickChan: make(chan struct{}), outChan: make(chan readResult), stopChan: make(chan struct{}), } t.SetConfig(cfg) // Get and cache the current terminal size. t.OnSizeChange() go t.ioloop() return t, nil } // SleepToResume will sleep myself, and return only if I'm resumed. func (t *terminal) SleepToResume() { if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) { return } defer atomic.StoreInt32(&t.sleeping, 0) t.ExitRawMode() platform.SuspendProcess() t.EnterRawMode() } func (t *terminal) EnterRawMode() (err error) { return t.GetConfig().FuncMakeRaw() } func (t *terminal) ExitRawMode() (err error) { return t.GetConfig().FuncExitRaw() } func (t *terminal) Write(b []byte) (int, error) { return t.GetConfig().Stdout.Write(b) } // getOffset sends a DSR query to get the current offset, then blocks // until the query returns. func (t *terminal) GetCursorPosition(deadline chan struct{}) (cursorPosition, error) { // ensure there is no in-flight query, set up a waiter ok := func() (ok bool) { t.dsrLock.Lock() defer t.dsrLock.Unlock() if t.dsrDone == nil { t.dsrDone = make(chan struct{}) ok = true } return }() if !ok { return cursorPosition{-1, -1}, concurrentReads } defer func() { t.dsrLock.Lock() defer t.dsrLock.Unlock() close(t.dsrDone) t.dsrDone = nil }() // send the DSR Cursor Position Report request to terminal stdout: // https://vt100.net/docs/vt510-rm/DSR-CPR.html _, err := t.Write([]byte("\x1b[6n")) if err != nil { return cursorPosition{-1, -1}, err } for { result, err := t.readFromStdin(deadline) if err != nil { return cursorPosition{-1, -1}, err } if result.ok { // non-CPR input, save it to be read later: t.buffer = append(t.buffer, result.r) if len(t.buffer) > maxCPRBufferLen { panic("did not receive DSR CPR response") } } if result.pos != nil { return *result.pos, nil } } } // waitForDSR waits for any in-flight DSR query to complete. this prevents // garbage from being written to the terminal when Close() interrupts an // in-flight query. func (t *terminal) waitForDSR() { t.dsrLock.Lock() dsrDone := t.dsrDone t.dsrLock.Unlock() if dsrDone != nil { // tradeoffs: if the timeout is too high, we risk slowing down Close(); // if it's too low, we risk writing the CPR to the terminal, which is bad UX, // but neither of these outcomes is catastrophic timer := time.NewTimer(dsrTimeout) select { case <-dsrDone: case <-timer.C: } timer.Stop() } } func (t *terminal) GetRune(deadline chan struct{}) (rune, error) { if len(t.buffer) > 0 { result := t.buffer[0] t.buffer = t.buffer[1:] return result, nil } return t.getRuneFromStdin(deadline) } func (t *terminal) getRuneFromStdin(deadline chan struct{}) (rune, error) { for { result, err := t.readFromStdin(deadline) if err != nil { return 0, err } else if result.ok { return result.r, nil } // else: CPR or something else we didn't understand, read again } } func (t *terminal) readFromStdin(deadline chan struct{}) (result readResult, err error) { // we may have sent a kick previously and given up on the response; // if so, don't kick again (we will try again to read the pending response) if !t.inFlight { select { case t.kickChan <- struct{}{}: t.inFlight = true case <-t.stopChan: return result, io.EOF case <-deadline: return result, deadlineExceeded } } select { case result = <-t.outChan: t.inFlight = false return result, nil case <-t.stopChan: return result, io.EOF case <-deadline: return result, deadlineExceeded } } func (t *terminal) ioloop() { // ensure close if we get an error from stdio defer t.Close() buf := bufio.NewReader(t.GetConfig().Stdin) var ansiBuf bytes.Buffer for { select { case <-t.kickChan: case <-t.stopChan: return } r, _, err := buf.ReadRune() if err != nil { return } var result readResult if r == '\x1b' { // we're starting an ANSI escape sequence: // keep reading until we reach the end of the sequence result, err = t.consumeANSIEscape(buf, &ansiBuf) if err != nil { return } } else { result = readResult{r: r, ok: true} } select { case t.outChan <- result: case <-t.stopChan: return } } } func (t *terminal) consumeANSIEscape(buf *bufio.Reader, ansiBuf *bytes.Buffer) (result readResult, err error) { ansiBuf.Reset() initial, _, err := buf.ReadRune() if err != nil { return } // we already read one \x1b. this can indicate either the start of an ANSI // escape sequence, or a keychord with Alt (e.g. Alt+f produces `\x1bf` in // a typical xterm). switch initial { case 'f': // Alt-f in xterm, or Option+RightArrow in iTerm2 with "Natural text editing" return readResult{r: MetaForward, ok: true}, nil // Alt-f case 'b': // Alt-b in xterm, or Option+LeftArrow in iTerm2 with "Natural text editing" return readResult{r: MetaBackward, ok: true}, nil // Alt-b case '[', 'O': // this is a real ANSI escape sequence, read the rest of the sequence below: case '\x1b': // Alt plus a real ANSI escape sequence. Handle this specially since // right now the only cases we want to handle are the arrow keys: return consumeAltSequence(buf) default: return // invalid, ignore } // data consists of ; and 0-9 , anything else terminates the sequence var type_ rune for { r, _, err := buf.ReadRune() if err != nil { return result, err } if r == ';' || ('0' <= r && r <= '9') { ansiBuf.WriteRune(r) } else { type_ = r break } } var r rune switch type_ { case 'R': if initial == '[' { // DSR CPR response; if we can't parse it, just ignore it // (do not return an error here because that would stop ioloop()) if cpos, err := parseCPRResponse(ansiBuf.Bytes()); err == nil { return readResult{r: 0, ok: false, pos: &cpos}, nil } } case 'D': if altModifierEnabled(ansiBuf.Bytes()) { r = MetaBackward } else { r = CharBackward } case 'C': if altModifierEnabled(ansiBuf.Bytes()) { r = MetaForward } else { r = CharForward } case 'A': r = CharPrev case 'B': r = CharNext case 'H': r = CharLineStart case 'F': r = CharLineEnd case '~': if initial == '[' { switch string(ansiBuf.Bytes()) { case "3": r = MetaDeleteKey // this is the key typically labeled "Delete" case "1", "7": r = CharLineStart // "Home" key case "4", "8": r = CharLineEnd // "End" key } } case 'Z': if initial == '[' { r = MetaShiftTab } } if r != 0 { return readResult{r: r, ok: true}, nil } return // default: no interpretable rune value } func consumeAltSequence(buf *bufio.Reader) (result readResult, err error) { initial, _, err := buf.ReadRune() if err != nil { return } if initial != '[' { return } second, _, err := buf.ReadRune() if err != nil { return } switch second { case 'D': return readResult{r: MetaBackward, ok: true}, nil case 'C': return readResult{r: MetaForward, ok: true}, nil default: return } } func altModifierEnabled(payload []byte) bool { // https://www.xfree86.org/current/ctlseqs.html ; modifier keycodes // go after the semicolon, e.g. Alt-LeftArrow is `\x1b[1;3D` in VTE // terminals, where 3 indicates Alt if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 { if string(payload[semicolonIdx+1:]) == "3" { return true } } return false } func parseCPRResponse(payload []byte) (cursorPosition, error) { if semicolonIdx := bytes.IndexByte(payload, ';'); semicolonIdx != -1 { if row, err := strconv.Atoi(string(payload[:semicolonIdx])); err == nil { if col, err := strconv.Atoi(string(payload[semicolonIdx+1:])); err == nil { return cursorPosition{row: row, col: col}, nil } } } return cursorPosition{-1, -1}, invalidCPR } func (t *terminal) Bell() { t.Write([]byte{CharBell}) } func (t *terminal) Close() error { t.closeOnce.Do(func() { t.waitForDSR() close(t.stopChan) // don't close outChan; outChan results should always be valid. // instead we always select on both outChan and stopChan t.closeErr = t.ExitRawMode() }) return t.closeErr } func (t *terminal) SetConfig(c *Config) error { t.cfg.Store(c) return nil } func (t *terminal) GetConfig() *Config { return t.cfg.Load() } // OnSizeChange gets the current terminal size and caches it func (t *terminal) OnSizeChange() { cfg := t.GetConfig() width, height := cfg.FuncGetSize() t.dimensions.Store(&termDimensions{ width: width, height: height, }) } // GetWidthHeight returns the cached width, height values from the terminal func (t *terminal) GetWidthHeight() (width, height int) { dimensions := t.dimensions.Load() return dimensions.width, dimensions.height } readline-0.1.3/undo.go000066400000000000000000000021221466750112300145740ustar00rootroot00000000000000package readline import ( "github.com/ergochat/readline/internal/ringbuf" ) type undoEntry struct { pos int buf []rune } // nil receiver is a valid no-op object type opUndo struct { op *operation stack ringbuf.Buffer[undoEntry] } func newOpUndo(op *operation) *opUndo { o := &opUndo{op: op} o.stack.Initialize(32, 64) o.init() return o } func (o *opUndo) add() { if o == nil { return } top, success := o.stack.Pop() buf, pos, changed := o.op.buf.CopyForUndo(top.buf) // if !success, top.buf is nil newEntry := undoEntry{pos: pos, buf: buf} if !success { o.stack.Add(newEntry) } else if !changed { o.stack.Add(newEntry) // update cursor position } else { o.stack.Add(top) o.stack.Add(newEntry) } } func (o *opUndo) undo() { if o == nil { return } top, success := o.stack.Pop() if !success { return } o.op.buf.Restore(top.buf, top.pos) o.op.buf.Refresh(nil) } func (o *opUndo) init() { if o == nil { return } buf, pos, _ := o.op.buf.CopyForUndo(nil) initialEntry := undoEntry{ pos: pos, buf: buf, } o.stack.Clear() o.stack.Add(initialEntry) } readline-0.1.3/utils.go000066400000000000000000000033651466750112300150010ustar00rootroot00000000000000package readline import ( "container/list" "fmt" "io" "os" "sync" "syscall" "github.com/ergochat/readline/internal/term" ) const ( CharLineStart = 1 CharBackward = 2 CharInterrupt = 3 CharEOT = 4 CharLineEnd = 5 CharForward = 6 CharBell = 7 CharCtrlH = 8 CharTab = 9 CharCtrlJ = 10 CharKill = 11 CharCtrlL = 12 CharEnter = 13 CharNext = 14 CharPrev = 16 CharBckSearch = 18 CharFwdSearch = 19 CharTranspose = 20 CharCtrlU = 21 CharCtrlW = 23 CharCtrlY = 25 CharCtrlZ = 26 CharEsc = 27 CharCtrl_ = 31 CharO = 79 CharEscapeEx = 91 CharBackspace = 127 ) const ( MetaBackward rune = -iota - 1 MetaForward MetaDelete MetaBackspace MetaTranspose MetaShiftTab MetaDeleteKey ) type rawModeHandler struct { sync.Mutex state *term.State } func (r *rawModeHandler) Enter() (err error) { r.Lock() defer r.Unlock() r.state, err = term.MakeRaw(int(syscall.Stdin)) return err } func (r *rawModeHandler) Exit() error { r.Lock() defer r.Unlock() if r.state == nil { return nil } err := term.Restore(int(syscall.Stdin), r.state) if err == nil { r.state = nil } return err } func clearScreen(w io.Writer) error { _, err := w.Write([]byte("\x1b[H\x1b[J")) return err } // ----------------------------------------------------------------------------- // print a linked list to Debug() func debugList(l *list.List) { idx := 0 for e := l.Front(); e != nil; e = e.Next() { debugPrint("%d %+v", idx, e.Value) idx++ } } // append log info to another file func debugPrint(fmtStr string, o ...interface{}) { f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) fmt.Fprintf(f, fmtStr, o...) fmt.Fprintln(f) f.Close() } readline-0.1.3/vim.go000066400000000000000000000050761466750112300144350ustar00rootroot00000000000000package readline const ( vim_NORMAL = iota vim_INSERT vim_VISUAL ) type opVim struct { op *operation vimMode int } func newVimMode(op *operation) *opVim { ov := &opVim{ op: op, vimMode: vim_INSERT, } return ov } func (o *opVim) IsEnableVimMode() bool { return o.op.GetConfig().VimMode } func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { rb := o.op.buf handled = true switch r { case 'h': t = CharBackward case 'j': t = CharNext case 'k': t = CharPrev case 'l': t = CharForward case '0', '^': rb.MoveToLineStart() case '$': rb.MoveToLineEnd() case 'x': rb.Delete() if rb.IsCursorInEnd() { rb.MoveBackward() } case 'r': rb.Replace(readNext()) case 'd': next := readNext() switch next { case 'd': rb.Erase() case 'w': rb.DeleteWord() case 'h': rb.Backspace() case 'l': rb.Delete() } case 'p': rb.Yank() case 'b', 'B': rb.MoveToPrevWord() case 'w', 'W': rb.MoveToNextWord() case 'e', 'E': rb.MoveToEndWord() case 'f', 'F', 't', 'T': next := readNext() prevChar := r == 't' || r == 'T' reverse := r == 'F' || r == 'T' switch next { case CharEsc: default: rb.MoveTo(next, prevChar, reverse) } default: return r, false } return t, true } func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { rb := o.op.buf handled = true switch r { case 'i': case 'I': rb.MoveToLineStart() case 'a': rb.MoveForward() case 'A': rb.MoveToLineEnd() case 's': rb.Delete() case 'S': rb.Erase() case 'c': next := readNext() switch next { case 'c': rb.Erase() case 'w': rb.DeleteWord() case 'h': rb.Backspace() case 'l': rb.Delete() } default: return r, false } o.EnterVimInsertMode() return } func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { switch r { case CharEnter, CharInterrupt: o.vimMode = vim_INSERT // ??? return r } if r, handled := o.handleVimNormalMovement(r, readNext); handled { return r } if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { return r } // invalid operation o.op.t.Bell() return 0 } func (o *opVim) EnterVimInsertMode() { o.vimMode = vim_INSERT } func (o *opVim) ExitVimInsertMode() { o.vimMode = vim_NORMAL } func (o *opVim) HandleVim(r rune, readNext func() rune) rune { if o.vimMode == vim_NORMAL { return o.HandleVimNormal(r, readNext) } if r == CharEsc { o.ExitVimInsertMode() return 0 } switch o.vimMode { case vim_INSERT: return r case vim_VISUAL: } return r }