<commit_before>// Copyright 2017 Google Inc.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"os/user"
	"path/filepath"
	"syscall"
	"time"

	blink1 "github.com/hink/go-blink1"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/calendar/v3"
)

// TODO - make color fade from green to yellow to red
// TODO - add Clock type to manage time-of-day where we currently use Time and hacks to set it to the current day
// Configuration file:
// JSON file with the following structure:
// {
//   excludes: [ "event", "names", "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
//}
// 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.
// SkipDays may be localized.
// Excludes is exact string matches only.
// ResponseState can be one of: "all" (all events whatever their response status), "accepted" (only accepted events),
// "notRejected" (any events that are not rejected).  Default is notRejected.
// DeviceFailureRetries is the number of consecutive failures to initialize the device before the program quits. Default is 10.
// ShowDots indicates whether to show dots and similar marks to indicate that the program has completed an update cycle.

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

const (
	responseStateAll         = responseState("all")
	responseStateAccepted    = responseState("accepted")
	responseStateNotRejected = responseState("notRejected")
)

// checkStatus returns true if the given event status is one that should activate the blink(1) in the given responseState.
func (state responseState) checkStatus(status string) bool {
	switch state {
	case responseStateAll:
		return true

	case responseStateAccepted:
		return (status == "accepted")

	case responseStateNotRejected:
		return (status != "declined")
	}
	return false
}

func (state responseState) isValidState() bool {
	switch state {
	case responseStateAll:
		return true
	case responseStateAccepted:
		return true
	case responseStateNotRejected:
		return true
	}
	return false
}

// userPrefs is a struct that manages the user preferences as set by the config file and command line.

type userPrefs struct {
	excludes             map[string]bool
	startTime            *time.Time
	endTime              *time.Time
	skipDays             [7]bool
	pollInterval         int
	calendar             string
	responseState        responseState
	deviceFailureRetries int
	showDots             bool
}

// Struct used for decoding the JSON
type prefLayout struct {
	Excludes             []string
	StartTime            string
	EndTime              string
	SkipDays             []string
	PollInterval         int64
	Calendar             string
	ResponseState        string
	DeviceFailureRetries int64
	ShowDots             string
}

// calendarState is a display state for the calendar event.  It encapsulates both the colors to display and the flash duration.
type calendarState struct {
	name          string
	blinkState    blink1.State
	flashState    blink1.State
	flashDuration time.Duration
}

func (state calendarState) execute(blinker *blinkerState) {
	blinker.newState <- state
}

var (
	black        = calendarState{name: "Black", blinkState: blink1.OffState}
	green        = calendarState{name: "Green", blinkState: blink1.State{Green: 255}}
	yellow       = calendarState{name: "Yellow", blinkState: blink1.State{Red: 255, Green: 160}}
	red          = calendarState{name: "Red", blinkState: blink1.State{Red: 255}}
	redFlash     = calendarState{name: "Red Flash", blinkState: blink1.State{Red: 255}, flashState: blink1.OffState, flashDuration: time.Duration(500) * time.Millisecond}
	fastRedFlash = calendarState{name: "Fast Red Flash", blinkState: blink1.State{Red: 255}, flashState: blink1.OffState, flashDuration: time.Duration(125) * time.Millisecond}
	blueFlash    = calendarState{name: "Red/Blue Flash", blinkState: blink1.State{Blue: 255}, flashState: blink1.State{Red: 255}, flashDuration: time.Duration(500) * time.Millisecond}
	blue         = calendarState{name: "Blue", blinkState: blink1.State{Blue: 255}}
	magentaFlash = calendarState{name: "MagentaFlash", blinkState: blink1.State{Red: 255, Blue: 255}, flashState: blink1.OffState, flashDuration: time.Duration(125) * time.Millisecond}
)

// flags
var debugFlag = flag.Bool("debug", false, "Show debug messages")
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 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")
var showDotsFlag = flag.Bool("show_dots", true, "Whether to show progress dots after every cycle of checking the calendar")

var debugOut io.Writer = ioutil.Discard
var dotOut io.Writer = ioutil.Discard

const failureRetries = 3

// blinkerState encapsulates the current device state of the blink(1).
type blinkerState struct {
	device      *blink1.Device
	newState    chan calendarState
	failures    int
	maxFailures int
}

func newBlinkerState(maxFailures int) *blinkerState {
	blinker := &blinkerState{
		newState:    make(chan calendarState, 1),
		maxFailures: maxFailures,
	}
	blinker.reinitialize()
	return blinker
}

func (blinker *blinkerState) reinitialize() error {
	device, err := blink1.OpenNextDevice()
	if err != nil {
		blinker.failures++
		if blinker.failures > blinker.maxFailures {
			log.Fatalf("Unable to initialize blink(1): %v", err)
		}
		fmt.Fprint(dotOut, "X")
	} else {
		blinker.failures = 0
	}
	blinker.device = device
	return err
}<commit_msg>Close the device if we had it open before we reinitialize.<commit_after>// Copyright 2017 Google Inc.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"os/user"
	"path/filepath"
	"syscall"
	"time"

	blink1 "github.com/hink/go-blink1"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/calendar/v3"
)

