├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── action.js ├── filter.js ├── icon.png └── info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | tty.alfredworkflow 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robin Breathe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | zip -j9 --filesync tty.alfredworkflow *.{plist,png,js} 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ^tty workflow for Alfred 2 | 3 | A JXA-based workflow for [Alfred](http://www.alfredapp.com/) Powerpack users to quickly switch between or close iTerm windows, tabs and panes based on title and tty, or trigger your preferred ssh workflow when no open session is found (supports both [isometry/alfred-ssh](https://github.com/isometry/alfred-ssh) and [deanishe/alfred-ssh](https://github.com/deanishe/alfred-ssh)). 4 | 5 | ## Releases 6 | - [Latest for Alfred 3.x/4.x](https://github.com/isometry/alfred-tty/releases/latest) 7 | 8 | ## Requirements 9 | 10 | - [Alfred](http://www.alfredapp.com/) (version 3.0+) 11 | - [Alfred Powerpack](http://www.alfredapp.com/powerpack/) 12 | - [iTerm2](https://www.iterm2.com/) (version 3.1+) 13 | - macOS Sierra or newer (strictly, OS X 10.10+, but untested on <10.12) 14 | 15 | ## Usage 16 | 17 | Trigger the workflow with the keyword `tty`, or via hotkey, followed by some characters from the title of an open window, tab or pane; press `Enter` to activate the selected window/tab/pane, `Alt-Enter` to close it, or `Cmd-Enter` to trigger your ssh workflow. For example, enter `tty as3` to switch to a tab with the title `user@azure-server-03`. 18 | 19 | If no active terminal matches, or you use the `Cmd`-modifier, trigger your preferred ssh workflow, e.g. `⇄ ssh as3`. 20 | By default, the ssh workflow is assumed to be `net.isometry.alfred.ssh` (i.e. [isometry/alfred-ssh](https://github.com/isometry/alfred-ssh), version 2.3+). Override by setting the `ssh_workflow` and `ssh_trigger` variables; for [deanishe/alfred-ssh](https://github.com/deanishe/alfred-ssh), set the `ssh_workflow` variable to `net.deanishe.alfred-ssh`. 21 | 22 | In order to make working with more than one window/tab/pane with the same title easier, the tty is displayed beneath the result, and can be provided as a second argument to the trigger. For example, enter `tty lo 3` to select the the session with title `localhost` running on `/dev/ttys003`. 23 | 24 | To select by tty alone, use two spaces between the trigger and the tty number. For example, `tty 4` will select `/dev/ttys004`. 25 | 26 | Optionally associate a hotkey trigger to further accelerate operation, e.g. `Ctrl+Cmd+T`, or change the trigger word via the `keyword` variable. 27 | 28 | Combine with an [iTerm2](https://www.iterm2.com/) profile configured as ssh protocol handler (e.g. "Name":=`$$USER$$@$$HOST$$`, "Command":=`$$` and "Schemes handled":=`ssh`) and an [alfred-ssh](https://github.com/isometry/alfred-ssh) workflow to make opening and jumping between remote sessions across many windows, tabs and panes easy. 29 | 30 | ### Workflow Variables 31 | 32 | `keyword` – the keyword trigger for the workflow; default: `tty`. 33 | 34 | `ssh_workflow` – the workflow bundle identifier for your preferred alfred-ssh workflow; default: `net.isometry.alfred.ssh`. 35 | 36 | `ssh_trigger` – the name of the trigger within `ssh_workflow`; default: `ssh`. 37 | 38 | `iterm_application` – the application identifier of the iTerm2 instance you want to control, either bundleId, application name or absolute path; default: `com.googlecode.iterm2`. 39 | -------------------------------------------------------------------------------- /action.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | function getenv(name, default_value) { 4 | var env = $.NSProcessInfo.processInfo.environment.js; 5 | return (name in env) ? env[name].js : default_value; 6 | } 7 | 8 | function run(args) { 9 | args = args.join(" ").split(" "); 10 | var app, action = String(args[0]); 11 | if (action == "ssh") { 12 | var host = String(args[1] || ""); 13 | app = Application("com.runningwithcrayons.Alfred"); 14 | app.runTrigger(getenv("ssh_trigger", "ssh"), { 15 | inWorkflow: getenv("ssh_workflow", "net.isometry.alfred.ssh"), 16 | withArgument: host 17 | }); 18 | } else { 19 | app = Application(getenv("iterm_application", "com.googlecode.iterm2")); 20 | app.strictPropertyScope = true; 21 | app.strictCommandScope = true; 22 | app.strictParameterType = true; 23 | var win = app.windows.byId(Number(args[1])); 24 | var tab = win.tabs[Number(args[2])]; 25 | var ses = tab.sessions.byId(String(args[3])); 26 | switch (action) { 27 | case "select": 28 | // tab => session => window => app 29 | tab.select(); 30 | ses.select(); 31 | win.select(); 32 | app.activate(); 33 | break; 34 | case "close": 35 | ses.close(); 36 | break; 37 | case "debug": 38 | console.log(JSON.stringify({ args: args, title: ses.name() })); 39 | break; 40 | default: 41 | console.log("Invalid arguments:", JSON.stringify(args)); 42 | break; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /filter.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | function getenv(name, default_value) { 4 | var env = $.NSProcessInfo.processInfo.environment.js; 5 | return (name in env) ? env[name].js : default_value; 6 | } 7 | 8 | function sessionToObj(windowId, tabIndex, query) { 9 | return (session) => ({ 10 | id: [windowId, tabIndex, session.id()].join(" "), 11 | title: session.name(), 12 | tty: session.tty(), 13 | output: session.isProcessing(), 14 | profile: session.profileName(), 15 | query: query 16 | }); 17 | } 18 | 19 | function objToItem(obj) { 20 | var description = obj.tty + (obj.output?" *":""); 21 | return { 22 | title: obj.title, 23 | subtitle: ["Select", description].join(" "), 24 | arg: ["select", obj.id].join(" "), 25 | uid: [obj.title, obj.profile].join("/"), 26 | icon: { path: "icon.png" }, 27 | mods: { 28 | alt: { 29 | arg: ["close", obj.id].join(" "), 30 | subtitle: ["Close", description].join(" ") 31 | }, 32 | cmd: { 33 | arg: ["ssh", obj.query].join(" "), 34 | title: ["⇌ ssh", obj.query].join(" "), 35 | subtitle: "Switch to ssh workflow" 36 | } 37 | } 38 | }; 39 | } 40 | 41 | function allSessionObjs(app, query) { 42 | var windows = app.windows; 43 | var results = new Array(); 44 | var windowIds = windows.id(); 45 | for (var i=0; i < windowIds.length; ++i) { 46 | var windowId = windowIds[i]; 47 | var tabs = windows.byId(windowId).tabs; 48 | for (var tabIndex=0; tabIndex < tabs.length; ++tabIndex) { 49 | results.push( 50 | ...tabs[tabIndex].sessions().map( 51 | sessionToObj(windowId, tabIndex, query) 52 | ) 53 | ); 54 | } 55 | } 56 | return results; 57 | } 58 | 59 | function titleFilter(pattern) { 60 | var re = new RegExp(pattern.replace(/./g, "$&.*?"), "i"); 61 | return (obj) => re.exec(obj.title); 62 | } 63 | 64 | function ttyFilter(ttyNum) { 65 | if (ttyNum.length == 0) { 66 | return () => true; 67 | } else { 68 | var re = new RegExp(ttyNum.padStart(3, "0") + "$"); 69 | return (obj) => re.exec(obj.tty); 70 | } 71 | } 72 | 73 | function run(args) { 74 | args = args.join(" ").split(" "); 75 | var query = String(args[0] || ""); 76 | var tty = String(args[1] || ""); 77 | var sessionObjs = new Array(); 78 | var app = Application(getenv("iterm_application", "com.googlecode.iterm2")); 79 | app.strictPropertyScope = true; 80 | app.strictCommandScope = true; 81 | app.strictParameterType = true; 82 | if (app.running()) { 83 | sessionObjs = allSessionObjs(app, query) 84 | .filter(titleFilter(query)) 85 | .filter(ttyFilter(tty)); 86 | } 87 | if (sessionObjs.length == 0) { 88 | return JSON.stringify({ 89 | items: [ 90 | { 91 | title: ["⇌ ssh", query].join(" "), 92 | subtitle: "No matches! Switch to ssh workflow?", 93 | arg: ["ssh", query].join(" "), 94 | icon: { path: "icon.png" }, 95 | } 96 | ] 97 | }); 98 | } else { 99 | return JSON.stringify({ 100 | items: sessionObjs.map(objToItem) 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isometry/alfred-tty/08ba1a5ae4a74aad281fbc1c4f7ded0323862844/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | net.isometry.alfred.tty 7 | category 8 | Tools 9 | connections 10 | 11 | 45E888C5-D745-4FEC-B0CC-80C4A33371F8 12 | 13 | 14 | destinationuid 15 | C9298943-AE22-4585-8C3B-4A3E72EEAAA4 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | C9298943-AE22-4585-8C3B-4A3E72EEAAA4 25 | 26 | 27 | destinationuid 28 | 83C7D354-7702-4395-AA13-7F6F3E1EC22F 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 38 | createdby 39 | Robin Breathe 40 | description 41 | Select or close windows, tabs and panes in iTerm2 42 | disabled 43 | 44 | name 45 | ^tty 46 | objects 47 | 48 | 49 | config 50 | 51 | alfredfiltersresults 52 | 53 | alfredfiltersresultsmatchmode 54 | 0 55 | argumenttreatemptyqueryasnil 56 | 57 | argumenttrimmode 58 | 1 59 | argumenttype 60 | 1 61 | escaping 62 | 0 63 | keyword 64 | {var:keyword} 65 | queuedelaycustom 66 | 3 67 | queuedelayimmediatelyinitially 68 | 69 | queuedelaymode 70 | 0 71 | queuemode 72 | 2 73 | runningsubtext 74 | Please wait… 75 | script 76 | 77 | scriptargtype 78 | 1 79 | scriptfile 80 | filter.js 81 | subtext 82 | 83 | title 84 | Select or close an iTerm window, tab or pane 85 | type 86 | 8 87 | withspace 88 | 89 | 90 | type 91 | alfred.workflow.input.scriptfilter 92 | uid 93 | C9298943-AE22-4585-8C3B-4A3E72EEAAA4 94 | version 95 | 3 96 | 97 | 98 | config 99 | 100 | action 101 | 0 102 | argument 103 | 0 104 | focusedappvariable 105 | 106 | focusedappvariablename 107 | 108 | hotkey 109 | 40 110 | hotmod 111 | 1310720 112 | hotstring 113 | T 114 | leftcursor 115 | 116 | modsmode 117 | 0 118 | relatedAppsMode 119 | 0 120 | 121 | type 122 | alfred.workflow.trigger.hotkey 123 | uid 124 | 45E888C5-D745-4FEC-B0CC-80C4A33371F8 125 | version 126 | 2 127 | 128 | 129 | config 130 | 131 | concurrently 132 | 133 | escaping 134 | 0 135 | script 136 | 137 | scriptargtype 138 | 1 139 | scriptfile 140 | action.js 141 | type 142 | 8 143 | 144 | type 145 | alfred.workflow.action.script 146 | uid 147 | 83C7D354-7702-4395-AA13-7F6F3E1EC22F 148 | version 149 | 2 150 | 151 | 152 | readme 153 | Trigger the workflow with the keyword `tty`, or via hotkey, followed by some characters from the title of an open window, tab or pane; press `Enter` to activate the selected window/tab/pane, `Alt-Enter` to close it, or `Cmd-Enter` to trigger your ssh workflow. For example, enter `tty as3` to switch to a tab with the title `user@azure-server-03`. 154 | 155 | If no active terminal matches, or you use the `Cmd`-modifier, trigger your preferred ssh workflow, e.g. `⇄ ssh as3`. 156 | By default, the ssh workflow is assumed to be `net.isometry.alfred.ssh` (i.e. [isometry/alfred-ssh](https://github.com/isometry/alfred-ssh), version 2.3+). Override by setting the `ssh_workflow` and `ssh_trigger` variables; for [deanishe/alfred-ssh](https://github.com/deanishe/alfred-ssh), set the `ssh_workflow` variable to `net.deanishe.alfred-ssh`. 157 | 158 | In order to make working with more than one window/tab/pane with the same title easier, the tty is displayed beneath the result, and can be provided as a second argument to the trigger. For example, enter `tty lo 3` to select the the session with title `localhost` running on `/dev/ttys003`. 159 | 160 | To select by tty alone, use two spaces between the trigger and the tty number. For example, `tty 4` will select `/dev/ttys004`. 161 | 162 | Optionally associate a hotkey trigger to further accelerate operation, e.g. `Ctrl+Cmd+T`, or change the trigger word via the `keyword` variable. 163 | 164 | Combine with an [iTerm2](https://www.iterm2.com/) profile configured as ssh protocol handler (e.g. "Name":=`$$USER$$@$$HOST$$`, "Command":=`$$` and "Schemes handled":=`ssh`) and an [alfred-ssh](https://github.com/isometry/alfred-ssh) workflow to make opening and jumping between remote sessions across many windows, tabs and panes easy. 165 | 166 | ### Workflow Variables 167 | 168 | `keyword` – the keyword trigger for the workflow; default: `tty`. 169 | 170 | `ssh_workflow` – the workflow bundle identifier for your preferred alfred-ssh workflow; default: `net.isometry.alfred.ssh`. 171 | 172 | `ssh_trigger` – the name of the trigger within `ssh_workflow`; default: `ssh`. 173 | 174 | `iterm_application` – the application identifier of the iTerm2 instance you want to control, either bundleId, application name or absolute path; default: `com.googlecode.iterm2`. 175 | uidata 176 | 177 | 45E888C5-D745-4FEC-B0CC-80C4A33371F8 178 | 179 | xpos 180 | 70 181 | ypos 182 | 70 183 | 184 | 83C7D354-7702-4395-AA13-7F6F3E1EC22F 185 | 186 | note 187 | action 188 | xpos 189 | 430 190 | ypos 191 | 70 192 | 193 | C9298943-AE22-4585-8C3B-4A3E72EEAAA4 194 | 195 | note 196 | filter 197 | xpos 198 | 250 199 | ypos 200 | 70 201 | 202 | 203 | variables 204 | 205 | iterm_application 206 | com.googlecode.iterm2 207 | keyword 208 | tty 209 | ssh_trigger 210 | ssh 211 | ssh_workflow 212 | net.isometry.alfred.ssh 213 | 214 | version 215 | 1.5 216 | webaddress 217 | https://github.com/isometry/alfred-tty 218 | 219 | 220 | --------------------------------------------------------------------------------