readline/runebuf.go

736 lines
14 KiB
Go
Raw Normal View History

2015-09-20 18:14:29 +03:00
package readline
import (
2016-03-13 13:32:48 +03:00
"bufio"
2015-09-20 18:14:29 +03:00
"bytes"
"fmt"
2015-09-20 18:14:29 +03:00
"io"
2015-11-20 15:56:42 +03:00
"strings"
"sync"
2015-09-20 18:14:29 +03:00
)
2015-10-02 05:37:21 +03:00
type runeBufferBck struct {
buf []rune
idx int
}
2015-09-20 18:14:29 +03:00
type RuneBuffer struct {
2015-09-22 13:16:24 +03:00
buf []rune
idx int
2015-09-23 09:52:45 +03:00
prompt []rune
2015-09-22 13:16:24 +03:00
w io.Writer
2015-09-27 05:12:15 +03:00
2016-03-31 05:55:53 +03:00
interactive bool
cfg *Config
2015-10-02 05:37:21 +03:00
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
width int
height int
2016-03-13 13:32:48 +03:00
2015-10-02 05:37:21 +03:00
bck *runeBufferBck
offset string // is offset useful? scrolling means row varies
ppos int // prompt start position (0 == column 1)
lastKill []rune
sync.Mutex
2015-10-02 05:37:21 +03:00
}
2022-07-15 15:48:48 +03:00
func (r *RuneBuffer) pushKill(text []rune) {
r.lastKill = append([]rune{}, text...)
}
2016-03-13 13:32:48 +03:00
func (r *RuneBuffer) OnWidthChange(newWidth int) {
r.Lock()
2016-03-13 13:32:48 +03:00
r.width = newWidth
r.Unlock()
2016-03-13 13:32:48 +03:00
}
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
func (r *RuneBuffer) OnSizeChange(newWidth, newHeight int) {
r.Lock()
r.width = newWidth
r.height = newHeight
r.Unlock()
}
2015-10-02 05:37:21 +03:00
func (r *RuneBuffer) Backup() {
r.Lock()
2015-10-02 05:37:21 +03:00
r.bck = &runeBufferBck{r.buf, r.idx}
r.Unlock()
2015-10-02 05:37:21 +03:00
}
func (r *RuneBuffer) Restore() {
r.Refresh(func() {
if r.bck == nil {
return
}
r.buf = r.bck.buf
r.idx = r.bck.idx
})
2015-09-20 18:14:29 +03:00
}
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int, height int) *RuneBuffer {
2015-09-20 18:14:29 +03:00
rb := &RuneBuffer{
w: w,
interactive: cfg.useInteractive(),
cfg: cfg,
2016-03-13 13:32:48 +03:00
width: width,
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
height: height,
2015-09-20 18:14:29 +03:00
}
2015-09-27 13:54:26 +03:00
rb.SetPrompt(prompt)
2015-09-20 18:14:29 +03:00
return rb
}
func (r *RuneBuffer) SetConfig(cfg *Config) {
r.Lock()
r.cfg = cfg
r.interactive = cfg.useInteractive()
r.Unlock()
}
2015-11-20 15:56:42 +03:00
func (r *RuneBuffer) SetMask(m rune) {
r.Lock()
2016-03-05 05:46:11 +03:00
r.cfg.MaskRune = m
r.Unlock()
2015-11-20 15:56:42 +03:00
}
func (r *RuneBuffer) CurrentWidth(x int) int {
r.Lock()
defer r.Unlock()
2015-10-04 16:56:34 +03:00
return runes.WidthAll(r.buf[:x])
}
2015-09-23 09:52:45 +03:00
func (r *RuneBuffer) PromptLen() int {
2016-09-10 11:03:59 +03:00
r.Lock()
width := r.promptLen()
r.Unlock()
return width
}
func (r *RuneBuffer) promptLen() int {
2015-10-04 16:56:34 +03:00
return runes.WidthAll(runes.ColorFilter(r.prompt))
2015-09-23 09:52:45 +03:00
}
2015-09-25 17:56:00 +03:00
func (r *RuneBuffer) RuneSlice(i int) []rune {
r.Lock()
defer r.Unlock()
2015-09-25 17:56:00 +03:00
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
}
2015-09-20 18:14:29 +03:00
func (r *RuneBuffer) Runes() []rune {
r.Lock()
2015-09-25 17:56:00 +03:00
newr := make([]rune, len(r.buf))
copy(newr, r.buf)
r.Unlock()
2015-09-25 17:56:00 +03:00
return newr
2015-09-20 18:14:29 +03:00
}
func (r *RuneBuffer) Pos() int {
r.Lock()
defer r.Unlock()
2015-09-20 18:14:29 +03:00
return r.idx
}
func (r *RuneBuffer) Len() int {
r.Lock()
defer r.Unlock()
2015-09-20 18:14:29 +03:00
return len(r.buf)
}
func (r *RuneBuffer) MoveToLineStart() {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
2015-09-28 06:13:39 +03:00
if r.idx == 0 {
return
}
2015-09-27 05:12:15 +03:00
r.idx = 0
})
2015-09-20 18:14:29 +03:00
}
2015-09-21 08:13:30 +03:00
func (r *RuneBuffer) MoveBackward() {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
2015-09-28 06:13:39 +03:00
if r.idx == 0 {
return
}
2015-09-27 05:12:15 +03:00
r.idx--
})
2015-09-20 18:14:29 +03:00
}
2015-09-23 08:52:26 +03:00
func (r *RuneBuffer) WriteString(s string) {
r.WriteRunes([]rune(s))
2015-09-20 18:14:29 +03:00
}
2015-09-23 08:52:26 +03:00
func (r *RuneBuffer) WriteRune(s rune) {
r.WriteRunes([]rune{s})
2015-09-20 18:14:29 +03:00
}
2015-09-23 08:52:26 +03:00
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...)
2015-09-27 05:12:15 +03:00
r.idx += len(s)
if r.interactive {
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)
})
}
2015-09-20 18:14:29 +03:00
}
2015-09-21 08:13:30 +03:00
func (r *RuneBuffer) MoveForward() {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
if r.idx == len(r.buf) {
return
}
r.idx++
})
2015-09-20 18:14:29 +03:00
}
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
})
}
2015-10-02 05:37:21 +03:00
func (r *RuneBuffer) Erase() {
r.Refresh(func() {
r.idx = 0
r.pushKill(r.buf[:])
2015-10-02 05:37:21 +03:00
r.buf = r.buf[:0]
})
}
2015-10-01 17:44:43 +03:00
func (r *RuneBuffer) Delete() (success bool) {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
if r.idx == len(r.buf) {
return
}
r.pushKill(r.buf[r.idx : r.idx+1])
2015-09-27 05:12:15 +03:00
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
2015-10-01 17:44:43 +03:00
success = true
2015-09-27 05:12:15 +03:00
})
2015-10-01 17:44:43 +03:00
return
2015-09-20 18:14:29 +03:00
}
func (r *RuneBuffer) DeleteWord() {
if r.idx == len(r.buf) {
return
}
2015-09-21 17:51:48 +03:00
init := r.idx
2015-09-23 06:59:39 +03:00
for init < len(r.buf) && IsWordBreak(r.buf[init]) {
2015-09-21 17:51:48 +03:00
init++
}
for i := init + 1; i < len(r.buf); i++ {
2015-09-23 06:59:39 +03:00
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
2022-07-15 15:48:48 +03:00
r.pushKill(r.buf[r.idx : i-1])
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
r.buf = append(r.buf[:r.idx], r.buf[i-1:]...)
})
2015-09-20 18:14:29 +03:00
return
}
}
2015-09-21 17:51:48 +03:00
r.Kill()
2015-09-20 18:14:29 +03:00
}
2015-10-01 17:44:43 +03:00
func (r *RuneBuffer) MoveToPrevWord() (success bool) {
2015-09-28 06:13:39 +03:00
r.Refresh(func() {
if r.idx == 0 {
2015-09-20 18:14:29 +03:00
return
}
2015-09-28 06:13:39 +03:00
for i := r.idx - 1; i > 0; i-- {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.idx = i
2015-10-01 17:44:43 +03:00
success = true
2015-09-28 06:13:39 +03:00
return
}
}
2015-09-27 05:12:15 +03:00
r.idx = 0
2015-10-01 17:44:43 +03:00
success = true
2015-09-27 05:12:15 +03:00
})
2015-10-01 17:44:43 +03:00
return
2015-09-20 18:14:29 +03:00
}
2015-09-28 06:13:39 +03:00
func (r *RuneBuffer) KillFront() {
r.Refresh(func() {
if r.idx == 0 {
return
}
length := len(r.buf) - r.idx
r.pushKill(r.buf[:r.idx])
2015-09-28 06:13:39 +03:00
copy(r.buf[:length], r.buf[r.idx:])
r.idx = 0
r.buf = r.buf[:length]
})
2015-09-20 18:14:29 +03:00
}
2015-09-21 17:51:48 +03:00
func (r *RuneBuffer) Kill() {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
r.pushKill(r.buf[r.idx:])
2015-09-27 05:12:15 +03:00
r.buf = r.buf[:r.idx]
})
2015-09-21 17:51:48 +03:00
}
2015-09-23 08:03:13 +03:00
func (r *RuneBuffer) Transpose() {
2015-09-28 06:13:39 +03:00
r.Refresh(func() {
2015-09-23 06:59:39 +03:00
if len(r.buf) == 1 {
2015-09-28 06:13:39 +03:00
r.idx++
2015-09-23 06:59:39 +03:00
}
2015-09-28 06:13:39 +03:00
if len(r.buf) < 2 {
return
}
2015-09-27 05:12:15 +03:00
if r.idx == 0 {
r.idx = 1
} else if r.idx >= len(r.buf) {
r.idx = len(r.buf) - 1
}
r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx]
r.idx++
})
2015-09-23 06:46:56 +03:00
}
2015-09-20 18:14:29 +03:00
func (r *RuneBuffer) MoveToNextWord() {
2015-09-28 06:13:39 +03:00
r.Refresh(func() {
for i := r.idx + 1; i < len(r.buf); i++ {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
2015-09-27 05:12:15 +03:00
r.idx = i
2015-09-28 06:13:39 +03:00
return
}
2015-09-20 18:14:29 +03:00
}
2015-09-28 06:13:39 +03:00
2015-09-27 05:12:15 +03:00
r.idx = len(r.buf)
})
2015-09-20 18:14:29 +03:00
}
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 !IsWordBreak(r.buf[r.idx]) && 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 IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) {
r.idx = i - 1
return
}
}
r.idx = len(r.buf)
})
}
2015-09-21 16:00:48 +03:00
func (r *RuneBuffer) BackEscapeWord() {
2015-09-28 06:13:39 +03:00
r.Refresh(func() {
if r.idx == 0 {
return
}
for i := r.idx - 1; i > 0; i-- {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.pushKill(r.buf[i:r.idx])
2015-09-27 05:12:15 +03:00
r.buf = append(r.buf[:i], r.buf[r.idx:]...)
r.idx = i
2015-09-28 06:13:39 +03:00
return
}
2015-09-21 16:00:48 +03:00
}
2015-09-27 05:12:15 +03:00
r.buf = r.buf[:0]
r.idx = 0
})
2015-09-21 16:00:48 +03:00
}
func (r *RuneBuffer) Yank() {
if len(r.lastKill) == 0 {
return
}
r.Refresh(func() {
2022-07-15 15:48:48 +03:00
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)
})
}
2015-09-21 16:00:48 +03:00
func (r *RuneBuffer) Backspace() {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
2015-09-28 06:13:39 +03:00
if r.idx == 0 {
return
}
2015-09-27 05:12:15 +03:00
r.idx--
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
})
2015-09-20 18:14:29 +03:00
}
func (r *RuneBuffer) MoveToLineEnd() {
r.Lock()
defer r.Unlock()
if r.idx == len(r.buf) {
return
}
r.refresh(func() {
2015-09-27 05:12:15 +03:00
r.idx = len(r.buf)
})
2015-09-20 18:14:29 +03:00
}
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
// 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)
2015-09-22 18:01:15 +03:00
}
2015-10-01 17:44:43 +03:00
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
}
2016-03-30 18:55:12 +03:00
func (r *RuneBuffer) isInLineEdge() bool {
if isWindows {
return false
}
sp := r.getSplitByLine(r.buf, 1)
return len(sp[len(sp)-1]) == 0 // last line is 0 len
2016-03-30 18:55:12 +03:00
}
func (r *RuneBuffer) getSplitByLine(rs []rune, nextWidth int) [][]rune {
if r.cfg.EnableMask {
w := runes.Width(r.cfg.MaskRune)
masked := []rune(strings.Repeat(string(r.cfg.MaskRune), len(rs)))
return SplitByLine(runes.ColorFilter(r.prompt), masked, r.ppos, r.width, w)
} else {
return SplitByLine(runes.ColorFilter(r.prompt), rs, r.ppos, r.width, nextWidth)
}
2016-03-30 18:55:12 +03:00
}
2016-03-13 13:32:48 +03:00
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)
2016-03-05 10:27:12 +03:00
return len(sp) - 1
2015-09-22 18:01:15 +03:00
}
func (r *RuneBuffer) CursorLineCount() int {
A Pager to list completion candidates if they don't fit on one page. Main Features: - If there are too many candidates returned by the completer, completemode and completeselectmode did not work properly. Similar to the bash completion pager, list candidates and offer "--More--" on the end of each page. User can select " ", "y" or "Y" to keep listing or "q", "Q", "n", "N" to stop listing. When paging completes, we also exit out of completion mode. - Added aggregate completion when entering completeselectmode where the candiddates dwindle down sharing a larger common prefix. This makes typing a little faster than having to select. More bash-like behaviour. Other Fixes: - Fix various crashes where candidates are too wide for the width of the screen and causes division by zero. - Fix crash with wide (Asian characters) in completion. - Streamline redrawing as CompleteRefresh was called too often. - Fix crashes around ctrl-a and ctrl-b in select mode when candidates don't fit on a line - Fix prev/next candidates in select mode when candidates don't fit on a line - Fix crash when ctrl-k was pressed in select mode. This caused us to exitselectmode which cleaned up all the data but left us in complete mode such that if CompleteRefresh was callled directly, the data was not initialized. - Fix complete and select mode redraw issues when candidates did not fit on one line. - Fix cursor position issues after CompleteRefresh especially if the prompt and buffer also went over 1 line. - Fix redraw issue where exiting completion mode using certain key presses leaves candidates on the screen. Fixes for Windows: - Use window size for visible height/width instead of buffer size - Adjust for Window's EOL behaviour. Notes: - Added Height info to different structures as the decision to page or not required height information. - Added OnSizeChange(). Didn't know if I could get rid of the OnWidthChange()? Would be nice to remove the Width stuff and just have Size (width + height info).
2022-06-28 08:49:19 +03:00
return r.LineCount() - r.IdxLine(r.width)
2015-09-22 18:01:15 +03:00
}
2015-09-27 05:12:15 +03:00
func (r *RuneBuffer) Refresh(f func()) {
r.Lock()
defer r.Unlock()
r.refresh(f)
}
func (r *RuneBuffer) refresh(f func()) {
if !r.interactive {
if f != nil {
f()
}
return
}
r.clean()
2015-09-27 05:12:15 +03:00
if f != nil {
f()
}
2016-03-13 13:32:48 +03:00
r.print()
}
// getAndSetOffset queries the terminal for the current cursor position by
// writing a control sequence to the terminal. This call is asynchronous
// and it returns before any offset has actually been set as the terminal
// will write the offset back to us via stdin and there may already be
// other data in the stdin buffer ahead of it.
// This function is called at the start of readline each time.
func (r *RuneBuffer) getAndSetOffset(t *Terminal) {
if !r.interactive {
return
}
if !isWindows {
// 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.
r.w.Write([]byte(" \b"))
}
t.GetOffset(r.SetOffset)
}
func (r *RuneBuffer) SetOffset(offset string) {
r.Lock()
defer r.Unlock()
r.setOffset(offset)
}
func (r *RuneBuffer) setOffset(offset string) {
r.offset = offset
if _, c, ok := (&escapeKeyPair{attr:offset}).Get2(); ok && c > 0 && c < r.width {
r.ppos = c - 1 // c should be 1..width
} 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)
if r.cfg.EnableMask {
if slen > 1 && r.cfg.MaskRune != 0 {
// write a mask character for all runes except the last rune
buf.WriteString(strings.Repeat(string(r.cfg.MaskRune), slen-1))
}
// for the last rune, write \n or mask it otherwise.
if s[slen-1] == '\n' {
buf.WriteRune('\n')
} else if r.cfg.MaskRune != 0 {
buf.WriteRune(r.cfg.MaskRune)
}
} else {
for _, e := range r.cfg.Painter.Paint(s, slen) {
if e == '\t' {
buf.WriteString(strings.Repeat(" ", 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.interactive {
return
}
r.print()
}
2016-03-13 13:32:48 +03:00
func (r *RuneBuffer) print() {
2015-09-27 05:12:15 +03:00
r.w.Write(r.output())
2015-09-20 18:14:29 +03:00
}
2015-09-27 05:12:15 +03:00
func (r *RuneBuffer) output() []byte {
2015-09-20 18:14:29 +03:00
buf := bytes.NewBuffer(nil)
2015-09-23 09:52:45 +03:00
buf.WriteString(string(r.prompt))
2016-03-05 05:46:11 +03:00
if r.cfg.EnableMask && len(r.buf) > 0 {
if r.cfg.MaskRune != 0 {
buf.WriteString(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1))
2015-11-20 15:56:42 +03:00
}
if r.buf[len(r.buf)-1] == '\n' {
buf.WriteRune('\n')
} else if r.cfg.MaskRune != 0 {
buf.WriteRune(r.cfg.MaskRune)
2016-03-05 10:27:12 +03:00
}
2015-11-20 15:56:42 +03:00
} else {
for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) {
if e == '\t' {
2016-09-02 15:10:31 +03:00
buf.WriteString(strings.Repeat(" ", TabWidth))
} else {
buf.WriteRune(e)
2016-09-02 15:10:31 +03:00
}
}
}
if r.isInLineEdge() {
buf.WriteString(" \b")
2015-09-23 06:10:36 +03:00
}
// cursor position
2016-03-30 18:55:12 +03:00
if len(r.buf) > r.idx {
buf.Write(r.getBackspaceSequence())
2016-03-30 18:55:12 +03:00
}
2015-09-20 18:14:29 +03:00
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()
}
2015-09-20 18:14:29 +03:00
func (r *RuneBuffer) Reset() []rune {
2016-02-17 17:20:03 +03:00
ret := runes.Copy(r.buf)
2015-09-20 18:14:29 +03:00
r.buf = r.buf[:0]
r.idx = 0
return ret
}
2015-09-21 08:13:30 +03:00
func (r *RuneBuffer) calWidth(m int) int {
if m > 0 {
2015-10-04 16:56:34 +03:00
return runes.WidthAll(r.buf[r.idx : r.idx+m])
}
2015-10-04 16:56:34 +03:00
return runes.WidthAll(r.buf[r.idx+m : r.idx])
}
2015-09-23 06:10:36 +03:00
func (r *RuneBuffer) SetStyle(start, end int, style string) {
if end < start {
panic("end < start")
}
// goto start
move := start - r.idx
2015-09-23 06:10:36 +03:00
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)))
2015-09-23 06:10:36 +03:00
}
2015-09-29 12:49:58 +03:00
r.w.Write([]byte("\033[" + style + "m"))
2015-09-23 06:10:36 +03:00
r.w.Write([]byte(string(r.buf[start:end])))
r.w.Write([]byte("\033[0m"))
// TODO: move back
2015-09-23 06:10:36 +03:00
}
2015-09-22 18:01:15 +03:00
func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
2015-09-27 05:12:15 +03:00
r.Refresh(func() {
r.buf = buf
r.idx = idx
})
2015-09-21 08:13:30 +03:00
}
2015-09-22 18:01:15 +03:00
func (r *RuneBuffer) Set(buf []rune) {
r.SetWithIdx(len(buf), buf)
}
2015-09-28 19:26:49 +03:00
func (r *RuneBuffer) SetPrompt(prompt string) {
2016-09-10 11:03:59 +03:00
r.Lock()
2015-09-28 19:26:49 +03:00
r.prompt = []rune(prompt)
2016-09-10 11:03:59 +03:00
r.Unlock()
2015-09-28 19:26:49 +03:00
}
2016-03-13 13:32:48 +03:00
func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
buf := bufio.NewWriter(w)
2015-09-28 19:26:49 +03:00
if r.width == 0 {
2016-09-10 11:03:59 +03:00
buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen()))
buf.Write([]byte("\033[J"))
2016-03-13 13:32:48 +03:00
} else {
if idxLine > 0 {
fmt.Fprintf(buf, "\033[%dA", idxLine) // move cursor up by idxLine
2016-03-13 13:32:48 +03:00
}
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
2015-09-28 19:26:49 +03:00
}
2016-03-13 13:32:48 +03:00
buf.Flush()
return
2015-09-28 19:26:49 +03:00
}
func (r *RuneBuffer) Clean() {
r.Lock()
r.clean()
r.Unlock()
}
func (r *RuneBuffer) clean() {
r.cleanWithIdxLine(r.idxLine(r.width))
2016-03-13 13:32:48 +03:00
}
func (r *RuneBuffer) cleanWithIdxLine(idxLine int) {
if !r.interactive {
2015-09-28 19:26:49 +03:00
return
}
2016-03-13 13:32:48 +03:00
r.cleanOutput(r.w, idxLine)
2015-09-28 19:26:49 +03:00
}