|
@@ -0,0 +1,333 @@
|
|
|
+// localizing all of the functions required to construct the user interface
|
|
|
+
|
|
|
+package itashi
|
|
|
+
|
|
|
+import (
|
|
|
+ "bufio"
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "log"
|
|
|
+ "math"
|
|
|
+ "os"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "text/template"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ tea "github.com/charmbracelet/bubbletea"
|
|
|
+)
|
|
|
+
|
|
|
+const SPRING_EQUINOX = 81
|
|
|
+const SUMMER_SOLSTICE = 173
|
|
|
+const AUTUMN_EQUINOX = 265
|
|
|
+const WINTER_SOLSTICE = 356
|
|
|
+
|
|
|
+var Quarters = []int{
|
|
|
+ SPRING_EQUINOX,
|
|
|
+ SUMMER_SOLSTICE,
|
|
|
+ AUTUMN_EQUINOX,
|
|
|
+ WINTER_SOLSTICE,
|
|
|
+}
|
|
|
+
|
|
|
+const HEADER_TEMPLATE = `
|
|
|
+{{.Date}} {{.Season}}, {{.DaysToQuarter}} days until the next {{.QuarterType}}.
|
|
|
+{{.DayOfWeek}}
|
|
|
+{{.Time}} {{.Meridiem}} ({{.TtEod.Hours}}H, {{.TtEod.Minutes}}M -> EoD, {{.TtSun.Hours}}H, {{.TtSun.Minutes}}M -> {{.SunCycle}})
|
|
|
+`
|
|
|
+
|
|
|
+const TASK_ITEM = `
|
|
|
++------------------------------------
|
|
|
+| Task ID: {{.Id}}
|
|
|
+| Title: {{.Title}}|
|
|
|
+| {{.Desc}}|
|
|
|
+| Due: {{.Due}}
|
|
|
+| Priority: {{.Priority}}
|
|
|
+| Done: {{.Done}}
|
|
|
+|
|
|
|
++---------------------------
|
|
|
+`
|
|
|
+
|
|
|
+const TIME_TO_TEMPLATE = `{{.Hours}}H, {{.Minutes}}M`
|
|
|
+
|
|
|
+// TODO: put all templates in their own file
|
|
|
+
|
|
|
+type HeaderData struct {
|
|
|
+ Date string
|
|
|
+ Season string
|
|
|
+ DaysToQuarter int
|
|
|
+ QuarterType string
|
|
|
+ DayOfWeek string
|
|
|
+ Time string
|
|
|
+ Meridiem string
|
|
|
+ TtEod TimeToSunShift
|
|
|
+ TtSun TimeToSunShift
|
|
|
+ SunCycle string
|
|
|
+}
|
|
|
+
|
|
|
+type TimeToSunShift struct {
|
|
|
+ Hours int
|
|
|
+ Minutes int
|
|
|
+}
|
|
|
+
|
|
|
+type UserDetails interface {
|
|
|
+ daysToQuarter(day int) int
|
|
|
+ getQuarterType(day int) string
|
|
|
+ getSeason(day int) string
|
|
|
+ getTime(ts time.Time) string
|
|
|
+ getDate(ts time.Time) string
|
|
|
+ getMeridiem(ts time.Time) string
|
|
|
+ getTimeToEod(ts time.Time) TimeToSunShift
|
|
|
+ getTimeToSunShift(ts time.Time) TimeToSunShift
|
|
|
+ getSunCycle(ts time.Time) string
|
|
|
+}
|
|
|
+
|
|
|
+type UserImplementation struct{}
|
|
|
+
|
|
|
+func (u UserImplementation) daysToQuarter(day int) int {
|
|
|
+ season := u.getSeason(day)
|
|
|
+ if season == "Spring" {
|
|
|
+ return SUMMER_SOLSTICE - day
|
|
|
+ }
|
|
|
+
|
|
|
+ return 1
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+Return the quarter (solstice/equinox). We have to remember that we are returning
|
|
|
+the NEXT season type, i.e. if its currently spring, the next quarter (summer) will have a solstice
|
|
|
+
|
|
|
+ :param day: the numerical day of the year
|
|
|
+ :returns: either solstice, or equinox
|
|
|
+*/
|
|
|
+func (u UserImplementation) getQuarterType(day int) string {
|
|
|
+ season := u.getSeason(day)
|
|
|
+ if season == "Winter" {
|
|
|
+ return "Equinox"
|
|
|
+ }
|
|
|
+ if season == "Summer" {
|
|
|
+ return "Equinox"
|
|
|
+ }
|
|
|
+ return "Solstice"
|
|
|
+}
|
|
|
+func (u UserImplementation) getSeason(day int) string {
|
|
|
+ if day > 365 {
|
|
|
+ return "[REDACTED]"
|
|
|
+ }
|
|
|
+ if day < 0 {
|
|
|
+ return "[REDACTED]"
|
|
|
+ }
|
|
|
+ if day > 0 && day < SPRING_EQUINOX {
|
|
|
+ return "Winter"
|
|
|
+ }
|
|
|
+ if day > SPRING_EQUINOX && day < SUMMER_SOLSTICE {
|
|
|
+ return "Spring"
|
|
|
+ }
|
|
|
+ if day > SUMMER_SOLSTICE && day < AUTUMN_EQUINOX {
|
|
|
+ return "Summer"
|
|
|
+ }
|
|
|
+ if day > AUTUMN_EQUINOX && day < WINTER_SOLSTICE {
|
|
|
+ return "Autumn"
|
|
|
+ }
|
|
|
+ if day > WINTER_SOLSTICE && day < 365 {
|
|
|
+ return "Winter"
|
|
|
+ }
|
|
|
+ return "idk bruh"
|
|
|
+
|
|
|
+}
|
|
|
+func (u UserImplementation) getMeridiem(ts time.Time) string {
|
|
|
+ if ts.Hour() < 12 {
|
|
|
+ return "AM"
|
|
|
+ }
|
|
|
+ if ts.Hour() >= 12 {
|
|
|
+ return "PM"
|
|
|
+ }
|
|
|
+ return "idk bruh"
|
|
|
+}
|
|
|
+func (u UserImplementation) getTimeToEod(ts time.Time) TimeToSunShift {
|
|
|
+ if ts.Hour() > 17 {
|
|
|
+ return TimeToSunShift{Hours: 0, Minutes: 0}
|
|
|
+ }
|
|
|
+ out := time.Date(ts.Year(), ts.Month(), ts.Day(), 17, 0, ts.Second(), ts.Nanosecond(), ts.Location())
|
|
|
+ dur := time.Until(out)
|
|
|
+ hours := dur.Minutes() / 60
|
|
|
+ hours = math.Floor(hours)
|
|
|
+ minutes := dur.Minutes() - (hours * 60)
|
|
|
+
|
|
|
+ return TimeToSunShift{Hours: int(hours), Minutes: int(minutes)}
|
|
|
+}
|
|
|
+
|
|
|
+func (u UserImplementation) getTimeToSunShift(ts time.Time) TimeToSunShift {
|
|
|
+ return TimeToSunShift{}
|
|
|
+}
|
|
|
+
|
|
|
+func (u UserImplementation) getSunCycle(ts time.Time) string {
|
|
|
+ return "☼"
|
|
|
+}
|
|
|
+func (u UserImplementation) getTime(ts time.Time) string {
|
|
|
+ var hour int
|
|
|
+ if ts.Hour() == 0 {
|
|
|
+ hour = 12
|
|
|
+ } else {
|
|
|
+ hour = ts.Hour()
|
|
|
+ }
|
|
|
+ return fmt.Sprintf("%v:%v", hour, ts.Minute())
|
|
|
+}
|
|
|
+func (u UserImplementation) getDate(ts time.Time) string {
|
|
|
+ return fmt.Sprintf("%s %v, %v", ts.Month().String(), ts.Day(), ts.Year())
|
|
|
+}
|
|
|
+
|
|
|
+// Format the header string with a template
|
|
|
+func getHeader(ud UserDetails, tmpl *template.Template) string {
|
|
|
+ rn := time.Now()
|
|
|
+ header := HeaderData{
|
|
|
+ Date: ud.getDate(rn),
|
|
|
+ Season: ud.getSeason(rn.YearDay()),
|
|
|
+ DaysToQuarter: ud.daysToQuarter(rn.YearDay()),
|
|
|
+ QuarterType: ud.getQuarterType(rn.YearDay()),
|
|
|
+ DayOfWeek: rn.Weekday().String(),
|
|
|
+ Time: ud.getTime(rn),
|
|
|
+ Meridiem: ud.getMeridiem(rn),
|
|
|
+ TtEod: ud.getTimeToEod(rn),
|
|
|
+ TtSun: ud.getTimeToSunShift(rn),
|
|
|
+ SunCycle: ud.getSunCycle(rn),
|
|
|
+ }
|
|
|
+ var bw bytes.Buffer
|
|
|
+ err := tmpl.Execute(&bw, header)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal("There was an issue parsing the header. sorry, ", err)
|
|
|
+ }
|
|
|
+ return bw.String()
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+type model struct {
|
|
|
+ choices []string
|
|
|
+ cursor int
|
|
|
+ selected map[int]struct{}
|
|
|
+}
|
|
|
+
|
|
|
+func InitialModel() model {
|
|
|
+ shelf := NewFilesystemShelf(GetDefualtSave())
|
|
|
+ return model{
|
|
|
+ choices: GetTaskNames(shelf.GetAll()),
|
|
|
+ selected: make(map[int]struct{}),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m model) Init() tea.Cmd {
|
|
|
+ // Just return `nil`, which means "no I/O right now, please."
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
+ switch msg := msg.(type) {
|
|
|
+
|
|
|
+ // Is it a key press?
|
|
|
+ case tea.KeyMsg:
|
|
|
+
|
|
|
+ // Cool, what was the actual key pressed?
|
|
|
+ switch msg.String() {
|
|
|
+
|
|
|
+ // These keys should exit the program.
|
|
|
+ case "ctrl+c", "q":
|
|
|
+ return m, tea.Quit
|
|
|
+
|
|
|
+ // The "up" and "k" keys move the cursor up
|
|
|
+ case "up", "k":
|
|
|
+ if m.cursor > 0 {
|
|
|
+ m.cursor--
|
|
|
+ }
|
|
|
+
|
|
|
+ // The "down" and "j" keys move the cursor down
|
|
|
+ case "down", "j":
|
|
|
+ if m.cursor < len(m.choices)-1 {
|
|
|
+ m.cursor++
|
|
|
+ }
|
|
|
+
|
|
|
+ // The "enter" key and the spacebar (a literal space) toggle
|
|
|
+ // the selected state for the item that the cursor is pointing at.
|
|
|
+ case "enter", " ":
|
|
|
+ _, ok := m.selected[m.cursor]
|
|
|
+ if ok {
|
|
|
+ delete(m.selected, m.cursor)
|
|
|
+ } else {
|
|
|
+ m.selected[m.cursor] = struct{}{}
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Return the updated model to the Bubble Tea runtime for processing.
|
|
|
+ // Note that we're not returning a command.
|
|
|
+ return m, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m model) View() string {
|
|
|
+ // The header
|
|
|
+ tmpl, err := template.New("header").Parse(HEADER_TEMPLATE)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal("Couldnt parse the header template.. sorry. ", err)
|
|
|
+ }
|
|
|
+ shelf := NewFilesystemShelf(GetDefualtSave())
|
|
|
+
|
|
|
+ s := getHeader(UserImplementation{}, tmpl)
|
|
|
+
|
|
|
+ // Iterate over our choices
|
|
|
+ for i, choice := range m.choices {
|
|
|
+
|
|
|
+ // Is the cursor pointing at this choice?
|
|
|
+ cursor := " " // no cursor
|
|
|
+ if m.cursor == i {
|
|
|
+ cursor = ">" // cursor!
|
|
|
+ }
|
|
|
+
|
|
|
+ // Is this choice selected?
|
|
|
+ var taskrender string
|
|
|
+ checked := " " // not selected
|
|
|
+ if _, ok := m.selected[i]; ok {
|
|
|
+ for x := range shelf.Tasks {
|
|
|
+ if shelf.Tasks[x].Title == choice {
|
|
|
+ taskrender = shelf.RenderTask(shelf.Tasks[x])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ checked = "x" // selected!
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Render the row
|
|
|
+ s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
|
|
|
+ s += taskrender
|
|
|
+ }
|
|
|
+
|
|
|
+ // The footer
|
|
|
+ s += "\nPress q to quit.\n"
|
|
|
+
|
|
|
+ // Send the UI for rendering
|
|
|
+ return s
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+Add task to the shelf
|
|
|
+*/
|
|
|
+func AddTaskPrompt(shelf TaskShelf) {
|
|
|
+ var title string
|
|
|
+ var desc string
|
|
|
+ var priority string
|
|
|
+ var due time.Time
|
|
|
+
|
|
|
+ var reader *bufio.Reader
|
|
|
+ reader = bufio.NewReader(os.Stdout)
|
|
|
+ fmt.Print("Enter Task Title: ")
|
|
|
+ title, _ = reader.ReadString('\n')
|
|
|
+ fmt.Print("Task description: ")
|
|
|
+ desc, _ = reader.ReadString('\n')
|
|
|
+ fmt.Print("Priority: ")
|
|
|
+ priority, _ = reader.ReadString('\n')
|
|
|
+ priorityInt, err := strconv.Atoi(strings.TrimSpace(priority))
|
|
|
+ if err != nil {
|
|
|
+ fmt.Print("We couldnt parse the priority value given. :(\n")
|
|
|
+ }
|
|
|
+
|
|
|
+ shelf.AddTask(title, desc, priorityInt, due)
|
|
|
+}
|