userinterface.go 7.4 KB

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