Skip to content

Commit

Permalink
Use fluent interface for Alfred
Browse files Browse the repository at this point in the history
  • Loading branch information
deanishe committed Feb 13, 2018
1 parent 28cc53e commit 26de564
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 42 deletions.
135 changes: 102 additions & 33 deletions alfred.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,64 +12,135 @@ import (
"errors"
"fmt"
"path/filepath"
"strings"

"github.com/deanishe/awgo/util"
)

// Alfred is a wrapper for Alfred's AppleScript API.
//
// The methods open Alfred in various states, with various input,
// and also allow you to manipulate persistent workflow variables,
// Alfred is a wrapper for Alfred's AppleScript API. With the API, you can
// open Alfred in various modes and manipulate persistent workflow variables,
// i.e. the values saved in info.plist.
//
// The BundleID is used as a default for methods that require a
// bundleID (e.g. RunTrigger).
// Because calling Alfred is slow, the API uses a "Doer" interface, where
// commands are collected and all sent together when Alfred.Do() is called:
//
// // Open Alfred
// a := NewAlfred()
// if err := a.Search("").Do(); err != nil {
// // handle error
// }
//
// // Browse /Applications
// a = NewAlfred()
// if err := a.Browse("/Applications").Do(); err != nil {
// // handle error
// }
//
// // Set multiple configuration values
// a = NewAlfred()
//
// a.SetConfig("USERNAME", "dave")
// a.SetConfig("CLEARANCE", "highest")
// a.SetConfig("PASSWORD", "hunter2")
//
// if err := a.Do(); err != nil {
// // handle error
// }
//
// The BundleID is used as a default for methods that require a bundleID (e.g.
// RunTrigger).
type Alfred struct {
// Default bundle ID for methods that require one
BundleID string
scripts []string
err error
}

// NewAlfred creates a new Alfred using the bundle ID from the environment.
func NewAlfred() Alfred { return Alfred{BundleID()} }
func NewAlfred() *Alfred { return &Alfred{BundleID(), []string{}, nil} }

// Do calls Alfred and runs the accumulated actions.
//
// If an error was encountered while preparing any commands, it will be
// returned here. It also returns an error if there are no commands to run,
// or if the call to Alfred fails.
//
// Succeed or fail, any accumulated scripts and errors are cleared when Do()
// is called.
func (a *Alfred) Do() error {

var err error

if a.err != nil {
// reset
err, a.err = a.err, nil
a.scripts = []string{}

return err
}

if len(a.scripts) == 0 {
return errors.New("no commands to run")
}

script := strings.Join(a.scripts, "\n")
// reset
a.scripts = []string{}

// log.Printf("-----------\n%s\n------------", script)

_, err = util.RunJS(script)

return err
}

// Search runs Alfred with the given query. Use an empty query to just open Alfred.
func (a Alfred) Search(query string) error { return a.runScript(scriptSearch, query) }
func (a *Alfred) Search(query string) *Alfred {
return a.addScript(scriptSearch, query)
}

