2021-03-23 05:46:01 +00:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
2021-03-24 06:15:29 +00:00
|
|
|
|
"reflect"
|
2021-03-23 05:46:01 +00:00
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
"tildegit.org/sloum/swim/termios"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
Body string
|
|
|
|
|
Created time.Time
|
|
|
|
|
Lanes []Lane
|
|
|
|
|
Current int // Index of current lane
|
2021-03-24 06:15:29 +00:00
|
|
|
|
LaneOff int
|
2021-03-23 05:46:01 +00:00
|
|
|
|
Message string
|
|
|
|
|
MsgErr bool
|
|
|
|
|
Width int
|
|
|
|
|
Height int
|
|
|
|
|
Zoom int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) Run() {
|
|
|
|
|
defer termios.Restore()
|
|
|
|
|
termios.SetCharMode()
|
|
|
|
|
|
|
|
|
|
var ch rune
|
|
|
|
|
for {
|
|
|
|
|
b.Draw()
|
|
|
|
|
ch = Getch()
|
|
|
|
|
|
|
|
|
|
switch ch {
|
|
|
|
|
case 'Q':
|
|
|
|
|
termios.Restore()
|
2021-03-24 06:15:29 +00:00
|
|
|
|
fmt.Print("\033[?25h")
|
2021-03-23 05:46:01 +00:00
|
|
|
|
os.Exit(0)
|
|
|
|
|
case 'N':
|
|
|
|
|
b.CreateLane()
|
|
|
|
|
case 'n':
|
|
|
|
|
b.CreateStory()
|
|
|
|
|
case '\n':
|
|
|
|
|
// View current story
|
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 'c':
|
|
|
|
|
// Comment on current story
|
|
|
|
|
case 'D':
|
2021-03-24 06:15:29 +00:00
|
|
|
|
// Delete current story
|
2021-03-23 05:46:01 +00:00
|
|
|
|
case 'e':
|
|
|
|
|
// Edit current story
|
|
|
|
|
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-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
|
|
|
|
laneTitle, err := GetAndConfirmCommandLine("Lane Title: ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.SetMessage(err.Error(), true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b.Lanes = append(b.Lanes, MakeLane(laneTitle))
|
|
|
|
|
if b.Current < 0 {
|
|
|
|
|
b.Current = 0
|
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.SetMessage("Lane created", false)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) CreateStory() {
|
|
|
|
|
b.Lanes[b.Current].CreateStory(b)
|
2021-03-24 06:15:29 +00:00
|
|
|
|
b.SetMessage("Story created", false)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 06:15:29 +00:00
|
|
|
|
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)
|
2021-03-23 05:46:01 +00:00
|
|
|
|
}
|
2021-03-24 06:15:29 +00:00
|
|
|
|
laneText[i] = 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
|
|
|
|
|
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))
|
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
|
|
|
|
|
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])
|
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() {
|
|
|
|
|
var out strings.Builder
|
|
|
|
|
out.WriteString(upAndLeft) // Move up one and over all
|
|
|
|
|
out.WriteString(style.Input)
|
|
|
|
|
fmt.Print(out.String())
|
|
|
|
|
command, err := GetLine(":")
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.SetMessage(err.Error(), true)
|
|
|
|
|
}
|
|
|
|
|
b.SetMessage(command, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b Board) PrintMessage() string {
|
|
|
|
|
var out strings.Builder
|
|
|
|
|
if b.MsgErr {
|
|
|
|
|
out.WriteString(style.MessageErr)
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(style.Message)
|
|
|
|
|
}
|
|
|
|
|
out.WriteString(fmt.Sprintf("%-*.*s%s", b.Width, b.Width, b.Message, styleOff))
|
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 06:15:29 +00:00
|
|
|
|
func (b *Board) ZoomIn() {
|
|
|
|
|
if b.Zoom > 1 {
|
|
|
|
|
b.Zoom -= 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) ZoomOut() {
|
|
|
|
|
if b.Width / (b.Zoom+1) > 15 {
|
|
|
|
|
b.Zoom += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Board) Move(ch rune) {
|
|
|
|
|
if len(b.Lanes) == 0 {
|
|
|
|
|
b.SetMessage("You cannot move what does not exist", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
switch ch {
|
|
|
|
|
case 'h':
|
|
|
|
|
// move left a lane
|
|
|
|
|
if b.Current > 0 {
|
|
|
|
|
b.Current -= 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
|
|
|
|
|
} 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
|
|
|
|
|
} 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
|
|
|
|
|
} 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':
|
|
|
|
|
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':
|
|
|
|
|
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-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())
|
|
|
|
|
}
|
|
|
|
|
|