suplemon-editor 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. suplemon/__init__.py +0 -0
  2. suplemon/__main__.py +3 -0
  3. suplemon/cli.py +65 -0
  4. suplemon/config/defaults.json +153 -0
  5. suplemon/config/keymap.json +63 -0
  6. suplemon/config.py +324 -0
  7. suplemon/cursor.py +135 -0
  8. suplemon/editor.py +568 -0
  9. suplemon/file.py +193 -0
  10. suplemon/help.py +327 -0
  11. suplemon/helpers.py +104 -0
  12. suplemon/hex2xterm.py +23 -0
  13. suplemon/key_mappings.py +239 -0
  14. suplemon/lexer.py +88 -0
  15. suplemon/line.py +51 -0
  16. suplemon/linelight/__init__.py +0 -0
  17. suplemon/linelight/color_map.py +9 -0
  18. suplemon/linelight/css.py +21 -0
  19. suplemon/linelight/diff.py +17 -0
  20. suplemon/linelight/html.py +19 -0
  21. suplemon/linelight/js.py +21 -0
  22. suplemon/linelight/json.py +15 -0
  23. suplemon/linelight/lua.py +10 -0
  24. suplemon/linelight/md.py +19 -0
  25. suplemon/linelight/php.py +27 -0
  26. suplemon/linelight/py.py +27 -0
  27. suplemon/logger.py +90 -0
  28. suplemon/main.py +771 -0
  29. suplemon/module_loader.py +98 -0
  30. suplemon/modules/application_state.py +84 -0
  31. suplemon/modules/autocomplete.py +114 -0
  32. suplemon/modules/autodocstring.py +142 -0
  33. suplemon/modules/battery.py +118 -0
  34. suplemon/modules/bulk_delete.py +75 -0
  35. suplemon/modules/clock.py +22 -0
  36. suplemon/modules/comment.py +59 -0
  37. suplemon/modules/config.py +20 -0
  38. suplemon/modules/date.py +22 -0
  39. suplemon/modules/diff.py +42 -0
  40. suplemon/modules/eval.py +43 -0
  41. suplemon/modules/hostname.py +34 -0
  42. suplemon/modules/input_test.py +23 -0
  43. suplemon/modules/keymap.py +20 -0
  44. suplemon/modules/linter.py +274 -0
  45. suplemon/modules/lower.py +21 -0
  46. suplemon/modules/lstrip.py +20 -0
  47. suplemon/modules/paste.py +49 -0
  48. suplemon/modules/reload.py +16 -0
  49. suplemon/modules/replace_all.py +23 -0
  50. suplemon/modules/reverse.py +22 -0
  51. suplemon/modules/rstrip.py +19 -0
  52. suplemon/modules/save.py +16 -0
  53. suplemon/modules/save_all.py +19 -0
  54. suplemon/modules/sort_lines.py +41 -0
  55. suplemon/modules/strip.py +19 -0
  56. suplemon/modules/system_clipboard.py +121 -0
  57. suplemon/modules/tabstospaces.py +18 -0
  58. suplemon/modules/toggle_whitespace.py +18 -0
  59. suplemon/modules/upper.py +21 -0
  60. suplemon/prompt.py +319 -0
  61. suplemon/suplemon_module.py +210 -0
  62. suplemon/themes/8colors.tmTheme +179 -0
  63. suplemon/themes/monokai.tmTheme +284 -0
  64. suplemon/themes.py +201 -0
  65. suplemon/ui.py +656 -0
  66. suplemon/viewer.py +1004 -0
  67. suplemon_editor-0.3.0.dist-info/METADATA +515 -0
  68. suplemon_editor-0.3.0.dist-info/RECORD +72 -0
  69. suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
  70. suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
  71. suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
  72. suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
