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

531
caldav.go
View File

@ -3,15 +3,15 @@ package main
import ( import (
"context" "context"
// "fmt" // "fmt"
"github.com/emersion/go-ical"
webdav "github.com/emersion/go-webdav" webdav "github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/caldav"
"github.com/emersion/go-ical"
"github.com/teambition/rrule-go" "github.com/teambition/rrule-go"
"strings"
"github.com/google/uuid"
"time"
"errors" "errors"
"github.com/google/uuid"
"strings"
"time"
"net/http" "net/http"
// "net/url" // "net/url"
@ -30,7 +30,7 @@ import (
// DueDateTime time.Time //for adding new events // 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 clientWebDAV *webdav.Client
var client *caldav.Client // clientCalDAV var client *caldav.Client // clientCalDAV
// var calendarObjects []caldav.CalendarObject // var calendarObjects []caldav.CalendarObject
@ -38,10 +38,10 @@ var ctx = context.Background()
// var authSession caldav.Client // clientCalDAV // var authSession caldav.Client // clientCalDAV
func InitDAVclients(url,user,pass string) error { func InitDAVclients(url, user, pass string) error {
var err error var err error
authSession := webdav.HTTPClientWithBasicAuth(nil, user,pass) authSession := webdav.HTTPClientWithBasicAuth(nil, user, pass)
clientWebDAV, err = webdav.NewClient(authSession, url) clientWebDAV, err = webdav.NewClient(authSession, url)
if err != nil { if err != nil {
@ -58,7 +58,7 @@ func InitDAVclients(url,user,pass string) error {
func (options *Options) InitDAVclients() 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 { if err != nil {
// Handle error // Handle error
return err return err
@ -100,7 +100,7 @@ func GetCalendars() ([]caldav.Calendar, error) {
} }
func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err 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() 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), 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 // 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) { // func ParseDueDateTODOs_depricated(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, error) {
// var output []TODO // var output []TODO
// //
// for _, calObj := range calObjs { // for _, calObj := range calObjs {
// // fmt.Println((*(*calObj.Data).Children[0]).Name) // // fmt.Println((*(*calObj.Data).Children[0]).Name)
// // TODO STATUS map[] COMPLETED // // TODO STATUS map[] COMPLETED
@ -153,7 +153,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// // var notCompletedTODO, withDate, fromToday bool // // var notCompletedTODO, withDate, fromToday bool
// var notCompletedTODO, fromToday bool // var notCompletedTODO, fromToday bool
// //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis // //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 // // notCompletedTODO
// if (*event).Props["COMPLETED"] == nil { // if (*event).Props["COMPLETED"] == nil {
// if (*event).Props["STATUS"] == nil { // if (*event).Props["STATUS"] == nil {
@ -164,7 +164,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// } // }
// } // }
// } // }
// //
// // withTodayDate // // withTodayDate
// if (*event).Props["DUE"] != nil { // if (*event).Props["DUE"] != nil {
// // withDate = true // // withDate = true
@ -173,12 +173,12 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// fromToday = true // fromToday = true
// } // }
// } // }
// //
// // if notCompletedTODO && withDate && fromToday { // // if notCompletedTODO && withDate && fromToday {
// if notCompletedTODO && fromToday { // if notCompletedTODO && fromToday {
// //
// var tmpTODO TODO // var tmpTODO TODO
// //
// if (*event).Props["SUMMARY"] != nil { // if (*event).Props["SUMMARY"] != nil {
// name := (*event).Props["SUMMARY"][0].Value // name := (*event).Props["SUMMARY"][0].Value
// tmpTODO.Name = name // tmpTODO.Name = name
@ -189,14 +189,14 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// description := (*event).Props["DESCRIPTION"][0].Value // description := (*event).Props["DESCRIPTION"][0].Value
// tmpTODO.Desc = description // tmpTODO.Desc = description
// } // }
// //
// var todoTime string // var todoTime string
// due := (*event).Props["DUE"][0].Value // due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T") // index := strings.Index(due, "T")
// if index != -1 { // if index != -1 {
// str := due[index+1:] // str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4] // todoTime = str[:2] + ":" + str[2:4]
// //
// tmpTODO.Time = todoTime // tmpTODO.Time = todoTime
// } // }
// output = append(output, tmpTODO) // output = append(output, tmpTODO)
@ -204,7 +204,7 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// // } // // }
// } // }
// } // }
// //
// // //TODO sort: time, priority (if no time) // // //TODO sort: time, priority (if no time)
// // // it means, put DUE at first, other DUE;VALUE=DATE in priority order // // // it means, put DUE at first, other DUE;VALUE=DATE in priority order
// // //TODO color: priority, add time icon if have time // // //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 RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// // //TODO PRIORITY:1 #1-high, 5-mid, 9-low // // //TODO PRIORITY:1 #1-high, 5-mid, 9-low
// //TODO repeat function??? // //TODO repeat function???
// //
// //TODO on complete -repeat function // //TODO on complete -repeat function
// // RRULE:FREQ=WEEKLY;INTERVAL=1 // // RRULE:FREQ=WEEKLY;INTERVAL=1
// //
// //TODO if no repeat - mark as complted // //TODO if no repeat - mark as complted
// // STATUS:COMPLETED // // STATUS:COMPLETED
// // COMPLETED:20240421T065323Z // // COMPLETED:20240421T065323Z
// // PERCENT-COMPLETE:100 // // PERCENT-COMPLETE:100
// //
// //TODO support notifcations/alarms??? // //TODO support notifcations/alarms???
// // BEGIN:VTODO // // BEGIN:VTODO
// // ... // // ...
@ -230,40 +230,15 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// // DESCRIPTION:Default Tasks.org description // // DESCRIPTION:Default Tasks.org description
// // END:VALARM // // END:VALARM
// // END:VTODO // // END:VTODO
// //
// return output, nil // return output, nil
// } // }
func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) { func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) {
var output []ical.Event var output []ical.Event
for _, calObj := range calObjs { for _, calObj := range calObjs {
// fmt.Println((*(*calObj.Data).Children[0]).Name) // fmt.Println((*(*calObj.Data).Children[0]).Name)
// TODO STATUS map[] COMPLETED // TODO STATUS map[] COMPLETED
// for _, event := range (*calObj.Data).Children { // for _, event := range (*calObj.Data).Children {
@ -277,16 +252,16 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// // fmt.Println("1:", event2) // // fmt.Println("1:", event2)
// // } // // }
// } // }
for _, eventComponent:= range (*calObj.Data).Children { for _, eventComponent := range (*calObj.Data).Children {
event := ical.Event{Component: eventComponent} event := ical.Event{Component: eventComponent}
// for _, event := range (*calObj.Data).Children { // for _, event := range (*calObj.Data).Children {
// fmt.Println("1:", event) //TODO rm me // fmt.Println("1:", event) //TODO rm me
// if (*event).Name == "VTODO" { // if (*event).Name == "VTODO" {
// var notCompletedTODO, withDate, fromToday bool // var notCompletedTODO, withDate, fromToday bool
var notCompletedTODO, fromToday bool var notCompletedTODO, fromToday bool
//TODO we can optimize there if we encounter wrong state to forcefully stop next analysis //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 // notCompletedTODO
if event.Props["COMPLETED"] == nil { if event.Props["COMPLETED"] == nil {
if event.Props["STATUS"] == 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 && withDate && fromToday {
if notCompletedTODO && fromToday { if notCompletedTODO && fromToday {
tmpTODO := event tmpTODO := event
if event.Props["SUMMARY"] == nil { if event.Props["SUMMARY"] == nil {
tmpTODO.Props.SetText(ical.PropSummary, "<EMPTY>") tmpTODO.Props.SetText(ical.PropSummary, "<EMPTY>")
} }
if event.Props["DESCRIPTION"] == nil { if event.Props["DESCRIPTION"] == nil {
tmpTODO.Props.SetText(ical.PropDescription, "") tmpTODO.Props.SetText(ical.PropDescription, "")
} }
if event.Props["PRIORITY"] == nil {
tmpTODO.Props.SetText("PRIORITY", "0")
}
// var todoTime string // var todoTime string
// due := (*event).Props["DUE"][0].Value // due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T") // index := strings.Index(due, "T")
// if index != -1 { // if index != -1 {
// str := due[index+1:] // str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4] // todoTime = str[:2] + ":" + str[2:4]
// //
// tmpTODO.Time = todoTime // tmpTODO.Time = todoTime
// } // }
output = append(output, tmpTODO) 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 UID:7ed30f40-fce1-422c-be3b-0486dcfe8943
// //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask // //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// //TODO PRIORITY:1 #1-high, 5-mid, 9-low // //TODO PRIORITY:1 #1-high, 5-mid, 9-low
//TODO on complete -repeat function //TODO on complete -repeat function
//TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1 //TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1
// RRULE:FREQ=WEEKLY;INTERVAL=1 // RRULE:FREQ=WEEKLY;INTERVAL=1
//TODO if no repeat - mark as complted //TODO if no repeat - mark as complted
// STATUS:COMPLETED // STATUS:COMPLETED
// COMPLETED:20240421T065323Z // COMPLETED:20240421T065323Z
// PERCENT-COMPLETE:100 // PERCENT-COMPLETE:100
//TODO support notifcations/alarms??? //TODO support notifcations/alarms???
// BEGIN:VTODO // BEGIN:VTODO
// ... // ...
@ -367,145 +343,144 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
return output, nil return output, nil
} }
type TodoInterface struct { type TodoInterface struct {
name string name string
description string description string
priority int priority int
dueTime time.Time dueTime time.Time
alarmOffset string alarmOffset string
repeat string repeat string
//TODO repeat //TODO repeat
//TODO subtasks //TODO subtasks
} }
func CreateTodo(info TodoInterface) (event ical.Event, err error) {
func CreateTodo(info TodoInterface) (event ical.Event,err error) {
uid, err := uuid.NewUUID() uid, err := uuid.NewUUID()
if err != nil {return} if err != nil {
return
}
event = *ical.NewEvent() event = *ical.NewEvent()
event.Name = ical.CompToDo //VTODO event.Name = ical.CompToDo //VTODO
event.Props.SetText(ical.PropUID, uid.String()) event.Props.SetText(ical.PropUID, uid.String())
event.Props.SetText(ical.PropSummary, info.name) event.Props.SetText(ical.PropSummary, info.name)
event.Props.SetText(ical.PropDescription, info.description) event.Props.SetText(ical.PropDescription, info.description)
if !info.dueTime.IsZero() {event.Props.SetDateTime(ical.PropDue, info.dueTime)} // 'zero' time is `time.Time{}` if !info.dueTime.IsZero() {
//TODO add alarm hour := info.dueTime.Hour()
switch info.priority { minute := info.dueTime.Minute()
case 0: //No priority if (hour != 0 || minute != 0 ) {
event.Props.SetText(ical.PropPriority, "0") event.Props.SetDateTime(ical.PropDue, info.dueTime)
case 1: //Light } else {
event.Props.SetText(ical.PropPriority, "9") event.Props.SetDate(ical.PropDue, info.dueTime)
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 != "" { } // 'zero' time is `time.Time{}`
// alarmComponent := ical.Component{Name:ical.CompAlarm} switch info.priority {
alarmComponent := ical.NewComponent(ical.CompAlarm) 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") {
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC()) offset, _ := strings.CutSuffix(info.alarmOffset, "h")
event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo) value = "-PT" + offset + "H"
event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC()) // TRIGGER;RELATED=END:-PT1H
//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 return
} }
func (m model) UploadTodo(event ical.Event) (err error) { 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 // TODO Alarm component properties
// PropAction = "ACTION" // PropAction = "ACTION"
// PropRepeat = "REPEAT" // PropRepeat = "REPEAT"
// PropTrigger = "TRIGGER"} // PropTrigger = "TRIGGER"}
// calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) //makes error on nextcloud // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) //makes error on nextcloud
// if err != nil {return err} // if err != nil {return err}
calendar := ical.NewCalendar() calendar := ical.NewCalendar()
calendar.Props.SetText(ical.PropProductID, "+//Casual//Tempus//EN") calendar.Props.SetText(ical.PropProductID, "+//Casual//Tempus//EN")
calendar.Props.SetText(ical.PropVersion, "2.0") calendar.Props.SetText(ical.PropVersion, "2.0")
calendar.Component.Children = append(calendar.Component.Children, event.Component) calendar.Component.Children = append(calendar.Component.Children, event.Component)
todoGUID,err := event.Props.Get(ical.PropUID).Text() todoGUID, err := event.Props.Get(ical.PropUID).Text()
if err != nil {return err} if err != nil {
//TODO check GUID uniq and regenerate if needed return err
}
//TODO check GUID uniq and regenerate if needed
var buf strings.Builder var buf strings.Builder
encoder := ical.NewEncoder(&buf) encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar) err = encoder.Encode(calendar)
if err != nil {return err} if err != nil {
return err
}
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar) _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar)
if err != nil {return err} if err != nil {
return err
}
return nil return nil
} }
// func (m model) DelTodo(delUID string) (err error) { // func (m model) DelTodo(delUID string) (err error) {
func (m model) DelTodo(todo ical.Event) (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} // if err != nil {return}
// calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath+delUID+".isc") // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath+delUID+".isc")
// if err != nil {return} // if err != nil {return}
// //
// var newEvents []*ical.Component // var newEvents []*ical.Component
// for _, component := range calendar.Data.Component.Children { // for _, component := range calendar.Data.Component.Children {
// if component.Name == ical.CompEvent { // if component.Name == ical.CompEvent {
@ -515,155 +490,183 @@ func (m model) DelTodo(todo ical.Event) (err error) {
// if uid != delUID { // if uid != delUID {
// newEvents = append(newEvents, component) // newEvents = append(newEvents, component)
// } // }
// } // }
// } // }
// //
// calendar.Data.Component.Children = newEvents // calendar.Data.Component.Children = newEvents
// var buf strings.Builder // var buf strings.Builder
// encoder := ical.NewEncoder(&buf) // encoder := ical.NewEncoder(&buf)
// err = encoder.Encode(calendar.Data) // err = encoder.Encode(calendar.Data)
// if err != nil {return} // if err != nil {return}
// //
// _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data) // _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data)
// if err != nil {return} // if err != nil {return}
// req := http.Request{ // req := http.Request{
// Method: "DELETE", // Method: "DELETE",
// URL: url.Parse(m.Creds.URL + m.Creds.CalendarPath + delUID + ".isc"), // URL: url.Parse(m.Creds.URL + m.Creds.CalendarPath + delUID + ".isc"),
// //
// } // }
client := &http.Client{} client := &http.Client{}
parts := strings.Split(m.Creds.URL, "/") parts := strings.Split(m.Creds.URL, "/")
baseURL := parts[0]+"//"+parts[2] baseURL := parts[0] + "//" + parts[2]
// Create request // Create request
req, err := http.NewRequest("DELETE", baseURL + m.Creds.CalendarPath + delUID + ".isc", nil) req, err := http.NewRequest("DELETE", baseURL+m.Creds.CalendarPath+delUID+".isc", nil)
if err != nil {return} if err != nil {
req.SetBasicAuth(m.Creds.Username, m.Creds.Password) return
}
req.SetBasicAuth(m.Creds.Username, m.Creds.Password)
// Fetch Request // Fetch Request
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil {return} if err != nil {
// resp.Body.Close() return
defer resp.Body.Close() }
// resp.Body.Close()
defer resp.Body.Close()
// Read Response Body // Read Response Body
// respBody, err := ioutil.ReadAll(resp.Body) // respBody, err := ioutil.ReadAll(resp.Body)
// if err != nil { // if err != nil {
// // fmt.Println(err) // // fmt.Println(err)
// return // return
// } // }
if resp.Status == "204 No Content" {return nil} if resp.Status == "204 No Content" {
return nil
}
if resp.Status == "404 Not Found" { if resp.Status == "404 Not Found" {
// Nextcloud Tasks create differend GUID for Vtodo and path // Nextcloud Tasks create differend GUID for Vtodo and path
// TODO code fmt - Highway to for-if hell // TODO code fmt - Highway to for-if hell
for _, calObj := range m.CalObjects { for _, calObj := range m.CalObjects {
for _, eventComponent:= range (*calObj.Data).Children { for _, eventComponent := range (*calObj.Data).Children {
event := ical.Event{Component: eventComponent} event := ical.Event{Component: eventComponent}
if event.Name == "VTODO" { if event.Name == "VTODO" {
// eventUID,_ := (*event).Props["UID"][0].Text() // eventUID,_ := (*event).Props["UID"][0].Text()
eventUID,err := event.Props.Get(ical.PropUID).Text()
if err != nil {eventUID = ""} eventUID, err := event.Props.Get(ical.PropUID).Text()
if (eventUID == delUID) {
if err != nil {
eventUID = ""
}
if eventUID == delUID {
//verify that it's indeed that event by comparing Name //verify that it's indeed that event by comparing Name
sum1,err:=todo.Props.Get("SUMMARY").Text() sum1, err := todo.Props.Get("SUMMARY").Text()
if err != nil {return err} if err != nil {
sum2,err:=event.Props.Get("SUMMARY").Text() return err
if err != nil {return err} }
if sum1==sum2 { sum2, err := event.Props.Get("SUMMARY").Text()
req2, err := http.NewRequest("DELETE", baseURL + calObj.Path, nil) if err != nil {
if err != nil {return err} return err
req2.SetBasicAuth(m.Creds.Username, m.Creds.Password) }
if sum1 == sum2 {
// Fetch Request req2, err := http.NewRequest("DELETE", baseURL+calObj.Path, nil)
resp2, err := client.Do(req2) if err != nil {
if err != nil {return err} return err
}
req2.SetBasicAuth(m.Creds.Username, m.Creds.Password)
defer resp2.Body.Close() // Fetch Request
if resp2.Status == "204 No Content" {return nil} else {return errors.New("Can't delete, response status: "+resp2.Status+".")} resp2, err := client.Do(req2)
if err != nil {
//TODO exit for loop return err
// return errors.New("test") }
}
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") // return errors.New("test")
} }
time.Sleep(2*time.Second) //TODO DEBUG RM ME time.Sleep(2 * time.Second) //TODO DEBUG RM ME
// Display Results // Display Results
// fmt.Println("response Status : ", resp.Status) // fmt.Println("response Status : ", resp.Status)
// fmt.Println("response Headers : ", resp.Header) // fmt.Println("response Headers : ", resp.Header)
// fmt.Println("response Body : ", string(respBody)) // fmt.Println("response Body : ", string(respBody))
// return nil // 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) { func (m model) EditTodo(todo ical.Event) (err error) {
//TODO is there proper edit function ??? //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} // if err != nil {return}
err = m.DelTodo(todo) err = m.DelTodo(todo)
if err != nil {return} if err != nil {
return
}
err = m.UploadTodo(todo) err = m.UploadTodo(todo)
if err != nil { 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 //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
} }
return nil return nil
} }
//TODO // TODO
func (m model) CompleteTodo(todo ical.Event) (err error) { 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 { if todo.Props["RRULE"] != nil {
var rOptions *rrule.ROption var rOptions *rrule.ROption
rOptions, err = todo.Props.RecurrenceRule() rOptions, err = todo.Props.RecurrenceRule()
if err != nil {return} if err != nil {
offset := rOptions.Interval return
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.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 { } else {
todo.Props.SetText(ical.PropStatus,"COMPLETED") todo.Props.SetText(ical.PropStatus, "COMPLETED")
todo.Props.SetText(ical.PropPercentComplete,"100") todo.Props.SetText(ical.PropPercentComplete, "100")
todo.Props.SetDateTime(ical.PropCompleted,time.Now()) todo.Props.SetDateTime(ical.PropCompleted, time.Now())
} }
err = m.EditTodo(todo) err = m.EditTodo(todo)
if err != nil {return} if err != nil {
return
}
return nil return nil
} }

View File

@ -43,7 +43,7 @@ const (
// log.Println(decoded) // 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?') //TODO inconsistend global funcs (or is it called 'exported funcs?')
@ -95,18 +95,19 @@ func getCredentialsFromKeyring_wrapper() (url, login, password, calendar string,
return return
} }
func getCredentialsFromKeyring() (Credentials, error) {
func getCredentialsFromKeyring() (Credentials,error) {
//TODO inconsistent approach compared to caldav.go //TODO inconsistent approach compared to caldav.go
url,username,password,calendar,err := getCredentialsFromKeyring_wrapper() url, username, password, calendar, err := getCredentialsFromKeyring_wrapper()
if err != nil {return Credentials{},err} if err != nil {
return Credentials{}, err
}
return Credentials{ return Credentials{
URL:url, URL: url,
Username:username, Username: username,
Password:password, Password: password,
CalendarPath:calendar, CalendarPath: calendar,
}, nil }, nil
} }
// func debugKeyring() { // func debugKeyring() {

44
main.go
View File

@ -1,10 +1,10 @@
package main package main
import ( import (
"fmt"
"os"
"net/http"
"crypto/tls" "crypto/tls"
"fmt"
"net/http"
"os"
// "sync" // "sync"
// "time" // "time"
@ -13,12 +13,16 @@ import (
"github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/caldav"
// "slices" // "slices"
// "strconv" // "strconv"
// "github.com/charmbracelet/bubbles/list" // "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 // var waitGroup sync.WaitGroup
func errHandler(err error, message string) { func errHandler(err error, message string) {
@ -31,7 +35,7 @@ func errHandler(err error, message string) {
} }
func main() { func main() {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
options, err := ParseOptions() options, err := ParseOptions()
errHandler(err, "Error parsing options") errHandler(err, "Error parsing options")
m := InitModel() m := InitModel()
@ -48,8 +52,8 @@ func main() {
errHandler(err, "Unexpected error (we couldn't initiate WebDAV/CalDAV client)") errHandler(err, "Unexpected error (we couldn't initiate WebDAV/CalDAV client)")
calendars, err = GetCalendars() calendars, err = GetCalendars()
errHandler(err, "Error getting calendars (incorrect url/login/password)") errHandler(err, "Error getting calendars (incorrect url/login/password)")
//TODO crushs START //TODO BUG crush START
var found bool var found bool
// var calPath string // var calPath string
if options.Calendar == "" { if options.Calendar == "" {
@ -57,33 +61,30 @@ func main() {
// m.ActiveWindow = "login" // m.ActiveWindow = "login"
} }
if options.Calendar != "" { if options.Calendar != "" {
for _,calendar := range calendars { for _, calendar := range calendars {
if calendar.Name == options.Calendar { if calendar.Name == options.Calendar {
found = true found = true
// calPath = calendar.Path // calPath = calendar.Path
m.Creds.CalendarPath = calendar.Path m.Creds.CalendarPath = calendar.Path
} }
} }
if ! found { if !found {
fmt.Println("we don't have calendar ", options.Calendar, ". We have:") fmt.Println("we don't have calendar ", options.Calendar, ". We have:")
for _,calendar := range calendars { for _, calendar := range calendars {
fmt.Println(calendar.Name) fmt.Println(calendar.Name)
} }
os.Exit(1) os.Exit(1)
} }
// fmt.Println(m) // fmt.Println(m)
m.LoginToCalendar() m.LoginToCalendar()
m.CalendarToTodo() m.CalendarToTodo()
//TODO BUG crush End
//TODO crushs End
} }
} else { } else {
//TODO I'm on a highway to (IfElse) hell! //TODO I'm on a highway to (IfElse) hell!
//TODO probably need to do more careful debug //TODO probably need to do more careful debug
creds, err := getCredentialsFromKeyring() creds, err := getCredentialsFromKeyring()
m.Creds = creds m.Creds = creds
if err != nil { if err != nil {
@ -106,17 +107,17 @@ func main() {
m.ActiveWindow = "today" m.ActiveWindow = "today"
} }
} }
}
}
}
}
//DEBUG stuff //DEBUG stuff
// task, err := CreateTodo("testName","description",3,time.Now()) // task, err := CreateTodo("testName","description",3,time.Now())
// errHandler(err,"test fail") // errHandler(err,"test fail")
// err = m.UploadTodo(task) // err = m.UploadTodo(task)
// errHandler(err,"test fail2") // errHandler(err,"test fail2")
// m.ActiveWindow = "addTODO"
//DEBUG stuff //DEBUG stuff
//TODO if task have alarm - make a notification / play sound... //TODO if task have alarm - make a notification / play sound...
@ -128,5 +129,4 @@ func main() {
// fmt.Println(m.) // fmt.Println(m.)
} }

View File

@ -1,21 +1,23 @@
package main package main
import ( import (
"os"
"errors" "errors"
"github.com/projectdiscovery/goflags" "github.com/projectdiscovery/goflags"
"os"
"sync" "sync"
) )
var onceOptions sync.Once var onceOptions sync.Once
var options = &Options{} var options = &Options{}
//TODO NEW FEATURE - batch/simple add tasks via CLI
type Options struct { type Options struct {
URL string URL string
// Threads int // Threads int
// Verbose bool // Verbose bool
SkipSave bool SkipSave bool
Calendar string Calendar string
User string User string
Password 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 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 ( import (
"fmt" "fmt"
// "github.com/charmbracelet/bubbles/list" // "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbletea" "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"io" "io"
"strings" "strings"
// "github.com/charmbracelet/lipgloss" // "github.com/charmbracelet/lipgloss"
@ -20,7 +20,6 @@ var ( //Calendars choose
quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
) )
func (m model) RenderCalendarChooser() string { func (m model) RenderCalendarChooser() string {
return m.calendarList.View() 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)) fmt.Fprint(w, fn(str))
} }
func (m *model) CalendarToTodo() (err error) { func (m *model) CalendarToTodo() (err error) {
m.LoggedIn = true m.LoggedIn = true
m.GatherTodos() m.GatherTodos()
m.ActiveWindow = "today" m.ActiveWindow = "today"
return nil return nil
} }

View File

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

View File

@ -3,13 +3,10 @@ package main
import ( import (
// "fmt" // "fmt"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/emersion/go-webdav/caldav"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
"github.com/emersion/go-webdav/caldav"
) )
type model struct { type model struct {
Tabs []string Tabs []string
// TabContent []string // TabContent []string
@ -18,69 +15,131 @@ type model struct {
TodayTab list.Model TodayTab list.Model
TomorrowTab list.Model TomorrowTab list.Model
calendarList list.Model calendarList list.Model
calendarChoice string calendarChoice string
todoAddInputs []textinput.Model
todoAddInputsTime []textinput.Model
addTimeFocus bool
loginInputs []textinput.Model loginInputs []textinput.Model
focused int focused int
btnFocus bool
err error err error
Creds Credentials Creds Credentials
Calendars []caldav.Calendar Calendars []caldav.Calendar
CalObjects []caldav.CalendarObject CalObjects []caldav.CalendarObject
errString string errString string
} }
type Credentials struct { type Credentials struct {
URL string URL string
Username string Username string
Password string Password string
CalendarName string CalendarName string
CalendarPath string CalendarPath string
} }
func (m *model) CredentialsSave() (err error) { func (m *model) CredentialsSave() (err error) {
//TODO some proper error handler in case if we cant save //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 //TODO add skip flag
if err != nil {return} if err != nil {
return
}
return nil return nil
} }
func InitModel() model { func InitModel() model {
var inputs []textinput.Model = make([]textinput.Model, 3) var loginInputs1 []textinput.Model = make([]textinput.Model, 3)
inputs[url] = textinput.New() loginInputs1[url] = textinput.New()
inputs[url].Placeholder = "https://nextcloud.example/remote.php/dav" loginInputs1[url].Placeholder = "https://nextcloud.example/remote.php/dav"
inputs[url].Focus() loginInputs1[url].Focus()
// inputs[url].CharLimit = 20 loginInputs1[url].Width = 30
inputs[url].Width = 30 loginInputs1[url].Prompt = ""
inputs[url].Prompt = "" // loginInputs1[url].Validate = urlValidator //TODO
// inputs[url].Validate = urlValidator
inputs[login] = textinput.New() loginInputs1[login] = textinput.New()
inputs[login].Placeholder = "username" loginInputs1[login].Placeholder = "username"
// inputs[login].CharLimit = 5 loginInputs1[login].Width = 30
inputs[login].Width = 30 loginInputs1[login].Prompt = ""
inputs[login].Prompt = ""
// inputs[login].Validate = loginValidator
inputs[pass] = textinput.New() //TODO make pass hidden with "github.com/erikgeiser/promptkit/textinput" loginInputs1[pass] = textinput.New()
inputs[pass].Placeholder = "MySecurePassword" loginInputs1[pass].Placeholder = "MySecurePassword"
// inputs[pass].CharLimit = 3 loginInputs1[pass].Width = 30
inputs[pass].Width = 30 loginInputs1[pass].Prompt = ""
inputs[pass].Prompt = "" loginInputs1[pass].EchoMode = 1 //list.EchoPassword
// inputs[pass].Validate = passValidator
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{ output := model{
Tabs: []string{"Today", "Tomorrow", "Add"}, Tabs: []string{"Today", "Tomorrow", "Add"},
loginInputs: inputs, loginInputs: loginInputs1,
focused: 0, focused: 0,
err: nil, err: nil,
todoAddInputs: addTODOinputs,
todoAddInputsTime: addTODOinputsTime,
// Creds: Credentials{"test","test","test","test","test"}, // Creds: Credentials{"test","test","test","test","test"},
// TabContent: []string{"ERROR?", "Mascara Tab", "Foundation Tab"}, // TabContent: []string{"ERROR?", "Mascara Tab", "Foundation Tab"},
} }

View File

@ -1,17 +1,27 @@
package main package main
import ( import (
"github.com/emersion/go-ical" "github.com/emersion/go-ical"
// "time" // "time"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
// "fmt"
// "io"
// "strings" // "strings"
// "errors" // "errors"
// "golang.org/x/term"
// "github.com/muesli/reflow/truncate"
// "strings"
) )
// const (
// bullet = "•"
// ellipsis = "…"
// )
var ( var (
appStyle = lipgloss.NewStyle().Padding(1, 2) appStyle = lipgloss.NewStyle().Padding(1, 2)
@ -25,10 +35,149 @@ var (
Render Render
) )
// buttons // type DefaultDelegate struct {
func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { // ShowDescription bool
d := list.NewDefaultDelegate() // 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 { d.UpdateFunc = func(msg tea.Msg, ml *list.Model) tea.Cmd {
var title string var title string
// var todoUID string // var todoUID string
@ -47,30 +196,56 @@ func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
switch { switch {
case key.Matches(msg, keys.choose): case key.Matches(msg, keys.choose):
err := m.CompleteTodo(todoIcal) 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() index := ml.Index()
ml.RemoveItem(index) ml.RemoveItem(index)
if len(ml.Items()) == 0 { if len(ml.Items()) == 0 {
keys.choose.SetEnabled(false) keys.choose.SetEnabled(false)
} }
return ml.NewStatusMessage(statusMessageStyle("You chose " + title)) return ml.NewStatusMessage(statusMessageStyle("You completed " + title+"!"))
case key.Matches(msg, keys.remove): case key.Matches(msg, keys.remove):
err := m.DelTodo(todoIcal) 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() index := ml.Index()
ml.RemoveItem(index) ml.RemoveItem(index)
if len(ml.Items()) == 0 { if len(ml.Items()) == 0 {
keys.remove.SetEnabled(false) keys.remove.SetEnabled(false)
} }
return ml.NewStatusMessage(statusMessageStyle("Deleted " + title)) 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 return nil
} }
help := []key.Binding{keys.choose, keys.remove} help := []key.Binding{keys.choose, keys.remove, keys.add}
d.ShortHelpFunc = func() []key.Binding { d.ShortHelpFunc = func() []key.Binding {
return help return help
@ -80,25 +255,52 @@ func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
return [][]key.Binding{help} 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 return d
} }
type delegateKeyMap struct { type delegateKeyMap struct {
choose key.Binding choose key.Binding
remove key.Binding remove key.Binding
add key.Binding
} }
func newDelegateKeyMap() *delegateKeyMap { func newDelegateKeyMap() *delegateKeyMap {
return &delegateKeyMap{ return &delegateKeyMap{
choose: key.NewBinding( choose: key.NewBinding(
key.WithKeys("enter"), key.WithKeys("enter"),
key.WithHelp("enter", "choose"), key.WithHelp("enter", "complete"),
), ),
remove: key.NewBinding( remove: key.NewBinding(
key.WithKeys("x", "backspace"), key.WithKeys("backspace", "backspace"),
key.WithHelp("x", "delete"), key.WithHelp("backspace", "delete"),
), ),
add: key.NewBinding(
key.WithKeys("a"),
key.WithHelp("a", "Add TODO"),
),
//TODO PRIORITY FEATURE - edit TODO button
} }
} }

View File

@ -1,68 +1,177 @@
package main package main
import ( import (
// "errors"
"github.com/emersion/go-ical"
"time"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/emersion/go-ical"
"strings" "strings"
"errors" "time"
"sort"
"fmt"
"strconv"
"github.com/charmbracelet/lipgloss"
) )
type TODO ical.Event type TODO ical.Event
func (i TODO) Title() string { func (i TODO) Title() string {
out,err := i.Props.Get(ical.PropSummary).Text()
if err != nil {return "<EMPTY>"}
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 return out
} }
func (i TODO) UID() string {
out,err := i.Props.Get(ical.PropUID).Text() func (i TODO) PriorityColor() lipgloss.Color {
if err != nil {return "<EMPTY>"} clr, err := i.Props.Get("PRIORITY").Int()
return out 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 { func (i TODO) Description() string {
out,err := i.Props.Get(ical.PropDescription).Text() out, err := i.Props.Get(ical.PropDescription).Text()
if err != nil {return ""} if err != nil {
return ""
}
return out return out
} }
func (i TODO) FilterValue() string { func (i TODO) FilterValue() string {
out1,err1 := i.Props.Get(ical.PropSummary).Text() // index
out2,err2 := i.Props.Get(ical.PropDescription).Text() out1, err1 := i.Props.Get(ical.PropSummary).Text()
if err1 != nil && err2 != nil {return ""} out2, err2 := i.Props.Get(ical.PropDescription).Text()
return out1+out2 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) { func (m *model) GatherTodos() (err error) {
//TODO more modular approach //TODO more modular approach
m.CalObjects, err = GetTODOs(m.Creds.CalendarPath) m.CalObjects, err = GetTODOs(m.Creds.CalendarPath)
if err != nil {return} if err != nil {
return
}
calendarObjects := m.CalObjects //TODO rm me calendarObjects := m.CalObjects //TODO rm me
// var todayTodos []TODO // var todayTodos []TODO
today := time.Now() today := time.Now()
todayTodosBuf, err := ParseDueDateTODOs(calendarObjects, today) todayTodosBuf, err := ParseDueDateTODOs(calendarObjects, today)
// if err != nil {
// return
// }
tomorrow := time.Now().AddDate(0, 0, 1) tomorrow := time.Now().AddDate(0, 0, 1)
tomorrowTodosBuf, err := ParseDueDateTODOs(calendarObjects, tomorrow) 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
// }
//TODO fmt - we can combine things below
var todayTodos,tomorrowTodos []TODO var todayTodos, tomorrowTodos []TODO
for _,event := range todayTodosBuf { for _, event := range todayTodosBuf {
todayTodos = append(todayTodos,TODO(event)) todayTodos = append(todayTodos, TODO(event))
} }
for _,event := range tomorrowTodosBuf { for _, event := range tomorrowTodosBuf {
tomorrowTodos = append(tomorrowTodos,TODO(event)) tomorrowTodos = append(tomorrowTodos, TODO(event))
} }
var itemsToday []list.Item var itemsToday []list.Item
var itemsTomorrow []list.Item var itemsTomorrow []list.Item
@ -73,32 +182,140 @@ func (m *model) GatherTodos() (err error) {
itemsTomorrow = append(itemsTomorrow, todo) itemsTomorrow = append(itemsTomorrow, todo)
} }
delegateKeys := newDelegateKeyMap()
delegateKeys := newDelegateKeyMap()
delegate := m.newItemDelegate(delegateKeys) delegate := m.newItemDelegate(delegateKeys)
// m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0) // m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0)
m.TodayTab = list.New(itemsToday, delegate, 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, list.NewDefaultDelegate(), 0, 0)
m.TomorrowTab = list.New(itemsTomorrow, delegate, 0, 0) m.TomorrowTab = list.New(itemsTomorrow, delegate, 0, 0)
m.TodayTab.FilterInput.Reset()//TODO debug
m.TomorrowTab.Title = "Tomorrow" 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 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) { func (m *model) UpdateTodos(todo ical.Event) (err error) {
today := time.Now() today := time.Now()
tomorrow := time.Now().AddDate(0, 0, 1) tomorrow := time.Now().AddDate(0, 0, 1)
errorI := 0 errorI := 0
if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {m.TodayTab.InsertItem(-1,TODO(todo))} else {errorI += 1} if todo.Props["DUE"] != nil {
if strings.HasPrefix(todo.Props["DUE"][0].Value, tomorrow.Format("20060102")) {m.TomorrowTab.InsertItem(-1,TODO(todo)) } else {errorI += 1} if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {
m.TodayTab.InsertItem(-1, TODO(todo))
if errorI == 2 {return errors.New("don't match today and tomorrow")} //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
}
return nil if errorI == 2 {
// return errors.New("don't match today and tomorrow") // debug???
return nil
}
return nil
} }

185
tui.go
View File

@ -3,7 +3,7 @@ package main
import ( import (
// "fmt" // "fmt"
// "os" // "os"
"time" // "time"
// "io" // "io"
// "strings" // "strings"
@ -20,13 +20,10 @@ import (
// "errors" // "errors"
) )
//TODO add new TODos //TODO NEW FEATURE IMPORTANT - list today/tomorrow - edit TODOs
//TODO edit TODOs //TODO NEW FEATURE - add search
//TODO add search //TODO NEW FEATURE - add custom filter (search that saves with filter)
//TODO add custom filter (search that saves with filter) // TODO NEW FEATURE - add tabs for days/searcf/filter?
// TODO add tabs for days/searcf/filter
@ -34,8 +31,7 @@ import (
type errMsg struct {message string} type errMsg struct {message string}
//TODO fix multiple errHandlers //TODO fix multiple errHandlers, rm m.errHandler, rename CLI errHandler (in main.go). rename errHandler_tui to errHandler. Rename ebery call
//TODO rm me
func (m model) errHandler(err error,desc string) (tea.Cmd) { func (m model) errHandler(err error,desc string) (tea.Cmd) {
if err != nil { if err != nil {
output := desc+": "+err.Error() output := desc+": "+err.Error()
@ -71,32 +67,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch m.ActiveWindow { switch m.ActiveWindow {
case "tomorrow": //TODO rm me debug
case "a":
m.ActiveWindow = "addTODO"
return m, nil
case "today": //TODO rm me debug case "today": //TODO rm me debug
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {
case "q", "ctrl+c": case "q", "ctrl+c":
// m.quitting = true // m.quitting = true
return m, tea.Quit return m, tea.Quit
case "a":
case "y": m.ActiveWindow = "addTODO"
// 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 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": case "calendarChoose":
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {
@ -124,41 +127,108 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "login": case "login":
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {
case "ctrl+c", "q": // case "ctrl+c", "q"://are you stupid?
return m, tea.Quit // return m, tea.Quit
case "enter": case "enter":
if m.focused == len(m.loginInputs)-1 { if m.focused == len(m.loginInputs)-1 {
//TODO check that we have all fields not empty and notificate about it //TODO check that we have all fields not empty and notificate about it
//TODO submit
for i := range m.loginInputs { for i := range m.loginInputs {
m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) 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.URL = m.loginInputs[url].Value()
m.Creds.Username = m.loginInputs[login].Value() m.Creds.Username = m.loginInputs[login].Value()
m.Creds.Password = m.loginInputs[pass].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() err := m.LoginToCalendar()
if err != nil {return m, m.errHandler(err,"Failed to authenticate")} if err != nil {return m, m.errHandler(err,"Failed to authenticate")}
// try login -> choose calendar -> store -> move to getting stuff // try login -> choose calendar -> store -> move to getting stuff
return m, nil return m, nil
// return m, tea.Quit // return m, tea.Quit
} }
m.nextInput() m.nextLoginInput()
case "shift+tab", "up": case "shift+tab", "up":
m.prevInput() // buttonStyle = lipgloss.NewStyle()
m.prevLoginInput()
case "tab", "down": case "tab", "down":
m.nextInput() // buttonStyle = buttonStyle.Background(lipgloss.Color("#7D56F4"))
m.nextLoginInput()
} }
for i := range m.loginInputs { for i := range m.loginInputs {
m.loginInputs[i].Blur() m.loginInputs[i].Blur()
} }
m.loginInputs[m.focused].Focus() 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 { switch keypress := msg.String(); keypress {
@ -185,17 +255,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// TODO add new element // TODO add new element
// return m, tea.Quit // return m, tea.Quit
// } // }
case "t": // case "t":
// TODO add new element // // TODO add new element
return m, tea.Quit // return m, tea.Quit
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
switch m.ActiveWindow { switch m.ActiveWindow {
//TODO BUG - addTODO/login - output render breaks a bit if errorHandler appeared on screen
// case "login": //TODO // case "login": //TODO
// m.TodayTab.SetSize(msg.Width-h, msg.Height-v) // m.TodayTab.SetSize(msg.Width-h, msg.Height-v)
// case "calendarChoose": //TODO // case "calendarChoose": //TODO
// case "addTODO": //TODO
// m.calendarList.SetWidth(msg.Width) // m.calendarList.SetWidth(msg.Width)
case "today": case "today":
h, v := docStyle.GetFrameSize() h, v := docStyle.GetFrameSize()
@ -218,6 +290,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
for i := range m.loginInputs { for i := range m.loginInputs {
m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) 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": case "calendarChoose":
m.calendarList, cmd = m.calendarList.Update(msg) m.calendarList, cmd = m.calendarList.Update(msg)
case "today": case "today":
@ -228,6 +310,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "": //exit on key press case "": //exit on key press
switch tmp := msg.(type) { //TODO debug and optimize switch tmp := msg.(type) { //TODO debug and optimize
case tea.KeyMsg: case tea.KeyMsg:
// TODO exit on key press
_ = tmp _ = tmp
if m.LoggedIn { if m.LoggedIn {
m.ActiveWindow = "today" m.ActiveWindow = "today"
@ -257,6 +340,18 @@ func (m model) View() string {
// MarginRight(width/3) // MarginRight(width/3)
tabOutput = loginStyle.Render(m.RenderLogin()) tabOutput = loginStyle.Render(m.RenderLogin())
// w, h := lipgloss.Size(tabOutput) // 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": case "calendarChoose":
width, height, _ := term.GetSize(0) width, height, _ := term.GetSize(0)
width -= 2 width -= 2