git.deanishe.net/deanishe/awgo.git@v0.15.0/feedback.go (about) 1 // 2 // Copyright (c) 2016 Dean Jackson <deanishe@deanishe.net> 3 // 4 // MIT Licence. See http://opensource.org/licenses/MIT 5 // 6 // Created on 2016-10-23 7 // 8 9 package aw 10 11 import ( 12 "encoding/json" 13 "fmt" 14 "log" 15 "os" 16 "path/filepath" 17 18 "github.com/deanishe/awgo/fuzzy" 19 "github.com/deanishe/awgo/util" 20 ) 21 22 // ModKey is a modifier key pressed by the user to run an alternate 23 // item action in Alfred (in combination with ↩). 24 // 25 // It is passed to Item.NewModifier(). ModKeys cannot be combined: 26 // Alfred only permits one modifier at a time. 27 type ModKey string 28 29 // Valid modifier keys used to specify alternate actions in Script Filters. 30 const ( 31 ModCmd ModKey = "cmd" // Alternate action for ⌘↩ 32 ModAlt ModKey = "alt" // Alternate action for ⌥↩ 33 ModOpt ModKey = "alt" // Synonym for ModAlt 34 ModCtrl ModKey = "ctrl" // Alternate action for ^↩ 35 ModShift ModKey = "shift" // Alternate action for ⇧↩ 36 ModFn ModKey = "fn" // Alternate action for fn↩ 37 ) 38 39 // Item is a single Alfred Script Filter result. 40 // Together with Feedback & Modifier, Item generates Script Filter feedback 41 // for Alfred. 42 // 43 // Create Items via NewItem(), so they are bound to their parent Feedback. 44 type Item struct { 45 title string 46 subtitle *string 47 match *string 48 uid *string 49 autocomplete *string 50 arg *string 51 valid bool 52 file bool 53 copytext *string 54 largetype *string 55 ql *string 56 vars map[string]string 57 mods map[ModKey]*Modifier 58 icon *Icon 59 noUID bool // Suppress UID in JSON 60 } 61 62 // Title sets the title of the item in Alfred's results. 63 func (it *Item) Title(s string) *Item { 64 it.title = s 65 return it 66 } 67 68 // Subtitle sets the subtitle of the item in Alfred's results. 69 func (it *Item) Subtitle(s string) *Item { 70 it.subtitle = &s 71 return it 72 } 73 74 // Match sets Item's match field for filtering. 75 // If present, this field is preferred over the item's title for fuzzy sorting 76 // via Feedback, and by Alfred's "Alfred filters results" feature. 77 func (it *Item) Match(s string) *Item { 78 it.match = &s 79 return it 80 } 81 82 // Arg sets Item's arg, the value passed as {query} to the next workflow action. 83 func (it *Item) Arg(s string) *Item { 84 it.arg = &s 85 return it 86 } 87 88 // UID sets Item's unique ID, which is used by Alfred to remember your choices. 89 // Use a blank string to force results to appear in the order you add them. 90 // 91 // You can also use the SuppressUIDs() Option to (temporarily) suppress 92 // output of UIDs. 93 func (it *Item) UID(s string) *Item { 94 if it.noUID { 95 return it 96 } 97 it.uid = &s 98 return it 99 } 100 101 // Autocomplete sets what Alfred's query expands to when the user TABs result. 102 // (or hits RETURN on a result where valid is false) 103 func (it *Item) Autocomplete(s string) *Item { 104 it.autocomplete = &s 105 return it 106 } 107 108 // Valid tells Alfred whether the result is "actionable", i.e. ENTER will 109 // pass Arg to subsequent action. 110 func (it *Item) Valid(b bool) *Item { 111 it.valid = b 112 return it 113 } 114 115 // IsFile tells Alfred that this Item is a file, i.e. Arg is a path 116 // and Alfred's File Actions should be made available. 117 func (it *Item) IsFile(b bool) *Item { 118 it.file = b 119 return it 120 } 121 122 // Copytext is what CMD+C should copy instead of Arg (the default). 123 func (it *Item) Copytext(s string) *Item { 124 it.copytext = &s 125 return it 126 } 127 128 // Largetype is what is shown in Alfred's Large Text window on CMD+L 129 // instead of Arg (the default). 130 func (it *Item) Largetype(s string) *Item { 131 it.largetype = &s 132 return it 133 } 134 135 // Quicklook is a path or URL shown in a macOS Quicklook window on SHIFT 136 // or CMD+Y. 137 func (it *Item) Quicklook(s string) *Item { 138 it.ql = &s 139 return it 140 } 141 142 // Icon sets the icon for the Item. 143 // Can point to an image file, a filepath of a file whose icon should be used, 144 // or a UTI. 145 // 146 // See the documentation for Icon for more details. 147 func (it *Item) Icon(icon *Icon) *Item { 148 it.icon = icon 149 return it 150 } 151 152 // Var sets an Alfred variable for subsequent workflow elements. 153 func (it *Item) Var(k, v string) *Item { 154 if it.vars == nil { 155 it.vars = make(map[string]string, 1) 156 } 157 it.vars[k] = v 158 return it 159 } 160 161 // NewModifier returns an initialised Modifier bound to this Item. 162 // It also populates the Modifier with any workflow variables set in the Item. 163 func (it *Item) NewModifier(key ModKey) *Modifier { 164 m, err := newModifier(key) 165 if err != nil { 166 panic(err) 167 } 168 169 // Add Item variables to Modifier 170 if it.vars != nil { 171 for k, v := range it.vars { 172 m.Var(k, v) 173 } 174 } 175 176 it.SetModifier(m) 177 return m 178 } 179 180 // SetModifier sets a Modifier for a modifier key. 181 func (it *Item) SetModifier(m *Modifier) error { 182 if it.mods == nil { 183 it.mods = map[ModKey]*Modifier{} 184 } 185 it.mods[m.Key] = m 186 return nil 187 } 188 189 // Vars returns the Item's workflow variables. 190 func (it *Item) Vars() map[string]string { 191 return it.vars 192 } 193 194 // MarshalJSON serializes Item to Alfred 3's JSON format. 195 // You shouldn't need to call this directly: use SendFeedback() instead. 196 func (it *Item) MarshalJSON() ([]byte, error) { 197 var ( 198 typ string 199 ql string 200 text *itemText 201 ) 202 203 if it.file { 204 typ = "file" 205 } 206 207 if it.ql != nil { 208 ql = *it.ql 209 } 210 211 if it.copytext != nil || it.largetype != nil { 212 text = &itemText{Copy: it.copytext, Large: it.largetype} 213 } 214 215 // Serialise Item 216 return json.Marshal(&struct { 217 Title string `json:"title"` 218 Subtitle *string `json:"subtitle,omitempty"` 219 Match *string `json:"match,omitempty"` 220 Auto *string `json:"autocomplete,omitempty"` 221 Arg *string `json:"arg,omitempty"` 222 UID *string `json:"uid,omitempty"` 223 Valid bool `json:"valid"` 224 Type string `json:"type,omitempty"` 225 Text *itemText `json:"text,omitempty"` 226 Icon *Icon `json:"icon,omitempty"` 227 Quicklook string `json:"quicklookurl,omitempty"` 228 Variables map[string]string `json:"variables,omitempty"` 229 Mods map[ModKey]*Modifier `json:"mods,omitempty"` 230 }{ 231 Title: it.title, 232 Subtitle: it.subtitle, 233 Match: it.match, 234 Auto: it.autocomplete, 235 Arg: it.arg, 236 UID: it.uid, 237 Valid: it.valid, 238 Type: typ, 239 Text: text, 240 Icon: it.icon, 241 Quicklook: ql, 242 Variables: it.vars, 243 Mods: it.mods, 244 }) 245 } 246 247 // itemText encapsulates the copytext and largetext values for a result Item. 248 type itemText struct { 249 // Copied to the clipboard on CMD+C 250 Copy *string `json:"copy,omitempty"` 251 // Shown in Alfred's Large Type window on CMD+L 252 Large *string `json:"largetype,omitempty"` 253 } 254 255 // Modifier encapsulates alterations to Item when a modifier key is held when 256 // the user actions the item. 257 // 258 // Create new Modifiers via Item.NewModifier(). This binds the Modifier to the 259 // Item, initializes Modifier's map and inherits Item's workflow variables. 260 // Variables are inherited at creation time, so any Item variables you set 261 // after creating the Modifier are not inherited. 262 type Modifier struct { 263 // The modifier key. May be any of ValidModifiers. 264 Key ModKey 265 arg *string 266 subtitle *string 267 subtitleSet bool 268 valid bool 269 icon *Icon 270 vars map[string]string 271 } 272 273 // newModifier creates a Modifier, validating key. 274 func newModifier(key ModKey) (*Modifier, error) { 275 return &Modifier{Key: key, vars: map[string]string{}}, nil 276 } 277 278 // Arg sets the arg for the Modifier. 279 func (m *Modifier) Arg(s string) *Modifier { 280 m.arg = &s 281 return m 282 } 283 284 // Subtitle sets the subtitle for the Modifier. 285 func (m *Modifier) Subtitle(s string) *Modifier { 286 m.subtitle = &s 287 return m 288 } 289 290 // Valid sets the valid status for the Modifier. 291 func (m *Modifier) Valid(v bool) *Modifier { 292 m.valid = v 293 return m 294 } 295 296 // Icon sets an icon for the Modifier. 297 func (m *Modifier) Icon(i *Icon) *Modifier { 298 m.icon = i 299 return m 300 } 301 302 // Var sets a variable for the Modifier. 303 func (m *Modifier) Var(k, v string) *Modifier { 304 m.vars[k] = v 305 return m 306 } 307 308 // Vars returns all Modifier variables. 309 func (m *Modifier) Vars() map[string]string { 310 return m.vars 311 } 312 313 // MarshalJSON serializes Item to Alfred 3's JSON format. 314 // You shouldn't need to call this directly: use SendFeedback() instead. 315 func (m *Modifier) MarshalJSON() ([]byte, error) { 316 317 return json.Marshal(&struct { 318 Arg *string `json:"arg,omitempty"` 319 Subtitle *string `json:"subtitle,omitempty"` 320 Valid bool `json:"valid,omitempty"` 321 Icon *Icon `json:"icon,omitempty"` 322 Variables map[string]string `json:"variables,omitempty"` 323 }{ 324 Arg: m.arg, 325 Subtitle: m.subtitle, 326 Valid: m.valid, 327 Icon: m.icon, 328 Variables: m.vars, 329 }) 330 } 331 332 // Feedback represents the results for an Alfred Script Filter. 333 // 334 // Normally, you won't use this struct directly, but via the 335 // package-level functions/Workflow methods NewItem(), SendFeedback(), etc. 336 // It is important to use the constructor functions for Feedback, Item 337 // and Modifier structs so they are properly initialised and bound to 338 // their parent. 339 type Feedback struct { 340 Items []*Item // The results to be sent to Alfred. 341 NoUIDs bool // If true, suppress Item UIDs. 342 rerun float64 // Tell Alfred to re-run Script Filter. 343 sent bool // Set to true when feedback has been sent. 344 vars map[string]string // Top-level feedback variables. 345 } 346 347 // NewFeedback creates a new, initialised Feedback struct. 348 func NewFeedback() *Feedback { 349 return &Feedback{Items: []*Item{}, vars: map[string]string{}} 350 } 351 352 // Var sets an Alfred variable for subsequent workflow elements. 353 func (fb *Feedback) Var(k, v string) *Feedback { 354 if fb.vars == nil { 355 fb.vars = make(map[string]string, 1) 356 } 357 fb.vars[k] = v 358 return fb 359 } 360 361 // Rerun tells Alfred to re-run the Script Filter after `secs` seconds. 362 func (fb *Feedback) Rerun(secs float64) *Feedback { 363 fb.rerun = secs 364 return fb 365 } 366 367 // Vars returns the Feedback's workflow variables. 368 func (fb *Feedback) Vars() map[string]string { 369 return fb.vars 370 } 371 372 // Clear removes any items. 373 func (fb *Feedback) Clear() { 374 if len(fb.Items) > 0 { 375 fb.Items = []*Item{} 376 } 377 } 378 379 // IsEmpty returns true if Feedback contains no items. 380 func (fb *Feedback) IsEmpty() bool { return len(fb.Items) == 0 } 381 382 // NewItem adds a new Item and returns a pointer to it. 383 // 384 // The Item inherits any workflow variables set on the Feedback parent at 385 // time of creation. 386 func (fb *Feedback) NewItem(title string) *Item { 387 it := &Item{title: title, vars: map[string]string{}, noUID: fb.NoUIDs} 388 389 // Add top-level variables to Item. The reason for this is 390 // that Alfred drops all item- and top-level variables on the 391 // floor if a modifier has any variables set (i.e. only the 392 // modifier's variables are retained). 393 // So, add top-level variables to Item (and in turn to any Modifiers) 394 // to enforce more sensible behaviour. 395 for k, v := range fb.vars { 396 it.vars[k] = v 397 } 398 399 fb.Items = append(fb.Items, it) 400 return it 401 } 402 403 // NewFileItem adds and returns a pointer to a new item pre-populated from path. 404 // Title and Autocomplete are the base name of the file; 405 // Subtitle is the path to the file (using "~" for $HOME); 406 // Valid is true; 407 // UID and Arg are set to path; 408 // Type is "file"; and 409 // Icon is the icon of the file at path. 410 func (fb *Feedback) NewFileItem(path string) *Item { 411 t := filepath.Base(path) 412 it := fb.NewItem(t) 413 it.Subtitle(util.PrettyPath(path)). 414 Arg(path). 415 Valid(true). 416 UID(path). 417 Autocomplete(t). 418 IsFile(true). 419 Icon(&Icon{path, "fileicon"}) 420 return it 421 } 422 423 // MarshalJSON serializes Feedback to Alfred 3's JSON format. 424 // You shouldn't need to call this: use Send() instead. 425 func (fb *Feedback) MarshalJSON() ([]byte, error) { 426 return json.Marshal(&struct { 427 Variables map[string]string `json:"variables,omitempty"` 428 Rerun float64 `json:"rerun,omitempty"` 429 Items []*Item `json:"items"` 430 }{ 431 Items: fb.Items, 432 Rerun: fb.rerun, 433 Variables: fb.vars, 434 }) 435 } 436 437 // Send generates JSON from this struct and sends it to Alfred 438 // (by writing the JSON to STDOUT). 439 // 440 // You shouldn't need to call this directly: use SendFeedback() instead. 441 func (fb *Feedback) Send() error { 442 if fb.sent { 443 log.Printf("Feedback already sent. Ignoring.") 444 return nil 445 } 446 output, err := json.MarshalIndent(fb, "", " ") 447 if err != nil { 448 return fmt.Errorf("Error generating JSON : %v", err) 449 } 450 451 os.Stdout.Write(output) 452 fb.sent = true 453 log.Printf("Sent %d result(s) to Alfred", len(fb.Items)) 454 return nil 455 } 456 457 // Sort sorts Items against query. Uses a fuzzy.Sorter with the specified 458 // options. 459 func (fb *Feedback) Sort(query string, opts ...fuzzy.Option) []*fuzzy.Result { 460 s := fuzzy.New(fb, opts...) 461 return s.Sort(query) 462 } 463 464 // Filter fuzzy-sorts Items against query and deletes Items that don't match. 465 // It returns a slice of Result structs, which contain the results of the 466 // fuzzy sorting. 467 func (fb *Feedback) Filter(query string, opts ...fuzzy.Option) []*fuzzy.Result { 468 var items []*Item 469 var res []*fuzzy.Result 470 471 r := fb.Sort(query, opts...) 472 for i, it := range fb.Items { 473 if r[i].Match { 474 items = append(items, it) 475 res = append(res, r[i]) 476 } 477 } 478 fb.Items = items 479 return res 480 } 481 482 // Keywords implements fuzzy.Sortable. 483 // 484 // Returns the match or title field for Item i. 485 func (fb *Feedback) Keywords(i int) string { 486 it := fb.Items[i] 487 // Sort on title if match isn't set 488 if it.match != nil { 489 return *it.match 490 } 491 return it.title 492 } 493 494 // Len implements sort.Interface. 495 func (fb *Feedback) Len() int { return len(fb.Items) } 496 497 // Less implements sort.Interface. 498 func (fb *Feedback) Less(i, j int) bool { return fb.Keywords(i) < fb.Keywords(j) } 499 500 // Swap implements sort.Interface. 501 func (fb *Feedback) Swap(i, j int) { fb.Items[i], fb.Items[j] = fb.Items[j], fb.Items[i] } 502 503 // ArgVars lets you set workflow variables from Run Script actions. 504 // It emits the arg and variables you set in the format required by Alfred. 505 // 506 // Use ArgVars.Send() to pass variables to downstream workflow elements. 507 type ArgVars struct { 508 arg *string 509 vars map[string]string 510 } 511 512 // NewArgVars returns an initialised ArgVars object. 513 func NewArgVars() *ArgVars { 514 return &ArgVars{vars: map[string]string{}} 515 } 516 517 // Arg sets the arg/query to be passed to the next workflow action. 518 func (a *ArgVars) Arg(s string) *ArgVars { 519 a.arg = &s 520 return a 521 } 522 523 // Vars returns ArgVars' variables. 524 func (a *ArgVars) Vars() map[string]string { 525 return a.vars 526 } 527 528 // Var sets the value of a workflow variable. 529 func (a *ArgVars) Var(k, v string) *ArgVars { 530 a.vars[k] = v 531 return a 532 } 533 534 // String returns a string representation. 535 // 536 // If any variables are set, JSON is returned. Otherwise, a plain string 537 // is returned. 538 func (a *ArgVars) String() (string, error) { 539 if len(a.vars) == 0 { 540 if a.arg != nil { 541 return *a.arg, nil 542 } 543 return "", nil 544 } 545 // Vars set, so return as JSON 546 data, err := a.MarshalJSON() 547 if err != nil { 548 return "", err 549 } 550 return string(data), nil 551 } 552 553 // Send outputs arg and variables to Alfred by printing a response to STDOUT. 554 func (a *ArgVars) Send() error { 555 s, err := a.String() 556 if err != nil { 557 return err 558 } 559 _, err = fmt.Print(s) 560 return err 561 } 562 563 // MarshalJSON serialises ArgVars to JSON. 564 // You probably don't need to call this: use ArgVars.String() instead. 565 func (a *ArgVars) MarshalJSON() ([]byte, error) { 566 567 // Return arg regardless of whether it's empty or not: 568 // we have to return *something* 569 if len(a.vars) == 0 { 570 // Want empty string, i.e. "", not null 571 var arg string 572 if a.arg != nil { 573 arg = *a.arg 574 } 575 return json.Marshal(arg) 576 } 577 578 return json.Marshal(&struct { 579 Root interface{} `json:"alfredworkflow"` 580 }{ 581 Root: &struct { 582 Arg *string `json:"arg,omitempty"` 583 Vars map[string]string `json:"variables"` 584 }{ 585 Arg: a.arg, 586 Vars: a.vars, 587 }, 588 }) 589 }