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

149
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"
@ -234,31 +234,6 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err
// 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
@ -318,7 +293,9 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
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
@ -330,7 +307,6 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// tmpTODO.Time = todoTime // tmpTODO.Time = todoTime
// } // }
output = append(output, tmpTODO) output = append(output, tmpTODO)
} }
// } // }
@ -367,18 +343,6 @@ 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
@ -390,17 +354,27 @@ type TodoInterface struct {
//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()
minute := info.dueTime.Minute()
if (hour != 0 || minute != 0 ) {
event.Props.SetDateTime(ical.PropDue, info.dueTime)
} else {
event.Props.SetDate(ical.PropDue, info.dueTime)
}
// }
} // 'zero' time is `time.Time{}`
switch info.priority { switch info.priority {
case 0: //No priority case 0: //No priority
event.Props.SetText(ical.PropPriority, "0") event.Props.SetText(ical.PropPriority, "0")
@ -420,10 +394,9 @@ func CreateTodo(info TodoInterface) (event ical.Event,err error) {
// alarmComponent := ical.Component{Name:ical.CompAlarm} // alarmComponent := ical.Component{Name:ical.CompAlarm}
alarmComponent := ical.NewComponent(ical.CompAlarm) alarmComponent := ical.NewComponent(ical.CompAlarm)
alarmComponent.Props.SetText(ical.PropAction, "DISPLAY") alarmComponent.Props.SetText(ical.PropAction, "DISPLAY")
// alarmComponent.Props.Add(&ical.Prop{Name:ical.PropAction,Value:"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.Add(&ical.Prop{Name: ical.PropDescription, Value: "Default Alarm Tempus description"})
alarmComponent.Props.SetText(ical.PropDescription, "Default Alarm Tempus description") alarmComponent.Props.SetText(ical.PropDescription, "Default Alarm Tempus description")
// ACTION:DISPLAY // ACTION:DISPLAY
// DESCRIPTION:Default Tasks.org description // DESCRIPTION:Default Tasks.org description
@ -456,7 +429,6 @@ func CreateTodo(info TodoInterface) (event ical.Event,err error) {
event.Children = append(event.Children, alarmComponent) event.Children = append(event.Children, alarmComponent)
} }
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC()) 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.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()) event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC())
@ -464,8 +436,6 @@ func CreateTodo(info TodoInterface) (event ical.Event,err error) {
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)
@ -483,20 +453,25 @@ func (m model) UploadTodo(event ical.Event) (err error) {
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 {
return err
}
//TODO check GUID uniq and regenerate if needed //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) {
@ -539,12 +514,16 @@ func (m model) DelTodo(todo ical.Event) (err error) {
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 {
return
}
req.SetBasicAuth(m.Creds.Username, m.Creds.Password) 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 {
return
}
// resp.Body.Close() // resp.Body.Close()
defer resp.Body.Close() defer resp.Body.Close()
@ -555,7 +534,9 @@ func (m model) DelTodo(todo ical.Event) (err error) {
// 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
@ -568,24 +549,38 @@ func (m model) DelTodo(todo ical.Event) (err error) {
eventUID, err := event.Props.Get(ical.PropUID).Text() eventUID, err := event.Props.Get(ical.PropUID).Text()
if err != nil {eventUID = ""} if err != nil {
if (eventUID == delUID) { 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 {
return err
}
sum2, err := event.Props.Get("SUMMARY").Text() sum2, err := event.Props.Get("SUMMARY").Text()
if err != nil {return err} if err != nil {
return err
}
if sum1 == sum2 { if sum1 == sum2 {
req2, err := http.NewRequest("DELETE", baseURL+calObj.Path, nil) req2, err := http.NewRequest("DELETE", baseURL+calObj.Path, nil)
if err != nil {return err} if err != nil {
return err
}
req2.SetBasicAuth(m.Creds.Username, m.Creds.Password) req2.SetBasicAuth(m.Creds.Username, m.Creds.Password)
// Fetch Request // Fetch Request
resp2, err := client.Do(req2) resp2, err := client.Do(req2)
if err != nil {return err} if err != nil {
return err
}
defer resp2.Body.Close() defer resp2.Body.Close()
if resp2.Status == "204 No Content" {return nil} else {return errors.New("Can't delete, response status: "+resp2.Status+".")} if resp2.Status == "204 No Content" {
return nil
} else {
return errors.New("Can't delete, response status: " + resp2.Status + ".")
}
//TODO exit for loop //TODO exit for loop
// return errors.New("test") // return errors.New("test")
@ -608,14 +603,15 @@ func (m model) DelTodo(todo ical.Event) (err error) {
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 {
@ -629,16 +625,19 @@ func (m model) EditTodo(todo ical.Event) (err error) {
// 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 {
return
}
offset := rOptions.Interval offset := rOptions.Interval
var currentDUE time.Time var currentDUE time.Time
currentDUE, err = todo.Props.DateTime("DUE", nil) currentDUE, err = todo.Props.DateTime("DUE", nil)
if err != nil {return} if err != nil {
return
}
switch rOptions.Freq { switch rOptions.Freq {
case rrule.DAILY: case rrule.DAILY:
currentDUE = currentDUE.AddDate(0, 0, offset) currentDUE = currentDUE.AddDate(0, 0, offset)
@ -654,7 +653,9 @@ func (m model) CompleteTodo(todo ical.Event) (err error) {
currentDUE = currentDUE.Add(time.Minute * time.Duration(offset)) currentDUE = currentDUE.Add(time.Minute * time.Duration(offset))
case rrule.SECONDLY: //TODO didn't debug case rrule.SECONDLY: //TODO didn't debug
currentDUE = currentDUE.Add(time.Second * time.Duration(offset)) currentDUE = currentDUE.Add(time.Second * time.Duration(offset))
//TODO case if nothing changed
} }
todo.Props.SetDateTime("DUE", currentDUE) todo.Props.SetDateTime("DUE", currentDUE)
} else { } else {
todo.Props.SetText(ical.PropStatus, "COMPLETED") todo.Props.SetText(ical.PropStatus, "COMPLETED")
@ -663,7 +664,9 @@ func (m model) CompleteTodo(todo ical.Event) (err error) {
} }
err = m.EditTodo(todo) err = m.EditTodo(todo)
if err != nil {return} if err != nil {
return
}
return nil return nil
} }

View File

@ -95,11 +95,12 @@ 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,

24
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) {
@ -49,7 +53,7 @@ func main() {
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 == "" {
@ -72,14 +76,11 @@ func main() {
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!
@ -111,12 +112,12 @@ func main() {
} }
//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,15 +1,17 @@
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
@ -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,18 +47,61 @@ 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() {
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) 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() {
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-- m.focused--
// Wrap around // Wrap around
if m.focused < 0 { if m.focused < 0 {
@ -65,13 +109,18 @@ func (m *model) prevInput() {
} }
} }
}
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{}
@ -95,6 +144,7 @@ func (m *model) LoginToCalendar() (err error) {
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
@ -22,8 +19,12 @@ type model struct {
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
@ -41,46 +42,104 @@ type Credentials struct {
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,60 +1,170 @@
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 {
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() out, err := i.Props.Get(ical.PropSummary).Text()
if err != nil {return "<EMPTY>"} if err != nil {
return out return "<EMPTY>"
}
return priority + dueTime + out
} }
func (i TODO) UID() string { func (i TODO) UID() string {
out, err := i.Props.Get(ical.PropUID).Text() out, err := i.Props.Get(ical.PropUID).Text()
if err != nil {return "<EMPTY>"} if err != nil {
return "<EMPTY>"
}
return out return out
} }
func (i TODO) PriorityColor() lipgloss.Color {
clr, err := i.Props.Get("PRIORITY").Int()
c := lipgloss.Color("255") //White
if err != nil {
return c
}
switch clr{
case 9:
c = lipgloss.Color("27") //Blue
case 5:
c = lipgloss.Color("220") //Yellow
case 1:
// c = lipgloss.Color("#FF9999") //Red
c = lipgloss.Color("161") //Red
}
return c
}
func (i TODO) Description() string { 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 {
// index
out1, err1 := i.Props.Get(ical.PropSummary).Text() out1, err1 := i.Props.Get(ical.PropSummary).Text()
out2, err2 := i.Props.Get(ical.PropDescription).Text() out2, err2 := i.Props.Get(ical.PropDescription).Text()
if err1 != nil && err2 != nil {return ""} if err1 != nil && err2 != nil {
return ""
}
return out1 + out2 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))
@ -63,7 +173,6 @@ func (m *model) GatherTodos() (err error) {
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
for _, todo := range todayTodos { for _, todo := range todayTodos {
@ -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
}
if errorI == 2 {
// return errors.New("don't match today and tomorrow") // debug???
return nil
}
return nil return nil
} }

183
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