├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/symbols/cloud-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/symbols/server-grey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/symbols/router-blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
99 |
--------------------------------------------------------------------------------
/symbols/router-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
99 |
--------------------------------------------------------------------------------
/symbols/pc-grey.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/symbols/pc-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 | 
43 |
44 |
45 | ### Install solarized-light theme
46 | ```sh
47 | $ gns3theme -s solarized-light
48 | ```
49 | 
50 |
51 |
52 | ### Install tomorrow theme
53 | ```sh
54 | $ gns3theme -s tomorrow
55 | ```
56 | 
57 |
58 |
59 | ### Install n30x-light
60 | ```sh
61 | $ gns3theme -s n30x-light
62 | ```
63 | 
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 |
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 |
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 |
76 |
--------------------------------------------------------------------------------
/symbols/multilayer-switch-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
201 |
--------------------------------------------------------------------------------
/symbols/multilayer-switch-blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
201 |
--------------------------------------------------------------------------------
/symbols/rsp-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
268 |
--------------------------------------------------------------------------------
/symbols/rsp-blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
268 |
--------------------------------------------------------------------------------
/symbols/firewall-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------