648 lines
17 KiB
Go
648 lines
17 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"reflect"
|
||
"regexp"
|
||
"strings"
|
||
"tildegit.org/sloum/swim/termios"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
swimLogo string = "\033[7m swim ▟\033[27m "
|
||
cursorHome string = "\033[0;0H"
|
||
styleOff string = "\033[0m"
|
||
upAndLeft string = "\033[1A\033[500D"
|
||
cursorEnd string = "\033[500;500H"
|
||
)
|
||
|
||
type Board struct {
|
||
Title string `json:"BoardTitle"`
|
||
Lanes []Lane `json:"Lanes"`
|
||
Current int `json:"CurrentLane"` // Index of current lane
|
||
laneOff int
|
||
message string
|
||
msgErr bool
|
||
width int
|
||
height int
|
||
Zoom int `json:"Zoom"`
|
||
StoryOpen bool
|
||
}
|
||
|
||
func (b *Board) PollForTermSize() {
|
||
for {
|
||
var w, h = termios.GetWindowSize()
|
||
if h != b.height || w != b.width {
|
||
b.height = h
|
||
b.width = w
|
||
if b.StoryOpen {
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].offset = 0
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Draw(b)
|
||
} else {
|
||
for l := range b.Lanes {
|
||
if len(b.Lanes[l].Stories) == 0 {
|
||
continue
|
||
}
|
||
b.Lanes[l].Current = 0
|
||
b.Lanes[l].storyOff = 0
|
||
}
|
||
b.Draw()
|
||
}
|
||
}
|
||
|
||
time.Sleep(500 * time.Millisecond)
|
||
}
|
||
}
|
||
|
||
func (b *Board) Run() {
|
||
defer termios.Restore()
|
||
termios.SetCharMode()
|
||
fmt.Print("\033[?25l")
|
||
go b.PollForTermSize()
|
||
|
||
var ch rune
|
||
for {
|
||
b.Draw()
|
||
ch = Getch()
|
||
|
||
switch ch {
|
||
case 'Q':
|
||
Quit()
|
||
case 'N':
|
||
b.CreateLane("")
|
||
case 'n':
|
||
b.CreateStory("")
|
||
case '\n':
|
||
b.StoryOpen = true
|
||
b.ViewStory()
|
||
b.StoryOpen = false
|
||
case 'g':
|
||
if len(b.Lanes[b.Current].Stories) > 0 {
|
||
b.Lanes[b.Current].Current = 0
|
||
b.Lanes[b.Current].storyOff = 0
|
||
}
|
||
case 'G':
|
||
if len(b.Lanes[b.Current].Stories) > 0 {
|
||
b.Lanes[b.Current].Current = len(b.Lanes[b.Current].Stories)-1
|
||
if b.Lanes[b.Current].Current * 3 + 1 > b.height-5 {
|
||
off := b.Lanes[b.Current].Current - 3 % (b.height-5)
|
||
if off+1 > b.height-5 {
|
||
b.Lanes[b.Current].storyOff = off
|
||
} else {
|
||
b.Lanes[b.Current].storyOff = off+1
|
||
}
|
||
}
|
||
}
|
||
case 'h', 'j', 'k', 'l', 'H', 'L', 'K', 'J':
|
||
b.ClearMessage()
|
||
b.Move(ch)
|
||
case ':':
|
||
b.EnterCommand()
|
||
case '+':
|
||
b.ZoomIn()
|
||
case '-':
|
||
b.ZoomOut()
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("There is no action bound to '%c'", ch), true)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (b *Board) ClearMessage() {
|
||
b.message = ""
|
||
b.msgErr = false
|
||
}
|
||
|
||
func (b *Board) SetMessage(msg string, isError bool) {
|
||
b.message = msg
|
||
b.msgErr = isError
|
||
}
|
||
|
||
func (b *Board) CreateLane(name string) {
|
||
if name == "" {
|
||
name = GetEditableLine("Lane title: ", "")
|
||
if name == "" {
|
||
b.SetMessage("Lane creation canceled", true)
|
||
}
|
||
}
|
||
b.Lanes = append(b.Lanes, MakeLane(name))
|
||
if b.Current < 0 {
|
||
b.Current = 0
|
||
}
|
||
b.SetMessage("Lane created", false)
|
||
}
|
||
|
||
func (b *Board) CreateStory(name string) {
|
||
if b.Current < 0 {
|
||
b.SetMessage("You must create a lane first", true)
|
||
return
|
||
}
|
||
b.Lanes[b.Current].CreateStory(name, b)
|
||
}
|
||
|
||
func (b Board) PrintHeader() string {
|
||
return fmt.Sprintf("%s%s%-*.*s%s\n", style.Header, swimLogo, b.width-12, b.width-12, b.Title, styleOff)
|
||
}
|
||
|
||
func (b Board) PrintInputArea() string {
|
||
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.width, b.width, " ", styleOff)
|
||
}
|
||
|
||
func (b Board) GetLaneSlices(width int) [][]string {
|
||
laneText := make([][]string, b.Zoom)
|
||
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
||
var s []string
|
||
if i < len(b.Lanes) {
|
||
s = b.Lanes[i].StringSlice(width, i == b.Current)
|
||
} else {
|
||
s = make([]string, 0)
|
||
}
|
||
laneText[i-b.laneOff] = s
|
||
}
|
||
return laneText
|
||
}
|
||
|
||
func (b Board) LaneHeaderRow(width, pad int) string {
|
||
var out strings.Builder
|
||
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
||
if i < len(b.Lanes) {
|
||
out.WriteString(b.Lanes[i].Header(width, i == b.Current))
|
||
} else {
|
||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||
}
|
||
}
|
||
if pad > 0 {
|
||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, pad, pad, " ", styleOff))
|
||
}
|
||
out.WriteRune('\n')
|
||
return out.String()
|
||
}
|
||
|
||
func (b Board) PrintLanes() string {
|
||
var out strings.Builder
|
||
laneWidth := b.width / b.Zoom
|
||
laneSlices := b.GetLaneSlices(laneWidth)
|
||
pad := b.width - laneWidth * b.Zoom
|
||
|
||
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
|
||
|
||
for row := 0; row < b.height - 4; row++ {
|
||
for _, l := range laneSlices {
|
||
if row < len(l) {
|
||
out.WriteString(l[row])
|
||
} else {
|
||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, laneWidth, laneWidth, " ", styleOff))
|
||
}
|
||
}
|
||
if pad > 0 {
|
||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, pad, pad, " ", styleOff))
|
||
}
|
||
out.WriteRune('\n')
|
||
}
|
||
out.WriteString(styleOff)
|
||
return out.String()
|
||
}
|
||
|
||
func (b *Board) EnterCommand() {
|
||
command := GetEditableLine(":", "")
|
||
if command == "" {
|
||
return
|
||
}
|
||
f := strings.Fields(command)
|
||
mainCom := strings.ToLower(f[0])
|
||
switch mainCom {
|
||
case "write", "wq", "w":
|
||
b.Write(f)
|
||
if mainCom == "wq" {
|
||
Quit()
|
||
}
|
||
case "regex", "re", "regexp":
|
||
if len(f) < 4 {
|
||
b.SetMessage("Not enough arguments: regex [target] [location] [/pattern/replacement/]", true)
|
||
return
|
||
}
|
||
b.Regex(f[1:])
|
||
case "q", "quit":
|
||
Quit()
|
||
case "c", "create":
|
||
if len(f) >= 2 {
|
||
target := strings.ToLower(f[1])
|
||
name := ""
|
||
if len(f) > 2 {
|
||
name = strings.Join(f[2:], " ")
|
||
}
|
||
switch target {
|
||
case "lane", "l", "la", "lan":
|
||
b.CreateLane(name)
|
||
case "story", "s", "st", "sto", "stor":
|
||
b.CreateStory(name)
|
||
case "task", "tas", "ta", "t":
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].AddTask(name, b)
|
||
case "comment", "com", "c":
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].AddComment(name, b)
|
||
}
|
||
}
|
||
case "d", "del", "delete":
|
||
if len(f) > 1 {
|
||
target := strings.ToLower(f[1])
|
||
switch target {
|
||
case "lane", "l", "la", "lan":
|
||
b.DeleteLane()
|
||
case "story", "s", "st", "sto", "stor":
|
||
b.Lanes[b.Current].DeleteStory(b)
|
||
case "t", "ta", "tas", "task":
|
||
val := ""
|
||
if len(f) > 2 {
|
||
val = f[2]
|
||
}
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].DeleteTask(val, b)
|
||
// TODO
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
|
||
}
|
||
} else {
|
||
b.SetMessage("More info needed: 'update [target] [location]'", true)
|
||
}
|
||
case "set", "s":
|
||
if len(f) > 2 {
|
||
target := strings.ToLower(f[1])
|
||
switch target {
|
||
case "board", "b", "bo", "boa", "boar":
|
||
b.Update(f[2:])
|
||
case "lane", "l", "la", "lan":
|
||
b.Lanes[b.Current].Update(f[2:], b)
|
||
case "story", "s", "st", "sto", "stor":
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Update(f[2:], b)
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
|
||
}
|
||
} else {
|
||
b.SetMessage("More info needed: 'set [target] [location] [[value]]'", true)
|
||
}
|
||
case "user", "toggle", "t", "u":
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Update(f, b)
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Unknown command %q", f[0]), true)
|
||
}
|
||
}
|
||
|
||
func (b *Board) Update(args []string) {
|
||
location := strings.ToLower(args[0])
|
||
switch location {
|
||
case "title", "t", "name", "n":
|
||
var title string
|
||
var err error
|
||
if len(args) == 1 {
|
||
title = GetEditableLine("New board title: ", "")
|
||
if title == "" {
|
||
b.SetMessage(err.Error(), true)
|
||
break
|
||
}
|
||
} else {
|
||
title = strings.Join(args[1:], " ")
|
||
}
|
||
b.Title = title
|
||
b.SetMessage("Board title updated", false)
|
||
}
|
||
|
||
}
|
||
|
||
func (b Board) PrintMessage() string {
|
||
var out strings.Builder
|
||
if b.msgErr {
|
||
out.WriteString(style.MessageErr)
|
||
} else {
|
||
out.WriteString(style.Message)
|
||
}
|
||
msg := b.message
|
||
if len(msg) > b.width {
|
||
msg = msg[:b.width] + "…"
|
||
}
|
||
out.WriteString(fmt.Sprintf("%-*s%s", b.width, msg, styleOff))
|
||
return out.String()
|
||
}
|
||
|
||
func (b *Board) ZoomIn() {
|
||
if b.Zoom > 1 {
|
||
b.Zoom -= 1
|
||
if b.Current + 1 > b.laneOff + b.Zoom {
|
||
b.laneOff += 1
|
||
}
|
||
}
|
||
}
|
||
|
||
func (b *Board) ZoomOut() {
|
||
if b.width / (b.Zoom+1) > 15 {
|
||
b.Zoom += 1
|
||
if len(b.Lanes) < b.laneOff + b.Zoom {
|
||
b.laneOff -= 1
|
||
if b.laneOff < 0 {
|
||
b.laneOff = 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (b *Board) Move(ch rune) {
|
||
if len(b.Lanes) == 0 {
|
||
return
|
||
}
|
||
switch ch {
|
||
case 'h':
|
||
// move left a lane
|
||
if b.Current > 0 {
|
||
b.Current -= 1
|
||
if b.Current < b.laneOff {
|
||
b.laneOff -= 1
|
||
}
|
||
} else {
|
||
b.SetMessage("Cannot move further left", true)
|
||
}
|
||
case 'l':
|
||
// move selection right a lane
|
||
if b.Current < len(b.Lanes)-1 {
|
||
b.Current += 1
|
||
if b.Current > b.laneOff + b.Zoom - 1 {
|
||
b.laneOff += 1
|
||
}
|
||
} else {
|
||
b.SetMessage("Cannot move further right", true)
|
||
}
|
||
case 'j':
|
||
// move selection down a story
|
||
if b.Lanes[b.Current].Current < len(b.Lanes[b.Current].Stories)-1 {
|
||
b.Lanes[b.Current].Current += 1
|
||
if b.Lanes[b.Current].Current * 3 + 1 - (b.Lanes[b.Current].storyOff*3) >= b.height-4 {
|
||
b.Lanes[b.Current].storyOff += 1
|
||
}
|
||
} else {
|
||
b.SetMessage("Cannot move further down", true)
|
||
}
|
||
case 'k':
|
||
// move selection up a story
|
||
if b.Lanes[b.Current].Current > 0 {
|
||
b.Lanes[b.Current].Current -= 1
|
||
if b.Lanes[b.Current].Current < b.Lanes[b.Current].storyOff {
|
||
b.Lanes[b.Current].storyOff -= 1
|
||
}
|
||
} else {
|
||
b.SetMessage("Cannot move further up", true)
|
||
}
|
||
case 'H':
|
||
// move story left a lane
|
||
if b.Current == 0 {
|
||
b.SetMessage("Cannot move story left", true)
|
||
break
|
||
}
|
||
storyIndex := b.Lanes[b.Current].Current
|
||
if len(b.Lanes[b.Current].Stories) <= 0 {
|
||
goto MoveLeft
|
||
}
|
||
b.Lanes[b.Current-1].Stories = append(b.Lanes[b.Current-1].Stories, b.Lanes[b.Current].Stories[storyIndex].Duplicate())
|
||
if storyIndex == len(b.Lanes[b.Current].Stories)-1 {
|
||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||
b.Lanes[b.Current].Current -= 1
|
||
} else {
|
||
b.Lanes[b.Current].Stories[storyIndex] = b.Lanes[b.Current].Stories[len(b.Lanes[b.Current].Stories)-1]
|
||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||
}
|
||
b.Lanes[b.Current-1].Current = len(b.Lanes[b.Current-1].Stories)-1
|
||
MoveLeft: b.Move('h')
|
||
case 'L':
|
||
// move story right a lane
|
||
if b.Current == len(b.Lanes)-1 {
|
||
b.SetMessage("Cannot move story right", true)
|
||
break
|
||
}
|
||
storyIndex := b.Lanes[b.Current].Current
|
||
if len(b.Lanes[b.Current].Stories) <= 0 {
|
||
goto MoveRight
|
||
}
|
||
b.Lanes[b.Current+1].Stories = append(b.Lanes[b.Current+1].Stories, b.Lanes[b.Current].Stories[storyIndex].Duplicate())
|
||
if storyIndex == len(b.Lanes[b.Current].Stories)-1 {
|
||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||
b.Lanes[b.Current].Current -= 1
|
||
} else {
|
||
b.Lanes[b.Current].Stories[storyIndex] = b.Lanes[b.Current].Stories[len(b.Lanes[b.Current].Stories)-1]
|
||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||
}
|
||
b.Lanes[b.Current+1].Current = len(b.Lanes[b.Current+1].Stories)-1
|
||
MoveRight: b.Move('l')
|
||
case 'K':
|
||
// moves story up
|
||
storyIndex := b.Lanes[b.Current].Current
|
||
if storyIndex <= 0 {
|
||
b.SetMessage("Cannot move story up", true)
|
||
break
|
||
}
|
||
swapper := reflect.Swapper(b.Lanes[b.Current].Stories)
|
||
swapper(storyIndex, storyIndex-1)
|
||
b.Move('k')
|
||
case 'J':
|
||
// moves story down
|
||
storyIndex := b.Lanes[b.Current].Current
|
||
if storyIndex > len(b.Lanes[b.Current].Stories)-2 {
|
||
b.SetMessage("Cannot move story down", true)
|
||
break
|
||
}
|
||
swapper := reflect.Swapper(b.Lanes[b.Current].Stories)
|
||
swapper(storyIndex, storyIndex+1)
|
||
b.Move('j')
|
||
|
||
}
|
||
}
|
||
|
||
func (b *Board) DeleteLane() {
|
||
if len(b.Lanes) < 1 {
|
||
b.SetMessage("There are no lanes to delete", true)
|
||
return
|
||
}
|
||
cont, err := GetConfirmation("Are you sure? Type 'yes' to delete: ")
|
||
if err != nil {
|
||
b.SetMessage(err.Error(), true)
|
||
return
|
||
} else if !cont {
|
||
b.SetMessage("Deletion canceled", true)
|
||
return
|
||
}
|
||
if len(b.Lanes[b.Current].Stories) > 0 {
|
||
cont, err = GetConfirmation("Are you really sure? There are stories in the lane... ")
|
||
if err != nil {
|
||
b.SetMessage(err.Error(), true)
|
||
return
|
||
} else if !cont {
|
||
b.SetMessage("Deletion canceled", true)
|
||
return
|
||
}
|
||
}
|
||
if b.Current == len(b.Lanes)-1 {
|
||
b.Lanes = b.Lanes[:len(b.Lanes)-1]
|
||
} else {
|
||
b.Lanes = append(b.Lanes[:b.Current], b.Lanes[b.Current+1:]...)
|
||
}
|
||
if b.Current > len(b.Lanes)-1 {
|
||
b.Current -= 1
|
||
}
|
||
b.SetMessage("Lane deleted", false)
|
||
}
|
||
|
||
func (b *Board) ViewStory() {
|
||
if b.Current > -1 {
|
||
if b.Lanes[b.Current].Current > -1 {
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].View(b)
|
||
}
|
||
} else {
|
||
b.SetMessage("There is no story to view", true)
|
||
}
|
||
}
|
||
|
||
func (b *Board) Regex(args []string) {
|
||
target := strings.ToLower(args[0])
|
||
location := strings.ToLower(args[1])
|
||
exp := strings.Join(args[2:], " ")
|
||
if !strings.HasPrefix(exp, "/") {
|
||
b.SetMessage("Invalid expression, must start with \"/\"", true)
|
||
return
|
||
}
|
||
if len(exp) < 5 {
|
||
b.SetMessage("Invalid expression", true)
|
||
return
|
||
}
|
||
exp = strings.Replace(exp, "\\/", "~@!", -1)
|
||
findReplaceFlag := strings.Split(exp[1:], "/")
|
||
if len(findReplaceFlag) < 2 {
|
||
b.SetMessage("Invalid expression, no replacement value", true)
|
||
return
|
||
}
|
||
for i := range findReplaceFlag {
|
||
findReplaceFlag[i] = strings.Replace(findReplaceFlag[i], "~@!", "\\/", -1)
|
||
}
|
||
re, err := regexp.Compile(findReplaceFlag[0])
|
||
if err != nil {
|
||
b.SetMessage(err.Error(), true)
|
||
return
|
||
}
|
||
var src string
|
||
switch target {
|
||
case "board", "b", "bo", "boa", "boar":
|
||
if !strings.HasPrefix(location, "t") {
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
src = b.Title
|
||
b.Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
case "lanes":
|
||
if !strings.HasPrefix(location, "t") {
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
for i := range b.Lanes {
|
||
src = b.Lanes[i].Title
|
||
b.Lanes[i].Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
}
|
||
case "lane", "l", "la", "lan":
|
||
if !strings.HasPrefix(location, "t") {
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
src = b.Lanes[b.Current].Title
|
||
b.Lanes[b.Current].Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
case "stories!":
|
||
for l := range b.Lanes {
|
||
for i := range b.Lanes[l].Stories {
|
||
switch location {
|
||
case "title", "t", "ti", "tit", "titl":
|
||
src = b.Lanes[l].Stories[i].Title
|
||
b.Lanes[l].Stories[i].Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
case "description", "desc", "d", "de":
|
||
src = b.Lanes[l].Stories[i].Body
|
||
b.Lanes[l].Stories[i].Body = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
case "stories":
|
||
for i := range b.Lanes[b.Current].Stories {
|
||
switch location {
|
||
case "title", "t", "ti", "tit", "titl":
|
||
src = b.Lanes[b.Current].Stories[i].Title
|
||
b.Lanes[b.Current].Stories[i].Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
case "description", "desc", "d", "de":
|
||
src = b.Lanes[b.Current].Stories[i].Body
|
||
b.Lanes[b.Current].Stories[i].Body = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
}
|
||
case "story", "s", "st", "sto", "stor":
|
||
switch location {
|
||
case "title", "t", "ti", "tit", "titl":
|
||
src = b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Title
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Title = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
case "description", "desc", "d", "de":
|
||
src = b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Body
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Body = re.ReplaceAllLiteralString(src, findReplaceFlag[1])
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Invalid location, %q, for %s", location, target), true)
|
||
return
|
||
}
|
||
default:
|
||
b.SetMessage(fmt.Sprintf("Invalid target %q", target), true)
|
||
return
|
||
}
|
||
if b.StoryOpen && strings.HasPrefix(target, "s") {
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].BuildStorySlice(b.width)
|
||
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Draw(b)
|
||
}
|
||
b.SetMessage("Regular expression applied", false)
|
||
}
|
||
|
||
func (b Board) Draw() {
|
||
var out strings.Builder
|
||
out.WriteString(cursorHome)
|
||
out.WriteString(b.PrintHeader())
|
||
out.WriteString(b.PrintLanes())
|
||
out.WriteString(b.PrintInputArea())
|
||
out.WriteString(b.PrintMessage())
|
||
fmt.Print(out.String())
|
||
}
|
||
|
||
func (b *Board) Write(com []string) {
|
||
j, err := json.Marshal(b)
|
||
if err != nil {
|
||
b.SetMessage(err.Error(), true)
|
||
return
|
||
}
|
||
var path string
|
||
if len(com) < 2 && fp == "" {
|
||
b.SetMessage("No path was provided for file write", true)
|
||
return
|
||
} else if len(com) < 2 {
|
||
path = fp
|
||
} else {
|
||
path = ExpandedAbsFilepath(strings.Join(com[1:], " "))
|
||
}
|
||
|
||
var perms os.FileMode = 0664
|
||
fstats, err := os.Stat(path)
|
||
if err == nil {
|
||
perms = fstats.Mode()
|
||
}
|
||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perms)
|
||
if err != nil {
|
||
b.SetMessage(err.Error(), true)
|
||
return
|
||
}
|
||
fp = path
|
||
defer f.Close()
|
||
f.Write(j)
|
||
b.SetMessage("File written", false)
|
||
}
|
||
|