userinterface.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // localizing all of the functions required to construct the user interface
  2. package itashi
  3. import (
  4. "bufio"
  5. "bytes"
  6. "fmt"
  7. "log"
  8. "math"
  9. "os"
  10. "strconv"
  11. "text/template"
  12. "time"
  13. tea "github.com/charmbracelet/bubbletea"
  14. )
  15. const SPRING_EQUINOX = 81
  16. const SUMMER_SOLSTICE = 173
  17. const AUTUMN_EQUINOX = 265
  18. const WINTER_SOLSTICE = 356
  19. var Quarters = []int{
  20. SPRING_EQUINOX,
  21. SUMMER_SOLSTICE,
  22. AUTUMN_EQUINOX,
  23. WINTER_SOLSTICE,
  24. }
  25. const HEADER_TEMPLATE = `
  26. {{.Date}} {{.Season}}, {{.DaysToQuarter}} days until the next {{.QuarterType}}.
  27. {{.DayOfWeek}}
  28. {{.Time}} {{.Meridiem}} ({{.TtEod.Hours}}H, {{.TtEod.Minutes}}M -> EoD, {{.TtSun.Hours}}H, {{.TtSun.Minutes}}M -> {{.SunCycle}})
  29. `
  30. const TASK_ITEM = `
  31. Title: {{.Title}}
  32. -------------------------
  33. {{.Desc}}
  34. Due: {{.Due}}
  35. Priority: {{.Priority}}
  36. Done?: {{.Priority}}
  37. `
  38. const TIME_TO_TEMPLATE = `{{.Hours}}H, {{.Minutes}}M`
  39. const HOME_TEMPLATE = ``
  40. // TODO: put all templates in their own file
  41. type HeaderData struct {
  42. Date string
  43. Season string
  44. DaysToQuarter int
  45. QuarterType string
  46. DayOfWeek string
  47. Time string
  48. Meridiem string
  49. TtEod TimeToSunShift
  50. TtSun TimeToSunShift
  51. SunCycle string
  52. }
  53. type TimeToSunShift struct {
  54. Hours int
  55. Minutes int
  56. }
  57. type UserDetails interface {
  58. daysToQuarter(day int) int
  59. getQuarterType(day int) string
  60. getSeason(day int) string
  61. getTime(ts time.Time) string
  62. getDate(ts time.Time) string
  63. getMeridiem(ts time.Time) string
  64. getTimeToEod(ts time.Time) TimeToSunShift
  65. getTimeToSunShift(ts time.Time) TimeToSunShift
  66. getSunCycle(ts time.Time) string
  67. }
  68. type UserImplementation struct{}
  69. func (u UserImplementation) daysToQuarter(day int) int {
  70. season := u.getSeason(day)
  71. if season == "Spring" {
  72. return SUMMER_SOLSTICE - day
  73. }
  74. return 1
  75. }
  76. /*
  77. Return the quarter (solstice/equinox). We have to remember that we are returning
  78. the NEXT season type, i.e. if its currently spring, the next quarter (summer) will have a solstice
  79. :param day: the numerical day of the year
  80. :returns: either solstice, or equinox
  81. */
  82. func (u UserImplementation) getQuarterType(day int) string {
  83. season := u.getSeason(day)
  84. if season == "Winter" {
  85. return "Equinox"
  86. }
  87. if season == "Summer" {
  88. return "Equinox"
  89. }
  90. return "Solstice"
  91. }
  92. func (u UserImplementation) getSeason(day int) string {
  93. if day > 365 {
  94. return "[REDACTED]"
  95. }
  96. if day < 0 {
  97. return "[REDACTED]"
  98. }
  99. if day > 0 && day < SPRING_EQUINOX {
  100. return "Winter"
  101. }
  102. if day > SPRING_EQUINOX && day < SUMMER_SOLSTICE {
  103. return "Spring"
  104. }
  105. if day > SUMMER_SOLSTICE && day < AUTUMN_EQUINOX {
  106. return "Summer"
  107. }
  108. if day > AUTUMN_EQUINOX && day < WINTER_SOLSTICE {
  109. return "Autumn"
  110. }
  111. if day > WINTER_SOLSTICE && day < 365 {
  112. return "Winter"
  113. }
  114. return "idk bruh"
  115. }
  116. func (u UserImplementation) getMeridiem(ts time.Time) string {
  117. if ts.Hour() < 12 {
  118. return "AM"
  119. }
  120. if ts.Hour() >= 12 {
  121. return "PM"
  122. }
  123. return "idk bruh"
  124. }
  125. func (u UserImplementation) getTimeToEod(ts time.Time) TimeToSunShift {
  126. if ts.Hour() > 17 {
  127. return TimeToSunShift{Hours: 0, Minutes: 0}
  128. }
  129. out := time.Date(ts.Year(), ts.Month(), ts.Day(), 17, 0, ts.Second(), ts.Nanosecond(), ts.Location())
  130. dur := time.Until(out)
  131. hours := dur.Minutes() / 60
  132. hours = math.Floor(hours)
  133. minutes := (hours * 60) - dur.Minutes()
  134. return TimeToSunShift{Hours: int(hours), Minutes: int(minutes)}
  135. }
  136. func (u UserImplementation) getTimeToSunShift(ts time.Time) TimeToSunShift {
  137. return TimeToSunShift{}
  138. }
  139. func (u UserImplementation) getSunCycle(ts time.Time) string {
  140. return "☼"
  141. }
  142. func (u UserImplementation) getTime(ts time.Time) string {
  143. return fmt.Sprintf("%v:%v", ts.Hour(), ts.Minute())
  144. }
  145. func (u UserImplementation) getDate(ts time.Time) string {
  146. return fmt.Sprintf("%s %v, %v", ts.Month().String(), ts.Day(), ts.Year())
  147. }
  148. // Format the header string with a template
  149. func getHeader(ud UserDetails, tmpl *template.Template) string {
  150. rn := time.Now()
  151. header := HeaderData{
  152. Date: ud.getDate(rn),
  153. Season: ud.getSeason(rn.YearDay()),
  154. DaysToQuarter: ud.daysToQuarter(rn.YearDay()),
  155. QuarterType: ud.getQuarterType(rn.YearDay()),
  156. DayOfWeek: rn.Weekday().String(),
  157. Time: ud.getTime(rn),
  158. Meridiem: ud.getMeridiem(rn),
  159. TtEod: ud.getTimeToEod(rn),
  160. TtSun: ud.getTimeToSunShift(rn),
  161. SunCycle: ud.getSunCycle(rn),
  162. }
  163. var bw bytes.Buffer
  164. err := tmpl.Execute(&bw, header)
  165. if err != nil {
  166. log.Fatal("There was an issue parsing the header. sorry, ", err)
  167. }
  168. return bw.String()
  169. }
  170. type model struct {
  171. choices []string
  172. cursor int
  173. selected map[int]struct{}
  174. }
  175. func InitialModel() model {
  176. shelf := NewFilesystemShelf(FS_SAVE_LOCATION)
  177. return model{
  178. choices: GetTaskNames(shelf.GetAll()),
  179. selected: make(map[int]struct{}),
  180. }
  181. }
  182. func (m model) Init() tea.Cmd {
  183. // Just return `nil`, which means "no I/O right now, please."
  184. return nil
  185. }
  186. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  187. switch msg := msg.(type) {
  188. // Is it a key press?
  189. case tea.KeyMsg:
  190. // Cool, what was the actual key pressed?
  191. switch msg.String() {
  192. // These keys should exit the program.
  193. case "ctrl+c", "q":
  194. return m, tea.Quit
  195. // The "up" and "k" keys move the cursor up
  196. case "up", "k":
  197. if m.cursor > 0 {
  198. m.cursor--
  199. }
  200. // The "down" and "j" keys move the cursor down
  201. case "down", "j":
  202. if m.cursor < len(m.choices)-1 {
  203. m.cursor++
  204. }
  205. // The "enter" key and the spacebar (a literal space) toggle
  206. // the selected state for the item that the cursor is pointing at.
  207. case "enter", " ":
  208. _, ok := m.selected[m.cursor]
  209. if ok {
  210. delete(m.selected, m.cursor)
  211. } else {
  212. m.selected[m.cursor] = struct{}{}
  213. }
  214. }
  215. }
  216. // Return the updated model to the Bubble Tea runtime for processing.
  217. // Note that we're not returning a command.
  218. return m, nil
  219. }
  220. func (m model) View() string {
  221. // The header
  222. tmpl, err := template.New("header").Parse(HEADER_TEMPLATE)
  223. if err != nil {
  224. log.Fatal("Couldnt parse the header template.. sorry. ", err)
  225. }
  226. shelf := NewFilesystemShelf(FS_SAVE_LOCATION)
  227. s := getHeader(UserImplementation{}, tmpl)
  228. // Iterate over our choices
  229. for i, choice := range m.choices {
  230. // Is the cursor pointing at this choice?
  231. cursor := " " // no cursor
  232. if m.cursor == i {
  233. cursor = ">" // cursor!
  234. }
  235. // Is this choice selected?
  236. checked := " " // not selected
  237. if _, ok := m.selected[i]; ok {
  238. for x := range shelf.Tasks {
  239. if shelf.Tasks[x].Title == choice {
  240. return shelf.RenderTask(shelf.Tasks[x])
  241. }
  242. }
  243. checked = "x" // selected!
  244. }
  245. // Render the row
  246. s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
  247. }
  248. // The footer
  249. s += "\nPress q to quit.\n"
  250. // Send the UI for rendering
  251. return s
  252. }
  253. /*
  254. Add task to the shelf
  255. */
  256. func AddTaskPrompt(shelf TaskShelf) {
  257. task := &Task{}
  258. var reader *bufio.Reader
  259. reader = bufio.NewReader(os.Stdout)
  260. fmt.Print("Enter Task Title: ")
  261. task.Title, _ = reader.ReadString('\n')
  262. fmt.Print("Task description: ")
  263. task.Desc, _ = reader.ReadString('\n')
  264. fmt.Print("Priority: ")
  265. priority, _ := reader.ReadString('\n')
  266. pri, err := strconv.Atoi(priority)
  267. if err != nil {
  268. fmt.Print("non-real number sry\n")
  269. }
  270. task.Priority = pri
  271. shelf.AddTask(*task)
  272. }