v1!!! a lot of fixes and new addTODO feature!

This commit is contained in:
Casual 2024-07-10 04:35:14 +03:00
parent 43843c705d
commit 9946c4ffce
11 changed files with 1398 additions and 471 deletions

429
caldav.go
View File

@ -3,15 +3,15 @@ package main
import (
"context"
// "fmt"
"github.com/emersion/go-ical"
webdav "github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"github.com/emersion/go-ical"
"github.com/teambition/rrule-go"
"strings"
"github.com/google/uuid"
"time"
"errors"
"github.com/google/uuid"
"strings"
"time"
"net/http"
// "net/url"
@ -30,7 +30,7 @@ import (
// DueDateTime time.Time //for adding new events
// }
//TODO Is it safe to create global variables?
// TODO Is it safe to create global variables?
var clientWebDAV *webdav.Client
var client *caldav.Client // clientCalDAV
// var calendarObjects []caldav.CalendarObject
@ -38,10 +38,10 @@ var ctx = context.Background()
// var authSession caldav.Client // clientCalDAV
func InitDAVclients(url,user,pass string) error {
func InitDAVclients(url, user, pass string) error {
var err error
authSession := webdav.HTTPClientWithBasicAuth(nil, user,pass)
authSession := webdav.HTTPClientWithBasicAuth(nil, user, pass)
clientWebDAV, err = webdav.NewClient(authSession, url)
if err != nil {
@ -58,7 +58,7 @@ func InitDAVclients(url,user,pass string) error {
func (options *Options) InitDAVclients() error {
err := InitDAVclients(options.URL,options.User, options.Password)
err := InitDAVclients(options.URL, options.User, options.Password)
if err != nil {
// Handle error
return err
@ -100,7 +100,7 @@ func GetCalendars() ([]caldav.Calendar, error) {
}
func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err error) {
//TODO can we use calendar.Data.Component.Children to dont make this close to pointless request?
//TODO can we use calendar.Data.Component.Children to dont make this close to pointless request?
date := time.Now()
dateStart := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()).AddDate(0, 0, -1) //TODO too complex - time.Now().Add(-92 * time.Hour),
// dateEnd:= time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()) //date +1 day
@ -234,31 +234,6 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// return output, nil
// }
func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) {
var output []ical.Event
@ -278,10 +253,10 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// // }
// }
for _, eventComponent:= range (*calObj.Data).Children {
for _, eventComponent := range (*calObj.Data).Children {
event := ical.Event{Component: eventComponent}
// for _, event := range (*calObj.Data).Children {
// fmt.Println("1:", event) //TODO rm me
// for _, event := range (*calObj.Data).Children {
// fmt.Println("1:", event) //TODO rm me
// if (*event).Name == "VTODO" {
// var notCompletedTODO, withDate, fromToday bool
var notCompletedTODO, fromToday bool
@ -308,7 +283,7 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
}
// if notCompletedTODO && withDate && fromToday {
if notCompletedTODO && fromToday {
if notCompletedTODO && fromToday {
tmpTODO := event
@ -318,18 +293,19 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
if event.Props["DESCRIPTION"] == nil {
tmpTODO.Props.SetText(ical.PropDescription, "")
}
if event.Props["PRIORITY"] == nil {
tmpTODO.Props.SetText("PRIORITY", "0")
}
// var todoTime string
// due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T")
// if index != -1 {
// str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4]
//
// tmpTODO.Time = todoTime
// }
// var todoTime string
// due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T")
// if index != -1 {
// str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4]
//
// tmpTODO.Time = todoTime
// }
output = append(output, tmpTODO)
}
@ -343,9 +319,9 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943
// //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// //TODO PRIORITY:1 #1-high, 5-mid, 9-low
//TODO on complete -repeat function
//TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1
// RRULE:FREQ=WEEKLY;INTERVAL=1
//TODO on complete -repeat function
//TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1
// RRULE:FREQ=WEEKLY;INTERVAL=1
//TODO if no repeat - mark as complted
// STATUS:COMPLETED
@ -367,112 +343,106 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
return output, nil
}
type TodoInterface struct {
name string
name string
description string
priority int
dueTime time.Time
priority int
dueTime time.Time
alarmOffset string
repeat string
repeat string
//TODO repeat
//TODO subtasks
}
func CreateTodo(info TodoInterface) (event ical.Event,err error) {
func CreateTodo(info TodoInterface) (event ical.Event, err error) {
uid, err := uuid.NewUUID()
if err != nil {return}
if err != nil {
return
}
event = *ical.NewEvent()
event.Name = ical.CompToDo //VTODO
event.Props.SetText(ical.PropUID, uid.String())
event.Props.SetText(ical.PropSummary, info.name)
event.Props.SetText(ical.PropDescription, info.description)
if !info.dueTime.IsZero() {event.Props.SetDateTime(ical.PropDue, info.dueTime)} // 'zero' time is `time.Time{}`
//TODO add alarm
switch info.priority {
case 0: //No priority
event.Props.SetText(ical.PropPriority, "0")
case 1: //Light
event.Props.SetText(ical.PropPriority, "9")
case 2: //Medium
event.Props.SetText(ical.PropPriority, "5")
case 3: //Urgent
event.Props.SetText(ical.PropPriority, "1")
default:
err = errors.New("Wrong priority, expecting 0-3")
return
event.Name = ical.CompToDo //VTODO
event.Props.SetText(ical.PropUID, uid.String())
event.Props.SetText(ical.PropSummary, info.name)
event.Props.SetText(ical.PropDescription, info.description)
if !info.dueTime.IsZero() {
hour := info.dueTime.Hour()
minute := info.dueTime.Minute()
if (hour != 0 || minute != 0 ) {
event.Props.SetDateTime(ical.PropDue, info.dueTime)
} else {
event.Props.SetDate(ical.PropDue, info.dueTime)
}
//TODO repeat - RRULE:FREQ=DAILY;INTERVAL=1
if info.alarmOffset != "" {
// }
} // 'zero' time is `time.Time{}`
// alarmComponent := ical.Component{Name:ical.CompAlarm}
alarmComponent := ical.NewComponent(ical.CompAlarm)
switch info.priority {
case 0: //No priority
event.Props.SetText(ical.PropPriority, "0")
case 1: //Light
event.Props.SetText(ical.PropPriority, "9")
case 2: //Medium
event.Props.SetText(ical.PropPriority, "5")
case 3: //Urgent
event.Props.SetText(ical.PropPriority, "1")
default:
err = errors.New("Wrong priority, expecting 0-3")
return
}
//TODO repeat - RRULE:FREQ=DAILY;INTERVAL=1
if info.alarmOffset != "" {
// alarmComponent := ical.Component{Name:ical.CompAlarm}
alarmComponent := ical.NewComponent(ical.CompAlarm)
alarmComponent.Props.SetText(ical.PropAction,"DISPLAY")
// alarmComponent.Props.Add(&ical.Prop{Name:ical.PropAction,Value:"DISPLAY",})
alarmComponent.Props.Add(&ical.Prop{Name:ical.PropDescription,Value:"Default Alarm Tempus description",})
alarmComponent.Props.SetText(ical.PropDescription,"Default Alarm Tempus description")
// ACTION:DISPLAY
// DESCRIPTION:Default Tasks.org description
var value string
if info.alarmOffset == "0" {
// TRIGGER;RELATED=END:PT0S
value = "PT0S"
alarmComponent.Props.SetText(ical.PropAction, "DISPLAY")
// alarmComponent.Props.Add(&ical.Prop{Name:ical.PropAction,Value:"DISPLAY",})
alarmComponent.Props.Add(&ical.Prop{Name: ical.PropDescription, Value: "Default Alarm Tempus description"})
alarmComponent.Props.SetText(ical.PropDescription, "Default Alarm Tempus description")
// ACTION:DISPLAY
// DESCRIPTION:Default Tasks.org description
var value string
if info.alarmOffset == "0" {
// TRIGGER;RELATED=END:PT0S
value = "PT0S"
}
if strings.HasSuffix(info.alarmOffset,"h") {
offset,_ := strings.CutSuffix(info.alarmOffset,"h")
value="-PT" + offset + "H"
// TRIGGER;RELATED=END:-PT1H
}
//TODO stop next if
if strings.HasSuffix(info.alarmOffset,"m") {
offset,_ := strings.CutSuffix(info.alarmOffset,"m")
value="-PT" + offset + "M"
// TRIGGER;RELATED=END:-PT10M
}
if strings.HasSuffix(info.alarmOffset,"d") {
offset,_ := strings.CutSuffix(info.alarmOffset,"d")
value="-P" + offset + "D"
// TRIGGER;RELATED=END:-P1D
}
alarmComponent.Props.SetText("TRIGGER;RELATED=END",value)
event.Children = append(event.Children,alarmComponent)
}
if strings.HasSuffix(info.alarmOffset, "h") {
offset, _ := strings.CutSuffix(info.alarmOffset, "h")
value = "-PT" + offset + "H"
// TRIGGER;RELATED=END:-PT1H
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo)
event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC())
//TODO add a function to verify event (exist in ical lib)
}
//TODO stop next if
if strings.HasSuffix(info.alarmOffset, "m") {
offset, _ := strings.CutSuffix(info.alarmOffset, "m")
value = "-PT" + offset + "M"
// TRIGGER;RELATED=END:-PT10M
}
if strings.HasSuffix(info.alarmOffset, "d") {
offset, _ := strings.CutSuffix(info.alarmOffset, "d")
value = "-P" + offset + "D"
// TRIGGER;RELATED=END:-P1D
}
alarmComponent.Props.SetText("TRIGGER;RELATED=END", value)
event.Children = append(event.Children, alarmComponent)
}
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo)
event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC())
//TODO add a function to verify event (exist in ical lib)
return
}
func (m model) UploadTodo(event ical.Event) (err error) {
// event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime)
// event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime)
// TODO Alarm component properties
// PropAction = "ACTION"
// PropRepeat = "REPEAT"
// PropTrigger = "TRIGGER"}
// PropAction = "ACTION"
// PropRepeat = "REPEAT"
// PropTrigger = "TRIGGER"}
// calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) //makes error on nextcloud
// if err != nil {return err}
@ -482,25 +452,30 @@ func (m model) UploadTodo(event ical.Event) (err error) {
calendar.Props.SetText(ical.PropVersion, "2.0")
calendar.Component.Children = append(calendar.Component.Children, event.Component)
todoGUID,err := event.Props.Get(ical.PropUID).Text()
if err != nil {return err}
todoGUID, err := event.Props.Get(ical.PropUID).Text()
if err != nil {
return err
}
//TODO check GUID uniq and regenerate if needed
var buf strings.Builder
encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar)
if err != nil {return err}
if err != nil {
return err
}
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar)
if err != nil {return err}
if err != nil {
return err
}
return nil
}
// func (m model) DelTodo(delUID string) (err error) {
func (m model) DelTodo(todo ical.Event) (err error) {
delUID,err := todo.Props.Get(ical.PropUID).Text()
delUID, err := todo.Props.Get(ical.PropUID).Text()
// if err != nil {return}
// calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath+delUID+".isc")
@ -536,60 +511,80 @@ func (m model) DelTodo(todo ical.Event) (err error) {
client := &http.Client{}
parts := strings.Split(m.Creds.URL, "/")
baseURL := parts[0]+"//"+parts[2]
// Create request
req, err := http.NewRequest("DELETE", baseURL + m.Creds.CalendarPath + delUID + ".isc", nil)
if err != nil {return}
req.SetBasicAuth(m.Creds.Username, m.Creds.Password)
baseURL := parts[0] + "//" + parts[2]
// Create request
req, err := http.NewRequest("DELETE", baseURL+m.Creds.CalendarPath+delUID+".isc", nil)
if err != nil {
return
}
req.SetBasicAuth(m.Creds.Username, m.Creds.Password)
// Fetch Request
resp, err := client.Do(req)
if err != nil {return}
// resp.Body.Close()
defer resp.Body.Close()
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return
}
// resp.Body.Close()
defer resp.Body.Close()
// Read Response Body
// respBody, err := ioutil.ReadAll(resp.Body)
// if err != nil {
// // fmt.Println(err)
// return
// }
// Read Response Body
// respBody, err := ioutil.ReadAll(resp.Body)
// if err != nil {
// // fmt.Println(err)
// return
// }
if resp.Status == "204 No Content" {return nil}
if resp.Status == "204 No Content" {
return nil
}
if resp.Status == "404 Not Found" {
// Nextcloud Tasks create differend GUID for Vtodo and path
// TODO code fmt - Highway to for-if hell
for _, calObj := range m.CalObjects {
for _, eventComponent:= range (*calObj.Data).Children {
for _, eventComponent := range (*calObj.Data).Children {
event := ical.Event{Component: eventComponent}
if event.Name == "VTODO" {
// eventUID,_ := (*event).Props["UID"][0].Text()
eventUID,err := event.Props.Get(ical.PropUID).Text()
eventUID, err := event.Props.Get(ical.PropUID).Text()
if err != nil {eventUID = ""}
if (eventUID == delUID) {
if err != nil {
eventUID = ""
}
if eventUID == delUID {
//verify that it's indeed that event by comparing Name
sum1,err:=todo.Props.Get("SUMMARY").Text()
if err != nil {return err}
sum2,err:=event.Props.Get("SUMMARY").Text()
if err != nil {return err}
if sum1==sum2 {
req2, err := http.NewRequest("DELETE", baseURL + calObj.Path, nil)
if err != nil {return err}
req2.SetBasicAuth(m.Creds.Username, m.Creds.Password)
sum1, err := todo.Props.Get("SUMMARY").Text()
if err != nil {
return err
}
sum2, err := event.Props.Get("SUMMARY").Text()
if err != nil {
return err
}
if sum1 == sum2 {
req2, err := http.NewRequest("DELETE", baseURL+calObj.Path, nil)
if err != nil {
return err
}
req2.SetBasicAuth(m.Creds.Username, m.Creds.Password)
// Fetch Request
resp2, err := client.Do(req2)
if err != nil {return err}
// Fetch Request
resp2, err := client.Do(req2)
if err != nil {
return err
}
defer resp2.Body.Close()
if resp2.Status == "204 No Content" {return nil} else {return errors.New("Can't delete, response status: "+resp2.Status+".")}
defer resp2.Body.Close()
if resp2.Status == "204 No Content" {
return nil
} else {
return errors.New("Can't delete, response status: " + resp2.Status + ".")
}
//TODO exit for loop
// return errors.New("test")
}
//TODO exit for loop
// return errors.New("test")
}
}
}
}
@ -597,25 +592,26 @@ func (m model) DelTodo(todo ical.Event) (err error) {
}
// return errors.New("test")
}
time.Sleep(2*time.Second) //TODO DEBUG RM ME
// Display Results
// fmt.Println("response Status : ", resp.Status)
// fmt.Println("response Headers : ", resp.Header)
// fmt.Println("response Body : ", string(respBody))
}
time.Sleep(2 * time.Second) //TODO DEBUG RM ME
// Display Results
// fmt.Println("response Status : ", resp.Status)
// fmt.Println("response Headers : ", resp.Header)
// fmt.Println("response Body : ", string(respBody))
// return nil
return errors.New("Can't delete, response status: "+resp.Status+".")
return errors.New("Can't delete, response status: " + resp.Status + ".")
}
func (m model) EditTodo(todo ical.Event) (err error) {
//TODO is there proper edit function ???
// uid,err := todo.Props.Get(ical.PropUID).Text()
// if err != nil {return}
err = m.DelTodo(todo)
if err != nil {return}
if err != nil {
return
}
err = m.UploadTodo(todo)
if err != nil {
@ -626,44 +622,51 @@ func (m model) EditTodo(todo ical.Event) (err error) {
return nil
}
//TODO
// TODO
func (m model) CompleteTodo(todo ical.Event) (err error) {
//TODO if it repitable - Repeate, otherwise complete it
//if it repitable - update DUE time, otherwise complete it
if todo.Props["RRULE"] != nil {
var rOptions *rrule.ROption
rOptions, err = todo.Props.RecurrenceRule()
if err != nil {return}
if err != nil {
return
}
offset := rOptions.Interval
var currentDUE time.Time
currentDUE,err = todo.Props.DateTime("DUE",nil)
if err != nil {return}
switch rOptions.Freq {
case rrule.DAILY:
currentDUE = currentDUE.AddDate(0,0,offset)
case rrule.WEEKLY:
currentDUE = currentDUE.AddDate(0,0,7*offset)
case rrule.MONTHLY:
currentDUE = currentDUE.AddDate(0,offset,0)
case rrule.YEARLY:
currentDUE = currentDUE.AddDate(offset,0,0)
case rrule.HOURLY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Hour * time.Duration(offset))
case rrule.MINUTELY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Minute * time.Duration(offset))
case rrule.SECONDLY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Second * time.Duration(offset))
currentDUE, err = todo.Props.DateTime("DUE", nil)
if err != nil {
return
}
todo.Props.SetDateTime("DUE",currentDUE)
switch rOptions.Freq {
case rrule.DAILY:
currentDUE = currentDUE.AddDate(0, 0, offset)
case rrule.WEEKLY:
currentDUE = currentDUE.AddDate(0, 0, 7*offset)
case rrule.MONTHLY:
currentDUE = currentDUE.AddDate(0, offset, 0)
case rrule.YEARLY:
currentDUE = currentDUE.AddDate(offset, 0, 0)
case rrule.HOURLY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Hour * time.Duration(offset))
case rrule.MINUTELY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Minute * time.Duration(offset))
case rrule.SECONDLY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Second * time.Duration(offset))
//TODO case if nothing changed
}
todo.Props.SetDateTime("DUE", currentDUE)
} else {
todo.Props.SetText(ical.PropStatus,"COMPLETED")
todo.Props.SetText(ical.PropPercentComplete,"100")
todo.Props.SetDateTime(ical.PropCompleted,time.Now())
todo.Props.SetText(ical.PropStatus, "COMPLETED")
todo.Props.SetText(ical.PropPercentComplete, "100")
todo.Props.SetDateTime(ical.PropCompleted, time.Now())
}
err = m.EditTodo(todo)
if err != nil {return}
if err != nil {
return
}
return nil
}

View File

@ -95,16 +95,17 @@ func getCredentialsFromKeyring_wrapper() (url, login, password, calendar string,
return
}
func getCredentialsFromKeyring() (Credentials,error) {
func getCredentialsFromKeyring() (Credentials, error) {
//TODO inconsistent approach compared to caldav.go
url,username,password,calendar,err := getCredentialsFromKeyring_wrapper()
if err != nil {return Credentials{},err}
url, username, password, calendar, err := getCredentialsFromKeyring_wrapper()
if err != nil {
return Credentials{}, err
}
return Credentials{
URL:url,
Username:username,
Password:password,
CalendarPath:calendar,
URL: url,
Username: username,
Password: password,
CalendarPath: calendar,
}, nil
}

36
main.go
View File

@ -1,10 +1,10 @@
package main
import (
"fmt"
"os"
"net/http"
"crypto/tls"
"fmt"
"net/http"
"os"
// "sync"
// "time"
@ -13,12 +13,16 @@ import (
"github.com/emersion/go-webdav/caldav"
// "slices"
// "strconv"
// "github.com/charmbracelet/bubbles/list"
)
//TODO autoupdate?
//TODO predefined filters (missed tasks, upcoming important tasks, tasks without tag, tasks without priority)
//TODO custom filters
//TODO alarms
//TODO search in all tasks
// var waitGroup sync.WaitGroup
func errHandler(err error, message string) {
@ -31,7 +35,7 @@ func errHandler(err error, message string) {
}
func main() {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
options, err := ParseOptions()
errHandler(err, "Error parsing options")
m := InitModel()
@ -49,7 +53,7 @@ func main() {
calendars, err = GetCalendars()
errHandler(err, "Error getting calendars (incorrect url/login/password)")
//TODO crushs START
//TODO BUG crush START
var found bool
// var calPath string
if options.Calendar == "" {
@ -57,33 +61,30 @@ func main() {
// m.ActiveWindow = "login"
}
if options.Calendar != "" {
for _,calendar := range calendars {
for _, calendar := range calendars {
if calendar.Name == options.Calendar {
found = true
// calPath = calendar.Path
m.Creds.CalendarPath = calendar.Path
}
}
if ! found {
if !found {
fmt.Println("we don't have calendar ", options.Calendar, ". We have:")
for _,calendar := range calendars {
for _, calendar := range calendars {
fmt.Println(calendar.Name)
}
os.Exit(1)
}
// fmt.Println(m)
m.LoginToCalendar()
m.CalendarToTodo()
//TODO crushs End
//TODO BUG crush End
}
} else {
//TODO I'm on a highway to (IfElse) hell!
//TODO probably need to do more careful debug
//TODO I'm on a highway to (IfElse) hell!
//TODO probably need to do more careful debug
creds, err := getCredentialsFromKeyring()
m.Creds = creds
if err != nil {
@ -111,12 +112,12 @@ func main() {
}
//DEBUG stuff
// task, err := CreateTodo("testName","description",3,time.Now())
// errHandler(err,"test fail")
// err = m.UploadTodo(task)
// errHandler(err,"test fail2")
// m.ActiveWindow = "addTODO"
//DEBUG stuff
//TODO if task have alarm - make a notification / play sound...
@ -128,5 +129,4 @@ func main() {
// fmt.Println(m.)
}

View File

@ -1,21 +1,23 @@
package main
import (
"os"
"errors"
"github.com/projectdiscovery/goflags"
"os"
"sync"
)
var onceOptions sync.Once
var options = &Options{}
//TODO NEW FEATURE - batch/simple add tasks via CLI
type Options struct {
URL string
// Threads int
// Verbose bool
SkipSave bool
Calendar string
SkipSave bool
Calendar string
User string
Password string
@ -68,8 +70,9 @@ func (options *Options) SanityCheck() error {
}
}
if options.Proxy != "" {os.Setenv("HTTPS_PROXY", options.Proxy)}
if options.Proxy != "" {
os.Setenv("HTTPS_PROXY", options.Proxy)
}
return nil
}

300
tui-addTodo.go Normal file
View File

@ -0,0 +1,300 @@
package main
import (
"fmt"
// "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/lipgloss"
"time"
"errors"
"strconv"
"regexp"
"strings"
)
const (
name = iota
dueDate
description
priority
//TODO add support for subtasks
alarmOffset
)
const (
dueTimeMinute = iota
dueTimeHour
)
func (m model) RenderAddTodo() string {
return fmt.Sprintf(
`%s
%s
%s
%s
%s %0-0s%s
%s
%s
%s
%s
%s
%s
%s
`,
inputStyle.Width(30).Align(lipgloss.Center).Render("Create TODO"),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Name"),
m.todoAddInputs[name].View(),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Due Time"),
m.todoAddInputs[dueDate].View(),
m.todoAddInputsTime[dueTimeHour].View(),
m.todoAddInputsTime[dueTimeMinute].View(),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Description"),
m.todoAddInputs[description].View(),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Priority"),
//TODO add support for subtasks
m.todoAddInputs[priority].View(),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Alarm offset"),
m.todoAddInputs[alarmOffset].View(),
buttonStyle.Render(" Add "),
)
// .Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder())
}
func (m *model) nextTODOInput() {
m.todoAddInputsTime[0].Blur()
m.todoAddInputsTime[1].Blur()
m.todoAddInputs[m.focused].Blur()
if m.focused == len(m.todoAddInputs)-1 {
if m.btnFocus == true {
m.btnFocus = false
buttonStyle = lipgloss.NewStyle()
m.focused = (m.focused + 1) % len(m.todoAddInputs)
//TODO do smth with blinking cursor?
} else {
buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
m.btnFocus = true
}
} else {
// buttonStyle = lipgloss.NewStyle()
m.focused = (m.focused + 1) % len(m.todoAddInputs)
}
m.todoAddInputs[m.focused].Focus()
}
// prevInput focuses the previous input field
func (m *model) prevTODOInput() {
m.todoAddInputs[m.focused].Blur()
if m.focused == len(m.todoAddInputs)-1 {
if m.btnFocus == true {
m.btnFocus = false
buttonStyle = lipgloss.NewStyle()
//TODO do smth with blinking cursor?
// m.focused = (m.focused + 1) % len(m.loginInputs)
} else {
m.focused--
// Wrap around
if m.focused < 0 {
m.focused = len(m.todoAddInputs) - 1
}
}
} else {
m.focused--
// Wrap around
if m.focused < 0 {
m.focused = len(m.todoAddInputs) - 1
}
}
m.todoAddInputs[m.focused].Focus()
}
func (m *model) AddTODOtoList() (err error) {
//TODO BUG - we can get another due date to timezon differs from UTC
//TODO BUG - need to fix `PRIORITY;VALUE=TEXT:0` - shouldn't be TEXT thing
now := time.Now()
// time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
// _, offset := now.Zone() // substract timezone offset because after `now.Truncate(24 * time.Hour)` it makes this -> 2024-07-08 03:00:00 +0300 MSK. so start of a day still in UTC??!? wtf
// timezoneOffset := time.Duration(-offset) * time.Second
// now = now.Add(timezoneOffset)
now = time.Date(now.Year(), now.Month(), now.Day(),0,0,0,0, now.Location())
// now := time.Now().UTC()
todoInfo := TodoInterface{
name: m.todoAddInputs[name].Value(),
description: m.todoAddInputs[description].Value(),
// priority: priorityTmp,
// dueTime: time.Now(),
// alarmOffset: m.todoAddInputs[alarmOffset].Value(),
}
var dueDateTime time.Time
m.todoAddInputs[dueDate].Value()
//t\d
tdigitRegexp := regexp.MustCompile(`t\d{1,2}`) //tXX
digitRegexp := regexp.MustCompile(`\d{1,2}`) // XX
dayMonthRegexp := regexp.MustCompile(`\d{1,2}\.\d{1,2}`) // xx.xx
fullDateRegexp := regexp.MustCompile(`\d{1,2}\.\d{1,2}\.\d{2,4}`) //xx.xx.xxxx
//Date
date := m.todoAddInputs[dueDate].Value()
switch {
// case "":
//no due date
case date == "t":
//if t = today (without time)
// now := time.Now()
dueDateTime = now
// dueDateTime = now.Truncate(24 * time.Hour)
// dueDateTime = now.Truncate(24 * time.Hour).UTC()
// return errors.New(fmt.Sprint(offset))
case tdigitRegexp.MatchString(date):
//if tX = today + x days (without time) //TODO notify user about this feature somehow
// now := time.Now()
addDays,err := strconv.Atoi(strings.TrimPrefix(date,"t"))
dueDateTime = now
// dueDateTime = now.Truncate(24 * time.Hour)
if err == nil {dueDateTime = dueDateTime.AddDate(0,0, addDays)
} else {return errors.New("Can't convert tXX date. Examples: t1, t7, t14")} //TODO return error?
case fullDateRegexp.MatchString(date):
//if 12.08.2024 / = you know
// if 12.08.24 = you know
sep := strings.Split(date, ".")
year,err1 := strconv.Atoi(sep[2])
monthInt,err2 := strconv.Atoi(sep[1])
day,err3 := strconv.Atoi(sep[0])
if year <=2000 {year+=2000}
// year := time.Year(yearInt)
month := time.Month(monthInt)
// day := time.Day(dayInt)
//TODO if it's not full year???
if err1== nil&&err2== nil&&err3 == nil {
dueDateTime = time.Date(year, month,day, 0, 0, 0, 0, time.Local)
}
case dayMonthRegexp.MatchString(date):
//if 12.08 = 12th day of 8th month of this year
currentYear := now.Year()
sep := strings.Split(date, ".")
monthInt,err2 := strconv.Atoi(sep[1])
month := time.Month(monthInt)
day,err3 := strconv.Atoi(sep[0])
if err2== nil&&err3 == nil {
dueDateTime = time.Date(currentYear, month,day, 0, 0, 0, 0, time.Local)
}
case digitRegexp.MatchString(date):
//if 12 = 12th day of this month of this year
// now := time.Now()
currentMonth := now.Month()
currentYear := now.Year()
intDate,err := strconv.Atoi(date)
if err != nil {_=err} //TODO return error?
dueDateTime = time.Date(currentYear, currentMonth, intDate, 0, 0, 0, 0, time.Local)
// case
// default:
}
//Time
//if hour!="" add to date
//if monute!="" add to date
hour,err1 := strconv.Atoi(m.todoAddInputsTime[1].Value())
minute,err2 := strconv.Atoi(m.todoAddInputsTime[0].Value())
if (hour != 0 && err1 ==nil ) || (err2 == nil && minute != 0) {
if dueDateTime.IsZero() { //set to today
dueDateTime = now
// dueDateTime = now.Truncate(24 * time.Hour)
}
dueDateTime = dueDateTime.Add(time.Hour * time.Duration(hour) + time.Minute * time.Duration(minute))
}
if !dueDateTime.IsZero() {
todoInfo.dueTime = dueDateTime
// todoInfo.dueTime = dueDateTime.Add(timezoneOffset)
// return errors.New(fmt.Sprint(todoInfo.dueTime)) //TODO RM ME DEBUG
}
todoInfo.priority = 0
var priorityTmp int
if m.todoAddInputs[priority].Value() != "" {
var err error
priorityTmp,err = strconv.Atoi(m.todoAddInputs[priority].Value())
if err != nil {return err} //TODO debug. do like in next line
// if err != nil {priorityTmp = 0}
} else {}
alarmOffsetTmp := m.todoAddInputs[alarmOffset].Value()
if priorityTmp != 0 {
todoInfo.priority = priorityTmp
}
if !dueDateTime.IsZero() && alarmOffsetTmp != "" {
todoInfo.alarmOffset = alarmOffsetTmp
}
task, err := CreateTodo(todoInfo)
// if err != nil {return m.errHandler(err,"Failed to creating todo")}
if err != nil {return err}
err = m.UploadTodo(task)
// if err != nil {return m.errHandler(err,"Failed to uploade todo")}
if err != nil {return err}
err = m.UpdateTodos(task)
// if err != nil {return m.errHandler(err,"Failed updating todo list")} //Useless
if err != nil {return err}
//TODO deal with errors - append comment on what broken?
m.ActiveWindow = "today" //TODO remove to previus window
//TODO clean previous input
for i,_ := range m.todoAddInputsTime {
m.todoAddInputsTime[i].Blur()
m.todoAddInputsTime[i].Reset()
}
for i,_ := range m.todoAddInputs {
m.todoAddInputs[i].Blur()
m.todoAddInputs[i].Reset()
}
m.focused = 0
m.todoAddInputs[m.focused].Focus()
// m.todoAddInputsTime.Reset()
// m.todoAddInputs.Reset()
return nil
}

View File

@ -3,9 +3,9 @@ package main
import (
"fmt"
// "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"io"
"strings"
// "github.com/charmbracelet/lipgloss"
@ -20,7 +20,6 @@ var ( //Calendars choose
quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
)
func (m model) RenderCalendarChooser() string {
return m.calendarList.View()
}
@ -52,13 +51,11 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
fmt.Fprint(w, fn(str))
}
func (m *model) CalendarToTodo() (err error) {
m.LoggedIn = true
m.GatherTodos()
m.ActiveWindow = "today"
return nil
}

View File

@ -8,18 +8,19 @@ import (
var docStyle = lipgloss.NewStyle().Margin(1, 2)
var loginStyle = lipgloss.NewStyle().Width(40).Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder())
var inputStyle = lipgloss.NewStyle()
// var buttonStyle = lipgloss.NewStyle().Background(lipgloss.Color("#7D56F4"))
var buttonStyle = lipgloss.NewStyle()
const (
url = iota
login
pass
// btnFocus
)
func (m model) RenderLogin() string {
return fmt.Sprintf(
@ -46,55 +47,104 @@ func (m model) RenderLogin() string {
m.loginInputs[login].View(),
inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Password"),
m.loginInputs[pass].View(), //TODO hide
inputStyle.Render("Continue ->"),
buttonStyle.Render("Continue ->"),
)
// .Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder())
}
func (m *model) nextInput() {
m.focused = (m.focused + 1) % len(m.loginInputs)
func (m *model) nextLoginInput() {
if m.focused == len(m.loginInputs)-1 {
if m.btnFocus == true {
m.btnFocus = false
buttonStyle = lipgloss.NewStyle()
m.focused = (m.focused + 1) % len(m.loginInputs)
//TODO do smth with blinking cursor?
} else {
buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
m.btnFocus = true
}
} else {
// buttonStyle = lipgloss.NewStyle()
m.focused = (m.focused + 1) % len(m.loginInputs)
}
// m.focused = (m.focused + 1) % len(m.loginInputs)
//btnFocus
// if m.focused == len(m.loginInputs) {
// buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
// } else {
// buttonStyle = lipgloss.NewStyle()
// }
}
// prevInput focuses the previous input field
func (m *model) prevInput() {
m.focused--
// Wrap around
if m.focused < 0 {
m.focused = len(m.loginInputs) - 1
}
}
func (m *model) prevLoginInput() {
if m.focused == len(m.loginInputs)-1 {
if m.btnFocus == true {
m.btnFocus = false
buttonStyle = lipgloss.NewStyle()
//TODO do smth with blinking cursor?
// m.focused = (m.focused + 1) % len(m.loginInputs)
} else {
m.focused--
// Wrap around
if m.focused < 0 {
m.focused = len(m.loginInputs) - 1
}
}
} else {
m.focused--
// Wrap around
if m.focused < 0 {
m.focused = len(m.loginInputs) - 1
}
}
}
func (m *model) LoginToCalendar() (err error) {
err = m.InitDAVclients()
if err != nil { return }
if err != nil {
return
}
m.Calendars, err = GetCalendars()
if err != nil { return }
if err != nil {
return
}
items := []list.Item{}
for _,calendar := range m.Calendars {
for _, calendar := range m.Calendars {
// fmt.Println(calendar.Name)
items = append(items,item(calendar.Name))
items = append(items, item(calendar.Name))
}
// m.LoggedIn = true TODO after calendar choose
const defaultWidth = 20
const listHeight = 24
l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
l.Title = "Which To-Do list do we need?"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
l.Title = "Which To-Do list do we need?"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
m.calendarList = l
m.ActiveWindow = "calendarChoose"
m.btnFocus = false
return nil
}

View File

@ -3,13 +3,10 @@ package main
import (
// "fmt"
"github.com/charmbracelet/bubbles/list"
"github.com/emersion/go-webdav/caldav"
"github.com/charmbracelet/bubbles/textinput"
"github.com/emersion/go-webdav/caldav"
)
type model struct {
Tabs []string
// TabContent []string
@ -19,68 +16,130 @@ type model struct {
TodayTab list.Model
TomorrowTab list.Model
calendarList list.Model
calendarChoice string
calendarList list.Model
calendarChoice string
todoAddInputs []textinput.Model
todoAddInputsTime []textinput.Model
addTimeFocus bool
loginInputs []textinput.Model
focused int
btnFocus bool
err error
Creds Credentials
Calendars []caldav.Calendar
Creds Credentials
Calendars []caldav.Calendar
CalObjects []caldav.CalendarObject
errString string
errString string
}
type Credentials struct {
URL string
Username string
Password string
URL string
Username string
Password string
CalendarName string
CalendarPath string
}
func (m *model) CredentialsSave() (err error) {
//TODO some proper error handler in case if we cant save
err = storeCredentialsToKeyring(m.Creds.URL,m.Creds.Username,m.Creds.Password,m.Creds.CalendarPath)
err = storeCredentialsToKeyring(m.Creds.URL, m.Creds.Username, m.Creds.Password, m.Creds.CalendarPath)
//TODO add skip flag
if err != nil {return}
if err != nil {
return
}
return nil
}
func InitModel() model {
var inputs []textinput.Model = make([]textinput.Model, 3)
inputs[url] = textinput.New()
inputs[url].Placeholder = "https://nextcloud.example/remote.php/dav"
inputs[url].Focus()
// inputs[url].CharLimit = 20
inputs[url].Width = 30
inputs[url].Prompt = ""
// inputs[url].Validate = urlValidator
var loginInputs1 []textinput.Model = make([]textinput.Model, 3)
loginInputs1[url] = textinput.New()
loginInputs1[url].Placeholder = "https://nextcloud.example/remote.php/dav"
loginInputs1[url].Focus()
loginInputs1[url].Width = 30
loginInputs1[url].Prompt = ""
// loginInputs1[url].Validate = urlValidator //TODO
inputs[login] = textinput.New()
inputs[login].Placeholder = "username"
// inputs[login].CharLimit = 5
inputs[login].Width = 30
inputs[login].Prompt = ""
// inputs[login].Validate = loginValidator
loginInputs1[login] = textinput.New()
loginInputs1[login].Placeholder = "username"
loginInputs1[login].Width = 30
loginInputs1[login].Prompt = ""
inputs[pass] = textinput.New() //TODO make pass hidden with "github.com/erikgeiser/promptkit/textinput"
inputs[pass].Placeholder = "MySecurePassword"
// inputs[pass].CharLimit = 3
inputs[pass].Width = 30
inputs[pass].Prompt = ""
// inputs[pass].Validate = passValidator
loginInputs1[pass] = textinput.New()
loginInputs1[pass].Placeholder = "MySecurePassword"
loginInputs1[pass].Width = 30
loginInputs1[pass].Prompt = ""
loginInputs1[pass].EchoMode = 1 //list.EchoPassword
var addTODOinputs []textinput.Model = make([]textinput.Model, 5)
//TODO NEW FEATURE - add config for default values in addTODO
//TODO NEW FEATURE - addTODO - add repeat field (and underlying support in caldav.go)
//TODO NEW FEATURE - addTODO - multiline input support
//TODO NEW FEATURE - addTODO - priority picker
// //TODO NEW FEATURE - addTODO - add support for subtasks
addTODOinputs[name] = textinput.New()
addTODOinputs[name].Placeholder = "Do task"
addTODOinputs[name].Focus()
addTODOinputs[name].Width = 30
addTODOinputs[name].Prompt = ""
addTODOinputs[dueDate] = textinput.New()
addTODOinputs[dueDate].Placeholder = "t/t1/12/12.08/12.08.2025" //TODO how
// ok, i want to choose from :
// no date
// today
// tomorrow
// custom - pick - https://github.com/EthanEFung/bubble-datepicker
// + time
addTODOinputs[dueDate].Width = 23
addTODOinputs[dueDate].CharLimit = 10
addTODOinputs[dueDate].Prompt = ""
addTODOinputs[description] = textinput.New()
addTODOinputs[description].Placeholder = "I need to do stuff"
// addTODOinputs[pass].CharLimit = 3
addTODOinputs[description].Width = 30
addTODOinputs[description].Prompt = ""
addTODOinputs[priority] = textinput.New()
addTODOinputs[priority].Placeholder = "0-3 (unknown,low,medium,high)"
// addTODOinputs[pass].CharLimit = 3
addTODOinputs[priority].Width = 30
addTODOinputs[priority].Prompt = ""
// suggestions := []string{"0","1","2","a3"}
// addTODOinputs[priority].SetSuggestions(suggestions)
addTODOinputs[alarmOffset] = textinput.New()
addTODOinputs[alarmOffset].Placeholder = "1d/1h/1m"
// addTODOinputs[pass].CharLimit = 3
addTODOinputs[alarmOffset].Width = 30
addTODOinputs[alarmOffset].Prompt = ""
var addTODOinputsTime []textinput.Model = make([]textinput.Model, 2)
addTODOinputsTime[dueTimeHour] = textinput.New()
addTODOinputsTime[dueTimeHour].Placeholder = "14" //TODO how
addTODOinputsTime[dueTimeHour].Width = 2
addTODOinputsTime[dueTimeHour].CharLimit = 2
addTODOinputsTime[dueTimeHour].Prompt = ""
addTODOinputsTime[dueTimeMinute] = textinput.New()
addTODOinputsTime[dueTimeMinute].Placeholder = "00" //TODO how
addTODOinputsTime[dueTimeMinute].Width = 2
addTODOinputsTime[dueTimeMinute].CharLimit = 2
addTODOinputsTime[dueTimeMinute].Prompt = ""
output := model{
Tabs: []string{"Today", "Tomorrow", "Add"},
loginInputs: inputs,
loginInputs: loginInputs1,
focused: 0,
err: nil,
todoAddInputs: addTODOinputs,
todoAddInputsTime: addTODOinputsTime,
// Creds: Credentials{"test","test","test","test","test"},
// TabContent: []string{"ERROR?", "Mascara Tab", "Foundation Tab"},
}

View File

@ -1,17 +1,27 @@
package main
import (
"github.com/emersion/go-ical"
// "time"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
// "fmt"
// "io"
// "strings"
// "errors"
// "golang.org/x/term"
// "github.com/muesli/reflow/truncate"
// "strings"
)
// const (
// bullet = "•"
// ellipsis = "…"
// )
var (
appStyle = lipgloss.NewStyle().Padding(1, 2)
@ -25,10 +35,149 @@ var (
Render
)
// buttons
func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
d := list.NewDefaultDelegate()
// type DefaultDelegate struct {
// ShowDescription bool
// Styles list.DefaultItemStyles
// UpdateFunc func(tea.Msg, *list.Model) tea.Cmd
// ShortHelpFunc func() []key.Binding
// FullHelpFunc func() [][]key.Binding
// // contains filtered or unexported fields
// }
var (
// titleStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyleList = lipgloss.NewStyle().PaddingLeft(4)
// selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
// paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
// helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
// quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
)
// func (d DefaultDelegate) Height() int { return 1 }
// func (d DefaultDelegate) Spacing() int { return 0 }
// func (d DefaultDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
// func (d DefaultDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
// i, ok := listItem.(item)
// if !ok {
// return
// }
//
// str := fmt.Sprintf("%d. %s", index+1, i)
//
// // fn := itemStyleList.Foreground(TODO(listItem).PriorityColor).Render
// fn := itemStyle.Render
// // fn := itemStyleList.Foreground(lipgloss.Color(m.SelectedItem().(TODO).PriorityColor())).Render
// if index == m.Index() {
// fn = func(s ...string) string {
// // return selectedItemStyle.Render("> " + s)
// return selectedItemStyle.Render(s[index])
// }
// }
//
// fmt.Fprintf(w, fn(str))
// }
// type filteredItems []filteredItem
// func (d DefaultDelegate) Render(w io.Writer, m *list.Model, index int, item list.Item) {
// var (
// title, desc string
// matchedRunes []int
// s = &d.Styles
// )
// width, height, _ := term.GetSize(0)
//
// if i, ok := item.(list.DefaultItem); ok {
// title = i.Title()
// desc = i.Description()
// } else {
// return
// }
//
// if width <= 0 {
// // short-circuit
// return
// }
//
// // Prevent text from exceeding list width
// textwidth := uint(width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
// title = truncate.StringWithTail(title, textwidth, ellipsis)
// if d.ShowDescription {
// var lines []string
// for i, line := range strings.Split(desc, "\n") {
// if i >= height-1 {
// break
// }
// lines = append(lines, truncate.StringWithTail(line, textwidth, ellipsis))
// }
// desc = strings.Join(lines, "\n")
// }
//
// // Conditions
// var (
// isSelected = index == m.Index()
// emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == ""
// isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
// )
//
// if m.IsFiltered && index < len(m.filteredItems) {
// // Get indices of matched characters
// matchedRunes = m.MatchesForItem(index)
// }
//
// if emptyFilter {
// title = s.DimmedTitle.Render(title)
// desc = s.DimmedDesc.Render(desc)
// } else if isSelected && m.FilterState() != Filtering {
// if isFiltered {
// // Highlight matches
// unmatched := s.SelectedTitle.Inline(true)
// matched := unmatched.Copy().Inherit(s.FilterMatch)
// title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
// }
// title = s.SelectedTitle.Render(title)
// desc = s.SelectedDesc.Render(desc)
// } else {
// if isFiltered {
// // Highlight matches
// unmatched := s.NormalTitle.Inline(true)
// matched := unmatched.Copy().Inherit(s.FilterMatch)
// title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
// }
// title = s.NormalTitle.Render(title)
// desc = s.NormalDesc.Render(desc)
// }
//
// if d.ShowDescription {
// fmt.Fprintf(w, "%s\n%s", title, desc)
// return
// }
// fmt.Fprintf(w, "%s", title)
// }
//
//
// buttons
func (m *model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
d := list.NewDefaultDelegate()
// d := DefaultDelegate{}
m.ActiveWindow = "addTODO"
d.UpdateFunc = func(msg tea.Msg, ml *list.Model) tea.Cmd {
var title string
// var todoUID string
@ -47,30 +196,56 @@ func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
switch {
case key.Matches(msg, keys.choose):
err := m.CompleteTodo(todoIcal)
if err != nil {return errHandler_tui(err,"can't complete item")}
if err != nil {
return errHandler_tui(err, "can't complete item")
}
index := ml.Index()
ml.RemoveItem(index)
if len(ml.Items()) == 0 {
keys.choose.SetEnabled(false)
}
return ml.NewStatusMessage(statusMessageStyle("You chose " + title))
return ml.NewStatusMessage(statusMessageStyle("You completed " + title+"!"))
case key.Matches(msg, keys.remove):
err := m.DelTodo(todoIcal)
if err != nil {return errHandler_tui(err,"can't delete item")}
if err != nil {
return errHandler_tui(err, "can't delete item")
}
index := ml.Index()
ml.RemoveItem(index)
if len(ml.Items()) == 0 {
keys.remove.SetEnabled(false)
}
return ml.NewStatusMessage(statusMessageStyle("Deleted " + title))
// case key.Matches(msg, keys.add):
//TODO IMPORTANT go to add todo form
// todoInfo := TodoInterface{
// name: "testName1",
// description: "description",
// priority: 3,
// dueTime: time.Now(),
// alarmOffset: "1h",
// }
// task, err := CreateTodo(todoInfo)
// if err != nil {return m.errHandler(err,"Failed to creating todo")}
// err = m.UploadTodo(task)
// if err != nil {return m.errHandler(err,"Failed to uploade todo")}
// err = m.UpdateTodos(task)
// if err != nil {return m.errHandler(err,"Failed updating todo list")} //Useless
// m.ActiveWindow = "addTODO"
// fmt.Println("ADSASDASDASD")
// return errHandler_tui(nil,"Failed updating todo list")
// return nil
}
}
return nil
}
help := []key.Binding{keys.choose, keys.remove}
help := []key.Binding{keys.choose, keys.remove, keys.add}
d.ShortHelpFunc = func() []key.Binding {
return help
@ -80,25 +255,52 @@ func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
return [][]key.Binding{help}
}
// d.Render = func(w io.Writer, m list.Model, index int, listItem list.Item) {
//
// i, ok := listItem.(item)
// if !ok {
// return
// }
//
// str := fmt.Sprintf("%d. %s", index+1, i)
//
// fn := itemStyle.Render
// if index == m.Index() {
// fn = func(s string) string {
// return lipgloss.NewStyle().PaddingLeft(2).Foreground(listItem.PriorityColor)
// // .Render("> " + s)
// }
// }
//
// fmt.Fprintf(w, fn(str))
// }
return d
}
type delegateKeyMap struct {
choose key.Binding
remove key.Binding
add key.Binding
}
func newDelegateKeyMap() *delegateKeyMap {
return &delegateKeyMap{
choose: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "choose"),
key.WithHelp("enter", "complete"),
),
remove: key.NewBinding(
key.WithKeys("x", "backspace"),
key.WithHelp("x", "delete"),
key.WithKeys("backspace", "backspace"),
key.WithHelp("backspace", "delete"),
),
add: key.NewBinding(
key.WithKeys("a"),
key.WithHelp("a", "Add TODO"),
),
//TODO PRIORITY FEATURE - edit TODO button
}
}

View File

@ -1,69 +1,178 @@
package main
import (
"github.com/emersion/go-ical"
"time"
// "errors"
"github.com/charmbracelet/bubbles/list"
"github.com/emersion/go-ical"
"strings"
"errors"
"time"
"sort"
"fmt"
"strconv"
"github.com/charmbracelet/lipgloss"
)
type TODO ical.Event
func (i TODO) Title() string {
out,err := i.Props.Get(ical.PropSummary).Text()
if err != nil {return "<EMPTY>"}
func (i TODO) Title() string {
priority := "? "
prioInt, err := i.Props.Get("PRIORITY").Int()
var prioStr string
if err != nil {
prioStr, err = i.Props.Get("PRIORITY").Text()
if err == nil {
prioInt, err = strconv.Atoi(prioStr)
}
}
if err == nil {
switch prioInt{
case 9:
priority = "" //Low - Blue
case 5:
priority = "❕ " //Mid - Yellow
case 1:
priority = "❗ " //High - Red
}
}
time1, err := i.Props.DateTime("DUE",nil)
// today := time.Now().Truncate(24 * time.Hour)
var dueTime,additionalZero string
if (time1.Hour() != 0) || (time1.Minute() != 0) {
if time1.Minute() <=9 {
additionalZero = "0"
}
dueTime = fmt.Sprint(time1.Hour())+":"+additionalZero+fmt.Sprint(time1.Minute()) + " " //TODO move to sprintf
// if time1.Minute() == 0 {
// dueTime += "0 "
// } else {dueTime += " "}
}
// timer, err := i.Props.Get("DUE").Int() //TODO
// currentDUE = currentDUE.Add(time.Hour * time.Duration(offset))
//TODO if both times exists
// if
// timer+"->"+dueTime +" |"
out, err := i.Props.Get(ical.PropSummary).Text()
if err != nil {
return "<EMPTY>"
}
return priority + dueTime + out
}
func (i TODO) UID() string {
out, err := i.Props.Get(ical.PropUID).Text()
if err != nil {
return "<EMPTY>"
}
return out
}
func (i TODO) UID() string {
out,err := i.Props.Get(ical.PropUID).Text()
if err != nil {return "<EMPTY>"}
return out
func (i TODO) PriorityColor() lipgloss.Color {
clr, err := i.Props.Get("PRIORITY").Int()
c := lipgloss.Color("255") //White
if err != nil {
return c
}
switch clr{
case 9:
c = lipgloss.Color("27") //Blue
case 5:
c = lipgloss.Color("220") //Yellow
case 1:
// c = lipgloss.Color("#FF9999") //Red
c = lipgloss.Color("161") //Red
}
return c
}
func (i TODO) Description() string {
out,err := i.Props.Get(ical.PropDescription).Text()
if err != nil {return ""}
out, err := i.Props.Get(ical.PropDescription).Text()
if err != nil {
return ""
}
return out
}
func (i TODO) FilterValue() string {
out1,err1 := i.Props.Get(ical.PropSummary).Text()
out2,err2 := i.Props.Get(ical.PropDescription).Text()
if err1 != nil && err2 != nil {return ""}
return out1+out2
// index
out1, err1 := i.Props.Get(ical.PropSummary).Text()
out2, err2 := i.Props.Get(ical.PropDescription).Text()
if err1 != nil && err2 != nil {
return ""
}
return out1 + out2
}
// type itemDelegate struct{}
//
// func (d itemDelegate) Height() int { return 1 }
// func (d itemDelegate) Spacing() int { return 0 }
// func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
// func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
// i, ok := listItem.(item)
// if !ok {
// return
// }
//
// str := fmt.Sprintf("%d. %s", index+1, i)
//
// fn := itemStyle.Render
// if index == m.Index() {
// fn = func(s string) string {
// return selectedItemStyle.Render("> " + s)
// }
// }
//
// fmt.Fprintf(w, fn(str))
// }
func (m *model) GatherTodos() (err error) {
//TODO more modular approach
//TODO more modular approach
m.CalObjects, err = GetTODOs(m.Creds.CalendarPath)
if err != nil {return}
if err != nil {
return
}
calendarObjects := m.CalObjects //TODO rm me
// var todayTodos []TODO
today := time.Now()
todayTodosBuf, err := ParseDueDateTODOs(calendarObjects, today)
// if err != nil {
// return
// }
tomorrow := time.Now().AddDate(0, 0, 1)
tomorrowTodosBuf, err := ParseDueDateTODOs(calendarObjects, tomorrow)
// if err != nil {
// return
// }
todayTodosBuf,err = SortTodos_Default(todayTodosBuf)
// if err != nil {
// return
// }
tomorrowTodosBuf,err = SortTodos_Default(tomorrowTodosBuf)
// if err != nil {
// return
// }
var todayTodos,tomorrowTodos []TODO
for _,event := range todayTodosBuf {
todayTodos = append(todayTodos,TODO(event))
//TODO fmt - we can combine things below
var todayTodos, tomorrowTodos []TODO
for _, event := range todayTodosBuf {
todayTodos = append(todayTodos, TODO(event))
}
for _,event := range tomorrowTodosBuf {
tomorrowTodos = append(tomorrowTodos,TODO(event))
for _, event := range tomorrowTodosBuf {
tomorrowTodos = append(tomorrowTodos, TODO(event))
}
var itemsToday []list.Item
var itemsTomorrow []list.Item
for _, todo := range todayTodos {
@ -73,32 +182,140 @@ func (m *model) GatherTodos() (err error) {
itemsTomorrow = append(itemsTomorrow, todo)
}
delegateKeys := newDelegateKeyMap()
delegateKeys := newDelegateKeyMap()
delegate := m.newItemDelegate(delegateKeys)
// m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0)
m.TodayTab = list.New(itemsToday, delegate, 0, 0)
m.TodayTab.Title = "Today"
m.TodayTab.FilterInput.Reset() //TODO debug
// m.TodayTab.Title = "Today"
// m.TodayTab.SetShowStatusBar(false)
// m.TodayTab.SetFilteringEnabled(false)
// m.TodayTab.Styles.Title = titleStyle
// m.TodayTab.Styles.PaginationStyle = paginationStyle
// m.TodayTab.Styles.HelpStyle = helpStyle
// m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0)
m.TomorrowTab = list.New(itemsTomorrow, delegate, 0, 0)
m.TodayTab.FilterInput.Reset()//TODO debug
m.TomorrowTab.Title = "Tomorrow"
// m.TomorrowTab.SetShowStatusBar(false)
// m.TomorrowTab.SetFilteringEnabled(false)
// m.TomorrowTab.Styles.Title = titleStyle
// m.TomorrowTab.Styles.PaginationStyle = paginationStyle
// m.TomorrowTab.Styles.HelpStyle = helpStyle
return nil
}
//for sorting
type ByTime []ical.Event
func (a ByTime) Len() int { return len(a) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTime) Less(i, j int) bool {
time1,_ := a[i].Props.DateTime("DUE",nil)
//TODO is there better way?
// if time1.Hour() == 0 || time1.Minute() == 0 {
// time1 = time.Date(time1.Year(), time1.Month(), time1.Day(), 23, 59, 0, 0, time.UTC)
// }
// timeInt1,_ := strconv.Atoi(fmt.Sprint("%d%d",time1.Hour(),time1.Minute()))
s1:=fmt.Sprintf("%d%d",time1.Hour(),time1.Minute())
time2,_ := a[j].Props.DateTime("DUE",nil)
// timeInt2,_ := strconv.Atoi(fmt.Sprint("%d%d",time2.Hour(),time2.Minute()))
s2 := fmt.Sprintf("%d%d",time2.Hour(),time2.Minute())
timeInt1, _ := strconv.Atoi(s1)
timeInt2, _ := strconv.Atoi(s2)
if timeInt1 == 0 {
timeInt1 = 9999
}
if timeInt2 == 0 {
timeInt2 = 9999
}
// if time2.Hour() == 0 || time2.Minute() == 0 {
// time2 = time.Date(time2.Year(), time2.Month(), time2.Day(), 23, 59, 0, 0, time.UTC)
// }
// return time2.After(time1)
return timeInt1<timeInt2
}
type ByPriority []ical.Event
func (a ByPriority) Len() int { return len(a) }
func (a ByPriority) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPriority) Less(i, j int) bool {
prior1,err := a[i].Props.Get("PRIORITY").Int()
// in case we have `PRIORITY;VALUE=TEXT:0`
if err != nil {
prioStr, err := a[i].Props.Get("PRIORITY").Text()
if err == nil {
prior1, err = strconv.Atoi(prioStr)
}
}
//if no priority
if prior1 == 0 {
prior1 = 11
}
prior2,_ := a[j].Props.Get("PRIORITY").Int()
if prior2 == 0 {
prior2 = 11
}
return prior1 < prior2
}
func SortTodos_Default(inputEvents []ical.Event) ([]ical.Event, error) {
//TODO sort other alphabetically
var timeEvents,priorityEvents,output []ical.Event
for _,event := range inputEvents {
time,err := event.Props.DateTime("DUE",nil)
if (err == nil) && (time.Hour() != 0 || time.Minute() != 0) {
timeEvents = append(timeEvents,event)
} else {
priorityEvents = append(priorityEvents, event)
}
}
sort.Sort(ByPriority(priorityEvents))
sort.Sort(ByTime(timeEvents))
output = timeEvents
output = append(output,priorityEvents...)
return output,nil
}
func (m *model) UpdateTodos(todo ical.Event) (err error) {
today := time.Now()
tomorrow := time.Now().AddDate(0, 0, 1)
errorI := 0
if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {m.TodayTab.InsertItem(-1,TODO(todo))} else {errorI += 1}
if strings.HasPrefix(todo.Props["DUE"][0].Value, tomorrow.Format("20060102")) {m.TomorrowTab.InsertItem(-1,TODO(todo)) } else {errorI += 1}
if errorI == 2 {return errors.New("don't match today and tomorrow")}
if todo.Props["DUE"] != nil {
if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {
m.TodayTab.InsertItem(-1, TODO(todo))
//TODO makey another type of "TodayTab"?, like []slice of tabs
//TODO sort after update
} else {
errorI += 1
}
if strings.HasPrefix(todo.Props["DUE"][0].Value, tomorrow.Format("20060102")) {
m.TomorrowTab.InsertItem(-1, TODO(todo))
//TODO sort after update
} else {
errorI += 1
}
} else {
errorI += 1
}
if errorI == 2 {
// return errors.New("don't match today and tomorrow") // debug???
return nil
}
return nil
}

183
tui.go
View File

@ -3,7 +3,7 @@ package main
import (
// "fmt"
// "os"
"time"
// "time"
// "io"
// "strings"
@ -20,13 +20,10 @@ import (
// "errors"
)
//TODO add new TODos
//TODO edit TODOs
//TODO add search
//TODO add custom filter (search that saves with filter)
// TODO add tabs for days/searcf/filter
//TODO NEW FEATURE IMPORTANT - list today/tomorrow - edit TODOs
//TODO NEW FEATURE - add search
//TODO NEW FEATURE - add custom filter (search that saves with filter)
// TODO NEW FEATURE - add tabs for days/searcf/filter?
@ -34,8 +31,7 @@ import (
type errMsg struct {message string}
//TODO fix multiple errHandlers
//TODO rm me
//TODO fix multiple errHandlers, rm m.errHandler, rename CLI errHandler (in main.go). rename errHandler_tui to errHandler. Rename ebery call
func (m model) errHandler(err error,desc string) (tea.Cmd) {
if err != nil {
output := desc+": "+err.Error()
@ -71,32 +67,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch m.ActiveWindow {
case "tomorrow": //TODO rm me debug
case "a":
m.ActiveWindow = "addTODO"
return m, nil
case "today": //TODO rm me debug
switch keypress := msg.String(); keypress {
case "q", "ctrl+c":
// m.quitting = true
return m, tea.Quit
case "y":
// m."2797322749061742597"
case "h":
todoInfo := TodoInterface{
name: "testName1",
description: "description",
priority: 3,
dueTime: time.Now(),
alarmOffset: "1h",
}
task, err := CreateTodo(todoInfo)
if err != nil {return m, m.errHandler(err,"test fail")}
err = m.UploadTodo(task)
if err != nil {return m, m.errHandler(err,"test fail2")}
m.UpdateTodos(task)
// fmt.Println(&task)
// fmt.Println(err)
// time.Sleep(2*time.Second)
case "a":
m.ActiveWindow = "addTODO"
return m, nil
// case "y":
// m."2797322749061742597"
// case "h":
//
// todoInfo := TodoInterface{
// name: "testName1",
// description: "description",
// priority: 3,
// dueTime: time.Now(),
// alarmOffset: "1h",
// }
// task, err := CreateTodo(todoInfo)
// if err != nil {return m, m.errHandler(err,"test fail")}
// err = m.UploadTodo(task)
// if err != nil {return m, m.errHandler(err,"test fail2")}
// m.UpdateTodos(task)
// // fmt.Println(&task)
// // fmt.Println(err)
// // time.Sleep(2*time.Second)
// return m, nil
}
case "calendarChoose":
switch keypress := msg.String(); keypress {
@ -124,41 +127,108 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "login":
switch keypress := msg.String(); keypress {
case "ctrl+c", "q":
return m, tea.Quit
// case "ctrl+c", "q"://are you stupid?
// return m, tea.Quit
case "enter":
if m.focused == len(m.loginInputs)-1 {
//TODO check that we have all fields not empty and notificate about it
//TODO submit
for i := range m.loginInputs {
m.loginInputs[i], cmd = m.loginInputs[i].Update(msg)
}
// fmt.Println(m.loginInputs[url].Value())//DEBUG
m.Creds.URL = m.loginInputs[url].Value()
m.Creds.Username = m.loginInputs[login].Value()
m.Creds.Password = m.loginInputs[pass].Value()
// fmt.Println(m.Creds.URL)//DEBUG
// time.Sleep(1 * time.Second) ///DEBUG
// m.Creds.
// return m, nil
err := m.LoginToCalendar()
if err != nil {return m, m.errHandler(err,"Failed to authenticate")}
// try login -> choose calendar -> store -> move to getting stuff
return m, nil
// return m, tea.Quit
}
m.nextInput()
m.nextLoginInput()
case "shift+tab", "up":
m.prevInput()
// buttonStyle = lipgloss.NewStyle()
m.prevLoginInput()
case "tab", "down":
m.nextInput()
// buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
m.nextLoginInput()
}
for i := range m.loginInputs {
m.loginInputs[i].Blur()
}
m.loginInputs[m.focused].Focus()
case "addTODO":
switch keypress := msg.String(); keypress {
// case "ctrl+c", "q": //are you stupid?
// return m, tea.Quit
case "enter":
if m.focused == len(m.todoAddInputs)-1 {
//TODO check that we have all fields not empty or contains wrong value and notify about it (add textinput validator in tui-model.go)
for i := range m.todoAddInputs {
m.todoAddInputs[i], cmd = m.todoAddInputs[i].Update(msg)
}
err := m.AddTODOtoList()
if err != nil {return m, m.errHandler(err,"Failed to add TODO")}
return m, nil
}
m.nextTODOInput()
m.addTimeFocus = false
// return m, nil
case "shift+tab", "up":
// buttonStyle = lipgloss.NewStyle()
m.addTimeFocus = false
m.prevTODOInput()
// return m, nil
case "right":
//TODO BUG - cant move left-right, probably just move return in IF statement
//TODO BUG - focus todoAddInputsTime only if cursor at last character
// if focus Due Time
if m.focused == 1 {
m.addTimeFocus = true
m.todoAddInputs[m.focused].Blur()
if m.todoAddInputsTime[1].Focused() {
m.todoAddInputsTime[1].Blur()
m.todoAddInputsTime[0].Focus()
} else {
m.todoAddInputsTime[0].Blur()
m.todoAddInputsTime[1].Focus()
}
}
return m, nil
case "left":
//TODO BUG - cant move left-right, probably just move return in IF statement
//TODO BUG - focus todoAddInputs only if cursor at zero character
if m.focused == 1 {
m.todoAddInputs[m.focused].Blur()
if m.todoAddInputsTime[0].Focused() {
m.todoAddInputsTime[0].Blur()
m.todoAddInputsTime[1].Focus()
} else {
m.todoAddInputsTime[0].Blur()
m.todoAddInputsTime[1].Blur()
m.todoAddInputs[m.focused].Focus()
}
}
return m, nil
case "tab", "down":
// buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
m.nextTODOInput()
m.addTimeFocus = false
// return m, nil
}
// for i := range m.todoAddInputs {
// m.todoAddInputs[i].Blur()
// }
// for i := range m.todoAddInputsTime {
// m.todoAddInputsTime[i].Blur()
// }
// if !m.addTimeFocus {m.todoAddInputs[m.focused].Focus()}
}
switch keypress := msg.String(); keypress {
@ -185,17 +255,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// TODO add new element
// return m, tea.Quit
// }
case "t":
// TODO add new element
return m, tea.Quit
// case "t":
// // TODO add new element
// return m, tea.Quit
}
case tea.WindowSizeMsg:
switch m.ActiveWindow {
//TODO BUG - addTODO/login - output render breaks a bit if errorHandler appeared on screen
// case "login": //TODO
// m.TodayTab.SetSize(msg.Width-h, msg.Height-v)
// case "calendarChoose": //TODO
// case "addTODO": //TODO
// m.calendarList.SetWidth(msg.Width)
case "today":
h, v := docStyle.GetFrameSize()
@ -218,6 +290,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
for i := range m.loginInputs {
m.loginInputs[i], cmd = m.loginInputs[i].Update(msg)
}
case "addTODO":
if (m.focused == 1) && (!m.todoAddInputs[m.focused].Focused()) {
for i := range m.todoAddInputsTime {
m.todoAddInputsTime[i], cmd = m.todoAddInputsTime[i].Update(msg)
}
} else {
for i := range m.todoAddInputs {
m.todoAddInputs[i], cmd = m.todoAddInputs[i].Update(msg)
}
}
case "calendarChoose":
m.calendarList, cmd = m.calendarList.Update(msg)
case "today":
@ -228,6 +310,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "": //exit on key press
switch tmp := msg.(type) { //TODO debug and optimize
case tea.KeyMsg:
// TODO exit on key press
_ = tmp
if m.LoggedIn {
m.ActiveWindow = "today"
@ -257,6 +340,18 @@ func (m model) View() string {
// MarginRight(width/3)
tabOutput = loginStyle.Render(m.RenderLogin())
// w, h := lipgloss.Size(tabOutput)
case "addTODO":
width, height, _ := term.GetSize(0)
width -= 2
height -= 2
loginStyle = loginStyle.
// Width(30).
// Height(height/5).
MarginTop(height / 5).
MarginLeft(width/2 - 20)
// MarginRight(width/3)
tabOutput = loginStyle.Render(m.RenderAddTodo())
// w, h := lipgloss.Size(tabOutput)
case "calendarChoose":
width, height, _ := term.GetSize(0)
width -= 2