├── .editorconfig ├── .gitignore ├── .travis.yml ├── Collections ├── C++.terminality-collections ├── C.terminality-collections ├── Lua.terminality-collections ├── Python.terminality-collections ├── Ruby.terminality-collections ├── Rust.terminality-collections ├── Swift.terminality-collections └── Terminal.terminality-collections ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Developers └── DevPlan.md ├── LICENSE ├── Main.sublime-menu ├── QuickMenu ├── Default.sublime-commands ├── QuickMenu.py ├── QuickMenu_main.py └── README.md ├── README.md ├── Terminality.sublime-project ├── Terminality.sublime-settings ├── generic_shell.py ├── macro.py ├── messages.json ├── messages ├── 0.1.1.txt ├── 0.2.0.txt ├── 0.3.0.txt ├── 0.3.1.txt ├── 0.3.10.txt ├── 0.3.2.txt ├── 0.3.3.txt ├── 0.3.4.txt ├── 0.3.5.txt ├── 0.3.6.txt ├── 0.3.7.txt ├── 0.3.8.txt ├── 0.3.9.txt └── install.txt ├── progress.py ├── settings.py ├── terminality.py ├── tests ├── run_tests.py ├── test_macro_internal.py └── test_macro_parser.py ├── unit_collections.py └── utils.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*{.py,.yml,.terminality-collections}] 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - PACKAGE="Terminality" 4 | matrix: 5 | - SUBLIME_TEXT_VERSION="3" 6 | 7 | before_install: 8 | - curl -OL https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/travis.sh 9 | 10 | install: 11 | - sh travis.sh bootstrap 12 | 13 | script: 14 | - sh travis.sh run_tests 15 | 16 | notifications: 17 | email: false 18 | webhooks: 19 | urls: 20 | - https://webhooks.gitter.im/e/53f8fe56235506742d24 21 | on_success: change # options: [always|never|change] default: always 22 | on_failure: always # options: [always|never|change] default: always 23 | on_start: false # default: false 24 | -------------------------------------------------------------------------------- /Collections/C++.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.c++": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Compile and Run $file_name with default C++ compiler", 7 | "required": ["file"], 8 | "command": "g++ $file -o $file_without_ext && $file_without_ext $arguments", 9 | "macros": { 10 | "file_without_ext": [ 11 | ["$file", ".*(?=\\.)"] 12 | ] 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Collections/C.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.c": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Compile and Run $file_name with default C++ compiler", 7 | "required": ["file"], 8 | "command": "gcc $file -o $file_without_ext && $file_without_ext $arguments", 9 | "macros": { 10 | "file_without_ext": [ 11 | ["$file", ".*(?=\\.)"] 12 | ] 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Collections/Lua.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.lua": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Run $file_name with default Lua runtime", 7 | "required": ["file"], 8 | "command": "lua $file $arguments" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Collections/Python.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.python": { 4 | "python2run": { 5 | "name": "Run $file_name", 6 | "description": "Run $file_name as Python 2 document", 7 | "required": ["file"], 8 | "command": "python -u $file $arguments" 9 | }, 10 | "python3run": { 11 | "name": "Run $file_name in python3", 12 | "description": "Run $file_name as Python 3 document", 13 | "required": ["file"], 14 | "command": "python3 -u $file $arguments" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Collections/Ruby.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.ruby": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Run $file_name with default Ruby runtime", 7 | "required": ["file"], 8 | "command": "ruby $file $arguments" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Collections/Rust.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.rust": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Compile and Run $file_name with default Rust compiler", 7 | "required": ["file"], 8 | "command": "rustc $file && $file_without_ext $arguments", 9 | "macros": { 10 | "file_without_ext": [ 11 | ["$file", ".*(?=\\.)"] 12 | ] 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Collections/Swift.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "source.swift": { 4 | "run": { 5 | "name": "Run $file_name", 6 | "description": "Run $file_name with default Swift runtime", 7 | "required": ["file"], 8 | "command": "swift $file $arguments", 9 | "platforms": ["osx"] 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Collections/Terminal.terminality-collections: -------------------------------------------------------------------------------- 1 | { 2 | "execution_units": { 3 | "*": { 4 | "terminal": { 5 | "name": "Run Terminal here", 6 | "description": "Run bash $nice_parent_name", 7 | "required": ["parent_relative", "parent"], 8 | "location": "$parent", 9 | "command": "bash --version; bash -i", 10 | "platforms": ["osx", "linux"], 11 | "no_echo": true, 12 | "macros": { 13 | "nice_parent_name": [ 14 | "at \"$parent_rel\"", 15 | "here" 16 | ], 17 | "parent_rel": [ 18 | ["$parent_relative", "(\\w+/?)+"] 19 | ] 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "terminality", 5 | "args": { 6 | "action": { 7 | "name": "main" 8 | }, 9 | "arguments": false 10 | } 11 | }, { 12 | "keys": ["ctrl+alt+shift+r"], 13 | "command": "terminality", 14 | "args": { 15 | "action": { 16 | "name": "main" 17 | }, 18 | "arguments": true 19 | } 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["super+ctrl+r"], 4 | "command": "terminality", 5 | "args": { 6 | "action": { 7 | "name": "main" 8 | }, 9 | "arguments": false 10 | } 11 | }, { 12 | "keys": ["super+ctrl+shift+r"], 13 | "command": "terminality", 14 | "args": { 15 | "action": { 16 | "name": "main" 17 | }, 18 | "arguments": true 19 | } 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+r"], 4 | "command": "terminality", 5 | "args": { 6 | "action": { 7 | "name": "main" 8 | }, 9 | "arguments": false 10 | } 11 | }, { 12 | "keys": ["ctrl+alt+shift+r"], 13 | "command": "terminality", 14 | "args": { 15 | "action": { 16 | "name": "main" 17 | }, 18 | "arguments": true 19 | } 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Terminality: Browse Commands...", 4 | "command": "terminality" 5 | }, 6 | { 7 | "caption": "Preferences: Terminality Settings – Default", 8 | "command": "open_file", "args": 9 | { 10 | "file": "${packages}/Terminality/Terminality.sublime-settings" 11 | } 12 | }, 13 | { 14 | "caption": "Preferences: Terminality Settings – User", 15 | "command": "open_file", "args": 16 | { 17 | "file": "${packages}/User/Terminality.sublime-settings" 18 | } 19 | }, 20 | { 21 | "caption": "Preferences: Terminality Key Bindings – Default", 22 | "command": "open_file", "args": 23 | { 24 | "file": "${packages}/Terminality/Default.sublime-keymap" 25 | } 26 | }, 27 | { 28 | "caption": "Preferences: Terminality Key Bindings – User", 29 | "command": "open_file", "args": 30 | { 31 | "file": "${packages}/User/Terminality.sublime-keymap" 32 | } 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /Developers/DevPlan.md: -------------------------------------------------------------------------------- 1 | ## Development Plan 2 | 3 | To do: 4 | 5 | - ReadTheDocs (more proper documentation) 6 | 7 | Features 8 | 9 | - Scope based command - command can be run only within specified scope 10 | (not only root scope) 11 | - Multi-value macros - macro which concatenate multiple values (useful on 12 | multi-argument command and also multi-selection) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ::Terminality:: 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Sirisak Lueangsaksri 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "Terminality", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "args": {"file": "${packages}/Terminality/README.md"}, 21 | "caption": "README" 22 | }, 23 | { "caption": "-" }, 24 | { 25 | "command": "open_file", 26 | "args": {"file": "${packages}/Terminality/Terminality.sublime-settings"}, 27 | "caption": "Settings – Default" 28 | }, 29 | { 30 | "command": "open_file", 31 | "args": {"file": "${packages}/User/Terminality.sublime-settings"}, 32 | "caption": "Settings – User" 33 | }, 34 | { "caption": "-" }, 35 | { 36 | "command": "open_file", 37 | "args": { 38 | "file": "${packages}/Terminality/Default (OSX).sublime-keymap", 39 | "platform": "OSX" 40 | }, 41 | "caption": "Key Bindings – Default" 42 | }, 43 | { 44 | "command": "open_file", 45 | "args": { 46 | "file": "${packages}/Terminality/Default (Linux).sublime-keymap", 47 | "platform": "Linux" 48 | }, 49 | "caption": "Key Bindings – Default" 50 | }, 51 | { 52 | "command": "open_file", 53 | "args": { 54 | "file": "${packages}/Terminality/Default (Windows).sublime-keymap", 55 | "platform": "Windows" 56 | }, 57 | "caption": "Key Bindings – Default" 58 | }, 59 | { 60 | "command": "open_file", 61 | "args": { 62 | "file": "${packages}/User/Default (OSX).sublime-keymap", 63 | "platform": "OSX" 64 | }, 65 | "caption": "Key Bindings – User" 66 | }, 67 | { 68 | "command": "open_file", 69 | "args": { 70 | "file": "${packages}/User/Default (Linux).sublime-keymap", 71 | "platform": "Linux" 72 | }, 73 | "caption": "Key Bindings – User" 74 | }, 75 | { 76 | "command": "open_file", 77 | "args": { 78 | "file": "${packages}/User/Default (Windows).sublime-keymap", 79 | "platform": "Windows" 80 | }, 81 | "caption": "Key Bindings – User" 82 | }, 83 | { "caption": "-" } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /QuickMenu/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "QuickMenu: Example Code", 4 | "command": "open_file", 5 | "args": { 6 | "file": "${packages}/QuickMenu/QuickMenu_main.py" 7 | } 8 | }, 9 | { 10 | "caption": "QuickMenu: Demo...", 11 | "command": "quick_menu" 12 | }, 13 | { 14 | "caption": "QuickMenu: Submenu > Dialogs", 15 | "command": "quick_menu", 16 | "args": { 17 | "action": { 18 | "name": "dialogs", 19 | } 20 | } 21 | }, 22 | { 23 | "caption": "QuickMenu: Submenu > Items", 24 | "command": "quick_menu", 25 | "args": { 26 | "action": { 27 | "name": "items", 28 | } 29 | } 30 | }, 31 | { 32 | "caption": "QuickMenu: Submenu > Commands", 33 | "command": "quick_menu", 34 | "args": { 35 | "action": { 36 | "name": "commands", 37 | } 38 | } 39 | }, 40 | { 41 | "caption": "QuickMenu: Select > Item 2 on Commands Menu", 42 | "command": "quick_menu", 43 | "args": { 44 | "action": { 45 | "name": "commands", 46 | "item": 2 47 | } 48 | } 49 | }, 50 | { 51 | "caption": "QuickMenu: Command > Hello, World!", 52 | "command": "quick_menu", 53 | "args": { 54 | "action": { 55 | "command": "message_dialog", 56 | "args": "Hello, World!" 57 | } 58 | } 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /QuickMenu/QuickMenu.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ::QuickMenu:: 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2014 Sirisak Lueangsaksri 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | import copy 28 | import sublime 29 | 30 | 31 | class QuickMenu: 32 | settings = { 33 | "menu": {}, 34 | "max_level": 50, 35 | "silent": True, 36 | "save_selected": True 37 | } 38 | tmp = { 39 | "menu": None, 40 | "select": None, 41 | "window": None, 42 | "callback": None, 43 | "sublime": True, 44 | "level": 0 45 | } 46 | 47 | def __init__(self, menu=None, silent=True, save_selected=True, max_level=50): 48 | menu = menu or {} 49 | self.settings["menu"] = copy.deepcopy(menu) 50 | self.settings["max_level"] = max_level 51 | self.settings["silent"] = silent 52 | self.settings["save_selected"] = save_selected 53 | 54 | def set(self, key, value): 55 | self.settings[key] = value 56 | 57 | def setMenu(self, name, menu): 58 | self.settings["menu"][name] = copy.deepcopy(menu) 59 | 60 | def setItems(self, menu, items, actions): 61 | if menu in self.settings["menu"] and "items" in self.settings["menu"][menu] and "actions" in self.settings["menu"][menu]: 62 | self.settings["menu"][menu]["items"] = copy.deepcopy(items) 63 | self.settings["menu"][menu]["actions"] = copy.deepcopy(actions) 64 | 65 | def addItems(self, menu, items, actions): 66 | if menu in self.settings["menu"] and "items" in self.settings["menu"][menu] and "actions" in self.settings["menu"][menu]: 67 | self.settings["menu"][menu]["items"] += copy.deepcopy(items) 68 | self.settings["menu"][menu]["actions"] += copy.deepcopy(actions) 69 | 70 | def insertItem(self, menu, index, item, action): 71 | if menu in self.settings["menu"] and "items" in self.settings["menu"][menu] and "actions" in self.settings["menu"][menu]: 72 | self.settings["menu"][menu]["items"].insert(index, copy.deepcopy(item)) 73 | self.settings["menu"][menu]["actions"].insert(index, copy.deepcopy(action)) 74 | if "selected_index" in self.settings["menu"][menu] and self.settings["menu"][menu]["selected_index"] > index: 75 | self.settings["menu"][menu]["selected_index"] += 1 76 | 77 | def setSelectedIndex(self, menu, index): 78 | if menu in self.settings["menu"] and "selected_index" in self.settings["menu"][menu]: 79 | self.settings["menu"][menu]["selected_index"] = index 80 | 81 | def show(self, window=None, on_done=None, menu=None, action=None, flags=0, on_highlight=None, level=0): 82 | selected_index = -1 83 | if window is None and self.tmp["window"] is not None: 84 | window = self.tmp["window"] 85 | self.tmp["window"] = None 86 | if window is None and not self.settings["silent"]: 87 | sublime.message_dialog("No window to show") 88 | return 89 | if on_done is None and self.tmp["callback"] is not None: 90 | on_done = self.tmp["callback"] 91 | if self.settings["menu"] is None or "main" not in self.settings["menu"] or "items" not in self.settings["menu"]["main"]: 92 | if not self.settings["silent"]: 93 | sublime.message_dialog("No menu to show") 94 | return 95 | if menu is None and self.tmp["menu"] is not None: 96 | menu = self.tmp["menu"] 97 | self.tmp["menu"] = None 98 | if menu is None or "items" not in menu: 99 | menu = self.settings["menu"]["main"] 100 | if action is None and self.tmp["select"] is not None: 101 | action = self.tmp["select"] 102 | self.tmp["select"] = None 103 | if action is not None: 104 | if "name" in action: 105 | if action["name"] not in self.settings["menu"]: 106 | if not self.settings["silent"]: 107 | sublime.message_dialog("No menu found") 108 | return 109 | menu = self.settings["menu"][action["name"]] 110 | if "item" in action and "actions" in menu: 111 | if level >= self.settings["max_level"]: 112 | if not self.settings["silent"]: 113 | sublime.message_dialog("Seem like menu go into too many levels now...") 114 | return 115 | if len(menu["actions"]) < action["item"]: 116 | if not self.settings["silent"]: 117 | sublime.message_dialog("Invalid menu selection") 118 | return 119 | self.tmp["sublime"] = False 120 | self.show(window, on_done, menu, menu["actions"][action["item"]-1], flags, on_highlight, level+1) 121 | return 122 | elif "command" in action: 123 | if "args" in action: 124 | if action["command"] == "message_dialog": 125 | sublime.message_dialog(action["args"]) 126 | elif action["command"] == "error_dialog": 127 | sublime.error_message(action["args"]) 128 | else: 129 | sublime.active_window().run_command(action["command"], action["args"]) 130 | else: 131 | sublime.active_window().run_command(action["command"]) 132 | return 133 | elif not self.settings["silent"]: 134 | sublime.message_dialog("No action assigned") 135 | return 136 | else: 137 | return 138 | if "selected_index" in menu and menu["selected_index"] > 0: 139 | selected_index = menu["selected_index"]-1 140 | if self.settings["save_selected"] and "previous_selected_index" in menu and menu["previous_selected_index"] >= 0: 141 | selected_index = menu["previous_selected_index"] 142 | self.tmp["menu"] = menu 143 | self.tmp["window"] = window 144 | self.tmp["callback"] = on_done 145 | self.tmp["level"] = self.tmp["level"]+1 146 | window.show_quick_panel(menu["items"], self.select, flags, selected_index, on_highlight) 147 | 148 | def select(self, index=-1): 149 | if self.tmp["callback"] is not None: 150 | self.tmp["callback"]({"index": index, "level": self.tmp["level"], "from_sublime": self.tmp["sublime"], "items": self.tmp["menu"]["items"]}) 151 | if index < 0: 152 | self.tmp["menu"] = None 153 | self.tmp["select"] = None 154 | self.tmp["window"] = None 155 | self.tmp["callback"] = None 156 | self.tmp["sublime"] = True 157 | self.tmp["level"] = 0 158 | return 159 | if "actions" in self.tmp["menu"] and len(self.tmp["menu"]["actions"]) > index: 160 | self.tmp["menu"]["previous_selected_index"] = index 161 | self.tmp["select"] = self.tmp["menu"]["actions"][index] 162 | self.tmp["sublime"] = False 163 | sublime.set_timeout(self.show, 50) 164 | -------------------------------------------------------------------------------- /QuickMenu/QuickMenu_main.py: -------------------------------------------------------------------------------- 1 | # QuickMenu Example 2 | # Type 'QuickMenu' in command pallete to see some possible commands 3 | # 4 | # QuickMenu Example by: spywhere 5 | # Please give credit to me! 6 | 7 | 8 | from QuickMenu.QuickMenu import * 9 | import sublime_plugin 10 | 11 | 12 | # Using it within WindowCommand or any command type you want 13 | class QuickMenuCommand(sublime_plugin.WindowCommand): 14 | # A variable to store a QuickMenu instance 15 | qm = None 16 | # An example menu 17 | menu = { 18 | # Startup menu 19 | "main": { 20 | # Its items 21 | "items": [["Dialogs...", "All dialog items"], ["Items...", "Do action on item"], ["Commands...", "Run command"]], 22 | # Item's actions 23 | "actions": [ 24 | { 25 | # Redirect to "dialogs" submenu 26 | "name": "dialogs" 27 | }, { 28 | # Redirect to "items" submenu 29 | "name": "items" 30 | }, { 31 | # Redirect to "commands" submenu 32 | "name": "commands" 33 | } 34 | ] 35 | }, 36 | # Custom menu named "dialogs" 37 | "dialogs": { 38 | # Selected second item as default 39 | "selected_index": 2, 40 | "items": [["Back", "Back to previous menu"], ["Message Dialog", "Hello, World on Message Dialog"], ["Error Dialog", "Hello, World on Error Dialog"]], 41 | "actions": [ 42 | { 43 | "name": "main", 44 | }, { 45 | # This will select "Message Dialog command" on "commands" menu 46 | "name": "commands", 47 | "item": 2 48 | }, { 49 | "name": "commands", 50 | "item": 3 51 | } 52 | ] 53 | }, 54 | "items": { 55 | "selected_index": 2, 56 | "items": [["Back", "Back to previous menu"], ["Item 2 on Dialogs", "Select item 2 in Dialogs"], ["Item 3 on Dialogs", "Select item 3 in Dialogs"], ["Item 4 on Commands", "Select item 4 in Commands"]], 57 | "actions": [ 58 | { 59 | "name": "main", 60 | }, { 61 | "name": "dialogs", 62 | "item": 2 63 | }, { 64 | "name": "dialogs", 65 | "item": 3 66 | }, { 67 | "name": "commands", 68 | "item": 4 69 | } 70 | ] 71 | }, 72 | "commands": { 73 | "selected_index": 2, 74 | "items": [["Back", "Back to previous menu"], ["Message Dialog command", "Hello, World on Message Dialog"], ["Error Dialog command", "Hello, World on Error Dialog"], ["Custom command", "Open User's settings file"]], 75 | "actions": [ 76 | { 77 | "name": "main", 78 | }, { 79 | # Show a message dialog 80 | "command": "message_dialog", 81 | "args": "Message: Hello, World" 82 | }, { 83 | # Show an error dialog 84 | "command": "error_dialog", 85 | "args": "Error: Hello, World" 86 | }, { 87 | # Run custom command 88 | "command": "open_file", 89 | "args": { 90 | "file": "${packages}/User/Preferences.sublime-settings" 91 | } 92 | } 93 | ] 94 | } 95 | } 96 | 97 | # This method receive a passing menu and action which can be use like this in keymap or other part of your package 98 | # 99 | # "command": "quick_menu", 100 | # "args": { 101 | # "action": { 102 | # "name": "main" 103 | # } 104 | # } 105 | # 106 | # or custom menu on the go! 107 | # 108 | # "command": "quick_menu", 109 | # "args": { 110 | # "menu": { 111 | # "main": { 112 | # //Blah Blah 113 | # } 114 | # } 115 | # } 116 | # 117 | def run(self, menu=None, action=None): 118 | # If QuickMenu is not instantiated yet 119 | if self.qm is None: 120 | # If passing menu is not None 121 | if menu is not None: 122 | # Set it 123 | self.menu = menu 124 | # Instantiate QuickMenu with menu from self.menu 125 | self.qm = QuickMenu(self.menu) 126 | # Show the menu on self.window and pass on_done to self.select with passing menu and action 127 | # More API documentation on README file 128 | self.qm.show(self.window, self.select, menu, action) 129 | 130 | def select(self, info): 131 | # if selected item's index is less than 0 (cancel menu selection) 132 | if info["index"] < 0: 133 | # Open console to see these messages (View > Show Console) 134 | print("Exit menu level " + str(info["level"]) + " and is from sublime: " + str(info["from_sublime"])) 135 | else: 136 | # items = menu's items 137 | # index = item's index 138 | # level = menu level (this is used to prevent self recursion menu) 139 | # from_sublime = is selected item comes from menu opened by sublime? 140 | print("Select item \"" + str(info["items"][info["index"]]) + "\" at menu level " + str(info["level"]) + " and is from sublime: " + str(info["from_sublime"])) 141 | -------------------------------------------------------------------------------- /QuickMenu/README.md: -------------------------------------------------------------------------------- 1 | ## QuickMenu 2 | 3 | A quick panel utility for Sublime Text 3 packages 4 | 5 | ![QuickMenu](http://spywhere.github.io/images/QuickMenu.png) 6 | 7 | ### Features 8 | * Submenu, Redirecting items 9 | * Dynamic menu system 10 | 11 | ### Installation and Package Integration 12 | Place this file inside `QuickMenu` folder in your package project and import into your package by using `from QuickMenu.QuickMenu import *` or similar. 13 | 14 | You should include all QuickMenu files in your project since this packages is only for package developer. 15 | 16 | **IMPORTANT: QuickMenu is not update itself when use. Please check this repository for an update.** 17 | 18 | ### Setup Menus 19 | To setup a menu, you will need a menu and variable to store an instance of QuickMenu. 20 | 21 | qm = None 22 | 23 | To create a menu just using Dict with one element with your menu name as a key with a list of item inside. 24 | 25 | menu = { 26 | "": { 27 | "items": 28 | } 29 | } 30 | 31 | and when you are ready to show it just using... 32 | 33 | if self.qm is None: 34 | self.qm = QuickMenu(self.menu) 35 | self.qm.show(self.window) # for WindowCommand 36 | # or 37 | self.qm.show(sublime.active_window()) # for another type of command 38 | 39 | **IMPORTANT: Every menu must have "main" as a startup menu** 40 | 41 | ### Setup Menu Interaction 42 | Once you have a menu to display, you will need some interactions with it, like go to submenu or run commands. To make items interactible, add a new list named "actions" into your menu. 43 | 44 | menu = { 45 | "": { 46 | "items": 47 | "actions": [] 48 | } 49 | } 50 | 51 | and then add an action order by your item's index. (Action format see below) 52 | 53 | ### Action Format 54 | Item action can be use in many ways. These are all possible actions can be used by your items... 55 | 56 | * Open a submenu 57 | * Select an item from other submenu (redirect) 58 | * Show a message dialog 59 | * Show an error dialog 60 | * Run command (with arguments) 61 | 62 | #### Submenu 63 | To make items go to a submenu, use a Dict with string named "name"... 64 | 65 | { 66 | "name": "" 67 | } 68 | 69 | #### Redirecting 70 | To make items redirect itself to another item on the same or different submenu, use a Dict with following format... 71 | 72 | { 73 | "name": "", 74 | "item": 75 | } 76 | 77 | #### Message Dialog 78 | To make items show a message dialog, use a Dict with following format... 79 | 80 | { 81 | "command": "message_dialog", 82 | "args": "" 83 | } 84 | 85 | #### Error Dialog 86 | To make items show an error dialog, use a Dict with following format... 87 | 88 | { 89 | "command": "error_dialog", 90 | "args": "" 91 | } 92 | 93 | #### Run Command 94 | To make items run a command, use a Dict with following format... 95 | 96 | { 97 | "command": "", 98 | "args": 99 | } 100 | 101 | ### Example 102 | You can see and try an example of QuickMenu within file named "QuickMenu_main.py" or type `QuickMenu: Example Code` in command pallete. 103 | 104 | ### API 105 | #### Constructor 106 | QuickMenu(defaultMenu=[], silentMode=False, saveSelected=True, maxRecursionLevel=50) 107 | * defaultMenu 108 | * Default menu Dict to display (starts with menu named "main") 109 | * silentMode 110 | * Set to 'False' to show all messages if invalid item or invalid menu is selected 111 | * saveSelected 112 | * Set to 'True' to remember selected item 113 | * maxRecursionLevel 114 | * Stop the self recursion menu if it goes deep to this number. 115 | 116 | #### Methods 117 | ##### Set a new default value 118 | set(key, value) 119 | * key 120 | * A key to set ("menu" to set a new defaultMenu, "silent" to set a new silentMode or "max_level" to set a new maxRecursionLevel) 121 | * value 122 | * A new value to be set 123 | 124 | ##### Set a new menu 125 | setMenu(name, menu) 126 | * name 127 | * A name of menu to be set 128 | * menu 129 | * A new menu to be set 130 | 131 | ##### Set items 132 | setItem(menu, items, actions) 133 | * menu 134 | * A name of menu to be set 135 | * items 136 | * Items list to be set 137 | * actions 138 | * Action list to be set with items 139 | ##### Add items 140 | addItems(menu, items, actions) 141 | * menu 142 | * A name of menu to be set 143 | * items 144 | * Items list to be set 145 | * actions 146 | * Action list to be set with items 147 | 148 | ##### Show a menu 149 | show(window=None, on_done=None, menu=None, action=None, flags=0, on_highlight=None) 150 | * window 151 | * A window to show quick panel 152 | * on_done 153 | * A callback when item is selected (must received one argument as a Dict) 154 | * menu 155 | * A passing menu 156 | * action 157 | * A passing action 158 | * flags 159 | * A quick panel's flags 160 | * on_highlight 161 | * A callback when highlight an item 162 | 163 | ### License 164 | 165 | ::QuickMenu:: 166 | 167 | The MIT License (MIT) 168 | 169 | Copyright (c) 2014 Sirisak Lueangsaksri 170 | 171 | Permission is hereby granted, free of charge, to any person obtaining a copy 172 | of this software and associated documentation files (the "Software"), to deal 173 | in the Software without restriction, including without limitation the rights 174 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 175 | copies of the Software, and to permit persons to whom the Software is 176 | furnished to do so, subject to the following conditions: 177 | 178 | The above copyright notice and this permission notice shall be included in all 179 | copies or substantial portions of the Software. 180 | 181 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 182 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 183 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 184 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 185 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 186 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 187 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Terminality 2 | 3 | A Sublime Text 3 Plugin for Sublime Text's Internal Console 4 | 5 | Branch|[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spywhere/Terminality?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)|[![Release](https://img.shields.io/github/release/spywhere/Terminality.svg?style=flat)](https://github.com/spywhere/Terminality/releases) 6 | :---:|:---:|:---: 7 | release|[![Build Status](https://img.shields.io/travis/spywhere/Terminality/release.svg?style=flat)](https://travis-ci.org/spywhere/Terminality)|[![Issues](https://img.shields.io/github/issues/spywhere/Terminality.svg?style=flat)](https://github.com/spywhere/Terminality/issues) 8 | master (develop)|[![Build Status](https://img.shields.io/travis/spywhere/Terminality/master.svg?style=flat)](https://travis-ci.org/spywhere/Terminality)|[![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](https://github.com/spywhere/Terminality/blob/master/LICENSE) 9 | 10 | ![Plugin in Action](http://spywhere.github.io/images/terminality/Terminality.gif) 11 | 12 | ### What is Terminality 13 | Terminality is a plugin to allows Sublime Text to be used as Terminal. This included input and output from/to Sublime Text's buffer. Although Terminality can run many commands, it **is not gurranteed** that it can be used for all commands. 14 | 15 | The command is language-based. Current version support the following languages... 16 | 17 | - C 18 | - Compile and Run 19 | - C++ 20 | - Compile and Run 21 | - Lua 22 | - Run 23 | - Python 24 | - Run as Python 2 (`python` command) 25 | - Run as Python 3 (`python3` command) 26 | - Rust (thanks to @divinites) 27 | - Run 28 | - Ruby 29 | - Run 30 | - Swift (OS X only) 31 | - Run 32 | 33 | Is that it? No, Terminality allows you to add your own commands to be used inside Sublime Text. Please see the section belows for more informations. 34 | 35 | ### How to use it? 36 | Just pressing `Ctrl+Key+R` and the menu will show up, let's you select which command to run. 37 | 38 | If you want to pass arguments to command (depends on how each command use the arguments), pressing `Ctrl+Key+Shift+R` instead. This will let's you select the command first, then ask you for arguments input. 39 | 40 | `Key` is `Alt` in Windows, Linux, `Cmd` in OS X 41 | 42 | **Note!** This key binding is conflicted with SFTP. You might have to override it yourself. 43 | 44 | ### Settings 45 | Terminality is using a very complex settings system in which you can control how settings affect the whole Sublime Text or each project you want. 46 | 47 | As you might already know, you can override default settings by set the desire settings inside user's settings file (can be access via `Preferences > Packages Settings > Terminality > Settings - User`). 48 | 49 | But if you want to override the settings for particular project, you can add the `terminality` dictionary to the .sublime-project file. Under this dictionary, it works like a user's settings file but for that project instead. 50 | 51 | To summarize, Terminality will look for any settings in your project settings file first, then user's settings file, and finally, the default package settings. 52 | 53 | ### How Terminality can helps my current workflow? 54 | Good question! You might think Terminality is just a plugin that showing a list of commands which you already know how to use it. Sure, that what it is under the hood but Terminality does not stop there. Here are the list of somethings that Terminality can do for you... 55 | 56 | - Run tests on your project 57 | - Exclusively build and run your project without affecting another project 58 | - Dynamically run Sublime Text's commands based on your project or current file 59 | - One keystroke project deployment 60 | - And much more... 61 | 62 | ### How can I created my own command to be used with Terminality? 63 | You can create your own command to be used with Terminality by override the commands in which using only `execution_units` key in the settings. 64 | 65 | Or if you want to create and share some of your Terminality commands, use the Collections (See Collections section belows). 66 | 67 | > In v0.3.7 or earlier, you must set it in `additional_execution_units` instead. 68 | > In v0.3.8 or later, `additional_execution_units` is deprecated and will be removed in v0.4.0 69 | 70 | ```javascript 71 | // Settings file 72 | { 73 | // ... Your other settings ... 74 | "execution_units": { 75 | // ... See Language Scopes section belows ... 76 | }, 77 | // ... Your other settings ... 78 | } 79 | ``` 80 | 81 | #### Language Scopes 82 | Terminality use Sublime Text's syntax language scope in which you can look it up at the status bar when pressing `Ctrl+Alt+Shift+P` (Windows and Linux) and `Ctrl+Shift+P` (OSX). 83 | 84 | Language Scope section is a dictionary contains commands which will available for specified language scope. 85 | 86 | The key of the language scope is simply a scope name you want to specified. If you want the command to be available to all language just simply use the `*` as a language scope. 87 | 88 | You cannot override the default language scope. However, you can remove the default commands for that language scope by set the value to non-dictionary type (such as `0`). 89 | 90 | ```javascript 91 | "": { 92 | // ... See Commands section belows ... 93 | } 94 | ``` 95 | 96 | #### Commands 97 | Command section is a dictionary contains informations about how to run the command. 98 | 99 | The key of the command is simply a command name you want to use as a command reference (this will also be used as command name if you did not specified the `name` key). 100 | 101 | You can override the command by use the exactly same command reference name of the command you want to override (included default one). And you can also remove the commands by set the value to non-dictionary type (such as `0`). 102 | 103 | Each key is optional (exceptions in the Limitations/Rules section belows) and has the following meaning... 104 | 105 | - `name` [macros string] A name of the command (which showing in the menu). 106 | - `description` [macros string] A description of the command (which show as subtitle in the menu). 107 | - `order` [string] A string which used for sorting menus 108 | - `location` [macros string] A location path to run the command 109 | - `required` [list] A list of macro name (without $) that have to be set before run the command (if any of the macro is not set, command will not run). 110 | - `arguments` [string] A text to show when ask for arguments input. 111 | - `command` [macros string] A macros string define the command that will be run. 112 | - `window_command` [macros string] A macros string define the Sublime Text's window command (included any plugin you installed) that will be run. 113 | - `view_command` [macros string] A macros string define the Sublime Text's view command (command in which only run within a view) that will be run. 114 | - `args` [dict] A dictionary that will be passed to `window_command` or `view_command`. Each macro inside the dictionary's value will be parsed recursively. 115 | - `platforms` [list] A list of supported platforms. In `-`, `` or `` format (`os` and `arch` are from Sublime Text's `sublime.platform()` and `sublime.arch()` command). 116 | - `no_echo` [bool] Specify whether input will be echo (`false`) or not (`true`). 117 | - `read_only` [bool] Specify whether running view can receives input from user (`false`) or not (`true`). 118 | - `close_on_exit` [bool] Specify whether running view will be closed when command is terminated (`true`) or not (`false`). 119 | - `macros` [dict] A dictionary contains custom macro definitions. See Custom Macros section belows. 120 | 121 | ```javascript 122 | "": { 123 | "name": "", 124 | "description": " command", 125 | "order": "" 126 | "location": "$working", 127 | "required": [], 128 | "arguments": "Arguments", 129 | // You can use only one of "command", "window_command" or "view_command" 130 | "command": "", 131 | "window_command": "", 132 | "view_command": "", 133 | // "args" will only use with "window_command" and "view_command" 134 | "args": {}, 135 | "platforms": [], 136 | "no_echo": false, 137 | "read_only": false, 138 | "close_on_exit": false, 139 | "macros": {} 140 | } 141 | ``` 142 | 143 | ##### Limitations/Rules 144 | 145 | - Every macro name (except inside `required`) should have `$` prefix. 146 | - Each action must contains only one of `command`, `window_command` and `view_command` (other can be omitted) 147 | - `location`, `no_echo`, `read_only` and `close_on_exit` only works with `command` only 148 | - `args` only works with `window_command` or `view_command` only 149 | 150 | See example inside Terminality's user settings file (and also in Terminality's .sublime-project file itself!). 151 | 152 | ### Predefined Macros 153 | 154 | - `file`: Path to current working file 155 | `file_relative`: Relative path of `file` 156 | - `file_name`: Name of `file` 157 | - `working`: This will use `working_project` but if not found it will use `project` and if still not found it will use `parent` 158 | `working_relative`: Relative path of `working` 159 | - `working_name`: Name of `working` 160 | - `working_project`: Project folder contains current working file 161 | `working_project_relative`: Relative path of `working_project` 162 | - `working_project_name`: Name of `working_project` 163 | - `project`: First project folder 164 | `project_relative`: Relative path of `project` 165 | - `project_name`: Name of `project` 166 | - `parent`: Parent folder contains current working file 167 | `parent_relative`: Relative path of `parent` 168 | - `parent_name`: Name of `parent` 169 | - `packages_path`: Path to Sublime Text's packages folder 170 | - `raw_selection`: Raw text of last selection 171 | - `selection`: Striped text of last selection 172 | - `arguments`: A text which passed via arguments input 173 | - `sep`: Path separator (`/` or `\` depends on your operating system) 174 | - `$`: `$` symbol 175 | 176 | ### Custom Macros 177 | You can create your own macro to be used with custom command by adding each macro to `macros` section in your execution unit (See Commands aboves). Each macro is a key-value pairs in which key indicated macro name (`a-zA-Z0-9_` without prefixed `$`) and value is a list of any combination of the following values... 178 | 179 | - `"String and/or $macro"` This will be a parsed string (which must not contains self-recursion) if previous value is not found 180 | - `["Start:End"]` This will substring the previous value (if any) from `start` to `end` 181 | - `["RegEx Pattern", ]` This will return a specified group (or default match if not specified) from previous value matching 182 | - `["String and/or $macro", "Start:End"]` This will substring the specified string/macro from `start` to `end` if previous value is not found 183 | - `["String and/or $macro", "RegEx Pattern", ]` This will return a specified group (or default match if not specified) from specified string/macro matching if previous value is not found 184 | 185 | Substring works just like Python's substring. You can omitted `start` or `end` as you like. 186 | 187 | Macro works as a sequence, if current macro is not found it will look at the next macro. 188 | 189 | Example: 190 | 191 | ```javascript 192 | "CustomMacro": [ 193 | "$file_name", // Get the file name from predefined macro 194 | [":-4"], // Remove the last 4 characters (if value is found) 195 | ["$file", "\\w+"], // Get the result from parsing "$file" with RegEx if previous value is not found 196 | "" // If nothing can be used, use empty string 197 | ] 198 | ``` 199 | 200 | ### Collections 201 | 202 | > *Implemented in v0.3.8 and later* 203 | 204 | Collections is a package contains Terminality commands which can be install by place in the `User` directory. 205 | 206 | The structure of Collections file is simply a JSON file with .terminality-collections extension. Inside the file contains the following format... 207 | 208 | ```javascript 209 | { 210 | "execution_units": { 211 | // ... See Language Scopes section aboves ... 212 | } 213 | } 214 | ``` 215 | 216 | Please note that Collections is not a settings file (although it contains the same key name). Any other key will be ignored completely. 217 | 218 | ### Contributors 219 | - @utensil 220 | - @zmLGBBM 221 | - @divinites 222 | -------------------------------------------------------------------------------- /Terminality.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": "." 7 | } 8 | ], 9 | "terminality": { 10 | "run_if_only_one_available": false, 11 | "additional_execution_units": { 12 | "source.python": { 13 | "python2run": 0, 14 | "python3run": 0 15 | }, 16 | "*": { 17 | "terminal": 0, 18 | "unittest": { 19 | "name": "Run $working_name Tests", 20 | "description": "Run unit testing on $working_name project", 21 | "window_command": "unit_testing", 22 | "args": { 23 | "package": "", 24 | "output": "panel" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Terminality.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Enable debug mode 3 | "debug": false, 4 | 5 | // Additional execution units 6 | // This will be merged with default execution units 7 | // See format in README.md file 8 | "execution_units": { 9 | "example_language_scope": { 10 | "just_a_random_command": { 11 | "name": "Random Command", 12 | "description": "An example command", 13 | "location": "$custom", 14 | "command": "executable_to_run $file", 15 | "macros": { 16 | "custom": [ 17 | "$parent", 18 | "$project", 19 | "$working" 20 | ] 21 | } 22 | }, 23 | // This will remove "a_very_long_command" command 24 | // in the "example_language_scope" scope 25 | "a_very_long_command": 0 26 | }, 27 | // This will remove any previous commands for "a_very_long_language_scope" 28 | "a_very_long_language_scope": 0 29 | }, 30 | 31 | // Set to "false" to show menu if no command is available 32 | "show_nothing_if_nothing": true, 33 | 34 | // Set to "true" to run command if there is only one command available 35 | "run_if_only_one_available": true, 36 | 37 | // Amount of lines from bottom to snap into autoscrolling area 38 | // Increase this number if autoscroll is not working properly 39 | "autoscroll_snap_range": 1, 40 | 41 | // Always scroll view to bottom in output window 42 | "autoscroll_to_bottom": true, 43 | 44 | // Refresh rate for Terminal (in second) 45 | // Increase this value can helps output to print more smoothly but 46 | // it's also affect the system performances 47 | "refresh_interval": 0.01, 48 | 49 | // The encoding to handle input/output of the invoked process 50 | // Using the same format as str.encode() in Python 3 used 51 | "encoding": "UTF-8", 52 | 53 | // Error handler on output encoding 54 | // ignore = Remove all invalid encoding character 55 | // replace = Replace all invalid encoding character with "?" symbol 56 | // Change this value without knowing what you're doing is not a good idea 57 | "encoding_handle": "replace" 58 | } 59 | -------------------------------------------------------------------------------- /generic_shell.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import os 3 | import sys 4 | import threading 5 | import subprocess 6 | from time import time, sleep 7 | from .settings import Settings 8 | 9 | 10 | """ 11 | Generic shell implementations, derived from Sublime Text's exec command 12 | implementation 13 | """ 14 | 15 | 16 | class GenericShell(threading.Thread): 17 | def __init__(self, cmds, view, on_complete=None, no_echo=False, 18 | read_only=False, to_console=False, params=None): 19 | self.params = params 20 | self.cmds = cmds 21 | self.on_complete = on_complete 22 | self.no_echo = no_echo 23 | self.read_only = read_only 24 | self.view = view 25 | self.to_console = to_console 26 | self.cwd = None 27 | threading.Thread.__init__(self) 28 | 29 | def set_cwd(self, path=""): 30 | self.cwd = path 31 | 32 | def popen(self, cmd, cwd): 33 | if not os.path.exists(cwd): 34 | cwd = os.path.expanduser('~') 35 | if sys.platform == "win32": 36 | return subprocess.Popen( 37 | cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 38 | stderr=subprocess.STDOUT, cwd=cwd, shell=True 39 | ) 40 | elif sys.platform == "darwin": 41 | return subprocess.Popen( 42 | ["/bin/bash", "-l", "-c", cmd], stdin=subprocess.PIPE, 43 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, 44 | shell=False 45 | ) 46 | elif sys.platform == "linux": 47 | return subprocess.Popen( 48 | ["/bin/bash", "-c", cmd], stdin=subprocess.PIPE, 49 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, 50 | shell=False 51 | ) 52 | else: 53 | return subprocess.Popen( 54 | cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 55 | stderr=subprocess.STDOUT, cwd=cwd, shell=False 56 | ) 57 | 58 | def kill(self, proc): 59 | if sys.platform == "win32": 60 | startupinfo = subprocess.STARTUPINFO() 61 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 62 | subprocess.Popen( 63 | "taskkill /F /PID %s /T" % (str(proc.pid)), 64 | startupinfo=startupinfo 65 | ) 66 | else: 67 | proc.terminate() 68 | 69 | def read_stdout(self): 70 | while True: 71 | data = os.read(self.proc.stdout.fileno(), 512) 72 | if len(data) > 0: 73 | _, layout_height = self.view.layout_extent() 74 | _, viewport_height = self.view.viewport_extent() 75 | viewport_posx, viewport_posy = self.view.viewport_position() 76 | decoded_data = data.decode( 77 | Settings.get("encoding"), 78 | Settings.get("encoding_handle") 79 | ).replace("\r\n", "\n") 80 | self.view.set_read_only(False) 81 | self.view.run_command( 82 | "terminality_utils", 83 | {"util_type": "add", "text": decoded_data} 84 | ) 85 | self.view.set_read_only(self.read_only) 86 | self.old_data += decoded_data 87 | if (Settings.get("autoscroll_to_bottom") and 88 | viewport_posy >= (layout_height - viewport_height - 89 | Settings.get("autoscroll_snap_range"))): 90 | _, layout_height = self.view.layout_extent() 91 | self.view.set_viewport_position( 92 | (viewport_posx, layout_height - viewport_height), 93 | False 94 | ) 95 | if self.to_console: 96 | print(decoded_data) 97 | elif self.proc.poll() is not None: 98 | break 99 | sleep(Settings.get("refresh_interval")) 100 | self.isReadable = False 101 | 102 | def read_stdin(self): 103 | while self.proc.poll() is None: 104 | # If input make output less than before, reset it 105 | if len(self.old_data) > self.view.size(): 106 | self.view.run_command( 107 | "terminality_utils", 108 | {"util_type": "clear"} 109 | ) 110 | self.view.run_command( 111 | "terminality_utils", 112 | {"util_type": "add", "text": self.old_data} 113 | ) 114 | elif len(self.old_data) < self.view.size(): 115 | self.data_in = self.view.substr( 116 | sublime.Region(len(self.old_data), self.view.size()) 117 | ) 118 | if "\n" in self.data_in: 119 | if self.no_echo: 120 | self.view.run_command( 121 | "terminality_utils", 122 | {"util_type": "erase", "region": [ 123 | len(self.old_data), self.view.size() 124 | ]} 125 | ) 126 | os.write( 127 | self.proc.stdin.fileno(), 128 | self.data_in.encode(Settings.get("encoding")) 129 | ) 130 | self.old_data = self.view.substr( 131 | sublime.Region(0, self.view.size()) 132 | ) 133 | self.data_in = "" 134 | sleep(Settings.get("refresh_interval")) 135 | self.isWritable = False 136 | 137 | def run(self): 138 | start_time = time() 139 | self.proc = self.popen(self.cmds, self.cwd) 140 | self.old_data = self.view.substr(sublime.Region(0, self.view.size())) 141 | self.data_in = "" 142 | self.return_code = None 143 | self.isReadable = True 144 | self.isWritable = True 145 | 146 | threading.Thread(target=self.read_stdout).start() 147 | if not self.read_only: 148 | threading.Thread(target=self.read_stdin).start() 149 | 150 | while self.view is not None and self.view.window() is not None: 151 | if self.proc.poll() is not None: 152 | self.return_code = self.proc.poll() 153 | if not self.isWritable and not self.isReadable: 154 | self.proc.stdout.close() 155 | self.proc.stdin.close() 156 | break 157 | sleep(Settings.get("refresh_interval")) 158 | if self.return_code is None: 159 | self.kill(self.proc) 160 | self.result = True 161 | if self.on_complete is not None: 162 | self.on_complete( 163 | time() - start_time, 164 | self.return_code, 165 | self.params 166 | ) 167 | -------------------------------------------------------------------------------- /macro.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import os 3 | import shlex 4 | import re 5 | 6 | 7 | MACRO_PATTERN = re.compile("\\$(\\w+|\\$)") 8 | SUBSTR_PATTERN = re.compile("^(-?\\d+)?:(-?\\d+)?$") 9 | MatchObject = type(re.search("", "")) 10 | 11 | 12 | class Macro: 13 | @staticmethod 14 | def get_macros(macros=None, custom_macros=None, required=None, 15 | restricted=None, arguments=None): 16 | if arguments is not None: 17 | arguments = str(arguments) 18 | macros = macros or { 19 | "file": Macro.get_file_path( 20 | relative=False 21 | ), 22 | "file_relative": Macro.get_file_path( 23 | relative=True 24 | ), 25 | "file_name": Macro.get_file_name(), 26 | "working": Macro.get_working_dir( 27 | relative=False 28 | ), 29 | "working_relative": Macro.get_working_dir( 30 | relative=True 31 | ), 32 | "working_name": Macro.get_working_name(), 33 | "working_project": Macro.get_working_project_dir( 34 | relative=False 35 | ), 36 | "working_project_relative": Macro.get_working_project_dir( 37 | relative=True 38 | ), 39 | "working_project_name": Macro.get_working_project_name(), 40 | "project": Macro.get_project_dir( 41 | relative=False 42 | ), 43 | "project_relative": Macro.get_project_dir( 44 | relative=True 45 | ), 46 | "project_name": Macro.get_project_name(), 47 | "parent": Macro.get_parent_dir( 48 | relative=False 49 | ), 50 | "parent_relative": Macro.get_parent_dir( 51 | relative=True 52 | ), 53 | "parent_name": Macro.get_parent_name(), 54 | "packages_path": Macro.get_packages_path(), 55 | "raw_selection": Macro.get_selection(raw=True), 56 | "selection": Macro.get_selection(raw=False), 57 | "arguments": arguments, 58 | "sep": Macro.get_separator(), 59 | "$": "$" 60 | } 61 | custom_macros = custom_macros or {} 62 | required = required or [] 63 | restricted = restricted or [] 64 | 65 | restricted = [x.lower() for x in restricted] 66 | 67 | for custom_macro_name in custom_macros: 68 | if custom_macro_name.lower() in restricted: 69 | continue 70 | macro_output = None 71 | macro_type = None 72 | for macro in custom_macros[custom_macro_name]: 73 | if isinstance(macro, str): 74 | if (macro_output is not None and 75 | macro_type and macro_type == "macro"): 76 | break 77 | macro_type = "macro" 78 | macro_output = Macro.parse_macro( 79 | string=macro, 80 | macros=macros, 81 | custom_macros=custom_macros, 82 | required=required, 83 | restricted=restricted+[custom_macro_name], 84 | arguments=arguments, 85 | internal=True 86 | ) 87 | elif isinstance(macro, list) or isinstance(macro, tuple): 88 | # Macro Type: Element Type (str, int, pattern) 89 | types = { 90 | "substr_reg": "p", 91 | "regex_reg": "s", 92 | "substr_dyn": "sp", 93 | "regex_dyn": "ss" 94 | } 95 | 96 | longest_matched = -1 97 | for m_type in types: 98 | macro_req = types[m_type] 99 | if len(macro) < len(macro_req): 100 | continue 101 | matched = True 102 | for i in range(len(macro_req)): 103 | req_type = macro_req[i] 104 | value = macro[i] 105 | if (req_type == "p" and 106 | (not isinstance(value, str) or 107 | not SUBSTR_PATTERN.match(value))): 108 | matched = False 109 | break 110 | elif (req_type == "s" and 111 | (not isinstance(value, str) or 112 | SUBSTR_PATTERN.match(value))): 113 | matched = False 114 | break 115 | elif (req_type == "i" and 116 | not isinstance(value, int)): 117 | matched = False 118 | break 119 | if matched and longest_matched < len(macro_req): 120 | macro_type = m_type 121 | longest_matched = len(macro_req) 122 | 123 | if macro_output is None: 124 | if macro_type and macro_type.endswith("_reg"): 125 | continue 126 | else: 127 | if macro_type and macro_type.endswith("_dyn"): 128 | break 129 | 130 | if macro_type: 131 | if macro_type.endswith("_dyn"): 132 | output = Macro.parse_macro( 133 | string=macro[0], 134 | macros=macros, 135 | custom_macros=custom_macros, 136 | required=required, 137 | restricted=restricted+[custom_macro_name], 138 | arguments=arguments, 139 | internal=True 140 | ) 141 | macro = macro[1:] 142 | else: 143 | output = macro_output 144 | 145 | if output is None: 146 | continue 147 | if macro_type.startswith("regex_"): 148 | matches = re.search(macro[0], output) 149 | if matches: 150 | capture = 0 151 | if len(macro) > 1 and isinstance(macro[1], int): 152 | capture = int(macro[1]) 153 | if (capture == 0 or 154 | (matches.lastindex and 155 | capture <= matches.lastindex)): 156 | macro_output = matches.group(capture) 157 | elif macro_type.startswith("substr_"): 158 | substr = SUBSTR_PATTERN.match(macro[0]) 159 | start = int(substr.group(1) or 0) 160 | end = int(substr.group(2) or len(output)) 161 | macro_output = output[start:end] 162 | 163 | if macro_output is not None: 164 | macros[custom_macro_name.lower()] = macro_output 165 | return macros 166 | 167 | @staticmethod 168 | def parse_macro(string, macros=None, custom_macros=None, required=None, 169 | restricted=None, arguments=None, escaped=False, 170 | internal=False): 171 | if isinstance(string, str): 172 | required = required or [] 173 | macros_list = Macro.get_macros( 174 | macros=macros, 175 | custom_macros=custom_macros, 176 | required=required, 177 | restricted=restricted, 178 | arguments=arguments 179 | ) 180 | 181 | if internal: 182 | for macro_name in [ 183 | x for x in re.findall(MACRO_PATTERN, string)]: 184 | if (macro_name not in macros_list or 185 | macros_list[macro_name] is None): 186 | return None 187 | else: 188 | for macro_name in required: 189 | if (macro_name not in macros_list or 190 | macros_list[macro_name] is None): 191 | return None 192 | 193 | if macros_list: 194 | return MACRO_PATTERN.sub( 195 | lambda m: Macro.escape_string( 196 | Macro.parse_macro( 197 | string=m, 198 | macros=macros_list, 199 | custom_macros=custom_macros, 200 | required=required, 201 | restricted=restricted, 202 | arguments=arguments 203 | ) or "", 204 | escaped 205 | ), 206 | string 207 | ) 208 | else: 209 | return None 210 | elif isinstance(string, MatchObject): 211 | macro_name = string.group(1).lower() 212 | return macros[macro_name] if macro_name in macros else "" 213 | else: 214 | return None 215 | 216 | @staticmethod 217 | def escape_string(string, escaped=True): 218 | return shlex.quote(string) if escaped else string 219 | 220 | @staticmethod 221 | def get_working_dir(relative=False): 222 | if relative: 223 | return "." if Macro.get_working_dir(relative=False) else None 224 | return (Macro.get_working_project_dir(relative=False) or 225 | Macro.get_project_dir(relative=False) or 226 | Macro.get_parent_dir(relative=False)) 227 | 228 | @staticmethod 229 | def get_working_name(): 230 | working = Macro.get_working_dir(relative=False) 231 | return os.path.basename(working) if working else None 232 | 233 | @staticmethod 234 | def get_working_project_dir(relative=False): 235 | if relative: 236 | return ("." if Macro.get_working_project_dir(relative=False) 237 | else None) 238 | folders = sublime.active_window().folders() 239 | for folder in folders: 240 | if Macro.contains_file(folder, Macro.get_file_path(relative=False)): 241 | return folder 242 | return None 243 | 244 | @staticmethod 245 | def get_working_project_name(): 246 | working_project = Macro.get_working_project_dir(relative=False) 247 | return os.path.basename(working_project) if working_project else None 248 | 249 | @staticmethod 250 | def get_project_dir(relative=False): 251 | if relative: 252 | return "." if Macro.get_project_dir(relative=False) else None 253 | folders = sublime.active_window().folders() 254 | if len(folders) > 0: 255 | return folders[0] 256 | return None 257 | 258 | @staticmethod 259 | def get_project_name(): 260 | project = Macro.get_project_dir(relative=False) 261 | return os.path.basename(project) if project else None 262 | 263 | @staticmethod 264 | def get_parent_dir(relative=False): 265 | if relative: 266 | parent_dir = Macro.get_parent_dir(relative=False) 267 | working_dir = Macro.get_working_dir(relative=False) 268 | return os.path.relpath( 269 | parent_dir, 270 | working_dir 271 | ) if parent_dir and working_dir else None 272 | file_path = Macro.get_file_path(relative=False) 273 | return os.path.dirname(file_path) if file_path else None 274 | 275 | @staticmethod 276 | def get_parent_name(): 277 | parent_path = Macro.get_parent_dir(relative=False) 278 | return os.path.basename(parent_path) if parent_path else None 279 | 280 | @staticmethod 281 | def get_selection(raw=False): 282 | view = sublime.active_window().active_view() 283 | selections = view.sel() 284 | if len(selections) <= 0: 285 | return None 286 | elif raw and view.substr(selections[-1]) == "": 287 | return None 288 | elif not raw and view.substr(selections[-1]).strip() == "": 289 | return None 290 | return (view.substr(selections[-1]) if raw 291 | else view.substr(selections[-1]).strip()) 292 | 293 | @staticmethod 294 | def get_packages_path(): 295 | return sublime.packages_path() 296 | 297 | @staticmethod 298 | def get_file_path(relative=False): 299 | if relative: 300 | file_path = Macro.get_file_path(relative=False) 301 | working_dir = Macro.get_working_dir(relative=False) 302 | return os.path.relpath( 303 | file_path, 304 | working_dir 305 | ) if file_path and working_dir else None 306 | return sublime.active_window().active_view().file_name() 307 | 308 | @staticmethod 309 | def get_file_name(): 310 | file_path = Macro.get_file_path(relative=False) 311 | return os.path.basename(file_path) if file_path else None 312 | 313 | @staticmethod 314 | def get_separator(): 315 | return os.sep 316 | 317 | @staticmethod 318 | def contains_file(directory, file_path): 319 | if not file_path: 320 | return False 321 | return os.path.normcase( 322 | os.path.normpath(file_path) 323 | ).startswith( 324 | os.path.normcase(os.path.normpath(directory)) 325 | ) 326 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "0.1.1": "messages/0.1.1.txt", 4 | "0.2.0": "messages/0.2.0.txt", 5 | "0.3.0": "messages/0.3.0.txt", 6 | "0.3.1": "messages/0.3.1.txt", 7 | "0.3.2": "messages/0.3.2.txt", 8 | "0.3.3": "messages/0.3.3.txt", 9 | "0.3.4": "messages/0.3.4.txt", 10 | "0.3.5": "messages/0.3.5.txt", 11 | "0.3.6": "messages/0.3.6.txt", 12 | "0.3.7": "messages/0.3.7.txt", 13 | "0.3.8": "messages/0.3.8.txt", 14 | "0.3.9": "messages/0.3.9.txt", 15 | "0.3.10": "messages/0.3.10.txt" 16 | } 17 | -------------------------------------------------------------------------------- /messages/0.1.1.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.1.1 2 | 3 | Changelogs: 4 | - Fix "close_on_exit" logic bug 5 | - Execution units merge improvements 6 | - Add "show_nothing_if_nothing" and "run_if_only_one_available" settings 7 | - Add EditorConfig's config file 8 | 9 | To gain maximum potentials of Terminality, please read how you can add your own 10 | commands in README.md file 11 | -------------------------------------------------------------------------------- /messages/0.2.0.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.2.0 2 | 3 | Changelogs: 4 | - Complex macro system implementation 5 | - Fix proper menu 6 | - "platforms" option added 7 | - Unit tests and travis supported 8 | - Debug messages 9 | - C/C++ commands supported 10 | - Macro parser validation improvements 11 | - Screenshot added 12 | 13 | To gain maximum potentials of Terminality, please read how you can add your own 14 | commands in README.md file 15 | -------------------------------------------------------------------------------- /messages/0.3.0.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.0 2 | 3 | Changelogs: 4 | - More pre-defined macros ("working_name", "working_project_name" 5 | and "project_name") 6 | - Fix Terminality load settings from invalid project data key 7 | - '*' selector for any language selection 8 | - Capable of running Sublime Text's commands 9 | - Ability to override action name (via "name") 10 | - Merge related tests together 11 | 12 | To gain maximum potentials of Terminality, please read how you can add your own 13 | commands in README.md file 14 | -------------------------------------------------------------------------------- /messages/0.3.1.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.1 2 | 3 | Changelogs: 4 | - Add "Run Terminal here" command for OS X and Linux 5 | - Fix selector merging not properly merged 6 | - Add relative path macro 7 | 8 | To gain maximum potentials of Terminality, please read how you can add your own 9 | commands in README.md file 10 | -------------------------------------------------------------------------------- /messages/0.3.10.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.10 2 | 3 | Changelogs: 4 | - Fix the action's order did not sorted with the menu items 5 | 6 | To gain maximum potentials of Terminality, please read how you can add your own 7 | commands in README.md file 8 | -------------------------------------------------------------------------------- /messages/0.3.2.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.2 2 | 3 | Changelogs: 4 | - Terminality's commands should now works on Windows 5 | 6 | To gain maximum potentials of Terminality, please read how you can add your own 7 | commands in README.md file 8 | -------------------------------------------------------------------------------- /messages/0.3.3.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.3 2 | 3 | Changelogs: 4 | - Brand new running animation! 5 | - Fix language selector does not find language source in additional execution units 6 | - More debug messages 7 | 8 | To gain maximum potentials of Terminality, please read how you can add your own 9 | commands in README.md file 10 | -------------------------------------------------------------------------------- /messages/0.3.4.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.4 2 | 3 | Changelogs: 4 | - Reprioritize the selectors (previously is language first, now language last) 5 | - Fix execution units did not count properly which make one key command doesn't work 6 | 7 | To gain maximum potentials of Terminality, please read how you can add your own 8 | commands in README.md file 9 | -------------------------------------------------------------------------------- /messages/0.3.5.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.5 2 | 3 | Changelogs: 4 | - Fix override commands did not properly ignored 5 | - Refactor some code 6 | 7 | To gain maximum potentials of Terminality, please read how you can add your own 8 | commands in README.md file 9 | -------------------------------------------------------------------------------- /messages/0.3.6.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.6 2 | 3 | Changelogs: 4 | - Fix windows multiple commands did not run properly 5 | - Fix ignored commands did not properly count 6 | 7 | To gain maximum potentials of Terminality, please read how you can add your own 8 | commands in README.md file 9 | -------------------------------------------------------------------------------- /messages/0.3.7.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.7 2 | 3 | Changelogs: 4 | - Output encoding can be configured (thanks to @utensil) 5 | 6 | To gain maximum potentials of Terminality, please read how you can add your own 7 | commands in README.md file 8 | -------------------------------------------------------------------------------- /messages/0.3.8.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.8 2 | 3 | Changelogs: 4 | - Now you can pass arguments to the command by pressing Ctrl+Key+Shift+R 5 | - Add `selection`, `raw_selection` and `arguments` macros 6 | - Add Collections feature (a collections of commands) 7 | - `additional_execution_units` is now deprecated and will be removed in v0.4.0 8 | (use `execution_units` instead). See README.md file for more informations 9 | - `args` key now recursively parsed 10 | - Default command description now use the name instead of the reference name 11 | - Fix required macros work incorrectly if the key is exists but the value 12 | is None 13 | - Updated README.md file for more informations about how to use custom commands 14 | - Conform the code with PEP8 15 | 16 | To gain maximum potentials of Terminality, please read how you can add your own 17 | commands in README.md file 18 | -------------------------------------------------------------------------------- /messages/0.3.9.txt: -------------------------------------------------------------------------------- 1 | Terminality has been updated to v0.3.9 2 | 3 | Changelogs: 4 | - Execution unit now supported custom sorting via `order` key 5 | - Fix a compatibility bug for v0.3.7 settings or earlier 6 | - Fix a shell running time calculation 7 | 8 | To gain maximum potentials of Terminality, please read how you can add your own 9 | commands in README.md file 10 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Terminality has been successfully installed 2 | 3 | Please note that Terminality supports these languages by default but 4 | not limited to... 5 | 6 | - C/C++ 7 | - Lua 8 | - Python 9 | - Ruby 10 | - Swift (OS X only) 11 | 12 | To gain maximum potentials of Terminality, please read how you can add your own 13 | commands in README.md file 14 | -------------------------------------------------------------------------------- /progress.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | 4 | class ThreadProgress(): 5 | def __init__(self, thread, message, success_message=None, anim_fx=None, 6 | set_status=None, view=None): 7 | self.thread = thread 8 | self.message = message 9 | self.success_message = success_message 10 | if anim_fx is not None: 11 | self.anim_fx = anim_fx 12 | if set_status is not None: 13 | self.set_status = set_status 14 | self.view = view 15 | sublime.set_timeout(lambda: self.run(0), 100) 16 | 17 | def anim_fx(self, i, message, thread): 18 | chars = "⢁⡈⠔⠢" 19 | return { 20 | "i": (i + 1) % len(chars), 21 | "message": "%s [%s]" % (self.message, chars[i]), 22 | "delay": 150 23 | } 24 | 25 | def set_status(self, status=""): 26 | sublime.status_message(status) 27 | 28 | def run(self, i): 29 | if not self.thread.is_alive(): 30 | if hasattr(self.thread, "result") and not self.thread.result: 31 | if hasattr(self.thread, "result_message"): 32 | self.set_status(self.thread.result_message) 33 | else: 34 | self.set_status() 35 | return 36 | if self.success_message is not None: 37 | self.set_status(self.success_message) 38 | return 39 | info = self.anim_fx(i, self.message, self.thread) 40 | tmsg = "" 41 | if hasattr(self.thread, "msg"): 42 | tmsg = self.thread.msg 43 | self.set_status(info["message"] + tmsg) 44 | if self.view: 45 | self.view.set_name(info["message"] + tmsg) 46 | sublime.set_timeout(lambda: self.run(info["i"]), info["delay"]) 47 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | 4 | class Settings: 5 | 6 | """ 7 | Settings collections for easier access and managements 8 | """ 9 | 10 | @staticmethod 11 | def reset(): 12 | """ 13 | Reset all changes (used on restart) 14 | """ 15 | Settings.settings = None 16 | Settings.sublime_settings = None 17 | Settings.settings_base = "Terminality.sublime-settings" 18 | Settings.sublime_base = "Preferences.sublime-settings" 19 | 20 | @staticmethod 21 | def startup(): 22 | """ 23 | Read all settings from files 24 | """ 25 | Settings.settings = sublime.load_settings(Settings.settings_base) 26 | Settings.sublime_settings = sublime.load_settings(Settings.sublime_base) 27 | 28 | @staticmethod 29 | def get_global(key, default=None, as_tuple=False): 30 | """ 31 | Returns a value in default settings file 32 | 33 | @param key: a key to get value 34 | @param default: a return value if specified key is not exists 35 | @param as_tuple: if provided as True, will returns as a tuple contains 36 | value and a boolean specified if value gather from project 37 | settings or not 38 | """ 39 | if as_tuple: 40 | return (Settings.get_global(key, default, as_tuple=False), False) 41 | else: 42 | return Settings.settings.get(key, default) 43 | 44 | @staticmethod 45 | def get_local(key, default=None, as_tuple=False): 46 | """ 47 | Returns a value in project settings file 48 | 49 | @param key: a key to get value 50 | @param default: a return value if specified key is not exists 51 | @param as_tuple: if provided as True, will returns as a tuple contains 52 | value and a boolean specified if value gather from project 53 | settings or not 54 | """ 55 | if as_tuple: 56 | return (Settings.get_local(key, default, as_tuple=False), True) 57 | else: 58 | project_data = sublime.active_window().project_data() 59 | if (project_data is not None and 60 | "terminality" in project_data and 61 | key in project_data["terminality"]): 62 | return project_data["terminality"][key] 63 | return default 64 | 65 | @staticmethod 66 | def get_sublime(key, default=None): 67 | """ 68 | Returns a value in Sublime Text's settings file 69 | 70 | @param key: a key to get value 71 | @param default: a return value if specified key is not exists 72 | """ 73 | return Settings.sublime_settings.get(key, default) 74 | 75 | @staticmethod 76 | def get(key, default=None, from_global=None, as_tuple=False): 77 | """ 78 | Returns a value in settings 79 | 80 | @param key: a key to get value 81 | @param default: a return value if specified key is not exists 82 | @param as_tuple: if provided as True, will returns as a tuple contains 83 | value and a boolean specified if value gather from project 84 | settings or not 85 | 86 | This method must return in local-default prioritize order 87 | """ 88 | if from_global is None: 89 | value = Settings.get( 90 | key, 91 | default=None, 92 | from_global=False, 93 | as_tuple=as_tuple 94 | ) 95 | if value is None: 96 | value = Settings.get( 97 | key, 98 | default=default, 99 | from_global=True, 100 | as_tuple=as_tuple 101 | ) 102 | return value 103 | elif from_global: 104 | return Settings.get_global(key, default, as_tuple) 105 | else: 106 | return Settings.get_local(key, default, as_tuple) 107 | 108 | @staticmethod 109 | def set(key, val, to_global=False): 110 | """ 111 | Set a value to specified key in settings 112 | 113 | @param key: a key to set value 114 | @param val: a value to be set, if provided as not None, 115 | otherwise key will be deleted instead 116 | @param to_global: if provided as True, will set the value to 117 | default settings 118 | """ 119 | if to_global: 120 | if val is None: 121 | if Settings.settings.has(key): 122 | Settings.settings.erase(key) 123 | else: 124 | # Return here so it won't wasting time saving old settings 125 | return 126 | else: 127 | Settings.settings.set(key, val) 128 | sublime.save_settings(Settings.settings_base) 129 | else: 130 | window = sublime.active_window() 131 | project_data = window.project_data() 132 | if val is None: 133 | if (project_data is not None and 134 | "terminality" in project_data and 135 | key in project_data["terminality"]): 136 | del project_data["terminality"][key] 137 | else: 138 | # Return here so it won't wasting time saving old settings 139 | return 140 | else: 141 | if "terminality" in project_data: 142 | data = project_data["terminality"] 143 | else: 144 | data = {} 145 | data[key] = val 146 | project_data["terminality"] = data 147 | window.set_project_data(project_data) 148 | 149 | @staticmethod 150 | def ready(): 151 | """ 152 | Returns whether settings are ready to be used 153 | """ 154 | return Settings.settings is not None 155 | -------------------------------------------------------------------------------- /terminality.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | from .generic_shell import GenericShell 4 | from .QuickMenu.QuickMenu import QuickMenu 5 | from .macro import Macro 6 | from .progress import ThreadProgress 7 | from .settings import Settings 8 | from .unit_collections import UnitCollections 9 | 10 | 11 | TERMINALITY_VERSION = "0.3.10" 12 | 13 | 14 | def plugin_loaded(): 15 | Settings.reset() 16 | Settings.startup() 17 | print("[Terminality] v%s" % (TERMINALITY_VERSION)) 18 | 19 | 20 | class TerminalityRunCommand(sublime_plugin.WindowCommand): 21 | def parse_list(self, in_list, macros): 22 | out_list = [] 23 | for value in in_list: 24 | if isinstance(value, str): 25 | value = Macro.parse_macro( 26 | string=value, 27 | custom_macros=macros 28 | ) 29 | elif (isinstance(value, list) or 30 | isinstance(value, tuple)): 31 | value = self.parse_list(value, macros) 32 | elif isinstance(value, dict): 33 | value = self.parse_dict(value, macros) 34 | out_list.append(value) 35 | return out_list 36 | 37 | def parse_dict(self, in_dict, macros): 38 | for key in in_dict: 39 | if isinstance(in_dict[key], str): 40 | in_dict[key] = Macro.parse_macro( 41 | string=in_dict[key], 42 | custom_macros=macros 43 | ) 44 | elif (isinstance(in_dict[key], list) or 45 | isinstance(in_dict[key], tuple)): 46 | in_dict[key] = self.parse_list(in_dict[key], macros) 47 | elif isinstance(in_dict[key], dict): 48 | in_dict[key] = self.parse_dict(in_dict[key], macros) 49 | return in_dict 50 | 51 | def run(self, selector=None, action=None, arguments_title=None): 52 | if arguments_title is None or arguments_title == "": 53 | self.run_command(selector, action) 54 | return 55 | self.window.show_input_panel( 56 | caption=arguments_title + ":", 57 | initial_text="", 58 | on_done=lambda args: self.run_command( 59 | selector=selector, 60 | action=action, 61 | arguments=args 62 | ), 63 | on_change=None, 64 | on_cancel=None 65 | ) 66 | 67 | def run_command(self, selector=None, action=None, arguments=None): 68 | execution_unit = None 69 | execution_units = UnitCollections.load_default_collections() 70 | # Global 71 | additional_execution_units = Settings.get_global( 72 | "execution_units", 73 | default=Settings.get_global( 74 | "additional_execution_units", 75 | default={} 76 | ) 77 | ) 78 | for sel in [x for x in ["*", selector] if x is not None]: 79 | if (sel in additional_execution_units and 80 | action in additional_execution_units[sel]): 81 | execution_unit = additional_execution_units[sel][action] 82 | if not isinstance(execution_unit, dict): 83 | continue 84 | # Local 85 | additional_execution_units = Settings.get_local( 86 | "execution_units", 87 | default=Settings.get_local( 88 | "additional_execution_units", 89 | default={} 90 | ) 91 | ) 92 | for sel in [x for x in ["*", selector] if x is not None]: 93 | if (sel in additional_execution_units and 94 | action in additional_execution_units[sel]): 95 | execution_unit = additional_execution_units[sel][action] 96 | if not isinstance(execution_unit, dict): 97 | continue 98 | elif (sel in execution_units and 99 | action in execution_units[sel]): 100 | execution_unit = execution_units[sel][action] 101 | if not isinstance(execution_unit, dict): 102 | continue 103 | if execution_unit is None: 104 | sublime.error_message("There is no such execution unit") 105 | return 106 | if not isinstance(execution_unit, dict): 107 | if Settings.get("debug"): 108 | print("Execution unit is ignored [%s][%s]" % (selector, action)) 109 | return 110 | command = None 111 | command_type = None 112 | for key in ["command", "window_command", "view_command"]: 113 | if key in execution_unit: 114 | command_type = key 115 | command = execution_unit[key] 116 | break 117 | if not command: 118 | sublime.error_message("No command to run") 119 | return 120 | 121 | custom_macros = {} 122 | required_macros = [] 123 | if "macros" in execution_unit: 124 | custom_macros = execution_unit["macros"] 125 | if "required" in execution_unit: 126 | required_macros = execution_unit["required"] 127 | if "location" not in execution_unit: 128 | execution_unit["location"] = "$working" 129 | 130 | is_not_windows = sublime.platform() != "windows" 131 | command_script = [Macro.parse_macro( 132 | string=cmd, 133 | custom_macros=custom_macros, 134 | required=required_macros, 135 | escaped=is_not_windows, 136 | arguments=arguments 137 | ) for cmd in command.split(" ")] 138 | 139 | if command_type == "window_command" or command_type == "view_command": 140 | args = {} 141 | if "args" in execution_unit: 142 | args = execution_unit["args"] 143 | args = self.parse_dict(args, custom_macros) 144 | if command_type == "window_command": 145 | self.window.run_command(" ".join(command_script), args) 146 | else: 147 | self.window.active_view().run_command( 148 | " ".join(command_script), 149 | args 150 | ) 151 | elif command_type == "command": 152 | working_dir = Macro.parse_macro( 153 | string=execution_unit["location"], 154 | custom_macros=custom_macros, 155 | required=required_macros, 156 | arguments=arguments 157 | ) 158 | if working_dir is None: 159 | sublime.error_message( 160 | "Working directory is invalid" 161 | ) 162 | return 163 | 164 | if Settings.get("debug"): 165 | print("Running \"%s\"" % (" ".join(command_script))) 166 | print("Working dir is \"%s\"" % (working_dir)) 167 | 168 | self.view = self.window.new_file() 169 | self.view.set_name("Running...") 170 | self.view.set_scratch(True) 171 | if is_not_windows: 172 | command_script = " ".join(command_script) 173 | shell = GenericShell( 174 | cmds=command_script, 175 | view=self.view, 176 | on_complete=lambda e, r, p: self.on_complete( 177 | e, r, p, execution_unit 178 | ), 179 | no_echo=("no_echo" in execution_unit and 180 | execution_unit["no_echo"]), 181 | read_only=("read_only" in execution_unit and 182 | execution_unit["read_only"]) 183 | ) 184 | shell.set_cwd(working_dir) 185 | shell.start() 186 | ThreadProgress( 187 | thread=shell, 188 | message="Running", 189 | success_message="Terminal has been stopped", 190 | set_status=self.set_status, 191 | view=self.view 192 | ) 193 | elif Settings.get("debug"): 194 | print("Invalid command type") 195 | 196 | def on_complete(self, elapse_time, return_code, params, execution_unit): 197 | if return_code is not None: 198 | self.view.set_name( 199 | "Terminal Ended (Return: {0}) [{1:.2f}s]".format( 200 | return_code, elapse_time 201 | ) 202 | ) 203 | if ("close_on_exit" in execution_unit and 204 | execution_unit["close_on_exit"]): 205 | self.view.window().focus_view(self.view) 206 | self.view.window().run_command("close") 207 | sublime.set_timeout(lambda: self.set_status(), 3000) 208 | 209 | def set_status(self, status=None): 210 | for window in sublime.windows(): 211 | for view in window.views(): 212 | if status is None: 213 | view.erase_status("Terminality") 214 | else: 215 | view.set_status("Terminality", status) 216 | 217 | 218 | class TerminalityCommand(sublime_plugin.WindowCommand): 219 | 220 | """ 221 | Command to show menu which use to run another command 222 | """ 223 | 224 | qm = None 225 | ready_retry = 0 226 | 227 | main_menu = { 228 | "items": [["Terminality", "v" + TERMINALITY_VERSION]], 229 | "actions": [""] 230 | } 231 | 232 | def get_execution_units(self, execution_units_map, selector): 233 | execution_units = {} 234 | # Default Execution Units 235 | for selector_name in [x for x in ["*", selector] if x is not None]: 236 | if selector_name in execution_units_map: 237 | for action in execution_units_map[selector_name]: 238 | execution_units[action] = execution_units_map[ 239 | selector_name 240 | ][action] 241 | for selector_name in [x for x in ["*", selector] if x is not None]: 242 | # Global 243 | additional_execution_units = Settings.get_global( 244 | "execution_units", 245 | default=Settings.get_global( 246 | "additional_execution_units", 247 | default={} 248 | ) 249 | ) 250 | if selector_name in additional_execution_units: 251 | additional_execution_units = additional_execution_units[ 252 | selector_name 253 | ] 254 | for key in additional_execution_units: 255 | if (key in execution_units and 256 | isinstance(additional_execution_units[key], dict)): 257 | for sub_key in additional_execution_units[key]: 258 | execution_units[key][ 259 | sub_key 260 | ] = additional_execution_units[key][sub_key] 261 | else: 262 | execution_units[key] = additional_execution_units[key] 263 | if isinstance(additional_execution_units[key], dict): 264 | execution_units[key]["selector"] = selector 265 | # Local 266 | additional_execution_units = Settings.get_local( 267 | "execution_units", 268 | default=Settings.get_local( 269 | "additional_execution_units", 270 | default={} 271 | ) 272 | ) 273 | if selector_name in additional_execution_units: 274 | additional_execution_units = additional_execution_units[ 275 | selector_name 276 | ] 277 | for key in additional_execution_units: 278 | if (key in execution_units and 279 | isinstance(additional_execution_units[key], dict)): 280 | for sub_key in additional_execution_units[key]: 281 | execution_units[key][ 282 | sub_key 283 | ] = additional_execution_units[key][sub_key] 284 | else: 285 | execution_units[key] = additional_execution_units[key] 286 | if isinstance(additional_execution_units[key], dict): 287 | execution_units[key]["selector"] = selector 288 | if Settings.get("debug") and not execution_units: 289 | print("Execution units is empty") 290 | return execution_units 291 | 292 | def generate_menu(self, ask_arguments=False): 293 | menu = { 294 | "items": [], "actions": [], 295 | "unsort_items": [] 296 | } 297 | execution_units_map = UnitCollections.load_default_collections() 298 | sel_name = None 299 | for selector in execution_units_map: 300 | if (len(self.window.active_view().find_by_selector( 301 | selector)) > 0): 302 | sel_name = selector 303 | break 304 | if not sel_name: 305 | for selector in Settings.get("execution_units", {}): 306 | if (len(self.window.active_view().find_by_selector( 307 | selector)) > 0): 308 | sel_name = selector 309 | break 310 | for selector in Settings.get("additional_execution_units", {}): 311 | if (len(self.window.active_view().find_by_selector( 312 | selector)) > 0): 313 | sel_name = selector 314 | break 315 | if Settings.get("debug") and not sel_name: 316 | print("Selector is not found") 317 | execution_units = self.get_execution_units( 318 | execution_units_map, 319 | sel_name 320 | ) 321 | # Generate menu 322 | for action in execution_units: 323 | execution_unit = execution_units[action] 324 | if not isinstance(execution_unit, dict): 325 | continue 326 | if "selector" in execution_unit: 327 | selector_name = execution_unit["selector"] 328 | else: 329 | selector_name = sel_name 330 | custom_macros = {} 331 | required_macros = [] 332 | platforms = None 333 | arguments_title = None 334 | if ask_arguments: 335 | arguments_title = "Arguments" 336 | if "arguments" in execution_unit: 337 | arguments_title = execution_unit["arguments"] 338 | if "macros" in execution_unit: 339 | custom_macros = execution_unit["macros"] 340 | if "required" in execution_unit: 341 | required_macros = execution_unit["required"] 342 | if "platforms" in execution_unit: 343 | platforms = execution_unit["platforms"] 344 | action_name = Macro.parse_macro( 345 | string=action, 346 | custom_macros=custom_macros, 347 | required=required_macros, 348 | arguments="" if ask_arguments else None 349 | ) 350 | if platforms: 351 | matched = False 352 | current_platforms = [ 353 | sublime.platform() + "-" + sublime.arch(), 354 | sublime.platform(), 355 | sublime.arch() 356 | ] 357 | for platform in current_platforms: 358 | if platform in platforms: 359 | matched = True 360 | break 361 | if not matched: 362 | continue 363 | if action_name is None: 364 | if Settings.get("debug"): 365 | print("Required params is not completed") 366 | continue 367 | if "name" in execution_unit: 368 | action_name = Macro.parse_macro( 369 | string=execution_unit["name"], 370 | custom_macros=custom_macros, 371 | required=required_macros, 372 | arguments="" if ask_arguments else None 373 | ) 374 | order = action_name 375 | if "order" in execution_unit: 376 | order = execution_unit["order"] 377 | dest = action_name + " command" 378 | if "description" in execution_unit: 379 | dest = Macro.parse_macro( 380 | string=execution_unit["description"], 381 | custom_macros=custom_macros, 382 | required=required_macros, 383 | arguments="" if ask_arguments else None 384 | ) 385 | menu["unsort_items"] += [[ 386 | action_name, 387 | dest, 388 | { 389 | "command": "terminality_run", 390 | "args": { 391 | "selector": selector_name, 392 | "action": action, 393 | "arguments_title": arguments_title 394 | } 395 | }, 396 | order 397 | ]] 398 | menu["unsort_items"] = sorted(menu["unsort_items"], key=lambda x: x[3]) 399 | while menu["unsort_items"]: 400 | menu["items"].append(menu["unsort_items"][0][:-2]) 401 | menu["actions"].append(menu["unsort_items"][0][2]) 402 | menu["unsort_items"] = menu["unsort_items"][1:] 403 | 404 | if (Settings.get("run_if_only_one_available") and 405 | len(menu["items"]) == 1): 406 | self.window.run_command( 407 | "terminality_run", 408 | menu["actions"][0]["args"] 409 | ) 410 | return None 411 | if len(menu["items"]) <= 0 and Settings.get("show_nothing_if_nothing"): 412 | return None 413 | menu["items"] += self.main_menu["items"] 414 | menu["actions"] += self.main_menu["actions"] 415 | return menu 416 | 417 | def run(self, arguments=False, menu=None, action=None, replaceMenu=None): 418 | """ 419 | Show menu to user, if ready 420 | """ 421 | if not Settings.ready(): 422 | if self.ready_retry > 2: 423 | sublime.message_dialog( 424 | "Terminality is starting up..." + 425 | "Please wait a few seconds and try again." 426 | ) 427 | else: 428 | sublime.status_message( 429 | "Terminality is starting up..." + 430 | "Please wait a few seconds and try again..." 431 | ) 432 | self.ready_retry += 1 433 | return 434 | if self.qm is None: 435 | self.qm = QuickMenu() 436 | if replaceMenu is not None: 437 | self.qm.setMenu(replaceMenu["name"], replaceMenu["menu"]) 438 | return 439 | menu = self.generate_menu(arguments) 440 | if menu is None: 441 | return 442 | self.qm.setMenu("main", menu) 443 | self.qm.show(window=self.window, menu=menu, action=action) 444 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | from os.path import dirname, join, abspath 4 | 5 | 6 | HERE = dirname(__file__) 7 | from_here = lambda *parts: abspath(join(HERE, *parts)) 8 | sys.path += [ 9 | from_here("..", "..") 10 | ] 11 | 12 | 13 | def main(): 14 | loader = unittest.TestLoader() 15 | suite = loader.discover(HERE) 16 | 17 | unittest.TextTestRunner( 18 | verbosity=5 19 | ).run(suite) 20 | 21 | 22 | if __name__ == "main": 23 | main() 24 | -------------------------------------------------------------------------------- /tests/test_macro_internal.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import unittest 3 | from unittest.mock import patch, MagicMock 4 | from Terminality.macro import Macro 5 | 6 | 7 | def file_content(region): 8 | contents = """ 9 | Hello, World! 10 | This might be a long file 11 | In which use to test something 12 | Blah blah blah... 13 | """ 14 | 15 | return contents[region.begin():region.end()] 16 | 17 | 18 | MockView = MagicMock(spec=sublime.View) 19 | MockView.substr = MagicMock(side_effect=file_content) 20 | MockView.file_name.return_value = "path/to/file.ext" 21 | 22 | MockView2 = MagicMock(spec=sublime.View) 23 | MockView2.substr = MagicMock(side_effect=file_content) 24 | MockView2.file_name.return_value = None 25 | 26 | MockWindow = MagicMock(spec=sublime.Window) 27 | MockWindow.active_view.return_value = MockView 28 | MockWindow.folders.return_value = ["another/path/to/directory", 29 | "path/to"] 30 | 31 | MockWindow2 = MagicMock(spec=sublime.Window) 32 | MockWindow2.active_view.return_value = MockView2 33 | MockWindow2.folders.return_value = ["another/path/to/directory", 34 | "more/path/to/directory"] 35 | 36 | 37 | class TestMacroInternal(unittest.TestCase): 38 | def test_internal(self): 39 | self.assertEqual(not True, False) 40 | self.assertEqual(not None, True) 41 | self.assertEqual(not not None, False) 42 | 43 | @patch('sublime.active_window', return_value=MockWindow) 44 | def test_working_dir(self, active_window): 45 | self.assertEqual( 46 | Macro.get_working_dir(), 47 | "path/to" 48 | ) 49 | self.assertEqual( 50 | Macro.get_working_name(), 51 | "to" 52 | ) 53 | 54 | @patch('sublime.active_window', return_value=MockWindow2) 55 | def test_working_dir2(self, active_window): 56 | self.assertEqual( 57 | Macro.get_working_dir(), 58 | "another/path/to/directory" 59 | ) 60 | self.assertEqual( 61 | Macro.get_working_name(), 62 | "directory" 63 | ) 64 | 65 | @patch('sublime.active_window', return_value=MockWindow) 66 | def test_working_project_dir(self, active_window): 67 | self.assertEqual( 68 | Macro.get_working_project_dir(), 69 | "path/to" 70 | ) 71 | self.assertEqual( 72 | Macro.get_working_project_name(), 73 | "to" 74 | ) 75 | 76 | @patch('sublime.active_window', return_value=MockWindow2) 77 | def test_working_project_dir2(self, active_window): 78 | self.assertEqual( 79 | Macro.get_working_project_dir(), 80 | None 81 | ) 82 | self.assertEqual( 83 | Macro.get_working_project_name(), 84 | None 85 | ) 86 | 87 | @patch('sublime.active_window', return_value=MockWindow) 88 | def test_project_dir(self, active_window): 89 | self.assertEqual( 90 | Macro.get_project_dir(), 91 | "another/path/to/directory" 92 | ) 93 | self.assertEqual( 94 | Macro.get_project_name(), 95 | "directory" 96 | ) 97 | 98 | @patch('sublime.active_window', return_value=MockWindow2) 99 | def test_project_dir2(self, active_window): 100 | self.assertEqual( 101 | Macro.get_project_dir(), 102 | "another/path/to/directory" 103 | ) 104 | self.assertEqual( 105 | Macro.get_project_name(), 106 | "directory" 107 | ) 108 | 109 | @patch('sublime.active_window', return_value=MockWindow) 110 | def test_parent_dir(self, active_window): 111 | self.assertEqual( 112 | Macro.get_parent_dir(), 113 | "path/to" 114 | ) 115 | self.assertEqual( 116 | Macro.get_parent_name(), 117 | "to" 118 | ) 119 | 120 | @patch('sublime.active_window', return_value=MockWindow2) 121 | def test_parent_dir2(self, active_window): 122 | self.assertEqual( 123 | Macro.get_parent_dir(), 124 | None 125 | ) 126 | self.assertEqual( 127 | Macro.get_parent_name(), 128 | None 129 | ) 130 | 131 | @patch('sublime.active_window', return_value=MockWindow) 132 | def test_selection(self, active_window): 133 | self.assertEqual( 134 | Macro.get_selection(raw=False), 135 | None 136 | ) 137 | self.assertEqual( 138 | Macro.get_selection(raw=True), 139 | None 140 | ) 141 | MockView.sel.return_value = [] 142 | self.assertEqual( 143 | Macro.get_selection(raw=False), 144 | None 145 | ) 146 | self.assertEqual( 147 | Macro.get_selection(raw=True), 148 | None 149 | ) 150 | MockView.sel.return_value = [sublime.Region(11, 11)] 151 | self.assertEqual( 152 | Macro.get_selection(raw=False), 153 | None 154 | ) 155 | self.assertEqual( 156 | Macro.get_selection(raw=True), 157 | None 158 | ) 159 | MockView.sel.return_value = [sublime.Region(11, 12)] 160 | self.assertEqual( 161 | Macro.get_selection(raw=False), 162 | None 163 | ) 164 | self.assertEqual( 165 | Macro.get_selection(raw=True), 166 | " " 167 | ) 168 | 169 | @patch('sublime.active_window', return_value=MockWindow) 170 | def test_selection2(self, active_window): 171 | MockView.sel.return_value = [sublime.Region(5, 10)] 172 | self.assertEqual( 173 | Macro.get_selection(raw=False), 174 | "Hello" 175 | ) 176 | 177 | MockView.sel.return_value = [ 178 | sublime.Region(5, 10), 179 | sublime.Region(12, 17) 180 | ] 181 | self.assertEqual( 182 | Macro.get_selection(raw=False), 183 | "World" 184 | ) 185 | 186 | MockView.sel.return_value = [ 187 | sublime.Region(5, 10), 188 | sublime.Region(12, 17), 189 | sublime.Region(11, 17) 190 | ] 191 | self.assertEqual( 192 | Macro.get_selection(raw=False), 193 | "World" 194 | ) 195 | self.assertEqual( 196 | Macro.get_selection(raw=True), 197 | " World" 198 | ) 199 | 200 | @patch('sublime.active_window', return_value=MockWindow) 201 | def test_file_path(self, active_window): 202 | self.assertEqual( 203 | Macro.get_file_path(), 204 | "path/to/file.ext" 205 | ) 206 | self.assertEqual( 207 | Macro.get_file_name(), 208 | "file.ext" 209 | ) 210 | 211 | @patch('sublime.active_window', return_value=MockWindow2) 212 | def test_file_path2(self, active_window): 213 | self.assertEqual( 214 | Macro.get_file_path(), 215 | None 216 | ) 217 | self.assertEqual( 218 | Macro.get_file_name(), 219 | None 220 | ) 221 | -------------------------------------------------------------------------------- /tests/test_macro_parser.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import unittest 3 | from unittest.mock import patch, MagicMock 4 | from Terminality.macro import Macro 5 | 6 | 7 | def file_content(region): 8 | contents = """ 9 | Hello, World! 10 | This might be a long file 11 | In which use to test something 12 | Blah blah blah... 13 | """ 14 | 15 | return contents[region.begin():region.end()] 16 | 17 | 18 | MockView = MagicMock(spec=sublime.View) 19 | MockView.substr = MagicMock(side_effect=file_content) 20 | MockView.file_name.return_value = "path/to/file.ext" 21 | 22 | MockWindow = MagicMock(spec=sublime.Window) 23 | MockWindow.active_view.return_value = MockView 24 | MockWindow.folders.return_value = ["another/path/to/directory", 25 | "path/to"] 26 | 27 | 28 | class TestMacroParser(unittest.TestCase): 29 | @patch('sublime.active_window', return_value=MockWindow) 30 | def test_none(self, active_window): 31 | macros = { 32 | "test": None, 33 | "expected": None, 34 | "required": None, 35 | "macros": None 36 | } 37 | 38 | self.assertEqual( 39 | Macro.parse_macro( 40 | string=macros["test"], 41 | custom_macros=macros["macros"], 42 | required=macros["required"] 43 | ), 44 | macros["expected"] 45 | ) 46 | 47 | @patch('sublime.active_window', return_value=MockWindow) 48 | def test_empty(self, active_window): 49 | macros = { 50 | "test": "", 51 | "expected": "", 52 | "required": [], 53 | "macros": {} 54 | } 55 | 56 | self.assertEqual( 57 | Macro.parse_macro( 58 | string=macros["test"], 59 | custom_macros=macros["macros"], 60 | required=macros["required"] 61 | ), 62 | macros["expected"] 63 | ) 64 | 65 | @patch('sublime.active_window', return_value=MockWindow) 66 | def test_predefined_macro1(self, active_window): 67 | macros = { 68 | "test": "", 69 | "expected": "", 70 | "required": ["file", "file_name"], 71 | "macros": {} 72 | } 73 | 74 | self.assertEqual( 75 | Macro.parse_macro( 76 | string=macros["test"], 77 | custom_macros=macros["macros"], 78 | required=macros["required"] 79 | ), 80 | macros["expected"] 81 | ) 82 | 83 | @patch('sublime.active_window', return_value=MockWindow) 84 | def test_predefined_macro2(self, active_window): 85 | macros = { 86 | "test": "$file_name", 87 | "expected": None, 88 | "required": ["required", "file_name"], 89 | "macros": {} 90 | } 91 | 92 | self.assertEqual( 93 | Macro.parse_macro( 94 | string=macros["test"], 95 | custom_macros=macros["macros"], 96 | required=macros["required"] 97 | ), 98 | macros["expected"] 99 | ) 100 | 101 | @patch('sublime.active_window', return_value=MockWindow) 102 | def test_predefined_macro3(self, active_window): 103 | macros = { 104 | "test": "$require ; $file", 105 | "expected": " ; path/to/file.ext", 106 | "required": [], 107 | "macros": {} 108 | } 109 | 110 | self.assertEqual( 111 | Macro.parse_macro( 112 | string=macros["test"], 113 | custom_macros=macros["macros"], 114 | required=macros["required"] 115 | ), 116 | macros["expected"] 117 | ) 118 | 119 | @patch('sublime.active_window', return_value=MockWindow) 120 | def test_predefined_macro4(self, active_window): 121 | macros = { 122 | "test": "$parent$file$file_name", 123 | "expected": "path/topath/to/file.extfile.ext", 124 | "required": [], 125 | "macros": {} 126 | } 127 | 128 | self.assertEqual( 129 | Macro.parse_macro( 130 | string=macros["test"], 131 | custom_macros=macros["macros"], 132 | required=macros["required"] 133 | ), 134 | macros["expected"] 135 | ) 136 | 137 | @patch('sublime.active_window', return_value=MockWindow) 138 | def test_predefined_macro5(self, active_window): 139 | macros = { 140 | "test": "$working$$$working_project$$$project", 141 | "expected": "path/to$path/to$another/path/to/directory", 142 | "required": [], 143 | "macros": {} 144 | } 145 | 146 | self.assertEqual( 147 | Macro.parse_macro( 148 | string=macros["test"], 149 | custom_macros=macros["macros"], 150 | required=macros["required"] 151 | ), 152 | macros["expected"] 153 | ) 154 | 155 | @patch('sublime.active_window', return_value=MockWindow) 156 | def test_required_macro1(self, active_window): 157 | macros = { 158 | "test": "", 159 | "expected": None, 160 | "required": ["required"], 161 | "macros": {} 162 | } 163 | 164 | self.assertEqual( 165 | Macro.parse_macro( 166 | string=macros["test"], 167 | custom_macros=macros["macros"], 168 | required=macros["required"] 169 | ), 170 | macros["expected"] 171 | ) 172 | 173 | @patch('sublime.active_window', return_value=MockWindow) 174 | def test_required_macro2(self, active_window): 175 | macros = { 176 | "test": "", 177 | "expected": None, 178 | "required": ["required"], 179 | "macros": { 180 | "required": [] 181 | } 182 | } 183 | 184 | self.assertEqual( 185 | Macro.parse_macro( 186 | string=macros["test"], 187 | custom_macros=macros["macros"], 188 | required=macros["required"] 189 | ), 190 | macros["expected"] 191 | ) 192 | 193 | @patch('sublime.active_window', return_value=MockWindow) 194 | def test_required_macro3(self, active_window): 195 | macros = { 196 | "test": "", 197 | "expected": None, 198 | "required": ["required"], 199 | "macros": { 200 | "required": [ 201 | 1, 202 | [1, 2], 203 | None, 204 | [None, None] 205 | ] 206 | } 207 | } 208 | 209 | self.assertEqual( 210 | Macro.parse_macro( 211 | string=macros["test"], 212 | custom_macros=macros["macros"], 213 | required=macros["required"] 214 | ), 215 | macros["expected"] 216 | ) 217 | 218 | @patch('sublime.active_window', return_value=MockWindow) 219 | def test_required_macro4(self, active_window): 220 | macros = { 221 | "test": "", 222 | "expected": "", 223 | "required": ["required"], 224 | "macros": { 225 | "required": [ 226 | 1, 227 | [1, 2], 228 | None, 229 | [None, None], 230 | "macro_output" 231 | ] 232 | } 233 | } 234 | 235 | self.assertEqual( 236 | Macro.parse_macro( 237 | string=macros["test"], 238 | custom_macros=macros["macros"], 239 | required=macros["required"] 240 | ), 241 | macros["expected"] 242 | ) 243 | 244 | @patch('sublime.active_window', return_value=MockWindow) 245 | def test_required_macro5(self, active_window): 246 | macros = { 247 | "test": "$required", 248 | "expected": "", 249 | "required": [], 250 | "macros": { 251 | "required": [ 252 | 1, 253 | [1, 2], 254 | None, 255 | [None, None] 256 | ] 257 | } 258 | } 259 | 260 | self.assertEqual( 261 | Macro.parse_macro( 262 | string=macros["test"], 263 | custom_macros=macros["macros"], 264 | required=macros["required"] 265 | ), 266 | macros["expected"] 267 | ) 268 | 269 | @patch('sublime.active_window', return_value=MockWindow) 270 | def test_required_macro6(self, active_window): 271 | macros = { 272 | "test": "$selection", 273 | "expected": "", 274 | "required": [], 275 | "macros": {} 276 | } 277 | 278 | self.assertEqual( 279 | Macro.parse_macro( 280 | string=macros["test"], 281 | custom_macros=macros["macros"], 282 | required=macros["required"] 283 | ), 284 | macros["expected"] 285 | ) 286 | 287 | @patch('sublime.active_window', return_value=MockWindow) 288 | def test_required_macro7(self, active_window): 289 | macros = { 290 | "test": "$selection", 291 | "expected": None, 292 | "required": ["selection"], 293 | "macros": {} 294 | } 295 | 296 | self.assertEqual( 297 | Macro.parse_macro( 298 | string=macros["test"], 299 | custom_macros=macros["macros"], 300 | required=macros["required"] 301 | ), 302 | macros["expected"] 303 | ) 304 | 305 | @patch('sublime.active_window', return_value=MockWindow) 306 | def test_required_macro8(self, active_window): 307 | MockView.sel.return_value = [sublime.Region(5, 10)] 308 | macros = { 309 | "test": "$selection", 310 | "expected": "Hello", 311 | "required": [], 312 | "macros": {} 313 | } 314 | 315 | self.assertEqual( 316 | Macro.parse_macro( 317 | string=macros["test"], 318 | custom_macros=macros["macros"], 319 | required=macros["required"] 320 | ), 321 | macros["expected"] 322 | ) 323 | 324 | @patch('sublime.active_window', return_value=MockWindow) 325 | def test_required_macro9(self, active_window): 326 | MockView.sel.return_value = [sublime.Region(5, 10)] 327 | macros = { 328 | "test": "$selection", 329 | "expected": "Hello", 330 | "required": ["selection"], 331 | "macros": {} 332 | } 333 | 334 | self.assertEqual( 335 | Macro.parse_macro( 336 | string=macros["test"], 337 | custom_macros=macros["macros"], 338 | required=macros["required"] 339 | ), 340 | macros["expected"] 341 | ) 342 | 343 | @patch('sublime.active_window', return_value=MockWindow) 344 | def test_required_macro10(self, active_window): 345 | macros = { 346 | "test": "", 347 | "expected": None, 348 | "required": [""], 349 | "macros": {} 350 | } 351 | 352 | self.assertEqual( 353 | Macro.parse_macro( 354 | string=macros["test"], 355 | custom_macros=macros["macros"], 356 | required=macros["required"] 357 | ), 358 | macros["expected"] 359 | ) 360 | 361 | @patch('sublime.active_window', return_value=MockWindow) 362 | def test_recursion_macro(self, active_window): 363 | macros = { 364 | "test": "$required", 365 | "expected": "", 366 | "required": [], 367 | "macros": { 368 | "required": [ 369 | "$required" 370 | ] 371 | } 372 | } 373 | 374 | self.assertEqual( 375 | Macro.parse_macro( 376 | string=macros["test"], 377 | custom_macros=macros["macros"], 378 | required=macros["required"] 379 | ), 380 | macros["expected"] 381 | ) 382 | 383 | @patch('sublime.active_window', return_value=MockWindow) 384 | def test_recursion_macro2(self, active_window): 385 | macros = { 386 | "test": "$required", 387 | "expected": "", 388 | "required": [], 389 | "macros": { 390 | "required": [ 391 | "$required2" 392 | ], 393 | "required2": [ 394 | "$required" 395 | ] 396 | } 397 | } 398 | 399 | self.assertEqual( 400 | Macro.parse_macro( 401 | string=macros["test"], 402 | custom_macros=macros["macros"], 403 | required=macros["required"] 404 | ), 405 | macros["expected"] 406 | ) 407 | 408 | @patch('sublime.active_window', return_value=MockWindow) 409 | def test_recursion_macro3(self, active_window): 410 | macros = { 411 | "test": "$required$required2", 412 | "expected": "OutputOutput", 413 | "required": [], 414 | "macros": { 415 | "required": [ 416 | "$required2", 417 | "Output" 418 | ], 419 | "required2": [ 420 | "$required" 421 | ] 422 | } 423 | } 424 | 425 | self.assertEqual( 426 | Macro.parse_macro( 427 | string=macros["test"], 428 | custom_macros=macros["macros"], 429 | required=macros["required"] 430 | ), 431 | macros["expected"] 432 | ) 433 | 434 | @patch('sublime.active_window', return_value=MockWindow) 435 | def test_substring_macro(self, active_window): 436 | macros = { 437 | "test": "$custom;$custom2;$custom3;$custom4", 438 | "expected": ".ext;.ext;.ext;.ext", 439 | "required": [], 440 | "macros": { 441 | "custom": [ 442 | "$file", 443 | ["-4:"] 444 | ], 445 | "custom2": [ 446 | "$file_name", 447 | ["-4:"] 448 | ], 449 | "custom3": [ 450 | ["$file", "-4:"] 451 | ], 452 | "custom4": [ 453 | ["$file_name", "-4:"] 454 | ] 455 | } 456 | } 457 | 458 | self.assertEqual( 459 | Macro.parse_macro( 460 | string=macros["test"], 461 | custom_macros=macros["macros"], 462 | required=macros["required"] 463 | ), 464 | macros["expected"] 465 | ) 466 | 467 | @patch('sublime.active_window', return_value=MockWindow) 468 | def test_regex_macro(self, active_window): 469 | macros = { 470 | "test": "$custom;$custom2;$custom3;$custom4", 471 | "expected": ".ext;.ext;.ext;.ext", 472 | "required": [], 473 | "macros": { 474 | "custom": [ 475 | "$file", 476 | ["\\.\\w+$"] 477 | ], 478 | "custom2": [ 479 | "$file_name", 480 | ["\\.\\w+$"] 481 | ], 482 | "custom3": [ 483 | ["$file", "\\.\\w+$"] 484 | ], 485 | "custom4": [ 486 | ["$file_name", "\\.\\w+$"] 487 | ] 488 | } 489 | } 490 | 491 | self.assertEqual( 492 | Macro.parse_macro( 493 | string=macros["test"], 494 | custom_macros=macros["macros"], 495 | required=macros["required"] 496 | ), 497 | macros["expected"] 498 | ) 499 | -------------------------------------------------------------------------------- /unit_collections.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | 4 | class UnitCollections: 5 | 6 | @staticmethod 7 | def load_default_collections(): 8 | execution_units = {} 9 | collections = sublime.find_resources("*.terminality-collections") 10 | for collection_file in collections: 11 | collection = sublime.decode_value( 12 | sublime.load_resource(collection_file) 13 | ) 14 | if "execution_units" not in collection: 15 | continue 16 | for scope in collection["execution_units"]: 17 | scope_value = collection["execution_units"][scope] 18 | if not isinstance(scope_value, dict): 19 | continue 20 | execution_units[scope] = UnitCollections.load_language_scopes( 21 | scope_value, 22 | execution_units[scope] if scope in execution_units else None 23 | ) 24 | return execution_units 25 | 26 | @staticmethod 27 | def load_language_scopes(scope_value, scope=None): 28 | scope = scope or {} 29 | for command in scope_value: 30 | if not isinstance(scope_value[command], dict): 31 | continue 32 | scope[command] = scope_value[command] 33 | return scope 34 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | 5 | class TerminalityUtilsCommand(sublime_plugin.TextCommand): 6 | def run(self, edit, util_type="", text="", region=None, dest=None): 7 | if util_type == "insert": 8 | self.view.insert(edit, 0, text) 9 | elif util_type == "add": 10 | self.view.insert(edit, self.view.size(), text) 11 | elif util_type == "erase": 12 | self.view.erase(edit, sublime.Region(region[0], region[1])) 13 | elif util_type == "clear": 14 | self.view.erase(edit, sublime.Region(0, self.view.size())) 15 | elif util_type == "set_read_only": 16 | self.view.set_read_only(True) 17 | elif util_type == "clear_read_only": 18 | self.view.set_read_only(False) 19 | 20 | def description(self, util_type="", text="", region=None, dest=None): 21 | return dest 22 | --------------------------------------------------------------------------------