From f4fe3f301d01fd4541438e6c5846263124a7c42e Mon Sep 17 00:00:00 2001 From: Casual Date: Sat, 29 Jun 2024 04:00:39 +0300 Subject: [PATCH] v0.9 login,todoList,keyring,caldav(read),codeRefactor... --- caldav.go | 410 ++++++++++++++++++++++++++++++++++++++++------- go.mod | 3 +- go.sum | 2 + keyring.go | 121 ++++++++------ main.go | 178 ++++++++++++-------- options.go | 30 ++-- tui-calChoose.go | 64 ++++++++ tui-login.go | 100 ++++++++++++ tui-model.go | 88 ++++++++++ tui-todo.go | 73 +++++++++ tui.go | 357 +++++++++++++++++++++-------------------- 11 files changed, 1068 insertions(+), 358 deletions(-) create mode 100644 tui-calChoose.go create mode 100644 tui-login.go create mode 100644 tui-model.go create mode 100644 tui-todo.go diff --git a/caldav.go b/caldav.go index 9e7808f..b4c89dd 100644 --- a/caldav.go +++ b/caldav.go @@ -5,21 +5,28 @@ import ( // "fmt" webdav "github.com/emersion/go-webdav" "github.com/emersion/go-webdav/caldav" + "github.com/emersion/go-ical" "strings" + "github.com/google/uuid" "time" + "errors" + // "fmt" ) -type TODO struct { - Name string - Desc string - Time string - // Priority int //TODO - // Subtasks []TODO //TODO - // Repeat //TODO - // Alarm //TODO -} +// type TODO struct { +// Name string +// Desc string +// Time string //only parsed _time_ +// // Priority int //TODO +// // Subtasks []TODO //TODO +// // Repeat //TODO +// // Alarm //TODO +// UID string //for editing events +// DueDateTime time.Time //for adding new events +// } +//TODO Is it safe to create global variables? var clientWebDAV *webdav.Client var client *caldav.Client // clientCalDAV // var calendarObjects []caldav.CalendarObject @@ -27,17 +34,36 @@ var ctx = context.Background() // var authSession caldav.Client // clientCalDAV -func (options *Options) InitDAVclients() error { +func InitDAVclients(url,user,pass string) error { var err error - authSession := webdav.HTTPClientWithBasicAuth(nil, options.User, options.Password) + authSession := webdav.HTTPClientWithBasicAuth(nil, user,pass) - clientWebDAV, err = webdav.NewClient(authSession, options.URL) + clientWebDAV, err = webdav.NewClient(authSession, url) if err != nil { // Handle error return err } - client, err = caldav.NewClient(authSession, options.URL) + client, err = caldav.NewClient(authSession, url) + if err != nil { + // Handle error + return err + } + return nil +} + +func (options *Options) InitDAVclients() error { + + err := InitDAVclients(options.URL,options.User, options.Password) + if err != nil { + // Handle error + return err + } + return nil +} +func (m model) InitDAVclients() error { + + err := InitDAVclients(m.Creds.URL, m.Creds.Username, m.Creds.Password) if err != nil { // Handle error return err @@ -70,6 +96,7 @@ func GetCalendars() ([]caldav.Calendar, error) { } func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err error) { +//TODO can we use calendar.Data.Component.Children to dont make this close to pointless request? date := time.Now() dateStart := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()).AddDate(0, 0, -1) //TODO too complex - time.Now().Add(-92 * time.Hour), // dateEnd:= time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 0, date.Location()) //date +1 day @@ -111,60 +138,195 @@ func GetTODOs(calendarPath string) (calendarObjects []caldav.CalendarObject, err return calendarObjects, nil } -func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, error) { - var output []TODO +// func ParseDueDateTODOs_depricated(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, error) { +// var output []TODO +// +// for _, calObj := range calObjs { +// // fmt.Println((*(*calObj.Data).Children[0]).Name) +// // TODO STATUS map[] COMPLETED +// for _, event := range (*calObj.Data).Children { +// // if (*event).Name == "VTODO" { +// // var notCompletedTODO, withDate, fromToday bool +// var notCompletedTODO, fromToday bool +// //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis +// //TODO we can use event.Props.Get +// // notCompletedTODO +// if (*event).Props["COMPLETED"] == nil { +// if (*event).Props["STATUS"] == nil { +// notCompletedTODO = true +// } else { +// if (*event).Props["STATUS"][0].Value != "COMPLETED" { +// notCompletedTODO = true +// } +// } +// } +// +// // withTodayDate +// if (*event).Props["DUE"] != nil { +// // withDate = true +// // fromToday +// if strings.HasPrefix((*event).Props["DUE"][0].Value, date.Format("20060102")) { +// fromToday = true +// } +// } +// +// // if notCompletedTODO && withDate && fromToday { +// if notCompletedTODO && fromToday { +// +// var tmpTODO TODO +// +// if (*event).Props["SUMMARY"] != nil { +// name := (*event).Props["SUMMARY"][0].Value +// tmpTODO.Name = name +// } else { +// tmpTODO.Name = "" +// } +// if (*event).Props["DESCRIPTION"] != nil { +// description := (*event).Props["DESCRIPTION"][0].Value +// tmpTODO.Desc = description +// } +// +// var todoTime string +// due := (*event).Props["DUE"][0].Value +// index := strings.Index(due, "T") +// if index != -1 { +// str := due[index+1:] +// todoTime = str[:2] + ":" + str[2:4] +// +// tmpTODO.Time = todoTime +// } +// output = append(output, tmpTODO) +// } +// // } +// } +// } +// +// // //TODO sort: time, priority (if no time) +// // // it means, put DUE at first, other DUE;VALUE=DATE in priority order +// // //TODO color: priority, add time icon if have time +// // //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 if no repeat - mark as complted +// // STATUS:COMPLETED +// // COMPLETED:20240421T065323Z +// // PERCENT-COMPLETE:100 +// +// //TODO support notifcations/alarms??? +// // BEGIN:VTODO +// // ... +// // BEGIN:VALARM +// // TRIGGER;RELATED=END:-PT15M +// // ACTION:DISPLAY +// // DESCRIPTION:Default Tasks.org description +// // END:VALARM +// // END:VTODO +// +// return output, nil +// } + + + + + + + + + + + + + + + + + + + + + + + + + + +func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.Event, error) { + var output []ical.Event for _, calObj := range calObjs { + // fmt.Println((*(*calObj.Data).Children[0]).Name) // TODO STATUS map[] COMPLETED - for _, event := range (*calObj.Data).Children { + // for _, event := range (*calObj.Data).Children { + // event + // for _, event1 := range (*calObj.Data).Children { + // // fmt.Println("1:", event1) + // tmp := ical.Event{Component: event1} + // fmt.Println(tmp.Props.Get(ical.PropUID).Text()) + // // for _, event2 := range tmp.Events() { + // // // for _, event2 := range event1.Events() { + // // fmt.Println("1:", event2) + // // } + // } + + for _, eventComponent:= range (*calObj.Data).Children { + event := ical.Event{Component: eventComponent} + // for _, event := range (*calObj.Data).Children { + // fmt.Println("1:", event) //TODO rm me // if (*event).Name == "VTODO" { - var notCompletedTODO, withDate, fromToday bool + // var notCompletedTODO, withDate, fromToday bool + var notCompletedTODO, fromToday bool //TODO we can optimize there if we encounter wrong state to forcefully stop next analysis + //TODO we can use event.Props.Get // notCompletedTODO - if (*event).Props["COMPLETED"] == nil { - if (*event).Props["STATUS"] == nil { + if event.Props["COMPLETED"] == nil { + if event.Props["STATUS"] == nil { notCompletedTODO = true } else { - if (*event).Props["STATUS"][0].Value != "COMPLETED" { + if event.Props["STATUS"][0].Value != "COMPLETED" { notCompletedTODO = true } } } // withTodayDate - if (*event).Props["DUE"] != nil { - withDate = true + if event.Props["DUE"] != nil { + // withDate = true // fromToday - if strings.HasPrefix((*event).Props["DUE"][0].Value, date.Format("20060102")) { + if strings.HasPrefix(event.Props["DUE"][0].Value, date.Format("20060102")) { fromToday = true } } - if notCompletedTODO && withDate && fromToday { + // if notCompletedTODO && withDate && fromToday { + if notCompletedTODO && fromToday { - var tmpTODO TODO - - if (*event).Props["SUMMARY"] != nil { - name := (*event).Props["SUMMARY"][0].Value - tmpTODO.Name = name - } else { - tmpTODO.Name = "" + tmpTODO := event + + if event.Props["SUMMARY"] == nil { + tmpTODO.Props.SetText(ical.PropSummary, "") } - if (*event).Props["DESCRIPTION"] != nil { - description := (*event).Props["DESCRIPTION"][0].Value - tmpTODO.Desc = description + if event.Props["DESCRIPTION"] == nil { + tmpTODO.Props.SetText(ical.PropDescription, "") } + - var todoTime string - due := (*event).Props["DUE"][0].Value - index := strings.Index(due, "T") - if index != -1 { - str := due[index+1:] - todoTime = str[:2] + ":" + str[2:4] +// var todoTime string +// due := (*event).Props["DUE"][0].Value +// index := strings.Index(due, "T") +// if index != -1 { +// str := due[index+1:] +// todoTime = str[:2] + ":" + str[2:4] +// +// tmpTODO.Time = todoTime +// } - tmpTODO.Time = todoTime - } + output = append(output, tmpTODO) } // } @@ -179,23 +341,157 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]TODO, // //TODO PRIORITY:1 #1-high, 5-mid, 9-low //TODO repeat function??? + //TODO on complete -repeat function + // RRULE:FREQ=WEEKLY;INTERVAL=1 + + //TODO if no repeat - mark as complted + // STATUS:COMPLETED + // COMPLETED:20240421T065323Z + // PERCENT-COMPLETE:100 + + //TODO support notifcations/alarms??? + // BEGIN:VTODO + // ... + // BEGIN:VALARM + // TRIGGER;RELATED=END:-PT15M + // ACTION:DISPLAY + // DESCRIPTION:Default Tasks.org description + // END:VALARM + // END:VTODO + // fmt.Println(output) //TODO rm me + // time.Sleep(3*time.Second)//TODO rm me + return output, nil } -//TODO on complete -repeat function -// RRULE:FREQ=WEEKLY;INTERVAL=1 -//TODO if no repeat - mark as complted -// STATUS:COMPLETED -// COMPLETED:20240421T065323Z -// PERCENT-COMPLETE:100 -//TODO support notifcations/alarms??? -// BEGIN:VTODO -// ... -// BEGIN:VALARM -// TRIGGER;RELATED=END:-PT15M -// ACTION:DISPLAY -// DESCRIPTION:Default Tasks.org description -// END:VALARM -// END:VTODO + + + + + + + + + + +// type todoInterface struct { +// name string +// description string +// priority int +// dueTime time.Time +// name string +// } + + +func CreateTodo(name,description string, priority int, dueTime time.Time) (event ical.Event,err error) { + uid, err := uuid.NewUUID() + if err != nil {return} + event = *ical.NewEvent() + 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 { + case 0: //No priority + event.Props.SetText(ical.PropPriority, "0") + case 1: //Light + event.Props.SetText(ical.PropPriority, "1") + case 2: //Medium + event.Props.SetText(ical.PropPriority, "5") + case 3: //Urgent + event.Props.SetText(ical.PropPriority, "9") + default: + err = errors.New("Wrong priority, expecting 0-3") + return + } + + 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()) + return +} + + + +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 + +} + + +func (m model) DelTodo(todo ical.Event) (err error) { + + 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} + + return nil +} + + +func (m model) EditTodo(todo ical.Event) (err error) { + //TODO is there proper edit function ??? + + err = m.DelTodo(todo) + if err != nil {return} + + err = m.UploadTodo(todo) + if err != nil { + //TODO panic, we deleted but cant upload event. Need to save somehow. Or at least write debug string with parameters so we can re-add manually in that case + return + } + + return nil +} + +//TODO +func (m model) CompleteTodo(todo ical.Event) (err error) { + + //TODO if it repitable - Repeate, otherwise complete it + + err = m.EditTodo(todo) + if err != nil {return} + + return nil +} diff --git a/go.mod b/go.mod index 2b74a27..d73fb3b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.26.4 github.com/charmbracelet/lipgloss v0.11.0 + github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f github.com/emersion/go-webdav v0.5.0 + github.com/google/uuid v1.3.1 github.com/projectdiscovery/goflags v0.1.56 github.com/zalando/go-keyring v0.2.5 golang.org/x/term v0.18.0 @@ -24,7 +26,6 @@ require ( github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/danieljoos/wincred v1.2.0 // indirect - github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/go.sum b/go.sum index 14affad..da3dffc 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/keyring.go b/keyring.go index 1fa35f1..96c1e0c 100644 --- a/keyring.go +++ b/keyring.go @@ -1,19 +1,18 @@ package main import ( - // "log" + // "log" - "github.com/zalando/go-keyring" - // "github.com/99designs/keyring" + "github.com/zalando/go-keyring" + // "github.com/99designs/keyring" "encoding/base64" - // "fmt" - "strings" + // "fmt" + "strings" ) - const ( service = "Tempus" - user = "login" + user = "login" ) // func debugKeyring() { @@ -26,15 +25,15 @@ const ( // if err != nil { // log.Fatal(err) // } -// +// // // get password // secret, err := keyring.Get(service, user) // if err != nil { // log.Fatal(err) // } -// +// // log.Println(secret) -// +// // decodedByte, err := base64.StdEncoding.DecodeString(secret) // decoded := string(decodedByte) // if err != nil { @@ -44,52 +43,74 @@ const ( // log.Println(decoded) // } -func storeCredentialsToKeyring(url,login,password string) error { +//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?') + +func storeCredentialsToKeyring(url, login, password, calendar string) error { url = base64.StdEncoding.EncodeToString([]byte(url)) login = base64.StdEncoding.EncodeToString([]byte(login)) password = base64.StdEncoding.EncodeToString([]byte(password)) + calendar = base64.StdEncoding.EncodeToString([]byte(calendar)) - credentials := url + " " + login + " " + password - - err := keyring.Set(service, user, credentials) - if err != nil { - return err - } - return nil + credentials := url + " " + login + " " + password + " " + calendar + + err := keyring.Set(service, user, credentials) + if err != nil { + return err + } + return nil } -func getCredentialsFromKeyring() (url,login,password string, err error) { - secret, err := keyring.Get(service, user) - if err != nil { - return "","","",err - } +func getCredentialsFromKeyring_wrapper() (url, login, password, calendar string, err error) { + secret, err := keyring.Get(service, user) + if err != nil { + return + } - v := strings.Split(secret," ") + v := strings.Split(secret, " ") - urlByte, err := base64.StdEncoding.DecodeString(v[0]) - if err != nil { - return "","","",err - } - loginByte, err := base64.StdEncoding.DecodeString(v[1]) - if err != nil { - return "","","",err - } - passwordByte, err := base64.StdEncoding.DecodeString(v[2]) - if err != nil { - return "","","",err - } - - url = string(urlByte) - login = string(loginByte) - password = string(passwordByte) - - - return + urlByte, err := base64.StdEncoding.DecodeString(v[0]) + if err != nil { + return + } + loginByte, err := base64.StdEncoding.DecodeString(v[1]) + if err != nil { + return + } + passwordByte, err := base64.StdEncoding.DecodeString(v[2]) + if err != nil { + return + } + calendarByte, err := base64.StdEncoding.DecodeString(v[3]) + if err != nil { + return + } + + url = string(urlByte) + login = string(loginByte) + password = string(passwordByte) + calendar = string(calendarByte) + + return } +func getCredentialsFromKeyring() (Credentials,error) { + //TODO inconsistent approach compared to caldav.go + url,username,password,calendar,err := getCredentialsFromKeyring_wrapper() + if err != nil {return Credentials{},err} + return Credentials{ + URL:url, + Username:username, + Password:password, + CalendarPath:calendar, + }, nil + +} + // func debugKeyring() { -// +// // kr, err := keyring.Open(keyring.Config{ // AllowedBackends: []keyring.BackendType{ // keyring.SecretServiceBackend, @@ -100,7 +121,7 @@ func getCredentialsFromKeyring() (url,login,password string, err error) { // if err != nil { // log.Fatal(err) // } -// +// // err = kr.Set(keyring.Item{ // Key: "foo", // // Data: []byte("secret-bar"), @@ -108,14 +129,14 @@ func getCredentialsFromKeyring() (url,login,password string, err error) { // if err != nil { // log.Fatal(err) // } -// +// // v, err := kr.Get("llamas") // if err != nil { // log.Fatal(err) // } -// +// // log.Printf("llamas was %v", v) -// -// -// +// +// +// // } diff --git a/main.go b/main.go index fcfe7d0..23a6f1a 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,20 @@ package main import ( "fmt" "os" - "sync" - "time" + // "sync" + // "time" + // "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/bubbles/list" + + "github.com/emersion/go-webdav/caldav" + // "slices" + // "strconv" ) -var waitGroup sync.WaitGroup +// var waitGroup sync.WaitGroup func errHandler(err error, message string) { if err != nil { @@ -24,77 +28,123 @@ func errHandler(err error, message string) { func main() { options, err := ParseOptions() errHandler(err, "Error parsing options") - - + m := InitModel() // debugKeyring() - err = storeCredentialsToKeyring("https://pkg.go.dev/encoding/base64#Encoding.EncodeToString","casual","h>ÕdzPlÇqQ+çCQ{ð±;Kм7¸Âhð~Ümy)v") - errHandler(err, "Error parsing options") + // err = storeCredentialsToKeyring("https://pkg.go.dev/encoding/base64#Encoding.EncodeToString", "casual", "h>ÕdzPlÇqQ+çCQ{ð±;Kм7¸Âhð~Ümy)v") + // errHandler(err, "Error parsing options:") -// url1,login1,password1,err := getCredentialsFromKeyring() -// errHandler(err, "Error parsing options") -// m.loginInputs[url].Value() -// m.loginInputs[login].Value() -// m.loginInputs[pass].Value() + // if we provide login credentials via cli + var calendars []caldav.Calendar + if options.URL != "" { + err = options.InitDAVclients() + errHandler(err, "Unexpected error (we couldn't initiate WebDAV/CalDAV client)") + calendars, err = GetCalendars() + errHandler(err, "Error getting calendars (incorrect url/login/password)") - // if true {os.Exit(1)} + var found bool + // var calPath string + if options.Calendar != "" { + for _,calendar := range calendars { + if calendar.Name == options.Calendar { + found = true + // calPath = calendar.Path + m.Creds.CalendarPath = calendar.Path + } + } + if ! found { + fmt.Println("we don't have calendar ", options.Calendar, ". We have:") + for _,calendar := range calendars { + fmt.Println(calendar.Name) + } + os.Exit(1) + } +// calendarObjects, err := GetTODOs(calPath) +// errHandler(err, "Error getting TODOs") +// +// today := time.Now() //TODO move to tui and remove it +// todayTodos, err := ParseDueDateTODOs(calendarObjects, today) +// tomorrow := time.Now().AddDate(0, 0, 1) +// tomorrowTodos, err := ParseDueDateTODOs(calendarObjects, tomorrow) +// //TODO remove it +// fmt.Println("In total we have", len(calendarObjects), "todos") +// //TODO remove it +// var itemsToday []list.Item +// var itemsTomorrow []list.Item +// for _, todo := range todayTodos { +// itemsToday = append(itemsToday, todo) +// } +// for _, todo := range tomorrowTodos { +// itemsTomorrow = append(itemsTomorrow, todo) +// } +//TODO remove it + m.GatherTodos() + + // m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0) + // m.TodayTab.Title = "Today" + // m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0) + // m.TomorrowTab.Title = "Tomorrow" + m.LoggedIn = true + m.ActiveWindow = "today" + + } else { + //TODO go to calendars page + // m.LoggedIn = true + m.ActiveWindow = "calendarChoose" - - - - - - - - - - - - - - options.InitDAVclients() - - calendars, err := GetCalendars() - errHandler(err, "Error getting calendars") - - for _, calendar := range calendars { - fmt.Println(calendar.Name, "-", calendar.Path) + // items := []list.Item{ + // item("Ramen"), + // item("Tomato Soup"), + // item("Hamburgers"), + // item("Cheeseburgers"), + // item("Currywurst"), + // item("Okonomiyaki"), + // item("Pasta"), + // item("Fillet Mignon"), + // item("Caviar"), + // item("Just Wine"), + // } + } + + } else { + //TODO I'm on a highway to (IfElse) hell! + //TODO probably need to do more careful debug + creds, err := getCredentialsFromKeyring() + m.Creds = creds + if err != nil { + //we don't have saved credentials, go to login page + m.ActiveWindow = "login" + } else { + err = m.LoginToCalendar() + if err != nil { + //we have problem to auth! Go to relogin + //TODO say that there is a problem with login + m.ActiveWindow = "login" + } else { + err = m.GatherTodos() + if err != nil { + //we have problem to auth! Go to relogin + //TODO say that there is a problem with login + m.ActiveWindow = "login" + } else { + m.LoggedIn = true + m.ActiveWindow = "today" + } + } + + } + } - calendarObjects, err := GetTODOs(calendars[1].Path) - errHandler(err, "Error getting TODOs") - today := time.Now() - todayTodos, err := ParseDueDateTODOs(calendarObjects, today) - tomorrow := time.Now().AddDate(0,0,1) - tomorrowTodos, err := ParseDueDateTODOs(calendarObjects, tomorrow) + //DEBUG stuff + + //DEBUG stuff - // fmt.Println(todos) - - fmt.Println("In total we have", len(calendarObjects), "todos") - - var itemsToday []list.Item - var itemsTomorrow []list.Item - - for _,todo := range todayTodos { - itemsToday = append(itemsToday,todo) - } - - for _,todo := range tomorrowTodos { - itemsTomorrow = append(itemsTomorrow,todo) - } - - m := InitModel() - m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0) - m.TodayTab.Title = "Today" - m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0) - m.TomorrowTab.Title = "Tomorrow" - // m.LoggedIn = true - m.ActiveWindow = "login" p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { @@ -102,5 +152,7 @@ func main() { os.Exit(1) } - fmt.Println(m.loginInputs[login].Value()) + // fmt.Println(m.) + + } diff --git a/options.go b/options.go index 1799703..4e631f9 100644 --- a/options.go +++ b/options.go @@ -10,9 +10,11 @@ var onceOptions sync.Once var options = &Options{} type Options struct { - URL string - Threads int + URL string + // Threads int // Verbose bool + SkipSave bool + Calendar string User string Password string @@ -26,15 +28,18 @@ func ParseOptions() (*Options, error) { flagSet := goflags.NewFlagSet() flagSet.SetDescription("Example - description TODO") - flagSet.CreateGroup("input", "Input", - flagSet.StringVarP(&options.URL, "u", "url", "", "target's url"), - flagSet.IntVarP(&options.Threads, "t", "threads", 10, "threads to run"), //TODO add estimate counter to packets/s - // flagSet.StringVarP(&options.URL, "u", "url", "", "verbose"), - ) + // flagSet.CreateGroup("input", "Input", + + // flagSet.IntVarP(&options.Threads, "t", "threads", 10, "threads to run"), //TODO add estimate counter to packets/s + // flagSet.StringVarP(&options.URL, "u", "url", "", "verbose"), + // ) flagSet.CreateGroup("debug", "WebDAV Debug", - flagSet.StringVarP(&options.User, "l", "login", "", "WebDAV login"), - flagSet.StringVarP(&options.Password, "p", "password", "", "WebDAV password (forbid filesystem access!!!)"), + flagSet.StringVarP(&options.URL, "u", "url", "", "target's url"), + 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.BoolVarP(&options.SkipSave, "s", "no-save", false, "skip save to keyring"), //TODO ) // flagSet.CreateGroup("debug", "Debug", @@ -52,8 +57,11 @@ func ParseOptions() (*Options, error) { func (options *Options) SanityCheck() error { - if options.URL == "" { - return errors.New("-u flag must present") + if (options.URL != "") || (options.User != "") || (options.Password != "") { + if (options.URL != "") && (options.User != "") && (options.Password != "") { + } else { + return errors.New("-u,-l,-p flags must present") + } } return nil diff --git a/tui-calChoose.go b/tui-calChoose.go new file mode 100644 index 0000000..f8987f4 --- /dev/null +++ b/tui-calChoose.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + // "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbletea" + "io" + "strings" + // "github.com/charmbracelet/lipgloss" +) + +var ( //Calendars choose + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = 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 (m model) RenderCalendarChooser() string { + return m.calendarList.View() +} + +type item string + +func (i item) FilterValue() string { return "" } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *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("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + + +func (m *model) CalendarToTodo() (err error) { + + m.LoggedIn = true + m.GatherTodos() + m.ActiveWindow = "today" + + + return nil +} diff --git a/tui-login.go b/tui-login.go new file mode 100644 index 0000000..dbdf065 --- /dev/null +++ b/tui-login.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" +) + +var docStyle = lipgloss.NewStyle().Margin(1, 2) + + +var loginStyle = lipgloss.NewStyle().Width(40).Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder()) + +var inputStyle = lipgloss.NewStyle() + +const ( + url = iota + login + pass +) + + +func (m model) RenderLogin() string { + + return fmt.Sprintf( + `%s + + +%s +%s + +%s +%s + +%s +%s + + +%s + `, + + inputStyle.Width(30).Align(lipgloss.Center).Render("Login"), + inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("WebDAV server URL"), + m.loginInputs[url].View(), + inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Login"), + m.loginInputs[login].View(), + inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Password"), + m.loginInputs[pass].View(), //TODO hide + inputStyle.Render("Continue ->"), + ) + // .Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder()) + +} + +func (m *model) nextInput() { + m.focused = (m.focused + 1) % len(m.loginInputs) +} + +// prevInput focuses the previous input field +func (m *model) prevInput() { + m.focused-- + // Wrap around + if m.focused < 0 { + m.focused = len(m.loginInputs) - 1 + } +} + + +func (m *model) LoginToCalendar() (err error) { + err = m.InitDAVclients() + if err != nil { return } + + m.Calendars, err = GetCalendars() + if err != nil { return } + + items := []list.Item{} + + for _,calendar := range m.Calendars { + // fmt.Println(calendar.Name) + items = append(items,item(calendar.Name)) + } + + // m.LoggedIn = true TODO after calendar choose + + const defaultWidth = 20 + const listHeight = 24 + l := list.New(items, itemDelegate{}, defaultWidth, listHeight) + l.Title = "Which To-Do list do we need?" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + + m.calendarList = l + m.ActiveWindow = "calendarChoose" + + + return nil +} diff --git a/tui-model.go b/tui-model.go new file mode 100644 index 0000000..7c83215 --- /dev/null +++ b/tui-model.go @@ -0,0 +1,88 @@ +package main + +import ( + // "fmt" + "github.com/charmbracelet/bubbles/list" + "github.com/emersion/go-webdav/caldav" + "github.com/charmbracelet/bubbles/textinput" + +) + + + +type model struct { + Tabs []string + // TabContent []string + LoggedIn bool + ActiveWindow string + + TodayTab list.Model + TomorrowTab list.Model + + calendarList list.Model + calendarChoice string + + loginInputs []textinput.Model + focused int + err error + + Creds Credentials + Calendars []caldav.Calendar + CalObjects []caldav.CalendarObject + + errString string +} + +type Credentials struct { + URL string + Username string + Password string + CalendarName string + CalendarPath string +} + + +func (m *model) CredentialsSave() (err error) { + + //TODO some proper error handler in case if we cant save + err = storeCredentialsToKeyring(m.Creds.URL,m.Creds.Username,m.Creds.Password,m.Creds.CalendarPath) + //TODO add skip flag + if err != nil {return} + return nil +} + + +func InitModel() model { + var inputs []textinput.Model = make([]textinput.Model, 3) + inputs[url] = textinput.New() + inputs[url].Placeholder = "https://nextcloud.example/remote.php/dav" + inputs[url].Focus() + // inputs[url].CharLimit = 20 + inputs[url].Width = 30 + inputs[url].Prompt = "" + // inputs[url].Validate = urlValidator + + inputs[login] = textinput.New() + inputs[login].Placeholder = "username" + // inputs[login].CharLimit = 5 + inputs[login].Width = 30 + inputs[login].Prompt = "" + // inputs[login].Validate = loginValidator + + inputs[pass] = textinput.New() //TODO make pass hidden with "github.com/erikgeiser/promptkit/textinput" + inputs[pass].Placeholder = "MySecurePassword" + // inputs[pass].CharLimit = 3 + inputs[pass].Width = 30 + inputs[pass].Prompt = "" + // inputs[pass].Validate = passValidator + + output := model{ + Tabs: []string{"Today", "Tomorrow", "Add"}, + loginInputs: inputs, + focused: 0, + err: nil, + // Creds: Credentials{"test","test","test","test","test"}, + // TabContent: []string{"ERROR?", "Mascara Tab", "Foundation Tab"}, + } + return output +} diff --git a/tui-todo.go b/tui-todo.go new file mode 100644 index 0000000..172ceee --- /dev/null +++ b/tui-todo.go @@ -0,0 +1,73 @@ +package main + +import ( + + "github.com/emersion/go-ical" + "time" + "github.com/charmbracelet/bubbles/list" +) + + +type TODO ical.Event + +func (i TODO) Title() string { + out,err := i.Props.Get(ical.PropSummary).Text() + if err != nil {return ""} + return out +} +func (i TODO) Description() string { + out,err := i.Props.Get(ical.PropDescription).Text() + if err != nil {return ""} + return out +} +func (i TODO) FilterValue() string { + out1,err1 := i.Props.Get(ical.PropSummary).Text() + out2,err2 := i.Props.Get(ical.PropDescription).Text() + if err1 != nil && err2 != nil {return ""} + return out1+out2 +} + + + +func (m *model) GatherTodos() (err error) { +//TODO more modular approach + calendarObjects, err := GetTODOs(m.Creds.CalendarPath) + if err != nil {return} + + // var todayTodos []TODO + + today := time.Now() + todayTodosBuf, err := ParseDueDateTODOs(calendarObjects, today) + tomorrow := time.Now().AddDate(0, 0, 1) + tomorrowTodosBuf, err := ParseDueDateTODOs(calendarObjects, tomorrow) + + + + + var todayTodos,tomorrowTodos []TODO + for _,event := range todayTodosBuf { + todayTodos = append(todayTodos,TODO(event)) + } + for _,event := range tomorrowTodosBuf { + tomorrowTodos = append(tomorrowTodos,TODO(event)) + } + + + var itemsToday []list.Item + var itemsTomorrow []list.Item + for _, todo := range todayTodos { + itemsToday = append(itemsToday, todo) + } + for _, todo := range tomorrowTodos { + itemsTomorrow = append(itemsTomorrow, todo) + } + + + + m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0) + m.TodayTab.Title = "Today" + m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0) + m.TomorrowTab.Title = "Tomorrow" + + return nil +} diff --git a/tui.go b/tui.go index 31257d6..616d756 100644 --- a/tui.go +++ b/tui.go @@ -1,120 +1,141 @@ package main - import ( - "fmt" + // "fmt" // "os" + // "time" + // "io" // "strings" - "github.com/charmbracelet/bubbles/list" + // "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" // textblink "github.com/charmbracelet/bubbles/textinput" // "github.com/erikgeiser/promptkit/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + // "github.com/charmbracelet/lipgloss" "golang.org/x/term" + + // "github.com/emersion/go-ical" + // "github.com/emersion/go-webdav/caldav" + + // "errors" ) +//TODO add new TODos +//TODO edit TODOs +//TODO add search +//TODO add custom filter (search that saves with filter) +// TODO add tabs for days/searcf/filter -var docStyle = lipgloss.NewStyle().Margin(1, 2) -var loginStyle = lipgloss.NewStyle().Width(40).Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder()) -var inputStyle = lipgloss.NewStyle() -func (i TODO) Title() string { return i.Name } -func (i TODO) Description() string { return i.Desc } -func (i TODO) FilterValue() string { return i.Name } -type model struct { - Tabs []string - // TabContent []string - LoggedIn bool - ActiveWindow string + +type errMsg struct {message string} + +func (m model) errHandler(desc string,err error) (tea.Cmd) { + if err != nil { + output := desc+": "+err.Error() + return func() tea.Msg { return errMsg{output} } + // return errMsg{output} + // m.Send(output) + } - TodayTab list.Model - TomorrowTab list.Model - - loginInputs []textinput.Model - focused int - err error + return nil } + + + + + + + + + + + + + func (m model) Init() tea.Cmd { return textinput.Blink // return nil } -const ( - url = iota - login - pass -) -//TODO add changing calendar -func InitModel() model { - var inputs []textinput.Model = make([]textinput.Model, 3) - inputs[url] = textinput.New() - inputs[url].Placeholder = "https://nextcloud.example/remote.php/dav" - inputs[url].Focus() - // inputs[url].CharLimit = 20 - inputs[url].Width = 30 - inputs[url].Prompt = "" - // inputs[url].Validate = urlValidator - inputs[login] = textinput.New() - inputs[login].Placeholder = "username" - // inputs[login].CharLimit = 5 - inputs[login].Width = 30 - inputs[login].Prompt = "" - // inputs[login].Validate = loginValidator +//TODO add changing calendar - inputs[pass] = textinput.New() - inputs[pass].Placeholder = "MySecurePassword" - // inputs[pass].CharLimit = 3 - inputs[pass].Width = 30 - inputs[pass].Prompt = "" - // inputs[pass].Validate = passValidator - output := model{ - Tabs: []string{"Today", "Tomorrow", "Add"}, - loginInputs: inputs, - focused: 0, - err: nil, - // TabContent: []string{"ERROR?", "Mascara Tab", "Foundation Tab"}, - } - return output -} 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 keypress := msg.String(); keypress { + case "q", "ctrl+c": + // m.quitting = true + return m, tea.Quit + + case "enter": + i, ok := m.calendarList.SelectedItem().(item) + if ok { + m.calendarChoice = string(i) //TODO remove from model - calendarChoice + } + for _,calendar := range m.Calendars { + if calendar.Name == m.calendarChoice { + m.Creds.CalendarPath = calendar.Path + } + } + + err := m.CredentialsSave() + if err != nil { return m, m.errHandler("Failed to save credentials",err)} + 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 submit + 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 + } + 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() - // try login -> store -> move to getting stuff - return m, tea.Quit - } - 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() - } - + switch keypress := msg.String(); keypress { case "ctrl+c", "q": return m, tea.Quit @@ -125,10 +146,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { h, v := docStyle.GetFrameSize() width, height, _ := term.GetSize(0) switch m.ActiveWindow { - case "today": + case "today": m.ActiveWindow = "tomorrow" m.TomorrowTab.SetSize(width-h, height-v) - case "tomorrow": + case "tomorrow": m.ActiveWindow = "today" m.TodayTab.SetSize(width-h, height-v) } @@ -143,39 +164,54 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // TODO add new element return m, tea.Quit } - + case tea.WindowSizeMsg: - h, v := docStyle.GetFrameSize() + switch m.ActiveWindow { - case "login": - m.TodayTab.SetSize(msg.Width-h, msg.Height-v) - case "today": - m.TodayTab.SetSize(msg.Width-h, msg.Height-v) - case "tomorrow": - m.TomorrowTab.SetSize(msg.Width-h, msg.Height-v) + // case "login": //TODO + // m.TodayTab.SetSize(msg.Width-h, msg.Height-v) + // case "calendarChoose": //TODO + // m.calendarList.SetWidth(msg.Width) + case "today": + h, v := docStyle.GetFrameSize() + m.TodayTab.SetSize(msg.Width-h, msg.Height-v) + case "tomorrow": + h, v := docStyle.GetFrameSize() + m.TomorrowTab.SetSize(msg.Width-h, msg.Height-v) } + + case errMsg: + m.errString = msg.message + m.ActiveWindow = "" } - - var cmd tea.Cmd + // text input switch m.ActiveWindow { - case "login": - for i := range m.loginInputs { - m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) - } - case "today": - m.TodayTab, cmd = m.TodayTab.Update(msg) - case "tomorrow": - m.TomorrowTab, cmd = m.TomorrowTab.Update(msg) + case "login": + for i := range m.loginInputs { + m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) + } + case "calendarChoose": + m.calendarList, cmd = m.calendarList.Update(msg) + case "today": + m.TodayTab, cmd = m.TodayTab.Update(msg) + case "tomorrow": + m.TomorrowTab, cmd = m.TomorrowTab.Update(msg) + + case "": //exit on key press + switch tmp := msg.(type) { //TODO debug and optimize + case tea.KeyMsg: + _ = tmp + if m.LoggedIn { + m.ActiveWindow = "today" + } else {m.ActiveWindow = "login"} + } } - - - // m.TodayTab, cmd = m.TodayTab.Update(msg) // m.TomorrowTab, cmd = m.TomorrowTab.Update(msg) - + return m, cmd } @@ -184,87 +220,56 @@ func (m model) View() string { var tabOutput string switch m.ActiveWindow { - case "login": - 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.RenderLogin()) - // w, h := lipgloss.Size(tabOutput) + case "login": + 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.RenderLogin()) + // w, h := lipgloss.Size(tabOutput) + case "calendarChoose": + 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.RenderCalendarChooser()) + // w, h := lipgloss.Size(tabOutput) - - case "today": - tabOutput = docStyle.Render(m.TodayTab.View()) - case "tomorrow": - tabOutput = docStyle.Render(m.TomorrowTab.View()) - case "": - width, height, _ := term.GetSize(0) - width -=2 - height -=2 - loginStyle = loginStyle. - Width(width/3). - Height(1). - MarginTop(height/2). - MarginLeft(width/3+2). - MarginRight(width/3) - tabOutput = loginStyle.Render("ERROR") - } + case "today": + tabOutput = docStyle.Render(m.TodayTab.View()) + case "tomorrow": + tabOutput = docStyle.Render(m.TomorrowTab.View()) + case "": + width, height, _ := term.GetSize(0) + width -= 2 + height -= 2 + loginStyle = loginStyle. + Width(width / 3). + Height(1). + MarginTop(height / 2). + MarginLeft(width/3 + 2). + MarginRight(width / 3) + tabOutput = loginStyle.Render("ERROR - " + m.errString) + } // if m.activeTab == 0 { // tabOutput = docStyle.Render(m.TodayTab.View()) // } // if m.activeTab == 1 { // tabOutput = docStyle.Render(m.TomorrowTab.View()) // } - + return tabOutput } -func (m model) RenderLogin() string { - - return fmt.Sprintf( - `%s - - -%s -%s - -%s -%s - -%s -%s -%s - `, - - inputStyle.Width(30).Align(lipgloss.Center).Render("Login"), - inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("WebDAV server URL"), - m.loginInputs[url].View(), - inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Login"), - m.loginInputs[login].View(), - inputStyle.Width(30).Foreground(lipgloss.AdaptiveColor{Dark: "50"}).Render("Password"), - m.loginInputs[pass].View(), //TODO hide - inputStyle.Render("Continue ->"), - ) - // .Align(lipgloss.Center).BorderStyle(lipgloss.NormalBorder()) - -} - -func (m *model) nextInput() { - m.focused = (m.focused + 1) % len(m.loginInputs) -} - -// prevInput focuses the previous input field -func (m *model) prevInput() { - m.focused-- - // Wrap around - if m.focused < 0 { - m.focused = len(m.loginInputs) - 1 - } -}