Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ go.sum
calblink
client_secret.json
conf.json
conf.toml
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ To use calblink, you need the following:
First off, run it with the --help option to see what the command-line options
are. Useful, perhaps, but maybe not what you want to use every time you run it.

calblink will look for a file named (by default) conf.json for its configuration
options. conf.json includes several useful options you can set:
calblink will look for a file named (by default) conf.toml for its configuration
options; if that doesn't exist, it will look for conf.json. The configuration file
includes several useful options you can set:

* excludes - a list of event titles which it will ignore. If you like blocking
out time with "Make Time" or similar, you can add these names to the
Expand Down Expand Up @@ -169,26 +170,26 @@ options. conf.json includes several useful options you can set:
* 'office:NAME' to match an office location called NAME.
* 'custom:NAME' to match a custom location called NAME.

An example file:

```json
{
"excludes": ["Commute"],
"skipDays": ["Saturday", "Sunday"],
"startTime": "08:45",
"endTime": "18:00",
"pollInterval": 60,
"calendars": ["primary", "username@example.com"],
"responseState": "accepted",
"multiEvent": "true",
"priorityFlashSide": 1,
"workingLocations": ["home"]
}
An example TOML file:

```toml
excludes = ["Commute"]
skipDays = ["Saturday", "Sunday"]
startTime = "08:45"
endTime = "18:00"
pollInterval = 60
calendars = ["primary", "username@example.com"]
responseState = "accepted"
multiEvent = true
priorityFlashSide = 1
workingLocations = ["home"]
```

(Yes, the curly braces are required. Sorry. It's a JSON dictionary.)


The JSON version should be considered deprecated, and new options will not be added
to it. At some later date, it may be removed entirely. Migrating to TOML is
recommended, not least because it's a much cleaner file format that supports handy
features like "comments" and "trailing commas in arrays" and "not needing to be wrapped
in braces and having a comma after every field".


### New Requirements
Expand Down
3 changes: 2 additions & 1 deletion calblink.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ var debugFlag = flag.Bool("debug", false, "Show debug messages")
var verboseFlag = flag.Bool("verbose", false, "Show verbose debug messages (forces --debug to true)")
var clientSecretFlag = flag.String("clientsecret", "client_secret.json", "Path to JSON file containing client secret")
var calNameFlag = flag.String("calendar", "primary", "Name of calendar to base blinker on (overrides value in config file)")
var configFileFlag = flag.String("config", "conf.json", "Path to configuration file")
var configFileFlag = flag.String("config", "conf.toml", "Path to configuration file")
var backupConfigFileFlag = flag.String("backup_config", "conf.json", "Path to configuration file that will be used if config doesn't exist.")
var pollIntervalFlag = flag.Int("poll_interval", 30, "Number of seconds between polls of calendar API (overrides value in config file)")
var responseStateFlag = flag.String("response_state", "notRejected", "Which events to consider based on response: all, accepted, or notRejected")
var deviceFailureRetriesFlag = flag.Int("device_failure_retries", 10, "Number of times to retry initializing the device before quitting the program")
Expand Down
152 changes: 136 additions & 16 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,34 @@ package main

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"strings"
"time"

"github.com/BurntSushi/toml"
)

// Configuration file:
// JSON file with the following structure:
// {
// excludes: [ "event", "names", "to", "ignore"],
// excludePrefixes: [ "prefixes", "to", "ignore"],
// startTime: "hh:mm (24 hr format) to start blinking at every day",
// endTime: "hh:mm (24 hr format) to stop blinking at every day",
// skipDays: [ "weekdays", "to", "skip"],
// pollInterval: 30
// calendar: "calendar"
// responseState: "all"
// deviceFailureRetries: 10
// showDots: true
// multiEvent: true
// priorityFlashSide: 1
//}
// TOML file with the following structure:
// excludes = ["event", "names", "to", "ignore"]
// excludePrefixes = ["prefixes", "to", "ignore"]
// startTime = "hh:mm (24 hr format) to start blinking at every day"
// endTime = "hh:mm (24 hr format) to stop blinking at every day"
// skipDays = [ "weekdays", "to", "skip"]
// pollInterval = 30
// calendar = "calendar"
// responseState = "all"
// deviceFailureRetries = 10
// showDots = true
// multiEvent = true
// priorityFlashSide = 1
//
// An older JSON format is also supported but you don't want to use it.
//
// Notes on items:
// Calendar is the calendar ID - the email address of the calendar. For a person's calendar, that's their email.
// For a secondary calendar, it's the base64 string @group.calendar.google.com on the calendar details page. "primary"
Expand Down Expand Up @@ -89,6 +94,23 @@ type prefLayout struct {
WorkingLocations []string
}

type tomlLayout struct {
Excludes []string
ExcludePrefixes []string
StartTime string
EndTime string
SkipDays []string
PollInterval int64
Calendar string
Calendars []string
ResponseState string
DeviceFailureRetries int64
ShowDots bool
MultiEvent bool
PriorityFlashSide int64
WorkingLocations []string
}

// responseState is an enumerated list of event response states, used to control which events will activate the blink(1).
type ResponseState string

Expand Down Expand Up @@ -179,14 +201,112 @@ func makeWorkSite(location string) WorkSite {
// User preferences methods

func readUserPrefs() *UserPrefs {
configFile := *configFileFlag
_, err := os.Stat(configFile)
if errors.Is(err, fs.ErrNotExist) {
// primary file doesn't exist, try the secondary.
configFile = *backupConfigFileFlag
}
_, err = os.Stat(configFile)
if errors.Is(err, fs.ErrNotExist) {
// There is no config file, so grab the default prefs.
// Lack of a config file is not a fatal error.
debugLog("No config file found.\n")
return getDefaultPrefs()
}
debugLog("Reading from config file %v\n", configFile)
if strings.HasSuffix(configFile, "toml") {
return readTomlPrefs(configFile)
} else {
return readJsonPrefs(configFile)
}
}

func getDefaultPrefs() *UserPrefs {
userPrefs := &UserPrefs{}
// Set defaults from command line
userPrefs.PollInterval = *pollIntervalFlag
userPrefs.Calendars = []string{*calNameFlag}
userPrefs.ResponseState = ResponseState(*responseStateFlag)
userPrefs.DeviceFailureRetries = *deviceFailureRetriesFlag
userPrefs.ShowDots = *showDotsFlag
file, err := os.Open(*configFileFlag)
return userPrefs
}

func readTomlPrefs(configFile string) *UserPrefs {
prefs := tomlLayout{}
userPrefs := getDefaultPrefs()
_, err := toml.DecodeFile(configFile, &prefs)
debugLog("Decoded TOML: %v\n", prefs)
if err != nil {
log.Fatalf("Unable to parse config file %v", err)
}
if prefs.StartTime != "" {
startTime, err := time.Parse("15:04", prefs.StartTime)
if err != nil {
log.Fatalf("Invalid start time %v : %v", prefs.StartTime, err)
}
userPrefs.StartTime = &startTime
}
if prefs.EndTime != "" {
endTime, err := time.Parse("15:04", prefs.EndTime)
if err != nil {
log.Fatalf("Invalid end time %v : %v", prefs.EndTime, err)
}
userPrefs.EndTime = &endTime
}
userPrefs.Excludes = make(map[string]bool)
for _, item := range prefs.Excludes {
debugLog("Excluding item %v\n", item)
userPrefs.Excludes[item] = true
}
userPrefs.ExcludePrefixes = prefs.ExcludePrefixes
weekdays := make(map[string]int)
for i := 0; i < 7; i++ {
weekdays[time.Weekday(i).String()] = i
}
for _, day := range prefs.SkipDays {
i, ok := weekdays[day]
if ok {
userPrefs.SkipDays[i] = true
} else {
log.Fatalf("Invalid day in skipdays: %v", day)
}
}
if prefs.Calendar != "" {
userPrefs.Calendars = []string{prefs.Calendar}
}
if len(prefs.Calendars) > 0 {
userPrefs.Calendars = prefs.Calendars
}
if prefs.PollInterval != 0 {
userPrefs.PollInterval = int(prefs.PollInterval)
}
if prefs.ResponseState != "" {
userPrefs.ResponseState = ResponseState(prefs.ResponseState)
if !userPrefs.ResponseState.isValidState() {
log.Fatalf("Invalid response state %v", prefs.ResponseState)
}
}
if prefs.DeviceFailureRetries != 0 {
userPrefs.DeviceFailureRetries = int(prefs.DeviceFailureRetries)
}
userPrefs.ShowDots = prefs.ShowDots
userPrefs.MultiEvent = prefs.MultiEvent
if prefs.PriorityFlashSide != 0 {
userPrefs.PriorityFlashSide = int(prefs.PriorityFlashSide)
}
for _, location := range prefs.WorkingLocations {
userPrefs.WorkingLocations = append(userPrefs.WorkingLocations, makeWorkSite(location))
}
debugLog("User prefs: %v\n", userPrefs)
return userPrefs
}

func readJsonPrefs(configFile string) *UserPrefs {
// Set defaults from command line
userPrefs := getDefaultPrefs()
file, err := os.Open(configFile)
defer file.Close()
if err != nil {
// Lack of a config file is not a fatal error.
Expand Down