v0.9 login,todoList,keyring,caldav(read),codeRefactor...

This commit is contained in:
Casual 2024-06-29 04:00:39 +03:00
parent fc9971696c
commit f4fe3f301d
11 changed files with 1068 additions and 358 deletions

384
caldav.go
View File

@ -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 = "<EMPTY>"
// }
// 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
tmpTODO := event
if (*event).Props["SUMMARY"] != nil {
name := (*event).Props["SUMMARY"][0].Value
tmpTODO.Name = name
} else {
tmpTODO.Name = "<EMPTY>"
if event.Props["SUMMARY"] == nil {
tmpTODO.Props.SetText(ical.PropSummary, "<EMPTY>")
}
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]
tmpTODO.Time = todoTime
}
// var todoTime string
// due := (*event).Props["DUE"][0].Value
// index := strings.Index(due, "T")
// if index != -1 {
// str := due[index+1:]
// todoTime = str[:2] + ":" + str[2:4]
//
// tmpTODO.Time = todoTime
// }
output = append(output, tmpTODO)
}
// }
@ -179,9 +341,6 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]TODO,
// //TODO PRIORITY:1 #1-high, 5-mid, 9-low
//TODO repeat function???
return output, nil
}
//TODO on complete -repeat function
// RRULE:FREQ=WEEKLY;INTERVAL=1
@ -199,3 +358,140 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]TODO,
// 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
}
// 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
}

3
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -10,7 +10,6 @@ import (
"strings"
)
const (
service = "Tempus"
user = "login"
@ -44,12 +43,17 @@ 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
credentials := url + " " + login + " " + password + " " + calendar
err := keyring.Set(service, user, credentials)
if err != nil {
@ -58,36 +62,53 @@ func storeCredentialsToKeyring(url,login,password string) error {
return nil
}
func getCredentialsFromKeyring() (url,login,password string, err error) {
func getCredentialsFromKeyring_wrapper() (url, login, password, calendar string, err error) {
secret, err := keyring.Get(service, user)
if err != nil {
return "","","",err
return
}
v := strings.Split(secret, " ")
urlByte, err := base64.StdEncoding.DecodeString(v[0])
if err != nil {
return "","","",err
return
}
loginByte, err := base64.StdEncoding.DecodeString(v[1])
if err != nil {
return "","","",err
return
}
passwordByte, err := base64.StdEncoding.DecodeString(v[2])
if err != nil {
return "","","",err
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{

176
main.go
View File

@ -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 true {os.Exit(1)}
options.InitDAVclients()
calendars, err := GetCalendars()
errHandler(err, "Error getting calendars")
// 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)")
var found bool
// var calPath string
if options.Calendar != "" {
for _,calendar := range calendars {
fmt.Println(calendar.Name, "-", calendar.Path)
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(calendars[1].Path)
errHandler(err, "Error getting TODOs")
// 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()
today := time.Now()
todayTodos, err := ParseDueDateTODOs(calendarObjects, today)
tomorrow := time.Now().AddDate(0,0,1)
tomorrowTodos, err := ParseDueDateTODOs(calendarObjects, tomorrow)
// fmt.Println(todos)
// 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"
fmt.Println("In total we have", len(calendarObjects), "todos")
m.LoggedIn = true
m.ActiveWindow = "today"
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"
} else {
//TODO go to calendars page
// m.LoggedIn = true
m.ActiveWindow = "calendarChoose"
// 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"
}
}
}
}
//DEBUG stuff
//DEBUG stuff
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.)
}

View File

@ -11,8 +11,10 @@ var options = &Options{}
type Options struct {
URL string
Threads int
// 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.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

64
tui-calChoose.go Normal file
View File

@ -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
}

100
tui-login.go Normal file
View File

@ -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
}

88
tui-model.go Normal file
View File

@ -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
}

73
tui-todo.go Normal file
View File

@ -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 "<EMPTY>"}
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
}

231
tui.go
View File

@ -1,106 +1,127 @@
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
TodayTab list.Model
TomorrowTab list.Model
type errMsg struct {message string}
loginInputs []textinput.Model
focused int
err error
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)
}
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
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 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
// try login -> store -> move to getting stuff
return m, tea.Quit
// 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":
@ -145,34 +166,49 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize()
switch m.ActiveWindow {
case "login":
m.TodayTab.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 "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)
@ -196,7 +232,18 @@ func (m model) View() string {
// 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())
@ -212,7 +259,7 @@ func (m model) View() string {
MarginTop(height / 2).
MarginLeft(width/3 + 2).
MarginRight(width / 3)
tabOutput = loginStyle.Render("ERROR")
tabOutput = loginStyle.Render("ERROR - " + m.errString)
}
// if m.activeTab == 0 {
// tabOutput = docStyle.Render(m.TodayTab.View())
@ -224,47 +271,5 @@ func (m model) View() string {
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
}
}