github.com/quantumghost/awgo@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  }