Adds qline for editable input
This commit is contained in:
parent
ffb90d269f
commit
5e5b8c4591
|
@ -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
|
||||
|
||||
|
|
9
cell.go
9
cell.go
|
@ -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
16
main.go
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
17
sheet.go
17
sheet.go
|
@ -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
14
tally.1
|
@ -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
|
||||
|
|
22
workbook.go
22
workbook.go
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue