From d10e81065a07411d52052b0880d058926e9f672a Mon Sep 17 00:00:00 2001 From: sloum Date: Sat, 3 Apr 2021 22:44:23 -0700 Subject: [PATCH] Integrates qline to allow for editable input lines --- board.go | 17 +-- lane.go | 16 +-- main.go | 9 ++ qline/qline.go | 293 +++++++++++++++++++++++++++++++++++++++++++++++++ story.go | 57 +++++----- swim.swim | 2 +- 6 files changed, 343 insertions(+), 51 deletions(-) create mode 100644 qline/qline.go diff --git a/board.go b/board.go index a114306..32c85e1 100644 --- a/board.go +++ b/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 } diff --git a/lane.go b/lane.go index 8130f26..d681d5b 100644 --- a/lane.go +++ b/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 diff --git a/main.go b/main.go index bb9ccd1..169f742 100644 --- a/main.go +++ b/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) diff --git a/qline/qline.go b/qline/qline.go new file mode 100644 index 0000000..6eed600 --- /dev/null +++ b/qline/qline.go @@ -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 + } +} + diff --git a/story.go b/story.go index 874d820..7765133 100644 --- a/story.go +++ b/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 } } diff --git a/swim.swim b/swim.swim index abe0b07..28d4f9e 100644 --- a/swim.swim +++ b/swim.swim @@ -1 +1 @@ -{"BoardTitle":"Working on swim","Lanes":[{"LaneTitle":"Backlog","Stories":[{"StoryTitle":"Add Web/Gemini Docs","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T12:54:20.343386682-07:00","StoryUpdated":"2021-03-26T14:05:02.15458881-07:00","StoryPoints":1}],"CurrentStory":0},{"LaneTitle":"Active","Stories":[{"StoryTitle":"Rework file writing","StoryBody":"Fixes an issue with mangled writes to json files","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Make sure existing permissions carry over and new files get a sane default","TaskComplete":true},{"TaskBody":"Make sure files get truncated on open for write","TaskComplete":true},{"TaskBody":"Make a backup file while writing, the delete it if the file write worked","TaskComplete":false}],"StoryComments":[],"StoryCreated":"2021-03-25T14:32:46.780453566-07:00","StoryUpdated":"2021-03-26T14:05:39.464143008-07:00","StoryPoints":1}],"CurrentStory":0},{"LaneTitle":"Completed","Stories":[{"StoryTitle":"Add quick mode for toggling tasks","StoryBody":"Make this work like Bombadillo's quick link navigation. Pressing '1' would toggle on or off the first task in a tasklist. This only functions on the story view and is not a part of the overall listener as provided by *Board. Make sure '0' functions as \"10\".","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Add runes to story listener loop","TaskComplete":true},{"TaskBody":"Call update toggle on story when pressed","TaskComplete":true},{"TaskBody":"If 0 is entered, make sure the string \"10\" is sent","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T12:58:09.871463382-07:00","StoryUpdated":"2021-03-25T14:14:15.97075787-07:00","StoryPoints":1},{"StoryTitle":"Add redraw on resume from job control","StoryBody":"Make signals behave in a clean way and provide expected behavior.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Copy over signal handler from tally","TaskComplete":true},{"TaskBody":"Update the SIGCONT handler to call board.Draw()","TaskComplete":true},{"TaskBody":"Make sure other signals are handled properly","TaskComplete":true}],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This is mostly done. Do a cursory look at the other signals and then start handling the terminal resize story.","CommentCreated":"2021-03-25T12:49:58.234148077-07:00"}],"StoryCreated":"2021-03-25T09:21:22.815587777-07:00","StoryUpdated":"2021-03-25T15:39:22.781661841-07:00","StoryPoints":1},{"StoryTitle":"Add resize/draw on terminal resize","StoryBody":"When the terminal is resized the main *Board should have its width and height updated","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Create listener for resize","TaskComplete":true},{"TaskBody":"Update *Board with new dimensions on resize","TaskComplete":true},{"TaskBody":"Redraw screen on resize","TaskComplete":true},{"TaskBody":"Make sure if a story is being viewed that the redraw is a story redraw not a board redraw","TaskComplete":true}],"StoryComments":[{"CommentUser":"sloum","CommentBody":"Some of this code should already be present in Bombadillo. It can be borrowed from there.","CommentCreated":"2021-03-25T09:23:39.245527096-07:00"},{"CommentUser":"sloum","CommentBody":"Individuak stories should automatically reflow their content when they are viewed. With the exception of a currently open story. That could create problems, so maybe always reflow that story.","CommentCreated":"2021-03-25T12:51:42.948281369-07:00"}],"StoryCreated":"2021-03-25T09:21:08.303781384-07:00","StoryUpdated":"2021-03-25T15:01:37.428712763-07:00","StoryPoints":1},{"StoryTitle":"Make zoom adjust the lane offset","StoryBody":"When you zoom in it is possible for the currently selected column to be off the screen. The lane offset should be adjusted to account for this and keep the selected lane on the screen.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Update zoom in to adjust offset","TaskComplete":true},{"TaskBody":"Update zoom out to adjust offset","TaskComplete":true},{"TaskBody":"Make sure offset cannot be \u003c 0","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T14:40:01.870503824-07:00","StoryUpdated":"2021-03-25T14:49:18.990485621-07:00","StoryPoints":1},{"StoryTitle":"Add story points to card lane display","StoryBody":"Currently, story cards only display the title. It would be nice to also display points.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-25T14:52:00.470752912-07:00","StoryUpdated":"2021-03-25T14:53:24.025863477-07:00","StoryPoints":1},{"StoryTitle":"Terminal color detection","StoryBody":"Terminal color avialability is tricky. Set up some kind of best guess scenario, falling back to 8 bit when necessary.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Check for $COLORTERM var and set to true if its value is 24bit || truecolor","TaskComplete":true},{"TaskBody":"Check the $TERM var for 256 anywhere in it. If found set to 256","TaskComplete":true},{"TaskBody":"When in doubt fall back to 8bit","TaskComplete":true},{"TaskBody":"Have this auto-set on run","TaskComplete":true},{"TaskBody":"Create a flag to manually override to any of the three values","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T22:46:50.10507806-07:00","StoryUpdated":"2021-03-25T22:49:34.18340222-07:00","StoryPoints":1},{"StoryTitle":"Add -color \"none\" mode","StoryBody":"Add a mode that does not do any color additions. This will result in a 2bit color mode (fg/bg and color on/off for each as set by the terminal).","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This was set up using the flag: -color none || -color off","CommentCreated":"2021-03-26T09:20:51.555905306-07:00"}],"StoryCreated":"2021-03-26T09:18:22.711096199-07:00","StoryUpdated":"2021-03-26T09:25:04.391052303-07:00","StoryPoints":1},{"StoryTitle":"Add 2nd Row to cards w/ pts and users","StoryBody":"Should make the board a bit more readable and useful for working in groups","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Add second row to cards","TaskComplete":true},{"TaskBody":"Remove story points from first row and add it to second","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-26T13:38:33.776434147-07:00","StoryUpdated":"2021-03-26T14:06:33.086132452-07:00","StoryPoints":1},{"StoryTitle":"Have selected story remain in view when term resized","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This ran into some trouble. I didnt want to spend a lot of time on it so for now I have it resetting the selected story to the first story in the lane on resize. This is not great, but will function for the moment to not cause weird breakages.","CommentCreated":"2021-03-26T14:18:29.872684519-07:00"}],"StoryCreated":"2021-03-26T09:22:00.450326411-07:00","StoryUpdated":"2021-03-26T14:05:22.684602202-07:00","StoryPoints":1},{"StoryTitle":"Fix text clearing when selecting 'n' at a verification prompt","StoryBody":"Currently if you select 'n', intending to rewrite your input, ghosting of the previous messaging is left.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T09:16:42.336417437-07:00","StoryUpdated":"2021-03-26T14:05:34.014292773-07:00","StoryPoints":1},{"StoryTitle":"Add 'g' and 'G' handling for lane view","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T09:22:55.368822582-07:00","StoryUpdated":"2021-03-26T14:05:08.362154894-07:00","StoryPoints":1},{"StoryTitle":"Write Full Readme","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Include command descriptions","TaskComplete":true},{"TaskBody":"Include hot key listing","TaskComplete":true},{"TaskBody":"Include info about files","TaskComplete":true},{"TaskBody":"Include info about command line flags","TaskComplete":true},{"TaskBody":"Include info about signals and resizing","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-26T12:53:59.797558294-07:00","StoryUpdated":"2021-03-27T14:23:36.454525168-07:00","StoryPoints":1},{"StoryTitle":"Create Man Page","StoryBody":"Pretty self explanatory.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T12:54:08.267784673-07:00","StoryUpdated":"2021-03-27T14:22:22.680263403-07:00","StoryPoints":1}],"CurrentStory":12}],"CurrentLane":1,"Zoom":3,"StoryOpen":false} \ No newline at end of file +{"BoardTitle":"Working on swim","Lanes":[{"LaneTitle":"Backlog","Stories":[],"CurrentStory":-1},{"LaneTitle":"Active","Stories":[{"StoryTitle":"Rework file writing","StoryBody":"Fixes an issue with mangled writes to json files","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Make sure existing permissions carry over and new files get a sane default","TaskComplete":true},{"TaskBody":"Make sure files get truncated on open for write","TaskComplete":true},{"TaskBody":"Make a backup file while writing, the delete it if the file write worked","TaskComplete":false}],"StoryComments":[],"StoryCreated":"2021-03-25T14:32:46.780453566-07:00","StoryUpdated":"2021-03-26T14:05:39.464143008-07:00","StoryPoints":1},{"StoryTitle":"Add Web/Gemini Docs","StoryBody":"This has begun, but is mostly just a skeleton.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T12:54:20.343386682-07:00","StoryUpdated":"2021-04-03T22:38:47.85111414-07:00","StoryPoints":1},{"StoryTitle":"Integrate qline","StoryBody":"I wrote a small package to make line input editable. If it works broadly I may use it in all of my applications. swim is a test run for it.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-04-03T22:39:12.715180609-07:00","StoryUpdated":"2021-04-03T22:40:19.49247739-07:00","StoryPoints":1}],"CurrentStory":2},{"LaneTitle":"Completed","Stories":[{"StoryTitle":"Add quick mode for toggling tasks","StoryBody":"Make this work like Bombadillo's quick link navigation. Pressing '1' would toggle on or off the first task in a tasklist. This only functions on the story view and is not a part of the overall listener as provided by *Board. Make sure '0' functions as \"10\".","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Add runes to story listener loop","TaskComplete":true},{"TaskBody":"Call update toggle on story when pressed","TaskComplete":true},{"TaskBody":"If 0 is entered, make sure the string \"10\" is sent","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T12:58:09.871463382-07:00","StoryUpdated":"2021-03-25T14:14:15.97075787-07:00","StoryPoints":1},{"StoryTitle":"Add redraw on resume from job control","StoryBody":"Make signals behave in a clean way and provide expected behavior.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Copy over signal handler from tally","TaskComplete":true},{"TaskBody":"Update the SIGCONT handler to call board.Draw()","TaskComplete":true},{"TaskBody":"Make sure other signals are handled properly","TaskComplete":true}],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This is mostly done. Do a cursory look at the other signals and then start handling the terminal resize story.","CommentCreated":"2021-03-25T12:49:58.234148077-07:00"}],"StoryCreated":"2021-03-25T09:21:22.815587777-07:00","StoryUpdated":"2021-03-25T15:39:22.781661841-07:00","StoryPoints":1},{"StoryTitle":"Add resize/draw on terminal resize","StoryBody":"When the terminal is resized the main *Board should have its width and height updated","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Create listener for resize","TaskComplete":true},{"TaskBody":"Update *Board with new dimensions on resize","TaskComplete":true},{"TaskBody":"Redraw screen on resize","TaskComplete":true},{"TaskBody":"Make sure if a story is being viewed that the redraw is a story redraw not a board redraw","TaskComplete":true}],"StoryComments":[{"CommentUser":"sloum","CommentBody":"Some of this code should already be present in Bombadillo. It can be borrowed from there.","CommentCreated":"2021-03-25T09:23:39.245527096-07:00"},{"CommentUser":"sloum","CommentBody":"Individuak stories should automatically reflow their content when they are viewed. With the exception of a currently open story. That could create problems, so maybe always reflow that story.","CommentCreated":"2021-03-25T12:51:42.948281369-07:00"}],"StoryCreated":"2021-03-25T09:21:08.303781384-07:00","StoryUpdated":"2021-03-25T15:01:37.428712763-07:00","StoryPoints":1},{"StoryTitle":"Make zoom adjust the lane offset","StoryBody":"When you zoom in it is possible for the currently selected column to be off the screen. The lane offset should be adjusted to account for this and keep the selected lane on the screen.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Update zoom in to adjust offset","TaskComplete":true},{"TaskBody":"Update zoom out to adjust offset","TaskComplete":true},{"TaskBody":"Make sure offset cannot be \u003c 0","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T14:40:01.870503824-07:00","StoryUpdated":"2021-03-25T14:49:18.990485621-07:00","StoryPoints":1},{"StoryTitle":"Add story points to card lane display","StoryBody":"Currently, story cards only display the title. It would be nice to also display points.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-25T14:52:00.470752912-07:00","StoryUpdated":"2021-03-25T14:53:24.025863477-07:00","StoryPoints":1},{"StoryTitle":"Terminal color detection","StoryBody":"Terminal color avialability is tricky. Set up some kind of best guess scenario, falling back to 8 bit when necessary.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Check for $COLORTERM var and set to true if its value is 24bit || truecolor","TaskComplete":true},{"TaskBody":"Check the $TERM var for 256 anywhere in it. If found set to 256","TaskComplete":true},{"TaskBody":"When in doubt fall back to 8bit","TaskComplete":true},{"TaskBody":"Have this auto-set on run","TaskComplete":true},{"TaskBody":"Create a flag to manually override to any of the three values","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-25T22:46:50.10507806-07:00","StoryUpdated":"2021-03-25T22:49:34.18340222-07:00","StoryPoints":1},{"StoryTitle":"Add -color \"none\" mode","StoryBody":"Add a mode that does not do any color additions. This will result in a 2bit color mode (fg/bg and color on/off for each as set by the terminal).","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This was set up using the flag: -color none || -color off","CommentCreated":"2021-03-26T09:20:51.555905306-07:00"}],"StoryCreated":"2021-03-26T09:18:22.711096199-07:00","StoryUpdated":"2021-03-26T09:25:04.391052303-07:00","StoryPoints":1},{"StoryTitle":"Add 2nd Row to cards w/ pts and users","StoryBody":"Should make the board a bit more readable and useful for working in groups","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Add second row to cards","TaskComplete":true},{"TaskBody":"Remove story points from first row and add it to second","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-26T13:38:33.776434147-07:00","StoryUpdated":"2021-03-26T14:06:33.086132452-07:00","StoryPoints":1},{"StoryTitle":"Have selected story remain in view when term resized","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[{"CommentUser":"sloum","CommentBody":"This ran into some trouble. I didnt want to spend a lot of time on it so for now I have it resetting the selected story to the first story in the lane on resize. This is not great, but will function for the moment to not cause weird breakages.","CommentCreated":"2021-03-26T14:18:29.872684519-07:00"}],"StoryCreated":"2021-03-26T09:22:00.450326411-07:00","StoryUpdated":"2021-03-26T14:05:22.684602202-07:00","StoryPoints":1},{"StoryTitle":"Fix text clearing when selecting 'n' at a verification prompt","StoryBody":"Currently if you select 'n', intending to rewrite your input, ghosting of the previous messaging is left.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T09:16:42.336417437-07:00","StoryUpdated":"2021-03-26T14:05:34.014292773-07:00","StoryPoints":1},{"StoryTitle":"Add 'g' and 'G' handling for lane view","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T09:22:55.368822582-07:00","StoryUpdated":"2021-03-26T14:05:08.362154894-07:00","StoryPoints":1},{"StoryTitle":"Write Full Readme","StoryBody":"","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Include command descriptions","TaskComplete":true},{"TaskBody":"Include hot key listing","TaskComplete":true},{"TaskBody":"Include info about files","TaskComplete":true},{"TaskBody":"Include info about command line flags","TaskComplete":true},{"TaskBody":"Include info about signals and resizing","TaskComplete":true}],"StoryComments":[],"StoryCreated":"2021-03-26T12:53:59.797558294-07:00","StoryUpdated":"2021-03-27T14:23:36.454525168-07:00","StoryPoints":1},{"StoryTitle":"Create Man Page","StoryBody":"Pretty self explanatory.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-03-26T12:54:08.267784673-07:00","StoryUpdated":"2021-03-27T14:22:22.680263403-07:00","StoryPoints":1}],"CurrentStory":1}],"CurrentLane":1,"Zoom":3,"StoryOpen":false} \ No newline at end of file