2
1
Fork 0
swim/board.go

381 lines
9.0 KiB
Go
Raw Normal View History

package main
import (
2021-03-25 06:22:54 +00:00
"encoding/json"
"fmt"
2021-03-25 06:22:54 +00:00
"os"
"reflect"
"strings"
"tildegit.org/sloum/swim/termios"
)
const (
swimLogo string = "\033[7m ▟\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 06:22:54 +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"`
}
2021-03-24 22:43:40 +00:00
func (b *Board) Run() {
defer termios.Restore()
termios.SetCharMode()
2021-03-25 06:22:54 +00:00
fmt.Print("\033[?25l")
var ch rune
for {
b.Draw()
ch = Getch()
switch ch {
case 'Q':
2021-03-24 22:43:40 +00:00
Quit()
case 'N':
b.CreateLane()
case 'n':
b.CreateStory()
case '\n':
2021-03-24 22:43:40 +00:00
b.ViewStory()
case 'h', 'j', 'k', 'l', 'H', 'L', 'K', 'J':
b.ClearMessage()
b.Move(ch)
case 'D':
// Delete current story
case ':':
b.EnterCommand()
case '+':
b.ZoomIn()
case '-':
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)
}
}
}
func (b *Board) ClearMessage() {
2021-03-25 06:22:54 +00:00
b.message = ""
b.msgErr = false
}
func (b *Board) SetMessage(msg string, isError bool) {
2021-03-25 06:22:54 +00:00
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
}
b.SetMessage("Lane created", false)
}
func (b *Board) CreateStory() {
2021-03-24 22:43:40 +00:00
if b.Current < 0 {
b.SetMessage("You must create a lane first", true)
return
}
b.Lanes[b.Current].CreateStory(b)
b.SetMessage("Story created", false)
}
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)
}
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)
}
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++ {
var s []string
if i < len(b.Lanes) {
s = b.Lanes[i].StringSlice(width, i == b.Current)
} else {
s = make([]string, 0)
}
2021-03-25 06:22:54 +00:00
laneText[i-b.laneOff] = s
}
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++ {
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
2021-03-25 06:22:54 +00:00
laneWidth := b.width / b.Zoom
laneSlices := b.GetLaneSlices(laneWidth)
2021-03-25 06:22:54 +00:00
pad := b.width - laneWidth * b.Zoom
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
2021-03-25 06:22:54 +00:00
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() {
2021-03-25 06:22:54 +00:00
command, err := GetCommandLine(":")
if err != nil {
b.SetMessage(err.Error(), true)
}
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()
}
case "q", "quit":
Quit()
case "set", "s":
if len(f) > 2 {
target := strings.ToLower(f[1])
switch target {
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: 'update [target] [location]'", true)
}
case "user", "toggle":
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) PrintMessage() string {
var out strings.Builder
2021-03-25 06:22:54 +00:00
if b.msgErr {
out.WriteString(style.MessageErr)
} else {
out.WriteString(style.Message)
}
2021-03-25 06:22:54 +00:00
out.WriteString(fmt.Sprintf("%-*.*s%s", b.width, b.width, b.message, styleOff))
return out.String()
}
func (b *Board) ZoomIn() {
if b.Zoom > 1 {
b.Zoom -= 1
}
}
func (b *Board) ZoomOut() {
2021-03-25 06:22:54 +00:00
if b.width / (b.Zoom+1) > 15 {
b.Zoom += 1
}
}
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
}
} 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
}
} 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-25 06:22:54 +00:00
if b.Lanes[b.Current].Current * 2 + 1 > b.height-5 {
b.Lanes[b.Current].storyOff += 1
2021-03-24 22:43:40 +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
}
} 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':
2021-03-24 22:43:40 +00:00
// 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':
2021-03-24 22:43:40 +00:00
// 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')
}
}
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)
}
}
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:], " "))
}
f, err := os.Create(path)
if err != nil {
b.SetMessage(err.Error(), true)
return
}
defer f.Close()
f.Write(j)
b.SetMessage("File written", false)
}