diff --git a/.gitignore b/.gitignore index 0e1cefe..9566f23 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ go.sum calblink client_secret.json conf.json +conf.toml diff --git a/README.md b/README.md index 70a020c..d1fb227 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/calblink.go b/calblink.go index 5f62b39..f803583 100644 --- a/calblink.go +++ b/calblink.go @@ -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") diff --git a/config.go b/config.go index ed390b7..8e0a670 100644 --- a/config.go +++ b/config.go @@ -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" @@ -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 @@ -179,6 +201,28 @@ 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 @@ -186,7 +230,83 @@ func readUserPrefs() *UserPrefs { 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.