├── .gitignore ├── LICENSE ├── symbols ├── server-red.svg ├── cloud-blue.svg ├── cloud-red.svg ├── server-grey.svg ├── router-blue.svg ├── router-red.svg ├── pc-grey.svg ├── pc-red.svg ├── ground-sw-red.svg ├── ground-sw-blue.svg ├── firewall-blue.svg ├── multilayer-switch-red.svg ├── multilayer-switch-blue.svg ├── rsp-red.svg ├── rsp-blue.svg └── firewall-red.svg ├── colorschemes.py ├── README.md ├── custom_style.py ├── argparser.py └── gns3theme.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | test/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 n3oxmind 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 | -------------------------------------------------------------------------------- /symbols/server-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /colorschemes.py: -------------------------------------------------------------------------------- 1 | """ 2 | File contains a list of predefined color schemes. Here where you can add/change gns3 color schemes 3 | where 4 | fg: forground color(text) 5 | fg2: secondary forground color(header text) 6 | bg: background color of main window 7 | bg2: secondary background color of sidebars, menus, fields, ...,etc 8 | sbg: selection background color 9 | sfg: selection forground color 10 | tbg: toolbar background color 11 | bbg: button background color 12 | bfg: button forground color 13 | lc: link color 14 | lw: link width 15 | gc: grid color 16 | color: theme color [light,dark] 17 | """ 18 | 19 | schemes = { 20 | 'solarized-light': { 21 | 'bg': '#fdf6e3', 22 | 'bg2': '#eee8d5', 23 | 'fg': '#657b83', 24 | 'fg2': '#0087ff', 25 | 'sbg': '#0087ff', 26 | 'sfg': '#e4e4e4', 27 | 'tbg': '#3338d5', 28 | 'bbg': '#d70000', 29 | 'bfg': '#1d2021', 30 | 'lc': '#657b83', 31 | 'lw': 1.2, 32 | 'gc': '#e6e6e6', 33 | 'color': 'light', 34 | }, 35 | 'solarized-dark': { 36 | 'bg': '#002b36', 37 | 'bg2': '#073642', 38 | 'fg': '#839496', 39 | 'fg2': '#0087ff', 40 | 'tbg': '#073642', 41 | 'sbg': '#d75f00', 42 | 'sfg': '#1c1c1c', 43 | 'bbg': '#8a8a8a', 44 | 'bfg': '#1d2021', 45 | 'lc': '#839496', 46 | 'lw': 1.2, 47 | 'gc': '#003d4d', 48 | 'color': 'dark', 49 | }, 50 | 'n30x-dark': { 51 | 'bg': '#252525', 52 | 'bg2': '#2a2a2a', 53 | 'fg': '#00997a', 54 | 'fg2': '#b7855f', 55 | 'tbg': '#404040', 56 | 'sbg': '#323232', 57 | 'sfg': '#9575cd', 58 | 'bbg': '#c2185b', 59 | 'bfg': '#1a1a1a', 60 | 'lc': '#939393', 61 | 'lw': 1.2, 62 | 'gc': '#323232', 63 | 'color': 'dark', 64 | }, 65 | 'n30x-darker': { 66 | 'bg': '#0d0d0d', 67 | 'bg2': '#141414', 68 | 'fg': '#008066', 69 | 'fg2': '#2979ff', 70 | 'tbg': '#181818', 71 | 'sbg': '#000000', 72 | 'sfg': '#ba4551', 73 | 'bbg': '#161616', 74 | 'bfg': '#b3b3b3', 75 | 'lc': '#008066', 76 | 'lw': 1.2, 77 | 'gc': '#181818', 78 | 'color': 'dark', 79 | }, 80 | 'n30x-darkblue': { 81 | 'bg': '#28283e', 82 | 'bg2': '#26263e', 83 | 'fg': '#00997a', 84 | 'fg2': '#934806', 85 | 'tbg': '#20203a', 86 | 'sbg': '#22223e', 87 | 'sfg': '#c46008', 88 | 'bbg': '#24243e', 89 | 'bfg': '#00997a', 90 | 'lc': '#939393', 91 | 'lw': 1.2, 92 | 'gc': '#32324e', 93 | 'color': 'dark', 94 | }, 95 | 'n30x-light': { 96 | 'bg': '#fafafa', 97 | 'bg2': '#eeeeee', 98 | 'fg': '#424242', 99 | 'fg2': '#03a9f4', 100 | 'tbg': '#eeeeee', 101 | 'sbg': '#03a9f4', 102 | 'sfg': '#ffffff', 103 | 'bbg': '#e91e63', 104 | 'bfg': '#1a1a1a', 105 | 'lc': '#424242', 106 | 'lw': 1.2, 107 | 'gc': '#e6e6e6', 108 | 'color': 'light', 109 | }, 110 | 'tomorrow': { 111 | 'bg': '#ffffff', 112 | 'bg2': '#f2f2f2', 113 | 'fg': '#4d4d4d', 114 | 'fg2': '#0087ff', 115 | 'tbg': '#cccccc', 116 | 'sbg': '#d6d6d6', 117 | 'sfg': '#4271ae', 118 | 'bbg': '#3e999f', 119 | 'bfg': '#ffffff', 120 | 'lc': '#4d4d4d', 121 | 'lw': 1.2, 122 | 'gc': '#e6e6e6', 123 | 'color': 'light', 124 | }, 125 | } 126 | -------------------------------------------------------------------------------- /symbols/cloud-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /symbols/cloud-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /symbols/server-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /symbols/router-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 62 | 67 | 72 | 76 | 81 | 86 | 91 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /symbols/router-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 62 | 67 | 72 | 76 | 81 | 86 | 91 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /symbols/pc-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /symbols/pc-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gns3theme 2 | 3 | gns3theme is python tool that will adds a custom theme to gns3. Support both Linux and MacOS. 4 | - Change gns3 theme from a predefined schemes 5 | - Change ethernet/serial link width and color 6 | - Apply full transparency to gns3-gui (not implemented yet) 7 | - Create a custom gns3-gui theme. 8 | 9 | ### Installation 10 | ```sh 11 | git clone https://github.com/n3oxmind/gns3theme.git 12 | cd gns3theme 13 | sudo ./gns3theme.py --install /path/to/gns3/installation/directory -u $USER --scheme 14 | ./gns3theme.py --scheme 15 | ``` 16 | Add custom theme `./gns3theme.py -s `. Check `./gns3theme.py --ls` for supported colorschemes. 17 | Start gns3 and choose Edit->Preferences->General->Interface Style->Custom 18 | 19 | ### Change grid color and/or link color (only with --install as root) 20 | ```sh 21 | sudo ./gns3theme.py --install /path/to/gns3/installation/directory -u $USER --scheme --lc ffffff --gc 000000 22 | ``` 23 | 24 | **Note**: `/path/to/gns3/installation/directory` is your gns3 installation directory. For example my gns3 is installed under `$HOME/.local/lib/python3.10/site-package`. gns3theme does not need the gns3-gui source code anymore. 25 | 26 | ### List default colorschemes 27 | ```sh 28 | $ ./gns3theme.py --ls 29 | solarized-light 30 | solarized-dark 31 | n30x-dark 32 | n30x-darker 33 | n30x-darkblue 34 | 35 | Add your colorscheme in 'colorschemes.py' file 36 | ``` 37 | 38 | ### Install n30x-dark 39 | ```sh 40 | $ ./gns3theme.sh -s n30x-dark 41 | ``` 42 | ![n30x-dark3](https://user-images.githubusercontent.com/10103340/44069564-3681323a-9f34-11e8-9f6c-7d458b0298bf.png) 43 | 44 | 45 | ### Install solarized-light theme 46 | ```sh 47 | $ gns3theme -s solarized-light 48 | ``` 49 | ![solarized-light](https://user-images.githubusercontent.com/10103340/44070067-9d04544a-9f36-11e8-9793-e73522e9002b.png) 50 | 51 | 52 | ### Install tomorrow theme 53 | ```sh 54 | $ gns3theme -s tomorrow 55 | ``` 56 | ![tomorrow](https://user-images.githubusercontent.com/10103340/44069498-f4c867aa-9f33-11e8-8ca1-82a26cca134e.png) 57 | 58 | 59 | ### Install n30x-light 60 | ```sh 61 | $ gns3theme -s n30x-light 62 | ``` 63 | ![n30x-light](https://user-images.githubusercontent.com/10103340/44069475-d54f28be-9f33-11e8-8a0e-f1fc3bf889c1.png) 64 | 65 | 66 | ### Tips 67 | `./gns3theme.py --install ` is only required on first installation and if you want to make/apply changes to grid color. 68 | 69 | Changing colorscheme is as simple as `./gns3theme.py -s n30x-darker`. To further customize specific colorscheme use --bg, bg2, --fg, --fg2 ..., etc options to target specific ui element. For example, if you like **n30x-dark** theme and you want to change the selection background color, you can achieve this as below: 70 | ```sh 71 | $ ./gns3theme.py -s n30x-dark --sbg ffffff 72 | ``` 73 | custom colorscheme file is stored in `~/.config/gns3theme/custom_style.css`. You can change any color manually with your fav text editor. 74 | 75 | To add more colorschemes follow the format in `colorschmes.py` and add as many colorschmes as you want. 76 | 77 | If you like the icons that i'm using in the screenshots. Copy the **symbols** folder to your GNS3 directory. They will appear under the custom_symbols when you right-click on any appliance. you can change individual devices manually or make them as default icons from gns3 preferences. 78 | 79 | ### gns3theme usage 80 | ```sh 81 | usage: gns3theme --install --scheme 82 | gns3theme --scheme [options] 83 | 84 | manditory arguments: 85 | -i, --install PATH path to gns3-gui source directory 86 | -s, --scheme NAME choose color scheme to apply for gns3-gui 87 | -u, --user USER Specify username for installation. only used with --install option 88 | 89 | optional arguments: 90 | --bg COLOR change primary background color 91 | --bg2 COLOR change secondary background color 92 | --fg COLOR change primary foreground color 93 | --fg2 COLOR change secondary foreground color 94 | --tbg COLOR change toolbar background color 95 | --sbg COLOR change selection background color 96 | --sfg COLOR change selection foreground color 97 | --bbg COLOR change button background color 98 | --lc COLOR change ethernet link color. Reinstall gns3-gui as root is required 99 | --lw NUM change ethernet and serial links width. Reinstall gns3-gui as root is required 100 | --gc COLOR change grid color. Reinstall gns3-gui as root is required 101 | 102 | optional flags: 103 | --help show this help 104 | --ls list predefined color schemes 105 | ``` 106 | -------------------------------------------------------------------------------- /symbols/ground-sw-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 56 | 57 | 58 | 60 | 65 | 69 | 74 | 78 | 83 | 88 | 89 | 93 | 97 | 101 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /custom_style.py: -------------------------------------------------------------------------------- 1 | # custom css style 2 | # shortcut for long dict key 3 | 4 | selector00 = 'QWidget' 5 | selector01 = 'QMenuBar::item' 6 | selector02 = 'QDockWidget::title' 7 | selector03 = 'QDockWidget,QMenuBar' 8 | selector04 = "QTextEdit,QPlainTextEdit,QLineEdit,QSpinBox,QComboBox" 9 | selector05 = 'QTextEdit#uiConsoleTextEdit' 10 | selector06 = 'QTabWidget' 11 | selector07 = 'QTabBar::tab' 12 | selector08 = 'QTabBar::tab:selected' 13 | selector09 = 'QGroupBox' 14 | selector10 = 'QMainWindow::separator' 15 | selector11 = 'QComboBox' 16 | selector12 = 'QToolBar' 17 | selector13 = 'QPushButton' 18 | selector14 = 'QToolButton' 19 | selector15 = 'QTreeWidget,QlistWidget' 20 | selector16 = 'QTreeWidget#uiTreeWidget' 21 | selector17 = "QTreeWidget::item:selected,QTreeWidget::item:hover,QMenu::item:selected,QToolButton::hover,QPushButton::hover,QTabBar::tab:hover" 22 | selector18 = 'QMenu' 23 | selector19 = 'QLabel' 24 | selector20 = 'QLabel#uiTitleLabel' 25 | selector21 = 'QAbstractScrollArea' 26 | selector22 = 'QScrollBar::handle:vertical' 27 | selector23 = 'QScrollBar::handle:horizontal' 28 | selector24 = 'QScrollBar::horizontal' 29 | selector25 = 'QScrollBar::vertical' 30 | selector26 = "QScrollBar::up-arrow:vertical,QScrollBar::down-arrow:vertical,QScrollBar::down-arrow:horizontal,QScrollBar::up-arrow:horizontal" 31 | selector27= "QScrollBar::add-page:horizontal,QScrollBar::sub-page:horizontal,QScrollBar::add-page:vertical,QScrollBar::sub-page:vertical" 32 | selector28 = 'QStatusBar' 33 | selector29 = 'QRadioButton,QCheckBox' 34 | selector30 = "QRadioButton::disabled,QCheckBox::disabled" 35 | 36 | custom_style = { 37 | selector00: { 38 | 'background-color': '#fbf1c7' 39 | }, 40 | selector01: { 41 | 'background-color': '#fbf1c7' 42 | }, 43 | selector02: { 44 | 'background': '#d5c4a1', 45 | 'padding-left': '5px' 46 | }, 47 | selector03: { 48 | 'color': '#282828', 49 | 'font': 'bold 14px' 50 | }, 51 | selector04: { 52 | 'background-color': '#d5c4a1', 53 | 'color': '#282828' 54 | }, 55 | selector05: { 56 | 'backgroun-color': '#fbf1c7', 57 | 'color': '#282828', 58 | 'font': 'bold 14px' 59 | }, 60 | selector06: { 61 | 'font': '14px', 62 | 'border-top': '2px' 63 | }, 64 | selector07: { 65 | 'background': '#d5c4a1', 66 | 'color': '#282828', 67 | 'min-width': '8ex', 68 | 'padding': '2px', 69 | 'border-top-right-radius': '6px', 70 | 'border-top-left-radius': '6px' 71 | }, 72 | selector08: { 73 | 'background': '#458588', 74 | 'color': '#ffffff' 75 | }, 76 | selector09: { 77 | 'color': '#076678', 78 | 'font': '14px', 79 | 'padding': '15px', 80 | 'border-style': 'none' 81 | }, 82 | selector10: { 83 | 'background': '#d5c4a1', 84 | 'width': '1px', 85 | 'height': '1px' 86 | }, 87 | selector11: { 88 | 'selection-background-color': '#458588', 89 | 'selection-color': '#ffffff' 90 | }, 91 | selector12: { 92 | 'background': '#d5c4a1', 93 | 'border': '0px' 94 | }, 95 | selector13: { 96 | 'background-color': '#d5c4a1', 97 | 'color': '#181818', 98 | 'font': '14px' 99 | }, 100 | selector14: { 101 | 'background-color': '#d5c4a1', 102 | 'color': '#181818', 103 | 'font': '14px' 104 | }, 105 | selector15: { 106 | 'background-color': '#fbf1c7', 107 | 'color': '#282828', 108 | 'alternate-background-color': '#d5c4a1', 109 | 'font': '14px' 110 | }, 111 | selector16: { 112 | 'background-color': '#d5c4a1', 113 | 'color': '#282828', 114 | 'font': 'bold 14px' 115 | }, 116 | selector17: { 117 | 'background-color': '#458588', 118 | 'color': '#fafafa' 119 | }, 120 | selector18: { 121 | 'background-color': '#458588', 122 | 'color': '#282828' 123 | }, 124 | selector19: { 125 | 'font': '14px', 126 | 'color': '#282828' 127 | }, 128 | selector20: { 129 | 'font': 'bold 16px', 130 | 'color': '#282828' 131 | }, 132 | selector21: { 133 | 'background': '#fbf1c7' 134 | }, 135 | selector22: { 136 | 'background': '#d5c4a1', 137 | 'min-width': '20px' 138 | }, 139 | selector23: { 140 | 'background': '#d5c4a1', 141 | 'min-width': '20px' 142 | }, 143 | selector24: { 144 | 'height': '6px' 145 | }, 146 | selector25: { 147 | 'width': '6px' 148 | }, 149 | selector26: { 150 | 'border': '0px', 151 | 'height': '0px', 152 | 'width': '0px' 153 | }, 154 | selector27: { 155 | 'background': 'none' 156 | }, 157 | selector28: { 158 | 'background-color': '#d5c4a1', 159 | 'color': '#282828' 160 | }, 161 | selector29: { 162 | 'color': '#282828' 163 | }, 164 | selector30: { 165 | 'color': 'gray' 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /symbols/ground-sw-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 56 | 57 | 65 | 69 | 70 | 71 | 73 | 78 | 82 | 87 | 91 | 95 | 100 | 101 | 105 | 109 | 113 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /argparser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import shutil 3 | 4 | 5 | class CustomFormatter(argparse.RawDescriptionHelpFormatter): 6 | def _format_action(self, action): 7 | # determine the required width and the entry label 8 | term_size = shutil.get_terminal_size(fallback=(80, 24)) 9 | self._width = term_size.columns - 11 if term_size.columns < 140 else 129 10 | help_position = min(self._action_max_length + 2, 11 | self._max_help_position) 12 | help_width = max(self._width - help_position, 11) 13 | action_header = self._format_action_invocation(action) 14 | 15 | header_indent = 2 * ' ' 16 | max_action_header_width = 30 17 | custom_action_header = [i.strip(',') for i in action_header.split()] 18 | 19 | if not action.help: 20 | tup = self._current_indent, '', action_header 21 | action_header = '%*s%s\n' % tup 22 | 23 | elif custom_action_header[0][:2] == '--' and len(custom_action_header) > 1: 24 | custom_header_format = f"{custom_action_header[0]} {custom_action_header[1]}" 25 | action_header = f"{header_indent}{custom_header_format:<{max_action_header_width}}" 26 | 27 | elif len(custom_action_header) > 4: 28 | custom_header_format = f"{custom_action_header[0]}, {custom_action_header[4]} {custom_action_header[1]}" 29 | action_header = f"{header_indent}{custom_header_format:<{max_action_header_width}}" 30 | 31 | elif len(custom_action_header) == 4: 32 | custom_header_format = f"{custom_action_header[0]}, {custom_action_header[2]} {custom_action_header[1]}" 33 | action_header = f"{header_indent}{custom_header_format:<{max_action_header_width}}" 34 | else: 35 | action_header = f"{header_indent}{action_header:<{max_action_header_width}}" 36 | 37 | parts = [action_header] 38 | 39 | if action.help: 40 | help_text = self._expand_help(action) 41 | help_lines = self._split_lines(help_text, help_width) 42 | parts.append(f"{help_lines[0]}\n") 43 | for line in help_lines[1:]: 44 | parts.append(f"{header_indent}{' ':<{max_action_header_width}}{line}\n") 45 | 46 | elif not action_header.endswith('\n'): 47 | parts.append('\n') 48 | 49 | for subaction in self._iter_indented_subactions(action): 50 | parts.append(self._format_action(subaction)) 51 | return self._join_parts(parts) 52 | 53 | 54 | def usage(): 55 | """usage header""" 56 | usage = """%(prog)s --install --scheme 57 | %(prog)s --scheme [options]\n""" 58 | return usage 59 | 60 | 61 | def description(): 62 | description_format = "gns3theme is a script that will add a custom style for gns3-gui." 63 | return description_format 64 | 65 | 66 | parser = argparse.ArgumentParser(prog='gns3theme', usage=usage(), formatter_class=CustomFormatter, add_help=False) 67 | 68 | parser_group_manditory = parser.add_argument_group(title='manditory arguments') 69 | parser_group_options = parser.add_argument_group(title='optional arguments') 70 | parser_group_flags = parser.add_argument_group(title='optional flags') 71 | 72 | parser_group_options.add_argument('--bg', dest='bg', metavar='COLOR', 73 | help='change primary background color') 74 | parser_group_options.add_argument('--bg2', dest='bg2', metavar='COLOR', 75 | help='change secondary background color') 76 | parser_group_options.add_argument('--fg', dest='fg', metavar='COLOR', 77 | help='change primary foreground color') 78 | parser_group_options.add_argument('--fg2', dest='fg2', metavar='COLOR', 79 | help='change secondary foreground color') 80 | parser_group_options.add_argument('--tbg', dest='tbg', metavar='COLOR', 81 | help='change toolbar background color') 82 | parser_group_options.add_argument('--sbg', dest='sbg', metavar='COLOR', 83 | help='change selection background color') 84 | parser_group_options.add_argument('--sfg', dest='sfg', metavar='COLOR', 85 | help='change selection foreground color') 86 | parser_group_options.add_argument('--bbg', dest='bbg', metavar='COLOR', 87 | help='change button background color') 88 | parser_group_options.add_argument('--lc', dest='lc', metavar='COLOR', 89 | help='change ethernet link color. Reinstall gns3-gui as root is required') 90 | parser_group_options.add_argument('--lw', dest='lw', metavar='NUM', 91 | help='change ethernet and serial links width. Reinstall gns3-gui as root is required') 92 | parser_group_options.add_argument('--gc', dest='gc', metavar='COLOR', 93 | help='change grid color. Reinstall gns3-gui as root is required') 94 | 95 | parser_group_manditory.add_argument('-i', '--install', dest='install_scheme', metavar='PATH', 96 | help='path to gns3-gui source directory ') 97 | 98 | parser_group_manditory.add_argument('-s', '--scheme', dest='color_scheme', metavar='NAME', 99 | help='choose color scheme to apply for gns3-gui') 100 | parser_group_manditory.add_argument('-u', '--user', dest='username', metavar='USER', 101 | help='Specify username for installation. only used with --install option') 102 | 103 | parser_group_flags.add_argument("--help", action='help', help='show this help') 104 | parser_group_flags.add_argument('--ls', dest='list_schemes', action='store_true', 105 | help='list predefined color schemes') 106 | 107 | parser_group_manditory = parser.add_argument_group(title='manditory arguments') 108 | 109 | #args = parser.parse_args() 110 | #parser.print_help() 111 | -------------------------------------------------------------------------------- /symbols/firewall-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /symbols/multilayer-switch-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 53 | 55 | 57 | 61 | 62 | 64 | 68 | 69 | 71 | 75 | 76 | 78 | 82 | 83 | 85 | 89 | 90 | 92 | 96 | 97 | 99 | 103 | 104 | 106 | 110 | 111 | 112 | 115 | 120 | 125 | 131 | 132 | 137 | 142 | 143 | 148 | 153 | 154 | 159 | 164 | 169 | 174 | 179 | 184 | 189 | 194 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /symbols/multilayer-switch-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 53 | 55 | 57 | 61 | 62 | 64 | 68 | 69 | 71 | 75 | 76 | 78 | 82 | 83 | 85 | 89 | 90 | 92 | 96 | 97 | 99 | 103 | 104 | 106 | 110 | 111 | 112 | 115 | 120 | 125 | 131 | 132 | 137 | 142 | 143 | 148 | 153 | 154 | 159 | 164 | 169 | 174 | 179 | 184 | 189 | 194 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /symbols/rsp-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 53 | 55 | 57 | 61 | 62 | 64 | 68 | 69 | 71 | 75 | 76 | 78 | 82 | 83 | 85 | 89 | 90 | 92 | 96 | 97 | 99 | 103 | 104 | 106 | 110 | 111 | 112 | 115 | 120 | 125 | 130 | 131 | 136 | 142 | 143 | 148 | 153 | 154 | 159 | 164 | 169 | 170 | 175 | 180 | 181 | 186 | 191 | 196 | 201 | 206 | 211 | 216 | 221 | 226 | 231 | 236 | 241 | 246 | 251 | 256 | 261 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /symbols/rsp-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 53 | 55 | 57 | 61 | 62 | 64 | 68 | 69 | 71 | 75 | 76 | 78 | 82 | 83 | 85 | 89 | 90 | 92 | 96 | 97 | 99 | 103 | 104 | 106 | 110 | 111 | 112 | 115 | 120 | 125 | 130 | 131 | 136 | 142 | 143 | 148 | 153 | 154 | 159 | 164 | 169 | 170 | 175 | 180 | 181 | 186 | 191 | 196 | 201 | 206 | 211 | 216 | 221 | 226 | 231 | 236 | 241 | 246 | 251 | 256 | 261 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /symbols/firewall-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 56 | 57 | 59 | 62 | 63 | 65 | 68 | 69 | 71 | 74 | 75 | 77 | 80 | 81 | 83 | 86 | 87 | 89 | 92 | 93 | 95 | 98 | 99 | 100 | 103 | 108 | 112 | 117 | 122 | 123 | 127 | 132 | 137 | 138 | 143 | 148 | 153 | 157 | 162 | 167 | 168 | 172 | 177 | 182 | 183 | 187 | 192 | 197 | 198 | 202 | 207 | 212 | 213 | 217 | 222 | 227 | 228 | 232 | 237 | 242 | 243 | 248 | 253 | 258 | 263 | 268 | 273 | 278 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /gns3theme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import os 5 | import sys 6 | from pathlib import Path 7 | from colorschemes import schemes 8 | from argparser import parser 9 | import custom_style as css 10 | import subprocess 11 | 12 | 13 | def is_valid_color(color): 14 | """validate color""" 15 | if re.fullmatch(r'#[0-9a-fA-f]{6}', color): 16 | return True 17 | return False 18 | 19 | 20 | def is_valid_opacity(opacity_num): 21 | """validate opacity""" 22 | if re.fullmatch(r'0\.[0-9]{,2}|1.0', opacity_num): 23 | return True 24 | return False 25 | 26 | 27 | def is_root(): 28 | """Check if the script has been run as root""" 29 | if os.getuid() != 0: 30 | return False 31 | return True 32 | 33 | 34 | def print_colorschemes(): 35 | """ Print colorschemes """ 36 | for th_name, th_color in schemes.items(): 37 | print(f"{th_name}") 38 | print("\nAdd your colorscheme in 'colorschemes.py' file") 39 | 40 | 41 | def is_valid_scheme(scheme): 42 | """ validate color scheme """ 43 | if scheme not in schemes: 44 | print(f"\033[91mSchemeValidation\033[0m: Invalid colorscheme '{scheme}'. " 45 | f"Use 'gns3theme --list' to list available colorschemes") 46 | 47 | 48 | def hex_to_rgb(hex_color, nobraces=False, tup=False): 49 | """convert hex color to rgb""" 50 | is_ok = is_valid_color(hex_color) 51 | if is_ok: 52 | hc = hex_color.lstrip("#") 53 | rgb = tuple(int(hc[i:i+2], 16) for i in range(0, len(hc), 2)) 54 | else: 55 | print(f"\033[91ColorValidation\033[0m: Invalid color format '{hex_color}'") 56 | sys.exit(1) 57 | if tup: 58 | return rgb 59 | elif nobraces: 60 | return str(rgb).strip(')').strip('(') 61 | return str(rgb) 62 | 63 | 64 | def update_gns3_ui(scheme, src_dir, username, home_dir=None): 65 | """ 66 | Update gns3_gui files 67 | Args 68 | scheme (dict): colorscheme to apply 69 | src_dir (path): path to gns3_gui dir 70 | custom_style_path (str): custom_style.css save location 71 | Returns: 72 | True (bool): if succussfully update scheme 73 | False (bool): otherwise 74 | """ 75 | if sys.platform == 'linux': 76 | home_dir = Path("/home", username) 77 | elif sys.platform == 'darwin': 78 | home_dir = Path("/Users", username) 79 | custom_style_path = Path(home_dir, ".config/gns3theme/custom_style.css") 80 | # mkdir(Path(custom_style_path).parent) 81 | gns3_main_window_path = Path(src_dir, "gns3/main_window.py") 82 | gns3_style_path = Path(src_dir, "gns3/style.py") 83 | gns3_settings_path = Path(src_dir, "gns3/settings.py") 84 | gns3_graphics_view_path = Path(src_dir, "gns3/graphics_view.py") 85 | gns3_ethernet_link_item_path = Path(src_dir, "gns3/items/ethernet_link_item.py") 86 | 87 | # apply patch 88 | tab = ' '*4 89 | new_main_window = None 90 | new_settings = None 91 | new_style = None 92 | new_graphics_view = None 93 | print("\033[92mPatchFile\033[0m: Start Patching gns3_gui source code") 94 | with open(gns3_main_window_path, 'r') as fh: 95 | contents = fh.read() 96 | if not re.search(r"style.setCustomStyle", contents): 97 | new_main_window = re.sub(r"(\s+)(style.setCharcoalStyle\(\))", 98 | f'\\1\\2\n{tab*2}elif style_name == "Custom":\\1style.setCustomStyle()', 99 | contents, 100 | re.M) 101 | print("\033[92mPatchFile\033[0m: Updated gns3_gui main_window.py") 102 | else: 103 | print("\033[93mPatchFile\033[0m: File already patched gns3_gui main_window.py, skipping..") 104 | 105 | with open(gns3_settings_path, 'r') as fh: 106 | contents = fh.read() 107 | if not re.search(r"STYLES.*Custom", contents): 108 | new_settings = re.sub(r"(STYLES\s+=\s+\[)(.*?])", 109 | r'\1"Custom", \2', 110 | contents, 111 | re.M) 112 | print("\033[92mPatchFile\033[0m: Updated gns3_gui settings.py") 113 | else: 114 | print("\033[93mPatchFile\033[0m: File already patched gns3_gui settings.py, skipping..") 115 | 116 | add_style_func = (f"{tab}def setCustomStyle(self):\n" 117 | f"{tab*2}self.setClassicStyle()\n" 118 | f"{tab*2}style_file = QtCore.QFile(\"{custom_style_path}\")\n" 119 | f"{tab*2}style_file.open(QtCore.QFile.ReadOnly)\n" 120 | f"{tab*2}style = QtCore.QTextStream(style_file).readAll()\n" 121 | f"{tab*2}self._mw.setStyleSheet(style)\n") 122 | with open(gns3_style_path, 'r') as fh: 123 | contents = fh.read() 124 | if not re.search(r"setCustomStyle", contents): 125 | new_style = f"{contents}\n{add_style_func}" 126 | print("\033[92mPatchFile\033[0m: Updated gns3_gui style.py") 127 | else: 128 | print("\033[93mPatchFile\033[0m: File already patched gns3_gui style.py, skipping..") 129 | 130 | if scheme['gc'] != 'default': 131 | print("\033[92mPatchFile\033[0m: Changed gns3_gui grid color") 132 | if scheme['color'] == 'light': 133 | new_graphics_view = change_grid_color(scheme['gc'], dark=True, file_path=gns3_graphics_view_path) 134 | else: 135 | new_graphics_view = change_grid_color(scheme['gc'], light=True, file_path=gns3_graphics_view_path) 136 | 137 | if scheme['lc'] != 'default': 138 | new_ethernet_link_item = change_link_color(scheme['lc'], light=True, file_path=gns3_ethernet_link_item_path) 139 | print("\033[92mPatchFile\033[0m: Changed ethernet_link_item color") 140 | 141 | print("\033[92mPatchFile\033[0m: Finished patching gns3_gui source code") 142 | 143 | save_file(new_main_window, gns3_main_window_path) 144 | save_file(new_settings, gns3_settings_path) 145 | save_file(new_style, gns3_style_path) 146 | save_file(new_graphics_view, gns3_graphics_view_path) 147 | save_file(new_ethernet_link_item, gns3_ethernet_link_item_path) 148 | 149 | return True 150 | 151 | 152 | def cleanup(): 153 | """ 154 | Delete old gns3 gui installation. 155 | """ 156 | try: 157 | output = subprocess.run(['python3', '-m', 'pip', 'uninstall', 'gns3-gui', '-y'], stderr=subprocess.PIPE) 158 | except Exception as err: 159 | print(f"\033[91mCleanupError\033[0m: Failed to cleanup, {err}") 160 | else: 161 | if 'WARNING' in output.stderr.decode('utf-8'): 162 | print(f"\033[93mCleanup\033[0m: {output.stderr.decode('utf-8')}") 163 | else: 164 | print("\033[92mCleanup\033[0m: Removed old gns3_gui installation") 165 | 166 | 167 | def generate_custom_css(data=None): 168 | """ 169 | Create custom style 170 | Args: 171 | data (dir): css data represented in dict format 172 | """ 173 | print("\033[92mCSSFormat\033[0m: Generating new CSS format") 174 | css_str = [] 175 | if not data: 176 | data = css.custom_style 177 | for selectors, properties in data.items(): 178 | css_str.append(selectors+'{\n') 179 | for property, value in properties.items(): 180 | css_str.append(f"\t{property}: {value};\n") 181 | css_str.append("}\n") 182 | return ''.join(css_str) 183 | 184 | 185 | def update_style(scheme): 186 | """ 187 | Update default css data 188 | Args: 189 | scheme (dict): a predefined color schemes 190 | Returns: 191 | new_scheme (dict): new color scheme 192 | """ 193 | new_scheme = css.custom_style 194 | for p, v in scheme.items(): 195 | if p == 'bg': 196 | new_scheme[css.selector00]['background-color'] = v 197 | new_scheme[css.selector01]['background-color'] = v 198 | new_scheme[css.selector05]['background-color'] = v 199 | new_scheme[css.selector15]['background-color'] = v 200 | new_scheme[css.selector18]['background-color'] = v 201 | new_scheme[css.selector21]['background'] = v 202 | new_scheme[css.selector22]['background-color'] = v 203 | new_scheme[css.selector23]['background-color'] = v 204 | elif p == 'bg2': 205 | new_scheme[css.selector04]['background-color'] = v 206 | new_scheme[css.selector07]['background-color'] = v 207 | new_scheme[css.selector15]['background-color'] = v 208 | new_scheme[css.selector16]['background-color'] = v 209 | new_scheme[css.selector20]['color'] = v 210 | new_scheme[css.selector22]['background-color'] = v 211 | new_scheme[css.selector23]['background-color'] = v 212 | elif p == 'fg': 213 | new_scheme[css.selector03]['color'] = v 214 | new_scheme[css.selector04]['color'] = v 215 | new_scheme[css.selector05]['color'] = v 216 | new_scheme[css.selector07]['color'] = v 217 | new_scheme[css.selector15]['color'] = v 218 | new_scheme[css.selector16]['color'] = v 219 | new_scheme[css.selector18]['color'] = v 220 | new_scheme[css.selector19]['color'] = v 221 | new_scheme[css.selector28]['color'] = v 222 | new_scheme[css.selector29]['color'] = v 223 | elif p == 'fg2': 224 | new_scheme[css.selector09]['color'] = v 225 | elif p == 'tbg': 226 | new_scheme[css.selector02]['background'] = v 227 | new_scheme[css.selector10]['background'] = v 228 | new_scheme[css.selector12]['background'] = v 229 | new_scheme[css.selector14]['background-color'] = v 230 | new_scheme[css.selector28]['background-color'] = v 231 | elif p == 'sbg': 232 | new_scheme[css.selector11]['selection-background-color'] = v 233 | new_scheme[css.selector17]['background-color'] = v 234 | elif p == 'sfg': 235 | new_scheme[css.selector08]['color'] = v 236 | new_scheme[css.selector11]['selection-color'] = v 237 | new_scheme[css.selector17]['color'] = v 238 | elif p == 'bbg': 239 | new_scheme[css.selector13]['background-color'] = v 240 | new_scheme[css.selector08]['background'] = v 241 | elif p == 'bfg': 242 | new_scheme[css.selector13]['color'] = v 243 | new_scheme[css.selector14]['color'] = v 244 | new_scheme[css.selector08]['color'] = v 245 | elif p == 'lc': 246 | # not needed anymore 247 | # change_link_color(v) 248 | pass 249 | elif p == 'lw': 250 | # not implemented 251 | # change_link_width(v) 252 | pass 253 | new_scheme = generate_custom_css(new_scheme) 254 | return new_scheme 255 | 256 | 257 | def change_link_color(link_color, light=False, dark=False, file_path=None): 258 | """ 259 | Change link color based on the select theme. Only new projects will be affected. 260 | Old projects will maintains their original link color. 261 | """ 262 | 263 | with open(file_path, 'r') as fh: 264 | contents = fh.read() 265 | new_ethernet_link_item = re.sub(r"(\s+self.setPen\(QtGui.QPen\(QtGui.QColor\(\").*?(\"\).*)", 266 | f"\\1{link_color}\\2", 267 | contents, 268 | re.M) 269 | return new_ethernet_link_item 270 | 271 | 272 | def color_luminate(color, lighten=False, darken=False, lum=0): 273 | """ 274 | Get Lighter/Darker variant of hex color 275 | Args: 276 | color (str): hex/rgb color format 277 | lum (int): 0 to 1 for lighter color, 0 to -1 for darker color 278 | e.g. lum=0.2, lum=-0.5 279 | Returns: 280 | rr, gg, bb 281 | """ 282 | if lighten: 283 | lum = 0.1 284 | elif darken: 285 | lum = -0.08 286 | if is_valid_color(color): 287 | color = hex_to_rgb(color, tup=True) 288 | r = color[0] 289 | g = color[1] 290 | b = color[2] 291 | r = round(min(max(0, r + r * lum), 255)) 292 | g = round(min(max(0, g + g * lum), 255)) 293 | b = round(min(max(0, b + b * lum), 255)) 294 | return f"({r}, {g}, {b})" 295 | 296 | 297 | def change_grid_color(grid_color, dark=False, light=False, file_path=None): 298 | """ 299 | Change grid color to fit the current theme 300 | """ 301 | drawing_grid_color = hex_to_rgb(grid_color) 302 | node_grid_color = color_luminate(grid_color, darken=dark, lighten=light) 303 | gns3_graphics_view_path = file_path 304 | 305 | with open(gns3_graphics_view_path, 'r') as fh: 306 | contents = fh.read() 307 | new_graphics_view = re.sub(r"(\s+)(grids\s+=\s+.*?QtGui.QColor).*?\)", 308 | f"\\1\\2{drawing_grid_color}", 309 | contents, 310 | re.M) 311 | new_graphics_view = re.sub(r"(\s+)(\(self.nodeGridSize.*?QtGui.QColor).*?\)", 312 | f"\\1\\2{node_grid_color}", 313 | new_graphics_view, 314 | re.M) 315 | return new_graphics_view 316 | 317 | 318 | def save_file(data, file_path): 319 | """ 320 | Write data to the given file 321 | Args: 322 | data (str): data to be written to file 323 | file_path (str): absolut path to file 324 | """ 325 | if not data: 326 | return 327 | try: 328 | with open(file_path, 'w') as fh: 329 | fh.write(data) 330 | except TypeError as err: 331 | print(f"\033[91m{err}\033[0m") 332 | sys.exit(1) 333 | else: 334 | print(f"\033[920mCreateFile\033[0m: Created new \033[92m{file_path}\033[0m") 335 | return True 336 | 337 | 338 | def mkdir(dir_path): 339 | """ 340 | Create new directory if not exist 341 | """ 342 | try: 343 | Path.mkdir(Path(dir_path), parents=True, exist_ok=True) 344 | except OSError as err: 345 | print(err) 346 | sys.exit(1) 347 | else: 348 | return dir_path 349 | 350 | 351 | def is_dir_exists(dir_path): 352 | """check if dir exist""" 353 | if Path(dir_path).is_dir(): 354 | return True 355 | return False 356 | 357 | 358 | def install_gns3_gui(gns3_gui_dir): 359 | """ 360 | Apply changes and install 361 | """ 362 | # cleanup previous gns3_gui installation 363 | if not is_root(): 364 | print("\033[91mInstallError\033[0m: Please run as root") 365 | exit(1) 366 | cleanup() 367 | cmd = f"cd {gns3_gui_dir}; python3 setup.py install" 368 | try: 369 | subprocess.run(['sh', '-c', cmd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 370 | except Exception as err: 371 | print(f"\033[91mInstall\033[0m: Could not install gns3_gui, {err}") 372 | exit(1) 373 | else: 374 | print("\033[92mInstall\033[0m: Successfully installed gns3_gui") 375 | 376 | 377 | def parse_scheme_args(scheme, **kwargs): 378 | """parse schemes options supplied via command line""" 379 | if kwargs['bg']: scheme['bg'] = f"#{kwargs['bg'].strip('#')}" 380 | if kwargs['bg2']: scheme['bg2'] = f"#{kwargs['bg2'].strip('#')}" 381 | if kwargs['fg']: scheme['fg'] = f"#{kwargs['fg'].strip('#')}" 382 | if kwargs['fg2']: scheme['fg2'] = f"#{kwargs['fg2'].strip('#')}" 383 | if kwargs['tbg']: scheme['tbg'] = f"#{kwargs['tbg'].strip('#')}" 384 | if kwargs['sbg']: scheme['sbg'] = f"#{kwargs['sbg'].strip('#')}" 385 | if kwargs['sfg']: scheme['sfg'] = f"#{kwargs['sfg'].strip('#')}" 386 | if kwargs['bbg']: scheme['bbg'] = f"#{kwargs['bbg'].strip('#')}" 387 | if kwargs['lc']: scheme['lc'] = f"#{kwargs['lc'].strip('#')}" 388 | if kwargs['lw']: scheme['lw'] = f"#{kwargs['lw'].strip('#')}" 389 | if kwargs['gc']: scheme['gc'] = f"#{kwargs['gc'].strip('#')}" 390 | 391 | return scheme 392 | 393 | 394 | def main(): 395 | """main""" 396 | 397 | args = parser.parse_args() 398 | if args.install_scheme: 399 | gns3_gui_dir = args.install_scheme 400 | if not args.color_scheme: 401 | print("\033[91mInstallError\033[0m: --install requires --scheme option to work") 402 | exit(1) 403 | if not args.username: 404 | print("\033[91mInstallError\033[0m: --install requires --username option to work") 405 | exit(1) 406 | if not is_root(): 407 | print("\033[91mInstallError\033[0m: Please run as root") 408 | exit(1) 409 | is_valid_scheme(args.color_scheme) 410 | if not is_dir_exists(args.install_scheme): 411 | print("\033[91mInstallError\033[0m: Director does not exists '{args.install_scheme}'") 412 | 413 | scheme = schemes[args.color_scheme] 414 | scheme = parse_scheme_args(scheme, bg=args.bg, bg2=args.bg2, fg=args.fg, fg2=args.fg2, tbg=args.tbg, 415 | sbg=args.sbg, sfg=args.sfg, bbg=args.bbg, lc=args.lc, lw=args.lw, gc=args.gc) 416 | update_style(scheme) 417 | update_gns3_ui(scheme, gns3_gui_dir, args.username) 418 | #install_gns3_gui(gns3_gui_dir) 419 | elif args.color_scheme and not args.install_scheme: 420 | if is_root(): 421 | print("\033[91mInstallError\033[0m: Please run as nonroot") 422 | exit(1) 423 | custom_style_path = f"{Path.home()}/.config/gns3theme/custom_style.css" 424 | mkdir(Path(custom_style_path).parent) 425 | 426 | scheme = schemes[args.color_scheme] 427 | 428 | scheme = parse_scheme_args(scheme, bg=args.bg, bg2=args.bg2, fg=args.fg, fg2=args.fg2, tbg=args.tbg, 429 | sbg=args.sbg, sfg=args.sfg, bbg=args.bbg, lc=args.lc, lw=args.lw, gc=args.gc) 430 | style = update_style(scheme) 431 | save_file(style, custom_style_path) 432 | elif args.list_schemes: 433 | print_colorschemes() 434 | else: 435 | print("\033[91mOptionError\033[0m: --install/--scheme are required") 436 | exit(1) 437 | 438 | 439 | if __name__ == "__main__": 440 | main() 441 | --------------------------------------------------------------------------------