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" ) const ( AppName = "galendar" AppVersion = 1 ) type AppState struct { Name string Version int ClientSecret []byte cfg *userConfig.Config account *Account StatusMsg string } var state *AppState var parameters []string func main() { parameters = []string{"add", "config", "event", "defaults", "delete", "next", "refresh", "today", "brief", "ui"} var err error var op string state = &AppState{Name: AppName, Version: AppVersion} state.cfg, err = userConfig.NewConfig(state.Name) if err != nil { panic(err) } if len(os.Args) > 1 && os.Args[1][0] != ':' { op = os.Args[1] } else { op = "today" } DoVersionCheck() op = matchParameter(op, parameters) // Some quick op rewrites. For Convenience if op == "refresh" { // Invalidate Cache, then just do a 'today' if err = InvalidateCache(); err != nil { fmt.Println("cacheInvalidate error:", err) } op = "today" } else if op == "next" { op = "event" } if op != "--reinit" { InitComm() } switch op { case "--reinit": // Reset all config for _, v := range state.cfg.GetKeyList() { fmt.Println("Deleting Key: " + v) state.cfg.DeleteKey(v) } case "add": // Quick event add to primary calendar quickAddText := strings.Join(os.Args[2:], " ") e, err := state.account.GetDefaultCalendar().QuickAdd(quickAddText) if err != nil { fmt.Println(err.Error()) } else { fmt.Println(e.ToCLIString()) InvalidateCache() } case "bail": // Just initialize communications and bail return case "config": // Open the Configuration CUI screen DoConfig() case "defaults": // Show Defaults for _, v := range state.account.ActiveCalendars { if c, err := state.account.GetCalendarById(v); err == nil { var colDef *calendar.ColorDefinition if colDef, err = state.account.GetCalendarColor(c.Id); err != nil { fmt.Println(err.Error()) return } fmt.Println(CalColToAnsi(*colDef).Sprint(" ") + " " + colDef.Background + " " + c.Summary) } } 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", "brief": 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 } sum := e.Summary if op == "brief" && len(sum) > 35 { sum = sum[:35] + "..." } eventString := stTmStr + CalColToAnsi(*colDef).Sprint(" ") + " " + sum + " " + 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() } func InitComm() { var err error sec := state.cfg.GetBytes("ClientSecret") tkn := state.cfg.GetBytes("Token") state.account, err = GetAccount(sec, tkn) if err != nil { log.Fatalf("Unable to get Account: %v", err) } // Save the Raw Token state.cfg.SetBytes("Token", state.account.CC.GetRawToken()) loadAccountState() } func DoVersionCheck() { confVer, _ := state.cfg.GetInt("version") for confVer < state.Version { // Update from confVer to state.Version switch confVer { case 0: // Initializing the app fmt.Println("Initializing gocal") if GoogleClientJson == "" { for { fmt.Println("Client Secret Filename (Ctrl-C to exit): ") var fn string if _, err := fmt.Scan(&fn); err == nil { var b []byte b, err = ioutil.ReadFile(fn) if len(b) > 0 && err == nil { state.cfg.SetBytes("ClientSecret", b) break } } } } else { state.cfg.Set("ClientSecret", GoogleClientJson) } state.cfg.SetInt("version", 1) state.cfg.SetArray("defaultCalendars", []string{"primary"}) state.cfg.Set("quickAddCalendar", "") } state.ClientSecret = state.cfg.GetBytes("ClientSecret") // Refetch the version from the config confVer, _ = state.cfg.GetInt("version") } } // saveAccountState saves all the bits of cache from the account // into our config func saveAccountState() { var err error var calListJson []byte if calListJson, err = json.Marshal(state.account.CalendarList); err == nil { if err = state.cfg.SetBytes("calendarList", calListJson); err != nil { fmt.Println("saveAccountState:calendarList error:", err) } if err = state.cfg.SetDateTime("calListUseBy", state.account.CalListUseBy); err != nil { fmt.Println("saveAccountState:calListUseBy error:", err) } } var evListJson []byte if evListJson, err = json.Marshal(state.account.EventList); err == nil { if err = state.cfg.SetBytes("eventList", evListJson); err != nil { fmt.Println("saveAccountState:eventList error:", err) } if err = state.cfg.SetDateTime("eventListUseBy", state.account.EventListUseBy); err != nil { fmt.Println("saveAccountState:eventListUseBy error:", err) } } if err = state.cfg.SetArray("defaultCalendars", state.account.ActiveCalendars); err != nil { fmt.Println("saveAccountState:defaultCalendars error:", err) } if err = state.cfg.Set("quickAddCalendar", state.account.QuickAddCal); err != nil { fmt.Println("saveAccountState:quickAddCalendar error:", err) } } // loadAccountState loads all of the config into the state struct func loadAccountState() { var err error var calListJson []byte calListJson = state.cfg.GetBytes("calendarList") if err = json.Unmarshal(calListJson, &state.account.CalendarList); err == nil { if len(state.account.CalendarList) > 0 { if err != nil { fmt.Println("error: ", err) } if state.account.CalListUseBy, err = state.cfg.GetDateTime("calListUseBy"); err != nil { fmt.Println("error: ", err) } } } else { fmt.Println("error: ", err) } var evListJson []byte evListJson = state.cfg.GetBytes("eventList") if err = json.Unmarshal(evListJson, &state.account.EventList); err == nil { if len(state.account.EventList) > 0 { if err != nil { fmt.Println("error: ", err) } if state.account.EventListUseBy, err = state.cfg.GetDateTime("eventListUseBy"); err != nil { fmt.Println("error: ", err) } } } else { fmt.Println("error: ", err) } // If the 'defaultCalendars' cfg is set to 'primary' (or not at all) // we need to actually but the primary cal id in there var defCal []string defCal, err = state.cfg.GetArray("defaultCalendars") if len(defCal) == 0 || (len(defCal) == 1 && defCal[0] == "primary") { gCal, err := state.account.Service.CalendarList.Get("primary").Do() if err == nil { defCal = []string{gCal.Id} } } state.account.ActiveCalendars = defCal // Verify/Set QuickAdd Calendar state.account.QuickAddCal = state.cfg.Get("quickAddCalendar") if state.account.QuickAddCal == "" { gCal, err := state.account.Service.CalendarList.Get("primary").Do() if err == nil { 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(" brief The same as 'today' except summaries are truncated to save space.") //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 strings.ToLower(hex) { case "#a47ae2", "#9a9cff", "#4986e7": return colBlue, nil case "#7bd148": return colGreen, nil case "#9fc6e7", "#9fe1e7": return colCyan, nil case "#f83a22", "#ac725e": return colRed, nil case "#fad165", "#ff7537", "#e6c800", "#fbe983", "#ffad46": return colYellow, nil case "#c2c2c2": return colWhite, 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) case colWhite: ret.Add(color.BgWhite) } } return ret }