mirror of https://github.com/mum4k/termdash.git
418 lines
12 KiB
Go
418 lines
12 KiB
Go
// Copyright 2019 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package textinput
|
|
|
|
// editor.go contains data types that edit the content of the text input field.
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mum4k/termdash/private/numbers"
|
|
"github.com/mum4k/termdash/private/runewidth"
|
|
)
|
|
|
|
// fieldData are the data currently present inside the text input field.
|
|
type fieldData []rune
|
|
|
|
// String implements fmt.Stringer.
|
|
func (fd fieldData) String() string {
|
|
var b strings.Builder
|
|
for _, r := range fd {
|
|
b.WriteRune(r)
|
|
}
|
|
return fmt.Sprintf("%q", b.String())
|
|
}
|
|
|
|
// insertAt inserts rune at the specified index.
|
|
func (fd *fieldData) insertAt(idx int, r rune) {
|
|
*fd = append(
|
|
(*fd)[:idx],
|
|
append(fieldData{r}, (*fd)[idx:]...)...,
|
|
)
|
|
}
|
|
|
|
// deleteAt deletes rune at the specified index.
|
|
func (fd *fieldData) deleteAt(idx int) {
|
|
*fd = append((*fd)[:idx], (*fd)[idx+1:]...)
|
|
}
|
|
|
|
// cellsBefore given an endIdx calculates startIdx that results in range that
|
|
// will take at most the provided number of cells to print on the screen.
|
|
func (fd *fieldData) cellsBefore(cells, endIdx int) int {
|
|
if endIdx == 0 {
|
|
return 0
|
|
}
|
|
|
|
usedCells := 0
|
|
for i := endIdx; i > 0; i-- {
|
|
prev := (*fd)[i-1]
|
|
width := runewidth.RuneWidth(prev)
|
|
|
|
if usedCells+width > cells {
|
|
return i
|
|
}
|
|
usedCells += width
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// cellsAfter given a startIdx calculates endIdx that results in range that
|
|
// will take at most the provided number of cells to print on the screen.
|
|
func (fd *fieldData) cellsAfter(cells, startIdx int) int {
|
|
if startIdx >= len(*fd) || cells == 0 {
|
|
return startIdx
|
|
}
|
|
|
|
first := (*fd)[startIdx]
|
|
usedCells := runewidth.RuneWidth(first)
|
|
for i := startIdx + 1; i < len(*fd); i++ {
|
|
r := (*fd)[i]
|
|
width := runewidth.RuneWidth(r)
|
|
if usedCells+width > cells {
|
|
return i
|
|
}
|
|
usedCells += width
|
|
}
|
|
return len(*fd)
|
|
}
|
|
|
|
// minForArrows is the smallest number of cells in the window where we can
|
|
// indicate hidden text with left and right arrow.
|
|
const minForArrows = 3
|
|
|
|
// curMinIdx returns the lowest acceptable index for cursor position that is
|
|
// still within the visible range.
|
|
func curMinIdx(start, cells int) int {
|
|
if start == 0 || cells < minForArrows {
|
|
// The very first rune is visible, so the cursor can go all the way to
|
|
// the start.
|
|
return start
|
|
}
|
|
|
|
// When the first rune isn't visible, the cursor cannot go on the first
|
|
// cell in the visible range since it contains the left arrow.
|
|
return start + 1
|
|
}
|
|
|
|
// curMaxIdx returns the highest acceptable index for cursor position that is
|
|
// still within the visible range given the number of runes in data.
|
|
func curMaxIdx(start, end, cells, runeCount int) int {
|
|
if end == runeCount+1 || cells < minForArrows {
|
|
// The last rune is visible, so the cursor can go all the way to the
|
|
// end.
|
|
return end - 1
|
|
}
|
|
|
|
// When the last rune isn't visible, the cursor cannot go on the last cell
|
|
// in the window that is reserved for appending text, since it contains the
|
|
// right arrow.
|
|
return end - 2
|
|
}
|
|
|
|
// shiftLeft shifts the visible range left so that it again contains the
|
|
// cursor.
|
|
// The visible range includes all fieldData indexes
|
|
// in range start <= idx < end.
|
|
func (fd *fieldData) shiftLeft(start, cells, curDataPos int) (int, int) {
|
|
var startIdx int
|
|
switch {
|
|
case curDataPos == 0 || cells < minForArrows:
|
|
startIdx = curDataPos
|
|
|
|
default:
|
|
startIdx = curDataPos - 1
|
|
}
|
|
forRunes := cells - 1
|
|
endIdx := fd.cellsAfter(forRunes, startIdx)
|
|
endIdx++ // Space for the cursor.
|
|
|
|
return startIdx, endIdx
|
|
}
|
|
|
|
// shiftRight shifts the visible range right so that it again contains the
|
|
// cursor.
|
|
// The visible range includes all fieldData indexes
|
|
// in range start <= idx < end.
|
|
func (fd *fieldData) shiftRight(start, cells, curDataPos int) (int, int) {
|
|
var endIdx int
|
|
switch dataLen := len(*fd); {
|
|
case curDataPos == dataLen:
|
|
// Cursor is in the empty space after the data.
|
|
// Print all runes until the end of data.
|
|
endIdx = dataLen
|
|
|
|
default:
|
|
// Cursor is within the data, print all runes including the one the
|
|
// cursor is on.
|
|
endIdx = curDataPos + 1
|
|
}
|
|
|
|
forRunes := cells - 1
|
|
startIdx := fd.cellsBefore(forRunes, endIdx)
|
|
|
|
// Invariant, if counting form the back ends in the middle of a full-width
|
|
// rune, cellsAfter doesn't include the full-width rune. This means that we
|
|
// might have recovered space for one half-with rune at the end if there is
|
|
// one.
|
|
endIdx = fd.cellsAfter(forRunes, startIdx)
|
|
endIdx++ // Space for the cursor.
|
|
|
|
return startIdx, endIdx
|
|
}
|
|
|
|
// lastVisible given an end index of visible range asserts whether the last
|
|
// rune in the data is visible.
|
|
// The visible range includes all fieldData indexes
|
|
// in range start <= idx < end.
|
|
func (fd *fieldData) lastVisible(end int) bool {
|
|
return end-1 >= len(*fd)
|
|
}
|
|
|
|
// runesIn returns all the runes in the visible range.
|
|
// The visible range includes all fieldData indexes
|
|
// in range start <= idx < end.
|
|
func (fd *fieldData) runesIn(start, end int) []rune {
|
|
var runes []rune
|
|
for i, r := range (*fd)[start:] {
|
|
if i+start > end-2 { // One last space is for the cursor after the text.
|
|
break
|
|
}
|
|
runes = append(runes, r)
|
|
}
|
|
return runes
|
|
}
|
|
|
|
// fitRunes starting from the firstRune index returns runes that take at most
|
|
// the specified number of cells. The last cell is reserved for a cursor
|
|
// position used for appending new runes.
|
|
// This might return smaller number of runes than the size of the range,
|
|
// depending on the width of the individual runes.
|
|
// Returns the text and the start and end positions within the data.
|
|
func (fd *fieldData) fitRunes(firstRune, curPos, cells int) (string, int, int) {
|
|
forRunes := cells - 1 // One cell reserved for the cursor when appending.
|
|
|
|
// Determine how many runes fit from the start.
|
|
start := firstRune
|
|
end := fd.cellsAfter(forRunes, start)
|
|
end++
|
|
|
|
if start > 0 && fd.lastVisible(end) {
|
|
// Start is in the middle, end is visible.
|
|
// Fit runes from the end.
|
|
end = len(*fd)
|
|
start = fd.cellsBefore(forRunes, end)
|
|
end++ // Space for the cursor within the visible range.
|
|
}
|
|
|
|
// The fitting of runes might have resulted in a visible range that no
|
|
// longer contains the cursor (it became shorter) or the cursor was outside
|
|
// to begin with (due to cursorLeft() or cursorRight() calls).
|
|
// Shift the range so the cursor is again inside.
|
|
if curPos < curMinIdx(start, cells) {
|
|
start, end = fd.shiftLeft(start, cells, curPos)
|
|
} else if curPos > curMaxIdx(start, end, cells, len(*fd)) {
|
|
start, end = fd.shiftRight(start, cells, curPos)
|
|
}
|
|
|
|
runes := fd.runesIn(start, end)
|
|
useArrows := cells >= minForArrows
|
|
var b strings.Builder
|
|
for i, r := range runes {
|
|
switch {
|
|
case useArrows && i == 0 && start > 0:
|
|
// Indicate that start is hidden by replacing the first visible
|
|
// rune with an arrow.
|
|
b.WriteRune('⇦')
|
|
if rw := runewidth.RuneWidth(r); rw == 2 {
|
|
// If the replaced rune was a full-width rune, place two arrows
|
|
// to keep the same space allocation as pre-calculated.
|
|
b.WriteRune('⇦')
|
|
}
|
|
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
|
|
if useArrows && !fd.lastVisible(end) {
|
|
// Indicate that end is hidden by placing an arrow at the end.
|
|
// THis has no impact on space allocation, since the last cell is
|
|
// always reserved for the cursor or the arrow.
|
|
b.WriteRune('⇨')
|
|
}
|
|
return b.String(), start, end
|
|
}
|
|
|
|
// fieldEditor maintains the cursor position and allows editing of the data in
|
|
// the text input field.
|
|
// This object isn't thread-safe.
|
|
type fieldEditor struct {
|
|
// data are the data currently present in the text input field.
|
|
data fieldData
|
|
|
|
// curDataPos is the current position of the cursor within the data.
|
|
// The cursor is allowed to go one cell beyond the data so appending is
|
|
// possible.
|
|
curDataPos int
|
|
|
|
// firstRune is the index of the first displayed rune in the text input
|
|
// field.
|
|
firstRune int
|
|
|
|
// width is the width of the text input field last time viewFor was called.
|
|
width int
|
|
}
|
|
|
|
// newFieldEditor returns a new fieldEditor instance.
|
|
func newFieldEditor() *fieldEditor {
|
|
return &fieldEditor{}
|
|
}
|
|
|
|
// minFieldWidth is the minimum supported width of the text input field.
|
|
const minFieldWidth = 4
|
|
|
|
// curCell returns the index of the cell the cursor is in within the text input field.
|
|
func (fe *fieldEditor) curCell(width int) int {
|
|
if width == 0 {
|
|
return 0
|
|
}
|
|
// The index of rune within the visible range the cursor is at.
|
|
runeNum := fe.curDataPos - fe.firstRune
|
|
|
|
cellNum := 0
|
|
rn := 0
|
|
for i, r := range fe.data {
|
|
if i < fe.firstRune {
|
|
continue
|
|
}
|
|
if rn >= runeNum {
|
|
break
|
|
}
|
|
rn++
|
|
cellNum += runewidth.RuneWidth(r)
|
|
}
|
|
return cellNum
|
|
}
|
|
|
|
// viewFor returns the currently visible data inside a text field with the
|
|
// specified width and the cursor position within the field.
|
|
func (fe *fieldEditor) viewFor(width int) (string, int, error) {
|
|
if min := minFieldWidth; width < min { // One for left arrow, two for one full-width rune and one for the cursor.
|
|
return "", -1, fmt.Errorf("width %d is too small, the minimum is %d", width, min)
|
|
}
|
|
runes, start, _ := fe.data.fitRunes(fe.firstRune, fe.curDataPos, width)
|
|
fe.firstRune = start
|
|
fe.width = width
|
|
return runes, fe.curCell(width), nil
|
|
}
|
|
|
|
// content returns the string content in the field editor.
|
|
func (fe *fieldEditor) content() string {
|
|
return string(fe.data)
|
|
}
|
|
|
|
// reset resets the content back to zero.
|
|
func (fe *fieldEditor) reset() {
|
|
*fe = *newFieldEditor()
|
|
}
|
|
|
|
// insert inserts the rune at the current position of the cursor.
|
|
func (fe *fieldEditor) insert(r rune) {
|
|
rw := runewidth.RuneWidth(r)
|
|
if rw == 0 {
|
|
// Don't insert invisible runes.
|
|
return
|
|
}
|
|
fe.data.insertAt(fe.curDataPos, r)
|
|
fe.curDataPos++
|
|
}
|
|
|
|
// delete deletes the rune at the current position of the cursor.
|
|
func (fe *fieldEditor) delete() {
|
|
if fe.curDataPos >= len(fe.data) {
|
|
// Cursor not on a rune, nothing to do.
|
|
return
|
|
}
|
|
fe.data.deleteAt(fe.curDataPos)
|
|
}
|
|
|
|
// deleteBefore deletes the rune that is immediately to the left of the cursor.
|
|
func (fe *fieldEditor) deleteBefore() {
|
|
if fe.curDataPos == 0 {
|
|
// Cursor at the beginning, nothing to do.
|
|
return
|
|
}
|
|
fe.cursorLeft()
|
|
fe.delete()
|
|
}
|
|
|
|
// cursorRight moves the cursor one position to the right.
|
|
func (fe *fieldEditor) cursorRight() {
|
|
fe.curDataPos, _ = numbers.MinMaxInts([]int{fe.curDataPos + 1, len(fe.data)})
|
|
}
|
|
|
|
// cursorLeft moves the cursor one position to the left.
|
|
func (fe *fieldEditor) cursorLeft() {
|
|
_, fe.curDataPos = numbers.MinMaxInts([]int{fe.curDataPos - 1, 0})
|
|
}
|
|
|
|
// cursorStart moves the cursor to the beginning of the data.
|
|
func (fe *fieldEditor) cursorStart() {
|
|
fe.curDataPos = 0
|
|
}
|
|
|
|
// cursorEnd moves the cursor to the end of the data.
|
|
func (fe *fieldEditor) cursorEnd() {
|
|
fe.curDataPos = len(fe.data)
|
|
}
|
|
|
|
// cursorRelCell sets the cursor onto the cell index within the visible
|
|
// area.
|
|
// If the index falls before the window, the cursor is moved onto the first
|
|
// visible position.
|
|
// If the pos falls after the end of data, the cursor is moved onto the last
|
|
// visible position.
|
|
func (fe *fieldEditor) cursorRelCell(cellIdx int) {
|
|
runes, start, end := fe.data.fitRunes(fe.firstRune, fe.curDataPos, fe.width)
|
|
minDataIdx := curMinIdx(start, fe.width)
|
|
maxDataIdx := curMaxIdx(start, end, fe.width, len(fe.data))
|
|
|
|
// Index of the rune we should move the cursor to relative to the visible
|
|
// range.
|
|
var relRuneIdx int
|
|
var cell int
|
|
for _, r := range runes {
|
|
cell += runewidth.RuneWidth(r)
|
|
if cell > cellIdx {
|
|
break
|
|
}
|
|
relRuneIdx++
|
|
}
|
|
|
|
// Absolute index of the rune we should move the cursor to.
|
|
dataIdx := fe.firstRune + relRuneIdx
|
|
switch {
|
|
case dataIdx < minDataIdx:
|
|
fe.curDataPos = minDataIdx
|
|
|
|
case dataIdx > maxDataIdx:
|
|
fe.curDataPos = maxDataIdx
|
|
|
|
default:
|
|
fe.curDataPos = dataIdx
|
|
}
|
|
}
|