├── .eslintrc.js ├── .gitignore ├── Firefox Assistant.afdesign ├── LICENCE.txt ├── README.md ├── actions.go ├── actions_custom.go ├── client.go ├── demo.gif ├── doc ├── Firefox Trigger Demo.alfredworkflow ├── README.md ├── bookmarklets.md ├── current-tab-actions.png ├── current-tab-info-to-pipe.zsh ├── customisation.md ├── extension-connected.png ├── extension-error.png ├── firefox-inspect-extension.png ├── index.md ├── integration.md ├── manage-addon.png ├── scripts.md ├── setup.md ├── troubleshooting.md ├── usage.md ├── workflow-error.png └── workflow-register.png ├── env ├── extension ├── alfred.js ├── icons │ ├── bowler-black.png │ ├── bowler-black.svg │ ├── bowler-purple.svg │ ├── bowler-red.svg │ ├── bowler.svg │ └── icon.svg ├── manifest.json ├── popup.css ├── popup.html └── popup.js ├── firefox.go ├── go.mod ├── go.sum ├── icon.png ├── icons.go ├── icons ├── addon.png ├── bookmark.png ├── bookmarklet.png ├── docs.png ├── error.png ├── history.png ├── incognito.png ├── install.png ├── issue.png ├── more.png ├── script.png ├── tab.png ├── update-available.png ├── update-ok.png ├── url.png └── warning.png ├── info.plist ├── magefile.go ├── main.go ├── modd.conf ├── models.go ├── rpc_client.go ├── rpc_server.go ├── screenshots ├── bookmark-search.png ├── history-search.png └── tab-actions.png ├── scripts ├── Open in Chrome.icns ├── Open in Chrome.sh ├── Open in Default Application.png ├── Open in Default Application.sh ├── Open in Firefox.icns ├── Open in Firefox.sh ├── Open in Safari.icns ├── Open in Safari.sh └── README.md ├── server.go └── server.sh /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /build 3 | -------------------------------------------------------------------------------- /Firefox Assistant.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/Firefox Assistant.afdesign -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Firefox Assistant - Alfred workflow for Firefox 2 | 3 | The MIT License (MIT) 4 | --------------------- 5 | 6 | Copyright (c) 2020 Dean Jackson 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included 17 | in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Alfred-Firefox icon 3 |
4 | 5 | Firefox Assistant for Alfred 6 | ============================ 7 | 8 | Search and manipulate Firefox's bookmarks, history and tabs from Alfred. 9 | 10 | ![Animated demo of workflow in use][demo] 11 | 12 | The workflow can be easily [extended with your own actions][scripts]. 13 | 14 | Installation 15 | ------------ 16 | 17 | The workflow supports Alfred 4+ and the extension works with (at least) Firefox, Firefox Nightly and Firefox Developer Edition. 18 | 19 | 1. Download and install the [latest version of the workflow][workflow]. 20 | 2. Run `ffass` in Alfred and choose `Install Firefox Extension` to get [the Firefox extension][addon]. 21 | 22 | See [the setup documentation][setup] for more details. 23 | 24 | 25 | Usage 26 | ----- 27 | 28 | The basic usage is: 29 | 30 | - `bm ` — Search bookmarks 31 | - `bml ` — Search bookmarklets 32 | - `hist ` — Search history 33 | - `dl []` — Search downloads 34 | - `tab []` — Search tabs 35 | - `ffass []` — Workflow status and links 36 | 37 | See [the usage documentation][usage] for full details. 38 | 39 | 40 | Integration 41 | ----------- 42 | 43 | The workflow can be used by other workflows to retrieve the title and URL of the active Firefox tab (in lieu of AppleScript, which Firefox doesn't support). See [the integration docs][integration] for details. 44 | 45 | 46 | Documentation 47 | ------------- 48 | 49 | See [the full documentation][docs] for detailed info on setting up, using and customising the workflow. 50 | 51 | 52 | Licensing & thanks 53 | ------------------ 54 | 55 | This workflow and extension are released under the [MIT licence][licence]. 56 | 57 | It is written in [Go][go] and heavily based on the [AwGo library][awgo]. The icons are based on [Font Awesome][fontawesome]. 58 | 59 | 60 | [addon]: https://addons.mozilla.org/en-US/firefox/addon/alfred-launcher-integration/ 61 | [licence]: https://github.com/deanishe/alfred-firefox/blob/master/LICENCE.txt 62 | [workflow]: https://github.com/deanishe/alfred-firefox/releases/latest 63 | [demo]: https://github.com/deanishe/alfred-firefox/blob/master/demo.gif 64 | [docs]: https://github.com/deanishe/alfred-firefox/blob/master/doc/index.md 65 | [scripts]: https://github.com/deanishe/alfred-firefox/blob/master/doc/scripts.md 66 | [integration]: https://github.com/deanishe/alfred-firefox/blob/master/doc/integration.md 67 | [usage]: https://github.com/deanishe/alfred-firefox/blob/master/doc/usage.md 68 | [setup]: https://github.com/deanishe/alfred-firefox/blob/master/doc/setup.md 69 | [go]: https://golang.org 70 | [awgo]: https://github.com/deanishe/awgo 71 | [fontawesome]: https://fontawesome.com/ 72 | 73 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | aw "github.com/deanishe/awgo" 15 | "github.com/deanishe/awgo/util" 16 | ) 17 | 18 | var ( 19 | tabActions = map[string]tabAction{} 20 | urlActions = map[string]urlAction{} 21 | ) 22 | 23 | type tabAction interface { 24 | Name() string 25 | Icon() *aw.Icon 26 | Run(tabID int) error 27 | } 28 | 29 | type urlAction interface { 30 | Name() string 31 | Icon() *aw.Icon 32 | Run(URL string) error 33 | } 34 | 35 | func init() { 36 | for _, a := range []tabAction{ 37 | tAction{name: "Activate Tab", action: "activate", icon: iconTab}, 38 | tAction{name: "Close Tabs to Left", action: "close-left", icon: iconTab}, 39 | tAction{name: "Close Tabs to Right", action: "close-right", icon: iconTab}, 40 | tAction{name: "Close Other Tabs", action: "close-other", icon: iconTab}, 41 | } { 42 | tabActions[a.Name()] = a 43 | } 44 | 45 | a := openIncognito{} 46 | urlActions[a.Name()] = a 47 | } 48 | 49 | func loadURLActions() error { 50 | var ( 51 | scripts = map[string]string{} 52 | infos []os.FileInfo 53 | err error 54 | ) 55 | for _, dir := range scriptDirs { 56 | if infos, err = ioutil.ReadDir(dir); err != nil { 57 | return err 58 | } 59 | 60 | for _, fi := range infos { 61 | if fi.IsDir() { 62 | continue 63 | } 64 | 65 | var ( 66 | path = filepath.Join(dir, fi.Name()) 67 | ext = strings.ToLower(filepath.Ext(fi.Name())) 68 | name = fi.Name()[0 : len(fi.Name())-len(ext)] 69 | _, known = util.DefaultInterpreters[ext] 70 | exe = fi.Mode()&0111 != 0 71 | ) 72 | if exe || known { 73 | scripts[name] = path 74 | } 75 | 76 | if imageExts[ext] { 77 | scriptIcons[name] = &aw.Icon{Value: path} 78 | } 79 | } 80 | } 81 | 82 | for name, path := range scripts { 83 | log.Printf("loaded URL action %q from %q", name, util.PrettyPath(path)) 84 | a := uAction{ 85 | name: name, 86 | icon: actionIcon(name, iconURL), 87 | script: path, 88 | } 89 | urlActions[name] = a 90 | } 91 | 92 | return nil 93 | } 94 | 95 | type tAction struct { 96 | name string 97 | icon *aw.Icon 98 | action string 99 | } 100 | 101 | func (a tAction) Name() string { return a.name } 102 | func (a tAction) Icon() *aw.Icon { return a.icon } 103 | func (a tAction) Run(tabID int) error { 104 | c := mustClient() 105 | switch a.action { 106 | case "activate": 107 | _, err := util.RunAS(fmt.Sprintf(`tell application "%s" to activate`, c.appName)) 108 | if err != nil { 109 | return err 110 | } 111 | return c.ActivateTab(tabID) 112 | case "close-left": 113 | return c.CloseTabsLeft(tabID) 114 | case "close-right": 115 | return c.CloseTabsRight(tabID) 116 | case "close-other": 117 | return c.CloseTabsOther(tabID) 118 | default: 119 | return fmt.Errorf("unknown action %q", action) 120 | } 121 | } 122 | 123 | type uAction struct { 124 | name string 125 | icon *aw.Icon 126 | script string 127 | } 128 | 129 | func (a uAction) Name() string { return a.name } 130 | func (a uAction) Icon() *aw.Icon { return a.icon } 131 | func (a uAction) Run(URL string) error { 132 | c, err := newClient() 133 | if err == nil { 134 | os.Setenv("BROWSER", c.appName) 135 | } 136 | data, err := util.Run(a.script, URL) 137 | if err != nil { 138 | return err 139 | } 140 | s := string(data) 141 | if s != "" { 142 | log.Print(util.Pad(fmt.Sprintf(" output: %q ", a.name), "-", 50)) 143 | log.Print(s) 144 | } 145 | return nil 146 | } 147 | 148 | // URL action to open a URL in a new incognito window 149 | type openIncognito struct{} 150 | 151 | func (a openIncognito) Name() string { return "Open in Incognito Window" } 152 | func (a openIncognito) Icon() *aw.Icon { return iconIncognito } 153 | func (a openIncognito) Run(URL string) error { 154 | mustClient().OpenIncognito(URL) 155 | return nil 156 | } 157 | 158 | var ( 159 | _ tabAction = (*tAction)(nil) 160 | _ urlAction = (*uAction)(nil) 161 | _ urlAction = openIncognito{} 162 | ) 163 | -------------------------------------------------------------------------------- /actions_custom.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | aw "github.com/deanishe/awgo" 12 | ) 13 | 14 | // actions defined by user via environment variables. 15 | type customActions []customAction 16 | 17 | // Add custom actions to a Bookmark/Tab item. If "tab" is false, 18 | // only URL actions are added. 19 | func (ca customActions) Add(it *aw.Item, tab bool) { 20 | for _, a := range ca { 21 | if a.kind == "tab" && !tab { 22 | continue 23 | } 24 | a.Add(it) 25 | } 26 | } 27 | 28 | // action defined via environment variable. 29 | type customAction struct { 30 | kind string // "tab", "url" or "bookmarklet" 31 | id string // only set on "bookmarklet" actions 32 | name string // human-readable name of action 33 | 34 | // Modifier keys. If empty, the action isn't bound to 35 | // any keyboard shortcut, but is still shown in action lists. 36 | mods []aw.ModKey 37 | } 38 | 39 | // add action to Bookmark/Tab item 40 | func (ca customAction) Add(it *aw.Item) { 41 | if len(ca.mods) == 0 { // list-only action 42 | return 43 | } 44 | m := it.NewModifier(ca.mods...). 45 | Subtitle(ca.name). 46 | Var("ACTION", ca.name) 47 | 48 | switch ca.kind { 49 | case "tab": 50 | m.Icon(iconTab).Var("CMD", "tab") 51 | case "url": 52 | m.Var("CMD", "url").Icon(actionIcon(ca.name, iconURL)) 53 | case "bookmarklet": 54 | m.Var("CMD", "run-bookmarklet").Var("BOOKMARK", ca.id). 55 | Icon(actionIcon(ca.name, iconBookmarklet)) 56 | } 57 | 58 | } 59 | 60 | // return custom actions set by user 61 | func loadCustomActions() customActions { 62 | var ( 63 | actions customActions 64 | key string 65 | id string 66 | name string 67 | kind string 68 | mods []aw.ModKey 69 | ) 70 | for _, s := range os.Environ() { 71 | parts := strings.SplitN(s, "=", 2) 72 | key, name = strings.ToLower(parts[0]), parts[1] 73 | if !strings.HasPrefix(key, "url_") && !strings.HasPrefix(key, "tab_") { 74 | continue 75 | } 76 | if key == "url_default" { 77 | continue 78 | } 79 | 80 | kind = key[0:3] 81 | if mods = parseMods(key[4:]); len(mods) == 0 { 82 | // Warn about, but allow, empty modifiers. 83 | // The action won't be bound to any modifier key, but 84 | // it will still show up in tab action lists (if it's 85 | // a bookmarklet). 86 | log.Printf("invalid modifier for %q", key) 87 | } 88 | 89 | if strings.HasPrefix(name, "bml:") { 90 | kind = "bookmarklet" 91 | parts := strings.SplitN(name[4:], ",", 2) 92 | id, name = parts[0], parts[1] 93 | } 94 | 95 | actions = append(actions, customAction{kind, id, name, mods}) 96 | } 97 | return actions 98 | } 99 | 100 | // parse string of form "cmd_opt_shift" into slice of ModKeys 101 | func parseMods(s string) []aw.ModKey { 102 | var keys []aw.ModKey 103 | for _, v := range strings.Split(s, "_") { 104 | switch v { 105 | case "cmd": 106 | keys = append(keys, aw.ModCmd) 107 | case "opt", "alt": 108 | keys = append(keys, aw.ModOpt) 109 | case "ctrl": 110 | keys = append(keys, aw.ModCtrl) 111 | case "shift": 112 | keys = append(keys, aw.ModShift) 113 | // default: 114 | // log.Printf("[warning] unknown modifier: %q", v) 115 | } 116 | } 117 | return keys 118 | } 119 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | 14 | aw "github.com/deanishe/awgo" 15 | "github.com/deanishe/awgo/util" 16 | "github.com/peterbourgon/ff/ffcli" 17 | ) 18 | 19 | var ( 20 | // search history 21 | historyCmd = &ffcli.Command{ 22 | Name: "history", 23 | Usage: "alfred-firefox -query history", 24 | ShortHelp: "search browsing history", 25 | LongHelp: wrap(`Search browser history.`), 26 | Exec: runHistory, 27 | } 28 | 29 | // search downloads 30 | downloadsCmd = &ffcli.Command{ 31 | Name: "downloads", 32 | Usage: "alfred-firefox -query downloads", 33 | ShortHelp: "search downloads", 34 | LongHelp: wrap(`Search browser downloads.`), 35 | Exec: runDownloads, 36 | } 37 | 38 | // search bookmarks 39 | bookmarksCmd = &ffcli.Command{ 40 | Name: "bookmarks", 41 | Usage: "alfred-firefox -query bookmarks", 42 | ShortHelp: "search bookmarks", 43 | LongHelp: wrap(`Search browser bookmarks.`), 44 | Exec: runBookmarks, 45 | } 46 | 47 | // search bookmarklets 48 | bookmarkletsCmd = &ffcli.Command{ 49 | Name: "bookmarklets", 50 | Usage: "alfred-firefox -query bookmarklets", 51 | ShortHelp: "search bookmarklets", 52 | LongHelp: wrap(`Search bookmarklets and execute in frontmost tab.`), 53 | Exec: runBookmarklets, 54 | } 55 | 56 | /* 57 | // open URL 58 | // TODO: is this used? can it be removed? 59 | openURLCmd = &ffcli.Command{ 60 | Name: "open-url", 61 | Usage: "alfred-firefox -url open-url", 62 | ShortHelp: "open URL", 63 | LongHelp: wrap(`Open specified URL`), 64 | Exec: runOpenURL, 65 | } 66 | */ 67 | 68 | // execute a bookmarklet in the specified tab 69 | runBookmarkletCmd = &ffcli.Command{ 70 | Name: "run-bookmarklet", 71 | Usage: "alfred-firefox [-tab ] -bookmark run-bookmarklet", 72 | ShortHelp: "execute bookmarklet in the specified tab", 73 | LongHelp: wrap(` 74 | Execute a bookmarklet in a tab. Bookmark ID is required. 75 | If no tab ID is specified, bookmarklet is run in the active tab. 76 | `), 77 | Exec: runBookmarklet, 78 | } 79 | 80 | // filter open tabs 81 | tabsCmd = &ffcli.Command{ 82 | Name: "tabs", 83 | Usage: "alfred-firefox [-query ] tabs", 84 | ShortHelp: "filter browser tabs", 85 | LongHelp: wrap(`Filter browser tabs and perform actions on them.`), 86 | Exec: runTabs, 87 | } 88 | 89 | // filter tab & URL actions for current tab 90 | currentTabCmd = &ffcli.Command{ 91 | Name: "current-tab", 92 | Usage: "alfred-firefox [-query ] current-tab", 93 | ShortHelp: "actions for current tab", 94 | LongHelp: wrap(`Filter and run actions for current tab`), 95 | Exec: runCurrentTab, 96 | } 97 | 98 | infoFlags = flag.NewFlagSet("tab-info", flag.ExitOnError) 99 | shellVars bool // export tab info as shell variables 100 | // export info for current tab 101 | currentTabInfoCmd = &ffcli.Command{ 102 | Name: "tab-info", 103 | Usage: "alfred-firefox tab-info [-shell]", 104 | ShortHelp: "export current tab info", 105 | LongHelp: wrap(`Export current tab info as variables`), 106 | FlagSet: infoFlags, 107 | Exec: runCurrentTabInfo, 108 | } 109 | 110 | // run a tab/URL action for the specified tab 111 | tabCmd = &ffcli.Command{ 112 | Name: "tab", 113 | Usage: "alfred-firefox [-tab ] -action tab", 114 | ShortHelp: "execute tab action", 115 | LongHelp: wrap(` 116 | Execute specified action on tab. Both URL and tab actions 117 | are available on tabs. 118 | `), 119 | Exec: runTabAction, 120 | } 121 | 122 | // inject JS into the specified tab 123 | injectCmd = &ffcli.Command{ 124 | Name: "inject", 125 | Usage: "alfred-firefox [-tab ] inject 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /extension/popup.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | const Popup = function() { 3 | const self = this; 4 | 5 | self.status = null; 6 | 7 | self.connected = () => { 8 | return self.status === true; 9 | }; 10 | 11 | self.receive = msg => { 12 | if ('status' in msg) { 13 | self.status = msg.status; 14 | console.debug(`[popup] status=${msg.status}`); 15 | } 16 | }; 17 | self.send = msg => { 18 | self.port.postMessage(msg); 19 | // console.debug(`[popup] sent message`, msg); 20 | }; 21 | 22 | self.reconnect = () => { 23 | self.send({ command: 'reconnect' }); 24 | }; 25 | 26 | self.reload = () => { 27 | self.send({ command: 'reload' }); 28 | }; 29 | 30 | self.getStatus = () => { 31 | self.send({ command: 'status' }); 32 | }; 33 | 34 | self.port = browser.runtime.connect({ name: 'alfred-firefox' }); 35 | self.port.onMessage.addListener(self.receive); 36 | console.debug(`[popup] started`); 37 | self.getStatus(); 38 | }; 39 | 40 | (function() { 41 | const popup = new Popup(); 42 | const status = document.querySelector('#status'); 43 | 44 | // document 45 | // .querySelector('#reloadButton') 46 | // .addEventListener('click', popup.reload); 47 | 48 | setInterval(function() { 49 | popup.getStatus(); 50 | switch (popup.status) { 51 | case 'connected': 52 | status.classList.remove('disconnected'); 53 | status.classList.add('connected'); 54 | break; 55 | case 'disconnected': 56 | status.classList.remove('connected'); 57 | status.classList.add('disconnected'); 58 | popup.reconnect(); 59 | break; 60 | } 61 | }, 500); 62 | })(); 63 | -------------------------------------------------------------------------------- /firefox.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/binary" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "time" 14 | ) 15 | 16 | const timeout = time.Second * 5 17 | 18 | // errTimeout is returned by firefox.call if execution time exceeds timeout. 19 | type errTimeout struct { 20 | ID string // command ID 21 | } 22 | 23 | func (err errTimeout) Error() string { 24 | return fmt.Sprintf("timeout: %q", err.ID) 25 | } 26 | 27 | // command is a command sent to the extension. 28 | type command struct { 29 | ID string `json:"id"` 30 | Name string `json:"command"` 31 | Params interface{} `json:"params"` 32 | ch chan response 33 | } 34 | 35 | func (c command) String() string { 36 | return fmt.Sprintf("command #%s - %q", c.ID, c.Name) 37 | } 38 | 39 | // encode command into extension STDIO format. 40 | func (c command) encode() ([]byte, error) { 41 | js, err := json.Marshal(c) 42 | if err != nil { 43 | return nil, err 44 | } 45 | b := make([]byte, 4) 46 | binary.LittleEndian.PutUint32(b, uint32(len(js))) 47 | b = append(b, js...) 48 | return b, nil 49 | } 50 | 51 | // response is a generic response from the browser extension. 52 | // The full JSON response from the extension is contained in data to 53 | // be unmarshalled by the receiver. 54 | type response struct { 55 | ID string `json:"id"` // ID of the command this is a response to 56 | Err string `json:"error"` // error message returned by extension 57 | data []byte // full JSON response 58 | err error // error encountered decoding response 59 | } 60 | 61 | func (r response) String() string { 62 | return fmt.Sprintf("response #%s - %d bytes", r.ID, len(r.data)) 63 | } 64 | 65 | // firefox communicates with the browser extension via STDIN/STOUT. 66 | type firefox struct { 67 | commands chan command 68 | responses chan response 69 | done chan struct{} 70 | handlers map[string]chan response 71 | } 72 | 73 | func newFirefox() *firefox { 74 | return &firefox{ 75 | commands: make(chan command, 1), 76 | responses: make(chan response, 1), 77 | done: make(chan struct{}), 78 | handlers: map[string]chan response{}, 79 | } 80 | } 81 | 82 | // run the read/write loop to send & receive messages from the extension. 83 | func (f *firefox) run() { 84 | go func() { 85 | b := make([]byte, 4) 86 | for { 87 | // read payload size 88 | _, err := os.Stdin.Read(b) 89 | if err == io.EOF { 90 | return 91 | } 92 | if err != nil { 93 | log.Printf("[ERROR] read from stdin: %v", err) 94 | return 95 | } 96 | 97 | // read payload 98 | data := make([]byte, binary.LittleEndian.Uint32(b)) 99 | _, err = io.ReadFull(os.Stdin, data) 100 | if err == io.EOF { 101 | return 102 | } 103 | if err != nil { 104 | log.Printf("[ERROR] read from stdin: %v", err) 105 | return 106 | } 107 | // log.Printf("received %s", data) 108 | var r response 109 | if err := json.Unmarshal(data, &r); err != nil { 110 | r.err = err 111 | } 112 | r.data = data 113 | log.Printf("received %v", r) 114 | f.responses <- r 115 | } 116 | }() 117 | 118 | for { 119 | select { 120 | case <-f.done: 121 | return 122 | 123 | case cmd := <-f.commands: 124 | var ( 125 | data []byte 126 | // n int 127 | err error 128 | ) 129 | if data, err = cmd.encode(); err != nil { 130 | cmd.ch <- response{err: err} 131 | break 132 | } 133 | if _, err = os.Stdout.Write(data); err != nil { 134 | cmd.ch <- response{err: err} 135 | break 136 | } 137 | log.Printf("sent %v", cmd) 138 | f.handlers[cmd.ID] = cmd.ch 139 | 140 | case r := <-f.responses: 141 | ch, ok := f.handlers[r.ID] 142 | if ok { 143 | ch <- r 144 | delete(f.handlers, r.ID) 145 | } else { 146 | log.Printf("[ERROR] no handler for message %q", r.ID) 147 | } 148 | } 149 | } 150 | } 151 | 152 | // call passes a command to the extension and unmarshals the response into pointer v. 153 | // It returns an error if the command fails, the response isn't understood or 154 | // the respones takes too long. 155 | func (f *firefox) call(cmd string, params, v interface{}) error { 156 | c := command{ 157 | ID: newID(), 158 | Name: cmd, 159 | Params: params, 160 | ch: make(chan response), 161 | } 162 | f.commands <- c 163 | 164 | t := time.Tick(timeout) 165 | select { 166 | case r := <-c.ch: 167 | if r.err != nil { 168 | return r.err 169 | } 170 | return json.Unmarshal(r.data, v) 171 | case <-t: 172 | return errTimeout{c.ID} 173 | } 174 | } 175 | 176 | // exist the run loop. 177 | func (f *firefox) stop() { close(f.done) } 178 | 179 | var lastUID = 0 180 | 181 | // create a new command ID. 182 | func newID() string { 183 | lastUID++ 184 | return fmt.Sprintf("%d.%d", time.Now().Unix(), lastUID) 185 | } 186 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.deanishe.net/alfred-firefox-assistant 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bmatcuk/doublestar v1.3.4 // indirect 7 | github.com/deanishe/awgo v0.28.0 8 | github.com/magefile/mage v1.11.0 9 | github.com/mitchellh/go-wordwrap v1.0.1 10 | github.com/peterbourgon/ff v1.7.1-0.20200101221212-72e429b6ad42 11 | golang.org/x/text v0.3.6 // indirect 12 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= 4 | github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= 5 | github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= 6 | github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/deanishe/awgo v0.28.0 h1:JEXNksHApotc9Cr0hKqdKmsQbZlbWPpIt2j2OMPSiLs= 12 | github.com/deanishe/awgo v0.28.0/go.mod h1:hwkoKOWPjZ8O6rPKIZUsKtTvShHbZakDXku8hIPwG3Q= 13 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 14 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 23 | github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= 24 | github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 25 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 26 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 27 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 28 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 29 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 30 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= 31 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 32 | github.com/peterbourgon/ff v1.7.1-0.20200101221212-72e429b6ad42 h1:uRI2skRHFOZnzhVQYvoNmhOEpvxMkUtQJ5WwWoKAvLg= 33 | github.com/peterbourgon/ff v1.7.1-0.20200101221212-72e429b6ad42/go.mod h1:/KKxnU5cBj4w21jEMj4Rway/kslRP6XAOHh7CH8AyAM= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 39 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 41 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | go.deanishe.net/env v0.5.1 h1:WiOncK5uJj8Um57Vj2dc1bq1lMN7fgRag9up7I3LZy0= 43 | go.deanishe.net/env v0.5.1/go.mod h1:ihEYfDm0K0hq3f5ACTCQDrMTWxH9fTiA1lh1i0aMqm0= 44 | go.deanishe.net/fuzzy v1.0.0 h1:3Qp6PCX0DLb9z03b5OHwAGsbRSkgJpSLncsiDdXDt4Y= 45 | go.deanishe.net/fuzzy v1.0.0/go.mod h1:2yEEMfG7jWgT1s5EO0TteVWmx2MXFBRMr5cMm84bQNY= 46 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 47 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 48 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 49 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 53 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 57 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 61 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 64 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 66 | howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= 67 | howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 68 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icon.png -------------------------------------------------------------------------------- /icons.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import aw "github.com/deanishe/awgo" 7 | 8 | var ( 9 | iconAddon = &aw.Icon{Value: "icons/addon.png"} 10 | iconBookmark = &aw.Icon{Value: "icons/bookmark.png"} 11 | iconBookmarklet = &aw.Icon{Value: "icons/bookmarklet.png"} 12 | iconDocs = &aw.Icon{Value: "icons/docs.png"} 13 | iconError = &aw.Icon{Value: "icons/error.png"} 14 | iconHistory = &aw.Icon{Value: "icons/history.png"} 15 | iconIncognito = &aw.Icon{Value: "icons/incognito.png"} 16 | iconInstall = &aw.Icon{Value: "icons/install.png"} 17 | iconIssue = &aw.Icon{Value: "icons/issue.png"} 18 | iconMore = &aw.Icon{Value: "icons/more.png"} 19 | iconScript = &aw.Icon{Value: "icons/script.png"} 20 | iconTab = &aw.Icon{Value: "icons/tab.png"} 21 | iconUpdateAvailable = &aw.Icon{Value: "icons/update-available.png"} 22 | iconUpdateOK = &aw.Icon{Value: "icons/update-ok.png"} 23 | iconURL = &aw.Icon{Value: "icons/url.png"} 24 | iconWarning = &aw.Icon{Value: "icons/warning.png"} 25 | 26 | // populated by loadURLActions 27 | scriptIcons = map[string]*aw.Icon{} 28 | 29 | imageExts = map[string]bool{ 30 | ".png": true, 31 | ".jpg": true, 32 | ".jpeg": true, 33 | ".gif": true, 34 | ".icns": true, 35 | } 36 | ) 37 | 38 | func init() { 39 | aw.IconError = iconError 40 | aw.IconWarning = iconWarning 41 | } 42 | 43 | // return custom icon or fallback 44 | func actionIcon(name string, fallback *aw.Icon) *aw.Icon { 45 | if icon, ok := scriptIcons[name]; ok { 46 | return icon 47 | } 48 | return fallback 49 | } 50 | -------------------------------------------------------------------------------- /icons/addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/addon.png -------------------------------------------------------------------------------- /icons/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/bookmark.png -------------------------------------------------------------------------------- /icons/bookmarklet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/bookmarklet.png -------------------------------------------------------------------------------- /icons/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/docs.png -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/error.png -------------------------------------------------------------------------------- /icons/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/history.png -------------------------------------------------------------------------------- /icons/incognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/incognito.png -------------------------------------------------------------------------------- /icons/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/install.png -------------------------------------------------------------------------------- /icons/issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/issue.png -------------------------------------------------------------------------------- /icons/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/more.png -------------------------------------------------------------------------------- /icons/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/script.png -------------------------------------------------------------------------------- /icons/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/tab.png -------------------------------------------------------------------------------- /icons/update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/update-available.png -------------------------------------------------------------------------------- /icons/update-ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/update-ok.png -------------------------------------------------------------------------------- /icons/url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/url.png -------------------------------------------------------------------------------- /icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/icons/warning.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | net.deanishe.alfred.firefox-assistant 7 | connections 8 | 9 | 01563DAF-B949-4BE5-A856-1C32ABFA03E9 10 | 11 | 12 | destinationuid 13 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | vitoclose 19 | 20 | 21 | 22 | 1EC09EB7-CFB9-47D8-84DE-37BF4875F906 23 | 24 | 25 | destinationuid 26 | AE956921-0416-405F-B748-94C23CDB2774 27 | modifiers 28 | 0 29 | modifiersubtext 30 | 31 | vitoclose 32 | 33 | 34 | 35 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 36 | 37 | 38 | destinationuid 39 | 1EC09EB7-CFB9-47D8-84DE-37BF4875F906 40 | modifiers 41 | 0 42 | modifiersubtext 43 | 44 | vitoclose 45 | 46 | 47 | 48 | 2C1CC9A6-A0BD-47C3-8B39-372093A348B1 49 | 50 | 51 | destinationuid 52 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 53 | modifiers 54 | 0 55 | modifiersubtext 56 | 57 | vitoclose 58 | 59 | 60 | 61 | 38D3C7F8-AD9B-4880-8502-5DE4D69EA265 62 | 63 | 64 | destinationuid 65 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 66 | modifiers 67 | 0 68 | modifiersubtext 69 | 70 | vitoclose 71 | 72 | 73 | 74 | 51A796F3-097D-42E9-B30B-156D2DDF354B 75 | 76 | 77 | destinationuid 78 | 3B55C751-F185-42F2-94E2-51D978345C6A 79 | modifiers 80 | 0 81 | modifiersubtext 82 | 83 | vitoclose 84 | 85 | 86 | 87 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 88 | 89 | 90 | destinationuid 91 | E2BDC50E-36DB-45DE-B40E-24BA116ADFE6 92 | modifiers 93 | 0 94 | modifiersubtext 95 | 96 | vitoclose 97 | 98 | 99 | 100 | 58CBD6D3-C6D1-48CF-9A34-7EE5EBA5886B 101 | 102 | 103 | destinationuid 104 | B353C303-DA6D-4AFC-8F19-04BA6ABB1E27 105 | modifiers 106 | 0 107 | modifiersubtext 108 | 109 | vitoclose 110 | 111 | 112 | 113 | 5A0372A7-7488-4146-B8CD-17B1834B2EF6 114 | 115 | 116 | destinationuid 117 | 01563DAF-B949-4BE5-A856-1C32ABFA03E9 118 | modifiers 119 | 0 120 | modifiersubtext 121 | 122 | vitoclose 123 | 124 | 125 | 126 | 6532A845-EED7-4D18-B9E5-CF600E823EC3 127 | 128 | 129 | destinationuid 130 | 9BC83CA0-50A2-4869-B7EB-BFBD30373A03 131 | modifiers 132 | 0 133 | modifiersubtext 134 | 135 | vitoclose 136 | 137 | 138 | 139 | 7D1126FC-FAE3-40C1-A536-43C272DF9E69 140 | 141 | 142 | destinationuid 143 | 38D3C7F8-AD9B-4880-8502-5DE4D69EA265 144 | modifiers 145 | 0 146 | modifiersubtext 147 | 148 | vitoclose 149 | 150 | 151 | 152 | 83477ED1-950C-4E4D-9D22-CDEC701CA68D 153 | 154 | 155 | destinationuid 156 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 157 | modifiers 158 | 0 159 | modifiersubtext 160 | 161 | vitoclose 162 | 163 | 164 | 165 | 90AB6B8E-FA06-41A9-84E6-5FFCE62AC5F2 166 | 167 | 168 | destinationuid 169 | 5BF0D3BF-6F14-4F9A-9B19-A9C967146414 170 | modifiers 171 | 0 172 | modifiersubtext 173 | 174 | vitoclose 175 | 176 | 177 | 178 | 93E61791-1520-4B4F-9257-C5B28A505058 179 | 180 | 181 | destinationuid 182 | A52B48B5-24BA-4D67-A6DD-C2F404F964CC 183 | modifiers 184 | 0 185 | modifiersubtext 186 | 187 | vitoclose 188 | 189 | 190 | 191 | 96C33C0A-5D89-4903-97AC-E430858902FA 192 | 193 | 194 | destinationuid 195 | E51D9E39-F895-4FD6-B159-0CB29463CA21 196 | modifiers 197 | 0 198 | modifiersubtext 199 | 200 | vitoclose 201 | 202 | 203 | 204 | 9AF41D0B-F6CB-4063-BF22-96F22182CFE3 205 | 206 | 207 | destinationuid 208 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 209 | modifiers 210 | 0 211 | modifiersubtext 212 | 213 | vitoclose 214 | 215 | 216 | 217 | 9BC83CA0-50A2-4869-B7EB-BFBD30373A03 218 | 219 | 220 | destinationuid 221 | E05E7619-441A-4B3A-A8FE-8E21C2C82F10 222 | modifiers 223 | 0 224 | modifiersubtext 225 | 226 | sourceoutputuid 227 | 3C2FA73E-6A07-47FB-893A-CA63B538D84C 228 | vitoclose 229 | 230 | 231 | 232 | destinationuid 233 | F1A91452-6475-4C85-B248-2FBCDB93AF89 234 | modifiers 235 | 0 236 | modifiersubtext 237 | 238 | vitoclose 239 | 240 | 241 | 242 | A52B48B5-24BA-4D67-A6DD-C2F404F964CC 243 | 244 | 245 | destinationuid 246 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 247 | modifiers 248 | 0 249 | modifiersubtext 250 | 251 | vitoclose 252 | 253 | 254 | 255 | B353C303-DA6D-4AFC-8F19-04BA6ABB1E27 256 | 257 | 258 | destinationuid 259 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 260 | modifiers 261 | 0 262 | modifiersubtext 263 | 264 | vitoclose 265 | 266 | 267 | 268 | E05E7619-441A-4B3A-A8FE-8E21C2C82F10 269 | 270 | 271 | destinationuid 272 | 5FA508A4-6E47-4864-9E18-E367A73CE48E 273 | modifiers 274 | 0 275 | modifiersubtext 276 | 277 | vitoclose 278 | 279 | 280 | 281 | E2BDC50E-36DB-45DE-B40E-24BA116ADFE6 282 | 283 | 284 | destinationuid 285 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 286 | modifiers 287 | 0 288 | modifiersubtext 289 | 290 | vitoclose 291 | 292 | 293 | 294 | destinationuid 295 | 9AF41D0B-F6CB-4063-BF22-96F22182CFE3 296 | modifiers 297 | 0 298 | modifiersubtext 299 | 300 | sourceoutputuid 301 | 65CCCA7A-4865-4AA0-ABB3-19B13996FC3E 302 | vitoclose 303 | 304 | 305 | 306 | E3C926B1-9A80-48ED-9705-8C835D7FB18F 307 | 308 | 309 | destinationuid 310 | 2C1CC9A6-A0BD-47C3-8B39-372093A348B1 311 | modifiers 312 | 0 313 | modifiersubtext 314 | 315 | vitoclose 316 | 317 | 318 | 319 | E51D9E39-F895-4FD6-B159-0CB29463CA21 320 | 321 | 322 | destinationuid 323 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 324 | modifiers 325 | 0 326 | modifiersubtext 327 | 328 | vitoclose 329 | 330 | 331 | 332 | 333 | createdby 334 | Dean Jackson <deanishe@deanishe.net> 335 | description 336 | Control Firefox from Alfred 337 | disabled 338 | 339 | name 340 | Firefox Assistant 341 | objects 342 | 343 | 344 | config 345 | 346 | alfredfiltersresults 347 | 348 | alfredfiltersresultsmatchmode 349 | 0 350 | argumenttreatemptyqueryasnil 351 | 352 | argumenttrimmode 353 | 0 354 | argumenttype 355 | 0 356 | escaping 357 | 102 358 | keyword 359 | bm 360 | queuedelaycustom 361 | 3 362 | queuedelayimmediatelyinitially 363 | 364 | queuedelaymode 365 | 0 366 | queuemode 367 | 1 368 | runningsubtext 369 | Loading results… 370 | script 371 | ./alfred-firefox -query "$1" bookmarks 372 | scriptargtype 373 | 1 374 | scriptfile 375 | fsearch 376 | subtext 377 | Search Firefox Bookmarks 378 | title 379 | Firefox Bookmarks 380 | type 381 | 5 382 | withspace 383 | 384 | 385 | type 386 | alfred.workflow.input.scriptfilter 387 | uid 388 | E51D9E39-F895-4FD6-B159-0CB29463CA21 389 | version 390 | 3 391 | 392 | 393 | config 394 | 395 | action 396 | 0 397 | argument 398 | 0 399 | focusedappvariable 400 | 401 | focusedappvariablename 402 | 403 | hotkey 404 | 0 405 | hotmod 406 | 0 407 | hotstring 408 | 409 | leftcursor 410 | 411 | modsmode 412 | 0 413 | relatedAppsMode 414 | 0 415 | 416 | type 417 | alfred.workflow.trigger.hotkey 418 | uid 419 | 96C33C0A-5D89-4903-97AC-E430858902FA 420 | version 421 | 2 422 | 423 | 424 | config 425 | 426 | alfredfiltersresults 427 | 428 | alfredfiltersresultsmatchmode 429 | 0 430 | argumenttreatemptyqueryasnil 431 | 432 | argumenttrimmode 433 | 0 434 | argumenttype 435 | 1 436 | escaping 437 | 102 438 | queuedelaycustom 439 | 3 440 | queuedelayimmediatelyinitially 441 | 442 | queuedelaymode 443 | 0 444 | queuemode 445 | 1 446 | runningsubtext 447 | Loading actions… 448 | script 449 | ./alfred-firefox -query "$1" current-tab 450 | scriptargtype 451 | 1 452 | scriptfile 453 | 454 | subtext 455 | Filter/Run Actions on Current Firefox Tab 456 | title 457 | Current Tab Actions 458 | type 459 | 5 460 | withspace 461 | 462 | 463 | type 464 | alfred.workflow.input.scriptfilter 465 | uid 466 | 38D3C7F8-AD9B-4880-8502-5DE4D69EA265 467 | version 468 | 3 469 | 470 | 471 | config 472 | 473 | action 474 | 0 475 | argument 476 | 0 477 | focusedappvariable 478 | 479 | focusedappvariablename 480 | 481 | hotkey 482 | 17 483 | hotmod 484 | 1835008 485 | hotstring 486 | T 487 | leftcursor 488 | 489 | modsmode 490 | 0 491 | relatedApps 492 | 493 | org.mozilla.nightly 494 | org.mozilla.firefoxdeveloperedition 495 | org.mozilla.firefox 496 | 497 | relatedAppsMode 498 | 1 499 | 500 | type 501 | alfred.workflow.trigger.hotkey 502 | uid 503 | 7D1126FC-FAE3-40C1-A536-43C272DF9E69 504 | version 505 | 2 506 | 507 | 508 | config 509 | 510 | action 511 | 0 512 | argument 513 | 0 514 | focusedappvariable 515 | 516 | focusedappvariablename 517 | 518 | hotkey 519 | 0 520 | hotmod 521 | 0 522 | hotstring 523 | 524 | leftcursor 525 | 526 | modsmode 527 | 0 528 | relatedAppsMode 529 | 0 530 | 531 | type 532 | alfred.workflow.trigger.hotkey 533 | uid 534 | 5A0372A7-7488-4146-B8CD-17B1834B2EF6 535 | version 536 | 2 537 | 538 | 539 | config 540 | 541 | alfredfiltersresults 542 | 543 | alfredfiltersresultsmatchmode 544 | 0 545 | argumenttreatemptyqueryasnil 546 | 547 | argumenttrimmode 548 | 0 549 | argumenttype 550 | 0 551 | escaping 552 | 102 553 | keyword 554 | bml 555 | queuedelaycustom 556 | 3 557 | queuedelayimmediatelyinitially 558 | 559 | queuedelaymode 560 | 0 561 | queuemode 562 | 1 563 | runningsubtext 564 | Loading results… 565 | script 566 | ./alfred-firefox -query "$1" bookmarklets 567 | scriptargtype 568 | 1 569 | scriptfile 570 | fsearch 571 | subtext 572 | Search Firefox Bookmarklets 573 | title 574 | Firefox Bookmarklets 575 | type 576 | 5 577 | withspace 578 | 579 | 580 | type 581 | alfred.workflow.input.scriptfilter 582 | uid 583 | 01563DAF-B949-4BE5-A856-1C32ABFA03E9 584 | version 585 | 3 586 | 587 | 588 | config 589 | 590 | action 591 | 0 592 | argument 593 | 0 594 | focusedappvariable 595 | 596 | focusedappvariablename 597 | 598 | hotkey 599 | 0 600 | hotmod 601 | 0 602 | hotstring 603 | 604 | leftcursor 605 | 606 | modsmode 607 | 0 608 | relatedAppsMode 609 | 0 610 | 611 | type 612 | alfred.workflow.trigger.hotkey 613 | uid 614 | 58CBD6D3-C6D1-48CF-9A34-7EE5EBA5886B 615 | version 616 | 2 617 | 618 | 619 | config 620 | 621 | alfredfiltersresults 622 | 623 | alfredfiltersresultsmatchmode 624 | 0 625 | argumenttreatemptyqueryasnil 626 | 627 | argumenttrimmode 628 | 0 629 | argumenttype 630 | 1 631 | escaping 632 | 102 633 | keyword 634 | tab 635 | queuedelaycustom 636 | 3 637 | queuedelayimmediatelyinitially 638 | 639 | queuedelaymode 640 | 0 641 | queuemode 642 | 1 643 | runningsubtext 644 | Loading tabs… 645 | script 646 | ./alfred-firefox -query "$1" tabs 647 | scriptargtype 648 | 1 649 | scriptfile 650 | 651 | subtext 652 | Filter Firefox Tabs 653 | title 654 | Firefox Tabs 655 | type 656 | 5 657 | withspace 658 | 659 | 660 | type 661 | alfred.workflow.input.scriptfilter 662 | uid 663 | B353C303-DA6D-4AFC-8F19-04BA6ABB1E27 664 | version 665 | 3 666 | 667 | 668 | config 669 | 670 | concurrently 671 | 672 | escaping 673 | 102 674 | script 675 | ./alfred-firefox $CMD "$1" 676 | scriptargtype 677 | 1 678 | scriptfile 679 | 680 | type 681 | 5 682 | 683 | type 684 | alfred.workflow.action.script 685 | uid 686 | AE956921-0416-405F-B748-94C23CDB2774 687 | version 688 | 2 689 | 690 | 691 | config 692 | 693 | conditions 694 | 695 | 696 | inputstring 697 | {var:CMD} 698 | matchcasesensitive 699 | 700 | matchmode 701 | 0 702 | matchstring 703 | actions 704 | outputlabel 705 | All Actions 706 | uid 707 | 65CCCA7A-4865-4AA0-ABB3-19B13996FC3E 708 | 709 | 710 | elselabel 711 | else 712 | 713 | type 714 | alfred.workflow.utility.conditional 715 | uid 716 | E2BDC50E-36DB-45DE-B40E-24BA116ADFE6 717 | version 718 | 1 719 | 720 | 721 | config 722 | 723 | argument 724 | . 725 | /------------------------\ 726 | query={query} 727 | variables={allvars} 728 | \------------------------/ 729 | cleardebuggertext 730 | 731 | processoutputs 732 | 733 | 734 | type 735 | alfred.workflow.utility.debug 736 | uid 737 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 738 | version 739 | 1 740 | 741 | 742 | type 743 | alfred.workflow.utility.hidealfred 744 | uid 745 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 746 | version 747 | 1 748 | 749 | 750 | config 751 | 752 | argument 753 | . 754 | /------------------------\ 755 | query={query} 756 | variables={allvars} 757 | \------------------------/ 758 | cleardebuggertext 759 | 760 | processoutputs 761 | 762 | 763 | type 764 | alfred.workflow.utility.debug 765 | uid 766 | 1EC09EB7-CFB9-47D8-84DE-37BF4875F906 767 | version 768 | 1 769 | 770 | 771 | config 772 | 773 | alfredfiltersresults 774 | 775 | alfredfiltersresultsmatchmode 776 | 0 777 | argumenttreatemptyqueryasnil 778 | 779 | argumenttrimmode 780 | 0 781 | argumenttype 782 | 1 783 | escaping 784 | 102 785 | queuedelaycustom 786 | 3 787 | queuedelayimmediatelyinitially 788 | 789 | queuedelaymode 790 | 0 791 | queuemode 792 | 1 793 | runningsubtext 794 | Loading actions… 795 | script 796 | ./alfred-firefox -query "$1" actions 797 | scriptargtype 798 | 1 799 | scriptfile 800 | 801 | subtext 802 | Filter/Run Actions 803 | title 804 | Actions 805 | type 806 | 5 807 | withspace 808 | 809 | 810 | type 811 | alfred.workflow.input.scriptfilter 812 | uid 813 | 9AF41D0B-F6CB-4063-BF22-96F22182CFE3 814 | version 815 | 3 816 | 817 | 818 | config 819 | 820 | alfredfiltersresults 821 | 822 | alfredfiltersresultsmatchmode 823 | 0 824 | argumenttreatemptyqueryasnil 825 | 826 | argumenttrimmode 827 | 0 828 | argumenttype 829 | 0 830 | escaping 831 | 102 832 | keyword 833 | hist 834 | queuedelaycustom 835 | 3 836 | queuedelayimmediatelyinitially 837 | 838 | queuedelaymode 839 | 0 840 | queuemode 841 | 1 842 | runningsubtext 843 | Loading history… 844 | script 845 | ./alfred-firefox -query "$1" history 846 | scriptargtype 847 | 1 848 | scriptfile 849 | 850 | subtext 851 | Search Firefox History 852 | title 853 | Firefox History 854 | type 855 | 5 856 | withspace 857 | 858 | 859 | type 860 | alfred.workflow.input.scriptfilter 861 | uid 862 | A52B48B5-24BA-4D67-A6DD-C2F404F964CC 863 | version 864 | 3 865 | 866 | 867 | config 868 | 869 | action 870 | 0 871 | argument 872 | 0 873 | focusedappvariable 874 | 875 | focusedappvariablename 876 | 877 | hotkey 878 | 0 879 | hotmod 880 | 0 881 | hotstring 882 | 883 | leftcursor 884 | 885 | modsmode 886 | 0 887 | relatedAppsMode 888 | 0 889 | 890 | type 891 | alfred.workflow.trigger.hotkey 892 | uid 893 | 93E61791-1520-4B4F-9257-C5B28A505058 894 | version 895 | 2 896 | 897 | 898 | config 899 | 900 | action 901 | 0 902 | argument 903 | 0 904 | focusedappvariable 905 | 906 | focusedappvariablename 907 | 908 | hotkey 909 | 0 910 | hotmod 911 | 0 912 | hotstring 913 | 914 | leftcursor 915 | 916 | modsmode 917 | 0 918 | relatedAppsMode 919 | 0 920 | 921 | type 922 | alfred.workflow.trigger.hotkey 923 | uid 924 | E3C926B1-9A80-48ED-9705-8C835D7FB18F 925 | version 926 | 2 927 | 928 | 929 | config 930 | 931 | alfredfiltersresults 932 | 933 | alfredfiltersresultsmatchmode 934 | 0 935 | argumenttreatemptyqueryasnil 936 | 937 | argumenttrimmode 938 | 0 939 | argumenttype 940 | 1 941 | escaping 942 | 102 943 | keyword 944 | dl 945 | queuedelaycustom 946 | 3 947 | queuedelayimmediatelyinitially 948 | 949 | queuedelaymode 950 | 0 951 | queuemode 952 | 1 953 | runningsubtext 954 | Loading downloads… 955 | script 956 | ./alfred-firefox -query "$1" downloads 957 | scriptargtype 958 | 1 959 | scriptfile 960 | 961 | subtext 962 | Search Firefox Downloads 963 | title 964 | Firefox Downloads 965 | type 966 | 5 967 | withspace 968 | 969 | 970 | type 971 | alfred.workflow.input.scriptfilter 972 | uid 973 | 2C1CC9A6-A0BD-47C3-8B39-372093A348B1 974 | version 975 | 3 976 | 977 | 978 | config 979 | 980 | action 981 | 0 982 | argument 983 | 0 984 | focusedappvariable 985 | 986 | focusedappvariablename 987 | 988 | hotkey 989 | 35 990 | hotmod 991 | 1441792 992 | hotstring 993 | P 994 | leftcursor 995 | 996 | modsmode 997 | 0 998 | relatedApps 999 | 1000 | org.mozilla.nightly 1001 | org.mozilla.firefoxdeveloperedition 1002 | org.mozilla.firefox 1003 | 1004 | relatedAppsMode 1005 | 1 1006 | 1007 | type 1008 | alfred.workflow.trigger.hotkey 1009 | uid 1010 | 90AB6B8E-FA06-41A9-84E6-5FFCE62AC5F2 1011 | version 1012 | 2 1013 | 1014 | 1015 | config 1016 | 1017 | concurrently 1018 | 1019 | escaping 1020 | 102 1021 | script 1022 | # Run a bookmarklet in the current tab 1023 | # 1024 | # To get the bookmarklet ID, search your bookmarklets (default keyword: bml) 1025 | # and hit ⌘C on the bookmarklet. The copied text has the format "bml:<id>,Name of Bookmarklet". 1026 | # The ID is the random string where <id> is in the above example. 1027 | 1028 | ./alfred-firefox -bookmark seoxED9MBuqi run-bookmarklet 1029 | scriptargtype 1030 | 1 1031 | scriptfile 1032 | 1033 | type 1034 | 0 1035 | 1036 | type 1037 | alfred.workflow.action.script 1038 | uid 1039 | 5BF0D3BF-6F14-4F9A-9B19-A9C967146414 1040 | version 1041 | 2 1042 | 1043 | 1044 | config 1045 | 1046 | alfredfiltersresults 1047 | 1048 | alfredfiltersresultsmatchmode 1049 | 0 1050 | argumenttreatemptyqueryasnil 1051 | 1052 | argumenttrimmode 1053 | 0 1054 | argumenttype 1055 | 1 1056 | escaping 1057 | 102 1058 | keyword 1059 | ffass 1060 | queuedelaycustom 1061 | 3 1062 | queuedelayimmediatelyinitially 1063 | 1064 | queuedelaymode 1065 | 0 1066 | queuemode 1067 | 1 1068 | runningsubtext 1069 | Loading… 1070 | script 1071 | ./alfred-firefox -query "$1" options 1072 | scriptargtype 1073 | 1 1074 | scriptfile 1075 | 1076 | subtext 1077 | Workflow Status & Option 1078 | title 1079 | Firefox Assistant 1080 | type 1081 | 5 1082 | withspace 1083 | 1084 | 1085 | type 1086 | alfred.workflow.input.scriptfilter 1087 | uid 1088 | 83477ED1-950C-4E4D-9D22-CDEC701CA68D 1089 | version 1090 | 3 1091 | 1092 | 1093 | config 1094 | 1095 | action 1096 | 0 1097 | argument 1098 | 0 1099 | focusedappvariable 1100 | 1101 | focusedappvariablename 1102 | 1103 | hotkey 1104 | 1 1105 | hotmod 1106 | 1179648 1107 | hotstring 1108 | S 1109 | leftcursor 1110 | 1111 | modsmode 1112 | 0 1113 | relatedApps 1114 | 1115 | org.mozilla.nightly 1116 | org.mozilla.firefoxdeveloperedition 1117 | org.mozilla.firefox 1118 | 1119 | relatedAppsMode 1120 | 1 1121 | 1122 | type 1123 | alfred.workflow.trigger.hotkey 1124 | uid 1125 | 51A796F3-097D-42E9-B30B-156D2DDF354B 1126 | version 1127 | 2 1128 | 1129 | 1130 | config 1131 | 1132 | concurrently 1133 | 1134 | escaping 1135 | 102 1136 | script 1137 | # Run an action on the current tab 1138 | 1139 | # Run the built-in "Open in Safari" URL action 1140 | # ./alfred-firefox -action "Open in Safari" tab 1141 | 1142 | # Run the built-in "Close Tabs to Left" tab action 1143 | # ./alfred-firefox -action "Close Tabs to Left" tab 1144 | 1145 | # Run custom "YouTube - Save Video" tab action 1146 | ./alfred-firefox -action "YouTube - Save Video" tab 1147 | scriptargtype 1148 | 1 1149 | scriptfile 1150 | 1151 | type 1152 | 0 1153 | 1154 | type 1155 | alfred.workflow.action.script 1156 | uid 1157 | 3B55C751-F185-42F2-94E2-51D978345C6A 1158 | version 1159 | 2 1160 | 1161 | 1162 | config 1163 | 1164 | concurrently 1165 | 1166 | escaping 1167 | 102 1168 | script 1169 | ./alfred-firefox tab-info 1170 | scriptargtype 1171 | 1 1172 | scriptfile 1173 | 1174 | type 1175 | 0 1176 | 1177 | type 1178 | alfred.workflow.action.script 1179 | uid 1180 | E05E7619-441A-4B3A-A8FE-8E21C2C82F10 1181 | version 1182 | 2 1183 | 1184 | 1185 | config 1186 | 1187 | externaltriggerid 1188 | {var:TRIGGER} 1189 | passinputasargument 1190 | 1191 | passvariables 1192 | 1193 | workflowbundleid 1194 | {var:BUNDLE_ID} 1195 | 1196 | type 1197 | alfred.workflow.output.callexternaltrigger 1198 | uid 1199 | 5FA508A4-6E47-4864-9E18-E367A73CE48E 1200 | version 1201 | 1 1202 | 1203 | 1204 | config 1205 | 1206 | triggerid 1207 | current-tab 1208 | 1209 | type 1210 | alfred.workflow.trigger.external 1211 | uid 1212 | 6532A845-EED7-4D18-B9E5-CF600E823EC3 1213 | version 1214 | 1 1215 | 1216 | 1217 | config 1218 | 1219 | conditions 1220 | 1221 | 1222 | inputstring 1223 | 1224 | matchcasesensitive 1225 | 1226 | matchmode 1227 | 0 1228 | matchstring 1229 | 1230 | outputlabel 1231 | Pass to External Trigger 1232 | uid 1233 | 3C2FA73E-6A07-47FB-893A-CA63B538D84C 1234 | 1235 | 1236 | elselabel 1237 | Write to file 1238 | 1239 | type 1240 | alfred.workflow.utility.conditional 1241 | uid 1242 | 9BC83CA0-50A2-4869-B7EB-BFBD30373A03 1243 | version 1244 | 1 1245 | 1246 | 1247 | config 1248 | 1249 | concurrently 1250 | 1251 | escaping 1252 | 102 1253 | script 1254 | ./alfred-firefox tab-info > "$1" 1255 | echo "wrote tab info to $1" >&2 1256 | scriptargtype 1257 | 1 1258 | scriptfile 1259 | 1260 | type 1261 | 0 1262 | 1263 | type 1264 | alfred.workflow.action.script 1265 | uid 1266 | F1A91452-6475-4C85-B248-2FBCDB93AF89 1267 | version 1268 | 2 1269 | 1270 | 1271 | readme 1272 | Firefox Assistant 1273 | ================= 1274 | 1275 | Search your Firefox bookmarks, history and tabs. 1276 | 1277 | This workflow requires the corresponding Firefox extension to be installed: 1278 | 1279 | https://addons.mozilla.org/en-US/firefox/addon/alfred-launcher-integration/ 1280 | 1281 | 1282 | Please see the online docs for more information: 1283 | 1284 | https://github.com/deanishe/alfred-firefox/blob/master/doc/index.md 1285 | uidata 1286 | 1287 | 01563DAF-B949-4BE5-A856-1C32ABFA03E9 1288 | 1289 | note 1290 | Search bookmarklets 1291 | xpos 1292 | 210 1293 | ypos 1294 | 210 1295 | 1296 | 1EC09EB7-CFB9-47D8-84DE-37BF4875F906 1297 | 1298 | xpos 1299 | 1115 1300 | ypos 1301 | 420 1302 | 1303 | 26D5BE1B-9DAB-4039-9C7E-D639974A2614 1304 | 1305 | xpos 1306 | 1025 1307 | ypos 1308 | 420 1309 | 1310 | 2C1CC9A6-A0BD-47C3-8B39-372093A348B1 1311 | 1312 | note 1313 | Search downloads 1314 | xpos 1315 | 210 1316 | ypos 1317 | 715 1318 | 1319 | 38D3C7F8-AD9B-4880-8502-5DE4D69EA265 1320 | 1321 | note 1322 | Actions for current tab 1323 | xpos 1324 | 640 1325 | ypos 1326 | 40 1327 | 1328 | 3B55C751-F185-42F2-94E2-51D978345C6A 1329 | 1330 | colorindex 1331 | 11 1332 | note 1333 | EXAMPLE: Run an action on current tab/URL via Hotkey 1334 | xpos 1335 | 705 1336 | ypos 1337 | 895 1338 | 1339 | 51A796F3-097D-42E9-B30B-156D2DDF354B 1340 | 1341 | colorindex 1342 | 11 1343 | xpos 1344 | 535 1345 | ypos 1346 | 895 1347 | 1348 | 56FBB613-EE25-4DE4-930D-C1F51B9235D8 1349 | 1350 | xpos 1351 | 535 1352 | ypos 1353 | 410 1354 | 1355 | 58CBD6D3-C6D1-48CF-9A34-7EE5EBA5886B 1356 | 1357 | xpos 1358 | 25 1359 | ypos 1360 | 380 1361 | 1362 | 5A0372A7-7488-4146-B8CD-17B1834B2EF6 1363 | 1364 | xpos 1365 | 25 1366 | ypos 1367 | 210 1368 | 1369 | 5BF0D3BF-6F14-4F9A-9B19-A9C967146414 1370 | 1371 | colorindex 1372 | 11 1373 | note 1374 | EXAMPLE: Run a bookmarklet via Hotkey 1375 | xpos 1376 | 705 1377 | ypos 1378 | 720 1379 | 1380 | 5FA508A4-6E47-4864-9E18-E367A73CE48E 1381 | 1382 | xpos 1383 | 705 1384 | ypos 1385 | 1080 1386 | 1387 | 6532A845-EED7-4D18-B9E5-CF600E823EC3 1388 | 1389 | note 1390 | Share current tab info with other workflows/processes. 1391 | 1392 | Can either write tab info to a file or pass it to another External Trigger. 1393 | xpos 1394 | 35 1395 | ypos 1396 | 1135 1397 | 1398 | 7D1126FC-FAE3-40C1-A536-43C272DF9E69 1399 | 1400 | colorindex 1401 | 11 1402 | note 1403 | Don't forget to set this Hotkey! 1404 | xpos 1405 | 455 1406 | ypos 1407 | 40 1408 | 1409 | 83477ED1-950C-4E4D-9D22-CDEC701CA68D 1410 | 1411 | note 1412 | Workflow status & options 1413 | xpos 1414 | 210 1415 | ypos 1416 | 880 1417 | 1418 | 90AB6B8E-FA06-41A9-84E6-5FFCE62AC5F2 1419 | 1420 | colorindex 1421 | 11 1422 | xpos 1423 | 535 1424 | ypos 1425 | 720 1426 | 1427 | 93E61791-1520-4B4F-9257-C5B28A505058 1428 | 1429 | xpos 1430 | 30 1431 | ypos 1432 | 545 1433 | 1434 | 96C33C0A-5D89-4903-97AC-E430858902FA 1435 | 1436 | xpos 1437 | 20 1438 | ypos 1439 | 40 1440 | 1441 | 9AF41D0B-F6CB-4063-BF22-96F22182CFE3 1442 | 1443 | note 1444 | Filter Tab/URL Actions 1445 | xpos 1446 | 815 1447 | ypos 1448 | 495 1449 | 1450 | 9BC83CA0-50A2-4869-B7EB-BFBD30373A03 1451 | 1452 | xpos 1453 | 210 1454 | ypos 1455 | 1155 1456 | 1457 | A52B48B5-24BA-4D67-A6DD-C2F404F964CC 1458 | 1459 | note 1460 | Search history 1461 | xpos 1462 | 210 1463 | ypos 1464 | 545 1465 | 1466 | AE956921-0416-405F-B748-94C23CDB2774 1467 | 1468 | xpos 1469 | 1205 1470 | ypos 1471 | 390 1472 | 1473 | B353C303-DA6D-4AFC-8F19-04BA6ABB1E27 1474 | 1475 | note 1476 | Filter tabs 1477 | xpos 1478 | 210 1479 | ypos 1480 | 380 1481 | 1482 | E05E7619-441A-4B3A-A8FE-8E21C2C82F10 1483 | 1484 | xpos 1485 | 535 1486 | ypos 1487 | 1080 1488 | 1489 | E2BDC50E-36DB-45DE-B40E-24BA116ADFE6 1490 | 1491 | xpos 1492 | 640 1493 | ypos 1494 | 400 1495 | 1496 | E3C926B1-9A80-48ED-9705-8C835D7FB18F 1497 | 1498 | xpos 1499 | 30 1500 | ypos 1501 | 715 1502 | 1503 | E51D9E39-F895-4FD6-B159-0CB29463CA21 1504 | 1505 | note 1506 | Search bookmarks 1507 | xpos 1508 | 210 1509 | ypos 1510 | 40 1511 | 1512 | F1A91452-6475-4C85-B248-2FBCDB93AF89 1513 | 1514 | xpos 1515 | 535 1516 | ypos 1517 | 1230 1518 | 1519 | 1520 | variables 1521 | 1522 | TAB_CTRL 1523 | bml:seoxED9MBuqi,Add to Pinboard 1524 | TAB_GODOC 1525 | bml:uFJCA875bTEt,Open in GoDoc 1526 | URL_DEFAULT 1527 | Open in Firefox 1528 | URL_OPT 1529 | Open in Safari 1530 | 1531 | variablesdontexport 1532 | 1533 | TAB_CTRL 1534 | TAB_GODOC 1535 | 1536 | version 1537 | 0.2.2 1538 | webaddress 1539 | https://github.com/deanishe/alfred-firefox 1540 | 1541 | 1542 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | // +build mage 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/magefile/mage/mg" 14 | "github.com/magefile/mage/sh" 15 | 16 | "github.com/deanishe/awgo/util/build" 17 | ) 18 | 19 | const ( 20 | buildDir = "./build" 21 | distDir = "./dist" 22 | binaryName = "alfred-firefox" 23 | ) 24 | 25 | var ( 26 | assetPatterns = []string{ 27 | "info.plist", 28 | "*.png", 29 | "scripts/*", 30 | "icons/*.png", 31 | "LICENCE.txt", 32 | "README.md", 33 | } 34 | info *build.Info 35 | ) 36 | 37 | func init() { 38 | var err error 39 | if info, err = build.NewInfo(); err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | // Aliases are aliases for Mage commands. 45 | var Aliases = map[string]interface{}{ 46 | "b": Build, 47 | "ext": Extension, 48 | } 49 | 50 | // Default target to run when none is specified 51 | // If not set, running mage will list available targets 52 | // var Default = Build 53 | 54 | // Dist build & export workflow 55 | func Dist() { 56 | mg.Deps(cleanBuild, Deps) 57 | mg.SerialDeps(Build, Export, Extension) 58 | } 59 | 60 | // Extension build browser extension in dist directory 61 | func Extension() error { 62 | if err := sh.Run("mkdir", "-p", distDir); err != nil { 63 | return err 64 | } 65 | if err := sh.Rm("dist/alfred-firefox-assistant.xpi"); err != nil { 66 | return err 67 | } 68 | return sh.Run("zsh", "-c", 69 | `cd extension && zip -r ../dist/alfred-firefox-assistant.xpi * -x '*.DS_Store'`) 70 | } 71 | 72 | // Export compile build directory into workflow in dist directory 73 | func Export() error { 74 | fmt.Println("exporting workflow ...") 75 | path, err := build.Export(buildDir, distDir) 76 | if err != nil { 77 | return err 78 | } 79 | fmt.Println("exported workflow:", path) 80 | return nil 81 | } 82 | 83 | // Assets copy assets to build directory 84 | func Assets() error { 85 | mg.Deps(cleanAssets) 86 | fmt.Println("linking assets to build directory ...") 87 | globs := build.Globs(assetPatterns...) 88 | 89 | if err := build.SymlinkGlobs(buildDir, globs...); err != nil { 90 | return fmt.Errorf("symlink %v to %q: %v", globs, buildDir, err) 91 | } 92 | return sh.Run("cp", "-vf", "server.sh", buildDir) 93 | } 94 | 95 | // Build build executable in build directory 96 | func Build() error { 97 | mg.Deps(Assets) 98 | fmt.Println("building executable ...") 99 | if err := os.MkdirAll(buildDir, 0750); err != nil { 100 | return err 101 | } 102 | return sh.RunWith(info.Env(), "go", "build", "-o", buildDir+"/"+binaryName, ".") 103 | } 104 | 105 | // Link symlink build directory to Alfred's workflow directory 106 | func Link() error { 107 | mg.Deps(Build) 108 | fmt.Printf("linking workflow to %q ...\n", info.InstallDir) 109 | if err := sh.Rm(info.InstallDir); err != nil { 110 | return err 111 | } 112 | if err := os.MkdirAll(filepath.Dir(info.InstallDir), 0750); err != nil { 113 | return err 114 | } 115 | if err := build.Symlink(info.InstallDir, buildDir, true); err != nil { 116 | return fmt.Errorf("symlink %q to %q: %v", buildDir, info.InstallDir, err) 117 | } 118 | return nil 119 | } 120 | 121 | // Deps tidy & download deps 122 | func Deps() error { 123 | fmt.Println("verifying dependencies ...") 124 | if err := sh.RunWith(info.Env(), "go", "mod", "tidy", "-v"); err != nil { 125 | return err 126 | } 127 | return sh.RunWith(info.Env(), "go", "mod", "download") 128 | } 129 | 130 | // Clean remove built files 131 | func Clean() { 132 | fmt.Println("cleaning ...") 133 | mg.Deps(cleanBuild, cleanMage) 134 | } 135 | 136 | // CleanDist delete exported workflows in dist directory 137 | func CleanDist() error { 138 | fmt.Println("cleaning dist directory ...") 139 | return cleanDir(distDir, func(fi os.FileInfo) bool { 140 | return true 141 | }) 142 | } 143 | 144 | func cleanBinary() error { 145 | return cleanDir(buildDir, func(fi os.FileInfo) bool { 146 | if fi.Name() == binaryName { 147 | return true 148 | } 149 | return false 150 | }) 151 | } 152 | 153 | func cleanAssets() error { 154 | return cleanDir(buildDir, func(fi os.FileInfo) bool { 155 | if fi.IsDir() || filepath.Ext(fi.Name()) != "" { 156 | return true 157 | } 158 | return false 159 | }) 160 | } 161 | 162 | func cleanBuild() { 163 | mg.Deps(cleanBinary, cleanAssets) 164 | } 165 | 166 | func cleanMage() error { 167 | fmt.Println("cleaning mage cache ...") 168 | return sh.Run("mage", "-clean") 169 | } 170 | 171 | func cleanDir(dirname string, match func(os.FileInfo) bool) error { 172 | var paths []string 173 | 174 | if _, err := os.Stat(dirname); os.IsNotExist(err) { 175 | return nil 176 | } 177 | 178 | err := filepath.Walk(dirname, func(path string, fi os.FileInfo, err error) error { 179 | if err != nil { 180 | return err 181 | } 182 | if path == dirname { // ignore root 183 | return nil 184 | } 185 | if match(fi) { 186 | paths = append(paths, path) 187 | } 188 | return nil 189 | }) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | for _, p := range paths { 195 | if err := os.RemoveAll(p); err != nil { 196 | return err 197 | } 198 | fmt.Println("deleted:", p) 199 | } 200 | 201 | return nil 202 | } 203 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | // Command firefox is an Alfred workflow to interact with Firefox. 5 | package main 6 | 7 | // TODO: package extension 8 | 9 | import ( 10 | "bufio" 11 | "encoding/json" 12 | "flag" 13 | "fmt" 14 | "io/ioutil" 15 | "log" 16 | "os" 17 | "os/user" 18 | "path/filepath" 19 | "regexp" 20 | "strings" 21 | "time" 22 | 23 | aw "github.com/deanishe/awgo" 24 | "github.com/deanishe/awgo/update" 25 | "github.com/deanishe/awgo/util" 26 | "github.com/mitchellh/go-wordwrap" 27 | "github.com/peterbourgon/ff" 28 | "github.com/peterbourgon/ff/ffcli" 29 | ) 30 | 31 | const ( 32 | maxCacheAge = time.Minute * 30 33 | wrapWidth = 72 34 | ) 35 | 36 | const ( 37 | helpURL = "https://github.com/deanishe/alfred-firefox/issues" 38 | docsURL = "https://github.com/deanishe/alfred-firefox/blob/master/doc/index.md" 39 | addonURL = "https://addons.mozilla.org/en-US/firefox/addon/alfred-launcher-integration/" 40 | repo = "deanishe/alfred-firefox" 41 | ) 42 | 43 | // native application manifest 44 | var ( 45 | extensionID = "alfredfirefox@deanishe.net" 46 | extensionName = "net.deanishe.alfred.firefox" 47 | manifestPath = os.ExpandEnv("${HOME}/Library/Application Support/Mozilla/" + 48 | "NativeMessagingHosts/" + extensionName + ".json") 49 | ) 50 | 51 | // workflow variables 52 | var ( 53 | wf = aw.New( 54 | aw.HelpURL(helpURL), 55 | update.GitHub(repo), 56 | aw.AddMagic(registerMagic{}), 57 | ) 58 | 59 | // Filepaths 60 | scriptDirs = []string{ 61 | filepath.Join(wf.Dir(), "scripts"), 62 | filepath.Join(wf.DataDir(), "scripts"), 63 | } 64 | socketPath string 65 | pidFile string 66 | logfile string 67 | 68 | // CLI flags/environment variables 69 | URL string 70 | urlDefault string 71 | tabID int 72 | action string 73 | bookmarkID string 74 | query string 75 | 76 | rootFlags = flag.NewFlagSet("alfred-firefox", flag.ExitOnError) 77 | rootCmd = &ffcli.Command{ 78 | Usage: "alfred-firefox [flags] [args...]", 79 | ShortHelp: "Firefox workflow for Alfred", 80 | LongHelp: wrap(` 81 | Alfred workflow to control Firefox. 82 | You must also install the Firefox extension for this workflow to work. 83 | `), 84 | FlagSet: rootFlags, 85 | Options: []ff.Option{ 86 | ff.WithEnvVarNoPrefix(), 87 | ff.WithEnvVarIgnoreCommas(true), 88 | }, 89 | Exec: func(args []string) error { 90 | return flag.ErrHelp 91 | }, 92 | } 93 | ) 94 | 95 | func init() { 96 | rootFlags.StringVar(&URL, "url", "", "URL") 97 | rootFlags.StringVar(&urlDefault, "url-default", "Open in Default Application", 98 | "Default URL action") 99 | rootFlags.IntVar(&tabID, "tab", 0, "ID of tab") 100 | rootFlags.StringVar(&bookmarkID, "bookmark", "", "ID of bookmark") 101 | rootFlags.StringVar(&query, "query", "", "search query") 102 | rootFlags.StringVar(&action, "action", "", "action name") 103 | 104 | rootCmd.Subcommands = []*ffcli.Command{ 105 | actionsCmd, 106 | bookmarkletsCmd, 107 | bookmarksCmd, 108 | currentTabCmd, 109 | currentTabInfoCmd, 110 | downloadsCmd, 111 | historyCmd, 112 | injectCmd, 113 | openCmd, 114 | revealCmd, 115 | runBookmarkletCmd, 116 | serveCmd, 117 | statusCmd, 118 | tabCmd, 119 | tabsCmd, 120 | urlCmd, 121 | updateCmd, 122 | } 123 | pidFile = filepath.Join(wf.CacheDir(), "server.pid") 124 | logfile = filepath.Join(wf.CacheDir(), fmt.Sprintf("%s.server.log", wf.BundleID())) 125 | u, _ := user.Current() 126 | socketPath = fmt.Sprintf("/tmp/alfred-firefox.%s.sock", u.Uid) 127 | } 128 | 129 | func run() { 130 | for _, dir := range scriptDirs { 131 | if err := os.MkdirAll(dir, 0700); err != nil { 132 | panic(err) 133 | } 134 | } 135 | 136 | if err := setup(false); err != nil { 137 | panic(err) 138 | } 139 | 140 | if err := loadURLActions(); err != nil { 141 | panic(err) 142 | } 143 | 144 | if err := rootCmd.Run(wf.Args()); err != nil { 145 | panic(err) 146 | } 147 | } 148 | 149 | func main() { wf.Run(run) } 150 | 151 | // Magic Action to install native application manifest in Firefox 152 | type registerMagic struct{} 153 | 154 | func (m registerMagic) Keyword() string { return "register" } 155 | func (m registerMagic) Description() string { 156 | return "Re-register workflow with Firefox" 157 | } 158 | func (m registerMagic) RunText() string { 159 | return "Registered. Re-open Firefox extension to connect." 160 | } 161 | func (m registerMagic) Run() error { return setup(true) } 162 | 163 | var _ aw.MagicAction = registerMagic{} 164 | 165 | func setup(force bool) error { 166 | if !force && util.PathExists(manifestPath) { 167 | return nil 168 | } 169 | 170 | path, err := filepath.Abs("./server.sh") 171 | if err != nil { 172 | return err 173 | } 174 | if path, err = filepath.EvalSymlinks(path); err != nil { 175 | return err 176 | } 177 | path = filepath.Clean(path) 178 | 179 | manifest := struct { 180 | Name string `json:"name"` 181 | Description string `json:"description"` 182 | Path string `json:"path"` 183 | Type string `json:"type"` 184 | Allowed []string `json:"allowed_extensions"` 185 | }{ 186 | Name: extensionName, 187 | Description: "Alfred plugin for Firefox", 188 | Path: path, 189 | Type: "stdio", 190 | Allowed: []string{extensionID}, 191 | } 192 | 193 | data, err := json.MarshalIndent(manifest, "", " ") 194 | if err != nil { 195 | return err 196 | } 197 | 198 | util.MustExist(filepath.Dir(manifestPath)) 199 | if err := ioutil.WriteFile(manifestPath, data, 0644); err != nil { 200 | return err 201 | } 202 | log.Printf("wrote native app manifest to %q", util.PrettyPath(manifestPath)) 203 | log.Print("\n" + string(data)) 204 | return nil 205 | } 206 | 207 | var rxPara = regexp.MustCompile(`\n\n+`) 208 | 209 | func wrap(text string) string { 210 | paras := rxPara.Split(text, -1) 211 | 212 | for i, s := range paras { 213 | var b strings.Builder 214 | scanner := bufio.NewScanner(strings.NewReader(s)) 215 | for scanner.Scan() { 216 | line := strings.TrimSpace(scanner.Text()) 217 | if line == "" { 218 | continue 219 | } 220 | b.WriteString(line + " ") 221 | } 222 | paras[i] = wordwrap.WrapString(b.String(), wrapWidth) 223 | } 224 | 225 | return strings.TrimSpace(strings.Join(paras, "\n\n")) 226 | } 227 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | 2 | *.go 3 | *_test.go 4 | !mage_*.go { 5 | prep: " 6 | # run tests 7 | go test -v @dirmods 8 | " 9 | } 10 | 11 | # magefile*.go { 12 | # prep: " 13 | # # magefile 14 | # mage 15 | # " 16 | # } 17 | 18 | modd.conf 19 | *.go 20 | *.png 21 | icons/*.png 22 | !*_test.go 23 | !mage_*.go 24 | !vendor/** { 25 | prep: " 26 | # build workflow 27 | mage -v build \ 28 | && ./build/alfred-firefox -h 29 | " 30 | } 31 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | /* 13 | type Window struct { 14 | ID int `json:"id"` 15 | Title string `json:"title"` 16 | Active bool `json:"active"` 17 | Tabs []Tab `json:"tabs"` 18 | } 19 | 20 | func (w Window) String() string { 21 | return fmt.Sprintf("Window(id=%d, title=%q, active=%v)", w.ID, w.Title, w.Active) 22 | } 23 | */ 24 | 25 | // Tab represents a Firefox tab. It contains a subset of the properties 26 | // of the tab.Tab object from Firefox's extensions API. 27 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab 28 | type Tab struct { 29 | ID int `json:"id"` // unique ID of tab 30 | WindowID int `json:"windowId"` // unique ID of window tab belongs to 31 | Index int `json:"index"` // position of tab in window 32 | Title string `json:"title"` // tab's title 33 | URL string `json:"url"` // tab's URL 34 | Active bool `json:"active"` // whether tab is the active tab in its window 35 | } 36 | 37 | func (t Tab) String() string { 38 | return fmt.Sprintf("Tab(id=%d, title=%q, url=%q, active=%v)", t.ID, t.Title, t.URL, t.Active) 39 | } 40 | 41 | // Bookmark represents a Firefox bookmark. It contains a subset of the properties 42 | // of the bookmarks.BookmarkTreeNode object from the extensions API. 43 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/bookmarks/BookmarkTreeNode 44 | type Bookmark struct { 45 | ID string `json:"id"` // unique ID 46 | Title string `json:"title"` // bookmark title 47 | Type string `json:"type"` // "bookmark" or "folder" 48 | URL string `json:"url"` // only present for type "bookmark" 49 | ParentID string `json:"parentId"` // ID of folder bookmark belongs to 50 | Index int `json:"index"` // position in containing folder 51 | } 52 | 53 | func (bm Bookmark) String() string { 54 | return fmt.Sprintf("Bookmark(id=%q, title=%q, url=%q)", bm.ID, bm.Title, bm.URL) 55 | } 56 | 57 | // IsBookmarklet returns true of bookmark URL starts with "javascript:" 58 | func (bm Bookmark) IsBookmarklet() bool { 59 | return strings.HasPrefix(bm.URL, "javascript:") 60 | } 61 | 62 | // JavaScript extracts JS code from a bookmarklet's URL. Returns an empty string 63 | // if Bookmark is not a bookmarklet. 64 | func (bm Bookmark) JavaScript() string { 65 | if !bm.IsBookmarklet() { 66 | return "" 67 | } 68 | s := strings.TrimPrefix(bm.URL, "javascript:") 69 | s, _ = url.PathUnescape(s) 70 | return s 71 | } 72 | 73 | // History is an entry from the browser history. It contains a subset of the properties 74 | // of a native history.HistoryItem object. 75 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/history/HistoryItem 76 | type History struct { 77 | ID string `json:"id"` // unique ID 78 | Title string `json:"title"` // page title 79 | URL string `json:"url"` // page URL 80 | } 81 | 82 | func (h History) String() string { 83 | return fmt.Sprintf("History(id=%q, title=%q, url=%q)", h.ID, h.Title, h.URL) 84 | } 85 | 86 | // Download is a file downloaded by Firefox. Contains a subset of the properties 87 | // of a Firefox downloads.DownloadItem object. 88 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/DownloadItem 89 | type Download struct { 90 | ID int `json:"id"` // unique ID 91 | Path string `json:"path"` // absolute filepath to downloaded file 92 | Size int64 `json:"size"` // size of file in bytes 93 | URL string `json:"url"` // URL file was downloaded from 94 | MimeType string `json:"mime"` // mime type of file 95 | Exists bool `json:"exists"` // whether Path still exists on disk 96 | Err string `json:"error"` // error message 97 | } 98 | 99 | func (d Download) String() string { 100 | return fmt.Sprintf("Download(id=%q, path=%q, url=%q)", d.ID, d.Path, d.URL) 101 | } 102 | -------------------------------------------------------------------------------- /rpc_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "net/rpc" 9 | ) 10 | 11 | // RPC client used by workflow to execute extension actions. 12 | type rpcClient struct { 13 | client *rpc.Client 14 | appName string 15 | } 16 | 17 | // Create new RPC client. Returns an error if connection to server fails. 18 | func newClient() (*rpcClient, error) { 19 | c, err := rpc.Dial("unix", socketPath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | client := &rpcClient{client: c} 24 | client.appName, err = client.AppName() 25 | if err != nil { 26 | return nil, err 27 | } 28 | log.Printf("RPC client connected to %q", client.appName) 29 | return client, nil 30 | } 31 | 32 | // return new RPC client, panicking if it can't connect to server 33 | func mustClient() *rpcClient { 34 | c, err := newClient() 35 | if err != nil { 36 | log.Printf("[ERROR] %v", err) 37 | panic("Cannot Connect to Extension") 38 | } 39 | return c 40 | } 41 | 42 | // AppName returns the name of the application running the server. 43 | func (c *rpcClient) AppName() (string, error) { 44 | var s string 45 | err := c.client.Call("Firefox.AppName", "", &s) 46 | return s, err 47 | } 48 | 49 | // Ping checks connection to Firefox extension. 50 | func (c *rpcClient) Ping() error { 51 | var s string 52 | return c.client.Call("Firefox.Ping", "", &s) 53 | } 54 | 55 | // Bookmarks returns all Firefox bookmarks matching query. 56 | func (c *rpcClient) Bookmarks(query string) ([]Bookmark, error) { 57 | var bookmarks []Bookmark 58 | err := c.client.Call("Firefox.Bookmarks", query, &bookmarks) 59 | return bookmarks, err 60 | } 61 | 62 | // History searches Firefox browsing history. 63 | func (c *rpcClient) History(query string) ([]History, error) { 64 | var history []History 65 | err := c.client.Call("Firefox.History", query, &history) 66 | return history, err 67 | } 68 | 69 | // Downloads searches Firefox downloads. 70 | func (c *rpcClient) Downloads(query string) ([]Download, error) { 71 | var downloads []Download 72 | err := c.client.Call("Firefox.Downloads", query, &downloads) 73 | return downloads, err 74 | } 75 | 76 | // Tabs returns all Firefox tabs. 77 | func (c *rpcClient) Tabs() ([]Tab, error) { 78 | var tabs []Tab 79 | err := c.client.Call("Firefox.Tabs", "", &tabs) 80 | return tabs, err 81 | } 82 | 83 | // Tab returns the specified tab. If tabID is 0, returns the active tab. 84 | func (c *rpcClient) Tab(tabID int) (Tab, error) { 85 | var tab Tab 86 | err := c.client.Call("Firefox.Tab", tabID, &tab) 87 | return tab, err 88 | } 89 | 90 | /* 91 | // CurrentTab returns the currently-active tab. 92 | func (c *rpcClient) CurrentTab() (Tab, error) { 93 | var tab Tab 94 | err := c.client.Call("Firefox.CurrentTab", "", &tab) 95 | return tab, err 96 | } 97 | */ 98 | 99 | // ActivateTab brings the specified tab to the front. 100 | func (c *rpcClient) ActivateTab(tabID int) error { 101 | return c.client.Call("Firefox.ActivateTab", tabID, nil) 102 | } 103 | 104 | // CloseTabsLeft closes tabs to the left of specified tab. 105 | func (c *rpcClient) CloseTabsLeft(tabID int) error { 106 | return c.client.Call("Firefox.CloseTabsLeft", tabID, nil) 107 | } 108 | 109 | // CloseTabsRight closes tabs to the right of specified tab. 110 | func (c *rpcClient) CloseTabsRight(tabID int) error { 111 | return c.client.Call("Firefox.CloseTabsRight", tabID, nil) 112 | } 113 | 114 | // CloseTabsOther closes other tabs in same window as the specified one. 115 | func (c *rpcClient) CloseTabsOther(tabID int) error { 116 | return c.client.Call("Firefox.CloseTabsOther", tabID, nil) 117 | } 118 | 119 | // OpenIncognito opens a URL in a new Incognito window. 120 | func (c *rpcClient) OpenIncognito(URL string) error { 121 | return c.client.Call("Firefox.OpenIncognito", URL, nil) 122 | } 123 | 124 | // RunJS executes JavaScript in the specified tab. If tabID is 0, the 125 | // script is executed in the current tab. 126 | func (c *rpcClient) RunJS(arg RunJSArg) (string, error) { 127 | var s string 128 | err := c.client.Call("Firefox.RunJS", arg, &s) 129 | return s, err 130 | } 131 | 132 | // RunBookmarklet executes a given bookmarklet in a given tab. 133 | func (c *rpcClient) RunBookmarklet(arg RunBookmarkletArg) error { 134 | return c.client.Call("Firefox.RunBookmarklet", arg, nil) 135 | } 136 | -------------------------------------------------------------------------------- /rpc_server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/rpc" 12 | "os" 13 | "time" 14 | 15 | "github.com/deanishe/awgo/util" 16 | ) 17 | 18 | // rpcServer provides the RPC API. It passes commands and responses between 19 | // RPC clients and the Firefox extension. 20 | type rpcServer struct { 21 | ff *firefox // native application client run by FF extension 22 | sock string // path to UNIX socket for RPC 23 | listener net.Listener 24 | server *rpc.Server 25 | } 26 | 27 | // create new RPC server on socket specified by filepath addr 28 | func newRPCService(addr string, client *firefox) (*rpcServer, error) { 29 | var err error 30 | s := &rpcServer{ 31 | ff: client, 32 | sock: addr, 33 | server: rpc.NewServer(), 34 | } 35 | 36 | if err = s.server.RegisterName("Firefox", s); err != nil { 37 | return nil, err 38 | } 39 | 40 | if s.listener, err = net.Listen("unix", s.sock); err != nil { 41 | return nil, err 42 | } 43 | 44 | if err = os.Chmod(addr, 0600); err != nil { 45 | return nil, err 46 | } 47 | 48 | return s, nil 49 | } 50 | 51 | // AppName returns name of browser running the server. 52 | func (s *rpcServer) AppName(_ string, name *string) error { 53 | *name = browserName 54 | return nil 55 | } 56 | 57 | // Ping checks connection to Firefox extension. Extension responds with "pong". 58 | func (s *rpcServer) Ping(_ string, result *string) error { 59 | defer util.Timed(time.Now(), "ping") 60 | var r responseString 61 | if err := s.ff.call("ping", nil, &r); err != nil { 62 | return err 63 | } 64 | if r.Error != "" { 65 | return errors.New(r.Error) 66 | } 67 | *result = r.String 68 | return nil 69 | } 70 | 71 | // func (s *rpcServer) Windows(_ string, windows *[]Window) error { 72 | // defer util.Timed(time.Now(), "get windows") 73 | // var r responseWindows 74 | // if err := s.ff.call("all-windows", nil, &r); err != nil { 75 | // return err 76 | // } 77 | // *windows = r.Windows 78 | // return nil 79 | // } 80 | 81 | // Tabs returns all Firefox tabs. 82 | func (s *rpcServer) Tabs(_ string, tabs *[]Tab) error { 83 | defer util.Timed(time.Now(), "get tabs") 84 | var r responseTabs 85 | if err := s.ff.call("all-tabs", nil, &r); err != nil { 86 | return err 87 | } 88 | if r.Error != "" { 89 | return errors.New(r.Error) 90 | } 91 | *tabs = r.Tabs 92 | return nil 93 | } 94 | 95 | // ActivateTab brings the specified tab to the front. 96 | func (s *rpcServer) ActivateTab(tabID int, _ *struct{}) error { 97 | defer util.Timed(time.Now(), "activate tab") 98 | var r responseNone 99 | if err := s.ff.call("activate-tab", tabID, &r); err != nil { 100 | return err 101 | } 102 | if r.Error != "" { 103 | return errors.New(r.Error) 104 | } 105 | return nil 106 | } 107 | 108 | // Tab returns the specified tab. If tabID is 0, returns the active tab. 109 | func (s *rpcServer) Tab(tabID int, tab *Tab) error { 110 | defer util.Timed(time.Now(), "get tab") 111 | var r responseTab 112 | if err := s.ff.call("tab", tabID, &r); err != nil { 113 | return err 114 | } 115 | if r.Error != "" { 116 | return errors.New(r.Error) 117 | } 118 | *tab = r.Tab 119 | return nil 120 | } 121 | 122 | /* 123 | // CurrentTab returns the currently-active tab. 124 | func (s *rpcServer) CurrentTab(_ string, tab *Tab) error { 125 | defer util.Timed(time.Now(), "get current tab") 126 | var r responseTab 127 | if err := s.ff.call("tab", 0, &r); err != nil { 128 | return err 129 | } 130 | if r.Error != "" { 131 | return errors.New(r.Error) 132 | } 133 | *tab = r.Tab 134 | return nil 135 | } 136 | */ 137 | 138 | // CloseTabsLeft closes tabs to the left of specified tab. 139 | func (s *rpcServer) CloseTabsLeft(tabID int, _ *struct{}) error { 140 | defer util.Timed(time.Now(), "close tabs to left") 141 | var r responseNone 142 | if err := s.ff.call("close-tabs-left", tabID, &r); err != nil { 143 | return err 144 | } 145 | if r.Error != "" { 146 | return errors.New(r.Error) 147 | } 148 | return nil 149 | } 150 | 151 | // CloseTabsRight closes tabs to the right of specified tab. 152 | func (s *rpcServer) CloseTabsRight(tabID int, _ *struct{}) error { 153 | defer util.Timed(time.Now(), "close tabs to right") 154 | var r responseNone 155 | if err := s.ff.call("close-tabs-right", tabID, &r); err != nil { 156 | return err 157 | } 158 | if r.Error != "" { 159 | return errors.New(r.Error) 160 | } 161 | return nil 162 | } 163 | 164 | // CloseTabsOther closes other tabs in same window as the specified one. 165 | func (s *rpcServer) CloseTabsOther(tabID int, _ *struct{}) error { 166 | defer util.Timed(time.Now(), "close other tabs") 167 | var r responseNone 168 | if err := s.ff.call("close-tabs-other", tabID, &r); err != nil { 169 | return err 170 | } 171 | if r.Error != "" { 172 | return errors.New(r.Error) 173 | } 174 | return nil 175 | } 176 | 177 | // Bookmarks returns all Firefox bookmarks matching query. 178 | func (s *rpcServer) Bookmarks(query string, bookmarks *[]Bookmark) error { 179 | defer util.Timed(time.Now(), fmt.Sprintf("search bookmarks for %q", query)) 180 | var ( 181 | r responseBookmarks 182 | err error 183 | ) 184 | if query == "" { 185 | err = s.ff.call("all-bookmarks", nil, &r) 186 | } else { 187 | err = s.ff.call("search-bookmarks", query, &r) 188 | } 189 | if err != nil { 190 | return err 191 | } 192 | if r.Error != "" { 193 | return errors.New(r.Error) 194 | } 195 | *bookmarks = r.Bookmarks 196 | return nil 197 | } 198 | 199 | // History searches Firefox browsing history. 200 | func (s *rpcServer) History(query string, history *[]History) error { 201 | defer util.Timed(time.Now(), fmt.Sprintf("search history for %q", query)) 202 | var ( 203 | r responseHistory 204 | err error 205 | ) 206 | err = s.ff.call("search-history", query, &r) 207 | if err != nil { 208 | return err 209 | } 210 | if r.Error != "" { 211 | return errors.New(r.Error) 212 | } 213 | *history = r.Entries 214 | return nil 215 | } 216 | 217 | // Downloads searches Firefox downloads. 218 | func (s *rpcServer) Downloads(query string, downloads *[]Download) error { 219 | defer util.Timed(time.Now(), fmt.Sprintf("search download for %q", query)) 220 | var ( 221 | r responseDownload 222 | err error 223 | ) 224 | err = s.ff.call("search-downloads", query, &r) 225 | if err != nil { 226 | return err 227 | } 228 | if r.Error != "" { 229 | return errors.New(r.Error) 230 | } 231 | *downloads = r.Downloads 232 | return nil 233 | } 234 | 235 | // OpenIncognito opens URL in a new incognito window. 236 | func (s *rpcServer) OpenIncognito(URL string, _ *struct{}) error { 237 | defer util.Timed(time.Now(), "open incognito") 238 | var r responseNone 239 | if err := s.ff.call("open-incognito", URL, &r); err != nil { 240 | return err 241 | } 242 | if r.Error != "" { 243 | return errors.New(r.Error) 244 | } 245 | return nil 246 | } 247 | 248 | // RunJSArg is the arguments required for RunJS call. TabID may be 0, in which 249 | // case the JavaScript is executed in the active tab. 250 | type RunJSArg struct { 251 | TabID int `json:"tabId"` 252 | JS string `json:"js"` 253 | } 254 | 255 | // RunJS executes JavaScript in the specified tab. If tabID is 0, the 256 | // script is executed in the current tab. 257 | func (s *rpcServer) RunJS(arg RunJSArg, JSON *string) error { 258 | defer util.Timed(time.Now(), "execute JS") 259 | var r responseString 260 | if err := s.ff.call("execute-js", arg, &r); err != nil { 261 | return err 262 | } 263 | if r.Error != "" { 264 | return errors.New(r.Error) 265 | } 266 | *JSON = r.String 267 | return nil 268 | } 269 | 270 | // RunBookmarkletArg is the arguments required for RunBookmarklet call. 271 | // TabID may be 0, in which case the bookmarklet is executed in the active tab. 272 | type RunBookmarkletArg struct { 273 | TabID int `json:"tabId"` 274 | BookmarkID string `json:"bookmarkId"` 275 | } 276 | 277 | // RunBookmarklet executes a given bookmarklet in a given tab. 278 | func (s *rpcServer) RunBookmarklet(arg RunBookmarkletArg, _ *struct{}) error { 279 | defer util.Timed(time.Now(), "run bookmarklet") 280 | var r responseNone 281 | if err := s.ff.call("run-bookmarklet", arg, &r); err != nil { 282 | return err 283 | } 284 | if r.Error != "" { 285 | return errors.New(r.Error) 286 | } 287 | return nil 288 | } 289 | 290 | func (s *rpcServer) run() { 291 | log.Printf("serving RPC on %q ...", s.sock) 292 | s.server.Accept(s.listener) 293 | } 294 | 295 | func (s *rpcServer) stop() error { 296 | return s.listener.Close() 297 | } 298 | 299 | type responseString struct { 300 | String string `json:"payload"` 301 | Error string `json:"error"` 302 | } 303 | 304 | // type responseWindows struct { 305 | // Windows []Window `json:"payload"` 306 | // } 307 | 308 | type responseTabs struct { 309 | Tabs []Tab `json:"payload"` 310 | Error string `json:"error"` 311 | } 312 | 313 | type responseTab struct { 314 | Tab Tab `json:"payload"` 315 | Error string `json:"error"` 316 | } 317 | 318 | type responseHistory struct { 319 | Entries []History `json:"payload"` 320 | Error string `json:"error"` 321 | } 322 | 323 | type responseTabCurrent struct { 324 | Tab Tab `json:"payload"` 325 | Error string `json:"error"` 326 | } 327 | 328 | type responseBookmarks struct { 329 | Bookmarks []Bookmark `json:"payload"` 330 | Error string `json:"error"` 331 | } 332 | 333 | type responseDownload struct { 334 | Downloads []Download `json:"payload"` 335 | Error string `json:"error"` 336 | } 337 | 338 | type responseBool struct { 339 | OK bool `json:"payload"` 340 | Error string `json:"error"` 341 | } 342 | 343 | type responseNone struct { 344 | Error string `json:"error"` 345 | } 346 | -------------------------------------------------------------------------------- /screenshots/bookmark-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/screenshots/bookmark-search.png -------------------------------------------------------------------------------- /screenshots/history-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/screenshots/history-search.png -------------------------------------------------------------------------------- /screenshots/tab-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/screenshots/tab-actions.png -------------------------------------------------------------------------------- /scripts/Open in Chrome.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/scripts/Open in Chrome.icns -------------------------------------------------------------------------------- /scripts/Open in Chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | /usr/bin/open -a 'Google Chrome' "$1" 4 | 5 | -------------------------------------------------------------------------------- /scripts/Open in Default Application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/scripts/Open in Default Application.png -------------------------------------------------------------------------------- /scripts/Open in Default Application.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | /usr/bin/open "$1" 4 | 5 | -------------------------------------------------------------------------------- /scripts/Open in Firefox.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/scripts/Open in Firefox.icns -------------------------------------------------------------------------------- /scripts/Open in Firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | BROWSER="${BROWSER:-Firefox}" 4 | 5 | /usr/bin/open -a "$BROWSER" "$1" 6 | -------------------------------------------------------------------------------- /scripts/Open in Safari.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-firefox/79d09e376525ee2db013c3a9c73d1d9eac57da09/scripts/Open in Safari.icns -------------------------------------------------------------------------------- /scripts/Open in Safari.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | /usr/bin/open -a 'Safari' "$1" 4 | 5 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | 2 | Scripts 3 | ======= 4 | 5 | The scripts in this folder are (most of) the built-in URL actions. 6 | 7 | Don't add your own scripts to this directory: they'll be deleted when you update the workflow. Instead, add your custom scripts to the `scripts` subdirectory of the workflow's data directory, which you can quickly access via the `ffass` keyword and the `Open Scripts Directory` item. 8 | 9 | See [the documentation][docs] for details on adding and customising scripts. 10 | 11 | [docs]: ../doc/scripts.md 12 | 13 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "regexp" 15 | "strconv" 16 | "syscall" 17 | "time" 18 | 19 | aw "github.com/deanishe/awgo" 20 | "github.com/deanishe/awgo/util" 21 | "github.com/peterbourgon/ff/ffcli" 22 | ) 23 | 24 | var ( 25 | // starts extension client & RPC server 26 | serveCmd = &ffcli.Command{ 27 | Name: "serve", 28 | Usage: "alfred-firefox serve", 29 | ShortHelp: "start extension server (called by Firefox)", 30 | LongHelp: wrap(` 31 | Run extension server. This is called by the Firefox 32 | extension and provides and RPC server for the workflow 33 | to call into Firefox. 34 | `), 35 | Exec: runServer, 36 | } 37 | browserName string 38 | ) 39 | 40 | // set up logging for the server. 41 | // doesn't use the same log as the rest of the workflow, as this is 42 | // a long-running process, and we don't want the log file it's using 43 | // being rotated by another process 44 | func initLogging() error { 45 | if fi, err := os.Stat(logfile); err == nil { 46 | if fi.Size() > int64(1048576) { 47 | _ = os.Rename(logfile, logfile+".1") 48 | } 49 | } 50 | file, err := os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) 51 | if err != nil { 52 | return err 53 | } 54 | multi := io.MultiWriter(file, os.Stderr) 55 | log.SetOutput(multi) 56 | log.SetFlags(log.Ltime | log.Lshortfile) 57 | log.SetPrefix(fmt.Sprintf("[%d] ", os.Getpid())) 58 | return nil 59 | } 60 | 61 | // return PID of running server or 0. 62 | func getPID() int { 63 | if data, err := ioutil.ReadFile(pidFile); err == nil { 64 | if pid, err := strconv.Atoi(string(data)); err == nil { 65 | return pid 66 | } 67 | } 68 | return 0 69 | } 70 | 71 | // return true if process with PID is running. 72 | func processRunning(pid int) bool { 73 | if err := syscall.Kill(pid, 0); err == nil { 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | // write PID file, terminating and waiting for existing server if it exists. 80 | func writePID() error { 81 | pid := getPID() 82 | if pid != 0 { 83 | log.Printf("signalling existing server %d to stop ...", pid) 84 | _ = syscall.Kill(pid, syscall.SIGTERM) 85 | 86 | start := time.Now() 87 | for processRunning(pid) { 88 | if time.Now().Sub(start) > time.Second*2 { 89 | return fmt.Errorf("server already running") 90 | } 91 | time.Sleep(time.Millisecond * 100) 92 | } 93 | } 94 | 95 | return ioutil.WriteFile(pidFile, []byte(strconv.FormatInt(int64(os.Getpid()), 10)), 0600) 96 | } 97 | 98 | func getBrowserName() (name string) { 99 | name = "Firefox" 100 | var ( 101 | data []byte 102 | err error 103 | ) 104 | ppid := os.Getppid() 105 | cmd := exec.Command("lsappinfo", "info", "-only", "name", fmt.Sprintf("#%d", ppid)) 106 | if data, err = util.RunCmd(cmd); err != nil { 107 | log.Printf("[ERROR] couldn't get app info for pid %d: %s", ppid, err) 108 | return 109 | } 110 | m := regexp.MustCompile(`"LSDisplayName"="(.+)"`).FindSubmatch(data) 111 | if m == nil { 112 | log.Printf("couldn't parse app name from %q", data) 113 | return 114 | } 115 | name = string(m[1]) 116 | return 117 | } 118 | 119 | // start extension client and RPC server 120 | func runServer(_ []string) error { 121 | wf.Configure(aw.TextErrors(true)) 122 | if err := writePID(); err != nil { 123 | return err 124 | } 125 | if err := initLogging(); err != nil { 126 | return err 127 | } 128 | browserName = getBrowserName() 129 | log.Printf("browser=%q", browserName) 130 | defer func() { 131 | _ = os.Remove(socketPath) 132 | _ = os.Remove(pidFile) 133 | }() 134 | 135 | quit := make(chan os.Signal, 1) 136 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 137 | 138 | f := newFirefox() 139 | go f.run() 140 | 141 | srv, err := newRPCService(socketPath, f) 142 | if err != nil { 143 | return err 144 | } 145 | go srv.run() 146 | 147 | var s string 148 | if err := srv.Ping("", &s); err != nil { 149 | log.Printf("[ERROR] %v", err) 150 | } else { 151 | log.Printf("ping => %q", s) 152 | } 153 | 154 | /* 155 | var bookmarks []Bookmark 156 | for _, q := range []string{"", "haze", "github", "p2p"} { 157 | if err := srv.Bookmarks(q, &bookmarks); err != nil { 158 | log.Printf("[ERROR] %v", err) 159 | } else { 160 | log.Printf("bookmarks(%q) => %d result(s)", q, len(bookmarks)) 161 | } 162 | } 163 | 164 | var tabs []Tab 165 | if err := srv.Tabs("", &tabs); err != nil { 166 | log.Printf("[ERROR] %v", err) 167 | } else { 168 | log.Printf("tabs => %d result(s)", len(tabs)) 169 | } 170 | */ 171 | 172 | <-quit 173 | log.Print("shutting down ...") 174 | f.stop() 175 | srv.stop() 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # This script is a wrapper for the Firefox extension client/RPC server 4 | # to set an Alfred-like environment when it is run by Firefox. 5 | 6 | here="${${(%):-%x}:A:h}" 7 | 8 | # getvar | Read a value from info.plist 9 | getvar() { 10 | local v="$1" 11 | /usr/libexec/PlistBuddy -c "Print :$v" "${here}/info.plist" 12 | } 13 | 14 | export alfred_workflow_bundleid=$( getvar "bundleid" ) 15 | export alfred_workflow_version=$( getvar "version" ) 16 | export alfred_workflow_name=$( getvar "name" ) 17 | export alfred_debug=1 18 | 19 | export alfred_workflow_cache="${HOME}/Library/Caches/com.runningwithcrayons.Alfred/Workflow Data/${alfred_workflow_bundleid}" 20 | export alfred_workflow_data="${HOME}/Library/Application Support/Alfred/Workflow Data/${alfred_workflow_bundleid}" 21 | 22 | mkdir -p "${alfred_workflow_data}" 23 | mkdir -p "${alfred_workflow_cache}" 24 | 25 | exec "${here}/alfred-firefox" serve 26 | 27 | --------------------------------------------------------------------------------