2
1
Fork 0

More or less working board

This commit is contained in:
sloum 2021-03-24 23:22:54 -07:00
parent 401526bccc
commit 1d44dc779e
7 changed files with 326 additions and 99 deletions

138
board.go
View File

@ -1,10 +1,11 @@
package main
import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"time"
"tildegit.org/sloum/swim/termios"
)
@ -17,23 +18,22 @@ const (
)
type Board struct {
Title string
Body string
Created time.Time
Lanes []Lane
Current int // Index of current lane
LaneOff int
Message string
MsgErr bool
Width int
Height int
Zoom int
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"`
}
func (b *Board) Run() {
defer termios.Restore()
termios.SetCharMode()
fmt.Print("\033[?25l")
var ch rune
for {
@ -54,8 +54,6 @@ func (b *Board) Run() {
b.Move(ch)
case 'D':
// Delete current story
case 'e':
// Edit current story
case ':':
b.EnterCommand()
case '+':
@ -63,19 +61,19 @@ func (b *Board) Run() {
case '-':
b.ZoomOut()
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() {
b.Message = ""
b.MsgErr = false
b.message = ""
b.msgErr = false
}
func (b *Board) SetMessage(msg string, isError bool) {
b.Message = msg
b.MsgErr = isError
b.message = msg
b.msgErr = isError
}
func (b *Board) CreateLane() {
@ -101,30 +99,30 @@ func (b *Board) CreateStory() {
}
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 {
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 {
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
if i < len(b.Lanes) {
s = b.Lanes[i].StringSlice(width, i == b.Current)
} else {
s = make([]string, 0)
}
laneText[i-b.LaneOff] = s
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++ {
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 {
@ -140,13 +138,13 @@ func (b Board) LaneHeaderRow(width, pad int) string {
func (b Board) PrintLanes() string {
var out strings.Builder
laneWidth := b.Width / b.Zoom
laneWidth := b.width / b.Zoom
laneSlices := b.GetLaneSlices(laneWidth)
pad := b.Width - laneWidth * b.Zoom
pad := b.width - laneWidth * b.Zoom
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 {
if row < len(l) {
out.WriteString(l[row])
@ -164,25 +162,52 @@ func (b Board) PrintLanes() 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(":")
command, err := GetCommandLine(":")
if err != nil {
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 {
var out strings.Builder
if b.MsgErr {
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))
out.WriteString(fmt.Sprintf("%-*.*s%s", b.width, b.width, b.message, styleOff))
return out.String()
}
@ -193,7 +218,7 @@ func (b *Board) ZoomIn() {
}
func (b *Board) ZoomOut() {
if b.Width / (b.Zoom+1) > 15 {
if b.width / (b.Zoom+1) > 15 {
b.Zoom += 1
}
}
@ -207,8 +232,8 @@ func (b *Board) Move(ch rune) {
// move left a lane
if b.Current > 0 {
b.Current -= 1
if b.Current < b.LaneOff {
b.LaneOff -= 1
if b.Current < b.laneOff {
b.laneOff -= 1
}
} else {
b.SetMessage("Cannot move further left", true)
@ -217,8 +242,8 @@ func (b *Board) Move(ch rune) {
// 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
if b.Current > b.laneOff + b.Zoom - 1 {
b.laneOff += 1
}
} else {
b.SetMessage("Cannot move further right", true)
@ -227,8 +252,8 @@ func (b *Board) Move(ch rune) {
// 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
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)
@ -237,8 +262,8 @@ func (b *Board) Move(ch rune) {
// 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
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)
@ -328,3 +353,28 @@ func (b Board) Draw() {
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)
}

View File

@ -22,7 +22,7 @@ var style Styles
var colors = map[int]map[string]string{
SimpleColor: map[string]string{
"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
"Lane": "\033[30;104m", // black on bright blue
"LaneSelected": "\033[30;103m", // black on bright yellow

View File

@ -5,7 +5,7 @@ import (
)
type Comment struct {
User string
Body string
Created time.Time
User string `json:"CommentUser"`
Body string `json:"CommentBody"`
Created time.Time `json:"CommentCreated"`
}

29
lane.go
View File

@ -2,13 +2,14 @@ package main
import (
"fmt"
"strings"
)
type Lane struct {
Title string
Stories []Story
Current int // Index of current story
StoryOff int // offset for the lane slice
Title string `json:"LaneTitle"`
Stories []Story `json:"Stories"`
Current int `json:"CurrentStory"` // Index of current story
storyOff int // offset for the lane slice
}
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 {
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 := " "
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%-*.*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
}
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 {
return Lane{title, make([]Story,0,5), -1, 0}
}

73
main.go
View File

@ -2,14 +2,30 @@ package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"tildegit.org/sloum/swim/termios"
"time"
)
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 {
reader := bufio.NewReader(os.Stdin)
@ -53,6 +69,9 @@ func GetAndConfirmCommandLine(prefix string) (string, error) {
if err != nil {
return line, err
}
if line == "" {
return line, fmt.Errorf("Cancelled input")
}
VerifyQuery: fmt.Print(upAndLeft) // Move up one and over all
fmt.Print(style.Input)
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)
continue
} else if conf == 'c' {
err = fmt.Errorf("Cancelled")
err = fmt.Errorf("Cancelled input")
break
} else {
fmt.Print(cursorEnd)
@ -100,25 +119,49 @@ func WrapText(text string, lineWidth int) string {
func Quit() {
termios.Restore()
fmt.Print("\033[?25h")
fmt.Print(cursorEnd)
fmt.Print("\033[0m\n\033[?25h")
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() {
flag.Parse()
args := flag.Args()
style.Init(SimpleColor)
cols, rows := termios.GetWindowSize()
board = Board{
Title: "My Test Board",
Body: "Some misc info",
Created: time.Now(),
Lanes: make([]Lane, 0, 1),
Current: -1,
LaneOff: 0,
Message: "Welcome to SWIM",
MsgErr: false,
Width: cols,
Height: rows,
Zoom: 3}
if len(args) > 0 {
LoadFile(args[0], cols, rows)
} else {
fp = ""
board = Board{
Title: "My Test Board",
Lanes: make([]Lane, 0, 1),
Current: -1,
laneOff: 0,
message: "Welcome to SWIM",
msgErr: false,
width: cols,
height: rows,
Zoom: 3}
}
board.Run()
}

173
story.go
View File

@ -3,26 +3,28 @@ package main
import (
"fmt"
"os/user"
"sort"
"strconv"
"strings"
"time"
)
type Story struct {
Title string
Body string
Users []string
Tag int
Tasks []Task
Comments []Comment
Created time.Time
Updated time.Time
Offset int
StSlice []string
Points int
Title string `json:"StoryTitle"`
Body string `json:"StoryBody"`
Users []string `json:"StoryUsers"`
Tag int `json:"StoryTag"`
Tasks []Task `json:"StoryTasks"`
Comments []Comment `json:"StoryComments"`
Created time.Time `json:"StoryCreated"`
Updated time.Time `json:"StoryUpdated"`
offset int
stSlice []string
Points int `json:"StoryPoints"`
}
func (s *Story) View(b *Board) {
s.BuildStorySlice(b.Width)
s.BuildStorySlice(b.width)
var ch rune
for {
s.Draw(b)
@ -30,16 +32,23 @@ func (s *Story) View(b *Board) {
switch ch {
case 'j', 'k':
b.ClearMessage()
s.Scroll(ch, b)
case ':':
b.EnterCommand()
case 'x':
s.Offset = 0
case 'h':
s.offset = 0
return
case 'Q':
Quit()
case 'c', 'C':
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
}
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) {
@ -82,12 +91,12 @@ func (s *Story) BuildStorySlice(width int) {
out.WriteRune('\n')
out.WriteRune('\n')
for i, task := range s.Tasks {
out.WriteString(fmt.Sprintf("%2d. ", i+1))
if task.Complete {
out.WriteString(" ")
out.WriteString(" ")
} else {
out.WriteString(" ")
out.WriteString(" ")
}
out.WriteString(fmt.Sprintf("%2d. ", i+1))
out.WriteString(WrapText(task.Body, width-6))
out.WriteRune('\n')
}
@ -107,19 +116,19 @@ func (s *Story) BuildStorySlice(width int) {
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) {
var out strings.Builder
out.WriteString(cursorHome)
out.WriteString(b.PrintHeader())
for i := 0; i < b.Height-3; i++ {
index := i+s.Offset
if index >= len(s.StSlice) {
out.WriteString(fmt.Sprintf("%*.*s\n", b.Width, b.Width, ""))
for i := 0; i < b.height-3; i++ {
index := i+s.offset
if index >= len(s.stSlice) {
out.WriteString(fmt.Sprintf("%*.*s\n", b.width, b.width, ""))
} 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())
@ -130,20 +139,128 @@ func (s Story) Draw(b *Board) {
func (s *Story) Scroll(dir rune, b *Board) {
switch dir {
case 'j':
if s.Offset + b.Height - 3 < len(s.StSlice) {
s.Offset += 1
if s.offset + b.height - 3 < len(s.stSlice) {
s.offset += 1
} else {
b.SetMessage("Cannot move further down", true)
}
case 'k':
if s.Offset > 0 {
s.Offset -= 1
if s.offset > 0 {
s.offset -= 1
} else {
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 {
out := Story{}
out.Title = s.Title

View File

@ -1,7 +1,7 @@
package main
type Task struct {
Body string
Complete bool
Body string `json:"TaskBody"`
Complete bool `json:"TaskComplete"`
}