diff --git a/caldav.go b/caldav.go index cbb8c99..42ffb8c 100644 --- a/caldav.go +++ b/caldav.go @@ -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 @@ -144,7 +144,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // func ParseDueDateTODOs_depricated(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, error) { // var output []TODO -// +// // for _, calObj := range calObjs { // // fmt.Println((*(*calObj.Data).Children[0]).Name) // // TODO STATUS map[] COMPLETED @@ -153,7 +153,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // // var notCompletedTODO, withDate, fromToday bool // var notCompletedTODO, fromToday bool // //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis -// //TODO we can use event.Props.Get +// //TODO we can use event.Props.Get // // notCompletedTODO // if (*event).Props["COMPLETED"] == nil { // if (*event).Props["STATUS"] == nil { @@ -164,7 +164,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // } // } // } -// +// // // withTodayDate // if (*event).Props["DUE"] != nil { // // withDate = true @@ -173,12 +173,12 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // fromToday = true // } // } -// +// // // if notCompletedTODO && withDate && fromToday { // if notCompletedTODO && fromToday { -// +// // var tmpTODO TODO -// +// // if (*event).Props["SUMMARY"] != nil { // name := (*event).Props["SUMMARY"][0].Value // tmpTODO.Name = name @@ -189,14 +189,14 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // description := (*event).Props["DESCRIPTION"][0].Value // tmpTODO.Desc = description // } -// +// // 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) @@ -204,7 +204,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // // } // } // } -// +// // // //TODO sort: time, priority (if no time) // // // it means, put DUE at first, other DUE;VALUE=DATE in priority order // // //TODO color: priority, add time icon if have time @@ -212,15 +212,15 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // // //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask // // //TODO PRIORITY:1 #1-high, 5-mid, 9-low // //TODO repeat function??? -// +// // //TODO on complete -repeat function // // RRULE:FREQ=WEEKLY;INTERVAL=1 -// +// // //TODO if no repeat - mark as complted // // STATUS:COMPLETED // // COMPLETED:20240421T065323Z // // PERCENT-COMPLETE:100 -// +// // //TODO support notifcations/alarms??? // // BEGIN:VTODO // // ... @@ -230,40 +230,15 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err // // DESCRIPTION:Default Tasks.org description // // END:VALARM // // END:VTODO -// +// // return output, nil // } - - - - - - - - - - - - - - - - - - - - - - - - - func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) { var output []ical.Event for _, calObj := range calObjs { - + // fmt.Println((*(*calObj.Data).Children[0]).Name) // TODO STATUS map[] COMPLETED // for _, event := range (*calObj.Data).Children { @@ -277,16 +252,16 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical. // // fmt.Println("1:", event2) // // } // } - - 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 //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis - //TODO we can use event.Props.Get + //TODO we can use event.Props.Get // notCompletedTODO if event.Props["COMPLETED"] == nil { if event.Props["STATUS"] == nil { @@ -308,29 +283,30 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical. } // if notCompletedTODO && withDate && fromToday { - if notCompletedTODO && fromToday { + if notCompletedTODO && fromToday { tmpTODO := event - + if event.Props["SUMMARY"] == nil { tmpTODO.Props.SetText(ical.PropSummary, "") } 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,15 +319,15 @@ 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 // COMPLETED:20240421T065323Z // PERCENT-COMPLETE:100 - + //TODO support notifcations/alarms??? // BEGIN:VTODO // ... @@ -367,145 +343,144 @@ 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 != "" { - - // alarmComponent := ical.Component{Name:ical.CompAlarm} - alarmComponent := ical.NewComponent(ical.CompAlarm) - + // } + } // 'zero' time is `time.Time{}` + + 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) } - - - 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) + + 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) + } + + 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} - + calendar := ical.NewCalendar() calendar.Props.SetText(ical.PropProductID, "+//Casual//Tempus//EN") 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} - //TODO check GUID uniq and regenerate if needed - + 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() - // if err != nil {return} + delUID, err := todo.Props.Get(ical.PropUID).Text() + // if err != nil {return} // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath+delUID+".isc") // if err != nil {return} - // + // // var newEvents []*ical.Component // for _, component := range calendar.Data.Component.Children { // if component.Name == ical.CompEvent { @@ -515,155 +490,183 @@ func (m model) DelTodo(todo ical.Event) (err error) { // if uid != delUID { // newEvents = append(newEvents, component) // } - // } + // } // } - // + // // calendar.Data.Component.Children = newEvents // var buf strings.Builder // encoder := ical.NewEncoder(&buf) // err = encoder.Encode(calendar.Data) // if err != nil {return} - // + // // _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data) // if err != nil {return} // req := http.Request{ // Method: "DELETE", // URL: url.Parse(m.Creds.URL + m.Creds.CalendarPath + delUID + ".isc"), - // + // // } - + 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() - if err != nil {eventUID = ""} - if (eventUID == delUID) { + eventUID, err := event.Props.Get(ical.PropUID).Text() + + 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) - - // Fetch Request - resp2, err := client.Do(req2) - if err != nil {return err} + 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) - 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") - } + // 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 + ".") + } + + //TODO exit for loop + // return errors.New("test") + } } } } - - } - + + } + // 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() + // 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 { - //TODO panic, we deleted but cant upload event. Need to save somehow. Or at least write debug string with parameters so we can re-add manually in that case - return + //TODO panic, we deleted but cant upload event. Need to save somehow. Or at least write debug string with parameters so we can re-add manually in that case + return } - + 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} - 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)) + if err != nil { + return } - todo.Props.SetDateTime("DUE",currentDUE) + 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)) + //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 } diff --git a/keyring.go b/keyring.go index 96c1e0c..007eba1 100644 --- a/keyring.go +++ b/keyring.go @@ -43,7 +43,7 @@ const ( // log.Println(decoded) // } -//TODO we can make custom type e.g. SaveData - with []string and put everything saved there. OR we can make the thing like in inputs[variable] - to make it convinient +//TODO we can make custom type e.g. SaveData - with []string and put everything saved there. OR we can make the thing like in inputs[variable] - to make it convinient //TODO inconsistend global funcs (or is it called 'exported funcs?') @@ -95,18 +95,19 @@ 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 - + } // func debugKeyring() { diff --git a/main.go b/main.go index b094156..b71789b 100644 --- a/main.go +++ b/main.go @@ -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() @@ -48,8 +52,8 @@ func main() { errHandler(err, "Unexpected error (we couldn't initiate WebDAV/CalDAV client)") 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 { @@ -106,17 +107,17 @@ func main() { m.ActiveWindow = "today" } } - - } - - } + } + + } //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.) - } diff --git a/options.go b/options.go index 89cc580..9794f38 100644 --- a/options.go +++ b/options.go @@ -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 } diff --git a/tui-addTodo.go b/tui-addTodo.go new file mode 100644 index 0000000..9e4ffb5 --- /dev/null +++ b/tui-addTodo.go @@ -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 +} diff --git a/tui-calChoose.go b/tui-calChoose.go index f8987f4..bcb2a06 100644 --- a/tui-calChoose.go +++ b/tui-calChoose.go @@ -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 } diff --git a/tui-login.go b/tui-login.go index dbdf065..52cb715 100644 --- a/tui-login.go +++ b/tui-login.go @@ -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 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 } diff --git a/tui-model.go b/tui-model.go index 7c83215..b03c67a 100644 --- a/tui-model.go +++ b/tui-model.go @@ -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 @@ -18,69 +15,131 @@ 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) + //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) //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"}, } diff --git a/tui-todo-keys.go b/tui-todo-keys.go index 4212759..f5d955f 100644 --- a/tui-todo-keys.go +++ b/tui-todo-keys.go @@ -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 } } diff --git a/tui-todo.go b/tui-todo.go index cfcc9a4..8fcdbeb 100644 --- a/tui-todo.go +++ b/tui-todo.go @@ -1,68 +1,177 @@ 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 ""} +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 "" + } + return priority + dueTime + out +} +func (i TODO) UID() string { + out, err := i.Props.Get(ical.PropUID).Text() + if err != nil { + return "" + } return out } -func (i TODO) UID() string { - out,err := i.Props.Get(ical.PropUID).Text() - if err != nil {return ""} - 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() + + 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 @@ -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 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