From 199bd2596bcf811463db92240af91f193f9a237d Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 1 Oct 2017 19:20:37 +0300 Subject: [PATCH 01/15] Change the top bar icon to a fancy unicode lemon. --- suplemon/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 277d38f..3358e06 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -311,7 +311,7 @@ def show_top_status(self): if display["show_app_name"]: name_str = "Suplemon Editor v{0} -".format(self.app.version) if self.app.config["app"]["use_unicode_symbols"]: - logo = "\u2688" # Simple lemon (filled) + logo = "\U0001f34b" # Fancy lemon name_str = " {0} {1}".format(logo, name_str) head_parts.append(name_str) From 9cffae186feb681026c7dce938c1eb1db871ac98 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 1 Oct 2017 19:41:16 +0300 Subject: [PATCH 02/15] Increase battery status polling time. --- suplemon/modules/battery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/modules/battery.py b/suplemon/modules/battery.py index 6416357..269bd07 100644 --- a/suplemon/modules/battery.py +++ b/suplemon/modules/battery.py @@ -14,7 +14,7 @@ class Battery(Module): def init(self): self.last_value = -1 self.checked = time.time() - self.interval = 10 + self.interval = 60 def value(self): """Get the battery charge percent and cache it.""" From 5dd6d51d20ba8338eb94f3b0f4fc0d5d07d9116c Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 1 Oct 2017 22:20:42 +0300 Subject: [PATCH 03/15] Fix #198. --- suplemon/modules/application_state.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/suplemon/modules/application_state.py b/suplemon/modules/application_state.py index 50424ac..0648923 100644 --- a/suplemon/modules/application_state.py +++ b/suplemon/modules/application_state.py @@ -1,5 +1,8 @@ # -*- encoding: utf-8 + +import hashlib + from suplemon.suplemon_module import Module @@ -30,9 +33,14 @@ def get_file_state(self, file): state = { "cursors": [cursor.tuple() for cursor in editor.get_cursors()], "scroll_pos": editor.get_scroll_pos(), + "hash": self.get_hash(editor), } return state + def get_hash(self, editor): + # We don't need cryptographic security so we just use md5 + return hashlib.md5(editor.data.encode("utf-8")).hexdigest() + def set_file_state(self, file, state): """Set the state of a file.""" file.editor.set_cursors(state["cursors"]) @@ -50,7 +58,11 @@ def restore_states(self): for file in self.app.get_files(): path = file.get_path() if path in self.storage.get_data().keys(): - self.set_file_state(file, self.storage[path]) + state = self.storage[path] + if "hash" not in state: + self.set_file_state(file, state) + elif state["hash"] == self.get_hash(file.get_editor()): + self.set_file_state(file, state) module = { From 5c82d728d2b1302aeb3bf0a3d8a83eea223d7521 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 2 Oct 2017 16:11:59 +0300 Subject: [PATCH 04/15] Keep module statuses in order in top bar. --- suplemon/ui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 3358e06..639319d 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -315,8 +315,9 @@ def show_top_status(self): name_str = " {0} {1}".format(logo, name_str) head_parts.append(name_str) - # Add module statuses to the status bar - for name in self.app.modules.modules.keys(): + # Add module statuses to the status bar in descending order + module_keys = sorted(self.app.modules.modules.keys()) + for name in module_keys: module = self.app.modules.modules[name] if module.options["status"] == "top": status = module.get_status() From 3ee1afd8fd2409ccda71dd2cc14a3e18c7f78ba9 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 2 Oct 2017 16:12:32 +0300 Subject: [PATCH 05/15] Get the most recent data from editor, not the cached data. --- suplemon/modules/application_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/modules/application_state.py b/suplemon/modules/application_state.py index 0648923..e598ecb 100644 --- a/suplemon/modules/application_state.py +++ b/suplemon/modules/application_state.py @@ -39,7 +39,7 @@ def get_file_state(self, file): def get_hash(self, editor): # We don't need cryptographic security so we just use md5 - return hashlib.md5(editor.data.encode("utf-8")).hexdigest() + return hashlib.md5(editor.get_data().encode("utf-8")).hexdigest() def set_file_state(self, file, state): """Set the state of a file.""" From 35e4d0e002ee52dedc0e3ffa83a781f57d1e0e9e Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 00:30:19 +0300 Subject: [PATCH 06/15] More pythonic code for file matching. --- suplemon/main.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index e570be2..f699220 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -434,20 +434,21 @@ def go_to(self): def find_file(self, s): """Return index of file matching string.""" + # REFACTOR: Move to a helper function or implement in a module + # Case insensitive matching s = s.lower() - i = 0 + # First match files beginning with s - for file in self.files: + for i, file in enumerate(self.files): if file.name.lower().startswith(s): return i - i += 1 - i = 0 - # Then match files that contain s - for file in self.files: + + # Then match any files that contain s + for i, file in enumerate(self.files): if s in file.name.lower(): return i - i += 1 + return -1 def run_command(self, data): From ab68446fa7a26ce557ea7ce95239f63babf53307 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 00:50:38 +0300 Subject: [PATCH 07/15] Add comment describing battery module interval. --- suplemon/modules/battery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suplemon/modules/battery.py b/suplemon/modules/battery.py index 269bd07..e4cfafc 100644 --- a/suplemon/modules/battery.py +++ b/suplemon/modules/battery.py @@ -14,7 +14,7 @@ class Battery(Module): def init(self): self.last_value = -1 self.checked = time.time() - self.interval = 60 + self.interval = 60 # Seconds to wait until polling again def value(self): """Get the battery charge percent and cache it.""" From 67d9b047b6e95b164479d19de8d4deb8ffaa9dff Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 20:15:47 +0300 Subject: [PATCH 08/15] Add ability to detect wether a module has it's own run method. --- suplemon/suplemon_module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/suplemon/suplemon_module.py b/suplemon/suplemon_module.py index 36d45a5..2aaf64d 100644 --- a/suplemon/suplemon_module.py +++ b/suplemon/suplemon_module.py @@ -134,6 +134,9 @@ def set_options(self, options): """Set module options.""" self.options = options + def is_runnable(self): + return self.run.__func__ != Module.run.__func__ + def init_logging(self, name): """Initialize the module logger (self.logger). From dc897398ba03a3711543386c11a3d90b58b90094 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 20:29:00 +0300 Subject: [PATCH 09/15] - Moved file autocompletion logic to generic autocompleting prompt - Refactored the file autocomplete to use the generic autocompleter as its base class - Fixed bug in file autocompleter that prevented properly traversing directories when using ~ in the path --- suplemon/prompt.py | 67 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/suplemon/prompt.py b/suplemon/prompt.py index 97fcb47..d7a3b5e 100644 --- a/suplemon/prompt.py +++ b/suplemon/prompt.py @@ -129,17 +129,19 @@ def get_input(self, caption="", initial=False): return False -class PromptFile(Prompt): - """An input prompt with path auto completion based on Prompt.""" +class PromptAutocmp(Prompt): + """An input prompt with basic autocompletion.""" - def __init__(self, app, window): + def __init__(self, app, window, initial_items=[]): Prompt.__init__(self, app, window) - # Wether the autocomplete feature is active - self.complete_active = 0 + # Whether the autocomplete feature is active + self.complete_active = False # Index of last item that was autocompleted self.complete_index = 0 - # Input path to use for autocompletion (stored when autocompletion is activated) + # Input data to use for autocompletion (stored when autocompletion is activated) self.complete_data = "" + # Default autocompletable items + self.complete_items = initial_items def handle_input(self, event): """Handle special bindings for the prompt.""" @@ -147,16 +149,16 @@ def handle_input(self, event): if self.complete_active: # Allow accepting completed directories with enter if name == "enter": - if os.path.isdir(self.get_data()): + if self.has_match(): self.deactivate_autocomplete() return False - # Revert auto completion with esc + # Revert autocompletion with esc if name == "escape": self.revert_autocomplete() self.deactivate_autocomplete() return False if name == "tab": - # Run auto completion when tab is pressed + # Run autocompletion when tab is pressed self.autocomplete() # Don't pass the event to the parent class return False @@ -166,7 +168,7 @@ def handle_input(self, event): # Don't pass the event to the parent class return False else: - # If any key other than tab is pressed deactivate the auto completer + # If any key other than tab is pressed deactivate the autocompleter self.deactivate_autocomplete() Prompt.handle_input(self, event) @@ -175,8 +177,8 @@ def autocomplete(self, previous=False): if self.complete_active: # If the completer is active use the its initial input value data = self.complete_data - name = os.path.basename(data) - items = self.get_path_contents(data) # Get directory listing of input path + name = self.get_completable_name(data) + items = self.get_completable_items(data) # Filter the items by name if the input path contains a name if name: @@ -187,8 +189,8 @@ def autocomplete(self, previous=False): return False if not self.complete_active: - # Initialize the auto completor - self.complete_active = 1 + # Initialize the autocompletor + self.complete_active = True self.complete_data = data self.complete_index = 0 else: @@ -205,15 +207,27 @@ def autocomplete(self, previous=False): self.complete_index = 0 item = items[self.complete_index] - new_data = os.path.join(os.path.dirname(data), item) + new_data = self.get_full_completion(data, item) if len(items) == 1: self.deactivate_autocomplete() - # Set the input data to the new path and move cursor to the end + # Set the input data to the completion and move cursor to the end self.set_data(new_data) self.end() + def get_completable_name(self, data=""): + return data + + def get_completable_items(self, data=""): + return self.complete_items + + def get_full_completion(self, data, item): + return item + + def has_match(self): + return False + def deactivate_autocomplete(self): - self.complete_active = 0 + self.complete_active = False self.complete_index = 0 self.complete_data = "" @@ -228,6 +242,25 @@ def filter_items(self, items, name): name = name.lower() return [item for item in items if item.lower().startswith(name)] + +class PromptFile(PromptAutocmp): + """An input prompt with path autocompletion based on PromptAutocmp.""" + + def __init__(self, app, window): + PromptAutocmp.__init__(self, app, window) + + def has_match(self): + return os.path.isdir(os.path.expanduser(self.get_data())) + + def get_completable_name(self, data=""): + return os.path.basename(data) + + def get_completable_items(self, data=""): + return self.get_path_contents(data) # Get directory listing of input path + + def get_full_completion(self, data, item): + return os.path.join(os.path.dirname(data), item) + def get_path_contents(self, path): path = os.path.dirname(os.path.expanduser(path)) # If we get an empty path use the current directory From efc71d98e23c2a90f731c0ac556190ea98dbebc7 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 20:33:07 +0300 Subject: [PATCH 10/15] Added shorthand method to ui for autocompletable prompts. --- suplemon/ui.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/suplemon/ui.py b/suplemon/ui.py index 639319d..b6e0737 100644 --- a/suplemon/ui.py +++ b/suplemon/ui.py @@ -8,7 +8,7 @@ import logging from wcwidth import wcswidth -from .prompt import Prompt, PromptBool, PromptFile +from .prompt import Prompt, PromptBool, PromptFile, PromptAutocmp from .key_mappings import key_map # Curses can't be imported yet but we'll @@ -468,7 +468,7 @@ def show_legend(self): x += len(label)+2 self.legend_win.refresh() - def _query(self, text, initial="", cls=Prompt): + def _query(self, text, initial="", cls=Prompt, inst=None): """Ask for text input via the status bar.""" # Disable render blocking @@ -476,7 +476,10 @@ def _query(self, text, initial="", cls=Prompt): self.app.block_rendering = 0 # Create our text input - self.text_input = cls(self.app, self.status_win) + if not inst: + self.text_input = cls(self.app, self.status_win) + else: + self.text_input = inst self.text_input.set_config(self.app.config["editor"].copy()) self.text_input.set_input_source(self.get_input) self.text_input.init() @@ -504,6 +507,12 @@ def query_file(self, text, initial=""): result = self._query(text, initial, PromptFile) return result + def query_autocmp(self, text, initial="", completions=[]): + """Get an arbitrary string from the user with autocomplete.""" + prompt_inst = PromptAutocmp(self.app, self.status_win, initial_items=completions) + result = self._query(text, initial, inst=prompt_inst) + return result + def get_input(self, blocking=True): """Get an input event from keyboard or mouse. Returns an InputEvent instance or False.""" event = InputEvent() # Initialize new empty event From 1f150b07d38901f9facbfa4e2c4fc5534284aae2 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 3 Oct 2017 20:34:53 +0300 Subject: [PATCH 11/15] Add autocomplete to run command prompt. Also fix a few bare excepts. --- suplemon/main.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index f699220..b773fc9 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -421,13 +421,13 @@ def go_to(self): try: input_str = int(lineno) self.get_editor().go_to_pos(input_str) - except: + except ValueError: pass else: try: line_no = int(input_str) self.get_editor().go_to_pos(line_no) - except: + except ValueError: file_index = self.find_file(input_str) if file_index != -1: self.switch_to_file(file_index) @@ -475,6 +475,7 @@ def run_module(self, module_name, args=""): self.modules.modules[module_name].run(self, self.get_editor(), args) return True except: + # Catch any error when running a module just incase self.set_status("Running command failed!") self.logger.exception("Running command failed!") return False @@ -512,6 +513,7 @@ def trigger_event(self, event, when): try: val = cb(event) except: + # Catch all errors in callbacks just incase self.logger.error("Failed running callback: {0}".format(cb), exc_info=True) continue if val: @@ -550,7 +552,13 @@ def toggle_mouse(self): def query_command(self): """Run editor commands.""" - data = self.ui.query("Command:") + modules = self.modules.modules + # Get built in operations + completions = [oper for oper in self.operations.keys()] + # Add runnable modules + completions += [name for name, m in modules.iteritems() if m.is_runnable()] + + data = self.ui.query_autocmp("Command:", completions=sorted(completions)) if not data: return False self.run_command(data) From f0c63a5385477ac21b222e56e2e279a44e2cea2e Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 8 Oct 2017 04:55:38 +0300 Subject: [PATCH 12/15] Python compatibility fixes. --- suplemon/main.py | 8 ++++++-- suplemon/suplemon_module.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/suplemon/main.py b/suplemon/main.py index b773fc9..be9fd11 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -552,11 +552,15 @@ def toggle_mouse(self): def query_command(self): """Run editor commands.""" - modules = self.modules.modules + if sys.version_info[0] < 3: + modules = self.modules.modules.iteritems() + else: + modules = self.modules.modules.items() + # Get built in operations completions = [oper for oper in self.operations.keys()] # Add runnable modules - completions += [name for name, m in modules.iteritems() if m.is_runnable()] + completions += [name for name, m in modules if m.is_runnable()] data = self.ui.query_autocmp("Command:", completions=sorted(completions)) if not data: diff --git a/suplemon/suplemon_module.py b/suplemon/suplemon_module.py index 2aaf64d..07b73ef 100644 --- a/suplemon/suplemon_module.py +++ b/suplemon/suplemon_module.py @@ -135,7 +135,8 @@ def set_options(self, options): self.options = options def is_runnable(self): - return self.run.__func__ != Module.run.__func__ + cls_method = getattr(Module, "run") + return self.run.__module__ != cls_method.__module__ def init_logging(self, name): """Initialize the module logger (self.logger). From 0ba0d58b922a0b55366ee56769257e41af0581d6 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 3 Nov 2017 16:05:03 +0200 Subject: [PATCH 13/15] Generate file hash in chunks, line by line. --- suplemon/modules/application_state.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/suplemon/modules/application_state.py b/suplemon/modules/application_state.py index e598ecb..db81054 100644 --- a/suplemon/modules/application_state.py +++ b/suplemon/modules/application_state.py @@ -39,7 +39,10 @@ def get_file_state(self, file): def get_hash(self, editor): # We don't need cryptographic security so we just use md5 - return hashlib.md5(editor.get_data().encode("utf-8")).hexdigest() + h = hashlib.md5() + for line in editor.lines: + h.update(line.get_data().encode("utf-8")) + return h.hexdigest() def set_file_state(self, file, state): """Set the state of a file.""" From 74ffc94ebad59743040da0f08ccde1382b79d474 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 3 Nov 2017 16:11:47 +0200 Subject: [PATCH 14/15] Implement paste mode, fixes #204 --- suplemon/main.py | 1 + suplemon/modules/paste.py | 47 +++++++++++++++++++++++++++++++++++++ suplemon/suplemon_module.py | 4 ++++ 3 files changed, 52 insertions(+) create mode 100644 suplemon/modules/paste.py diff --git a/suplemon/main.py b/suplemon/main.py index be9fd11..15ba8ee 100644 --- a/suplemon/main.py +++ b/suplemon/main.py @@ -279,6 +279,7 @@ def reload_config(self): self.config.reload() for f in self.files: self.setup_editor(f.editor) + self.trigger_event_after("config_loaded") self.ui.resize() self.ui.refresh() diff --git a/suplemon/modules/paste.py b/suplemon/modules/paste.py new file mode 100644 index 0000000..130e255 --- /dev/null +++ b/suplemon/modules/paste.py @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 + +from suplemon.suplemon_module import Module + + +class Paste(Module): + def init(self): + # Flag for paste mode + self.active = False + # Initial state of auto indent + self.auto_indent_active = self.app.config["editor"]["auto_indent_newline"] + # Listen for config changes + self.bind_event_after("config_loaded", self.config_loaded) + + def run(self, app, editor, args): + # Simply toggle pastemode when the command is run + self.active = not self.active + self.set_paste_mode(self.active) + self.show_confirmation() + return True + + def config_loaded(self, e): + # Refresh the auto indent state when config is reloaded + self.auto_indent_active = self.app.config["editor"]["auto_indent_newline"] + + def get_status(self): + # Return the paste mode status for the statusbar + return "[PASTEMODE]" if self.active else "" + + def show_confirmation(self): + # Show a status message when pastemode is toggled + state = "activated" if self.active else "deactivated" + self.app.set_status("Paste mode " + state) + + def set_paste_mode(self, active): + # Enable or disable auto indent + if active: + self.app.config["editor"]["auto_indent_newline"] = False + else: + self.app.config["editor"]["auto_indent_newline"] = self.auto_indent_active + + +module = { + "class": Paste, + "name": "paste", + "status": "bottom", +} diff --git a/suplemon/suplemon_module.py b/suplemon/suplemon_module.py index 07b73ef..5a8c148 100644 --- a/suplemon/suplemon_module.py +++ b/suplemon/suplemon_module.py @@ -126,6 +126,10 @@ def get_options(self): """Get module options.""" return self.options + def get_status(self): + """Called by app when to get status bar contents.""" + return "" + def set_name(self, name): """Set module name.""" self.name = name From f5ef033cc179f295a78650a06069f9ca6cde6b10 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Fri, 3 Nov 2017 16:13:55 +0200 Subject: [PATCH 15/15] Updated changelog. --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d93a10..b9914b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,24 @@ Change Log ========== -## [v0.1.62](https://github.com/richrd/suplemon/tree/v0.1.62) (2017-08-22) compared to previous master branch. + +## [v0.1.63](https://github.com/richrd/suplemon/tree/v0.1.63) (2017-10-05) compared to previous master branch. +[Full Changelog](https://github.com/richrd/suplemon/compare/v0.1.62...v0.1.63) + +**Implemented enhancements:** + +- Add autocomplete to run command prompt (fixes #171) +- Increase battery status polling time to 60 sec (previously 10 sec) +- Change the top bar suplemon icon to a fancy unicode lemon. +- Add paste mode for better pasting over SSH (disables auto indentation) + +**Fixed bugs:** + +- Keep top bar statuses of modules in alphabetical order based on module name. (fixes #57) +- Prevent restoring file state if file has changed since last time (fixes #198) + + +## [v0.1.62](https://github.com/richrd/suplemon/tree/v0.1.62) (2017-09-25) compared to previous master branch. [Full Changelog](https://github.com/richrd/suplemon/compare/v0.1.61...v0.1.62) **Fixed bugs:**