suplemon/__init__.py ADDED
File without changes
suplemon/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ # -*- encoding: utf-8
2
+ from .cli import main
3
+ main()
suplemon/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ # -*- encoding: utf-8
3
+ """
4
+ Start a Suplemon instance in the current window
5
+ """
6
+
7
+ import sys
8
+
9
+ try:
10
+ import argparse
11
+ except ImportError:
12
+ # Python < 2.7
13
+ argparse = False
14
+
15
+ from .main import App, __version__
16
+
17
+
18
+ def print_debug_notice(app):
19
+ """Explain the log that debug mode just printed, and how to turn it off.
20
+
21
+ Without this the messages look like something went wrong, when they are
22
+ only shown because debug mode asked for them.
23
+ """
24
+ print(
25
+ "\nThe messages above are Suplemon's debug log, shown because debug mode is on.\n"
26
+ 'Set "debug": false in {0} to stop showing them.'.format(app.config.path()),
27
+ file=sys.stderr
28
+ )
29
+
30
+
31
+ def main():
32
+ """Handle CLI invocation"""
33
+ # Parse our CLI arguments
34
+ config_file = None
35
+ log_level = None
36
+ if argparse:
37
+ parser = argparse.ArgumentParser(description="Console text editor with multi cursor support")
38
+ parser.add_argument("filenames", metavar="filename", type=str, nargs="*", help="files to open")
39
+ parser.add_argument("--version", action="version", version=__version__)
40
+ parser.add_argument("--config", type=str, help="configuration file path")
41
+ parser.add_argument("--log-level", type=int, help="debug logging level")
42
+ args = parser.parse_args()
43
+ filenames = args.filenames
44
+ config_file = args.config
45
+ log_level = args.log_level
46
+ else:
47
+ # Python < 2.7 fallback
48
+ filenames = sys.argv[1:]
49
+
50
+ # Generate and start our application
51
+ app = App(filenames=filenames, config_file=config_file, log_level=log_level)
52
+ if app.init():
53
+ app.run()
54
+
55
+ # Output log info
56
+ if app.debug:
57
+ for logger_handler in app.logger.handlers:
58
+ logger_handler.close()
59
+ # Only explain the log if there was actually something to show
60
+ if any(getattr(handler, "flushed_records", 0) for handler in app.logger.handlers):
61
+ print_debug_notice(app)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,153 @@
1
+ // Suplemon Default Config
2
+
3
+ // This file contains the default config for Suplemon and should not be edited.
4
+ // If the file doesn't exist, or if it has errors Suplemon can't run.
5
+ // Suplemon supports single line comments in JSON as seen here.
6
+
7
+ // There are three main groups for settings:
8
+ // - app: Global settings
9
+ // - editor: Editor behaviour
10
+ // - display: How the UI looks
11
+
12
+ {
13
+ // Global settings
14
+ "app": {
15
+ // Print the debug log to the terminal when Suplemon exits.
16
+ // The log is written to output.log next to this file either way.
17
+ "debug": false,
18
+ // Debug log level (0: Notset, 10: Debug, 20: Info, 30: Warning, 40: Error, 50: Critical)
19
+ "debug_level": 20,
20
+ // How long curses will wait to detect ESC key
21
+ "escdelay": 50,
22
+ // Whether to use special unicode symbols for decoration
23
+ "use_unicode_symbols": true,
24
+ // If your $TERM ends in -256color and this is true, 'xterm-256color'
25
+ // will be used instead, working around an issue with curses.
26
+ "imitate_256color": false
27
+ },
28
+ // Editor settings
29
+ "editor": {
30
+ // Indent new lines to same level as previous line
31
+ "auto_indent_newline": true,
32
+ // Character to use for end of line
33
+ "end_of_line": "\n",
34
+ // Unindent with backspace
35
+ "backspace_unindent": true,
36
+ // Cursor style. 'reverse' or 'underline'
37
+ "cursor_style": "reverse",
38
+ // Encoding for reading and writing files
39
+ "default_encoding": "utf-8",
40
+ // Use hard tabs (insert actual tabulator character instead of spaces)
41
+ "hard_tabs": 0,
42
+ // Number of spaces to insert when pressing tab
43
+ "tab_width": 4,
44
+ // Amount of undo states to store
45
+ "max_history": 50,
46
+ // Characters considered to separate words
47
+ "punctuation": " (){}[]<>$@!%'\"=+-/*.:,;_\n\r",
48
+ // Character drawn at the end of each line when line ends are shown.
49
+ // Line ends are hidden at startup; F10 toggles them.
50
+ // Default is U+21B5, a downwards arrow with tip leftwards.
51
+ "line_end_char": "\u21B5",
52
+ // White space characters and their visual matches
53
+ "white_space_map": {
54
+ // Null byte as null symbol
55
+ "\u0000": "\u2400",
56
+ // Space as interpunct
57
+ " ": "\u00B7",
58
+ // Tab as tab symbol
59
+ "\t": "\u21B9",
60
+ // Nonbreaking space as open box
61
+ "\u00A0": "\u237D",
62
+ // Soft hyphen as letter shelf
63
+ "\u00AD": "\u2423",
64
+
65
+ // Other special unicode spaces shown as a space symbol (s/p)
66
+ // See here for details: http://www.cs.tut.fi/~jkorpela/chars/spaces.html
67
+
68
+ // no-break space
69
+ "\u00A0": "\u2420",
70
+ // mongolian vowel separator
71
+ "\u180E": "\u2420",
72
+ // en quad
73
+ "\u2000": "\u2420",
74
+ // em quad
75
+ "\u2001": "\u2420",
76
+ // en space
77
+ "\u2002": "\u2420",
78
+ // em space
79
+ "\u2003": "\u2420",
80
+ // three-per-em space
81
+ "\u2004": "\u2420",
82
+ // four-per-em space
83
+ "\u2005": "\u2420",
84
+ // six-per-em space
85
+ "\u2006": "\u2420",
86
+ // figure space
87
+ "\u2007": "\u2420",
88
+ // punctuation space
89
+ "\u2008": "\u2420",
90
+ // thin space
91
+ "\u2009": "\u2420",
92
+ // hair space
93
+ "\u200A": "\u2420",
94
+ // zero width space
95
+ "\u200B": "\u2420",
96
+ // narrow no-break space
97
+ "\u202F": "\u2420",
98
+ // medium mathematical space
99
+ "\u205F": "\u2420",
100
+ // ideographic space
101
+ "\u3000": "\u2420",
102
+ // zero width no-break space
103
+ "\uFEFF": "\u2420"
104
+ },
105
+ // Whether to visually show white space chars
106
+ "show_white_space": false,
107
+ // Show tab indicators in whitespace
108
+ "show_tab_indicators": true,
109
+ // Tab indicator charatrer
110
+ "tab_indicator_character": "\u203A",
111
+ // Highlight current line(s)
112
+ "highlight_current_line": true,
113
+ // Line numbering
114
+ "show_line_nums": true,
115
+ // Pad line numbers with spaces instead of zeros
116
+ "line_nums_pad_space": true,
117
+ // Color of the line numbers as a terminal color index (0-255).
118
+ // The grayscale ramp runs from 232 (near black) to 255 (near white),
119
+ // so raise this to make line numbers brighter, lower it to dim them.
120
+ // Only used on terminals that support 256 colors.
121
+ "line_number_color": 245,
122
+ // Naive line highlighting
123
+ "show_line_colors": true,
124
+ // Proper syntax highlighting
125
+ "show_highlighting": true,
126
+ // Syntax highlighting theme
127
+ "theme": "monokai",
128
+ // Listen for mouse events
129
+ "use_mouse": false,
130
+ // Whether to use copy/paste across multiple files
131
+ "use_global_buffer": true,
132
+ // Find with regex by default
133
+ "regex_find": false
134
+ },
135
+ // UI Display Settings
136
+ "display": {
137
+ // Show top status bar
138
+ "show_top_bar": true,
139
+ // Show app name and version in top bar
140
+ "show_app_name": true,
141
+ // Show list of open files in top bar
142
+ "show_file_list": true,
143
+ // Show indicator in the file list for files that are modified
144
+ // NOTE: if you experience performance issues, set this to false
145
+ "show_file_modified_indicator": true,
146
+ // Show the keyboard legend
147
+ "show_legend": true,
148
+ // Show the bottom status bar
149
+ "show_bottom_bar": true,
150
+ // Invert status bar colors (switch text and background colors)
151
+ "invert_status_bars": false
152
+ }
153
+ }
@@ -0,0 +1,63 @@
1
+ // Suplemon Default Key Map
2
+
3
+ // This file contains the default key map for Suplemon and should not be edited.
4
+ // If the file doesn't exist, or if it has errors Suplemon can't run.
5
+ // Suplemon supports single line comments in JSON as seen here.
6
+
7
+ [
8
+ // App
9
+ {"keys": ["f1", "ctrl+h"], "command": "help"},
10
+ {"keys": ["ctrl+s"], "command": "save_file"},
11
+ {"keys": ["ctrl+e"], "command": "run_command"},
12
+ {"keys": ["ctrl+f"], "command": "find"},
13
+ {"keys": ["ctrl+g"], "command": "go_to"},
14
+ {"keys": ["ctrl+o"], "command": "open"},
15
+ {"keys": ["ctrl+w"], "command": "close_file"},
16
+ {"keys": ["ctrl+n"], "command": "new_file"},
17
+ {"keys": ["ctrl+q"], "command": "ask_exit"},
18
+ {"keys": ["ctrl+p"], "command": "comment"},
19
+ {"keys": ["f4", "ctrl+pageup", "ctrl+alt+pageup"], "command": "next_file"},
20
+ {"keys": ["shift+f4", "ctrl+pagedown", "ctrl+alt+pagedown"], "command": "prev_file"},
21
+ {"keys": ["f3"], "command": "save_file_as"},
22
+ {"keys": ["f2"], "command": "reload_file"},
23
+ {"keys": ["f7"], "command": "toggle_whitespace"},
24
+ {"keys": ["f8"], "command": "toggle_mouse"},
25
+ {"keys": ["f12"], "command": "toggle_fullscreen"},
26
+ // Editor
27
+ {"keys": ["up"], "command": "arrow_up"},
28
+ {"keys": ["down"], "command": "arrow_down"},
29
+ {"keys": ["left"], "command": "arrow_left"},
30
+ {"keys": ["right"], "command": "arrow_right"},
31
+ {"keys": ["enter"], "command": "enter"},
32
+ {"keys": ["backspace"], "command": "backspace"},
33
+ {"keys": ["delete"], "command": "delete"},
34
+ {"keys": ["tab"], "command": "tab"},
35
+ {"keys": ["shift+tab"], "command": "untab"},
36
+ {"keys": ["home"], "command": "home"},
37
+ {"keys": ["end"], "command": "end"},
38
+ {"keys": ["escape"], "command": "escape"},
39
+ {"keys": ["pageup"], "command": "page_up"},
40
+ {"keys": ["pagedown"], "command": "page_down"},
41
+ {"keys": ["f5", "ctrl+z"], "command": "undo"},
42
+ {"keys": ["f6", "ctrl+y"], "command": "redo"},
43
+ {"keys": ["f9"], "command": "toggle_line_nums"},
44
+ {"keys": ["f10"], "command": "toggle_line_ends"},
45
+ {"keys": ["f11"], "command": "toggle_highlight"},
46
+ {"keys": ["alt+up"], "command": "new_cursor_up"},
47
+ {"keys": ["alt+down"], "command": "new_cursor_down"},
48
+ {"keys": ["alt+left"], "command": "new_cursor_left"},
49
+ {"keys": ["alt+right"], "command": "new_cursor_right"},
50
+ {"keys": ["alt+pageup"], "command": "push_up"},
51
+ {"keys": ["alt+pagedown"], "command": "push_down"},
52
+ {"keys": ["ctrl+c"], "command": "copy"},
53
+ {"keys": ["ctrl+x"], "command": "cut"},
54
+ {"keys": ["ctrl+k"], "command": "duplicate_line"},
55
+ {"keys": ["ctrl+v", "insert"], "command": "insert"},
56
+ {"keys": ["ctrl+d"], "command": "find_next"},
57
+ {"keys": ["ctrl+a"], "command": "find_all"},
58
+ {"keys": ["ctrl+left"], "command": "jump_left"},
59
+ {"keys": ["ctrl+right"], "command": "jump_right"},
60
+ {"keys": ["ctrl+up"], "command": "jump_up"},
61
+ {"keys": ["ctrl+down"], "command": "jump_down"},
62
+ {"keys": ["ctrl+t"], "command": "strip"}
63
+ ]
suplemon/config.py ADDED
@@ -0,0 +1,324 @@
1
+ # -*- encoding: utf-8
2
+ """
3
+ Config handler.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import logging
9
+
10
+ from . import suplemon_module
11
+
12
+ # Preamble written to the user config created on first run. The defaults file
13
+ # has its own preamble saying it shouldn't be edited, which is the opposite of
14
+ # what's wanted here, so it's replaced with this one.
15
+ USER_CONFIG_HEADER = """\
16
+ // Suplemon Config
17
+ //
18
+ // Created from Suplemon's defaults on first run, and safe to edit. Any
19
+ // setting you change here overrides the default; anything you delete falls
20
+ // back to it. Single line comments are supported, as seen here.
21
+ //
22
+ // There are three main groups for settings:
23
+ // - app: Global settings
24
+ // - editor: Editor behaviour
25
+ // - display: How the UI looks
26
+
27
+ """
28
+
29
+
30
+ class Config:
31
+ def __init__(self, app):
32
+ self.app = app
33
+ self.logger = logging.getLogger(__name__)
34
+ self.default_config_filename = "defaults.json"
35
+ self.default_keymap_filename = "keymap.json"
36
+ self.config_filename = "suplemon-config.json"
37
+ self.keymap_filename = "suplemon-keymap.json"
38
+ self.home_dir = os.path.expanduser("~")
39
+ self.config_dir = os.path.join(self.home_dir, ".config", "suplemon")
40
+
41
+ self.defaults = {}
42
+ self.keymap = {}
43
+ self.config = {}
44
+ self.key_bindings = {}
45
+
46
+ def init(self):
47
+ self.create_config_dir()
48
+ return self.load_defaults()
49
+
50
+ def path(self):
51
+ return os.path.join(self.config_dir, self.config_filename)
52
+
53
+ def keymap_path(self):
54
+ return os.path.join(self.config_dir, self.keymap_filename)
55
+
56
+ def set_path(self, path):
57
+ parts = os.path.split(path)
58
+ self.config_dir = parts[0]
59
+ self.config_filename = parts[1]
60
+
61
+ def load(self):
62
+ path = self.path()
63
+ config = False
64
+ if not os.path.exists(path):
65
+ self.logger.debug("Configuration file '{0}' doesn't exist.".format(path))
66
+ # First run. Give the user a documented config they can actually
67
+ # edit rather than running on defaults they can't see.
68
+ self.create_user_config()
69
+ if os.path.exists(path):
70
+ config = self.load_config_file(path)
71
+ if config is not False:
72
+ self.logger.debug("Loaded configuration file '{0}'".format(path))
73
+ self.config = self.merge_defaults(config)
74
+ else:
75
+ self.logger.info("Failed to load config file '{0}'.".format(path))
76
+ self.config = dict(self.defaults)
77
+ self.load_keys()
78
+ return config
79
+
80
+ def load_keys(self):
81
+ path = self.keymap_path()
82
+ keymap = []
83
+
84
+ # Reload the defaults first. self.keymap already holds the previous
85
+ # merge of defaults and user keymap, so appending to it again would
86
+ # keep bindings the user has since deleted from their keymap file.
87
+ self.load_default_keys()
88
+
89
+ if not os.path.exists(path):
90
+ self.logger.debug("Keymap file '{0}' doesn't exist.".format(path))
91
+ else:
92
+ keymap = self.load_config_file(path) or []
93
+ if not keymap:
94
+ self.logger.warning("Failed to load keymap file '{0}'.".format(path))
95
+
96
+ # Build the key bindings
97
+ # User keymap overwrites the defaults in the bindings
98
+ self.keymap = self.normalize_keys(self.keymap + keymap)
99
+ self.key_bindings = {}
100
+ for binding in self.keymap:
101
+ for key in binding["keys"]:
102
+ self.key_bindings[key] = binding["command"]
103
+
104
+ return True
105
+
106
+ def normalize_keys(self, keymap):
107
+ """Normalize the order of modifier keys in keymap."""
108
+ modifiers = ["shift", "ctrl", "alt", "meta"] # The modifiers in correct order
109
+ for item in keymap:
110
+ new_keys = []
111
+ for key_item in item["keys"]:
112
+ parts = key_item.split("+")
113
+ key = parts[-1]
114
+ if len(parts) < 2:
115
+ new_keys.append(key)
116
+ continue
117
+ normalized = ""
118
+ for mod in modifiers: # Add the used modifiers back in correct order
119
+ if mod in parts:
120
+ normalized += mod + "+"
121
+ normalized += key
122
+ new_keys.append(normalized)
123
+ item["keys"] = new_keys
124
+ return keymap
125
+
126
+ def load_defaults(self):
127
+ if not self.load_default_config() or not self.load_default_keys():
128
+ return False
129
+ return True
130
+
131
+ def default_config_path(self):
132
+ """Path of the default config file shipped with Suplemon."""
133
+ return os.path.join(self.app.path, "config", self.default_config_filename)
134
+
135
+ def create_user_config(self):
136
+ """Create the user config file by copying the defaults.
137
+
138
+ Run on first start so that the user gets a commented config file to
139
+ edit instead of silently running on built in defaults.
140
+
141
+ :return: True if the config file was created.
142
+ """
143
+ source = self.default_config_path()
144
+ target = self.path()
145
+ try:
146
+ with open(source) as f:
147
+ data = f.read()
148
+ except OSError:
149
+ self.logger.warning("Couldn't read default config '{0}'.".format(source))
150
+ return False
151
+ try:
152
+ with open(target, "w") as f:
153
+ f.write(self.build_user_config(data))
154
+ except OSError:
155
+ self.logger.warning("Couldn't create config file '{0}'.".format(target))
156
+ return False
157
+ self.logger.info("Created config file '{0}' from the defaults.".format(target))
158
+ return True
159
+
160
+ def build_user_config(self, data):
161
+ """Replace the default config's preamble with a user facing one.
162
+
163
+ Everything before the opening brace of the defaults is a comment block
164
+ stating the file shouldn't be edited. The settings themselves and the
165
+ comments documenting them are left untouched.
166
+
167
+ :param data: Contents of the default config file.
168
+ :return: Contents to write to the user config file.
169
+ """
170
+ lines = data.split("\n")
171
+ for i, line in enumerate(lines):
172
+ if line.lstrip().startswith("{"):
173
+ return USER_CONFIG_HEADER + "\n".join(lines[i:])
174
+ # Nothing that looks like a config body, copy it verbatim
175
+ return data
176
+
177
+ def load_default_config(self):
178
+ path = self.default_config_path()
179
+ config = self.load_config_file(path)
180
+ if not config:
181
+ self.logger.error("Failed to load default config file '{0}'!".format(path))
182
+ return False
183
+ self.defaults = config
184
+ return True
185
+
186
+ def load_module_configs(self):
187
+ module_config = {}
188
+ modules = self.app.modules.modules
189
+ for module_name in modules.keys():
190
+ module = modules[module_name]
191
+ conf = module.get_default_config()
192
+ module_config[module_name] = conf
193
+ self.logger.debug("Loading default config for module '%s': %s" % (module_name, str(conf)))
194
+ self.defaults["modules"] = module_config
195
+ self.config = self.merge_defaults(self.config)
196
+
197
+ def load_default_keys(self):
198
+ path = os.path.join(self.app.path, "config", self.default_keymap_filename)
199
+ config = self.load_config_file(path)
200
+ if not config:
201
+ self.logger.error("Failed to load default keymap file '{0}'!".format(path))
202
+ return False
203
+ self.keymap = config
204
+ return True
205
+
206
+ def reload(self):
207
+ """Reload the config file."""
208
+ return self.load()
209
+
210
+ def store(self):
211
+ """Write current config state to file.
212
+
213
+ :return: True if the config was written.
214
+ """
215
+ data = json.dumps(self.config, indent=4)
216
+ try:
217
+ with open(self.path(), "w", encoding="utf-8") as f:
218
+ f.write(data)
219
+ except OSError:
220
+ self.logger.warning("Couldn't write config file '{0}'.".format(self.path()))
221
+ return False
222
+ return True
223
+
224
+ def merge_defaults(self, config):
225
+ """Fill any missing config options with defaults."""
226
+ return self._merge_defaults(self.defaults, config)
227
+
228
+ def _merge_defaults(self, defaults, config):
229
+ """Recursivley merge two dicts."""
230
+ for key in defaults.keys():
231
+ item = defaults[key]
232
+ if key not in config.keys():
233
+ config[key] = item
234
+ continue
235
+ if not isinstance(item, dict):
236
+ continue
237
+ config[key] = self._merge_defaults(item, config[key])
238
+ return config
239
+
240
+ def load_config_file(self, path):
241
+ try:
242
+ f = open(path)
243
+ data = f.read()
244
+ f.close()
245
+ data = self.remove_config_comments(data)
246
+ config = json.loads(data)
247
+ return config
248
+ except (OSError, ValueError):
249
+ return False
250
+
251
+ def remove_config_comments(self, data):
252
+ """Remove comments from a 'pseudo' JSON config file.
253
+
254
+ Removes all lines that begin with '#' or '//' ignoring whitespace.
255
+
256
+ :param data: Commented JSON data to clean.
257
+ :return: Cleaned pure JSON.
258
+ """
259
+ lines = data.split("\n")
260
+ cleaned = []
261
+ for line in lines:
262
+ line = line.strip()
263
+ if line.startswith(("//", "#")):
264
+ continue
265
+ cleaned.append(line)
266
+ return "\n".join(cleaned)
267
+
268
+ def create_config_dir(self):
269
+ if not os.path.exists(self.config_dir):
270
+ try:
271
+ os.makedirs(self.config_dir)
272
+ except OSError:
273
+ self.app.logger.warning("Config folder '{0}' doesn't exist and couldn't be created.".format(
274
+ self.config_dir))
275
+
276
+ def __getitem__(self, i):
277
+ """Get a config variable."""
278
+ return self.config[i]
279
+
280
+ def __setitem__(self, i, v):
281
+ """Set a config variable."""
282
+ self.config[i] = v
283
+
284
+ def __str__(self):
285
+ """Convert entire config array to string."""
286
+ return str(self.config)
287
+
288
+ def __len__(self):
289
+ """Return length of top level config variables."""
290
+ return len(self.config)
291
+
292
+
293
+ class ConfigModule(suplemon_module.Module):
294
+ """Helper for shortcut for opening config files."""
295
+ def init(self):
296
+ self.config_name = "defaults.json"
297
+ self.config_default_path = os.path.join(self.app.path, "config", self.config_name)
298
+ self.config_user_path = self.app.config.path()
299
+
300
+ def run(self, app, editor, args):
301
+ if args == "defaults":
302
+ # Open the default config in a new file only for viewing
303
+ self.open(app, self.config_default_path, read_only=True)
304
+ else:
305
+ self.open(app, self.config_user_path)
306
+
307
+ def open(self, app, path, read_only=False):
308
+ if read_only:
309
+ f = open(path)
310
+ data = f.read()
311
+ f.close()
312
+ file = app.new_file()
313
+ file.set_name(self.config_name)
314
+ file.set_data(data)
315
+ app.switch_to_file(app.last_file_index())
316
+ else:
317
+ # Open the user config file for editing
318
+ f = app.file_is_open(path)
319
+ if f:
320
+ app.switch_to_file(app.get_file_index(f))
321
+ else:
322
+ if not app.open_file(path):
323
+ app.new_file(path)
324
+ app.switch_to_file(app.last_file_index())