// TODO - make color fade from green to yellow to red
// TODO - add Clock type to manage time-of-day where we currently use Time and hacks to set it to the current day
// Configuration file:
// JSON file with the following structure:
// {
//   excludes: [ "event", "names", "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
//}
// 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.
// SkipDays may be localized.
// Excludes is exact string matches only.
// ResponseState can be one of: "all" (all events whatever their response status), "accepted" (only accepted events),
// "notRejected" (any events that are not rejected).  Default is notRejected.
// DeviceFailureRetries is the number of consecutive failures to initialize the device before the program quits. Default is 10.
// ShowDots indicates whether to show dots and similar marks to indicate that the program has completed an update cycle.

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

const (
	responseStateAll         = responseState("all")
	responseStateAccepted    = responseState("accepted")
	responseStateNotRejected = responseState("notRejected")
)

// checkStatus returns true if the given event status is one that should activate the blink(1) in the given responseState.
func (state responseState) checkStatus(status string) bool {
	switch state {
	case responseStateAll:
		return true

	case responseStateAccepted:
		return (status == "accepted")

	case responseStateNotRejected:
		return (status != "declined")
	}
	return false
}

func (state responseState) isValidState() bool {
	switch state {
	case responseStateAll:
		return true
	case responseStateAccepted:
		return true
	case responseStateNotRejected:
		return true
	}
	return false
}

// userPrefs is a struct that manages the user preferences as set by the config file and command line.

type userPrefs struct {
	excludes             map[string]bool
	startTime            *time.Time
	endTime              *time.Time
	skipDays             [7]bool
	pollInterval         int
	calendar             string
	responseState        responseState
	deviceFailureRetries int
	showDots             bool
}

// Struct used for decoding the JSON
type prefLayout struct {
	Excludes             []string
	StartTime            string
	EndTime              string
	SkipDays             []string
	PollInterval         int64
	Calendar             string
	ResponseState        string
	DeviceFailureRetries int64
	ShowDots             string
}

// calendarState is a display state for the calendar event.  It encapsulates both the colors to display and the flash duration.
type calendarState struct {
	name          string
	blinkState    blink1.State
	flashState    blink1.State
	flashDuration time.Duration
}

func (state calendarState) execute(blinker *blinkerState) {
	blinker.newState <- state
}

var (
	black        = calendarState{name: "Black", blinkState: blink1.OffState}
	green        = calendarState{name: "Green", blinkState: blink1.State{Green: 255}}
	yellow       = calendarState{name: "Yellow", blinkState: blink1.State{Red: 255, Green: 160}}
	red          = calendarState{name: "Red", blinkState: blink1.State{Red: 255}}
	redFlash     = calendarState{name: "Red Flash", blinkState: blink1.State{Red: 255}, flashState: blink1.OffState, flashDuration: time.Duration(500) * time.Millisecond}
	fastRedFlash = calendarState{name: "Fast Red Flash", blinkState: blink1.State{Red: 255}, flashState: blink1.OffState, flashDuration: time.Duration(125) * time.Millisecond}
	blueFlash    = calendarState{name: "Red/Blue Flash", blinkState: blink1.State{Blue: 255}, flashState: blink1.State{Red: 255}, flashDuration: time.Duration(500) * time.Millisecond}
	blue         = calendarState{name: "Blue", blinkState: blink1.State{Blue: 255}}
	magentaFlash = calendarState{name: "MagentaFlash", blinkState: blink1.State{Red: 255, Blue: 255}, flashState: blink1.OffState, flashDuration: time.Duration(125) * time.Millisecond}
)

// flags
var debugFlag = flag.Bool("debug", false, "Show debug messages")
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 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")
var showDotsFlag = flag.Bool("show_dots", true, "Whether to show progress dots after every cycle of checking the calendar")

var debugOut io.Writer = ioutil.Discard
var dotOut io.Writer = ioutil.Discard

const failureRetries = 3

// blinkerState encapsulates the current device state of the blink(1).
type blinkerState struct {
	device      *blink1.Device
	newState    chan calendarState
	failures    int
	maxFailures int
}

func newBlinkerState(maxFailures int) *blinkerState {
	blinker := &blinkerState{
		newState:    make(chan calendarState, 1),
		maxFailures: maxFailures,
	}
	blinker.reinitialize()
	return blinker
}

func (blinker *blinkerState) reinitialize() error {
	if blinker.device != nil {
		blinker.device.Close()
		blinker.device = nil
	}
	device, err := blink1.OpenNextDevice()
	if err != nil {
		blinker.failures++
		if blinker.failures > blinker.maxFailures {
			log.Fatalf("Unable to initialize blink(1): %v", err)
		}
		fmt.Fprint(dotOut, "X")
	} else {
		blinker.failures = 0
	}
	blinker.device = device
	return err
}
{'fraction_non_alphanumeric': 0.09694646035875668, 'fraction_numerical': 0.014103792961796521, 'mean_word_length': 3.8207920792079206, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5161523', 'n_tokens_mistral': 4518, 'n_tokens_neox': 4242, 'n_words': 1530}