gal/main.go

517 lines
14 KiB
Go

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 <command> [<mods>]")
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 <id> Delete the event with the specified id")
fmt.Println("")
fmt.Println(" event [<id>] 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(" '@<id>' 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
}