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} }