// Browse tells Alfred to open path in navigation mode.
func (a Alfred) Browse(path string) error {
func (a *Alfred) Browse(path string) *Alfred {

path, err := filepath.Abs(path)
if err != nil {
return err
a.err = err
return a
}

return a.runScript(scriptBrowse, path)
return a.addScript(scriptBrowse, path)
}

// SetTheme tells Alfred to use the specified theme.
func (a Alfred) SetTheme(name string) error { return a.runScript(scriptSetTheme, name) }
func (a *Alfred) SetTheme(name string) *Alfred {
return a.addScript(scriptSetTheme, name)
}

// Action tells Alfred to show File Actions for path(s).
func (a Alfred) Action(path ...string) error {
func (a *Alfred) Action(path ...string) *Alfred {

if len(path) == 0 {
return errors.New("Action requires at least one path")
return a
}

var paths []string

for _, p := range path {

p, err := filepath.Abs(p)
if err != nil {
return fmt.Errorf("couldn't make path absolute (%s): %v", p, err)
a.err = fmt.Errorf("[action] couldn't make path absolute (%s): %v", p, err)
continue
}

paths = append(paths, p)
}

script := fmt.Sprintf(scriptAction, util.QuoteJS(paths))

_, err := util.RunJS(script)
a.scripts = append(a.scripts, script)

return err
return a
}

// RunTrigger runs an External Trigger in the given workflow. Query may be empty.
Expand All @@ -78,7 +149,7 @@ func (a Alfred) Action(path ...string) error {
// workflow whose trigger should be run.
// If not specified, the ID defaults to Alfred.BundleID or the bundle ID
// from the environment.
func (a Alfred) RunTrigger(name, query string, bundleID ...string) error {
func (a *Alfred) RunTrigger(name, query string, bundleID ...string) *Alfred {

bid := a.getBundleID(bundleID...)
opts := map[string]interface{}{
Expand All @@ -89,7 +160,7 @@ func (a Alfred) RunTrigger(name, query string, bundleID ...string) error {
opts["withArgument"] = query
}

return a.runScriptOpts(scriptTrigger, name, opts)
return a.addScriptOpts(scriptTrigger, name, opts)
}

// SetConfig saves a workflow variable to info.plist.
Expand All @@ -98,7 +169,7 @@ func (a Alfred) RunTrigger(name, query string, bundleID ...string) error {
// workflow whose configuration should be changed.
// If not specified, the ID defaults to Alfred.BundleID or the bundle ID
// from the environment.
func (a Alfred) SetConfig(key, value string, export bool, bundleID ...string) error {
func (a *Alfred) SetConfig(key, value string, export bool, bundleID ...string) *Alfred {

bid := a.getBundleID(bundleID...)
opts := map[string]interface{}{
Expand All @@ -107,7 +178,7 @@ func (a Alfred) SetConfig(key, value string, export bool, bundleID ...string) er
"exportable": export,
}

return a.runScriptOpts(scriptSetConfig, key, opts)
return a.addScriptOpts(scriptSetConfig, key, opts)
}

// RemoveConfig removes a workflow variable from info.plist.
Expand All @@ -116,38 +187,36 @@ func (a Alfred) SetConfig(key, value string, export bool, bundleID ...string) er
// workflow whose configuration should be changed.
// If not specified, the ID defaults to Alfred.BundleID or the bundle ID
// from the environment.
func (a Alfred) RemoveConfig(key string, bundleID ...string) error {
func (a *Alfred) RemoveConfig(key string, bundleID ...string) *Alfred {

bid := a.getBundleID(bundleID...)
opts := map[string]interface{}{
"inWorkflow": bid,
}

return a.runScriptOpts(scriptRmConfig, key, opts)
return a.addScriptOpts(scriptRmConfig, key, opts)
}

// Run a JavaScript that takes a single argument.
func (a Alfred) runScript(script, arg string) error {
// Add a JavaScript that takes a single argument.
func (a *Alfred) addScript(script, arg string) *Alfred {

script = fmt.Sprintf(script, util.QuoteJS(arg))
a.scripts = append(a.scripts, script)

_, err := util.RunJS(script)

return err
return a
}

// Run a JavaScript that takes two arguments, a string and an object.
func (a Alfred) runScriptOpts(script, name string, opts map[string]interface{}) error {
func (a *Alfred) addScriptOpts(script, name string, opts map[string]interface{}) *Alfred {

script = fmt.Sprintf(script, util.QuoteJS(name), util.QuoteJS(opts))
a.scripts = append(a.scripts, script)

_, err := util.RunJS(script)

return err
return a
}

// Extract bundle ID from argument, Alfred.BundleID or environment (via BundleID()).
func (a Alfred) getBundleID(bundleID ...string) string {
func (a *Alfred) getBundleID(bundleID ...string) string {

if len(bundleID) > 0 {
return bundleID[0]
Expand Down
57 changes: 49 additions & 8 deletions alfred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func TestAlfred(t *testing.T) {

if testSearch {

if err := a.Search(""); err != nil {
if err := a.Search("").Do(); err != nil {
t.Error(err)
}

if err := a.Search("awgo alfred"); err != nil {
if err := a.Search("awgo alfred").Do(); err != nil {
t.Error(err)
}
}
Expand All @@ -43,42 +43,83 @@ func TestAlfred(t *testing.T) {

h := os.ExpandEnv("$HOME")

if err := a.Action(h+"/Desktop", "."); err != nil {
if err := a.Action(h+"/Desktop", ".").Do(); err != nil {
t.Error(err)
}
}

if testBrowse {

if err := a.Browse("."); err != nil {
if err := a.Browse(".").Do(); err != nil {
t.Error(err)
}
}

if testTrigger {

if err := a.RunTrigger("test", "AwGo, yo!"); err != nil {
if err := a.RunTrigger("test", "AwGo, yo!").Do(); err != nil {
t.Error(err)
}
}

if testSetConf {

if err := a.SetConfig("AWGO_TEST_UNITTEST", "AwGo, yo!", true); err != nil {
if err := a.SetConfig("AWGO_TEST_UNITTEST", "AwGo, yo!", true).Do(); err != nil {
t.Error(err)
}

many := map[string]string{
"MANY_0": "VALUE_0",
"MANY_1": "VALUE_1",
"MANY_2": "VALUE_2",
"MANY_3": "VALUE_3",
"MANY_4": "VALUE_4",
"MANY_5": "VALUE_5",
"MANY_6": "VALUE_6",
"MANY_7": "VALUE_7",
"MANY_8": "VALUE_8",
"MANY_9": "VALUE_9",
}

a := NewAlfred()
for k, v := range many {
a.SetConfig(k, v, true)
}
if err := a.Do(); err != nil {
t.Error(err)
}
}

if testRmConf {

if err := a.RemoveConfig("AWGO_TEST_UNITTEST"); err != nil {
keys := []string{
"AWGO_TEST_BOOL", "AWGO_TEST_DURATION", "AWGO_TEST_EMPTY",
"AWGO_TEST_FLOAT", "AWGO_TEST_INT", "AWGO_TEST_NAME",
"AWGO_TEST_QUOTED", "AWGO_TEST_UNITTEST",
"BENCH_0", "BENCH_1", "BENCH_10", "BENCH_11",
"BENCH_12", "BENCH_13", "BENCH_14", "BENCH_15",
"BENCH_16", "BENCH_17", "BENCH_18", "BENCH_19",
"BENCH_2", "BENCH_20", "BENCH_21", "BENCH_22",
"BENCH_23", "BENCH_24", "BENCH_3", "BENCH_4",
"BENCH_5", "BENCH_6", "BENCH_7", "BENCH_8",
"BENCH_9", "MANY_0", "MANY_1", "MANY_2",
"MANY_3", "MANY_4", "MANY_5", "MANY_6",
"MANY_7", "MANY_8", "MANY_9",
}
a := NewAlfred()

for _, k := range keys {
a.RemoveConfig(k)
}

if err := a.Do(); err != nil {
t.Error(err)
}
}

if testSetTheme {

if err := a.SetTheme("Alfred Notepad"); err != nil {
if err := a.SetTheme("Alfred Notepad").Do(); err != nil {
t.Error(err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func (c Config) Save(key string, value interface{}, export ...bool) error {
exp = true
}

return a.SetConfig(key, val, exp)
return a.SetConfig(key, val, exp).Do()
}

// Check that minimum required values are set.
Expand Down

0 comments on commit 26de564

Please sign in to comment.