diff --git a/caldav.go b/caldav.go index b4c89dd..20a4275 100644 --- a/caldav.go +++ b/caldav.go @@ -11,6 +11,9 @@ import ( "github.com/google/uuid" "time" "errors" + + "net/http" + // "net/url" // "fmt" ) @@ -339,10 +342,9 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical. // //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943 // //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask // //TODO PRIORITY:1 #1-high, 5-mid, 9-low - //TODO repeat function??? - - //TODO on complete -repeat function - // 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 @@ -376,40 +378,88 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical. -// type todoInterface struct { -// name string -// description string -// priority int -// dueTime time.Time -// name string -// } +type TodoInterface struct { + name string + description string + priority int + dueTime time.Time + alarmOffset string + repeat string + //TODO repeat + //TODO subtasks +} -func CreateTodo(name,description string, priority int, dueTime time.Time) (event ical.Event,err error) { +func CreateTodo(info TodoInterface) (event ical.Event,err error) { uid, err := uuid.NewUUID() if err != nil {return} event = *ical.NewEvent() + event.Name = ical.CompToDo //VTODO event.Props.SetText(ical.PropUID, uid.String()) - event.Props.SetText(ical.PropSummary, name) - event.Props.SetText(ical.PropDescription, description) - if !dueTime.IsZero() {event.Props.SetDateTime(ical.PropDateTimeEnd, dueTime)} // 'zero' time is `time.Time{}` - switch priority { + 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, "1") + event.Props.SetText(ical.PropPriority, "9") case 2: //Medium event.Props.SetText(ical.PropPriority, "5") case 3: //Urgent - event.Props.SetText(ical.PropPriority, "9") + 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" + + } + + 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 } @@ -417,54 +467,102 @@ func CreateTodo(name,description string, priority int, dueTime time.Time) (event func (m model) UploadTodo(event ical.Event) (err error) { // event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime) - //TODO PropPriority = "PRIORITY" + // TODO Alarm component properties // PropAction = "ACTION" // PropRepeat = "REPEAT" // PropTrigger = "TRIGGER"} - calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) - if err != nil {return} - calendar.Data.Component.Children = append(calendar.Data.Component.Children, event.Component) - 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} - return nil + // 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 + + var buf strings.Builder + encoder := ical.NewEncoder(&buf) + err = encoder.Encode(calendar) + if err != nil {return err} + _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar) + if err != nil {return err} + return nil } -func (m model) DelTodo(todo ical.Event) (err error) { +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) - if err != nil {return} - - var newEvents []*ical.Component - for _, component := range calendar.Data.Component.Children { - if component.Name == ical.CompEvent { - var uid string - uid, err = component.Props.Text(ical.PropUID) - if err != nil {return} - 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} + // 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 { + // var uid string + // uid, err = component.Props.Text(ical.PropUID) + // if err != nil {return} + // 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 { + // fmt.Println(err) + return + } + req.SetBasicAuth(m.Creds.Username, m.Creds.Password) + + // Fetch Request + resp, err := client.Do(req) + if err != nil { + // fmt.Println(err) + return + } + defer resp.Body.Close() + + // Read Response Body + // respBody, err := ioutil.ReadAll(resp.Body) + // if err != nil { + // // fmt.Println(err) + // return + // } + + if resp.Status != "204 No Content" {return errors.New("Can't delete, response status: "+resp.Status+".")} + // Display Results + // fmt.Println("response Status : ", resp.Status) + // fmt.Println("response Headers : ", resp.Header) + // fmt.Println("response Body : ", string(respBody)) return nil } @@ -472,8 +570,10 @@ func (m model) DelTodo(todo ical.Event) (err error) { func (m model) EditTodo(todo ical.Event) (err error) { //TODO is there proper edit function ??? + uid,err := todo.Props.Get(ical.PropUID).Text() + if err != nil {return} - err = m.DelTodo(todo) + err = m.DelTodo(uid) if err != nil {return} err = m.UploadTodo(todo) diff --git a/main.go b/main.go index 23a6f1a..c43c129 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,8 @@ package main import ( "fmt" "os" + "net/http" + "crypto/tls" // "sync" // "time" @@ -26,6 +28,7 @@ func errHandler(err error, message string) { } func main() { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} options, err := ParseOptions() errHandler(err, "Error parsing options") m := InitModel() @@ -61,6 +64,11 @@ func main() { os.Exit(1) } + + m.LoginToCalendar() + m.GatherTodos() + + // calendarObjects, err := GetTODOs(calPath) // errHandler(err, "Error getting TODOs") // @@ -142,7 +150,10 @@ func main() { //DEBUG stuff - + // task, err := CreateTodo("testName","description",3,time.Now()) + // errHandler(err,"test fail") + // err = m.UploadTodo(task) + // errHandler(err,"test fail2") //DEBUG stuff diff --git a/options.go b/options.go index 4e631f9..e8a8456 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,7 @@ package main import ( + "os" "errors" "github.com/projectdiscovery/goflags" "sync" @@ -18,6 +19,8 @@ type Options struct { User string Password string + + Proxy string } func ParseOptions() (*Options, error) { @@ -39,6 +42,7 @@ func ParseOptions() (*Options, error) { flagSet.StringVarP(&options.User, "l", "login", "", "WebDAV login username"), flagSet.StringVarP(&options.Password, "p", "password", "", "WebDAV password (forbid filesystem access in WebDAV and don't forget to clean shell history!)"), flagSet.StringVarP(&options.Calendar, "c", "calendar", "", "CalDAV calendar (to-do list) name to use (works only with -u,-l,-p flags)"), + flagSet.StringVarP(&options.Proxy, "P", "proxy", "", "HTTP proxy to debug errors"), // flagSet.BoolVarP(&options.SkipSave, "s", "no-save", false, "skip save to keyring"), //TODO ) @@ -64,5 +68,8 @@ func (options *Options) SanityCheck() error { } } + if options.Proxy != "" {os.Setenv("HTTP_PROXY", options.Proxy)} + + return nil } diff --git a/tui-todo-keys.go b/tui-todo-keys.go new file mode 100644 index 0000000..848d1bd --- /dev/null +++ b/tui-todo-keys.go @@ -0,0 +1,95 @@ +package main + +import ( + + // "github.com/emersion/go-ical" + // "time" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + // "strings" + // "errors" +) + +var ( + appStyle = lipgloss.NewStyle().Padding(1, 2) + + // titleStyle = lipgloss.NewStyle(). + // Foreground(lipgloss.Color("#FFFDF5")). + // Background(lipgloss.Color("#25A065")). + // Padding(0, 1) + + statusMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). + Render +) + +// buttons +func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, ml *list.Model) tea.Cmd { + var title string + var todoUID string + + if i, ok := ml.SelectedItem().(TODO); ok { + title = i.Title() + todoUID = i.UID() + } else { + return nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.choose): + return ml.NewStatusMessage(statusMessageStyle("You chose " + title)) + + case key.Matches(msg, keys.remove): + err := m.DelTodo(todoUID) + 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)) + } + } + + return nil + } + + help := []key.Binding{keys.choose, keys.remove} + + d.ShortHelpFunc = func() []key.Binding { + return help + } + + d.FullHelpFunc = func() [][]key.Binding { + return [][]key.Binding{help} + } + + return d +} + + +type delegateKeyMap struct { + choose key.Binding + remove key.Binding +} + + +func newDelegateKeyMap() *delegateKeyMap { + return &delegateKeyMap{ + choose: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "choose"), + ), + remove: key.NewBinding( + key.WithKeys("x", "backspace"), + key.WithHelp("x", "delete"), + ), + } +} diff --git a/tui-todo.go b/tui-todo.go index 172ceee..0c0b68d 100644 --- a/tui-todo.go +++ b/tui-todo.go @@ -5,6 +5,8 @@ import ( "github.com/emersion/go-ical" "time" "github.com/charmbracelet/bubbles/list" + "strings" + "errors" ) @@ -15,6 +17,11 @@ func (i TODO) Title() string { 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) Description() string { out,err := i.Props.Get(ical.PropDescription).Text() if err != nil {return ""} @@ -29,6 +36,9 @@ func (i TODO) FilterValue() string { + + + func (m *model) GatherTodos() (err error) { //TODO more modular approach calendarObjects, err := GetTODOs(m.Creds.CalendarPath) @@ -62,12 +72,32 @@ func (m *model) GatherTodos() (err error) { itemsTomorrow = append(itemsTomorrow, todo) } - - m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0) + + 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.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.Title = "Tomorrow" + return nil +} + + +func (m *model) UpdateTodos(todo ical.Event) (err error) { + today := time.Now() + tomorrow := time.Now().AddDate(0, 0, 1) + errorI := 0 + if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {m.TodayTab.InsertItem(-1,TODO(todo))} else {errorI += 1} + if strings.HasPrefix(todo.Props["DUE"][0].Value, tomorrow.Format("20060102")) {m.TomorrowTab.InsertItem(-1,TODO(todo)) } else {errorI += 1} + + if errorI == 2 {return errors.New("don't match today and tomorrow")} + + + + return nil } diff --git a/tui.go b/tui.go index 616d756..74b3d7d 100644 --- a/tui.go +++ b/tui.go @@ -3,7 +3,7 @@ package main import ( // "fmt" // "os" - // "time" + "time" // "io" // "strings" @@ -33,7 +33,20 @@ import ( type errMsg struct {message string} -func (m model) errHandler(desc string,err error) (tea.Cmd) { + +//TODO fix multiple errHandlers +//TODO rm me +func (m model) errHandler(err error,desc string) (tea.Cmd) { + if err != nil { + output := desc+": "+err.Error() + return func() tea.Msg { return errMsg{output} } + // return errMsg{output} + // m.Send(output) + } + + return nil +} +func errHandler_tui(err error,desc string) (tea.Cmd) { if err != nil { output := desc+": "+err.Error() return func() tea.Msg { return errMsg{output} } @@ -46,34 +59,46 @@ func (m model) errHandler(desc string,err error) (tea.Cmd) { - - - - - - - - - - - func (m model) Init() tea.Cmd { return textinput.Blink // return nil } - -//TODO add changing calendar - - - func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //TODO separate to funcs var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: - if m.ActiveWindow == "calendarChoose" { + switch m.ActiveWindow { + case "today": //TODO rm me debug + switch keypress := msg.String(); keypress { + case "q", "ctrl+c": + // m.quitting = true + return m, tea.Quit + + case "y": + // m."2797322749061742597" + case "h": + + todoInfo := TodoInterface{ + name: "testName1", + description: "description", + priority: 3, + dueTime: time.Now(), + alarmOffset: "1h", + } + task, err := CreateTodo(todoInfo) + if err != nil {return m, m.errHandler(err,"test fail")} + err = m.UploadTodo(task) + if err != nil {return m, m.errHandler(err,"test fail2")} + m.UpdateTodos(task) + // fmt.Println(&task) + // fmt.Println(err) + // time.Sleep(2*time.Second) + return m, nil + } + case "calendarChoose": switch keypress := msg.String(); keypress { case "q", "ctrl+c": // m.quitting = true @@ -91,49 +116,49 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } err := m.CredentialsSave() - if err != nil { return m, m.errHandler("Failed to save credentials",err)} + if err != nil { return m, m.errHandler(err,"Failed to save credentials")} m.CalendarToTodo() return m, nil } - } - if m.ActiveWindow == "login" { - switch keypress := msg.String(); keypress { - case "ctrl+c", "q": - return m, tea.Quit - case "enter": - if m.focused == len(m.loginInputs)-1 { - //TODO check that we have all fields not empty and notificate about it - //TODO submit - for i := range m.loginInputs { - m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) - } - // fmt.Println(m.loginInputs[url].Value())//DEBUG - m.Creds.URL = m.loginInputs[url].Value() - m.Creds.Username = m.loginInputs[login].Value() - m.Creds.Password = m.loginInputs[pass].Value() - // fmt.Println(m.Creds.URL)//DEBUG - - // time.Sleep(1 * time.Second) ///DEBUG - // m.Creds. - // return m, nil - err := m.LoginToCalendar() - if err != nil {return m, m.errHandler("Failed to authenticate",err)} - // try login -> choose calendar -> store -> move to getting stuff - return m, nil - // return m, tea.Quit + + case "login": + switch keypress := msg.String(); keypress { + case "ctrl+c", "q": + return m, tea.Quit + case "enter": + if m.focused == len(m.loginInputs)-1 { + //TODO check that we have all fields not empty and notificate about it + //TODO submit + for i := range m.loginInputs { + m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) + } + // fmt.Println(m.loginInputs[url].Value())//DEBUG + m.Creds.URL = m.loginInputs[url].Value() + m.Creds.Username = m.loginInputs[login].Value() + m.Creds.Password = m.loginInputs[pass].Value() + // fmt.Println(m.Creds.URL)//DEBUG + + // time.Sleep(1 * time.Second) ///DEBUG + // m.Creds. + // return m, nil + err := m.LoginToCalendar() + if err != nil {return m, m.errHandler(err,"Failed to authenticate")} + // try login -> choose calendar -> store -> move to getting stuff + return m, nil + // return m, tea.Quit + } + m.nextInput() + case "shift+tab", "up": + m.prevInput() + case "tab", "down": + m.nextInput() } - m.nextInput() - case "shift+tab", "up": - m.prevInput() - case "tab", "down": - m.nextInput() - } - for i := range m.loginInputs { - m.loginInputs[i].Blur() - } - m.loginInputs[m.focused].Focus() - + for i := range m.loginInputs { + m.loginInputs[i].Blur() + } + m.loginInputs[m.focused].Focus() + } switch keypress := msg.String(); keypress {