userinterface.go 7.3 KB

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