github.com/clysto/awgo@v0.15.0/config.go (about)

     1  //
     2  // Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net>
     3  //
     4  // MIT Licence. See http://opensource.org/licenses/MIT
     5  //
     6  // Created on 2018-06-30
     7  //
     8  
     9  package aw
    10  
    11  import (
    12  	"errors"
    13  	"fmt"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/deanishe/awgo/util"
    19  )
    20  
    21  // Environment variables containing workflow and Alfred info.
    22  //
    23  // Read the values with os.Getenv(EnvVarName) or via Alfred:
    24  //
    25  //    // Returns a string
    26  //    Alfred.Get(EnvVarName)
    27  //    // Parse string into a bool
    28  //    Alfred.GetBool(EnvVarDebug)
    29  //
    30  const (
    31  	// Workflow info assigned in Alfred Preferences
    32  	EnvVarName     = "alfred_workflow_name"     // Name of workflow
    33  	EnvVarBundleID = "alfred_workflow_bundleid" // Bundle ID
    34  	EnvVarVersion  = "alfred_workflow_version"  // Workflow version
    35  
    36  	EnvVarUID = "alfred_workflow_uid" // Random UID assigned by Alfred
    37  
    38  	// Workflow storage directories
    39  	EnvVarCacheDir = "alfred_workflow_cache" // For temporary data
    40  	EnvVarDataDir  = "alfred_workflow_data"  // For permanent data
    41  
    42  	// Set to 1 when Alfred's debugger is open
    43  	EnvVarDebug = "alfred_debug"
    44  
    45  	// Theme info. Colours are in rgba format, e.g. "rgba(255,255,255,1.0)"
    46  	EnvVarTheme            = "alfred_theme"                      // ID of user's selected theme
    47  	EnvVarThemeBG          = "alfred_theme_background"           // Background colour
    48  	EnvVarThemeSelectionBG = "alfred_theme_selection_background" // BG colour of selected item
    49  
    50  	// Alfred info
    51  	EnvVarAlfredVersion = "alfred_version"       // Alfred's version number
    52  	EnvVarAlfredBuild   = "alfred_version_build" // Alfred's build number
    53  	EnvVarPreferences   = "alfred_preferences"   // Path to "Alfred.alfredpreferences" file
    54  	// Machine-specific hash. Machine preferences are stored in
    55  	// Alfred.alfredpreferences/local/<hash>
    56  	EnvVarLocalhash = "alfred_preferences_localhash"
    57  )
    58  
    59  // Config loads workflow settings from Alfred's environment variables.
    60  //
    61  // The Get* methods read a variable from the environment, converting it to
    62  // the desired type, and the Set() method saves a variable to info.plist.
    63  //
    64  // NOTE: Because calling Alfred via AppleScript is very slow (~0.2s/call),
    65  // Config users a "Doer" API for setting variables, whereby calls are collected
    66  // and all executed at once when Config.Do() is called:
    67  //
    68  //     cfg := NewConfig()
    69  //     if err := cfg.Set("key1", "value1").Set("key2", "value2").Do(); err != nil {
    70  //         // handle error
    71  //     }
    72  //
    73  // Finally, you can use Config.To() to populate a struct from environment
    74  // variables, and Config.From() to read a struct's fields and save them
    75  // to info.plist.
    76  type Config struct {
    77  	Env
    78  	scripts []string
    79  	err     error
    80  }
    81  
    82  // NewConfig creates a new Config from the environment.
    83  //
    84  // It accepts one optional Env argument. If an Env is passed, Config
    85  // is initialised from that instead of the system environment.
    86  func NewConfig(env ...Env) *Config {
    87  
    88  	var e Env
    89  	if len(env) > 0 {
    90  		e = env[0]
    91  	} else {
    92  		e = sysEnv{}
    93  	}
    94  	return &Config{e, []string{}, nil}
    95  }
    96  
    97  // Get returns the value for envvar "key".
    98  // It accepts one optional "fallback" argument. If no envvar is set, returns
    99  // fallback or an empty string.
   100  //
   101  // If a variable is set, but empty, its value is used.
   102  func (cfg *Config) Get(key string, fallback ...string) string {
   103  
   104  	var fb string
   105  
   106  	if len(fallback) > 0 {
   107  		fb = fallback[0]
   108  	}
   109  	s, ok := cfg.Lookup(key)
   110  	if !ok {
   111  		return fb
   112  	}
   113  	return s
   114  }
   115  
   116  // GetString is a synonym for Get.
   117  func (cfg *Config) GetString(key string, fallback ...string) string {
   118  	return cfg.Get(key, fallback...)
   119  }
   120  
   121  // GetInt returns the value for envvar "key" as an int.
   122  // It accepts one optional "fallback" argument. If no envvar is set, returns
   123  // fallback or 0.
   124  //
   125  // Values are parsed with strconv.ParseInt(). If strconv.ParseInt() fails,
   126  // tries to parse the number with strconv.ParseFloat() and truncate it to an
   127  // int.
   128  func (cfg *Config) GetInt(key string, fallback ...int) int {
   129  
   130  	var fb int
   131  
   132  	if len(fallback) > 0 {
   133  		fb = fallback[0]
   134  	}
   135  	s, ok := cfg.Lookup(key)
   136  	if !ok {
   137  		return fb
   138  	}
   139  
   140  	i, err := parseInt(s)
   141  	if err != nil {
   142  		return fb
   143  	}
   144  
   145  	return int(i)
   146  }
   147  
   148  // GetFloat returns the value for envvar "key" as a float.
   149  // It accepts one optional "fallback" argument. If no envvar is set, returns
   150  // fallback or 0.0.
   151  //
   152  // Values are parsed with strconv.ParseFloat().
   153  func (cfg *Config) GetFloat(key string, fallback ...float64) float64 {
   154  
   155  	var fb float64
   156  
   157  	if len(fallback) > 0 {
   158  		fb = fallback[0]
   159  	}
   160  	s, ok := cfg.Lookup(key)
   161  	if !ok {
   162  		return fb
   163  	}
   164  
   165  	n, err := strconv.ParseFloat(s, 64)
   166  	if err != nil {
   167  		return fb
   168  	}
   169  
   170  	return n
   171  }
   172  
   173  // GetDuration returns the value for envvar "key" as a time.Duration.
   174  // It accepts one optional "fallback" argument. If no envvar is set, returns
   175  // fallback or 0.
   176  //
   177  // Values are parsed with time.ParseDuration().
   178  func (cfg *Config) GetDuration(key string, fallback ...time.Duration) time.Duration {
   179  
   180  	var fb time.Duration
   181  
   182  	if len(fallback) > 0 {
   183  		fb = fallback[0]
   184  	}
   185  	s, ok := cfg.Lookup(key)
   186  	if !ok {
   187  		return fb
   188  	}
   189  
   190  	d, err := time.ParseDuration(s)
   191  	if err != nil {
   192  		return fb
   193  	}
   194  
   195  	return d
   196  }
   197  
   198  // GetBool returns the value for envvar "key" as a boolean.
   199  // It accepts one optional "fallback" argument. If no envvar is set, returns
   200  // fallback or false.
   201  //
   202  // Values are parsed with strconv.ParseBool().
   203  func (cfg *Config) GetBool(key string, fallback ...bool) bool {
   204  
   205  	var fb bool
   206  
   207  	if len(fallback) > 0 {
   208  		fb = fallback[0]
   209  	}
   210  	s, ok := cfg.Lookup(key)
   211  	if !ok {
   212  		return fb
   213  	}
   214  
   215  	b, err := strconv.ParseBool(s)
   216  	if err != nil {
   217  		return fb
   218  	}
   219  
   220  	return b
   221  }
   222  
   223  // Set saves a workflow variable to info.plist.
   224  //
   225  // It accepts one optional bundleID argument, which is the bundle ID of the
   226  // workflow whose configuration should be changed.
   227  // If not specified, it defaults to the current workflow's.
   228  func (cfg *Config) Set(key, value string, export bool, bundleID ...string) *Config {
   229  
   230  	bid := cfg.getBundleID(bundleID...)
   231  	opts := map[string]interface{}{
   232  		"toValue":    value,
   233  		"inWorkflow": bid,
   234  		"exportable": export,
   235  	}
   236  
   237  	return cfg.addScriptOpts(scriptSetConfig, key, opts)
   238  }
   239  
   240  // Unset removes a workflow variable from info.plist.
   241  //
   242  // It accepts one optional bundleID argument, which is the bundle ID of the
   243  // workflow whose configuration should be changed.
   244  // If not specified, it defaults to the current workflow's.
   245  func (cfg *Config) Unset(key string, bundleID ...string) *Config {
   246  
   247  	bid := cfg.getBundleID(bundleID...)
   248  	opts := map[string]interface{}{
   249  		"inWorkflow": bid,
   250  	}
   251  
   252  	return cfg.addScriptOpts(scriptRmConfig, key, opts)
   253  }
   254  
   255  // Do calls Alfred and runs the accumulated actions.
   256  //
   257  // If an error was encountered while preparing any commands, it will be
   258  // returned here. It also returns an error if there are no commands to run,
   259  // or if the call to Alfred fails.
   260  //
   261  // Succeed or fail, any accumulated scripts and errors are cleared when Do()
   262  // is called.
   263  func (cfg *Config) Do() error {
   264  
   265  	var err error
   266  
   267  	if cfg.err != nil {
   268  		// reset
   269  		err, cfg.err = cfg.err, nil
   270  		cfg.scripts = []string{}
   271  
   272  		return err
   273  	}
   274  
   275  	if len(cfg.scripts) == 0 {
   276  		return errors.New("no commands to run")
   277  	}
   278  
   279  	script := strings.Join(cfg.scripts, "\n")
   280  	// reset
   281  	cfg.scripts = []string{}
   282  
   283  	_, err = util.RunJS(script)
   284  
   285  	return err
   286  }
   287  
   288  // Extract bundle ID from argument or default.
   289  func (cfg *Config) getBundleID(bundleID ...string) string {
   290  
   291  	if len(bundleID) > 0 {
   292  		return bundleID[0]
   293  	}
   294  
   295  	bid, _ := cfg.Lookup(EnvVarBundleID)
   296  	return bid
   297  }
   298  
   299  // Add a JavaScript that takes a single argument.
   300  func (cfg *Config) addScript(script, arg string) *Config {
   301  
   302  	script = fmt.Sprintf(script, util.QuoteJS(arg))
   303  	cfg.scripts = append(cfg.scripts, script)
   304  
   305  	return cfg
   306  }
   307  
   308  // Run a JavaScript that takes two arguments, a string and an object.
   309  func (cfg *Config) addScriptOpts(script, name string, opts map[string]interface{}) *Config {
   310  
   311  	script = fmt.Sprintf(script, util.QuoteJS(name), util.QuoteJS(opts))
   312  	cfg.scripts = append(cfg.scripts, script)
   313  
   314  	return cfg
   315  }
   316  
   317  // parse an int, falling back to parsing it as a float
   318  func parseInt(s string) (int, error) {
   319  	i, err := strconv.ParseInt(s, 10, 32)
   320  	if err == nil {
   321  		return int(i), nil
   322  	}
   323  
   324  	// Try to parse as float, then convert
   325  	n, err := strconv.ParseFloat(s, 64)
   326  	if err != nil {
   327  		return 0, fmt.Errorf("invalid int: %v", s)
   328  	}
   329  	return int(n), nil
   330  }
   331  
   332  // Convert interface{} to a string.
   333  func stringify(v interface{}) string { return fmt.Sprintf("%v", v) }