More or less working board
This commit is contained in:
parent
401526bccc
commit
1d44dc779e
138
board.go
138
board.go
|
@ -1,10 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"tildegit.org/sloum/swim/termios"
|
"tildegit.org/sloum/swim/termios"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,23 +18,22 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Board struct {
|
type Board struct {
|
||||||
Title string
|
Title string `json:"BoardTitle"`
|
||||||
Body string
|
Lanes []Lane `json:"Lanes"`
|
||||||
Created time.Time
|
Current int `json:"CurrentLane"` // Index of current lane
|
||||||
Lanes []Lane
|
laneOff int
|
||||||
Current int // Index of current lane
|
message string
|
||||||
LaneOff int
|
msgErr bool
|
||||||
Message string
|
width int
|
||||||
MsgErr bool
|
height int
|
||||||
Width int
|
Zoom int `json:"Zoom"`
|
||||||
Height int
|
|
||||||
Zoom int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (b *Board) Run() {
|
func (b *Board) Run() {
|
||||||
defer termios.Restore()
|
defer termios.Restore()
|
||||||
termios.SetCharMode()
|
termios.SetCharMode()
|
||||||
|
fmt.Print("\033[?25l")
|
||||||
|
|
||||||
var ch rune
|
var ch rune
|
||||||
for {
|
for {
|
||||||
|
@ -54,8 +54,6 @@ func (b *Board) Run() {
|
||||||
b.Move(ch)
|
b.Move(ch)
|
||||||
case 'D':
|
case 'D':
|
||||||
// Delete current story
|
// Delete current story
|
||||||
case 'e':
|
|
||||||
// Edit current story
|
|
||||||
case ':':
|
case ':':
|
||||||
b.EnterCommand()
|
b.EnterCommand()
|
||||||
case '+':
|
case '+':
|
||||||
|
@ -63,19 +61,19 @@ func (b *Board) Run() {
|
||||||
case '-':
|
case '-':
|
||||||
b.ZoomOut()
|
b.ZoomOut()
|
||||||
default:
|
default:
|
||||||
b.SetMessage(fmt.Sprintf("There is no action bount to '%c'", ch), true)
|
b.SetMessage(fmt.Sprintf("There is no action bound to '%c'", ch), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) ClearMessage() {
|
func (b *Board) ClearMessage() {
|
||||||
b.Message = ""
|
b.message = ""
|
||||||
b.MsgErr = false
|
b.msgErr = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) SetMessage(msg string, isError bool) {
|
func (b *Board) SetMessage(msg string, isError bool) {
|
||||||
b.Message = msg
|
b.message = msg
|
||||||
b.MsgErr = isError
|
b.msgErr = isError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) CreateLane() {
|
func (b *Board) CreateLane() {
|
||||||
|
@ -101,30 +99,30 @@ func (b *Board) CreateStory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Board) PrintHeader() string {
|
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)
|
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 {
|
func (b Board) PrintInputArea() string {
|
||||||
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.Width, b.Width, " ", styleOff)
|
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.width, b.width, " ", styleOff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Board) GetLaneSlices(width int) [][]string {
|
func (b Board) GetLaneSlices(width int) [][]string {
|
||||||
laneText := make([][]string, b.Zoom)
|
laneText := make([][]string, b.Zoom)
|
||||||
for i := b.LaneOff; i < b.LaneOff + b.Zoom; i++ {
|
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
||||||
var s []string
|
var s []string
|
||||||
if i < len(b.Lanes) {
|
if i < len(b.Lanes) {
|
||||||
s = b.Lanes[i].StringSlice(width, i == b.Current)
|
s = b.Lanes[i].StringSlice(width, i == b.Current)
|
||||||
} else {
|
} else {
|
||||||
s = make([]string, 0)
|
s = make([]string, 0)
|
||||||
}
|
}
|
||||||
laneText[i-b.LaneOff] = s
|
laneText[i-b.laneOff] = s
|
||||||
}
|
}
|
||||||
return laneText
|
return laneText
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Board) LaneHeaderRow(width, pad int) string {
|
func (b Board) LaneHeaderRow(width, pad int) string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
for i := b.LaneOff; i < b.LaneOff + b.Zoom; i++ {
|
for i := b.laneOff; i < b.laneOff + b.Zoom; i++ {
|
||||||
if i < len(b.Lanes) {
|
if i < len(b.Lanes) {
|
||||||
out.WriteString(b.Lanes[i].Header(width, i == b.Current))
|
out.WriteString(b.Lanes[i].Header(width, i == b.Current))
|
||||||
} else {
|
} else {
|
||||||
|
@ -140,13 +138,13 @@ func (b Board) LaneHeaderRow(width, pad int) string {
|
||||||
|
|
||||||
func (b Board) PrintLanes() string {
|
func (b Board) PrintLanes() string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
laneWidth := b.Width / b.Zoom
|
laneWidth := b.width / b.Zoom
|
||||||
laneSlices := b.GetLaneSlices(laneWidth)
|
laneSlices := b.GetLaneSlices(laneWidth)
|
||||||
pad := b.Width - laneWidth * b.Zoom
|
pad := b.width - laneWidth * b.Zoom
|
||||||
|
|
||||||
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
|
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
|
||||||
|
|
||||||
for row := 0; row < b.Height - 4; row++ {
|
for row := 0; row < b.height - 4; row++ {
|
||||||
for _, l := range laneSlices {
|
for _, l := range laneSlices {
|
||||||
if row < len(l) {
|
if row < len(l) {
|
||||||
out.WriteString(l[row])
|
out.WriteString(l[row])
|
||||||
|
@ -164,25 +162,52 @@ func (b Board) PrintLanes() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) EnterCommand() {
|
func (b *Board) EnterCommand() {
|
||||||
var out strings.Builder
|
command, err := GetCommandLine(":")
|
||||||
out.WriteString(upAndLeft) // Move up one and over all
|
|
||||||
out.WriteString(style.Input)
|
|
||||||
fmt.Print(out.String())
|
|
||||||
command, err := GetLine(":")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.SetMessage(err.Error(), true)
|
b.SetMessage(err.Error(), true)
|
||||||
}
|
}
|
||||||
b.SetMessage(command, false)
|
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 {
|
func (b Board) PrintMessage() string {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
if b.MsgErr {
|
if b.msgErr {
|
||||||
out.WriteString(style.MessageErr)
|
out.WriteString(style.MessageErr)
|
||||||
} else {
|
} else {
|
||||||
out.WriteString(style.Message)
|
out.WriteString(style.Message)
|
||||||
}
|
}
|
||||||
out.WriteString(fmt.Sprintf("%-*.*s%s", b.Width, b.Width, b.Message, styleOff))
|
out.WriteString(fmt.Sprintf("%-*.*s%s", b.width, b.width, b.message, styleOff))
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +218,7 @@ func (b *Board) ZoomIn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) ZoomOut() {
|
func (b *Board) ZoomOut() {
|
||||||
if b.Width / (b.Zoom+1) > 15 {
|
if b.width / (b.Zoom+1) > 15 {
|
||||||
b.Zoom += 1
|
b.Zoom += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,8 +232,8 @@ func (b *Board) Move(ch rune) {
|
||||||
// move left a lane
|
// move left a lane
|
||||||
if b.Current > 0 {
|
if b.Current > 0 {
|
||||||
b.Current -= 1
|
b.Current -= 1
|
||||||
if b.Current < b.LaneOff {
|
if b.Current < b.laneOff {
|
||||||
b.LaneOff -= 1
|
b.laneOff -= 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further left", true)
|
b.SetMessage("Cannot move further left", true)
|
||||||
|
@ -217,8 +242,8 @@ func (b *Board) Move(ch rune) {
|
||||||
// move selection right a lane
|
// move selection right a lane
|
||||||
if b.Current < len(b.Lanes)-1 {
|
if b.Current < len(b.Lanes)-1 {
|
||||||
b.Current += 1
|
b.Current += 1
|
||||||
if b.Current > b.LaneOff + b.Zoom - 1 {
|
if b.Current > b.laneOff + b.Zoom - 1 {
|
||||||
b.LaneOff += 1
|
b.laneOff += 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further right", true)
|
b.SetMessage("Cannot move further right", true)
|
||||||
|
@ -227,8 +252,8 @@ func (b *Board) Move(ch rune) {
|
||||||
// move selection down a story
|
// move selection down a story
|
||||||
if b.Lanes[b.Current].Current < len(b.Lanes[b.Current].Stories)-1 {
|
if b.Lanes[b.Current].Current < len(b.Lanes[b.Current].Stories)-1 {
|
||||||
b.Lanes[b.Current].Current += 1
|
b.Lanes[b.Current].Current += 1
|
||||||
if b.Lanes[b.Current].Current * 2 + 1 > b.Height-5 {
|
if b.Lanes[b.Current].Current * 2 + 1 > b.height-5 {
|
||||||
b.Lanes[b.Current].StoryOff += 1
|
b.Lanes[b.Current].storyOff += 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further down", true)
|
b.SetMessage("Cannot move further down", true)
|
||||||
|
@ -237,8 +262,8 @@ func (b *Board) Move(ch rune) {
|
||||||
// move selection up a story
|
// move selection up a story
|
||||||
if b.Lanes[b.Current].Current > 0 {
|
if b.Lanes[b.Current].Current > 0 {
|
||||||
b.Lanes[b.Current].Current -= 1
|
b.Lanes[b.Current].Current -= 1
|
||||||
if b.Lanes[b.Current].Current < b.Lanes[b.Current].StoryOff {
|
if b.Lanes[b.Current].Current < b.Lanes[b.Current].storyOff {
|
||||||
b.Lanes[b.Current].StoryOff -= 1
|
b.Lanes[b.Current].storyOff -= 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further up", true)
|
b.SetMessage("Cannot move further up", true)
|
||||||
|
@ -328,3 +353,28 @@ func (b Board) Draw() {
|
||||||
fmt.Print(out.String())
|
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:], " "))
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ var style Styles
|
||||||
var colors = map[int]map[string]string{
|
var colors = map[int]map[string]string{
|
||||||
SimpleColor: map[string]string{
|
SimpleColor: map[string]string{
|
||||||
"Header": "\033[34;107;1m", // bold blue on bright white
|
"Header": "\033[34;107;1m", // bold blue on bright white
|
||||||
"Message": "\033[97;42m", // bright white on green
|
"Message": "\033[97;44m", // bright white on blue
|
||||||
"MessageErr": "\033[97;41m", // bright white on red
|
"MessageErr": "\033[97;41m", // bright white on red
|
||||||
"Lane": "\033[30;104m", // black on bright blue
|
"Lane": "\033[30;104m", // black on bright blue
|
||||||
"LaneSelected": "\033[30;103m", // black on bright yellow
|
"LaneSelected": "\033[30;103m", // black on bright yellow
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
User string
|
User string `json:"CommentUser"`
|
||||||
Body string
|
Body string `json:"CommentBody"`
|
||||||
Created time.Time
|
Created time.Time `json:"CommentCreated"`
|
||||||
}
|
}
|
||||||
|
|
29
lane.go
29
lane.go
|
@ -2,13 +2,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Lane struct {
|
type Lane struct {
|
||||||
Title string
|
Title string `json:"LaneTitle"`
|
||||||
Stories []Story
|
Stories []Story `json:"Stories"`
|
||||||
Current int // Index of current story
|
Current int `json:"CurrentStory"` // Index of current story
|
||||||
StoryOff int // offset for the lane slice
|
storyOff int // offset for the lane slice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Lane) CreateStory(b *Board) {
|
func (l *Lane) CreateStory(b *Board) {
|
||||||
|
@ -42,10 +43,10 @@ func (l Lane) Header(width int, selected bool) string {
|
||||||
|
|
||||||
func (l Lane) StringSlice(width int, selected bool) []string {
|
func (l Lane) StringSlice(width int, selected bool) []string {
|
||||||
out := make([]string, 0, len(l.Stories) * 3 + 1)
|
out := make([]string, 0, len(l.Stories) * 3 + 1)
|
||||||
for i := l.StoryOff; i < len(l.Stories); i++ {
|
for i := l.storyOff; i < len(l.Stories); i++ {
|
||||||
leadIn := " "
|
leadIn := " "
|
||||||
if selected && l.Current == i {
|
if selected && l.Current == i {
|
||||||
leadIn = "\033[1m➜\033[21m "
|
leadIn = "\033[1m➜ "
|
||||||
}
|
}
|
||||||
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||||||
out = append(out, fmt.Sprintf("%s %s%s%-*.*s%s %s", style.Lane, style.Input, leadIn, width-4, width-4, l.Stories[i].Title, style.Lane, styleOff))
|
out = append(out, fmt.Sprintf("%s %s%s%-*.*s%s %s", style.Lane, style.Input, leadIn, width-4, width-4, l.Stories[i].Title, style.Lane, styleOff))
|
||||||
|
@ -56,6 +57,22 @@ func (l Lane) StringSlice(width int, selected bool) []string {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Lane) Update(args []string, b *Board) {
|
||||||
|
location := strings.ToLower(args[0])
|
||||||
|
switch location {
|
||||||
|
case "title", "t", "name", "n":
|
||||||
|
title, err := GetAndConfirmCommandLine("New lane title: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.Title = title
|
||||||
|
b.SetMessage("Lane title updated", false)
|
||||||
|
default:
|
||||||
|
b.SetMessage(fmt.Sprintf("Unknown lane location %q", args[0]), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func MakeLane(title string) Lane {
|
func MakeLane(title string) Lane {
|
||||||
return Lane{title, make([]Story,0,5), -1, 0}
|
return Lane{title, make([]Story,0,5), -1, 0}
|
||||||
}
|
}
|
||||||
|
|
73
main.go
73
main.go
|
@ -2,14 +2,30 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"tildegit.org/sloum/swim/termios"
|
"tildegit.org/sloum/swim/termios"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var board Board
|
var board Board
|
||||||
|
var fp string
|
||||||
|
|
||||||
|
func ExpandedAbsFilepath(p string) string {
|
||||||
|
if strings.HasPrefix(p, "~/") {
|
||||||
|
usr, _ := user.Current()
|
||||||
|
homedir := usr.HomeDir
|
||||||
|
p = filepath.Join(homedir, p[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
path, _ := filepath.Abs(p)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func Getch() rune {
|
func Getch() rune {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
@ -53,6 +69,9 @@ func GetAndConfirmCommandLine(prefix string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return line, err
|
return line, err
|
||||||
}
|
}
|
||||||
|
if line == "" {
|
||||||
|
return line, fmt.Errorf("Cancelled input")
|
||||||
|
}
|
||||||
VerifyQuery: fmt.Print(upAndLeft) // Move up one and over all
|
VerifyQuery: fmt.Print(upAndLeft) // Move up one and over all
|
||||||
fmt.Print(style.Input)
|
fmt.Print(style.Input)
|
||||||
fmt.Printf("%s%s%sIs %q correct? (y/n/c)", cursorEnd, upAndLeft, style.Input, line)
|
fmt.Printf("%s%s%sIs %q correct? (y/n/c)", cursorEnd, upAndLeft, style.Input, line)
|
||||||
|
@ -63,7 +82,7 @@ func GetAndConfirmCommandLine(prefix string) (string, error) {
|
||||||
fmt.Print(cursorEnd)
|
fmt.Print(cursorEnd)
|
||||||
continue
|
continue
|
||||||
} else if conf == 'c' {
|
} else if conf == 'c' {
|
||||||
err = fmt.Errorf("Cancelled")
|
err = fmt.Errorf("Cancelled input")
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(cursorEnd)
|
fmt.Print(cursorEnd)
|
||||||
|
@ -100,25 +119,49 @@ func WrapText(text string, lineWidth int) string {
|
||||||
|
|
||||||
func Quit() {
|
func Quit() {
|
||||||
termios.Restore()
|
termios.Restore()
|
||||||
fmt.Print("\033[?25h")
|
fmt.Print(cursorEnd)
|
||||||
|
fmt.Print("\033[0m\n\033[?25h")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadFile(path string, cols, rows int) {
|
||||||
|
p := ExpandedAbsFilepath(path)
|
||||||
|
bytes, err := ioutil.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Could not open file %q\n", path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fp = p
|
||||||
|
err = json.Unmarshal(bytes, &board)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Could not understand input file:\n%s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
board.width = cols
|
||||||
|
board.height = rows
|
||||||
|
board.message = fmt.Sprintf("Loaded file: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
args := flag.Args()
|
||||||
style.Init(SimpleColor)
|
style.Init(SimpleColor)
|
||||||
cols, rows := termios.GetWindowSize()
|
cols, rows := termios.GetWindowSize()
|
||||||
board = Board{
|
if len(args) > 0 {
|
||||||
Title: "My Test Board",
|
LoadFile(args[0], cols, rows)
|
||||||
Body: "Some misc info",
|
} else {
|
||||||
Created: time.Now(),
|
fp = ""
|
||||||
Lanes: make([]Lane, 0, 1),
|
board = Board{
|
||||||
Current: -1,
|
Title: "My Test Board",
|
||||||
LaneOff: 0,
|
Lanes: make([]Lane, 0, 1),
|
||||||
Message: "Welcome to SWIM",
|
Current: -1,
|
||||||
MsgErr: false,
|
laneOff: 0,
|
||||||
Width: cols,
|
message: "Welcome to SWIM",
|
||||||
Height: rows,
|
msgErr: false,
|
||||||
Zoom: 3}
|
width: cols,
|
||||||
|
height: rows,
|
||||||
|
Zoom: 3}
|
||||||
|
}
|
||||||
|
|
||||||
board.Run()
|
board.Run()
|
||||||
}
|
}
|
||||||
|
|
173
story.go
173
story.go
|
@ -3,26 +3,28 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Story struct {
|
type Story struct {
|
||||||
Title string
|
Title string `json:"StoryTitle"`
|
||||||
Body string
|
Body string `json:"StoryBody"`
|
||||||
Users []string
|
Users []string `json:"StoryUsers"`
|
||||||
Tag int
|
Tag int `json:"StoryTag"`
|
||||||
Tasks []Task
|
Tasks []Task `json:"StoryTasks"`
|
||||||
Comments []Comment
|
Comments []Comment `json:"StoryComments"`
|
||||||
Created time.Time
|
Created time.Time `json:"StoryCreated"`
|
||||||
Updated time.Time
|
Updated time.Time `json:"StoryUpdated"`
|
||||||
Offset int
|
offset int
|
||||||
StSlice []string
|
stSlice []string
|
||||||
Points int
|
Points int `json:"StoryPoints"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Story) View(b *Board) {
|
func (s *Story) View(b *Board) {
|
||||||
s.BuildStorySlice(b.Width)
|
s.BuildStorySlice(b.width)
|
||||||
var ch rune
|
var ch rune
|
||||||
for {
|
for {
|
||||||
s.Draw(b)
|
s.Draw(b)
|
||||||
|
@ -30,16 +32,23 @@ func (s *Story) View(b *Board) {
|
||||||
|
|
||||||
switch ch {
|
switch ch {
|
||||||
case 'j', 'k':
|
case 'j', 'k':
|
||||||
|
b.ClearMessage()
|
||||||
s.Scroll(ch, b)
|
s.Scroll(ch, b)
|
||||||
case ':':
|
case ':':
|
||||||
b.EnterCommand()
|
b.EnterCommand()
|
||||||
case 'x':
|
case 'h':
|
||||||
s.Offset = 0
|
s.offset = 0
|
||||||
return
|
return
|
||||||
case 'Q':
|
case 'Q':
|
||||||
Quit()
|
Quit()
|
||||||
case 'c', 'C':
|
case 'c', 'C':
|
||||||
s.AddComment(b)
|
s.AddComment(b)
|
||||||
|
case 't', 'T':
|
||||||
|
s.AddTask(b)
|
||||||
|
case 'u':
|
||||||
|
s.Update([]string{"user"}, b)
|
||||||
|
case 'p':
|
||||||
|
s.Update([]string{"points"}, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +65,7 @@ func (s *Story) AddComment(b *Board) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.Comments = append(s.Comments, Comment{u.Name, comment, time.Now()})
|
s.Comments = append(s.Comments, Comment{u.Name, comment, time.Now()})
|
||||||
s.BuildStorySlice(b.Width)
|
s.BuildStorySlice(b.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Story) BuildStorySlice(width int) {
|
func (s *Story) BuildStorySlice(width int) {
|
||||||
|
@ -82,12 +91,12 @@ func (s *Story) BuildStorySlice(width int) {
|
||||||
out.WriteRune('\n')
|
out.WriteRune('\n')
|
||||||
out.WriteRune('\n')
|
out.WriteRune('\n')
|
||||||
for i, task := range s.Tasks {
|
for i, task := range s.Tasks {
|
||||||
out.WriteString(fmt.Sprintf("%2d. ", i+1))
|
|
||||||
if task.Complete {
|
if task.Complete {
|
||||||
out.WriteString("☒ ")
|
out.WriteString("✔ ")
|
||||||
} else {
|
} else {
|
||||||
out.WriteString("☐ ")
|
out.WriteString(" ")
|
||||||
}
|
}
|
||||||
|
out.WriteString(fmt.Sprintf("%2d. ", i+1))
|
||||||
out.WriteString(WrapText(task.Body, width-6))
|
out.WriteString(WrapText(task.Body, width-6))
|
||||||
out.WriteRune('\n')
|
out.WriteRune('\n')
|
||||||
}
|
}
|
||||||
|
@ -107,19 +116,19 @@ func (s *Story) BuildStorySlice(width int) {
|
||||||
out.WriteRune('\n')
|
out.WriteRune('\n')
|
||||||
out.WriteRune('\n')
|
out.WriteRune('\n')
|
||||||
}
|
}
|
||||||
s.StSlice = strings.Split(out.String(), "\n")
|
s.stSlice = strings.Split(out.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Story) Draw(b *Board) {
|
func (s Story) Draw(b *Board) {
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
out.WriteString(cursorHome)
|
out.WriteString(cursorHome)
|
||||||
out.WriteString(b.PrintHeader())
|
out.WriteString(b.PrintHeader())
|
||||||
for i := 0; i < b.Height-3; i++ {
|
for i := 0; i < b.height-3; i++ {
|
||||||
index := i+s.Offset
|
index := i+s.offset
|
||||||
if index >= len(s.StSlice) {
|
if index >= len(s.stSlice) {
|
||||||
out.WriteString(fmt.Sprintf("%*.*s\n", b.Width, b.Width, ""))
|
out.WriteString(fmt.Sprintf("%*.*s\n", b.width, b.width, ""))
|
||||||
} else {
|
} else {
|
||||||
out.WriteString(fmt.Sprintf("%-*.*s\n", b.Width, b.Width, s.StSlice[index]))
|
out.WriteString(fmt.Sprintf("%-*.*s\n", b.width, b.width, s.stSlice[index]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.WriteString(b.PrintInputArea())
|
out.WriteString(b.PrintInputArea())
|
||||||
|
@ -130,20 +139,128 @@ func (s Story) Draw(b *Board) {
|
||||||
func (s *Story) Scroll(dir rune, b *Board) {
|
func (s *Story) Scroll(dir rune, b *Board) {
|
||||||
switch dir {
|
switch dir {
|
||||||
case 'j':
|
case 'j':
|
||||||
if s.Offset + b.Height - 3 < len(s.StSlice) {
|
if s.offset + b.height - 3 < len(s.stSlice) {
|
||||||
s.Offset += 1
|
s.offset += 1
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further down", true)
|
b.SetMessage("Cannot move further down", true)
|
||||||
}
|
}
|
||||||
case 'k':
|
case 'k':
|
||||||
if s.Offset > 0 {
|
if s.offset > 0 {
|
||||||
s.Offset -= 1
|
s.offset -= 1
|
||||||
} else {
|
} else {
|
||||||
b.SetMessage("Cannot move further up", true)
|
b.SetMessage("Cannot move further up", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Story) Update(args []string, b *Board) {
|
||||||
|
location := strings.ToLower(args[0])
|
||||||
|
switch location {
|
||||||
|
case "title", "t":
|
||||||
|
title, err := GetAndConfirmCommandLine("New story title: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Title = title
|
||||||
|
s.Updated = time.Now()
|
||||||
|
b.SetMessage("Story title updated", false)
|
||||||
|
s.BuildStorySlice(b.width)
|
||||||
|
case "description", "d", "desc", "body", "b":
|
||||||
|
body, err := GetAndConfirmCommandLine("New story description: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Body = body
|
||||||
|
s.Updated = time.Now()
|
||||||
|
b.SetMessage("Story body updated", false)
|
||||||
|
s.BuildStorySlice(b.width)
|
||||||
|
case "points", "sp", "pts", "p":
|
||||||
|
if len(args) != 2 {
|
||||||
|
ps, err := GetAndConfirmCommandLine("New story description: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args = append(args, ps)
|
||||||
|
}
|
||||||
|
val, err := strconv.Atoi(args[1])
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Points = val
|
||||||
|
s.Updated = time.Now()
|
||||||
|
b.SetMessage("Story points updated", false)
|
||||||
|
s.BuildStorySlice(b.width)
|
||||||
|
case "user":
|
||||||
|
var users []string
|
||||||
|
if len(args) == 1 {
|
||||||
|
u, err := GetAndConfirmCommandLine("User(s) to add: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
users = strings.Fields(u)
|
||||||
|
} else {
|
||||||
|
users = args[1:]
|
||||||
|
}
|
||||||
|
s.AddRemoveUser(users, b)
|
||||||
|
case "toggle":
|
||||||
|
if len(args) < 2 {
|
||||||
|
n, err := GetAndConfirmCommandLine("Task # to toggle: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args = append(args, n)
|
||||||
|
}
|
||||||
|
num, err := strconv.Atoi(args[1])
|
||||||
|
num -= 1
|
||||||
|
if err != nil || num < 0 || num >= len(s.Tasks) {
|
||||||
|
b.SetMessage("Invalid task number", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Tasks[num].Complete = !s.Tasks[num].Complete
|
||||||
|
b.SetMessage("Task state updated", false)
|
||||||
|
default:
|
||||||
|
b.SetMessage(fmt.Sprintf("Unknown story location %q", args[0]), true)
|
||||||
|
}
|
||||||
|
s.BuildStorySlice(b.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Story) AddRemoveUser(users []string, b *Board) {
|
||||||
|
var found bool
|
||||||
|
for _, user := range users {
|
||||||
|
found = false
|
||||||
|
for i, u := range s.Users {
|
||||||
|
if user == u {
|
||||||
|
found = true
|
||||||
|
s.Users[i] = s.Users[len(s.Users)-1]
|
||||||
|
s.Users = s.Users[:len(s.Users)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
s.Users = append(s.Users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(s.Users)
|
||||||
|
b.SetMessage("Updated user list", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Story) AddTask(b *Board) {
|
||||||
|
body, err := GetAndConfirmCommandLine("New task: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Tasks = append(s.Tasks, Task{body, false})
|
||||||
|
s.BuildStorySlice(b.width)
|
||||||
|
b.SetMessage("Task added", false)
|
||||||
|
}
|
||||||
|
|
||||||
func (s Story) Duplicate() Story {
|
func (s Story) Duplicate() Story {
|
||||||
out := Story{}
|
out := Story{}
|
||||||
out.Title = s.Title
|
out.Title = s.Title
|
||||||
|
|
Loading…
Reference in New Issue