From c4e55cc52fefe9225fbe9f395a9b743150e70672 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 16 Mar 2017 16:11:39 -0500 Subject: [PATCH] Weekly Update * Handle Some Settings Better * Better Parameter Detection * Implemented Event IDs for Event Operations * View an Event's Details * Delete Events * Display Help * Show approximation of some Calendar Colors (This is kind of a mess, since I'm converting web hex colors to 3 bit ansi, so it's not done) --- config_mode.go | 12 +- main.go | 376 ++++++++++++++++++++++++++++++++++++---------- struct_account.go | 63 +++++++- struct_event.go | 30 +++- 4 files changed, 386 insertions(+), 95 deletions(-) diff --git a/config_mode.go b/config_mode.go index e65e529..878ed9a 100644 --- a/config_mode.go +++ b/config_mode.go @@ -103,10 +103,10 @@ func toggleCalendar(g *gocui.Gui, v *gocui.View) error { _, cy := v.Cursor() var removed bool calList := state.account.GetCalendarList() - for i := range state.defaultCalendars { - if state.defaultCalendars[i] == calList[cy].Id { + for i := range state.account.ActiveCalendars { + if state.account.ActiveCalendars[i] == calList[cy].Id { // Remove calendar from defaults - state.defaultCalendars = append(state.defaultCalendars[:i], state.defaultCalendars[i+1:]...) + state.account.ActiveCalendars = append(state.account.ActiveCalendars[:i], state.account.ActiveCalendars[i+1:]...) state.StatusMsg = "Calendar '" + calList[cy].Summary + "' Removed from Defaults" removed = true break @@ -115,7 +115,7 @@ func toggleCalendar(g *gocui.Gui, v *gocui.View) error { if !removed { // Add calendar to defaults state.StatusMsg = "Calendar '" + calList[cy].Summary + "' Added to Defaults" - state.defaultCalendars = append(state.defaultCalendars, calList[cy].Id) + state.account.ActiveCalendars = append(state.account.ActiveCalendars, calList[cy].Id) } updateViews(g) return nil @@ -161,8 +161,8 @@ func drawCalList(g *gocui.Gui, v *gocui.View) error { list := state.account.GetCalendarList() for i := range list { isDef := false - for defIdx := range state.defaultCalendars { - if list[i].Id == state.defaultCalendars[defIdx] { + for defIdx := range state.account.ActiveCalendars { + if list[i].Id == state.account.ActiveCalendars[defIdx] { isDef = true break } diff --git a/main.go b/main.go index ef24941..9a05b93 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,17 @@ package main import ( "encoding/json" + "errors" "fmt" "io/ioutil" "log" "os" + "strconv" "strings" "time" + calendar "google.golang.org/api/calendar/v3" + "github.com/br0xen/user-config" "github.com/fatih/color" ) @@ -19,20 +23,21 @@ const ( ) type AppState struct { - Name string - Version int - ClientSecret []byte - cfg *userConfig.Config - account *Account - defaultCalendars []string - quickAddCalendar string + Name string + Version int + ClientSecret []byte + cfg *userConfig.Config + account *Account StatusMsg string } var state *AppState +var parameters []string + func main() { + parameters = []string{"add", "colors", "config", "event", "defaults", "delete", "next", "refresh", "today", "ui"} var err error var op string state = &AppState{Name: AppName, Version: AppVersion} @@ -42,7 +47,7 @@ func main() { panic(err) } - if len(os.Args) > 1 { + if len(os.Args) > 1 && os.Args[1][0] != ':' { op = os.Args[1] } else { op = "today" @@ -50,12 +55,21 @@ func main() { DoVersionCheck() + op = matchParameter(op, parameters) + + // Some quick op rewrites. For Convenience if op == "refresh" { // Invalidate Cache, then just do a 'today' - if err = state.cfg.SetDateTime("calListUseBy", time.Now()); err != nil { - fmt.Println("cahceInvalidate error:", err) + if err = InvalidateCache(); err != nil { + fmt.Println("cacheInvalidate error:", err) } op = "today" + } else if op == "next" { + op = "event" + } + + if op != "--reinit" { + InitComm() } switch op { @@ -65,83 +79,131 @@ func main() { fmt.Println("Deleting Key: " + v) state.cfg.DeleteKey(v) } - case "today": - // Show everything on the calendar for today - InitComm() - events := state.account.GetTodaysEvents() - if len(events) == 0 { - fmt.Println("No Events Found") - } else { - for _, e := range events { - isOnCal := false - for _, idx := range state.defaultCalendars { - if idx == e.CalendarId { - isOnCal = true - break - } - } - if !isOnCal { - continue - } - stTmStr := e.GetStartTimeString() - durStr := "(" + e.GetEndTime().Sub(e.GetStartTime()).String() + ")" - if stTmStr == "00:00" { - stTmStr = "All Day" - durStr = "" - } else { - stTmStr = " " + stTmStr - } - - eventString := "[" + stTmStr + "] " + e.Summary + " " + durStr - if time.Until(e.GetStartTime()) < 0 && time.Until(e.GetEndTime()) < 0 { - // Event is in the past - color.New(color.FgGreen).Add(color.Bold).Println(eventString) - } else if time.Until(e.GetStartTime()) < 0 && time.Until(e.GetEndTime()) > 0 { - // Event is NOW - color.New(color.FgRed).Add(color.Bold).Println(eventString) - } else if time.Until(e.GetStartTime()) < time.Hour { - // Event is in less than an hour - color.New(color.FgYellow).Add(color.Bold).Println(eventString) - } else { - fmt.Println(eventString) - } - } - } - - case "config": - // Open the Configuration CUI screen - InitComm() - DoConfig() - - case "ui": - // Open the UI mode - InitComm() - DoUIMode() - - case "defaults": - // Show Defaults - InitComm() - for i := range state.defaultCalendars { - fmt.Println(state.defaultCalendars[i]) - } case "add": // Quick event add to primary calendar quickAddText := strings.Join(os.Args[2:], " ") - InitComm() e, err := state.account.GetDefaultCalendar().QuickAdd(quickAddText) if err != nil { fmt.Println(err.Error()) } else { fmt.Println(e.ToCLIString()) - // Invalidate the cache - if err = state.cfg.SetDateTime("calListUseBy", time.Now()); err != nil { - fmt.Println("cahceInvalidate error:", err) - } + InvalidateCache() } + case "bail": // Just initialize communications and bail - InitComm() + return + + case "config": + // Open the Configuration CUI screen + DoConfig() + + case "defaults": + // Show Defaults + for i := range state.account.ActiveCalendars { + fmt.Println(state.account.ActiveCalendars[i]) + } + + case "delete": + // Figure out which event we're deleting + evNum := 0 + if len(os.Args) > 2 { + if evNum, err = strconv.Atoi(os.Args[2]); err != nil { + fmt.Println("Error parsing requested event number: " + err.Error()) + } + } + events := state.account.GetTodaysActiveEvents() + if len(events) == 0 { + fmt.Println("No Events Found") + } else { + for _, e := range events { + if e.InstanceId == evNum { + fmt.Print("Are you sure you want to delete '" + e.Summary + " (" + e.GetColorizedDateTimeString() + ")'? [y/N] ") + var resp string + fmt.Scanln(&resp) + if strings.ToUpper(resp) == "Y" { + fmt.Println("Deleting event '" + e.Summary + " (" + e.GetColorizedDateTimeString() + ")'") + if c, err := state.account.GetCalendarById(e.CalendarId); err == nil { + if err = c.DeleteEvent(e.Id); err != nil { + fmt.Println("Error Deleting Event: " + err.Error()) + } else { + InvalidateCache() + } + } + } else { + fmt.Println("Event deletion cancelled") + } + return + } + } + } + + case "event": + // Figure out which event we're getting details for + evNum := 0 + if len(os.Args) > 2 { + if evNum, err = strconv.Atoi(os.Args[2]); err != nil { + fmt.Println("Error parsing requested event number: " + err.Error()) + } + } + events := state.account.GetTodaysActiveEvents() + if len(events) == 0 { + fmt.Println("No Events Found") + } else { + for _, e := range events { + if e.InstanceId == evNum { + fmt.Println("== " + e.Summary + " ==") + fmt.Println(e.GetColorizedDateTimeString()) + fmt.Println("") + if strings.TrimSpace(e.Description) != "" { + fmt.Println("== Description ==") + fmt.Println(e.Description) + fmt.Println("") + } + return + } + } + } + fmt.Println("Couldn't find event with ID " + strconv.Itoa(evNum)) + + case "today": + events := state.account.GetTodaysActiveEvents() + if len(events) == 0 { + fmt.Println("No Events Found") + } else { + for _, e := range events { + stTmStr := e.GetColorizedDateTimeString() + durStr := "" + if !e.IsAllDayEvent() { + durStr = "(" + e.GetEndTime().Sub(e.GetStartTime()).String() + ")" + } + var colDef *calendar.ColorDefinition + if colDef, err = state.account.GetCalendarColor(e.CalendarId); err != nil { + fmt.Println(err.Error()) + return + } + eventString := stTmStr + CalColToAnsi(*colDef).Sprint(" ") + " " + e.Summary + " " + durStr + if hasMod("ids") { + idString := "@" + strconv.Itoa(e.InstanceId) + for len(idString) < 5 { + idString = " " + idString + } + eventString = idString + " " + eventString + } + + fmt.Println(eventString) + } + } + + // case "ui": + // Open the UI mode + // DoUIMode() + + case "help": + printHelp() + default: + printHelp() } // All done, save the account state saveAccountState() @@ -219,10 +281,10 @@ func saveAccountState() { fmt.Println("saveAccountState:eventListUseBy error:", err) } } - if err = state.cfg.SetArray("defaultCalendars", state.defaultCalendars); err != nil { + if err = state.cfg.SetArray("defaultCalendars", state.account.ActiveCalendars); err != nil { fmt.Println("saveAccountState:defaultCalendars error:", err) } - if err = state.cfg.Set("quickAddCalendar", state.quickAddCalendar); err != nil { + if err = state.cfg.Set("quickAddCalendar", state.account.QuickAddCal); err != nil { fmt.Println("saveAccountState:quickAddCalendar error:", err) } } @@ -269,14 +331,166 @@ func loadAccountState() { defCal = []string{gCal.Id} } } + state.account.ActiveCalendars = defCal - state.defaultCalendars = defCal // Verify/Set QuickAdd Calendar - state.quickAddCalendar = state.cfg.Get("quickAddCalendar") - if state.quickAddCalendar == "" { + state.account.QuickAddCal = state.cfg.Get("quickAddCalendar") + if state.account.QuickAddCal == "" { gCal, err := state.account.Service.CalendarList.Get("primary").Do() if err == nil { - state.quickAddCalendar = gCal.Id + state.account.QuickAddCal = gCal.Id } } } + +func eventIsOnActiveCalendar(e *Event) bool { + for _, idx := range state.account.ActiveCalendars { + if idx == e.CalendarId { + return true + } + } + return false +} + +func InvalidateCache() error { + var err error + if err = state.cfg.SetDateTime("calListUseBy", time.Now()); err != nil { + return err + } + err = state.cfg.SetDateTime("eventListUseBy", time.Now()) + return err +} + +func printHelp() { + fmt.Println("Usage: gal []") + fmt.Println("== Valid Commands ==") + fmt.Println(" --reinit Reset all of the configuration") + fmt.Println("") + fmt.Println(" add [event text]") + fmt.Println(" Do a quick event add") + fmt.Println(" This takes text like \"Lunch with Amy at Noon\"") + fmt.Println(" Which would add the event \"Lunch with Amy\" at 12:00pm") + fmt.Println(" to the default Quick-Add Calendar (configured in 'config')") + fmt.Println("") + fmt.Println(" bail Does nothing") + fmt.Println("") + fmt.Println(" config Display the configuration screen") + fmt.Println("") + fmt.Println(" defaults Display the list of default calendar ids") + fmt.Println("") + fmt.Println(" delete Delete the event with the specified id") + fmt.Println("") + fmt.Println(" event [] Display details about event [num]") + fmt.Println(" the event number is 0 indexed, so the next upcoming event is 0") + fmt.Println(" if no [num] is specified, 0 is implied") + fmt.Println("") + fmt.Println(" today [:ids] Show today's Agenda") + fmt.Println(" This is the default mode if no operation is specified") + fmt.Println(" If the ':ids' mod is specified, then each event will be preceeded by") + fmt.Println(" '@' which reveals the current id of the event.") + fmt.Println(" Event ids change such that the next upcoming event is always event 0,") + fmt.Println(" the one after that is 1, the previous one is -1, etc.") + fmt.Println("") + //fmt.Println(" ui Open galendar in UI mode") + fmt.Println("") +} + +func matchParameter(in string, chkParms []string) string { + var nextParms []string + + for i := range in { + for _, p := range chkParms { + if p[i] == in[i] { + nextParms = append(nextParms, p) + } + } + // If we get here and there is only one parameter left, return it + chkParms = nextParms + if len(nextParms) == 1 { + break + } + // Otherwise, loop + nextParms = []string{} + } + if len(chkParms) == 0 { + return "help" + } + return chkParms[0] +} + +func hasMod(in string) bool { + for i := range os.Args { + if os.Args[i] == ":"+in { + return true + } + } + return false +} + +func CalColToAnsi(col calendar.ColorDefinition) *color.Color { + hexfg := strings.ToLower(col.Foreground) + hexbg := strings.ToLower(col.Background) + + // We naively approximate this to: + colBlack := 0 + colBlue := 1 + colGreen := 2 + colCyan := 3 + colRed := 4 + colMagenta := 5 + colYellow := 6 + //colWhite := 7 + + var hexToAnsi = func(hex string) (int, error) { + switch hex { + case "#7bd148": + return colGreen, nil + case "#f83a22", "#ac725e": + return colRed, nil + case "#fad165": + return colYellow, nil + } + return 0, errors.New("Not found") + } + + ret := color.New(color.FgWhite) + if col, err := hexToAnsi(hexfg); err == nil { + switch col { + case colBlack: + ret = color.New(color.FgBlack) + case colBlue: + ret = color.New(color.FgBlue) + case colGreen: + ret = color.New(color.FgGreen) + case colCyan: + ret = color.New(color.FgCyan) + case colRed: + ret = color.New(color.FgRed) + case colMagenta: + ret = color.New(color.FgMagenta) + case colYellow: + ret = color.New(color.FgYellow) + } + } + + if col, err := hexToAnsi(hexbg); err == nil { + switch col { + case colBlack: + ret.Add(color.BgBlack) + case colBlue: + ret.Add(color.BgBlue) + case colGreen: + ret.Add(color.BgGreen) + case colCyan: + ret.Add(color.BgCyan) + case colRed: + ret.Add(color.BgRed) + case colMagenta: + ret.Add(color.BgMagenta) + case colYellow: + ret.Add(color.BgYellow) + } + } + + return ret +} diff --git a/struct_account.go b/struct_account.go index ff822f8..e5f7291 100644 --- a/struct_account.go +++ b/struct_account.go @@ -18,12 +18,15 @@ import ( ) type Account struct { - CC *CalClient - Service *calendar.Service - CalendarList []Calendar - CalListUseBy time.Time - EventList []Event // Today's Events - EventListUseBy time.Time + CC *CalClient + Service *calendar.Service + CalendarList []Calendar + CalListUseBy time.Time + EventList []Event // Today's Events + EventListUseBy time.Time + ActiveCalendars []string + QuickAddCal string + Colors *calendar.Colors } func GetAccount(secret, token []byte) (*Account, error) { @@ -39,7 +42,9 @@ func GetAccount(secret, token []byte) (*Account, error) { if err != nil { return nil, errors.New("Unable to retrieve calendar Client: " + err.Error()) } - return a, nil + + a.Colors, err = calendar.NewColorsService(a.Service).Get().Do() + return a, err } // Get Todays Events gets events for today from all calendars @@ -61,6 +66,28 @@ func (a *Account) GetTodaysEvents() []Event { return ret } +func (a *Account) GetTodaysActiveEvents() []Event { + allEvents := a.GetTodaysEvents() + var ret []Event + var startIdx int + for i := range allEvents { + for _, calId := range a.ActiveCalendars { + if allEvents[i].CalendarId == calId { + if time.Until(allEvents[i].GetStartTime()) <= 0 { + startIdx-- + } + ret = append(ret, allEvents[i]) + break + } + } + } + for i := range ret { + ret[i].InstanceId = startIdx + startIdx++ + } + return ret +} + func (a *Account) GetDefaultCalendar() *Calendar { c, _ := a.Service.CalendarList.Get("primary").Do() return GoogleCalendarToLocal(c) @@ -83,6 +110,28 @@ func (a *Account) GetCalendarList() []Calendar { return ret } +func (a *Account) GetCalendarById(id string) (*Calendar, error) { + calList := a.GetCalendarList() + for _, c := range calList { + if c.Id == id { + return &c, nil + } + } + return nil, errors.New("Couldn't find calendar") +} + +func (a *Account) GetCalendarColor(id string) (*calendar.ColorDefinition, error) { + var cal *Calendar + var err error + if cal, err = a.GetCalendarById(id); err != nil { + return nil, err + } + if c, ok := a.Colors.Calendar[cal.ColorId]; ok { + return &c, nil + } + return nil, errors.New("Error Finding Calendar Color") +} + type CalClient struct { rawClientSecret []byte rawToken []byte diff --git a/struct_event.go b/struct_event.go index 3eadf6c..d1a6756 100644 --- a/struct_event.go +++ b/struct_event.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + "github.com/fatih/color" + calendar "google.golang.org/api/calendar/v3" ) @@ -43,6 +45,32 @@ type Event struct { Transparency string Updated string Visibility string + + // The instance id is the id by which the user can refer to this event + InstanceId int +} + +func (e *Event) IsAllDayEvent() bool { + _, err := time.Parse(time.RFC3339, e.Start.DateTime) + return err != nil +} + +func (e *Event) GetColorizedDateTimeString() string { + if e.IsAllDayEvent() { + return color.New(color.FgGreen).Sprint("[== All Day ==]") + } + tmString := e.GetStartTimeString() + " - " + e.GetEndTimeString() + if time.Until(e.GetStartTime()) < 0 && time.Until(e.GetEndTime()) < 0 { + // Event is in the past + return color.New(color.FgGreen).Sprint("[" + tmString + "]") + } else if time.Until(e.GetStartTime()) < 0 && time.Until(e.GetEndTime()) > 0 { + // Event is NOW + return color.New(color.FgRed).Add(color.Bold).Sprint("[" + tmString + "]") + } else if time.Until(e.GetStartTime()) < time.Hour { + // Event is in less than an hour + return color.New(color.FgYellow).Add(color.Bold).Sprint("[" + tmString + "]") + } + return "[" + tmString + "]" } func (e *Event) ToCLIString() string { @@ -52,7 +80,7 @@ func (e *Event) ToCLIString() string { func (e *Event) GetStartTimeString() string { tm, err := time.Parse(time.RFC3339, e.Start.DateTime) if err != nil { - return "00:00" + return "--:--" } return tm.Local().Format("15:04") }