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) }