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)
This commit is contained in:
Brian Buller 2017-03-16 16:11:39 -05:00
parent 17cfbbc5fe
commit c4e55cc52f
4 changed files with 386 additions and 95 deletions

View File

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

366
main.go
View File

@ -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"
)
@ -24,15 +28,16 @@ type AppState struct {
ClientSecret []byte
cfg *userConfig.Config
account *Account
defaultCalendars []string
quickAddCalendar string
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 <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(" 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
}

View File

@ -24,6 +24,9 @@ type Account struct {
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

View File

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