├── .gitignore ├── src ├── resources │ ├── WindowsTerminal.png │ ├── pwsh.scale-200.png │ ├── pwsh-preview.scale-200.png │ ├── {0caa0dad-35be-5f56-a8ff-afceeeaa6101}.scale-200.png │ ├── {2c4de342-38b7-51cf-b940-2309a097f518}.scale-200.png │ ├── {550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png │ ├── {574e775e-4f2a-5b96-ac1e-a2962a402336}.scale-200.png │ ├── {61c54bbd-c2c6-5271-96e7-009a87ff44bf}.scale-200.png │ ├── {9acb9455-ca41-5af7-950f-6bca1bc9722f}.scale-200.png │ ├── {b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png │ ├── download.bat │ └── README.md ├── lib │ ├── jsmin │ │ ├── LICENSE.txt │ │ ├── __main__.py │ │ ├── __init__.py │ │ └── test.py │ └── windows_terminal_wrapper.py ├── terminal-profiles.ini └── terminal_profiles.py ├── .github └── keypirinha-terminal-profiles.png ├── .editorconfig ├── README.md ├── LICENSE └── make.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | # Package builds 2 | /build/ 3 | -------------------------------------------------------------------------------- /src/resources/WindowsTerminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/WindowsTerminal.png -------------------------------------------------------------------------------- /src/resources/pwsh.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/pwsh.scale-200.png -------------------------------------------------------------------------------- /.github/keypirinha-terminal-profiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/.github/keypirinha-terminal-profiles.png -------------------------------------------------------------------------------- /src/resources/pwsh-preview.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/pwsh-preview.scale-200.png -------------------------------------------------------------------------------- /src/resources/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{2c4de342-38b7-51cf-b940-2309a097f518}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{2c4de342-38b7-51cf-b940-2309a097f518}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{574e775e-4f2a-5b96-ac1e-a2962a402336}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{574e775e-4f2a-5b96-ac1e-a2962a402336}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.scale-200.png -------------------------------------------------------------------------------- /src/resources/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fran-f/keypirinha-terminal-profiles/HEAD/src/resources/{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = crlf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{md,rst,txt}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /src/resources/download.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | git clone -n --depth=1 https://github.com/microsoft/terminal/ 3 | git --git-dir=terminal/.git checkout main -- src/cascadia/CascadiaPackage/ProfileIcons/*-200.png 4 | git --git-dir=terminal/.git checkout main -- res/terminal/images/Square44x44Logo.targetsize-96.png 5 | move src\cascadia\CascadiaPackage\ProfileIcons\*-200.png . 6 | move res\terminal\images\Square44x44Logo.targetsize-96.png WindowsTerminal.png 7 | rd /s /q terminal src res 8 | -------------------------------------------------------------------------------- /src/resources/README.md: -------------------------------------------------------------------------------- 1 | # Icons for auto-generated profiles 2 | 3 | Windows Terminal can generate a number of predefined profiles, and assigns them 4 | icons included in the application packages. Since I have not found a way to read 5 | those icons directly, I include these icons in the plugin package. 6 | 7 | The set can be refreshed running the script `download.bat`, which clones the 8 | [Windows Terminal repository](https://github.com/microsoft/terminal/) and extracts 9 | the profile icons. The script assumes that `git` is installed and available on the 10 | path. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keypirinha Plugin: Windows Terminal Profiles 2 | 3 | This is a plugin for the [Keypirinha](http://keypirinha.com) launcher, which adds catalog items 4 | for all profiles defined in the [Windows Terminal](https://github.com/microsoft/terminal/) 5 | configuration. Launching any of these items will open a new terminal with the chosen profile. 6 | 7 | Hit `Tab` for extra options, including opening a profile as Administrator. 8 | 9 | ![Screenshot of the plugin in action](.github/keypirinha-terminal-profiles.png) 10 | 11 | ## Download and installation 12 | 13 | Grab the most recent 14 | [release package](https://github.com/fran-f/keypirinha-terminal-profiles/releases) 15 | and copy it to the `InstalledPackages` directory of Keypirinha. 16 | 17 | If you have installed Keypirinha, this will be under your profile folder, at 18 | `%APPDATA%\Keypirinha\InstalledPackages`. If you are using it in *portable 19 | mode*, look under `Keypirinha\portable\Profile\InstalledPackages`. 20 | 21 | 22 | ## License 23 | 24 | This package is distributed under the terms of the MIT license. 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Francesco Figari (https://fran.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/jsmin/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Dave St.Germain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/jsmin/__main__.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | # This code is original from jsmin by Douglas Crockford, it was translated to 4 | # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 5 | # 6 | # The MIT License (MIT) 7 | #· 8 | # Copyright (c) 2013 Dave St.Germain 9 | #· 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | #· 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | #· 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | import sys, os, glob 29 | from jsmin import JavascriptMinify 30 | 31 | for f in sys.argv[1:]: 32 | with open(f, 'r') as js: 33 | minifier = JavascriptMinify(js, sys.stdout) 34 | minifier.minify() 35 | sys.stdout.write('\n') 36 | 37 | 38 | -------------------------------------------------------------------------------- /make.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | set PACKAGE_NAME=Terminal-Profiles 5 | set INSTALL_DIR=%APPDATA%\Keypirinha\InstalledPackages 6 | 7 | if "%1"=="" goto help 8 | if "%1"=="-h" goto help 9 | if "%1"=="--help" goto help 10 | if "%1"=="help" ( 11 | :help 12 | echo Usage: 13 | echo make help 14 | echo make clean 15 | echo make build 16 | echo make install 17 | echo make py [python_args] 18 | goto end 19 | ) 20 | 21 | if "%BUILD_DIR%"=="" set BUILD_DIR=%~dp0build 22 | if "%KEYPIRINHA_SDK%"=="" ( 23 | echo ERROR: Keypirinha SDK environment not setup. 24 | echo Run SDK's "kpenv" script and try again. 25 | exit /b 1 26 | ) 27 | 28 | if "%1"=="clean" ( 29 | if exist "%BUILD_DIR%" rmdir /s /q "%BUILD_DIR%" 30 | goto end 31 | ) 32 | 33 | if "%1"=="build" ( 34 | if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" 35 | pushd "%~dp0" 36 | call "%KEYPIRINHA_SDK%\cmd\kparch" ^ 37 | "%BUILD_DIR%\%PACKAGE_NAME%.keypirinha-package" ^ 38 | -r LICENSE* README* src 39 | popd 40 | goto end 41 | ) 42 | 43 | if "%1"=="install" ( 44 | copy /Y "%BUILD_DIR%\*.keypirinha-package" "%INSTALL_DIR%\" 45 | goto end 46 | ) 47 | 48 | if "%1"=="dev" ( 49 | if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" 50 | pushd "%~dp0" 51 | call "%KEYPIRINHA_SDK%\cmd\kparch" ^ 52 | "%BUILD_DIR%\%PACKAGE_NAME%.keypirinha-package" ^ 53 | -r LICENSE* README* src 54 | popd 55 | 56 | echo TODO: ensure the INSTALL_DIR variable declared at the top of this 57 | echo script complies to your configuration and remove this message 58 | exit /1 59 | 60 | copy /Y "%BUILD_DIR%\*.keypirinha-package" "%INSTALL_DIR%\" 61 | goto end 62 | ) 63 | 64 | if "%1"=="py" ( 65 | call "%KEYPIRINHA_SDK%\cmd\kpy" %2 %3 %4 %5 %6 %7 %8 %9 66 | goto end 67 | ) 68 | 69 | :end 70 | -------------------------------------------------------------------------------- /src/terminal-profiles.ini: -------------------------------------------------------------------------------- 1 | # 2 | # Windows Terminal Profiles configuration file 3 | # More info at https://github.com/fran-f/keypirinha-terminal-profiles 4 | # 5 | 6 | [items] 7 | # Settings related to the catalog items created by this plugin. 8 | 9 | # Use icons defined in the Terminal settings for each profile. 10 | # * If we cannot load an icon or this option is set to false, the item 11 | # will appear with the default Windows Terminal icon. 12 | # * Restart Keypirinha to apply this setting. 13 | # * Default: true 14 | #use_profile_icons = true 15 | 16 | 17 | # Standard installations from the Windows Store 18 | # When a package id is defined, we use it to look up executable and settings. 19 | [terminal/stable] 20 | app_package = Microsoft.WindowsTerminal_8wekyb3d8bbwe 21 | enabled = true 22 | prefix = "Windows Terminal: " 23 | 24 | [terminal/preview] 25 | app_package = Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe 26 | enabled = true 27 | prefix = "Windows Terminal (Preview): " 28 | 29 | 30 | # If you have a custom installation of Windows Terminal, you can add a new 31 | # section pointing to its files. 32 | # NOTE: below is a sample section, unlikely to work if enabled as it is! 33 | 34 | # Custom sections must follow the format "terminal/xxxxxxx" to be considered 35 | [terminal/dev] 36 | # Instruct Keypirinha to collect profiles from this installation 37 | # * Default: true 38 | enabled = false 39 | 40 | # The Windows Terminal executable to run when a catalog item is invoked. 41 | #executable = "${var:KNOWNFOLDER_LOCALAPPDATA}\Microsoft\WindowsApps\wt.exe" 42 | executable = "C:\my-terminal\wt.exe" 43 | 44 | # The settings file for Windows Terminal. The plugin will look in this 45 | # file to find terminal profiles. 46 | #settings_file = "${var:KNOWNFOLDER_LOCALAPPDATA}\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\Localstate\settings.json" 47 | settings_file = "C:\my-terminal\settings.json" 48 | 49 | # The text that will appear before a profile name in Keypirinha 50 | # * Default: "Windows Terminal (): " 51 | prefix = "My Custom Terminal: " 52 | 53 | -------------------------------------------------------------------------------- /src/lib/windows_terminal_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Windows Terminal wrapper class 3 | More info at https://github.com/fran-f/keypirinha-terminal-profiles 4 | """ 5 | 6 | # Disable warning for relative import statements 7 | # pylint: disable=import-error, relative-beyond-top-level 8 | 9 | import json 10 | import os 11 | 12 | import keypirinha_util as kpu 13 | from .jsmin import jsmin 14 | 15 | class WindowsTerminalWrapper: 16 | 17 | def __init__(self, settings, executable): 18 | if not os.path.exists(settings): 19 | raise ValueError("Could not find Windows Terminal settings at %s" % (settings)) 20 | if not os.path.exists(executable) and not os.path.lexists(executable): 21 | raise ValueError("Could not find Windows Terminal at %s" % (executable)) 22 | 23 | self._wt_settings = settings 24 | self._wt_executable = executable 25 | 26 | def profiles(self): 27 | with kpu.chardet_open(self._wt_settings, mode="rt") as terminal_settings: 28 | # remove comments and whitespace 29 | settings = jsmin(terminal_settings.read()) 30 | # remove commas from last properties 31 | settings = settings.replace(",}", "}").replace(",]", "]") 32 | 33 | data = json.loads(settings) 34 | 35 | profiles = data.get("profiles") 36 | if not profiles: 37 | return [] 38 | 39 | # the profile list can be 'profiles' itself, or nested under 'list' 40 | profiles_list = profiles.get("list", []) \ 41 | if isinstance(profiles, dict) else profiles 42 | 43 | return [ 44 | p for p in profiles_list if p.get('hidden', False) == False 45 | ] 46 | 47 | def openprofile(self, guid, elevate=False): 48 | if elevate: 49 | kpu.shell_execute( 50 | "cmd.exe", 51 | args=['/c', 'start', '', '/b', self._wt_executable, '--profile', guid], 52 | verb="runas" 53 | ) 54 | else: 55 | kpu.shell_execute(self._wt_executable, args=['--profile', guid]) 56 | 57 | def opennewtab(self, guid): 58 | kpu.shell_execute(self._wt_executable, args=['--window', '0', '--profile', guid]) 59 | -------------------------------------------------------------------------------- /src/lib/jsmin/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | # This code is original from jsmin by Douglas Crockford, it was translated to 4 | # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 5 | # 6 | # The MIT License (MIT) 7 | # 8 | # Copyright (c) 2013 Dave St.Germain 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | 29 | import sys 30 | is_3 = sys.version_info >= (3, 0) 31 | if is_3: 32 | import io 33 | else: 34 | import StringIO 35 | try: 36 | import cStringIO 37 | except ImportError: 38 | cStringIO = None 39 | 40 | 41 | __all__ = ['jsmin', 'JavascriptMinify'] 42 | __version__ = '2.2.3.dev' 43 | 44 | 45 | def jsmin(js, **kwargs): 46 | """ 47 | returns a minified version of the javascript string 48 | """ 49 | if not is_3: 50 | if cStringIO and not isinstance(js, unicode): 51 | # strings can use cStringIO for a 3x performance 52 | # improvement, but unicode (in python2) cannot 53 | klass = cStringIO.StringIO 54 | else: 55 | klass = StringIO.StringIO 56 | else: 57 | klass = io.StringIO 58 | ins = klass(js) 59 | outs = klass() 60 | JavascriptMinify(ins, outs, **kwargs).minify() 61 | return outs.getvalue() 62 | 63 | 64 | class JavascriptMinify(object): 65 | """ 66 | Minify an input stream of javascript, writing 67 | to an output stream 68 | """ 69 | 70 | def __init__(self, instream=None, outstream=None, quote_chars="'\""): 71 | self.ins = instream 72 | self.outs = outstream 73 | self.quote_chars = quote_chars 74 | 75 | def minify(self, instream=None, outstream=None): 76 | if instream and outstream: 77 | self.ins, self.outs = instream, outstream 78 | 79 | self.is_return = False 80 | self.return_buf = '' 81 | 82 | def write(char): 83 | # all of this is to support literal regular expressions. 84 | # sigh 85 | if char in 'return': 86 | self.return_buf += char 87 | self.is_return = self.return_buf == 'return' 88 | else: 89 | self.return_buf = '' 90 | self.is_return = self.is_return and char < '!' 91 | self.outs.write(char) 92 | if self.is_return: 93 | self.return_buf = '' 94 | 95 | read = self.ins.read 96 | 97 | space_strings = "abcdefghijklmnopqrstuvwxyz"\ 98 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" 99 | self.space_strings = space_strings 100 | starters, enders = '{[(+-', '}])+-/' + self.quote_chars 101 | newlinestart_strings = starters + space_strings + self.quote_chars 102 | newlineend_strings = enders + space_strings + self.quote_chars 103 | self.newlinestart_strings = newlinestart_strings 104 | self.newlineend_strings = newlineend_strings 105 | 106 | do_newline = False 107 | do_space = False 108 | escape_slash_count = 0 109 | in_quote = '' 110 | quote_buf = [] 111 | 112 | previous = ';' 113 | previous_non_space = ';' 114 | next1 = read(1) 115 | 116 | while next1: 117 | next2 = read(1) 118 | if in_quote: 119 | quote_buf.append(next1) 120 | 121 | if next1 == in_quote: 122 | numslashes = 0 123 | for c in reversed(quote_buf[:-1]): 124 | if c != '\\': 125 | break 126 | else: 127 | numslashes += 1 128 | if numslashes % 2 == 0: 129 | in_quote = '' 130 | write(''.join(quote_buf)) 131 | elif next1 in '\r\n': 132 | next2, do_newline = self.newline( 133 | previous_non_space, next2, do_newline) 134 | elif next1 < '!': 135 | if (previous_non_space in space_strings \ 136 | or previous_non_space > '~') \ 137 | and (next2 in space_strings or next2 > '~'): 138 | do_space = True 139 | elif previous_non_space in '-+' and next2 == previous_non_space: 140 | # protect against + ++ or - -- sequences 141 | do_space = True 142 | elif self.is_return and next2 == '/': 143 | # returning a regex... 144 | write(' ') 145 | elif next1 == '/': 146 | if do_space: 147 | write(' ') 148 | if next2 == '/': 149 | # Line comment: treat it as a newline, but skip it 150 | next2 = self.line_comment(next1, next2) 151 | next1 = '\n' 152 | next2, do_newline = self.newline( 153 | previous_non_space, next2, do_newline) 154 | elif next2 == '*': 155 | self.block_comment(next1, next2) 156 | next2 = read(1) 157 | if previous_non_space in space_strings: 158 | do_space = True 159 | next1 = previous 160 | else: 161 | if previous_non_space in '{(,=:[?!&|;' or self.is_return: 162 | self.regex_literal(next1, next2) 163 | # hackish: after regex literal next1 is still / 164 | # (it was the initial /, now it's the last /) 165 | next2 = read(1) 166 | else: 167 | write('/') 168 | else: 169 | if do_newline: 170 | write('\n') 171 | do_newline = False 172 | do_space = False 173 | if do_space: 174 | do_space = False 175 | write(' ') 176 | 177 | write(next1) 178 | if next1 in self.quote_chars: 179 | in_quote = next1 180 | quote_buf = [] 181 | 182 | if next1 >= '!': 183 | previous_non_space = next1 184 | 185 | if next1 == '\\': 186 | escape_slash_count += 1 187 | else: 188 | escape_slash_count = 0 189 | 190 | previous = next1 191 | next1 = next2 192 | 193 | def regex_literal(self, next1, next2): 194 | assert next1 == '/' # otherwise we should not be called! 195 | 196 | self.return_buf = '' 197 | 198 | read = self.ins.read 199 | write = self.outs.write 200 | 201 | in_char_class = False 202 | 203 | write('/') 204 | 205 | next = next2 206 | while next and (next != '/' or in_char_class): 207 | write(next) 208 | if next == '\\': 209 | write(read(1)) # whatever is next is escaped 210 | elif next == '[': 211 | write(read(1)) # character class cannot be empty 212 | in_char_class = True 213 | elif next == ']': 214 | in_char_class = False 215 | next = read(1) 216 | 217 | write('/') 218 | 219 | def line_comment(self, next1, next2): 220 | assert next1 == next2 == '/' 221 | 222 | read = self.ins.read 223 | 224 | while next1 and next1 not in '\r\n': 225 | next1 = read(1) 226 | while next1 and next1 in '\r\n': 227 | next1 = read(1) 228 | 229 | return next1 230 | 231 | def block_comment(self, next1, next2): 232 | assert next1 == '/' 233 | assert next2 == '*' 234 | 235 | read = self.ins.read 236 | 237 | # Skip past first /* and avoid catching on /*/...*/ 238 | next1 = read(1) 239 | next2 = read(1) 240 | 241 | comment_buffer = '/*' 242 | while next1 != '*' or next2 != '/': 243 | comment_buffer += next1 244 | next1 = next2 245 | next2 = read(1) 246 | 247 | if comment_buffer.startswith("/*!"): 248 | # comment needs preserving 249 | self.outs.write(comment_buffer) 250 | self.outs.write("*/\n") 251 | 252 | 253 | def newline(self, previous_non_space, next2, do_newline): 254 | read = self.ins.read 255 | 256 | if previous_non_space and ( 257 | previous_non_space in self.newlineend_strings 258 | or previous_non_space > '~'): 259 | while 1: 260 | if next2 < '!': 261 | next2 = read(1) 262 | if not next2: 263 | break 264 | else: 265 | if next2 in self.newlinestart_strings \ 266 | or next2 > '~' or next2 == '/': 267 | do_newline = True 268 | break 269 | 270 | return next2, do_newline 271 | -------------------------------------------------------------------------------- /src/terminal_profiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Windows Terminal Profiles plugin 3 | More info at https://github.com/fran-f/keypirinha-terminal-profiles 4 | """ 5 | 6 | # Disable warning for relative import statements 7 | # pylint: disable=import-error, relative-beyond-top-level 8 | 9 | import os 10 | import sys 11 | import shutil 12 | 13 | import keypirinha as kp 14 | import keypirinha_util as kpu 15 | from .lib.windows_terminal_wrapper import WindowsTerminalWrapper 16 | 17 | class TerminalProfiles(kp.Plugin): 18 | """ 19 | Add catalog items for all the profiles configured in Windows Terminal. 20 | """ 21 | 22 | ACTION_OPEN = { 23 | 'name' : "wt.open", 24 | 'label' : "Open", 25 | 'short_desc' : "Open this profile in a new window" 26 | } 27 | ACTION_OPEN_NEW_TAB = { 28 | 'name' : "wt.open_new_tab", 29 | 'label' : "Open new tab", 30 | 'short_desc' : "Open this profile in a new tab of an existing window" 31 | } 32 | ACTION_ELEVATE = { 33 | 'name' : "wt.elevate", 34 | 'label' : "Run as Administrator", 35 | 'short_desc' : "Open this profile in a new window with elevated privileges" 36 | } 37 | 38 | ICON_POSTFIX = ".scale-200.png" 39 | INSTANCE_SEPARATOR = "::" 40 | 41 | default_icon = None 42 | use_profile_icons = False 43 | 44 | terminal_instances = None 45 | 46 | def on_start(self): 47 | """Respond to on_start Keypirinha messages""" 48 | self._load_settings() 49 | self._set_up() 50 | 51 | actions = [ 52 | self.ACTION_OPEN, 53 | self.ACTION_OPEN_NEW_TAB, 54 | self.ACTION_ELEVATE, 55 | ] 56 | self.set_actions( 57 | kp.ItemCategory.REFERENCE, 58 | [self.create_action(**a) for a in actions] 59 | ) 60 | 61 | def on_events(self, flags): 62 | """Respond to on_events Keypirinha messages""" 63 | if flags & kp.Events.PACKCONFIG: 64 | self._clean_up() 65 | self._load_settings() 66 | self._set_up() 67 | 68 | def on_catalog(self): 69 | """Respond to on_catalog Keypirinha messages""" 70 | if not self.terminal_instances: 71 | return 72 | 73 | self.set_catalog([ 74 | self._item_for_profile(instance, profile) 75 | for instance in self.terminal_instances.values() 76 | for profile in instance["wrapper"].profiles() 77 | ]) 78 | 79 | def on_execute(self, item, action): 80 | """Respond to on_execute Keypirinha messages""" 81 | [instance, _, profile] = item.target().partition(self.INSTANCE_SEPARATOR) 82 | terminal = self.terminal_instances[instance]["wrapper"] 83 | 84 | if action is None: 85 | terminal.openprofile(profile) 86 | return 87 | 88 | if action.name() == self.ACTION_ELEVATE['name']: 89 | terminal.openprofile(profile, elevate=True) 90 | elif action.name() == self.ACTION_OPEN_NEW_TAB['name']: 91 | terminal.opennewtab(profile) 92 | else: 93 | terminal.openprofile(profile) 94 | 95 | def on_suggest(self, user_input, items_chain): 96 | """Respond to on_suggest Keypirinha messages""" 97 | # pass 98 | 99 | def _load_settings(self): 100 | """ 101 | Load the configuration file and extract settings to local variables. 102 | """ 103 | settings = PluginSettings(self) 104 | self.use_profile_icons = settings.use_profile_icons() 105 | 106 | self.terminal_instances = dict(settings.terminal_instances()) 107 | 108 | def _set_up(self): 109 | """ 110 | Initialise the plugin based on the extracted configuration. 111 | """ 112 | self.default_icon = self.load_icon(self._resource("WindowsTerminal.png")) 113 | self.set_default_icon(self.default_icon) 114 | 115 | def _clean_up(self): 116 | """ 117 | Clean up any resources, to start anew with fresh configuration. 118 | """ 119 | if self.default_icon: 120 | self.default_icon.free() 121 | self.default_icon = None 122 | 123 | def _item_for_profile(self, instance, profile): 124 | """ 125 | Return a catalog item for a profile. 126 | """ 127 | guid = profile.get("guid") 128 | name = profile.get("name") 129 | if not guid or not name: 130 | self.warn("Skipping invalid profile with name:'%s' guid:'%s'" % (name, guid)) 131 | return None 132 | 133 | icon = profile.get("icon", None) 134 | icon_handle = self._load_profile_icon(icon, guid) \ 135 | if self.use_profile_icons else None 136 | 137 | return self.create_item( 138 | category=kp.ItemCategory.REFERENCE, 139 | label=instance["prefix"] + name, 140 | short_desc="Open a new terminal", 141 | icon_handle=icon_handle, 142 | target=instance["name"] + self.INSTANCE_SEPARATOR + guid, 143 | args_hint=kp.ItemArgsHint.FORBIDDEN, 144 | hit_hint=kp.ItemHitHint.IGNORE 145 | ) 146 | 147 | def _load_profile_icon(self, icon, guid): 148 | """ 149 | Attempt to load an icon for the given profile. 150 | """ 151 | iconfile = None 152 | if not icon: 153 | # check if this is a default profile 154 | if guid[0] == '{' and guid[-1] == '}': 155 | iconfile = self._resource(guid + self.ICON_POSTFIX) 156 | else: 157 | # internal icons ms-appx:///ProfileIcons/{...}.png 158 | if icon.startswith("ms-appx:///ProfileIcons/"): 159 | iconfile = self._resource(icon[24:-4] + self.ICON_POSTFIX) 160 | 161 | if iconfile: 162 | try: 163 | return self.load_icon(iconfile) 164 | except ValueError: 165 | pass 166 | else: 167 | # could it be an external file? 168 | try: 169 | # External files cannot be loaded as icon, so we try to copy it 170 | # to the plugin's cache directory, and load it from there. 171 | cache_dir = self.get_package_cache_path(True) 172 | icon_file = guid + ".ico" 173 | source = icon[8:] if icon[0:8] == "file:///" else os.path.expandvars(icon) 174 | shutil.copyfile(source, cache_dir + "\\" + icon_file) 175 | return self.load_icon("cache://Terminal-Profiles/" + icon_file) 176 | except (ValueError, FileNotFoundError, OSError): 177 | self.warn("Cannot load icon '%s' for profile %s" % (icon, guid)) 178 | 179 | return None 180 | 181 | @staticmethod 182 | def _resource(filename): 183 | return "res://Terminal-Profiles/resources/" + filename 184 | 185 | 186 | class PluginSettings: 187 | """Wrapper for the plugin configuration file.""" 188 | 189 | INSTANCE_PREFIX = "terminal/" 190 | DEFAULT_ITEM_PREFIX = "Windows Terminal (%s): " 191 | 192 | LOCALAPPDATA = kpu.shell_known_folder_path("{f1b32785-6fba-4fcf-9d55-7b8e7f157091}") 193 | WINDOWSAPPS = LOCALAPPDATA + "\\Microsoft\\WindowsApps" 194 | 195 | PACKAGED_SETTINGS = LOCALAPPDATA + "\\Packages\\%s\\LocalState\\settings.json" 196 | PACKAGED_EXECUTABLE = WINDOWSAPPS + "\\%s\\wt.exe" 197 | 198 | MISSING_KEY_ERROR = """ 199 | ⚠ Config section [%s] defines a custom installation, but the value for '%s' is missing. 200 | """ 201 | 202 | def __init__(self, plugin): 203 | self._settings = plugin.load_settings() 204 | self._logger = plugin 205 | 206 | def use_profile_icons(self): 207 | """True if we should show try to load per-profile icons.""" 208 | return self._settings.get_bool( 209 | key="use_profile_icons", 210 | section="items", 211 | fallback=True 212 | ) 213 | 214 | def terminal_instances(self): 215 | """Return the list of terminal instances in the configuration file.""" 216 | for section_name in self._instancesections(): 217 | instance_name = section_name[len(self.INSTANCE_PREFIX):] 218 | 219 | # Skip an instance if it defines 'enabled = false' 220 | if not self._settings.get_bool(key="enabled", section=section_name, fallback=True): 221 | continue 222 | 223 | prefix = self._get(section_name, "prefix", "Windows Terminal (%s)" % (instance_name)) 224 | app_package = self._get(section_name, "app_package") 225 | 226 | if app_package and not self._package_exists(app_package): 227 | self._logger.info( 228 | "Skipping '%s', package %s does not exist" % (instance_name, app_package) 229 | ) 230 | continue 231 | 232 | # For packaged instances, paths are derived from the package id... 233 | packaged_settings_file = self.PACKAGED_SETTINGS % (app_package) if app_package else None 234 | packaged_executable = self.PACKAGED_EXECUTABLE % (app_package) if app_package else None 235 | 236 | # ...but you can still override them 237 | settings_file = self._get(section_name, "settings_file", packaged_settings_file) 238 | executable = self._get(section_name, "executable", packaged_executable) 239 | 240 | # For custom instances, settings_file and executable are required 241 | if not app_package: 242 | if not settings_file: 243 | self._logger.warn(self.MISSING_KEY_ERROR % (section_name, "settings_file")) 244 | continue 245 | if not executable: 246 | self._logger.warn(self.MISSING_KEY_ERROR % (section_name, "executable")) 247 | continue 248 | 249 | self._logger.info( 250 | "Adding profiles for '%s' (%s)" % (instance_name, app_package or "custom") 251 | ) 252 | try: 253 | wrapper = WindowsTerminalWrapper(settings_file, executable) 254 | yield (instance_name, { 255 | "name": instance_name, 256 | "prefix": prefix, 257 | "wrapper": wrapper 258 | }) 259 | except ValueError: 260 | message = sys.exc_info()[1] 261 | self._logger.warn(message) 262 | 263 | def _instancesections(self): 264 | return [ 265 | s for s in self._settings.sections() \ 266 | if s.lower().startswith(self.INSTANCE_PREFIX) 267 | ] 268 | 269 | def _get(self, section, key, fallback=None): 270 | return self._settings.get(key=key, section=section, fallback=fallback, unquote=True) 271 | 272 | def _package_exists(self, app_package): 273 | return os.path.exists(self.WINDOWSAPPS + "\\" + app_package) 274 | -------------------------------------------------------------------------------- /src/lib/jsmin/test.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | # This code is original from jsmin by Douglas Crockford, it was translated to 4 | # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 5 | # 6 | # The MIT License (MIT) 7 | #· 8 | # Copyright (c) 2013 Dave St.Germain 9 | #· 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | #· 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | #· 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | import unittest 29 | import jsmin 30 | 31 | 32 | class JsTests(unittest.TestCase): 33 | def _minify(self, js): 34 | return jsmin.jsmin(js) 35 | 36 | def assertEqual(self, thing1, thing2): 37 | if thing1 != thing2: 38 | print(repr(thing1), repr(thing2)) 39 | raise AssertionError 40 | return True 41 | 42 | def assertMinified(self, js_input, expected, **kwargs): 43 | minified = jsmin.jsmin(js_input, **kwargs) 44 | assert minified == expected, "\ngot: %r\nexp: %r" % (minified, expected) 45 | 46 | def testQuoted(self): 47 | js = r''' 48 | Object.extend(String, { 49 | interpret: function(value) { 50 | return value == null ? '' : String(value); 51 | }, 52 | specialChar: { 53 | '\b': '\\b', 54 | '\t': '\\t', 55 | '\n': '\\n', 56 | '\f': '\\f', 57 | '\r': '\\r', 58 | '\\': '\\\\' 59 | } 60 | }); 61 | 62 | ''' 63 | expected = r"""Object.extend(String,{interpret:function(value){return value==null?'':String(value);},specialChar:{'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','\\':'\\\\'}});""" 64 | self.assertMinified(js, expected) 65 | 66 | def testSingleComment(self): 67 | js = r'''// use native browser JS 1.6 implementation if available 68 | if (Object.isFunction(Array.prototype.forEach)) 69 | Array.prototype._each = Array.prototype.forEach; 70 | 71 | if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { 72 | 73 | // hey there 74 | function() {// testing comment 75 | foo; 76 | //something something 77 | 78 | location = 'http://foo.com;'; // goodbye 79 | } 80 | //bye 81 | ''' 82 | expected = r"""if(Object.isFunction(Array.prototype.forEach)) 83 | Array.prototype._each=Array.prototype.forEach;if(!Array.prototype.indexOf)Array.prototype.indexOf=function(item,i){function(){foo;location='http://foo.com;';}""" 84 | self.assertMinified(js, expected) 85 | 86 | def testEmpty(self): 87 | self.assertMinified('', '') 88 | self.assertMinified(' ', '') 89 | self.assertMinified('\n', '') 90 | self.assertMinified('\r\n', '') 91 | self.assertMinified('\t', '') 92 | 93 | 94 | def testMultiComment(self): 95 | js = r""" 96 | function foo() { 97 | print('hey'); 98 | } 99 | /* 100 | if(this.options.zindex) { 101 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 102 | this.element.style.zIndex = this.options.zindex; 103 | } 104 | */ 105 | another thing; 106 | """ 107 | expected = r"""function foo(){print('hey');} 108 | another thing;""" 109 | self.assertMinified(js, expected) 110 | 111 | def testLeadingComment(self): 112 | js = r"""/* here is a comment at the top 113 | 114 | it ends here */ 115 | function foo() { 116 | alert('crud'); 117 | } 118 | 119 | """ 120 | expected = r"""function foo(){alert('crud');}""" 121 | self.assertMinified(js, expected) 122 | 123 | def testBlockCommentStartingWithSlash(self): 124 | self.assertMinified('A; /*/ comment */ B', 'A;B') 125 | 126 | def testBlockCommentEndingWithSlash(self): 127 | self.assertMinified('A; /* comment /*/ B', 'A;B') 128 | 129 | def testLeadingBlockCommentStartingWithSlash(self): 130 | self.assertMinified('/*/ comment */ A', 'A') 131 | 132 | def testLeadingBlockCommentEndingWithSlash(self): 133 | self.assertMinified('/* comment /*/ A', 'A') 134 | 135 | def testEmptyBlockComment(self): 136 | self.assertMinified('/**/ A', 'A') 137 | 138 | def testBlockCommentMultipleOpen(self): 139 | self.assertMinified('/* A /* B */ C', 'C') 140 | 141 | def testJustAComment(self): 142 | self.assertMinified(' // a comment', '') 143 | 144 | def test_issue_bitbucket_10(self): 145 | js = ''' 146 | files = [{name: value.replace(/^.*\\\\/, '')}]; 147 | // comment 148 | A 149 | ''' 150 | expected = '''files=[{name:value.replace(/^.*\\\\/,'')}];A''' 151 | self.assertMinified(js, expected) 152 | 153 | def test_issue_bitbucket_10_without_semicolon(self): 154 | js = ''' 155 | files = [{name: value.replace(/^.*\\\\/, '')}] 156 | // comment 157 | A 158 | ''' 159 | expected = '''files=[{name:value.replace(/^.*\\\\/,'')}]\nA''' 160 | self.assertMinified(js, expected) 161 | 162 | def testRe(self): 163 | js = r''' 164 | var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); 165 | return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); 166 | });''' 167 | expected = r"""var str=this.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"/g,'');return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);});""" 168 | self.assertMinified(js, expected) 169 | 170 | def testIgnoreComment(self): 171 | js = r""" 172 | var options_for_droppable = { 173 | overlap: options.overlap, 174 | containment: options.containment, 175 | tree: options.tree, 176 | hoverclass: options.hoverclass, 177 | onHover: Sortable.onHover 178 | } 179 | 180 | var options_for_tree = { 181 | onHover: Sortable.onEmptyHover, 182 | overlap: options.overlap, 183 | containment: options.containment, 184 | hoverclass: options.hoverclass 185 | } 186 | 187 | // fix for gecko engine 188 | Element.cleanWhitespace(element); 189 | """ 190 | expected = r"""var options_for_droppable={overlap:options.overlap,containment:options.containment,tree:options.tree,hoverclass:options.hoverclass,onHover:Sortable.onHover} 191 | var options_for_tree={onHover:Sortable.onEmptyHover,overlap:options.overlap,containment:options.containment,hoverclass:options.hoverclass} 192 | Element.cleanWhitespace(element);""" 193 | self.assertMinified(js, expected) 194 | 195 | def testHairyRe(self): 196 | js = r""" 197 | inspect: function(useDoubleQuotes) { 198 | var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { 199 | var character = String.specialChar[match[0]]; 200 | return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); 201 | }); 202 | if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; 203 | return "'" + escapedString.replace(/'/g, '\\\'') + "'"; 204 | }, 205 | 206 | toJSON: function() { 207 | return this.inspect(true); 208 | }, 209 | 210 | unfilterJSON: function(filter) { 211 | return this.sub(filter || Prototype.JSONFilter, '#{1}'); 212 | }, 213 | """ 214 | expected = r"""inspect:function(useDoubleQuotes){var escapedString=this.gsub(/[\x00-\x1f\\]/,function(match){var character=String.specialChar[match[0]];return character?character:'\\u00'+match[0].charCodeAt().toPaddedString(2,16);});if(useDoubleQuotes)return'"'+escapedString.replace(/"/g,'\\"')+'"';return"'"+escapedString.replace(/'/g,'\\\'')+"'";},toJSON:function(){return this.inspect(true);},unfilterJSON:function(filter){return this.sub(filter||Prototype.JSONFilter,'#{1}');},""" 215 | self.assertMinified(js, expected) 216 | 217 | def testLiteralRe(self): 218 | js = r""" 219 | myString.replace(/\\/g, '/'); 220 | console.log("hi"); 221 | """ 222 | expected = r"""myString.replace(/\\/g,'/');console.log("hi");""" 223 | self.assertMinified(js, expected) 224 | 225 | js = r''' return /^data:image\//i.test(url) || 226 | /^(https?|ftp|file|about|chrome|resource):/.test(url); 227 | ''' 228 | expected = r'''return /^data:image\//i.test(url)||/^(https?|ftp|file|about|chrome|resource):/.test(url);''' 229 | self.assertMinified(js, expected) 230 | 231 | def testNoBracesWithComment(self): 232 | js = r""" 233 | onSuccess: function(transport) { 234 | var js = transport.responseText.strip(); 235 | if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 236 | throw 'Server returned an invalid collection representation.'; 237 | this._collection = eval(js); 238 | this.checkForExternalText(); 239 | }.bind(this), 240 | onFailure: this.onFailure 241 | }); 242 | """ 243 | expected = r"""onSuccess:function(transport){var js=transport.responseText.strip();if(!/^\[.*\]$/.test(js)) 244 | throw'Server returned an invalid collection representation.';this._collection=eval(js);this.checkForExternalText();}.bind(this),onFailure:this.onFailure});""" 245 | self.assertMinified(js, expected) 246 | js_without_comment = r""" 247 | onSuccess: function(transport) { 248 | var js = transport.responseText.strip(); 249 | if (!/^\[.*\]$/.test(js)) 250 | throw 'Server returned an invalid collection representation.'; 251 | this._collection = eval(js); 252 | this.checkForExternalText(); 253 | }.bind(this), 254 | onFailure: this.onFailure 255 | }); 256 | """ 257 | self.assertMinified(js_without_comment, expected) 258 | 259 | def testSpaceInRe(self): 260 | js = r""" 261 | num = num.replace(/ /g,''); 262 | """ 263 | self.assertMinified(js, "num=num.replace(/ /g,'');") 264 | 265 | def testEmptyString(self): 266 | js = r''' 267 | function foo('') { 268 | 269 | } 270 | ''' 271 | self.assertMinified(js, "function foo(''){}") 272 | 273 | def testDoubleSpace(self): 274 | js = r''' 275 | var foo = "hey"; 276 | ''' 277 | self.assertMinified(js, 'var foo="hey";') 278 | 279 | def testLeadingRegex(self): 280 | js = r'/[d]+/g ' 281 | self.assertMinified(js, js.strip()) 282 | 283 | def testLeadingString(self): 284 | js = r"'a string in the middle of nowhere'; // and a comment" 285 | self.assertMinified(js, "'a string in the middle of nowhere';") 286 | 287 | def testSingleCommentEnd(self): 288 | js = r'// a comment\n' 289 | self.assertMinified(js, '') 290 | 291 | def testInputStream(self): 292 | try: 293 | from StringIO import StringIO 294 | except ImportError: 295 | from io import StringIO 296 | 297 | ins = StringIO(r''' 298 | function foo('') { 299 | 300 | } 301 | ''') 302 | outs = StringIO() 303 | m = jsmin.JavascriptMinify() 304 | m.minify(ins, outs) 305 | output = outs.getvalue() 306 | assert output == "function foo(''){}" 307 | 308 | def testUnicode(self): 309 | instr = u'\u4000 //foo' 310 | expected = u'\u4000' 311 | output = jsmin.jsmin(instr) 312 | self.assertEqual(output, expected) 313 | 314 | def testCommentBeforeEOF(self): 315 | self.assertMinified("//test\r\n", "") 316 | 317 | def testCommentInObj(self): 318 | self.assertMinified("""{ 319 | a: 1,//comment 320 | }""", "{a:1,}") 321 | 322 | def testCommentInObj2(self): 323 | self.assertMinified("{a: 1//comment\r\n}", "{a:1}") 324 | 325 | def testImplicitSemicolon(self): 326 | # return \n 1 is equivalent with return; 1 327 | # so best make sure jsmin retains the newline 328 | self.assertMinified("return\na", "return\na") 329 | 330 | def test_explicit_semicolon(self): 331 | self.assertMinified("return;//comment\r\na", "return;a") 332 | 333 | def testImplicitSemicolon2(self): 334 | self.assertMinified("return//comment...\r\nar", "return\nar") 335 | 336 | def testImplicitSemicolon3(self): 337 | self.assertMinified("return//comment...\r\na", "return\na") 338 | 339 | def testSingleComment2(self): 340 | self.assertMinified('x.replace(/\//, "_")// slash to underscore', 341 | 'x.replace(/\//,"_")') 342 | 343 | def testSlashesNearComments(self): 344 | original = ''' 345 | { a: n / 2, } 346 | // comment 347 | ''' 348 | expected = '''{a:n/2,}''' 349 | self.assertMinified(original, expected) 350 | 351 | def testReturn(self): 352 | original = ''' 353 | return foo;//comment 354 | return bar;''' 355 | expected = 'return foo;return bar;' 356 | self.assertMinified(original, expected) 357 | original = ''' 358 | return foo 359 | return bar;''' 360 | expected = 'return foo\nreturn bar;' 361 | self.assertMinified(original, expected) 362 | 363 | def test_space_plus(self): 364 | original = '"s" + ++e + "s"' 365 | expected = '"s"+ ++e+"s"' 366 | self.assertMinified(original, expected) 367 | 368 | def test_no_final_newline(self): 369 | original = '"s"' 370 | expected = '"s"' 371 | self.assertMinified(original, expected) 372 | 373 | def test_space_with_regex_repeats(self): 374 | original = '/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' 375 | self.assertMinified(original, original) # there should be nothing jsmin can do here 376 | 377 | def test_space_with_regex_repeats_not_at_start(self): 378 | original = 'aaa;/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' 379 | self.assertMinified(original, original) # there should be nothing jsmin can do here 380 | 381 | def test_space_in_regex(self): 382 | original = '/a (a)/.test("a")' 383 | self.assertMinified(original, original) 384 | 385 | def test_brackets_around_slashed_regex(self): 386 | original = 'function a() { /\//.test("a") }' 387 | expected = 'function a(){/\//.test("a")}' 388 | self.assertMinified(original, expected) 389 | 390 | def test_angular_1(self): 391 | original = '''var /** holds major version number for IE or NaN for real browsers */ 392 | msie, 393 | jqLite, // delay binding since jQuery could be loaded after us.''' 394 | minified = jsmin.jsmin(original) 395 | self.assertTrue('var\nmsie' in minified) 396 | 397 | def test_angular_2(self): 398 | original = 'var/* comment */msie;' 399 | expected = 'var msie;' 400 | self.assertMinified(original, expected) 401 | 402 | def test_angular_3(self): 403 | original = 'var /* comment */msie;' 404 | expected = 'var msie;' 405 | self.assertMinified(original, expected) 406 | 407 | def test_angular_4(self): 408 | original = 'var /* comment */ msie;' 409 | expected = 'var msie;' 410 | self.assertMinified(original, expected) 411 | 412 | def test_angular_5(self): 413 | original = 'a/b' 414 | self.assertMinified(original, original) 415 | 416 | def testBackticks(self): 417 | original = '`test`' 418 | self.assertMinified(original, original, quote_chars="'\"`") 419 | 420 | original = '` test with leading whitespace`' 421 | self.assertMinified(original, original, quote_chars="'\"`") 422 | 423 | original = '`test with trailing whitespace `' 424 | self.assertMinified(original, original, quote_chars="'\"`") 425 | 426 | original = '''`test 427 | with a new line`''' 428 | self.assertMinified(original, original, quote_chars="'\"`") 429 | 430 | original = '''dumpAvStats: function(stats) { 431 | var statsString = ""; 432 | if (stats.mozAvSyncDelay) { 433 | statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `; 434 | } 435 | if (stats.mozJitterBufferDelay) { 436 | statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`; 437 | } 438 | 439 | return React.DOM.div(null, statsString);''' 440 | expected = 'dumpAvStats:function(stats){var statsString="";if(stats.mozAvSyncDelay){statsString+=`A/V sync: ${stats.mozAvSyncDelay} ms `;}\nif(stats.mozJitterBufferDelay){statsString+=`Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;}\nreturn React.DOM.div(null,statsString);' 441 | self.assertMinified(original, expected, quote_chars="'\"`") 442 | 443 | def testBackticksExpressions(self): 444 | original = '`Fifteen is ${a + b} and not ${2 * a + b}.`' 445 | self.assertMinified(original, original, quote_chars="'\"`") 446 | 447 | original = '''`Fifteen is ${a + 448 | b} and not ${2 * a + "b"}.`''' 449 | self.assertMinified(original, original, quote_chars="'\"`") 450 | 451 | def testBackticksTagged(self): 452 | original = 'tag`Hello ${ a + b } world ${ a * b}`;' 453 | self.assertMinified(original, original, quote_chars="'\"`") 454 | 455 | def test_issue_bitbucket_16(self): 456 | original = """ 457 | f = function() { 458 | return /DataTree\/(.*)\//.exec(this._url)[1]; 459 | } 460 | """ 461 | self.assertMinified( 462 | original, 463 | 'f=function(){return /DataTree\/(.*)\//.exec(this._url)[1];}') 464 | 465 | def test_issue_bitbucket_17(self): 466 | original = "// hi\n/^(get|post|head|put)$/i.test('POST')" 467 | self.assertMinified(original, 468 | "/^(get|post|head|put)$/i.test('POST')") 469 | 470 | def test_issue_6(self): 471 | original = ''' 472 | respond.regex = { 473 | comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi, 474 | urls: 'whatever' 475 | }; 476 | ''' 477 | expected = original.replace(' ', '').replace('\n', '') 478 | self.assertMinified(original, expected) 479 | 480 | def test_issue_9(self): 481 | original = '\n'.join([ 482 | 'var a = \'hi\' // this is a comment', 483 | 'var a = \'hi\' /* this is also a comment */', 484 | 'console.log(1) // this is a comment', 485 | 'console.log(1) /* this is also a comment */', 486 | '1 // this is a comment', 487 | '1 /* this is also a comment */', 488 | '{} // this is a comment', 489 | '{} /* this is also a comment */', 490 | '"YOLO" /* this is a comment */', 491 | '"YOLO" // this is a comment', 492 | '(1 + 2) // comment', 493 | '(1 + 2) /* yup still comment */', 494 | 'var b' 495 | ]) 496 | expected = '\n'.join([ 497 | 'var a=\'hi\'', 498 | 'var a=\'hi\'', 499 | 'console.log(1)', 500 | 'console.log(1)', 501 | '1', 502 | '1', 503 | '{}', 504 | '{}', 505 | '"YOLO"', 506 | '"YOLO"', 507 | '(1+2)', 508 | '(1+2)', 509 | 'var b' 510 | ]) 511 | self.assertMinified(expected, expected) 512 | self.assertMinified(original, expected) 513 | 514 | def test_newline_between_strings(self): 515 | self.assertMinified('"yolo"\n"loyo"', '"yolo"\n"loyo"') 516 | 517 | def test_issue_10_comments_between_tokens(self): 518 | self.assertMinified('var/* comment */a', 'var a') 519 | 520 | def test_ends_with_string(self): 521 | self.assertMinified('var s = "s"', 'var s="s"') 522 | 523 | def test_short_comment(self): 524 | self.assertMinified('a;/**/b', 'a;b') 525 | 526 | def test_shorter_comment(self): 527 | self.assertMinified('a;/*/*/b', 'a;b') 528 | 529 | def test_block_comment_with_semicolon(self): 530 | self.assertMinified('a;/**/\nb', 'a;b') 531 | 532 | def test_block_comment_With_implicit_semicolon(self): 533 | self.assertMinified('a/**/\nvar b', 'a\nvar b') 534 | 535 | def test_issue_9_single_comments(self): 536 | original = ''' 537 | var a = "hello" // this is a comment 538 | a += " world" 539 | ''' 540 | self.assertMinified(original, 'var a="hello"\na+=" world"') 541 | 542 | def test_issue_9_multi_comments(self): 543 | original = ''' 544 | var a = "hello" /* this is a comment */ 545 | a += " world" 546 | ''' 547 | self.assertMinified(original, 'var a="hello"\na+=" world"') 548 | 549 | def test_issue_12_re_nl_if(self): 550 | original = ''' 551 | var re = /\d{4}/ 552 | if (1) { console.log(2); }''' 553 | self.assertMinified( 554 | original, 'var re=/\d{4}/\nif(1){console.log(2);}') 555 | 556 | def test_issue_12_re_nl_other(self): 557 | original = ''' 558 | var re = /\d{4}/ 559 | g = 10''' 560 | self.assertMinified(original , 'var re=/\d{4}/\ng=10') 561 | 562 | def test_preserve_copyright(self): 563 | original = ''' 564 | function this() { 565 | /*! Copyright year person */ 566 | console.log('hello!'); 567 | } 568 | 569 | /*! Copyright blah blah 570 | * 571 | * Some other text 572 | */ 573 | 574 | var a; 575 | ''' 576 | expected = """function this(){/*! Copyright year person */ 577 | console.log('hello!');}/*! Copyright blah blah 578 | * 579 | * Some other text 580 | */\n\nvar a;""" 581 | self.assertMinified(original, expected) 582 | 583 | def test_issue_14(self): 584 | original = 'return x / 1;' 585 | self.assertMinified(original, 'return x/1;') 586 | 587 | def test_issue_14_with_char_from_return(self): 588 | original = 'return r / 1;' 589 | self.assertMinified(original, 'return r/1;') 590 | 591 | 592 | class RegexTests(unittest.TestCase): 593 | 594 | def regex_recognise(self, js): 595 | if not jsmin.is_3: 596 | if jsmin.cStringIO and not isinstance(js, unicode): 597 | # strings can use cStringIO for a 3x performance 598 | # improvement, but unicode (in python2) cannot 599 | klass = jsmin.cStringIO.StringIO 600 | else: 601 | klass = jsmin.StringIO.StringIO 602 | else: 603 | klass = jsmin.io.StringIO 604 | ins = klass(js[2:]) 605 | outs = klass() 606 | jsmin.JavascriptMinify(ins, outs).regex_literal(js[0], js[1]) 607 | return outs.getvalue() 608 | 609 | def assert_regex(self, js_input, expected): 610 | assert js_input[0] == '/' # otherwise we should not be testing! 611 | recognised = self.regex_recognise(js_input) 612 | assert recognised == expected, "\n in: %r\ngot: %r\nexp: %r" % (js_input, recognised, expected) 613 | 614 | def test_simple(self): 615 | self.assert_regex('/123/g', '/123/') 616 | 617 | def test_character_class(self): 618 | self.assert_regex('/a[0-9]b/g', '/a[0-9]b/') 619 | 620 | def test_character_class_with_slash(self): 621 | self.assert_regex('/a[/]b/g', '/a[/]b/') 622 | 623 | def test_escaped_forward_slash(self): 624 | self.assert_regex(r'/a\/b/g', r'/a\/b/') 625 | 626 | def test_escaped_back_slash(self): 627 | self.assert_regex(r'/a\\/g', r'/a\\/') 628 | 629 | def test_empty_character_class(self): 630 | # This one is subtle: an empty character class is not allowed, afaics 631 | # from http://regexpal.com/ Chrome Version 44.0.2403.155 (64-bit) Mac 632 | # so this char class is interpreted as containing ]/ *not* as char 633 | # class [] followed by end-of-regex /. 634 | self.assert_regex('/a[]/]b/g', '/a[]/]b/') 635 | 636 | def test_precedence_of_parens(self): 637 | # judging from 638 | # http://regexpal.com/ Chrome Version 44.0.2403.155 (64-bit) Mac 639 | # () have lower precedence than [] 640 | self.assert_regex('/a([)])b/g', '/a([)])b/') 641 | self.assert_regex('/a[(]b/g', '/a[(]b/') 642 | 643 | if __name__ == '__main__': 644 | unittest.main() 645 | --------------------------------------------------------------------------------