2
1
Fork 0
swim/board.go

439 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"tildegit.org/sloum/swim/termios"
"time"
)
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 {
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 {
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 '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()
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() {
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() {
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 {
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, err := GetCommandLine(":")
if err != nil {
b.SetMessage(err.Error(), true)
}
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 "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: 'update [target] [location]'", 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":
title, err := GetAndConfirmCommandLine("New board title: ")
if err != nil {
b.SetMessage(err.Error(), true)
break
}
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 * 2 + 1 > b.height-5 {
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) 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())
}
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)
}