Skip to content
This repository was archived by the owner on May 3, 2023. It is now read-only.
Open
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
48 changes: 33 additions & 15 deletions cmd/thyme/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"runtime"
"time"

"github.com/jessevdk/go-flags"
"github.com/sourcegraph/thyme"
Expand All @@ -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"`
Expand Down Expand Up @@ -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()
Expand Down
29 changes: 21 additions & 8 deletions darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
}
Expand Down
11 changes: 7 additions & 4 deletions data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
28 changes: 21 additions & 7 deletions linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package thyme

import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
Expand All @@ -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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that package names are different for RPM, as in #48

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, hence the 'usually'. Might be a good idea to add those names too I guess.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #48 for the RPM names

"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) {
Expand Down
4 changes: 2 additions & 2 deletions windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down