311 lines
7.2 KiB
Go
311 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os/user"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Story struct {
|
|
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)
|
|
var ch rune
|
|
|
|
for {
|
|
s.Draw(b)
|
|
ch = Getch()
|
|
|
|
switch ch {
|
|
case 'j', 'k', 'g', 'G':
|
|
b.ClearMessage()
|
|
s.Scroll(ch, b)
|
|
case ':':
|
|
b.EnterCommand()
|
|
case 'h':
|
|
s.offset = 0
|
|
b.ClearMessage()
|
|
return
|
|
case 'Q':
|
|
Quit()
|
|
case 'c', 'C':
|
|
s.AddComment(b)
|
|
case 't':
|
|
s.AddTask(b)
|
|
case 'd':
|
|
s.Update([]string{"description"}, b)
|
|
case 'T':
|
|
s.Update([]string{"title"}, b)
|
|
case 'u':
|
|
s.Update([]string{"user"}, b)
|
|
case 'p':
|
|
s.Update([]string{"points"}, b)
|
|
case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0':
|
|
call := []string{"toggle", string(ch)}
|
|
if ch == '0' {
|
|
call[1] = "10"
|
|
}
|
|
s.Update(call, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Story) AddComment(b *Board) {
|
|
comment, err := GetAndConfirmCommandLine("Comment: ")
|
|
if err != nil {
|
|
b.SetMessage(err.Error(), true)
|
|
return
|
|
}
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
b.SetMessage(err.Error(), true)
|
|
return
|
|
}
|
|
s.Comments = append(s.Comments, Comment{u.Name, comment, time.Now()})
|
|
s.BuildStorySlice(b.width)
|
|
}
|
|
|
|
func (s *Story) BuildStorySlice(width int) {
|
|
var out strings.Builder
|
|
out.WriteRune('\n')
|
|
out.WriteString(WrapText(s.Title, width-7))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\n')
|
|
out.WriteString("Updated: ")
|
|
out.WriteString(s.Updated.Format(time.UnixDate))
|
|
pts := s.Points
|
|
if pts < 1 {
|
|
out.WriteString("\nPoints: -\n\n")
|
|
} else {
|
|
out.WriteString(fmt.Sprintf("\nPoints: %d\n\n", s.Points))
|
|
}
|
|
out.WriteString("Users: ")
|
|
out.WriteString(WrapText(strings.Join(s.Users, ", "), width-7))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\n')
|
|
out.WriteString("Description:\n\n")
|
|
out.WriteString(WrapText(s.Body, width))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\n')
|
|
for i, task := range s.Tasks {
|
|
if task.Complete {
|
|
out.WriteString("✔ ")
|
|
} else {
|
|
out.WriteString(" ")
|
|
}
|
|
out.WriteString(fmt.Sprintf("%2d. ", i+1))
|
|
out.WriteString(WrapText(task.Body, width-6))
|
|
out.WriteRune('\n')
|
|
}
|
|
out.WriteRune('\n')
|
|
out.WriteString(strings.Repeat("━", width))
|
|
out.WriteString("\n\nComments:\n\n")
|
|
for _, c := range s.Comments {
|
|
out.WriteString(c.User)
|
|
out.WriteRune('\n')
|
|
out.WriteString(WrapText(c.Created.Format(time.UnixDate), width))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\n')
|
|
out.WriteString(WrapText(c.Body, width))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\n')
|
|
out.WriteString(strings.Repeat("╍", width))
|
|
out.WriteRune('\n')
|
|
out.WriteRune('\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, ""))
|
|
} else {
|
|
out.WriteString(fmt.Sprintf("%-*.*s\n", b.width, b.width, s.stSlice[index]))
|
|
}
|
|
}
|
|
out.WriteString(b.PrintInputArea())
|
|
out.WriteString(b.PrintMessage())
|
|
fmt.Print(out.String())
|
|
}
|
|
|
|
func (s *Story) Scroll(dir rune, b *Board) {
|
|
switch dir {
|
|
case 'j':
|
|
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
|
|
} else {
|
|
b.SetMessage("Cannot move further up", true)
|
|
}
|
|
case 'g':
|
|
if s.offset > 0 {
|
|
s.offset = 0
|
|
} else {
|
|
b.SetMessage("Cannot move further up", true)
|
|
}
|
|
case 'G':
|
|
if s.offset + b.height - 3 < len(s.stSlice) {
|
|
s.offset = len(s.stSlice) - b.height + 3
|
|
} else {
|
|
b.SetMessage("Cannot move further down", true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Story) Update(args []string, b *Board) {
|
|
location := strings.ToLower(args[0])
|
|
switch location {
|
|
case "title":
|
|
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 := GetCommandLine("Set story points: ")
|
|
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", "u":
|
|
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", "t":
|
|
if len(args) < 2 {
|
|
n, err := GetCommandLine("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
|
|
s.Updated = time.Now()
|
|
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)
|
|
s.Updated = time.Now()
|
|
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.Updated = time.Now()
|
|
s.BuildStorySlice(b.width)
|
|
b.SetMessage("Task added", false)
|
|
}
|
|
|
|
func (s Story) Duplicate() Story {
|
|
out := Story{}
|
|
out.Title = s.Title
|
|
out.Body = s.Body
|
|
out.Users = make([]string, len(s.Users))
|
|
copy(out.Users, s.Users)
|
|
out.Tag = s.Tag
|
|
out.Tasks = make([]Task, len(s.Tasks))
|
|
copy(out.Tasks, s.Tasks)
|
|
out.Comments = make([]Comment, len(s.Comments))
|
|
copy(out.Comments, s.Comments)
|
|
out.Created = s.Created
|
|
out.Updated = s.Updated
|
|
out.Points = s.Points
|
|
return out
|
|
}
|
|
|
|
func MakeStory(title string) Story {
|
|
return Story{title,"", make([]string,0,2), -1, make([]Task,0,2), make([]Comment,0,2), time.Now(), time.Now(), 0, []string{}, -1}
|
|
}
|