Integrates qline to allow for editable input lines
This commit is contained in:
parent
77de8e95af
commit
d10e81065a
17
board.go
17
board.go
|
@ -122,12 +122,10 @@ func (b *Board) SetMessage(msg string, isError bool) {
|
|||
}
|
||||
|
||||
func (b *Board) CreateLane(name string) {
|
||||
var err error
|
||||
if name == "" {
|
||||
name, err = GetAndConfirmCommandLine("Lane Title: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
return
|
||||
name = GetEditableLine("Lane title: ", "")
|
||||
if name == "" {
|
||||
b.SetMessage("Lane creation canceled", true)
|
||||
}
|
||||
}
|
||||
b.Lanes = append(b.Lanes, MakeLane(name))
|
||||
|
@ -209,10 +207,7 @@ func (b Board) PrintLanes() string {
|
|||
}
|
||||
|
||||
func (b *Board) EnterCommand() {
|
||||
command, err := GetCommandLine(":")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
}
|
||||
command := GetEditableLine(":", "")
|
||||
if command == "" {
|
||||
return
|
||||
}
|
||||
|
@ -301,8 +296,8 @@ func (b *Board) Update(args []string) {
|
|||
var title string
|
||||
var err error
|
||||
if len(args) == 1 {
|
||||
title, err = GetAndConfirmCommandLine("New board title: ")
|
||||
if err != nil {
|
||||
title = GetEditableLine("New board title: ", "")
|
||||
if title == "" {
|
||||
b.SetMessage(err.Error(), true)
|
||||
break
|
||||
}
|
||||
|
|
16
lane.go
16
lane.go
|
@ -14,11 +14,10 @@ type Lane struct {
|
|||
}
|
||||
|
||||
func (l *Lane) CreateStory(name string, b *Board) {
|
||||
var err error
|
||||
if name == "" {
|
||||
name, err = GetAndConfirmCommandLine("Story Title: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
name = GetEditableLine("Story Title: ", "")
|
||||
if name == "" {
|
||||
b.SetMessage("Cancelled story creation", true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +95,7 @@ func (l *Lane) DeleteStory(b *Board) {
|
|||
b.SetMessage("There are no stories to delete in this lane", true)
|
||||
return
|
||||
}
|
||||
cont, err := GetConfirmation("Are you sure? Type 'yes' to delete: ")
|
||||
cont, err := GetConfirmation("Are you sure you want to delete this story? Type 'yes' to delete: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
return
|
||||
|
@ -113,6 +112,7 @@ func (l *Lane) DeleteStory(b *Board) {
|
|||
if l.Current > len(l.Stories)-1 {
|
||||
l.Current -= 1
|
||||
}
|
||||
b.StoryOpen = false
|
||||
b.SetMessage("Story deleted", false)
|
||||
}
|
||||
|
||||
|
@ -120,9 +120,9 @@ func (l *Lane) Update(args []string, b *Board) {
|
|||
location := strings.ToLower(args[0])
|
||||
switch location {
|
||||
case "title", "t", "name", "n":
|
||||
title, err := GetAndConfirmCommandLine("New lane title: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
title := GetEditableLine("New lane title: ", l.Title)
|
||||
if title == "" {
|
||||
b.SetMessage("Canceled lane title update", true)
|
||||
break
|
||||
}
|
||||
l.Title = title
|
||||
|
|
9
main.go
9
main.go
|
@ -31,6 +31,7 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
"tildegit.org/sloum/swim/termios"
|
||||
"tildegit.org/sloum/swim/qline"
|
||||
)
|
||||
|
||||
var board Board
|
||||
|
@ -72,6 +73,14 @@ func GetLine(prefix string) (string, error) {
|
|||
return text[:len(text)-1], nil
|
||||
}
|
||||
|
||||
func GetEditableLine(prompt, defaultText string) string {
|
||||
fmt.Printf("%s%s\033[2K\033[?25h", upAndLeft, style.Input)
|
||||
str := qline.GetInput(prompt, defaultText)
|
||||
fmt.Print("\033[?25l")
|
||||
fmt.Print(cursorEnd)
|
||||
return str
|
||||
}
|
||||
|
||||
func GetCommandLine(prefix string) (string, error) {
|
||||
fmt.Printf("%s%s\033[2K", upAndLeft, style.Input)
|
||||
line, err := GetLine(prefix)
|
||||
|
|
|
@ -0,0 +1,293 @@
|
|||
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"
|
||||
|
||||
"tildegit.org/sloum/swim/termios"
|
||||
)
|
||||
|
||||
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) string {
|
||||
// termios.SetCharMode()
|
||||
cols, _ := termios.GetWindowSize()
|
||||
b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
|
||||
|
||||
fmt.Print(prompt)
|
||||
fmt.Print("\033[6n")
|
||||
|
||||
var ch rune
|
||||
var err error
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
ch, err = readKey(reader)
|
||||
if err != nil && err.Error() == "response" {
|
||||
b.cursorStart = int(ch)
|
||||
b.maxWidth = cols - b.cursorStart
|
||||
b.seedContent(content)
|
||||
b.printBuf()
|
||||
} else 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
|
||||
}
|
||||
}
|
||||
|
57
story.go
57
story.go
|
@ -68,11 +68,10 @@ func (s *Story) View(b *Board) {
|
|||
}
|
||||
|
||||
func (s *Story) AddComment(comment string, b *Board) {
|
||||
var err error
|
||||
if comment == "" {
|
||||
comment, err = GetAndConfirmCommandLine("Comment: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
comment = GetEditableLine("Comment: ", "")
|
||||
if comment == "" {
|
||||
b.SetMessage("Comment canceled", true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -185,17 +184,15 @@ func (s *Story) Scroll(dir rune, b *Board) {
|
|||
func (s *Story) Update(args []string, b *Board) {
|
||||
location := strings.ToLower(args[0])
|
||||
var stringVal string
|
||||
var err error
|
||||
if len(args) > 1 {
|
||||
stringVal = strings.Join(args[1:], " ")
|
||||
}
|
||||
switch location {
|
||||
case "title":
|
||||
if stringVal == "" {
|
||||
stringVal, err = GetAndConfirmCommandLine("New story title: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
return
|
||||
stringVal = GetEditableLine("Story title: ", s.Title)
|
||||
if stringVal == "" {
|
||||
b.SetMessage("Canceled story title update", true)
|
||||
}
|
||||
}
|
||||
s.Title = stringVal
|
||||
|
@ -204,10 +201,9 @@ func (s *Story) Update(args []string, b *Board) {
|
|||
s.BuildStorySlice(b.width)
|
||||
case "description", "d", "desc", "body", "b":
|
||||
if stringVal == "" {
|
||||
stringVal, err = GetAndConfirmCommandLine("New story description: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
return
|
||||
stringVal = GetEditableLine("Story description: ", s.Body)
|
||||
if stringVal == "" {
|
||||
b.SetMessage("Canceled description update", true)
|
||||
}
|
||||
}
|
||||
s.Body = stringVal
|
||||
|
@ -216,9 +212,13 @@ func (s *Story) Update(args []string, b *Board) {
|
|||
s.BuildStorySlice(b.width)
|
||||
case "points", "sp", "pts", "p":
|
||||
if len(args) != 2 {
|
||||
ps, err := GetCommandLine("Set story points: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
current := strconv.Itoa(s.Points)
|
||||
if s.Points < 1 {
|
||||
current = "-"
|
||||
}
|
||||
ps := GetEditableLine("Set story points: ", current)
|
||||
if ps == "" {
|
||||
b.SetMessage("Canceled setting points", true)
|
||||
return
|
||||
}
|
||||
args = append(args, ps)
|
||||
|
@ -235,9 +235,9 @@ func (s *Story) Update(args []string, b *Board) {
|
|||
case "user", "u":
|
||||
var users []string
|
||||
if len(args) == 1 {
|
||||
u, err := GetAndConfirmCommandLine("User(s) to add: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
u := GetEditableLine("User(s) to toggle: ", "")
|
||||
if u == "" {
|
||||
b.SetMessage("Canceled user toggle", true)
|
||||
return
|
||||
}
|
||||
users = strings.Fields(u)
|
||||
|
@ -247,9 +247,9 @@ func (s *Story) Update(args []string, b *Board) {
|
|||
s.AddRemoveUser(users, b)
|
||||
case "toggle", "t":
|
||||
if len(args) < 2 {
|
||||
n, err := GetCommandLine("Task # to toggle: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
n := GetEditableLine("Task # to toggle: ", "")
|
||||
if n == "" {
|
||||
b.SetMessage("Canceled task toggle", true)
|
||||
return
|
||||
}
|
||||
args = append(args, n)
|
||||
|
@ -291,13 +291,8 @@ func (s *Story) AddRemoveUser(users []string, b *Board) {
|
|||
}
|
||||
|
||||
func (s *Story) AddTask(body string, b *Board) {
|
||||
var err error
|
||||
if body == "" {
|
||||
body, err = GetAndConfirmCommandLine("New task: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
return
|
||||
}
|
||||
body = GetEditableLine("New task: ", "")
|
||||
}
|
||||
s.Tasks = append(s.Tasks, Task{body, false})
|
||||
s.Updated = time.Now()
|
||||
|
@ -308,9 +303,9 @@ func (s *Story) AddTask(body string, b *Board) {
|
|||
func (s *Story) DeleteTask(id string, b *Board) {
|
||||
var err error
|
||||
if id == "" {
|
||||
id, err = GetCommandLine("Task # to delete: ")
|
||||
if err != nil {
|
||||
b.SetMessage(err.Error(), true)
|
||||
id = GetEditableLine("Task # to delete: ", "")
|
||||
if id == "" {
|
||||
b.SetMessage("Canceled task deletion", true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue