Adds qline for editable input

This commit is contained in:
sloum 2021-04-05 22:30:00 -07:00
parent ffb90d269f
commit 5e5b8c4591
7 changed files with 333 additions and 39 deletions

View File

@ -42,9 +42,9 @@ When starting a new sheet from scratch the workbook will have one sheet with one
To move around the sheet, use vi-like keys:
- `h`, `j`, `k`, `l`: Left, Down, Up, Right
- `^`, `$`: Move to First or Last column in current row
- `g`, `G`: Move to First or Last row in current column
- `h`, `j`, `k`, `l`, or _arrow keys_: Left, Down, Up, Right
- `^`, `$`, `home`, `end`: Move to First or Last column in current row
- `g`, `G`, `PageUp`, `PageDown`: Move to First or Last row in current column
#### Data
@ -62,7 +62,7 @@ When referencing a cell in an expressions, such as `A2`, you may optionally "loc
#### Deleting Data
To delete the contents of a cell, that is - to revert it to an empty state, press `d`.
To delete the contents of a cell, that is - to revert it to an empty state, press `d`, `Backspace`, or `Delete`
#### Yank / Paste

View File

@ -63,17 +63,14 @@ func (c *cell) ToggleMod(mod int) {
}
}
func (c *cell) Edit(row, col int, text bool) {
line, err := GetLine(fmt.Sprintf("\033[2KUpdate %c%d: \033[?25h", col+64, row))
if err != nil {
panic(err)
}
func (c *cell) Edit(row, col, width int, text bool) {
line := GetEditLine(fmt.Sprintf("%c%d > ", col+64, row), c.rawVal, width)
if len(line) == 0 {
return
}
fmt.Print("\033[?25l")
line = strings.TrimSpace(line)
if text {
if text && (!strings.HasPrefix(line, "\"") && !strings.HasSuffix(line, "\"")){
line = fmt.Sprintf("\"%s\"", line)
}
updated := c.Update(line)

16
main.go
View File

@ -12,6 +12,7 @@ import (
"strings"
"syscall"
"tildegit.org/sloum/tally/termios"
"tildegit.org/sloum/tally/qline"
)
const (
@ -95,13 +96,16 @@ func runCommand(elems []string) {
}
}
func Getch() rune {
func Getch() (rune, error) {
reader := bufio.NewReader(os.Stdin)
char, _, err := reader.ReadRune()
if err != nil {
return 0
}
return char
return qline.ReadKey(reader)
}
func GetEditLine(prefix, content string, width int) string {
fmt.Print("\033[?25h")
s := qline.GetInput(prefix, content, width)
fmt.Print("\033[?25h")
return s
}
func GetLine(prefix string) (string, error) {

286
qline/qline.go Normal file
View File

@ -0,0 +1,286 @@
package qline
// qline is a line input library for terminals that utilize
// vt100 compatible escape sequences
//
// Copyright © 2021 Brian Evans
//
// 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.
import (
"bufio"
"fmt"
"os"
"strings"
)
const (
UpArrow rune = iota - 20
DownArrow
LeftArrow
RightArrow
Delete
Home
End
PageUp
PageDown
Escape rune = 27
NewLine rune = 10
CarriageReturn rune = 13
BackSpace rune = 127
)
var (
width int
)
type buffer struct {
buf []rune
cursor int
maxWidth int
offset int
cursorStart int
prompt string
}
func GetInput(prompt string, content string, cols int) string {
b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
fmt.Printf("\033[1m%s\033[22m", prompt)
var ch rune
var err error
reader := bufio.NewReader(os.Stdin)
b.seedContent(content)
b.printBuf()
for {
ch, err = ReadKey(reader)
if err != nil {
continue
}
if ch == CarriageReturn || ch == NewLine {
break
}
if isControl(ch) {
b.controlInput(ch)
} else {
b.addChar(ch, true)
}
}
return b.string()
}
func (lb buffer) string() string {
return string(lb.buf)
}
func (lb *buffer) deleteChar() {
if lb.cursor == len(lb.buf) {
return
} else if lb.cursor == len(lb.buf)-1 {
lb.buf = lb.buf[:len(lb.buf)-1]
} else {
lb.buf = append(lb.buf[:lb.cursor], lb.buf[lb.cursor+1:]...)
}
if lb.offset > 0 {
lb.offset--
}
}
func (lb *buffer) addChar(c rune, echo bool) {
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) {
return
}
if lb.cursor == len(lb.buf) {
lb.buf = append(lb.buf, c)
lb.cursor++
} else {
lb.buf = append(lb.buf[:lb.cursor+1], lb.buf[lb.cursor:]...)
lb.buf[lb.cursor] = c
lb.cursor++
}
if lb.cursor - lb.offset > lb.maxWidth {
lb.offset++
}
if echo {
lb.printBuf()
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (lb buffer) printBuf() {
out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)]
fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt))
}
func (lb *buffer) controlInput(c rune) {
switch c {
case Delete:
lb.deleteChar()
case BackSpace:
if lb.cursor > 0 {
lb.cursor--
lb.deleteChar()
}
case LeftArrow:
if lb.offset > 0 && lb.cursor - lb.offset == 0 {
lb.offset--
}
if lb.cursor > 0 {
lb.cursor--
}
case RightArrow:
// This is still mildly funky, but works enough
for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); {
lb.offset++
}
if lb.cursor < len(lb.buf) {
lb.cursor++
}
case Home:
lb.offset = 0
lb.cursor = 0
case End:
lb.cursor = len(lb.buf)
lb.offset = max(lb.cursor - lb.maxWidth, 0)
}
lb.printBuf()
}
func (lb *buffer) seedContent(s string) {
for _, r := range s {
lb.addChar(r, false)
}
}
func parseCursorPosition(esc string) (int, int, error) {
var row, col int
r := strings.NewReader(esc)
_, err := fmt.Fscanf(r, "\033[%d;%dR", &row, &col)
return row, col, err
}
func ReadKey(reader *bufio.Reader) (rune, error) {
char, _, err := reader.ReadRune()
if err != nil {
return 0, err
}
avail := reader.Buffered()
if char == Escape && avail > 0 {
var b strings.Builder
b.WriteRune(27)
for ; avail > 0; avail-- {
c, _, e := reader.ReadRune()
if e != nil {
break
}
b.WriteRune(c)
}
escSeq := b.String()
switch true {
case escSeq == "\033[A":
char = UpArrow
case escSeq == "\033[B":
char = DownArrow
case escSeq == "\033[C":
char = RightArrow
case escSeq == "\033[D":
char = LeftArrow
case escSeq == "\033[5~":
char = PageUp
case escSeq == "\033[6~":
char = PageDown
case isHomeKey(escSeq):
char = Home
case isEndKey(escSeq):
char = End
case isDeleteKey(escSeq):
char = Delete
case escSeq[len(escSeq)-1] == 'R':
// This is a request for cursor position
_, cols, err := parseCursorPosition(escSeq)
if err == nil {
err = fmt.Errorf("response")
}
return rune(cols), err
}
}
return char, nil
}
func isControl(c rune) bool {
switch c {
case UpArrow, DownArrow, RightArrow, LeftArrow, PageUp, PageDown, Home, End, Delete, BackSpace, CarriageReturn, NewLine, Escape:
return true
default:
return false
}
}
func isDeleteKey(seq string) bool {
switch seq {
case "\033[3~", "\033[P":
return true
default:
return false
}
}
func isHomeKey(seq string) bool {
switch seq {
case "\033[1~", "\033[7~", "\033[H", "\033OH":
return true
default:
return false
}
}
func isEndKey(seq string) bool {
switch seq {
case "\033[4~", "\033[8~", "\033[F", "\033OF":
return true
default:
return false
}
}

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"strings"
"tildegit.org/sloum/tally/qline"
)
type sheet struct {
@ -173,14 +174,14 @@ func (s *sheet) PasteRelative() {
func (s *sheet) moveSelection(dir rune, termRows int) {
switch dir {
case Left:
case Left, qline.LeftArrow:
if s.selection.col > 1 {
s.selection.col--
}
if s.selection.col <= s.colOff {
s.colOff--
}
case Right:
case Right, qline.RightArrow:
s.selection.col++
if s.selection.col > s.cols && s.selection.col < 27 {
s.AddCols(1)
@ -191,14 +192,14 @@ func (s *sheet) moveSelection(dir rune, termRows int) {
if s.selection.col > 26 {
s.selection.col = 26
}
case Up:
case Up, qline.UpArrow:
if s.selection.row > 1 {
s.selection.row--
}
if s.selection.row <= s.rowOff {
s.rowOff--
}
case Down:
case Down, qline.DownArrow:
s.selection.row++
if s.selection.row > s.rows {
s.AddRows(1)
@ -206,19 +207,19 @@ func (s *sheet) moveSelection(dir rune, termRows int) {
if s.selection.row > s.rowOff + termRows - 5 && s.rows > termRows - 5 {
s.rowOff++
}
case RowStart:
case RowStart, qline.Home:
s.selection.col = 1
s.colOff = 0
case ToTop:
case ToTop, qline.PageUp:
s.selection.row = 1
s.rowOff = 0
case RowEnd:
case RowEnd, qline.End:
s.selection.col = s.cols
s.colOff = s.cols - s.zoom
if s.colOff < 0 {
s.colOff = 0
}
case ToBottom:
case ToBottom, qline.PageDown:
s.selection.row = s.rows
s.rowOff = s.rows - termRows + 5
if s.rowOff < 0 {

14
tally.1
View File

@ -22,22 +22,26 @@ These commands work as a single keypress anytime \fBtally\fP is not taking in a
.TP
.B
h, j, k, l
Move the selection one cell left (h), down (j), up (k), or right (l).
Move the selection one cell left (h), down (j), up (k), or right (l). Arrow keys will also perform the same function.
.TP
.B
^
d, <Del>, <Backspace>
Revert a cell to its empty state/delete its content.
.TP
.B
^, <Home>
Move the selection to the first column of the current row.
.TP
.B
$
$, <End>
Move the selection to the last column of the current row.
.TP
.B
g
g, <PageUp>
Move the selection to the frist row of the current column.
.TP
.B
G
G, <PageDown>
Move the selection to the last row of the current column.
.TP
.B

View File

@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"tildegit.org/sloum/tally/qline"
"tildegit.org/sloum/tally/termios"
"time"
)
@ -75,17 +76,21 @@ func (w *workbook) Run() {
go w.PollForTermSize()
var input rune
var chErr error
for {
w.Draw()
input = Getch()
input, chErr = Getch()
if chErr != nil {
continue
}
switch input {
case Left, Right, Up, Down, ToTop, RowStart, ToBottom, RowEnd:
case Left, Right, Up, Down, ToTop, RowStart, ToBottom, RowEnd, qline.LeftArrow, qline.RightArrow, qline.DownArrow, qline.UpArrow, qline.Home, qline.End, qline.PageUp, qline.PageDown:
w.sheets[w.sheet].moveSelection(input, w.termRows)
case Quit:
if modified {
fmt.Print("There are unsaved changes. Quit anyway? [y/n]")
answer := Getch()
if answer == 'n' || answer == 'N' {
answer, err := Getch()
if err != nil || answer == 'n' || answer == 'N' {
continue
}
fmt.Print("\n")
@ -98,16 +103,13 @@ func (w *workbook) Run() {
if input == EditText {
quote = true
}
w.sheets[w.sheet].cells[w.sheets[w.sheet].selection.row-1][w.sheets[w.sheet].selection.col-1].Edit(w.sheets[w.sheet].selection.row, w.sheets[w.sheet].selection.col, quote)
w.sheets[w.sheet].cells[w.sheets[w.sheet].selection.row-1][w.sheets[w.sheet].selection.col-1].Edit(w.sheets[w.sheet].selection.row, w.sheets[w.sheet].selection.col, w.termCols, quote)
w.sheets[w.sheet].Recalculate()
case Del:
case Del, qline.Delete, qline.BackSpace:
w.sheets[w.sheet].cells[w.sheets[w.sheet].selection.row-1][w.sheets[w.sheet].selection.col-1] = cell{}
w.sheets[w.sheet].Recalculate()
case Com:
line, err := GetLine(":")
if err != nil {
panic(err)
}
line := GetEditLine(":", "", w.termCols)
runCommand(strings.Fields(line))
case ZoomIn:
w.sheets[w.sheet].ZoomIn()