2
1
Fork 0
swim/story.go

379 lines
8.7 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 'D':
s.DeleteTask("", 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(comment string, b *Board) {
if comment == "" {
comment = GetEditableLine("Comment: ", "")
if comment == "" {
b.SetMessage("Comment canceled", 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)
unsavedChanges = true
}
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])
var stringVal string
if len(args) > 1 {
stringVal = strings.Join(args[1:], " ")
}
switch location {
case "title":
if stringVal == "" {
stringVal = GetEditableLine("Story title: ", s.Title)
if stringVal == "" {
b.SetMessage("Canceled story title update", true)
}
}
s.Title = stringVal
s.Updated = time.Now()
b.SetMessage("Story title updated", false)
s.BuildStorySlice(b.width)
case "description", "d", "desc", "body", "b":
if stringVal == "" {
stringVal = GetEditableLine("Story description: ", s.Body)
if stringVal == "" {
b.SetMessage("Canceled description update", true)
}
}
s.Body = stringVal
s.Updated = time.Now()
b.SetMessage("Story body updated", false)
s.BuildStorySlice(b.width)
case "points", "sp", "pts", "p":
if len(args) != 2 {
current := strconv.Itoa(s.Points)
if s.Points < 1 {
current = "-"
}
ps := GetEditableLine("Set story points: ", current)
if ps == "" {
b.SetMessage("Canceled setting points", 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 := GetEditableLine("User(s) to toggle: ", "")
if u == "" {
b.SetMessage("Canceled user toggle", true)
return
}
users = strings.Fields(u)
} else {
users = args[1:]
}
s.AddRemoveUser(users, b)
case "toggle", "t":
if len(args) < 2 {
n := GetEditableLine("Task # to toggle: ", "")
if n == "" {
b.SetMessage("Canceled task toggle", true)
return
}
args = append(args, n)
}
num, err := strconv.Atoi(args[1])
if err != nil || num < 1 || num > len(s.Tasks) {
b.SetMessage("Invalid task number", true)
return
}
num -= 1
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)
return
}
unsavedChanges = 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()
unsavedChanges = true
b.SetMessage("Updated user list", false)
}
func (s *Story) AddTask(body string, b *Board) {
if body == "" {
body = GetEditableLine("New task: ", "")
}
s.Tasks = append(s.Tasks, Task{body, false})
s.Updated = time.Now()
s.BuildStorySlice(b.width)
unsavedChanges = true
b.SetMessage("Task added", false)
}
func (s *Story) DeleteTask(id string, b *Board) {
var err error
if id == "" {
id = GetEditableLine("Task # to delete: ", "")
if id == "" {
b.SetMessage("Canceled task deletion", true)
return
}
}
num, err := strconv.Atoi(id)
if err != nil || num < 1 || num > len(s.Tasks) {
b.SetMessage("Invalid task number", true)
return
}
num -= 1
var cont bool
cont, err = GetConfirmation("Are you sure? Type 'yes' to delete: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
} else if !cont {
b.SetMessage("Deletion canceled", true)
return
}
if num == len(s.Tasks)-1 {
s.Tasks = s.Tasks[:len(s.Tasks)-1]
} else {
s.Tasks = append(s.Tasks[:num], s.Tasks[num+1:]...)
}
s.Updated = time.Now()
s.BuildStorySlice(b.width)
unsavedChanges = true
b.SetMessage("Task deleted", false)
}
func (s Story) GetUserAbbrString() string {
var out strings.Builder
for i := range s.Users {
if len(s.Users[i]) > 1 {
out.WriteString(strings.ToUpper(s.Users[i][:2]))
} else {
out.WriteString(strings.ToUpper(string(s.Users[i][0])))
}
if i < len(s.Users)-1 {
out.WriteString(", ")
}
}
return out.String()
}
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}
}