diff --git a/cmd/thyme/main.go b/cmd/thyme/main.go index 9ea4c9d..12476bc 100644 --- a/cmd/thyme/main.go +++ b/cmd/thyme/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "runtime" + "time" "github.com/jessevdk/go-flags" "github.com/sourcegraph/thyme" @@ -17,14 +18,44 @@ func init() { if _, err := CLI.AddCommand("track", "", "record current windows", &trackCmd); err != nil { log.Fatal(err) } - if _, err := CLI.AddCommand("show", "", "visualize data", &showCmd); err != nil { + if _, err := CLI.AddCommand("watch", "", "record current windows at regular intervals (default 30s)", &watchCmd); err != nil { log.Fatal(err) } - if _, err := CLI.AddCommand("dep", "", "external dependencies that need to be installed", &depCmd); err != nil { + if _, err := CLI.AddCommand("show", "", "visualize data", &showCmd); err != nil { log.Fatal(err) } } +// WatchCmd is the subcommand that tracks application usage at regular intervals. +type WatchCmd struct { + // The track command is a subset of the watch command + TrackCmd + Interval int64 `long:"interval" short:"n" description:"update interval (default 30 seconds)"` +} + +var watchCmd WatchCmd + +func (c *WatchCmd) Execute(args []string) error { + var interval time.Duration + if c.Interval <= 0 { + // Set default interval + interval = 30 * time.Second + } else { + interval = time.Duration(c.Interval) * time.Second + } + + // Loop until the user aborts the command + for { + err := c.TrackCmd.Execute(args) + if err != nil { + return err + } + + // Sleep for a while until the next time we should track active windows + time.Sleep(interval) + } +} + // TrackCmd is the subcommand that tracks application usage. type TrackCmd struct { Out string `long:"out" short:"o" description:"output file"` @@ -123,19 +154,6 @@ func (c *ShowCmd) Execute(args []string) error { return nil } -type DepCmd struct{} - -var depCmd DepCmd - -func (c *DepCmd) Execute(args []string) error { - t, err := getTracker() - if err != nil { - return err - } - fmt.Println(t.Deps()) - return nil -} - func main() { run := func() error { _, err := CLI.Parse() diff --git a/darwin.go b/darwin.go index 8ae0612..7d769c7 100644 --- a/darwin.go +++ b/darwin.go @@ -90,13 +90,11 @@ end repeat ` ) -func (t *DarwinTracker) Deps() string { - return ` -You will need the osascript command-line utility. You can install it via the Apple developer tools ('xcode-select --install') or npm ('npm install --save osascript'). - -You will need to enable privileges for "Terminal" in System Preferences > Security & Privacy > Privacy > Accessibility. -See https://support.apple.com/en-us/HT202802 for details. -` +func (t *DarwinTracker) CheckDependencies() { + _, err := exec.LookPath("osascript") + if err != nil { + log.Fatal("You will need the osascript command-line utility. You can install it via the Apple developer tools ('xcode-select --install') or npm ('npm install --save osascript').") + } } func (t *DarwinTracker) Snap() (*Snapshot, error) { @@ -186,7 +184,22 @@ func runAS(script string) (map[process][]*Window, error) { cmd.Stdin = bytes.NewBuffer([]byte(script)) b, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("AppleScript error: %s, output was:\n%s", err, string(b)) + // This is the error code for 'osascript is not allowed assistive access'. + // Add a more informative error message than the one applescript normally outputs. + if strings.Contains(string(b), "-25211") { + formatString := ` +AppleScript error: %s +You will need to enable privileges for "Terminal" (or Iterm2 if you are using that) in +System Preferences > Security & Privacy > Privacy > Accessibility. +See https://support.apple.com/en-us/HT202802 for details. + +output was: +%s` + + return nil, fmt.Errorf(formatString, err, string(b)) + } else { + return nil, fmt.Errorf("AppleScript error: %s, output was:\n%s", err, string(b)) + } } return parseASOutput(string(b)) } diff --git a/data.go b/data.go index 66889e7..2f0f03c 100644 --- a/data.go +++ b/data.go @@ -25,7 +25,9 @@ func NewTracker(name string) Tracker { if _, exists := trackers[name]; !exists { log.Fatalf("no Tracker constructor has been registered with name %s", name) } - return trackers[name]() + tracker := trackers[name]() + tracker.CheckDependencies() + return tracker } // Tracker tracks application usage. An implementation that satisfies @@ -36,9 +38,10 @@ type Tracker interface { // at the current time. Snap() (*Snapshot, error) - // Deps returns a string listing the dependencies that still need - // to be installed with instructions for how to install them. - Deps() string + // CheckDependencies checks for external dependencies (for example + // 'osascript' on OS X) and logs a fatal error if they are not available + // as well as instructions for how to install them. + CheckDependencies() } // Stream represents all the sampling data gathered by Thyme. diff --git a/linux.go b/linux.go index 4fd2210..f8d9618 100644 --- a/linux.go +++ b/linux.go @@ -2,6 +2,7 @@ package thyme import ( "fmt" + "os" "os/exec" "regexp" "strconv" @@ -22,13 +23,26 @@ func NewLinuxTracker() Tracker { return &LinuxTracker{} } -func (t *LinuxTracker) Deps() string { - return `Install the following command-line utilities via your package manager (e.g., apt) of choice: -* xdpyinfo -* xwininfo -* xdotool -* wmctrl -` +func (t *LinuxTracker) CheckDependencies() { + deps := map[string]string{ + "xdpyinfo": "x11-utils", + "xwininfo": "x11-utils", + "xdotool": "xdotool", + "wmctrl": "wmctrl", + } + + anyFailed := false + for k, v := range deps { + _, err := exec.LookPath(k) + if err != nil { + fmt.Printf("You need to install the command line utility '%s' (usually in the package named '%s') via your package manager of choice.\nFor example 'apt-get install %s'\n\n", k, v, v) + anyFailed = true + } + } + + if anyFailed { + os.Exit(1) + } } func (t *LinuxTracker) Snap() (*Snapshot, error) { diff --git a/windows.go b/windows.go index 3aeb167..927309b 100644 --- a/windows.go +++ b/windows.go @@ -40,8 +40,8 @@ var ( procGetWindowThreadProcessId = user.NewProc("GetWindowThreadProcessId") ) -func (t *WindowsTracker) Deps() string { - return "Nothing, Ready to Go!" +func (t *WindowsTracker) CheckDependencies() { + // Nothing, Ready to Go! } // getWindowTitle returns a title of a window of the provided system window handle