├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── terminal-plus.cson ├── lib ├── config-schema.coffee ├── core.coffee ├── dialog.coffee ├── fork.coffee ├── input-dialog.coffee ├── panel-view.coffee ├── rename-dialog.coffee ├── shell.coffee ├── status-bar.coffee ├── status-icon.coffee ├── tab-view.coffee ├── terminal-plus.coffee ├── terminal-view.coffee └── terminal.coffee ├── menus └── terminal-plus.cson ├── package.json ├── resources ├── demo copy.gif ├── demo.gif ├── full_screen_demo.png ├── insert_selected_text.gif ├── insert_text.png ├── map_terminals_to_auto_open.gif ├── map_terminals_to_file.gif ├── map_terminals_to_folder.gif ├── plus-icon.png ├── red-x.png ├── sorting.gif ├── special_keys.gif ├── status-bar.png ├── status-icon.png ├── status-icon_color_coding.png ├── status-icon_rename-dialog.png ├── status-icon_rename.png ├── terminal_title.png └── terminal_with_status-bar.png ├── spec ├── terminal-plus-spec.coffee └── terminal-plus-view-spec.coffee └── styles ├── terminal-plus.less ├── themes.less └── themes ├── dracula.less ├── grass.less ├── homebrew.less ├── inverse.less ├── man-page.less ├── novel.less ├── ocean.less ├── pro.less ├── red-sands.less ├── red.less ├── silver-aerogel.less ├── solid-colors.less └── standard.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.14.5 - Patch 2 | * Fix key-presses deselecting lines from the terminal 3 | * Add alt-(arrow) and alt-(click) for moving cursor in terminal 4 | * Improve `clear` command handling 5 | * Make sure initial height is to the nearest row 6 | 7 | ## v0.14.4 - Patch 8 | * Fix `reset` and `clear` command issues 9 | * Improve `cmd-k` clear command 10 | * Insert text as a command by default 11 | * Match text editor font family for consistency 12 | * Add font size setting back 13 | * Destroy panel on terminal destroy 14 | * Allow any css value for default panel height 15 | 16 | ## v0.14.3 - Patch 17 | * Remove extra logs 18 | * Bug fixes 19 | * Match Atom's font size 20 | 21 | ## v0.14.2 - Patch 22 | * Bring back default selection process 23 | * Improvements to terminal (jeremyramin/term.js) 24 | * Fix several bugs 25 | * Clean up menus 26 | 27 | ## v0.14.1 - Patch 28 | * Recompile binaries for Atom 1.2.0 29 | * Fix focus error 30 | * Return focus to terminal on window focus 31 | * Minor fixes to text area and terminal resizing 32 | 33 | ## v0.14.0 - Beta Release 34 | * Added support for non-English characters 35 | * Thanks to @yoshiokatsuneo for [his work](https://github.com/jeremyramin/term.js/commit/e851ea232a114902ea6a8e5cc8f7d34d07969c42). 36 | * Dead keys now work in the terminal 37 | * CJK (IME) should now work in the terminal 38 | * Keep terminals maximized on new terminal view 39 | * Improve terminal title handling 40 | * Fix fullscreen not focusing 41 | * Refactor dialog classes 42 | * Update Windows binaries 43 | 44 | ## v0.13.1 - Patch 45 | * Only run inserted text if `Run Inserted Text` is enabled 46 | * Improve terminal title handling 47 | * Make sure the terminal title does not stay behind on new title 48 | * Open the correct project folder for the active file 49 | * Post-install clean-up script for pty.js (see issue #71) 50 | 51 | ## v0.13.0 - Beta Release 52 | * Add support for alt+key combinations 53 | * Right alt for escape sequences 54 | * Fix terminal mapping toggle error when `Auto Open a New Terminal` is false 55 | * Improve cursor: 56 | * Preserve the color of the character the cursor is over 57 | * Fix terminal cursor background-color being overwritten 58 | * Only intercept ctrl+key combinations if ctrl is the only key being pressed 59 | 60 | ## v0.12.5 - Patch 61 | * Fix `ctrl` key intercepting to ignore `ctrl-shift` combinations 62 | * Fix `ctrl-s` pausing terminal on Linux systems 63 | 64 | ## v0.12.4 - Patch 65 | * Map terminals while the active terminal is hidden 66 | * Fix prev/next terminal switch 67 | * Improve cursor blink animation 68 | * New commands: 69 | * terminal-plus:close-all for closing all terminals 70 | * terminal-plus:rename for renaming the active terminal 71 | 72 | ## v0.12.3 - Patch 73 | * Blur terminal window on window blur 74 | * Adjust default ANSI colors for terminal 75 | * Confirm that the active terminal is not null 76 | 77 | ## v0.12.2 - Patch 78 | * Prevent terminal from intercepting alt+(key) events 79 | * Fixes broken copy and paste on Linux and Windows 80 | 81 | ## v0.12.1 - Patch 82 | * Make sure status icon tooltip dismisses when the status icon is detached 83 | * Fix copy and pasting bug with tabs 84 | * Improve active terminal system 85 | * Fix terminal resizing removing lines 86 | 87 | ## v0.12.0 - Beta Release 88 | * Clean up tooltip 89 | * Prevent file path insertion for empty file paths (Atom tabs) 90 | * Add experimental support for tab view 91 | 92 | ## v0.11.2 - Patch 93 | * Fix tooltips staying after the terminal has been closed 94 | 95 | ## v0.11.1 - Patch 96 | * Fix broken links in README 97 | 98 | ## v0.11.0 - Beta Release 99 | * Add insert text dialog for inserting special characters and running commands 100 | * Users can enable `Run Inserted Text` in the settings to have Terminal-Plus run inserted text as a command 101 | * Users can use the insert text dialog to type special characters 102 | * Center terminal lines in the terminal-view 103 | * Improved terminal mapping 104 | * Improve terminal view focusing 105 | * Do not steal focus for the cursor blink 106 | * Do not steal focus for text input 107 | 108 | ## v0.10.1 - Patch 109 | * Fix resizing bug 110 | * Fix language overwrite bug 111 | 112 | ## v0.10.0 - Beta Release 113 | * Added automatic terminal switching 114 | * Add CMD+K to clear terminal [Term.js fork] 115 | * Fix terminal errors relating to Atom setting project path to `atom://config` 116 | 117 | ## v0.9.1 - Patch 118 | * Fix bug where Atom rebuilds Terminal-Plus for every update 119 | * Fix status icon colors keypath 120 | 121 | ## v0.9.0 - Beta Release 122 | * Add support for custom ANSI color set 123 | * Fix `ctrl+c` (SIGINT) not working in bash 124 | * Update winpty module (for Windows) in pty.js 125 | * Fix issues with maintaining focus on the terminal 126 | 127 | ## v0.8.2 - Patch 128 | * Detect system language on OS X 129 | * Even finer scrolling algorithm implemented 130 | 131 | ## v0.8.1 - Patch 132 | * Disable double click on status icons 133 | 134 | ## v0.8.0 - Beta Release 135 | * Implement finer scrolling in dependencies 136 | 137 | ## v0.7.1 - Patch 138 | * Block resize and input when there is no pty process to message 139 | 140 | ## v0.7.0 - Beta Release 141 | * Add support for international characters 142 | * Make sure to declare the terminal as xterm-256color 143 | * Improve colors in xterm-256color 144 | * Set TERM_PROGRAM to Terminal-Plus 145 | 146 | ## v0.6.5 - Patch 147 | * Focus bug fix 148 | 149 | ## v0.6.4 - Patch 150 | * Fix terminal not scrolling for zsh shell with plugins 151 | 152 | ## v0.6.3 - Patch 153 | * Call super after overriding focus 154 | * Update the author's note with Windows 10 fix 155 | 156 | ## v0.6.2 - Patch 157 | * Fix path variable overwrite bug 158 | 159 | ## v0.6.1 - Patch 160 | * Fix text-wrap overflow hiding prompt 161 | 162 | ## v0.6.0 - Beta Release 163 | * Dynamic terminal view resizing 164 | 165 | ## v0.5.1 - Patch 166 | * Remove trailing whitespace from terminal rename 167 | 168 | ## v0.5.0 - Beta Release 169 | * Add terminal naming via the status icon 170 | 171 | ## v0.4.3 - Patch 172 | * Rebuild pty.js binaries for electron release 0.30.6 173 | * Requires Atom >= 1.0.12 174 | 175 | ## v0.4.2 - Patch 176 | * Specify commit for pty.js prebuilt 177 | 178 | ## v0.4.1 - Patch 179 | * Make button toolbar smaller by keeping buttons minimal 180 | * No more names next to button 181 | * Make button fit to icon 182 | * Use --login shell argument by default for bash and zsh 183 | 184 | ## v0.4.0 - Beta Release 185 | * Add prebuilt binaries for pty.js 186 | * Better support for systems without the tools needed to compile (Windows) 187 | 188 | ## v0.3.1 - Patch 189 | * Add warning for custom font family (must use monospaced font) 190 | 191 | ## v0.3.0 - Beta Release 192 | * Refactor resizing to snap to row 193 | * Fix cursor line being removed if blank 194 | * Possible fix for refresh error 195 | * Fix for improper resizing when displaying the terminal for the first time 196 | 197 | ## v0.2.0 - Beta Release 198 | * Bump up to minor version 2 199 | * New settings and features added 200 | * Bug fixes listed below in v0.1.x patches 201 | 202 | ## v0.1.10 - Patch 203 | * Add option to auto close terminal on shell process exit 204 | 205 | ## v0.1.9 - Patch 206 | * Add insert selected text (see commit) 207 | * Remove login command 208 | 209 | ## v0.1.8 - Patch 210 | * Remove quiet option from login 211 | * Disable resize and input on terminal exit 212 | 213 | ## v0.1.7 - Patch 214 | * Resize terminal on maximize and minimize 215 | * Fix powershell.exe resolve 216 | * Fix shell launch bugs 217 | 218 | ## v0.1.6 - Patch 219 | * Make sure to properly resize terminal on open 220 | 221 | ## v0.1.5 - Patch 222 | * On shell process exit, disable input to prevent error 223 | 224 | ## v0.1.4 - Patch 225 | * Make terminal scroll to bottom on input 226 | * Don't close the terminal view on process exit 227 | 228 | ## v0.1.3 - Patch 229 | * Add more features to README.md 230 | * Fix issue #1 231 | 232 | ## v0.1.2 - Patch 233 | * Absolute image source paths in README.md 234 | * Update image in color coding section 235 | 236 | ## v0.1.1 - Patch 237 | * Update the README.md and CHANGELOG.md 238 | 239 | ## v0.1.0 - Beta Release 240 | * Initial release 241 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jeremy Ebneyamin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Author's Note 2 | * Please make sure you are on the [latest version of Atom](https://atom.io/releases) before reporting bugs! 3 | * This package requires that you have the dependencies for node-gyp. 4 | [See node-gyp install instructions](https://github.com/nodejs/node-gyp#installation). 5 | * You must use a monospaced font in order for the spacing to be right. 6 | * Having issues on Windows 10? [Try this](https://github.com/jeremyramin/terminal-plus/issues/15#issuecomment-144618245). 7 | 8 | # Terminal-Plus 9 | Terminal-Plus is a terminal package for Atom, complete with themes and more. 10 | 11 | ![demo](https://github.com/jeremyramin/terminal-plus/raw/master/resources/demo.gif) 12 | 13 | *[Nucleus Dark UI](https://atom.io/themes/nucleus-dark-ui) with [Atom Material Syntax](https://atom.io/themes/atom-material-syntax) and our Homebrew theme.* 14 | 15 | ## Usage 16 | Terminal-Plus stays in the bottom of your editor while you work. 17 | 18 | ### Status Bar 19 | ![status-bar](https://github.com/jeremyramin/terminal-plus/raw/master/resources/status-bar.png) 20 | You can keep track of terminal instances via the status bar. Each terminal has a status icon ( ![status icon](https://github.com/jeremyramin/terminal-plus/raw/master/resources/status-icon.png) ) in the status bar. The ( ![plus-icon](https://github.com/jeremyramin/terminal-plus/raw/master/resources/plus-icon.png) ) button creates a new terminal, while the ( ![red-x](https://github.com/jeremyramin/terminal-plus/raw/master/resources/red-x.png) ) button closes all terminals. 21 | 22 | Click on a status icon to toggle that terminal. Right click the status icon for a list of available commands. From the right-click menu you can color code the status icon as well as hide or close the terminal instance. 23 | 24 | ### Terminal 25 | You can open the last active terminal with the `terminal-plus:toggle` command (Default:`` ctrl-` ``). If no terminal instances are available, then a new one will be created. The same toggle command is used to hide the currently active terminal. 26 | 27 | From there you can begin typing into the terminal. By default the terminal will change directory into the project folder if possible. The default working directory can be changed in the settings to the home directory or to the active file directory. 28 | 29 | [See available commands below](#commands). 30 | 31 | ## Features 32 | 33 | ### Full Terminal 34 | Every terminal is loaded with your system’s default initialization files. This ensures that you have access to the same commands and aliases as you would in your standard terminal. 35 | 36 | ### Themes 37 | The terminal is preloaded with several themes that you can choose from. Not satisfied? 38 | Use the following template in your stylesheet: 39 | ```css 40 | .terminal-plus .xterm { 41 | background-color: ; 42 | color: ; 43 | 44 | ::selection { 45 | background-color: ; 46 | } 47 | 48 | .terminal-cursor { 49 | background-color: ; 50 | } 51 | } 52 | ``` 53 | 54 | ### Process Titles 55 | By hovering over the terminal status icon, you can see which command process is currently running in the terminal. 56 | 57 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/terminal_title.png) 58 | 59 | ### Terminal Naming 60 | Need a faster way to figure out which terminal is which? Name your status icons! 61 | 62 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/status-icon_rename.png) 63 | 64 | Available via the status icon context menu. 65 | 66 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/status-icon_rename-dialog.png) 67 | 68 | ### Color Coding 69 | Color code your status icons! 70 | 71 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/status-icon_color_coding.png) 72 | 73 | The colors are customizable in the settings, however the color names remain the same in the context menu. 74 | 75 | ### Sorting 76 | Organize your open terminal instances by dragging and dropping them. 77 | 78 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/sorting.gif) 79 | 80 | ### Resizable 81 | You can resize the view vertically, or just maximize it with the maximize button. 82 | 83 | ### Working Directory 84 | You can set the default working directory for new terminals. By default this will be the project folder. 85 | 86 | ### File Dropping 87 | Dropping a file on the terminal will insert the file path into the input. This works with external files, tabs from the Atom tab-view, and entries from the Atom tree-view. 88 | 89 | ### Insert Selected Text 90 | Insert and run selected text from your text editor by running the `terminal-plus:insert-selected-text` command (`ctrl-enter`). 91 | 92 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/insert_selected_text.gif) 93 | 94 | If you have text selected, it will insert your selected text into the active terminal and run it. 95 | If you don't have text selected it, will run the text on the line where your cursor is then proceed to the next line. 96 | 97 | ### Quick Command Insert 98 | Quickly insert a command to your active terminal by executing the `terminal-plus:insert-text` command. 99 | 100 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/insert_text.png) 101 | 102 | A dialog will pop up asking for the input to insert. If you have the `Run Inserted Text` option enabled in the settings (default is false), Terminal-Plus will automatically run the command for you. 103 | 104 | #### Support for Special Keys 105 | Support for IME, dead keys and other key combinations via the `Insert Text` dialog box. Just click the keyboard button in the top left of the terminal or set up a keymap to the `terminal-plus:insert-text` command. 106 | 107 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/special_keys.gif) 108 | 109 | Note: Make sure you have the `Run Inserted Command` toggle off otherwise it will run your inserted text. 110 | 111 | ### Map Terminal To 112 | Map your terminals to each file or folder you are working on for automatic terminal switching. 113 | 114 | #### File 115 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/map_terminals_to_file.gif) 116 | 117 | #### Folder 118 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/map_terminals_to_folder.gif) 119 | 120 | Toggling the `Auto Open a New Terminal (For Terminal Mapping)` option will have the mapping create a new terminal automatically for files and folders that don't have a terminal. The toggle is located right under the `Map Terminals To` option. 121 | 122 | ![](https://github.com/jeremyramin/terminal-plus/raw/master/resources/map_terminals_to_auto_open.gif) 123 | 124 | ## Install 125 | Ready to install? 126 | 127 | You can install via apm: `apm install terminal-plus` 128 | 129 | Or navigate to the install tab in Atom’s settings view, and search for `terminal-plus`. 130 | 131 | ## Commands 132 | | Command | Action | Default Keybind | 133 | |---------|--------|:-----------------:| 134 | | terminal-plus:new | Create a new terminal instance. | `ctrl-shift-t`
or
`cmd-shift-t` | 135 | | terminal-plus:toggle | Toggle the last active terminal instance.
**Note:** This will create a new terminal if it needs to. | `` ctrl-` ``
(Control + Backtick) | 136 | | terminal-plus:prev | Switch to the terminal left of the last active terminal. | `ctrl-shift-j`
or
`cmd-shift-j` | 137 | | terminal-plus:next | Switch to the terminal right of the last active terminal. | `ctrl-shift-k`
or
`cmd-shift-k` | 138 | | terminal-plus:insert-selected-text | Run the selected text as a command in the active terminal. | `ctrl-enter` | 139 | | terminal-plus:insert-text | Bring up an input box for using IME and special keys. | –––––––––––– | 140 | | terminal-plus:close | Close the active terminal. | `ctrl-shift-x`
or
`cmd-shift-x` | 141 | | terminal-plus:close-all | Close all terminals. | –––––––––––– | 142 | | terminal-plus:rename | Rename the active terminal. | –––––––––––– | 143 | 144 | ## To-Do List 145 | - [ ] Possibly merge dependencies into Terminal-Plus? 146 | - [ ] Add support for dead keys and IME input 147 | - [x] Add support for terminal tabs 148 | - [x] Add support for automatic directory switching 149 | - [x] Fix Atom requesting a rebuild after every update 150 | - [x] Update winpty in pty.js dependency 151 | - [x] Add support for custom ANSI colors in terminal 152 | - [x] Fix `ctrl+c` for bash prompts on OS X and Linux 153 | - [x] Add support for status icon names 154 | -------------------------------------------------------------------------------- /keymaps/terminal-plus.cson: -------------------------------------------------------------------------------- 1 | '.platform-darwin atom-workspace': 2 | 'cmd-shift-t': 'terminal-plus:new' 3 | 'cmd-shift-j': 'terminal-plus:prev' 4 | 'cmd-shift-k': 'terminal-plus:next' 5 | 'cmd-shift-x': 'terminal-plus:close' 6 | 'ctrl-enter': 'terminal-plus:insert-selected-text' 7 | 'ctrl-`': 'terminal-plus:toggle' 8 | 9 | '.platform-linux atom-workspace, .platform-win32 atom-workspace': 10 | 'alt-shift-t': 'terminal-plus:new' 11 | 'alt-shift-j': 'terminal-plus:prev' 12 | 'alt-shift-k': 'terminal-plus:next' 13 | 'alt-shift-x': 'terminal-plus:close' 14 | 'ctrl-enter': 'terminal-plus:insert-selected-text' 15 | 'ctrl-`': 'terminal-plus:toggle' 16 | 17 | '.platform-darwin .terminal-plus .terminal': 18 | 'cmd-c': 'terminal-plus:copy' 19 | 'cmd-v': 'terminal-plus:paste' 20 | 21 | '.platform-linux .terminal-plus .terminal, .platform-win32 .terminal-plus .terminal': 22 | 'alt-v': 'terminal-plus:paste' 23 | 'alt-c': 'terminal-plus:copy' 24 | -------------------------------------------------------------------------------- /lib/config-schema.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = 4 | toggles: 5 | type: 'object' 6 | order: 1 7 | properties: 8 | autoClose: 9 | title: 'Close Terminal on Exit' 10 | description: 'Should the terminal close if the shell exits?' 11 | type: 'boolean' 12 | default: false 13 | cursorBlink: 14 | title: 'Cursor Blink' 15 | description: 'Should the cursor blink when the terminal is active?' 16 | type: 'boolean' 17 | default: true 18 | runInsertedText: 19 | title: 'Run Inserted Text' 20 | description: 'Run text inserted via `terminal-plus:insert-text` as a command? **This will append an end-of-line character to input.**' 21 | type: 'boolean' 22 | default: true 23 | core: 24 | type: 'object' 25 | order: 2 26 | properties: 27 | autoRunCommand: 28 | title: 'Auto Run Command' 29 | description: 'Command to run on terminal initialization.' 30 | type: 'string' 31 | default: '' 32 | defaultView: 33 | title: 'Default View Type' 34 | description: 'Set the default view to open when creating a new terminal.' 35 | type: 'string' 36 | default: 'Match Active Terminal' 37 | enum: [ 38 | 'Panel', 39 | 'Tab', 40 | 'Match Active Terminal' 41 | ] 42 | mapTerminalsTo: 43 | title: 'Map Terminals To' 44 | description: 'Map terminals to each file or folder. Default is no action or mapping at all. **Restart required.**' 45 | type: 'string' 46 | default: 'None' 47 | enum: ['None', 'File', 'Folder'] 48 | mapTerminalsToAutoOpen: 49 | title: 'Auto Open a New Terminal (For Terminal Mapping)' 50 | description: 'Should a new terminal be opened for new items? **Note:** This works in conjunction with `Map Terminals To` above.' 51 | type: 'boolean' 52 | default: false 53 | scrollback: 54 | title: 'Scroll Back' 55 | description: 'How many lines of history should be kept?' 56 | type: 'integer' 57 | default: 1000 58 | shell: 59 | title: 'Shell Override' 60 | description: 'Override the default shell instance to launch.' 61 | type: 'string' 62 | default: do -> 63 | if process.platform is 'win32' 64 | path.resolve(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') 65 | else 66 | process.env.SHELL 67 | shellArguments: 68 | title: 'Shell Arguments' 69 | description: 'Specify some arguments to use when launching the shell.' 70 | type: 'string' 71 | default: '' 72 | statusBar: 73 | title: 'Status Bar' 74 | description: 'Choose how you want the status bar displayed.' 75 | type: 'string' 76 | default: 'Full' 77 | enum: ['Full', 'Collapsed', 'None'] 78 | workingDirectory: 79 | title: 'Working Directory' 80 | description: 'Which directory should be the present working directory when a new terminal is made?' 81 | type: 'string' 82 | default: 'Project' 83 | enum: ['Home', 'Project', 'Active File'] 84 | style: 85 | type: 'object' 86 | order: 3 87 | properties: 88 | animationSpeed: 89 | title: 'Animation Speed' 90 | description: 'How fast should the window animate?' 91 | type: 'number' 92 | default: '1' 93 | minimum: '0' 94 | maximum: '100' 95 | fontAntialiasing: 96 | title: 'Font Antialiasing' 97 | description: 'Set the type of font antialiasing for the terminal.' 98 | type: 'string' 99 | default: 'Antialiased' 100 | enum: ['Antialiased', 'Default', 'None'] 101 | fontFamily: 102 | title: 'Font Family' 103 | description: 'Override the terminal\'s default font family. **You must use a [monospaced font](https://en.wikipedia.org/wiki/List_of_typefaces#Monospace)!**' 104 | type: 'string' 105 | default: '' 106 | fontSize: 107 | title: 'Font Size' 108 | description: 'Override the terminal\'s default font size.' 109 | type: 'string' 110 | default: '' 111 | defaultPanelHeight: 112 | title: 'Default Panel Height' 113 | description: 'Default height of a terminal panel. **You may enter a value in px, em, or %.**' 114 | type: 'string' 115 | default: '300px' 116 | theme: 117 | title: 'Theme' 118 | description: 'Select a theme for the terminal.' 119 | type: 'string' 120 | default: 'standard' 121 | enum: [ 122 | 'standard', 123 | 'inverse', 124 | 'grass', 125 | 'homebrew', 126 | 'man-page', 127 | 'novel', 128 | 'ocean', 129 | 'pro', 130 | 'red', 131 | 'red-sands', 132 | 'silver-aerogel', 133 | 'solid-colors', 134 | 'dracula' 135 | ] 136 | ansiColors: 137 | type: 'object' 138 | order: 4 139 | properties: 140 | normal: 141 | type: 'object' 142 | order: 1 143 | properties: 144 | black: 145 | title: 'Black' 146 | description: 'Black color used for terminal ANSI color set.' 147 | type: 'color' 148 | default: '#000000' 149 | red: 150 | title: 'Red' 151 | description: 'Red color used for terminal ANSI color set.' 152 | type: 'color' 153 | default: '#CD0000' 154 | green: 155 | title: 'Green' 156 | description: 'Green color used for terminal ANSI color set.' 157 | type: 'color' 158 | default: '#00CD00' 159 | yellow: 160 | title: 'Yellow' 161 | description: 'Yellow color used for terminal ANSI color set.' 162 | type: 'color' 163 | default: '#CDCD00' 164 | blue: 165 | title: 'Blue' 166 | description: 'Blue color used for terminal ANSI color set.' 167 | type: 'color' 168 | default: '#0000CD' 169 | magenta: 170 | title: 'Magenta' 171 | description: 'Magenta color used for terminal ANSI color set.' 172 | type: 'color' 173 | default: '#CD00CD' 174 | cyan: 175 | title: 'Cyan' 176 | description: 'Cyan color used for terminal ANSI color set.' 177 | type: 'color' 178 | default: '#00CDCD' 179 | white: 180 | title: 'White' 181 | description: 'White color used for terminal ANSI color set.' 182 | type: 'color' 183 | default: '#E5E5E5' 184 | zBright: 185 | type: 'object' 186 | order: 2 187 | properties: 188 | brightBlack: 189 | title: 'Bright Black' 190 | description: 'Bright black color used for terminal ANSI color set.' 191 | type: 'color' 192 | default: '#7F7F7F' 193 | brightRed: 194 | title: 'Bright Red' 195 | description: 'Bright red color used for terminal ANSI color set.' 196 | type: 'color' 197 | default: '#FF0000' 198 | brightGreen: 199 | title: 'Bright Green' 200 | description: 'Bright green color used for terminal ANSI color set.' 201 | type: 'color' 202 | default: '#00FF00' 203 | brightYellow: 204 | title: 'Bright Yellow' 205 | description: 'Bright yellow color used for terminal ANSI color set.' 206 | type: 'color' 207 | default: '#FFFF00' 208 | brightBlue: 209 | title: 'Bright Blue' 210 | description: 'Bright blue color used for terminal ANSI color set.' 211 | type: 'color' 212 | default: '#0000FF' 213 | brightMagenta: 214 | title: 'Bright Magenta' 215 | description: 'Bright magenta color used for terminal ANSI color set.' 216 | type: 'color' 217 | default: '#FF00FF' 218 | brightCyan: 219 | title: 'Bright Cyan' 220 | description: 'Bright cyan color used for terminal ANSI color set.' 221 | type: 'color' 222 | default: '#00FFFF' 223 | brightWhite: 224 | title: 'Bright White' 225 | description: 'Bright white color used for terminal ANSI color set.' 226 | type: 'color' 227 | default: '#FFFFFF' 228 | iconColors: 229 | type: 'object' 230 | order: 5 231 | properties: 232 | red: 233 | title: 'Status Icon Red' 234 | description: 'Red color used for status icon.' 235 | type: 'color' 236 | default: 'red' 237 | orange: 238 | title: 'Status Icon Orange' 239 | description: 'Orange color used for status icon.' 240 | type: 'color' 241 | default: 'orange' 242 | yellow: 243 | title: 'Status Icon Yellow' 244 | description: 'Yellow color used for status icon.' 245 | type: 'color' 246 | default: 'yellow' 247 | green: 248 | title: 'Status Icon Green' 249 | description: 'Green color used for status icon.' 250 | type: 'color' 251 | default: 'green' 252 | blue: 253 | title: 'Status Icon Blue' 254 | description: 'Blue color used for status icon.' 255 | type: 'color' 256 | default: 'blue' 257 | purple: 258 | title: 'Status Icon Purple' 259 | description: 'Purple color used for status icon.' 260 | type: 'color' 261 | default: 'purple' 262 | pink: 263 | title: 'Status Icon Pink' 264 | description: 'Pink color used for status icon.' 265 | type: 'color' 266 | default: 'hotpink' 267 | cyan: 268 | title: 'Status Icon Cyan' 269 | description: 'Cyan color used for status icon.' 270 | type: 'color' 271 | default: 'cyan' 272 | magenta: 273 | title: 'Status Icon Magenta' 274 | description: 'Magenta color used for status icon.' 275 | type: 'color' 276 | default: 'magenta' 277 | -------------------------------------------------------------------------------- /lib/core.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | PanelView = null 4 | TabView = null 5 | StatusBar = null 6 | 7 | Path = require 'path' 8 | 9 | class Core 10 | subscriptions: null 11 | activeTerminal: null 12 | terminals: [] 13 | returnFocus: false 14 | 15 | constructor: -> 16 | @subscriptions = new CompositeDisposable() 17 | 18 | @registerCommands() 19 | @registerActiveItemSubscription() 20 | @registerWindowEvents() 21 | 22 | destroy: -> 23 | @subscriptions.dispose() 24 | for terminal in @terminals 25 | terminal.destroy() 26 | 27 | ### 28 | Section: Setup 29 | ### 30 | 31 | registerCommands: -> 32 | @subscriptions.add atom.commands.add 'atom-workspace', 33 | 'terminal-plus:new': => @newTerminalView()?.toggle() 34 | 35 | 'terminal-plus:toggle': => @toggle() 36 | 37 | 'terminal-plus:next': => 38 | @activeTerminal.open() if @activateNextTerminal() 39 | 'terminal-plus:prev': => 40 | @activeTerminal.open() if @activatePrevTerminal() 41 | 42 | 'terminal-plus:close': => @destroyActiveTerminal() 43 | 'terminal-plus:close-all': => @closeAll() 44 | 45 | 'terminal-plus:rename': => 46 | @runInActiveTerminal (i) -> i.promptForRename() 47 | 'terminal-plus:insert-selected-text': => 48 | @runInActiveTerminal (i) -> i.insertSelection() 49 | 'terminal-plus:insert-text': => 50 | @runInActiveTerminal (i) -> i.promptForInput() 51 | 'terminal-plus:toggle-focus': => 52 | @runInActiveTerminal (i) -> i.toggleFocus() 53 | 'terminal-plus:toggle-full-screen': => 54 | @runInActiveTerminal (i) -> i.toggleFullscreen() 55 | 56 | @subscriptions.add atom.commands.add '.xterm', 57 | 'terminal-plus:paste': => 58 | @runInActiveTerminal (i) -> i.paste() 59 | 'terminal-plus:copy': => 60 | @runInActiveTerminal (i) -> i.copy() 61 | 62 | registerActiveItemSubscription: -> 63 | @subscriptions.add atom.workspace.onDidChangeActivePaneItem (item) => 64 | return unless item? 65 | 66 | if item.constructor.name is "TabView" 67 | setTimeout item.focus, 100 68 | else if item.constructor.name is "TextEditor" 69 | mapping = atom.config.get('terminal-plus.core.mapTerminalsTo') 70 | return if mapping is 'None' 71 | 72 | switch mapping 73 | when 'File' 74 | nextTerminal = @findFirstTerminal (terminal) -> 75 | item.getPath() == terminal.getId().filePath 76 | when 'Folder' 77 | nextTerminal = @findFirstTerminal (terminal) -> 78 | Path.dirname(item.getPath()) == terminal.getId().folderPath 79 | 80 | prevTerminal = @getActiveTerminal() 81 | if prevTerminal != nextTerminal 82 | if not nextTerminal? 83 | if item.getTitle() isnt 'untitled' 84 | if atom.config.get('terminal-plus.core.mapTerminalsToAutoOpen') 85 | nextTerminal = @createTerminalView().getTerminal() 86 | else 87 | @setActiveTerminal(nextTerminal) 88 | if prevTerminal.getParentView()?.panel.isVisible() 89 | nextTerminal.getParentView().toggle() 90 | 91 | registerWindowEvents: -> 92 | handleBlur = => 93 | if @activeTerminal and @activeTerminal.isFocused() 94 | @returnFocus = true 95 | @activeTerminal.getParentView().blur() 96 | 97 | handleFocus = => 98 | if @returnFocus and @activeTerminal 99 | setTimeout => 100 | @activeTerminal.focus() 101 | @returnFocus = false 102 | , 100 103 | 104 | window.addEventListener 'blur', handleBlur 105 | @subscriptions.add dispose: -> 106 | window.removeEventListener 'blur', handleBlur 107 | 108 | window.addEventListener 'focus', handleFocus 109 | @subscriptions.add dispose: -> 110 | window.removeEventListener 'focus', handleFocus 111 | 112 | 113 | ### 114 | Section: Command Handling 115 | ### 116 | 117 | activateNextTerminal: -> 118 | return false if (not @activeTerminal) or @activeTerminal.isAnimating() 119 | 120 | index = @terminals.indexOf(@activeTerminal) 121 | return false if index < 0 122 | @activateTerminalAtIndex index + 1 123 | 124 | activatePrevTerminal: -> 125 | return false if (not @activeTerminal) or @activeTerminal.isAnimating() 126 | 127 | index = @terminals.indexOf(@activeTerminal) 128 | return false if index < 0 129 | @activateTerminalAtIndex index - 1 130 | 131 | closeAll: => 132 | panels = @getPanelViews() 133 | @terminals = @getTabViews() 134 | 135 | for panel in panels 136 | panel.getParentView().destroy() 137 | 138 | @activeTerminal = @terminals[0] 139 | 140 | destroyActiveTerminal: -> 141 | return unless @activeTerminal? 142 | 143 | index = @terminals.indexOf(@activeTerminal) 144 | @removeTerminalAt(index) 145 | if @activeTerminal.isTabView() 146 | parent = @activeTerminal.getParentView() 147 | if pane = atom.workspace.paneForItem(parent) 148 | pane.destroyItem(parent) 149 | else 150 | @activeTerminal.getParentView().destroy() 151 | @activeTerminal = null 152 | 153 | @activateAdjacentTerminal index 154 | 155 | newTerminalView: => 156 | PanelView ?= require './panel-view' 157 | TabView ?= require './tab-view' 158 | StatusBar ?= require './status-bar' 159 | StatusBar.registerPaneSubscription() 160 | 161 | return null if @activeTerminal and @activeTerminal.isAnimating() 162 | 163 | terminalView = @createTerminalView() 164 | @terminals.push terminalView.getTerminal() 165 | return terminalView 166 | 167 | runInActiveTerminal: (callback) -> 168 | terminal = @getActiveTerminal() 169 | if terminal? 170 | return callback(terminal) 171 | return null 172 | 173 | toggle: -> 174 | return if @activeTerminal and @activeTerminal.isAnimating() 175 | 176 | if @terminals.length == 0 177 | @activeTerminal = @newTerminalView().getTerminal() 178 | else if @activeTerminal == null 179 | @activeTerminal = @terminals[0] 180 | @activeTerminal.toggle() 181 | 182 | 183 | ### 184 | Section: External Methods 185 | ### 186 | 187 | getActiveTerminal: -> 188 | return @activeTerminal 189 | 190 | getActiveTerminalView: -> 191 | return @activeTerminal.getParentView() 192 | 193 | getTerminals: -> 194 | return @terminals 195 | 196 | length: -> 197 | return @terminals.length 198 | 199 | removeTerminal: (terminal) -> 200 | index = @terminals.indexOf terminal 201 | return if index < 0 202 | @terminals.splice index, 1 203 | 204 | if terminal == @activeTerminal 205 | unless @activateAdjacentTerminal() 206 | @activeTerminal = null 207 | 208 | removeTerminalAt: (index) -> 209 | return if index < 0 or index > @terminals.length 210 | return @terminals.splice(index, 1)[0] 211 | 212 | removeTerminalView: (view) -> 213 | @removeTerminal view.getTerminal() 214 | 215 | setActiveTerminal: (terminal) -> 216 | @activeTerminal = terminal 217 | 218 | setActiveTerminalView: (view) -> 219 | @setActiveTerminal view.getTerminal() 220 | 221 | terminalAt: (index) -> 222 | return @terminals[index] 223 | 224 | moveTerminal: (fromIndex, toIndex) => 225 | fromIndex = Math.max(0, fromIndex) 226 | toIndex = Math.min(toIndex, @terminals.length) 227 | 228 | terminal = @terminals.splice(fromIndex, 1)[0] 229 | @terminals.splice toIndex, 0, terminal 230 | 231 | 232 | ### 233 | Section: Helper Methods 234 | ### 235 | 236 | activateAdjacentTerminal: (index = 0) -> 237 | return false unless @terminals.length > 0 238 | 239 | index = Math.max(0, index - 1) 240 | @activeTerminal = @terminals[index] 241 | 242 | activateTerminalAtIndex: (index) -> 243 | return false if @terminals.length < 2 244 | 245 | if index >= @terminals.length 246 | index = 0 247 | if index < 0 248 | index = @terminals.length - 1 249 | 250 | @activeTerminal = @terminals[index] 251 | return true 252 | 253 | createTerminalView: -> 254 | projectFolder = atom.project.getPaths()[0] 255 | editorPath = atom.workspace.getActiveTextEditor()?.getPath() 256 | 257 | if editorPath? 258 | editorFolder = Path.dirname(editorPath) 259 | for directory in atom.project.getPaths() 260 | if editorPath.indexOf(directory) >= 0 261 | projectFolder = directory 262 | 263 | projectFolder = undefined if projectFolder?.indexOf('atom://') >= 0 264 | 265 | home = if process.platform is 'win32' then process.env.HOMEPATH else process.env.HOME 266 | 267 | switch atom.config.get('terminal-plus.core.workingDirectory') 268 | when 'Project' then pwd = projectFolder or editorFolder or home 269 | when 'Active File' then pwd = editorFolder or projectFolder or home 270 | else pwd = home 271 | 272 | id = editorPath or projectFolder or home 273 | id = filePath: id, folderPath: Path.dirname(id) 274 | 275 | shellPath = atom.config.get 'terminal-plus.core.shell' 276 | 277 | ViewType = null 278 | switch atom.config.get 'terminal-plus.core.defaultView' 279 | when "Panel" then ViewType = PanelView 280 | when "Tab" then ViewType = TabView 281 | when "Match Active Terminal" 282 | if @activeTerminal?.isTabView() 283 | ViewType = TabView 284 | else 285 | ViewType = PanelView 286 | 287 | return new ViewType { 288 | id, pwd, shellPath 289 | } 290 | 291 | findFirstTerminal: (filter) -> 292 | matches = @terminals.filter filter 293 | return matches[0] 294 | 295 | iconAtIndex: (index) -> 296 | @getStatusIcons().eq(index) 297 | 298 | getPanelViews: -> 299 | @terminals.filter (terminal) -> terminal.isPanelView() 300 | 301 | getTabViews: -> 302 | @terminals.filter (terminal) -> terminal.isTabView() 303 | 304 | module.exports = exports = new Core() 305 | -------------------------------------------------------------------------------- /lib/dialog.coffee: -------------------------------------------------------------------------------- 1 | {TextEditorView, View} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class Dialog extends View 5 | @content: ({prompt} = {}) -> 6 | @div class: 'terminal-plus-dialog', => 7 | @label prompt, class: 'icon', outlet: 'promptText' 8 | @subview 'miniEditor', new TextEditorView(mini: true) 9 | @label 'Escape (Esc) to exit', style: 'float: left;' 10 | @label 'Enter (\u21B5) to confirm', style: 'float: right;' 11 | 12 | initialize: ({iconClass, placeholderText, stayOpen} = {}) -> 13 | @promptText.addClass(iconClass) if iconClass 14 | atom.commands.add @element, 15 | 'core:confirm': => @onConfirm(@miniEditor.getText()) 16 | 17 | atom.commands.add 'atom-workspace', 18 | 'core:cancel': => @cancel() 19 | 20 | unless stayOpen 21 | @miniEditor.on 'blur', => @close() 22 | 23 | if placeholderText 24 | @miniEditor.getModel().setText placeholderText 25 | @miniEditor.getModel().selectAll() 26 | 27 | attach: -> 28 | @panel = atom.workspace.addModalPanel(item: this.element) 29 | @miniEditor.focus() 30 | @miniEditor.getModel().scrollToCursorPosition() 31 | 32 | close: -> 33 | panelToDestroy = @panel 34 | @panel = null 35 | panelToDestroy?.destroy() 36 | atom.workspace.getActivePane().activate() 37 | 38 | cancel: -> 39 | @close() 40 | -------------------------------------------------------------------------------- /lib/fork.coffee: -------------------------------------------------------------------------------- 1 | pty = require 'pty.js' 2 | path = require 'path' 3 | fs = require 'fs' 4 | _ = require 'underscore' 5 | 6 | systemLanguage = do -> 7 | language = "en_US.UTF-8" 8 | if process.platform is 'darwin' 9 | try 10 | command = 'plutil -convert json -o - ~/Library/Preferences/.GlobalPreferences.plist' 11 | language = "#{JSON.parse(child.execSync(command).toString()).AppleLocale}.UTF-8" 12 | return language 13 | 14 | filteredEnvironment = do -> 15 | env = _.omit process.env, 'ATOM_HOME', 'ATOM_SHELL_INTERNAL_RUN_AS_NODE', 'GOOGLE_API_KEY', 'NODE_ENV', 'NODE_PATH', 'userAgent', 'taskPath' 16 | env.LANG ?= systemLanguage 17 | env.TERM_PROGRAM = 'Terminal-Plus' 18 | return env 19 | 20 | module.exports = (pwd, shellPath, args) -> 21 | callback = @async() 22 | 23 | ptyProcess = pty.fork shellPath, args, 24 | cwd: pwd, 25 | env: filteredEnvironment, 26 | name: 'xterm-256color' 27 | 28 | title = shell = path.basename shellPath 29 | 30 | emitTitle = _.throttle -> 31 | emit('terminal-plus:title', ptyProcess.process) 32 | , 500, true 33 | 34 | ptyProcess.on 'data', (data) -> 35 | emit('terminal-plus:data', data) 36 | emitTitle() 37 | 38 | ptyProcess.on 'exit', -> 39 | emit('terminal-plus:exit') 40 | callback() 41 | 42 | process.on 'message', ({event, cols, rows, text}={}) -> 43 | switch event 44 | when 'resize' then ptyProcess.resize(cols, rows) 45 | when 'input' then ptyProcess.write(text) 46 | -------------------------------------------------------------------------------- /lib/input-dialog.coffee: -------------------------------------------------------------------------------- 1 | Dialog = require "./dialog" 2 | os = require "os" 3 | 4 | module.exports = 5 | class InputDialog extends Dialog 6 | constructor: (@terminal) -> 7 | @focus = @terminal.isFocused() 8 | @terminal.blur() 9 | 10 | super 11 | prompt: "Insert Text" 12 | iconClass: "icon-keyboard" 13 | stayOpen: true 14 | 15 | onConfirm: (input) -> 16 | if atom.config.get('terminal-plus.toggles.runInsertedText') 17 | eol = os.EOL 18 | else 19 | eol = '' 20 | 21 | data = "#{input}#{eol}" 22 | @terminal.input data 23 | @cancel() 24 | 25 | cancel: -> 26 | @terminal.focus() if @focus 27 | super() 28 | -------------------------------------------------------------------------------- /lib/panel-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {$} = require 'atom-space-pen-views' 3 | 4 | TerminalView = require './terminal-view' 5 | 6 | lastOpenedTerminal = null 7 | 8 | defaultHeight = do -> 9 | height = atom.config.get('terminal-plus.style.defaultPanelHeight') 10 | if height.indexOf('%') > 0 11 | percent = Math.abs(Math.min(parseFloat(height) / 100.0, 1)) 12 | bottomHeight = $('atom-panel.bottom').children(".terminal-view").height() or 0 13 | height = percent * ($('.item-views').height() + bottomHeight) 14 | return height 15 | 16 | module.exports = 17 | class PanelView extends TerminalView 18 | animating: false 19 | windowHeight: atom.getSize().height 20 | 21 | @getFocusedTerminal: -> 22 | return TerminalView.getFocusedTerminal() 23 | 24 | initialize: (options) -> 25 | super(options) 26 | 27 | @addDefaultButtons() 28 | 29 | @terminal.showIcon() 30 | @updateName(@terminal.getName()) 31 | 32 | @attachResizeEvents() 33 | @attach() 34 | 35 | destroy: ({keepTerminal}={}) => 36 | @detachResizeEvents() 37 | 38 | if @panel.isVisible() and not keepTerminal 39 | @onTransitionEnd => 40 | @panel.destroy() 41 | @hide() 42 | else 43 | @panel.destroy() 44 | 45 | super(keepTerminal) 46 | 47 | ### 48 | Section: Setup 49 | ### 50 | 51 | addDefaultButtons: -> 52 | @closeBtn = @addButton 'right', @destroy, 'x' 53 | @hideBtn = @addButton 'right', @hide, 'chevron-down' 54 | @fullscreenBtn = @addButton 'right', @toggleFullscreen, 'screen-full' 55 | @inputBtn = @addButton 'left', @promptForInput, 'keyboard' 56 | 57 | @subscriptions.add atom.tooltips.add @closeBtn, 58 | title: 'Close' 59 | @subscriptions.add atom.tooltips.add @hideBtn, 60 | title: 'Hide' 61 | @subscriptions.add atom.tooltips.add @fullscreenBtn, 62 | title: 'Maximize' 63 | @subscriptions.add atom.tooltips.add @inputBtn, 64 | title: 'Insert Text' 65 | 66 | attach: -> 67 | return if @panel? 68 | @panel = atom.workspace.addBottomPanel(item: this, visible: false) 69 | 70 | 71 | ### 72 | Section: Resizing 73 | ### 74 | 75 | attachResizeEvents: -> 76 | @panelDivider.on 'mousedown', @resizeStarted 77 | 78 | detachResizeEvents: -> 79 | @panelDivider.off 'mousedown' 80 | 81 | onWindowResize: (event) => 82 | @terminal.disableAnimation() 83 | delta = atom.getSize().height - @windowHeight 84 | 85 | if lines = (delta / @terminal.getRowHeight()|0) 86 | offset = lines * @terminal.getRowHeight() 87 | newHeight = @terminal.height() + offset 88 | 89 | if newHeight >= @terminal.getRowHeight() 90 | @terminal.height newHeight 91 | @maxHeight += offset 92 | @windowHeight += offset 93 | else 94 | @terminal.height @terminal.getRowHeight() 95 | 96 | @terminal.enableAnimation() 97 | super() 98 | 99 | resizeStarted: => 100 | return if @maximized 101 | @maxHeight = @terminal.getPrevHeight() + $('.item-views').height() 102 | $(document).on('mousemove', @resizePanel) 103 | $(document).on('mouseup', @resizeStopped) 104 | @terminal.disableAnimation() 105 | 106 | resizeStopped: => 107 | $(document).off('mousemove', @resizePanel) 108 | $(document).off('mouseup', @resizeStopped) 109 | @terminal.enableAnimation() 110 | 111 | resizePanel: (event) => 112 | return @resizeStopped() unless event.which is 1 113 | 114 | mouseY = $(window).height() - event.pageY 115 | delta = mouseY - $('atom-panel-container.bottom').height() 116 | return unless Math.abs(delta) > (@terminal.getRowHeight() * 5 / 6) 117 | 118 | nearestRow = @nearestRow(@terminal.height() + delta) 119 | clamped = Math.max(nearestRow, @terminal.getRowHeight()) 120 | return if clamped > @maxHeight 121 | 122 | @terminal.height clamped 123 | @terminal.recalibrateSize() 124 | 125 | 126 | ### 127 | Section: External Methods 128 | ### 129 | 130 | open: => 131 | super() 132 | @terminal.getStatusIcon().activate() 133 | 134 | if lastOpenedTerminal and lastOpenedTerminal != @terminal 135 | lastOpenedTerminal.getParentView().hide({refocus: false}) 136 | lastOpenedTerminal = @terminal 137 | 138 | @onTransitionEnd => 139 | if @terminal.displayView() 140 | height = @nearestRow(@terminal.height()) 141 | @terminal.height(height) 142 | @focus() 143 | 144 | @panel.show() 145 | @terminal.height 0 146 | @animating = true 147 | @terminal.height @terminal.getPrevHeight() or defaultHeight 148 | 149 | hide: ({refocus}={})=> 150 | refocus ?= true 151 | lastOpenedTerminal = null 152 | @terminal.getStatusIcon().deactivate() 153 | 154 | @onTransitionEnd => 155 | @panel.hide() 156 | super(refocus) 157 | 158 | @terminal.height @terminal.getPrevHeight() 159 | @animating = true 160 | @terminal.height 0 161 | 162 | updateName: (name) -> 163 | @terminal.getStatusIcon().setName(name) 164 | 165 | toggleFullscreen: => 166 | @destroy keepTerminal: true 167 | @terminal.clearHeight().disableAnimation() 168 | tabView = new (require './tab-view') {@terminal} 169 | tabView.toggle() 170 | @remove() 171 | 172 | isVisible: -> 173 | @panel.isVisible() 174 | 175 | 176 | ### 177 | Section: Helper Methods 178 | ### 179 | 180 | nearestRow: (value) -> 181 | rowHeight = @terminal.getRowHeight() 182 | return rowHeight * Math.round(value / rowHeight) 183 | 184 | onTransitionEnd: (callback) -> 185 | @terminal.one 'webkitTransitionEnd', => 186 | callback() 187 | @animating = false 188 | -------------------------------------------------------------------------------- /lib/rename-dialog.coffee: -------------------------------------------------------------------------------- 1 | Dialog = require "./dialog" 2 | 3 | module.exports = 4 | class RenameDialog extends Dialog 5 | constructor: (@terminal) -> 6 | super 7 | prompt: "Rename" 8 | iconClass: "icon-pencil" 9 | placeholderText: @terminal.getName() 10 | 11 | onConfirm: (newTitle) -> 12 | @terminal.setName newTitle.trim() 13 | @cancel() 14 | -------------------------------------------------------------------------------- /lib/shell.coffee: -------------------------------------------------------------------------------- 1 | {Task} = require 'atom' 2 | 3 | Path = require 'path' 4 | fork = require.resolve './fork' 5 | 6 | module.exports = 7 | class Shell 8 | child: null 9 | 10 | constructor: ({pwd, shellPath}) -> 11 | 12 | disableInput = => 13 | @fork = null 14 | @input = -> 15 | @resize = -> 16 | 17 | shellArguments = atom.config.get 'terminal-plus.core.shellArguments' 18 | args = shellArguments.split(/\s+/g).filter (arg) -> arg 19 | if /zsh|bash/.test(shellPath) and args.indexOf('--login') == -1 20 | args.unshift '--login' 21 | 22 | @fork = Task.once fork, Path.resolve(pwd), shellPath, args, disableInput 23 | 24 | destroy: -> 25 | @terminate() 26 | 27 | 28 | ### 29 | Section: External Methods 30 | ### 31 | 32 | input: (data) -> 33 | return unless @isStillAlive() 34 | 35 | @fork.send event: 'input', text: data 36 | 37 | resize: (cols, rows) -> 38 | return unless @isStillAlive() 39 | 40 | @fork.send {event: 'resize', rows, cols} 41 | 42 | isStillAlive: -> 43 | return false unless @fork and @fork.childProcess 44 | return @fork.childProcess.connected and !@fork.childProcess.killed 45 | 46 | terminate: -> 47 | @fork.terminate() 48 | 49 | on: (event, handler) -> 50 | @fork.on event, handler 51 | 52 | off: (event, handler) -> 53 | if handler 54 | @fork.off event, handler 55 | else 56 | @fork.off event 57 | -------------------------------------------------------------------------------- /lib/status-bar.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {$, View} = require 'atom-space-pen-views' 3 | 4 | class StatusBar extends View 5 | attached: false 6 | container: null 7 | 8 | @content: -> 9 | @div class: 'terminal-plus inline-block', style: 'width: 100%;', => 10 | @div class: 'terminal-plus-status-bar', => 11 | @i class: "icon icon-plus", click: 'newTerminalView', outlet: 'plusBtn' 12 | @ul { 13 | class: "status-container list-inline", 14 | tabindex: '-1', 15 | outlet: 'statusContainer' 16 | } 17 | @i { 18 | class: "icon icon-x", 19 | style: "color: red;", 20 | click: 'closeAll', 21 | outlet: 'closeBtn' 22 | } 23 | 24 | initialize: -> 25 | @core = require './core' 26 | 27 | @subscriptions = new CompositeDisposable() 28 | 29 | @subscriptions.add atom.tooltips.add @plusBtn, title: 'New Terminal' 30 | @subscriptions.add atom.tooltips.add @closeBtn, title: 'Close All' 31 | 32 | @statusContainer.on 'dblclick', (event) => 33 | if event.target == event.delegateTarget 34 | @newTerminalView() 35 | 36 | @registerContextMenu() 37 | @registerDragDropInterface() 38 | 39 | destroy: -> 40 | @destroyContainer() 41 | 42 | 43 | ### 44 | Section: Setup 45 | ### 46 | 47 | registerContextMenu: -> 48 | findStatusIcon = (event) -> 49 | return $(event.target).closest('.status-icon')[0] 50 | 51 | @subscriptions.add atom.commands.add '.terminal-plus-status-bar', 52 | 'terminal-plus:status-red': @setStatusColor 53 | 'terminal-plus:status-orange': @setStatusColor 54 | 'terminal-plus:status-yellow': @setStatusColor 55 | 'terminal-plus:status-green': @setStatusColor 56 | 'terminal-plus:status-blue': @setStatusColor 57 | 'terminal-plus:status-purple': @setStatusColor 58 | 'terminal-plus:status-pink': @setStatusColor 59 | 'terminal-plus:status-cyan': @setStatusColor 60 | 'terminal-plus:status-magenta': @setStatusColor 61 | 'terminal-plus:status-default': @clearStatusColor 62 | 'terminal-plus:context-close': (event) -> 63 | findStatusIcon(event).getTerminalView().destroy() 64 | 'terminal-plus:context-hide': (event) -> 65 | statusIcon = findStatusIcon(event) 66 | statusIcon.getTerminalView().hide() if statusIcon.isActive() 67 | 'terminal-plus:context-rename': (event) -> 68 | findStatusIcon(event).getTerminal().promptForRename() 69 | 70 | registerDragDropInterface: -> 71 | @statusContainer.on 'dragstart', '.status-icon', @onDragStart 72 | @statusContainer.on 'dragend', '.status-icon', @onDragEnd 73 | @statusContainer.on 'dragleave', @onDragLeave 74 | @statusContainer.on 'dragover', @onDragOver 75 | @statusContainer.on 'drop', @onDrop 76 | 77 | registerPaneSubscription: -> 78 | return if @paneSubscription 79 | 80 | @subscriptions.add @paneSubscription = atom.workspace.observePanes (pane) => 81 | paneElement = $(atom.views.getView(pane)) 82 | tabBar = paneElement.find('ul') 83 | 84 | tabBar.on 'drop', (event) => @onDropTabBar(event, pane) 85 | tabBar.on 'dragstart', (event) -> 86 | return unless event.target.item?.constructor.name is 'TabView' 87 | event.originalEvent.dataTransfer.setData 'terminal-plus-tab', 'true' 88 | pane.onDidDestroy -> tabBar.off 'drop', @onDropTabBar 89 | 90 | 91 | ### 92 | Section: Button Handlers 93 | ### 94 | 95 | closeAll: -> 96 | @core.closeAll() 97 | 98 | newTerminalView: -> 99 | @core.newTerminalView()?.toggle() 100 | 101 | 102 | ### 103 | Section: Drag and drop 104 | ### 105 | 106 | onDragStart: (event) => 107 | event.originalEvent.dataTransfer.setData 'terminal-plus-panel', 'true' 108 | 109 | element = $(event.target).closest('.status-icon') 110 | element.addClass 'is-dragging' 111 | event.originalEvent.dataTransfer.setData 'from-index', element.index() 112 | 113 | onDragLeave: (event) => 114 | @removePlaceholder() 115 | 116 | onDragEnd: (event) => 117 | @clearDropTarget() 118 | 119 | onDragOver: (event) => 120 | event.preventDefault() 121 | event.stopPropagation() 122 | unless event.originalEvent.dataTransfer.getData('terminal-plus-panel') is 'true' 123 | return 124 | 125 | newDropTargetIndex = @getDropTargetIndex(event) 126 | return unless newDropTargetIndex? 127 | @removeDropTargetClasses() 128 | statusIcons = @getStatusIcons() 129 | 130 | if newDropTargetIndex < statusIcons.length 131 | element = statusIcons.eq(newDropTargetIndex).addClass 'is-drop-target' 132 | @getPlaceholder().insertBefore(element) 133 | else 134 | element = statusIcons.eq(newDropTargetIndex - 1).addClass 'drop-target-is-after' 135 | @getPlaceholder().insertAfter(element) 136 | 137 | onDrop: (event) => 138 | {dataTransfer} = event.originalEvent 139 | panelEvent = dataTransfer.getData('terminal-plus-panel') is 'true' 140 | tabEvent = dataTransfer.getData('terminal-plus-tab') is 'true' 141 | return unless panelEvent or tabEvent 142 | 143 | event.preventDefault() 144 | event.stopPropagation() 145 | 146 | toIndex = @getDropTargetIndex(event) 147 | @clearDropTarget() 148 | 149 | if tabEvent 150 | fromIndex = parseInt(dataTransfer.getData('sortable-index')) 151 | paneIndex = parseInt(dataTransfer.getData('from-pane-index')) 152 | pane = atom.workspace.getPanes()[paneIndex] 153 | view = pane.itemAtIndex(fromIndex) 154 | 155 | pane.removeItem(view, false) 156 | view.toggleFullscreen() 157 | fromIndex = @core.length() - 1 158 | else 159 | fromIndex = parseInt(dataTransfer.getData('from-index')) 160 | @updateOrder(fromIndex, toIndex) 161 | 162 | onDropTabBar: (event, pane) => 163 | {dataTransfer} = event.originalEvent 164 | return unless dataTransfer.getData('terminal-plus-panel') is 'true' 165 | 166 | event.preventDefault() 167 | event.stopPropagation() 168 | @clearDropTarget() 169 | 170 | fromIndex = parseInt(dataTransfer.getData('from-index')) 171 | terminal = @core.terminalAt(fromIndex) 172 | terminal.getParentView().toggleFullscreen() 173 | 174 | 175 | ### 176 | Section: External Methods 177 | ### 178 | 179 | addStatusIcon: (icon) -> 180 | @statusContainer.append icon 181 | 182 | destroyContainer: -> 183 | if @container 184 | @container.destroy() 185 | @container = null 186 | 187 | getContainer: -> 188 | return @container 189 | 190 | setContainer: (container) -> 191 | @container = container 192 | return this 193 | 194 | 195 | ### 196 | Section: Helper Methods 197 | ### 198 | 199 | clearDropTarget: -> 200 | element = @find('.is-dragging') 201 | element.removeClass 'is-dragging' 202 | @removeDropTargetClasses() 203 | @removePlaceholder() 204 | 205 | removeDropTargetClasses: -> 206 | @statusContainer.find('.is-drop-target').removeClass 'is-drop-target' 207 | @statusContainer.find('.drop-target-is-after').removeClass 'drop-target-is-after' 208 | 209 | getDropTargetIndex: (event) -> 210 | target = $(event.target) 211 | return if @isPlaceholder(target) 212 | 213 | statusIcons = @statusContainer.children('.status-icon') 214 | element = target.closest('.status-icon') 215 | element = statusIcons.last() if element.length is 0 216 | 217 | return 0 unless element.length 218 | 219 | elementCenter = element.offset().left + element.width() / 2 220 | 221 | if event.originalEvent.pageX < elementCenter 222 | statusIcons.index(element) 223 | else if element.next('.status-icon').length > 0 224 | statusIcons.index(element.next('.status-icon')) 225 | else 226 | statusIcons.index(element) + 1 227 | 228 | getPlaceholder: -> 229 | @placeholderEl ?= $('
  • ') 230 | 231 | removePlaceholder: -> 232 | @placeholderEl?.remove() 233 | @placeholderEl = null 234 | 235 | isPlaceholder: (element) -> 236 | element.is('.placeholder') 237 | 238 | getStatusIcons: -> 239 | @statusContainer.children('.status-icon') 240 | 241 | moveIconToIndex: (icon, toIndex) -> 242 | followingIcon = @getStatusIcons()[toIndex] 243 | container = @statusContainer[0] 244 | if followingIcon? 245 | container.insertBefore(icon, followingIcon) 246 | else 247 | container.appendChild(icon) 248 | 249 | updateOrder: (fromIndex, toIndex) -> 250 | return if fromIndex is toIndex 251 | toIndex-- if fromIndex < toIndex 252 | 253 | icon = @getStatusIcons().eq(fromIndex).detach() 254 | @moveIconToIndex icon.get(0), toIndex 255 | @core.moveTerminal fromIndex, toIndex 256 | icon.addClass 'inserted' 257 | icon.one 'webkitAnimationEnd', -> icon.removeClass('inserted') 258 | 259 | clearStatusColor: (event) -> 260 | $(event.target).closest('.status-icon').css 'color', '' 261 | 262 | setStatusColor: (event) -> 263 | color = event.type.match(/\w+$/)[0] 264 | color = atom.config.get("terminal-plus.iconColors.#{color}").toRGBAString() 265 | $(event.target).closest('.status-icon').css 'color', color 266 | 267 | module.exports = new StatusBar() 268 | -------------------------------------------------------------------------------- /lib/status-icon.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | StatusBar = require './status-bar' 4 | 5 | module.exports = 6 | class StatusIcon extends HTMLElement 7 | active: false 8 | 9 | initialize: (@terminal) -> 10 | @classList.add 'status-icon' 11 | 12 | @icon = document.createElement('i') 13 | @icon.classList.add 'icon', 'icon-terminal' 14 | @appendChild(@icon) 15 | 16 | @name = document.createElement('span') 17 | @name.classList.add 'name' 18 | @appendChild(@name) 19 | 20 | @addEventListener 'click', ({which, ctrlKey}) => 21 | if which is 1 22 | @terminal.getParentView().toggle() 23 | return true 24 | else if which is 2 25 | @terminal.getParentView().destroy() 26 | return false 27 | 28 | @setupTooltip() 29 | @attach() 30 | 31 | attach: -> 32 | StatusBar.addStatusIcon(this) 33 | 34 | destroy: -> 35 | @removeTooltip() 36 | @mouseEnterSubscription.dispose() if @mouseEnterSubscription 37 | @remove() 38 | 39 | 40 | ### 41 | Section: Tooltip 42 | ### 43 | 44 | setupTooltip: -> 45 | onMouseEnter = (event) => 46 | return if event.detail is 'terminal-plus' 47 | @updateTooltip() 48 | 49 | @mouseEnterSubscription = dispose: => 50 | @removeEventListener('mouseenter', onMouseEnter) 51 | @mouseEnterSubscription = null 52 | 53 | @addEventListener('mouseenter', onMouseEnter) 54 | 55 | updateTooltip: -> 56 | @removeTooltip() 57 | 58 | if process = @terminal.getTitle() 59 | @tooltip = atom.tooltips.add this, 60 | title: process 61 | html: false 62 | delay: 63 | show: 1000 64 | hide: 100 65 | 66 | @dispatchEvent(new CustomEvent('mouseenter', bubbles: true, detail: 'terminal-plus')) 67 | 68 | removeTooltip: -> 69 | @tooltip.dispose() if @tooltip 70 | @tooltip = null 71 | 72 | 73 | ### 74 | Section: Name 75 | ### 76 | 77 | getName: -> @name.textContent.substring(1) 78 | 79 | setName: (name) -> 80 | name = " " + name if name 81 | @name.innerHTML = name 82 | 83 | 84 | ### 85 | Section: Active Status 86 | ### 87 | 88 | activate: -> 89 | @classList.add 'active' 90 | @active = true 91 | 92 | deactivate: -> 93 | @classList.remove 'active' 94 | @active = false 95 | 96 | toggle: -> 97 | if @active 98 | @classList.remove 'active' 99 | else 100 | @classList.add 'active' 101 | @active = !@active 102 | 103 | isActive: -> 104 | return @active 105 | 106 | 107 | ### 108 | Section: External Methods 109 | ### 110 | 111 | getTerminal: -> 112 | return @terminal 113 | 114 | getTerminalView: -> 115 | return @terminal.getParentView() 116 | 117 | hide: -> 118 | @style.display = 'none' 119 | @deactivate() 120 | 121 | show: -> 122 | @style.display = '' 123 | 124 | module.exports = document.registerElement('status-icon', prototype: StatusIcon.prototype, extends: 'li') 125 | -------------------------------------------------------------------------------- /lib/tab-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Emitter} = require 'atom' 2 | {$} = require 'atom-space-pen-views' 3 | 4 | TerminalView = require './terminal-view' 5 | 6 | module.exports = 7 | class TabView extends TerminalView 8 | opened: false 9 | lastActiveItem: null 10 | windowHeight: $(window).height() 11 | 12 | @getFocusedTerminal: -> 13 | return TerminalView.getFocusedTerminal() 14 | 15 | initialize: (options) -> 16 | super(options) 17 | @emitter = new Emitter 18 | 19 | @fullscreenBtn = @addButton 'right', @toggleFullscreen, 'screen-normal' 20 | @inputBtn = @addButton 'left', @promptForInput, 'keyboard' 21 | 22 | @subscriptions.add atom.tooltips.add @fullscreenBtn, 23 | title: 'Minimize' 24 | @subscriptions.add atom.tooltips.add @inputBtn, 25 | title: 'Insert Text' 26 | 27 | @attach() 28 | @terminal.hideIcon() 29 | @terminal.displayView() 30 | 31 | destroy: ({keepTerminal}={}) => 32 | @emitter.dispose() 33 | super(keepTerminal) 34 | 35 | 36 | ### 37 | Section: Setup 38 | ### 39 | 40 | attach: (pane, index) -> 41 | pane ?= atom.workspace.getActivePane() 42 | index ?= pane.getItems().length 43 | 44 | pane.addItem this, index 45 | 46 | detach: -> 47 | atom.workspace.paneForItem(this)?.removeItem(this, true) 48 | 49 | 50 | ### 51 | Section: External Methods 52 | ### 53 | 54 | open: => 55 | super() 56 | if pane = atom.workspace.paneForItem(this) 57 | pane.activateItem this 58 | @focus() 59 | 60 | hide: ({refocus}={}) => 61 | refocus ?= true 62 | 63 | @blur() 64 | super(refocus) 65 | 66 | getIconName: -> 67 | "terminal" 68 | 69 | getTitle: -> 70 | @terminal.getName() or "Terminal-Plus" 71 | 72 | getPath: -> 73 | return @terminal.getTitle() 74 | 75 | onDidChangeTitle: (callback) -> 76 | @emitter.on 'did-change-title', callback 77 | 78 | updateName: (name) -> 79 | @emitter.emit 'did-change-title', name 80 | 81 | toggleFullscreen: => 82 | @destroy keepTerminal: true 83 | @terminal.enableAnimation() 84 | panel = new (require './panel-view') {@terminal} 85 | panel.toggle() if @isVisible() 86 | @detach() 87 | 88 | isVisible: -> 89 | pane = atom.workspace.paneForItem(this) 90 | return false unless pane 91 | return this == pane.getActiveItem() 92 | -------------------------------------------------------------------------------- /lib/terminal-plus.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | config: require './config-schema' 3 | core: null 4 | statusBar: null 5 | 6 | activate: -> 7 | @core = require './core' 8 | @statusBar = require './status-bar' 9 | 10 | deactivate: -> 11 | @core.destroy() 12 | @statusBar.destroy() 13 | @core = null 14 | @statusBar = null 15 | 16 | consumeStatusBar: (atomStatusBar) -> 17 | atom.config.observe 'terminal-plus.core.statusBar', (value) => 18 | @statusBar.destroyContainer() 19 | 20 | switch value 21 | when "Full" 22 | @statusBar.setContainer atom.workspace.addBottomPanel { 23 | item: @statusBar 24 | priority: 100 25 | } 26 | when "Collapsed" 27 | @statusBar.setContainer atomStatusBar.addLeftTile { 28 | item: @statusBar 29 | priority: 100 30 | } 31 | -------------------------------------------------------------------------------- /lib/terminal-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {$, View} = require 'atom-space-pen-views' 3 | 4 | Terminal = require './terminal' 5 | 6 | lastActiveItem = null 7 | 8 | module.exports = 9 | class TerminalView extends View 10 | subscriptions: null 11 | core: null 12 | emitter: null 13 | animating: false 14 | 15 | @content: ({terminal, shellPath, pwd, id}) -> 16 | @div class: 'terminal-plus', => 17 | @div class: 'terminal-view', => 18 | @div class: 'panel-divider', outlet: 'panelDivider' 19 | @div class: 'btn-toolbar', outlet:'toolbar' 20 | terminal = terminal or new Terminal({shellPath, pwd, id}) 21 | @subview 'terminal', terminal.setParentView(this) 22 | 23 | @getFocusedTerminal: -> 24 | return Terminal.getFocusedTerminal() 25 | 26 | initialize: -> 27 | @subscriptions = new CompositeDisposable() 28 | @attachWindowEvents() 29 | 30 | destroy: (keepTerminal) -> 31 | @subscriptions.dispose() 32 | @terminal.destroy() if @terminal and not keepTerminal 33 | 34 | 35 | ### 36 | Section: Window Events 37 | ### 38 | 39 | attachWindowEvents: -> 40 | $(window).on 'resize', @onWindowResize 41 | 42 | detachWindowEvents: -> 43 | $(window).off 'resize', @onWindowResize 44 | 45 | onWindowResize: => 46 | @terminal.recalibrateSize() 47 | 48 | 49 | ### 50 | Section: External Methods 51 | ### 52 | 53 | focus: => 54 | @terminal?.focus() 55 | super() 56 | 57 | blur: => 58 | @terminal?.blur() 59 | super() 60 | 61 | open: => 62 | lastActiveItem ?= atom.workspace.getActiveTextEditor() 63 | 64 | hide: (refocus) => 65 | if lastActiveItem and refocus 66 | if pane = atom.workspace.paneForItem(lastActiveItem) 67 | if activeEditor = atom.workspace.getActiveTextEditor() 68 | if lastActiveItem != activeEditor 69 | lastActiveItem = activeEditor 70 | pane.activateItem lastActiveItem 71 | atom.views.getView(lastActiveItem).focus() 72 | lastActiveItem = null 73 | 74 | toggle: -> 75 | return if @isAnimating() 76 | 77 | if @isVisible() 78 | @hide() 79 | else 80 | @open() 81 | 82 | toggleFocus: -> 83 | return unless @isVisible() 84 | 85 | if @terminal.isFocused() 86 | @blur() 87 | if lastActiveItem 88 | if pane = atom.workspace.paneForItem(lastActiveItem) 89 | pane.activateItem lastActiveItem 90 | atom.views.getView(lastActiveItem).focus() 91 | lastActiveItem = null 92 | else 93 | lastActiveItem ?= atom.workspace.getActiveTextEditor() 94 | @focus() 95 | 96 | addButton: (side, onClick, icon) -> 97 | if icon.indexOf('icon-') < 0 98 | icon = 'icon-' + icon 99 | 100 | button = $("