11 Commits 01b2a68f5f ... 8fbf4eb5ed

Author SHA1 Message Date
  aeth 8fbf4eb5ed minor tweaks 1 month ago
  aeth 6fc15f2734 added a cleaning function 1 month ago
  aeth 42d12bfb36 got some add modify and delete functionality 1 month ago
  aeth 5739d64c7e made default save loc in the users home dir 1 month ago
  aeth 5edd84855f views on it look pretty alright 1 month ago
  aeth 8a03c6c2ae this is getting messy really fast 1 month ago
  aeth 8e5583536c add task to todo list 1 month ago
  aeth fa1d2b5dab working on teh header 1 month ago
  aeth 203bbf34d2 more 1 month ago
  aeth 5620faba5f working on stuff 1 month ago
  aeth ad59052406 an example 1 month ago
13 changed files with 834 additions and 0 deletions
  1. 4 0
      .gitignore
  2. 6 0
      Makefile
  3. 6 0
      cmd/banner.txt
  4. 66 0
      cmd/main.go
  5. 21 0
      go.mod
  6. 41 0
      go.sum
  7. 49 0
      pkg/options.go
  8. 0 0
      pkg/save.go
  9. 305 0
      pkg/savestate.go
  10. 2 0
      pkg/sundown.go
  11. 1 0
      pkg/taskedit.go
  12. 0 0
      pkg/user.go
  13. 333 0
      pkg/userinterface.go

+ 4 - 0
.gitignore

@@ -24,3 +24,7 @@ _testmain.go
 *.test
 *.prof
 
