package output import ( "axolotl/db" "axolotl/models" "encoding/json" "fmt" "io" "sort" "strings" "github.com/fatih/color" ) var ( idColor = color.New(color.FgYellow) titleColor = color.New(color.FgWhite, color.Bold) statusOpen = color.New(color.FgYellow) statusDone = color.New(color.FgHiBlack) prioHigh = color.New(color.FgHiRed) prioMedium = color.New(color.FgYellow) prioLow = color.New(color.FgYellow, color.Faint) tagColor = color.New(color.FgCyan) nsColor = color.New(color.FgYellow) dimColor = color.New(color.FgHiBlack) contentColor = color.New(color.FgWhite) labelColor = color.New(color.FgYellow) typeIssue = color.New(color.FgMagenta) typeNote = color.New(color.FgHiBlue) typeUser = color.New(color.FgHiGreen) typeNs = color.New(color.FgHiYellow) ) type iconSet struct { Issue, Note, User, Namespace string Blocks, Subtask, Related string Assignee, Created string Tag, Calendar string Check, Cross string } var icons = iconSet{ Issue: "\uf188", // bug icon Note: "\uf15c", // file-text User: "\uf007", // user Namespace: "\uf07b", // folder Blocks: "\uf068", // minus Subtask: "\uf0da", // caret-right Related: "\uf0c1", // link Assignee: "\uf007", // user Created: "\uf007", // user Tag: "\uf02b", // tag Calendar: "\uf133", // calendar Check: "\uf00c", // check Cross: "\uf00d", // times } func typeIcon(t string) string { switch t { case "issue": return icons.Issue case "note": return icons.Note case "user": return icons.User case "namespace": return icons.Namespace default: return icons.Issue } } func typeIconColored(t string) string { switch t { case "issue": return typeIssue.Sprint(icons.Issue) case "note": return typeNote.Sprint(icons.Note) case "user": return typeUser.Sprint(icons.User) case "namespace": return typeNs.Sprint(icons.Namespace) default: return typeIssue.Sprint(icons.Issue) } } func statusIcon(s string) string { if s == "done" { return icons.Check } return icons.Cross } func statusIconColored(s string) string { if s == "" { return " " } if s == "done" { return dimColor.Sprint("○") } return dimColor.Sprint("●") } func statusColored(s string) string { if s == "" { return dimColor.Sprint("—") } if s == "done" { return statusDone.Sprint("○ done") } return statusOpen.Sprint("● open") } func prioColored(p string) string { switch p { case "high": return prioHigh.Sprint("● high") case "medium": return prioMedium.Sprint("◆ medium") case "low": return prioLow.Sprint("○ low") default: return dimColor.Sprint("—") } } func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error { if jsonOut { return json.NewEncoder(w).Encode(nodes) } fmt.Fprintln(w) if len(nodes) == 0 { fmt.Fprintln(w, dimColor.Sprint(" No results.")) return nil } sort.Slice(nodes, func(i, j int) bool { si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status") if si != sj { return statusRank(si) > statusRank(sj) } pi, pj := nodes[i].GetProperty("prio"), nodes[j].GetProperty("prio") return prioRank(pi) > prioRank(pj) }) for _, n := range nodes { tags := getDisplayTags(n) nodeType := n.GetType() var typeAndStatus string if nodeType == "issue" { typeAndStatus = statusIconColored(n.GetProperty("status")) } else { typeAndStatus = typeIconColored(nodeType) + " " + statusIconColored(n.GetProperty("status")) } fmt.Fprintf(w, " %s %s %s %s %s", idColor.Sprintf("%s", n.ID), prioColoredShort(n.GetProperty("prio")), typeAndStatus, titleColor.Sprint(truncate(n.Title, 35)), dimColor.Sprint("["+n.GetProperty("namespace")+"]"), ) if len(tags) > 0 { var hashTags []string for _, t := range tags { hashTags = append(hashTags, "#"+t) } fmt.Fprintf(w, " %s", tagColor.Sprint(strings.Join(hashTags, " "))) } fmt.Fprintln(w) } fmt.Fprintln(w) return nil } func prioColoredShort(p string) string { switch p { case "high": return prioHigh.Sprint("\uf0e7 ") case "medium": return prioMedium.Sprint("\uf12a ") case "low": return prioLow.Sprint("\uf068 ") default: return " " } } func prioRank(p string) int { switch p { case "high": return 3 case "medium": return 2 case "low": return 1 default: return 0 } } func statusRank(s string) int { switch s { case "open": return 2 case "done": return 0 default: return 1 } } func getDisplayTags(n *models.Node) []string { var tags []string for _, t := range n.Tags { if _, _, ok := models.ParseTag(t); !ok { tags = append(tags, t) } } return tags } func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error { if jsonOut { return json.NewEncoder(w).Encode(n) } icon := typeIcon(n.GetType()) nodeType := strings.Title(n.GetType()) fmt.Fprintln(w) fmt.Fprintf(w, " %s %s %s %s\n", icon, idColor.Sprint(n.ID), titleColor.Sprint(n.Title), dimColor.Sprint("["+nodeType+"]"), ) fmt.Fprintln(w, dimColor.Sprint(" ───────────────────────────")) fmt.Fprintf(w, " Status: %s\n", statusColored(n.GetProperty("status"))) fmt.Fprintf(w, " Priority: %s\n", prioColored(n.GetProperty("prio"))) fmt.Fprintf(w, " Namespace: %s\n", nsColor.Sprint(n.GetProperty("namespace"))) if n.DueDate != "" { fmt.Fprintf(w, " Due: %s %s\n", icons.Calendar, n.DueDate) } fmt.Fprintf(w, " Created: %s\n", dimColor.Sprint(n.CreatedAt)) if n.Content != "" { fmt.Fprintln(w) fmt.Fprintln(w, labelColor.Sprint(" Content:")) for _, line := range strings.Split(n.Content, "\n") { fmt.Fprintf(w, dimColor.Sprint(" │ ")+contentColor.Sprint("%s\n"), line) } } if len(n.Tags) > 0 { var tags []string for _, t := range n.Tags { if _, _, ok := models.ParseTag(t); !ok { tags = append(tags, t) } } if len(tags) > 0 { fmt.Fprintln(w) fmt.Fprintf(w, " Tags: %s\n", tagColor.Sprint(strings.Join(tags, " • "))) } } if len(n.Relations) > 0 { fmt.Fprintln(w) fmt.Fprintln(w, labelColor.Sprint(" Relations:")) for relType, ids := range n.Relations { if relType == "created" { continue } relIcon := "" switch relType { case "blocks": relIcon = icons.Blocks case "subtask": relIcon = icons.Subtask case "related": relIcon = icons.Related case "assignee": relIcon = icons.Assignee } coloredIDs := make([]string, len(ids)) for i, id := range ids { coloredIDs[i] = idColor.Sprint(id) } fmt.Fprintf(w, " %s %s %s\n", relIcon, strings.Title(string(relType)), strings.Join(coloredIDs, " ")) } } fmt.Fprintln(w) return nil } func PrintAliases(w io.Writer, aliases []*db.Alias, jsonOut bool) error { if jsonOut { return json.NewEncoder(w).Encode(aliases) } if len(aliases) == 0 { fmt.Fprintln(w, dimColor.Sprint(" No aliases defined.")) return nil } fmt.Fprintln(w) for _, a := range aliases { fmt.Fprintf(w, " %s %s\n", idColor.Sprint(a.Name), dimColor.Sprint(a.Command), ) } fmt.Fprintln(w) return nil } func PrintSuccess(w io.Writer, format string, args ...interface{}) { fmt.Fprintf(w, icons.Check+" "+format+"\n", args...) } func PrintDeleted(w io.Writer, id string) { fmt.Fprintf(w, " "+icons.Cross+" Deleted %s\n", idColor.Sprint(id)) } func PrintCreated(w io.Writer, dbPath string) { fmt.Fprintf(w, " "+icons.Namespace+" Created %s\n", dimColor.Sprint(dbPath)) } func truncate(s string, max int) string { if len(s) <= max { return s } return s[:max-1] + "…" }