2021-03-23 05:46:01 +00:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2021-03-25 06:22:54 +00:00
|
|
|
|
"encoding/json"
|
2021-03-23 05:46:01 +00:00
|
|
|
|
"fmt"
|
2021-03-25 06:22:54 +00:00
|
|
|
|
"os"
|
2021-03-24 06:15:29 +00:00
|
|
|
|
"reflect"
|
2021-03-28 02:57:29 +00:00
|
|
|
|
"regexp"
|
2021-03-23 05:46:01 +00:00
|
|
|
|
"strings"
|
|
|
|
|
"tildegit.org/sloum/swim/termios"
|
2021-03-25 22:53:22 +00:00
|
|
|
|
"time"
|
2021-03-23 05:46:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 {
|
2021-03-25 22:53:22 +00:00
|
|
|
|
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
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 22:53:22 +00:00
|
|
|
|
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 {
|
2021-03-26 22:54:43 +00:00
|
|
|
|
for l := range b.Lanes {
|
|
|
|
|
if len(b.Lanes[l].Stories) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
b.Lanes[l].Current = 0
|
|
|
|
|
b.Lanes[l].storyOff = 0
|
|
|
|
|
}
|
2021-03-25 22:53:22 +00:00
|
|
|
|
b.Draw()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-24 22:43:40 +00:00
|
|
|
|
|
2021-03-23 05:46:01 +00:00
|
|
|
|
func (b *Board) Run() {
|
|
|
|
|
defer termios.Restore()
|
|
|
|
|
termios.SetCharMode()
|
2021-03-25 06:22:54 +00:00
|
|
|
|
fmt.Print("\033[?25l")
|
2021-03-25 22:53:22 +00:00
|
|
|
|
go b.PollForTermSize()
|
2021-03-23 05:46:01 +00:00
|
|
|
|
|
|
|
|
|
var ch rune
|
|
|
|
|
for {
|
|
|
|
|
b.Draw()
|
|
|
|
|
ch = Getch()
|
|
|
|
|
|
|
|
|
|
switch ch {
|
|
|
|
|
case 'Q':
|
2021-03-24 22:43:40 +00:00
|
|
|
|
Quit()
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case 'N':
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.CreateLane("")
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case 'n':
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.CreateStory("")
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case '\n':
|
2021-03-25 22:53:22 +00:00
|
|
|
|
b.StoryOpen = true
|
2021-03-24 22:43:40 +00:00
|
|
|
|
b.ViewStory()
|
2021-03-25 22:53:22 +00:00
|
|
|
|
b.StoryOpen = false
|
2021-03-27 02:53:37 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
case 'h', 'j', 'k', 'l', 'H', 'L', 'K', 'J':
|
|
|
|
|
b.ClearMessage()
|
|
|
|
|
b.Move(ch)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case ':':
|
|
|
|
|
b.EnterCommand()
|
|
|
|
|
case '+':
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.ZoomIn()
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case '-':
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.ZoomOut()
|
2021-03-24 22:43:40 +00:00
|
|
|
|
default:
|
2021-03-25 06:22:54 +00:00
|
|
|
|
b.SetMessage(fmt.Sprintf("There is no action bound to '%c'", ch), true)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) ClearMessage() {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
b.message = ""
|
|
|
|
|
b.msgErr = false
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) SetMessage(msg string, isError bool) {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
b.message = msg
|
|
|
|
|
b.msgErr = isError
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 22:54:43 +00:00
|
|
|
|
func (b *Board) CreateLane(name string) {
|
|
|
|
|
if name == "" {
|
2021-04-04 05:44:23 +00:00
|
|
|
|
name = GetEditableLine("Lane title: ", "")
|
|
|
|
|
if name == "" {
|
|
|
|
|
b.SetMessage("Lane creation canceled", true)
|
2021-04-04 22:22:41 +00:00
|
|
|
|
return
|
2021-03-26 22:54:43 +00:00
|
|
|
|
}
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.Lanes = append(b.Lanes, MakeLane(name))
|
2021-03-23 05:46:01 +00:00
|
|
|
|
if b.Current < 0 {
|
|
|
|
|
b.Current = 0
|
|
|
|
|
}
|
2021-04-30 21:39:23 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.SetMessage("Lane created", false)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 22:54:43 +00:00
|
|
|
|
func (b *Board) CreateStory(name string) {
|
2021-03-24 22:43:40 +00:00
|
|
|
|
if b.Current < 0 {
|
|
|
|
|
b.SetMessage("You must create a lane first", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.Lanes[b.Current].CreateStory(name, b)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b Board) PrintHeader() string {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
return fmt.Sprintf("%s%s%-*.*s%s\n", style.Header, swimLogo, b.width-12, b.width-12, b.Title, styleOff)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b Board) PrintInputArea() string {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.width, b.width, " ", styleOff)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 06:15:29 +00:00
|
|
|
|
func (b Board) GetLaneSlices(width int) [][]string {
|
|
|
|
|
laneText := make([][]string, b.Zoom)
|
2021-03-25 06:22:54 +00:00
|
|
|
|
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
2021-03-24 06:15:29 +00:00
|
|
|
|
var s []string
|
|
|
|
|
if i < len(b.Lanes) {
|
|
|
|
|
s = b.Lanes[i].StringSlice(width, i == b.Current)
|
|
|
|
|
} else {
|
|
|
|
|
s = make([]string, 0)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
2021-03-25 06:22:54 +00:00
|
|
|
|
laneText[i-b.laneOff] = s
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
return laneText
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b Board) LaneHeaderRow(width, pad int) string {
|
|
|
|
|
var out strings.Builder
|
2021-03-25 06:22:54 +00:00
|
|
|
|
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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))
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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
|
2021-03-25 06:22:54 +00:00
|
|
|
|
laneWidth := b.width / b.Zoom
|
2021-03-24 06:15:29 +00:00
|
|
|
|
laneSlices := b.GetLaneSlices(laneWidth)
|
2021-03-25 06:22:54 +00:00
|
|
|
|
pad := b.width - laneWidth * b.Zoom
|
2021-03-24 06:15:29 +00:00
|
|
|
|
|
|
|
|
|
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
|
|
|
|
|
|
2021-03-25 06:22:54 +00:00
|
|
|
|
for row := 0; row < b.height - 4; row++ {
|
2021-03-24 06:15:29 +00:00
|
|
|
|
for _, l := range laneSlices {
|
|
|
|
|
if row < len(l) {
|
|
|
|
|
out.WriteString(l[row])
|
2021-03-23 05:46:01 +00:00
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, laneWidth, laneWidth, " ", styleOff))
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
if pad > 0 {
|
|
|
|
|
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, pad, pad, " ", styleOff))
|
|
|
|
|
}
|
|
|
|
|
out.WriteRune('\n')
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
out.WriteString(styleOff)
|
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) EnterCommand() {
|
2021-04-04 05:44:23 +00:00
|
|
|
|
command := GetEditableLine(":", "")
|
2021-03-25 06:22:54 +00:00
|
|
|
|
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()
|
|
|
|
|
}
|
2021-03-28 02:57:29 +00:00
|
|
|
|
case "regex", "re", "regexp":
|
|
|
|
|
if len(f) < 4 {
|
|
|
|
|
b.SetMessage("Not enough arguments: regex [target] [location] [/pattern/replacement/]", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b.Regex(f[1:])
|
2021-03-25 06:22:54 +00:00
|
|
|
|
case "q", "quit":
|
|
|
|
|
Quit()
|
2021-03-26 22:54:43 +00:00
|
|
|
|
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)
|
2021-04-05 03:14:43 +00:00
|
|
|
|
default:
|
|
|
|
|
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
|
2021-03-26 22:54:43 +00:00
|
|
|
|
}
|
2021-04-05 03:14:43 +00:00
|
|
|
|
} else {
|
|
|
|
|
b.SetMessage("More info needed: 'create [target|location] [[value]]'", true)
|
2021-03-26 22:54:43 +00:00
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
default:
|
|
|
|
|
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2021-04-05 03:14:43 +00:00
|
|
|
|
b.SetMessage("More info needed: 'delete [target] [[location]]'", true)
|
2021-03-26 22:54:43 +00:00
|
|
|
|
}
|
2021-03-25 06:22:54 +00:00
|
|
|
|
case "set", "s":
|
|
|
|
|
if len(f) > 2 {
|
|
|
|
|
target := strings.ToLower(f[1])
|
|
|
|
|
switch target {
|
2021-03-25 22:53:22 +00:00
|
|
|
|
case "board", "b", "bo", "boa", "boar":
|
|
|
|
|
b.Update(f[2:])
|
2021-03-25 06:22:54 +00:00
|
|
|
|
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 {
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.SetMessage("More info needed: 'set [target] [location] [[value]]'", true)
|
2021-03-25 06:22:54 +00:00
|
|
|
|
}
|
2021-03-25 16:26:45 +00:00
|
|
|
|
case "user", "toggle", "t", "u":
|
2021-03-25 06:22:54 +00:00
|
|
|
|
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Update(f, b)
|
|
|
|
|
default:
|
|
|
|
|
b.SetMessage(fmt.Sprintf("Unknown command %q", f[0]), true)
|
|
|
|
|
}
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 22:53:22 +00:00
|
|
|
|
func (b *Board) Update(args []string) {
|
|
|
|
|
location := strings.ToLower(args[0])
|
|
|
|
|
switch location {
|
|
|
|
|
case "title", "t", "name", "n":
|
2021-03-26 22:54:43 +00:00
|
|
|
|
var title string
|
|
|
|
|
var err error
|
|
|
|
|
if len(args) == 1 {
|
2021-04-04 22:22:41 +00:00
|
|
|
|
title = GetEditableLine("Board title: ", b.Title)
|
2021-04-04 05:44:23 +00:00
|
|
|
|
if title == "" {
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.SetMessage(err.Error(), true)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
title = strings.Join(args[1:], " ")
|
2021-03-25 22:53:22 +00:00
|
|
|
|
}
|
|
|
|
|
b.Title = title
|
|
|
|
|
b.SetMessage("Board title updated", false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 05:46:01 +00:00
|
|
|
|
func (b Board) PrintMessage() string {
|
|
|
|
|
var out strings.Builder
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if b.msgErr {
|
2021-03-23 05:46:01 +00:00
|
|
|
|
out.WriteString(style.MessageErr)
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(style.Message)
|
|
|
|
|
}
|
2021-03-25 16:26:45 +00:00
|
|
|
|
msg := b.message
|
|
|
|
|
if len(msg) > b.width {
|
|
|
|
|
msg = msg[:b.width] + "…"
|
|
|
|
|
}
|
|
|
|
|
out.WriteString(fmt.Sprintf("%-*s%s", b.width, msg, styleOff))
|
2021-03-23 05:46:01 +00:00
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 06:15:29 +00:00
|
|
|
|
func (b *Board) ZoomIn() {
|
|
|
|
|
if b.Zoom > 1 {
|
|
|
|
|
b.Zoom -= 1
|
2021-03-25 22:53:22 +00:00
|
|
|
|
if b.Current + 1 > b.laneOff + b.Zoom {
|
|
|
|
|
b.laneOff += 1
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) ZoomOut() {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if b.width / (b.Zoom+1) > 15 {
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.Zoom += 1
|
2021-03-25 22:53:22 +00:00
|
|
|
|
if len(b.Lanes) < b.laneOff + b.Zoom {
|
|
|
|
|
b.laneOff -= 1
|
|
|
|
|
if b.laneOff < 0 {
|
|
|
|
|
b.laneOff = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if b.Current < b.laneOff {
|
|
|
|
|
b.laneOff -= 1
|
2021-03-24 22:43:40 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
} 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
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if b.Current > b.laneOff + b.Zoom - 1 {
|
|
|
|
|
b.laneOff += 1
|
2021-03-24 22:43:40 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
} 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
|
2021-03-27 02:53:37 +00:00
|
|
|
|
if b.Lanes[b.Current].Current * 3 + 1 - (b.Lanes[b.Current].storyOff*3) >= b.height-4 {
|
2021-03-25 06:22:54 +00:00
|
|
|
|
b.Lanes[b.Current].storyOff += 1
|
2021-03-24 22:43:40 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
} 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
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if b.Lanes[b.Current].Current < b.Lanes[b.Current].storyOff {
|
|
|
|
|
b.Lanes[b.Current].storyOff -= 1
|
2021-03-24 22:43:40 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
} 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')
|
2021-05-11 02:51:22 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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')
|
2021-05-11 02:51:22 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-24 06:15:29 +00:00
|
|
|
|
case 'K':
|
2021-03-24 22:43:40 +00:00
|
|
|
|
// moves story up
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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')
|
2021-05-11 02:51:22 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-24 06:15:29 +00:00
|
|
|
|
case 'J':
|
2021-03-24 22:43:40 +00:00
|
|
|
|
// moves story down
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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')
|
2021-05-11 02:51:22 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-24 06:15:29 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-26 22:54:43 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
2021-04-30 21:39:23 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-26 22:54:43 +00:00
|
|
|
|
b.SetMessage("Lane deleted", false)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 22:43:40 +00:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-28 02:57:29 +00:00
|
|
|
|
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)
|
|
|
|
|
}
|
2021-04-30 21:39:23 +00:00
|
|
|
|
unsavedChanges = true
|
2021-03-28 02:57:29 +00:00
|
|
|
|
b.SetMessage("Regular expression applied", false)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 05:46:01 +00:00
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 06:22:54 +00:00
|
|
|
|
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:], " "))
|
|
|
|
|
}
|
2021-03-25 22:53:22 +00:00
|
|
|
|
|
|
|
|
|
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)
|
2021-03-25 06:22:54 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
b.SetMessage(err.Error(), true)
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-03-25 16:26:45 +00:00
|
|
|
|
fp = path
|
2021-03-25 06:22:54 +00:00
|
|
|
|
defer f.Close()
|
|
|
|
|
f.Write(j)
|
|
|
|
|
b.SetMessage("File written", false)
|
2021-04-30 21:39:23 +00:00
|
|
|
|
unsavedChanges = false
|
2021-03-25 06:22:54 +00:00
|
|
|
|
}
|
|
|
|
|
|