+build/*
+
+*.ta
+

+ 6 - 0
Makefile

@@ -0,0 +1,6 @@
+.PHONY: build
+
+
+
+build:
+	go build -o ./build/itashi ./cmd/main.go 

+ 6 - 0
cmd/banner.txt

@@ -0,0 +1,6 @@
+    _ __             __    _ __
+   (_) /_____ ______/ /_  (_) /
+  / / __/ __ `/ ___/ __ \/ / / 
+ / / /_/ /_/ (__  ) / / / /_/  
+/_/\__/\__,_/____/_/ /_/_(_)   
+                               

+ 66 - 0
cmd/main.go

@@ -0,0 +1,66 @@
+package main
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+
+	itashi "git.aetherial.dev/aeth/itashi/pkg"
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+const ADD_TASK = "add"
+const DEL_TASK = "del"
+const DONE_TASK = "done"
+const TIDY_TASK = "tidy"
+
+//go:embed banner.txt
+var banner []byte
+
+func main() {
+	args := os.Args
+	if len(args) >= 2 {
+		action := args[1]
+		shelf := itashi.NewFilesystemShelf(itashi.GetDefualtSave())
+		switch action {
+		case ADD_TASK:
+			itashi.AddTaskPrompt(shelf)
+			os.Exit(0)
+		case DEL_TASK:
+			id, err := strconv.Atoi(args[2])
+			if err != nil {
+				log.Fatal("ID passed was not a valid integer: ", err)
+			}
+			shelf.DeleteTask(id)
+			os.Exit(0)
+		case DONE_TASK:
+			var taskName string
+			id, err := strconv.Atoi(args[2])
+			if err != nil {
+				log.Fatal("ID passed was not a valid integer: ", err)
+			}
+			taskName = shelf.MarkDone(id)
+			if taskName == "" {
+				fmt.Printf("No task was indexed with ID: %v\n", id)
+				os.Exit(0)
+			}
+			fmt.Printf("よくできた! Good job! Task '%s' was marked complete!\n", strings.TrimSuffix(taskName, "\n"))
+			os.Exit(0)
+		case TIDY_TASK:
+			fmt.Printf("Shelf tidied, removed %v completed tasks.\n", shelf.Clean())
+			os.Exit(0)
+
+		}
+	}
+	fmt.Printf("%+v\n", string(banner))
+
+	p := tea.NewProgram(itashi.InitialModel())
+
+	if _, err := p.Run(); err != nil {
+		fmt.Printf("Alas, there's been an error: %v", err)
+		os.Exit(1)
+	}
+}

+ 21 - 0
go.mod

@@ -1,3 +1,24 @@
 module git.aetherial.dev/aeth/itashi
 
 go 1.22.3
+
+require github.com/charmbracelet/bubbletea v0.26.2
+
+require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.18 // indirect
+	github.com/mattn/go-localereader v0.0.1 // indirect
+	github.com/mattn/go-runewidth v0.0.15 // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
+	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/muesli/termenv v0.15.2 // indirect
+	github.com/rivo/uniseg v0.4.6 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/term v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+)

+ 41 - 0
go.sum

@@ -0,0 +1,41 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ=
+github.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
+github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

+ 49 - 0
pkg/options.go

@@ -0,0 +1,49 @@
+package itashi
+
+import "text/template"
+
+type ShelfHome struct {
+	Items     []Option
+	Tasks     TaskShelf
+	TaskTempl *template.Template
+}
+
+// Return a list of the options as strings for the UI to render
+func (s ShelfHome) OptionList() []string {
+	var optnames []string
+	for i := range s.Items {
+		optnames = append(optnames, s.Items[i].Name)
+	}
+	return optnames
+
+}
+
+type Option struct {
+	Name     string             // the display name in the UI
+	Template *template.Template // The template to render in the Render() func
+
+}
+
+// Render the template stored in the Template struct field
+func (o Option) Render() string {
+	return "This is a placeholder"
+}
+
+// Create the task shelf homepage
+func GetShelfHome(save string) ShelfHome {
+	return ShelfHome{
+		Items: GetOptions(),
+		Tasks: NewFilesystemShelf(save),
+	}
+}
+
+// Removing this from GetShelfHome to allow for indirecting the data feed
+func GetOptions() []Option {
+	var opts []Option
+	opts = append(opts, Option{Name: "Add task to your shelf"})
+	opts = append(opts, Option{Name: "Edit Task"})
+	opts = append(opts, Option{Name: "Move task to done pile"})
+	opts = append(opts, Option{Name: "View my shelf"})
+	return opts
+
+}

+ 0 - 0
pkg/save.go


+ 305 - 0
pkg/savestate.go

@@ -0,0 +1,305 @@
+package itashi
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+	"text/template"
+	"time"
+
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+const SHELF_LINE_DELIM = "\n----    ----    ----    ----\n"
+const SHELF_COL_DELIM = "    "
+const TIME_FORMAT = "2006-01-02T15:04:05 -07:00:00"
+const FS_SAVE_LOCATION = "./todo.ta"
+const SHELF_TEMPLATE = "{{.Id}}    {{.Title}}    {{.Desc}}    {{.Due}}    {{.Done}}    {{.Priority}}"
+
+type Task struct {
+	Id       int
+	Title    string
+	Desc     string
+	Due      time.Time
+	Done     bool
+	Priority int
+}
+
+func GetDefualtSave() string {
+	return fmt.Sprintf("%s/.config/itashi/todo.ta", os.Getenv("HOME"))
+}
+
+// lets make Task implement the tea.Model interface
+func (t Task) Init() tea.Cmd {
+	return nil
+
+}
+
+func (t Task) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	return t, nil
+}
+
+func (t Task) View() string {
+	return ""
+}
+
+type TaskShelf interface {
+	// Modify the due date of an existing task
+	ModifyDue(id int, due time.Time)
+	// Modify the description field of an existing task
+	ModifyDesc(id int, desc string)
+	// Modify the priority of an existing task
+	ModifyPriority(id int, pri int)
+	// modify the title of an existing task
+	ModifyTitle(id int, title string)
+	// delete an existing task from the shelf
+	DeleteTask(id int)
+	// Mark a task as complete
+	MarkDone(id int) string
+	// hopefully you dont need to call this! ;)
+	ResetDone(id int)
+	// Add a task to the shelf
+	AddTask(title string, desc string, priority int, due time.Time)
+	// Retrieve all tasks in the shelf
+	GetAll() []Task
+	// Render a task to a task template
+	RenderTask(task Task) string
+	// Clean the shelf of all completed tasks
+	Clean() int
+}
+
+/*
+Retrieve all the tasks from the designated TaskShelf
+*/
+func GetTaskList(taskio TaskShelf) []Task {
+	return taskio.GetAll()
+}
+
+/*
+Grab all of the names of the tasks from the TaskShelf
+
+	    :param t: a list of Task structs
+		:returns: A list of the task names
+*/
+func GetTaskNames(t []Task) []string {
+	var taskn []string
+	for i := range t {
+		taskn = append(taskn, t[i].Title)
+	}
+	return taskn
+}
+
+type FilesystemShelf struct {
+	SaveLocation string
+	Template     *template.Template
+	TaskTempl    *template.Template
+	Tasks        []Task
+}
+
+func (t *FilesystemShelf) RenderTask(task Task) string {
+	var bw bytes.Buffer
+	err := t.TaskTempl.Execute(&bw, task)
+	if err != nil {
+		log.Fatal("Had a problem rendering this task.", task, err)
+	}
+	return bw.String()
+}
+
+/*
+Create a new filesystem shelf struct to reflect the filesystem shelf
+
+	    :param save: the save location to store the shelf in
+		:returns: a pointer to a FilesystemShelf struct
+*/
+func NewFilesystemShelf(save string) *FilesystemShelf {
+	tmpl, err := template.New("task").Parse(SHELF_TEMPLATE)
+	if err != nil {
+		log.Fatal("Could not parse the shelf template! ", err)
+	}
+	tasktmpl, err := template.New("task").Parse(TASK_ITEM)
+	if err != nil {
+		log.Fatal("Couldnt parse task template. ", err)
+	}
+
+	shelf := &FilesystemShelf{
+		SaveLocation: save,
+		Template:     tmpl,
+		TaskTempl:    tasktmpl,
+		Tasks:        []Task{},
+	}
+	shelf.Tasks = shelf.GetAll()
+	return shelf
+
+}
+
+// Retrieve all the tasks from the filesystem shelf
+func (f *FilesystemShelf) GetAll() []Task {
+	b, err := os.ReadFile(f.SaveLocation)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return parseFilesystemShelf(b)
+}
+
+/*
+Add a task to the filesystem shelf
+
+	    :param title: the title to give the task
+		:param desc: the description to give the task
+		:param priority: the priority to give the task
+		:param due: the due date for the task
+		:returns: Nothing
+*/
+func (f *FilesystemShelf) AddTask(title string, desc string, priority int, due time.Time) {
+	var inc int
+	inc = 0
+	for i := range f.Tasks {
+		if f.Tasks[i].Id > inc {
+			inc = f.Tasks[i].Id
+		}
+	}
+	inc += 1
+	task := Task{
+		Id:       inc,
+		Title:    title,
+		Desc:     desc,
+		Due:      due,
+		Done:     false,
+		Priority: priority,
+	}
+	f.Tasks = append(f.Tasks, task)
+
+	err := os.WriteFile(f.SaveLocation, marshalTaskToShelf(f.Tasks, f.Template), os.ModePerm)
+	if err != nil {
+		log.Fatal("Need to fix later, error writing to fs ", err)
+	}
+}
+
+// Boiler plate so i can implement later
+func (f *FilesystemShelf) ModifyDue(id int, due time.Time)  {}
+func (f *FilesystemShelf) ModifyDesc(id int, desc string)   {}
+func (f *FilesystemShelf) ModifyPriority(id int, pri int)   {}
+func (f *FilesystemShelf) ModifyTitle(id int, title string) {}
+func (f *FilesystemShelf) DeleteTask(id int) {
+	replTasks := []Task{}
+	for i := range f.Tasks {
+		if f.Tasks[i].Id == id {
+			continue
+		}
+		replTasks = append(replTasks, f.Tasks[i])
+	}
+	os.WriteFile(f.SaveLocation, marshalTaskToShelf(replTasks, f.Template), os.ModePerm)
+
+}
+
+// Clean the filesystem shelf of all completed tasks
+func (f *FilesystemShelf) Clean() int {
+	replTasks := []Task{}
+	var cleaned int
+	cleaned = 0
+	for i := range f.Tasks {
+		if f.Tasks[i].Done {
+			cleaned += 1
+			continue
+		}
+		replTasks = append(replTasks, f.Tasks[i])
+	}
+	os.WriteFile(f.SaveLocation, marshalTaskToShelf(replTasks, f.Template), os.ModePerm)
+	return cleaned
+}
+
+/*
+Mark task as done and write the shelf to disk. since the Tasks within FilesystemShelf are
+values and not pointers, we need to copy the entirety of the shelf over to a new set
+and write it, as opposed to just modifying the pointer and then writing.
+
+	    :param id: the ID of the task to mark as done
+		:returns: Nothing
+*/
+func (f *FilesystemShelf) MarkDone(id int) string {
+	replTasks := []Task{}
+	var taskName string
+	for i := range f.Tasks {
+		if f.Tasks[i].Id == id {
+			replTasks = append(replTasks, Task{
+				Id:       f.Tasks[i].Id,
+				Title:    f.Tasks[i].Title,
+				Desc:     f.Tasks[i].Desc,
+				Due:      f.Tasks[i].Due,
+				Done:     true,
+				Priority: f.Tasks[i].Priority,
+			})
+			taskName = f.Tasks[i].Title
+			continue
+		}
+		replTasks = append(replTasks, f.Tasks[i])
+	}
+	os.WriteFile(f.SaveLocation, marshalTaskToShelf(replTasks, f.Template), os.ModePerm)
+	return taskName
+}
+
+func (f *FilesystemShelf) ResetDone(id int) {}
+
+// private function for parsing the byte stream from the filesystem
+func parseFilesystemShelf(data []byte) []Task {
+	var filestring string
+	filestring = string(data)
+	items := strings.Split(filestring, SHELF_LINE_DELIM)
+	var shelf []Task
+	for i := range items {
+		sect := strings.Split(items[i], SHELF_COL_DELIM)
+		if len(sect) < 6 {
+			continue
+		}
+		var id int
+		var due time.Time
+		var done bool
+		var pri int
+		var err error
+		id, err = strconv.Atoi(sect[0])
+		due, err = time.Parse(TIME_FORMAT, sect[3])
+		done, err = strconv.ParseBool(sect[4])
+		pri, err = strconv.Atoi(sect[5])
+		if err != nil {
+			log.Fatal("Couldnt parse from filesystem shelf", err)
+		}
+
+		shelf = append(shelf, Task{
+			Id:       id,
+			Title:    sect[1],
+			Desc:     sect[2],
+			Due:      due,
+			Done:     done,
+			Priority: pri,
+		})
+
+	}
+	return shelf
+
+}
+
+/*
+Helper function to marshal the tasks to the custom format
+
+	     :param tasks: an array of Task structs
+		 :returns: a byte array
+*/
+func marshalTaskToShelf(tasks []Task, templ *template.Template) []byte {
+	var bw bytes.Buffer
+	for i := range tasks {
+		err := templ.Execute(&bw, tasks[i])
+		if err != nil {
+			log.Fatal("Error parsing data into template: ", err)
+		}
+		// dynamically allocating, no need for the read delim
+		_, err = bw.Write([]byte(SHELF_LINE_DELIM))
+		if err != nil {
+			log.Fatal("Error parsing data into template: ", err)
+		}
+
+	}
+	return bw.Bytes()
+}

+ 2 - 0
pkg/sundown.go

@@ -0,0 +1,2 @@
+package itashi
+

+ 1 - 0
pkg/taskedit.go

@@ -0,0 +1 @@
+package itashi

+ 0 - 0
pkg/user.go


+ 333 - 0
pkg/userinterface.go

@@ -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)
+}