Moved plugins and refactor command system

- Moved plugins to apropriate sub folders
- Refactor command system with new add_command method and rename GetCommandSystemEvent to GetNewCommandSystemEvent
- Add RegisterCommandEvent for dynamic command registration
- Change footer container orientation to VERTICAL
- Add search-highlight tag to source buffer
- Add file change detection (deleted, externally modified) in source_file
- Add JSON prettify option to source view popup menu
- Enable hexpand on VTE widget
- Update plugins_controller_mixin to use widget_registry
This commit is contained in:
2026-02-18 23:45:07 -06:00
parent 69c8418a72
commit 6714053776
51 changed files with 176 additions and 68 deletions

View File

@@ -6,8 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from libs.dto.base_event import BaseEvent from libs.event_factory import Event_Factory, Code_Event_Types
from libs.event_factory import Event_Factory
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
@@ -22,7 +21,7 @@ class Plugin(PluginCode):
self.provider: Provider = None self.provider: Provider = None
def _controller_message(self, event: BaseEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
... ...
def load(self): def load(self):

View File

@@ -30,13 +30,13 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
return 'Example Code Completion' return 'Example Code Completion'
def do_match(self, context): def do_match(self, context):
# word = context.get_word() # Note: If provider is in interactive activation then need to check
# if not word or len(word) < 2: return False # view focus as otherwise non focus views start trying to grab it.
completion = context.get_property("completion")
if not completion.get_view().has_focus(): return
""" Get whether the provider match the context of completion detailed in context. """
word = self.response_cache.get_word(context) word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False if not word or len(word) < 2: return False
return True return True
def do_get_priority(self): def do_get_priority(self):

View File

@@ -1,4 +1,5 @@
# Python imports # Python imports
from concurrent.futures import ThreadPoolExecutor
import re import re
# Lib imports # Lib imports
@@ -21,6 +22,9 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self): def __init__(self):
super(ProviderResponseCache, self).__init__() super(ProviderResponseCache, self).__init__()
# Note: Using asyncio.run causes a keyboard trap that prevents app
# closure from terminal. ThreadPoolExecutor seems to not have such issues...
self.executor = ThreadPoolExecutor(max_workers = 1)
self.matchers: dict = { self.matchers: dict = {
"hello": { "hello": {
"label": "Hello, World!", "label": "Hello, World!",

View File

@@ -6,8 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from libs.dto.base_event import BaseEvent from libs.event_factory import Event_Factory, Code_Event_Types
from libs.event_factory import Event_Factory
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
@@ -22,7 +21,7 @@ class Plugin(PluginCode):
self.provider: Provider = None self.provider: Provider = None
def _controller_message(self, event: BaseEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
... ...
def load(self): def load(self):

View File

@@ -1,4 +1,5 @@
# Python imports # Python imports
from concurrent.futures import ThreadPoolExecutor
# Lib imports # Lib imports
import gi import gi
@@ -17,9 +18,13 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def __init__(self): def __init__(self):
super(ProviderResponseCache, self).__init__() super(ProviderResponseCache, self).__init__()
self.executor = ThreadPoolExecutor(max_workers = 1)
self.matchers: dict = {}
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent): def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
... buffer = event.file.buffer
self.executor.submit(self._handle_change, buffer)
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent): def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
... ...
@@ -28,8 +33,13 @@ class ProviderResponseCache(ProviderResponseCacheBase):
... ...
def process_file_change(self, event: Code_Event_Types.TextChangedEvent): def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
buffer = event.file.buffer
self.executor.submit(self._handle_change, buffer)
def _handle_change(self, buffer):
... ...
def filter(self, word: str) -> list[dict]: def filter(self, word: str) -> list[dict]:
return [] return []

View File

@@ -6,8 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from libs.dto.base_event import BaseEvent from libs.event_factory import Event_Factory, Code_Event_Types
from libs.event_factory import Event_Factory
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
@@ -22,7 +21,7 @@ class Plugin(PluginCode):
self.provider: Provider = None self.provider: Provider = None
def _controller_message(self, event: BaseEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
... ...
def load(self): def load(self):

View File

@@ -6,8 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from libs.dto.base_event import BaseEvent from libs.event_factory import Event_Factory, Code_Event_Types
from libs.event_factory import Event_Factory
from plugins.plugin_types import PluginCode from plugins.plugin_types import PluginCode
@@ -22,7 +21,7 @@ class Plugin(PluginCode):
self.provider: Provider = None self.provider: Provider = None
def _controller_message(self, event: BaseEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
... ...
def load(self): def load(self):

View File

@@ -30,6 +30,11 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
return 'Words Completion' return 'Words Completion'
def do_match(self, context): def do_match(self, context):
# Note: If provider is in interactive activation then need to check
# view focus as otherwise non focus views start trying to grab it.
completion = context.get_property("completion")
if not completion.get_view().has_focus(): return
word = self.response_cache.get_word(context) word = self.response_cache.get_word(context)
if not word or len(word) < 2: return False if not word or len(word) < 2: return False
return True return True

View File

@@ -1,5 +1,5 @@
# Python imports # Python imports
import asyncio from concurrent.futures import ThreadPoolExecutor
# Lib imports # Lib imports
import gi import gi
@@ -24,7 +24,8 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent): def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
buffer = event.file.buffer buffer = event.file.buffer
asyncio.run( self._handle_change(buffer) ) with ThreadPoolExecutor(max_workers = 1) as executor:
executor.submit(self._handle_change, buffer)
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent): def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
self.matchers[event.file.buffer] = set() self.matchers[event.file.buffer] = set()
@@ -35,9 +36,10 @@ class ProviderResponseCache(ProviderResponseCacheBase):
def process_file_change(self, event: Code_Event_Types.TextChangedEvent): def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
buffer = event.file.buffer buffer = event.file.buffer
asyncio.run( self._handle_change(buffer) ) with ThreadPoolExecutor(max_workers = 1) as executor:
executor.submit(self._handle_change, buffer)
async def _handle_change(self, buffer): def _handle_change(self, buffer):
start_itr = buffer.get_start_iter() start_itr = buffer.get_start_iter()
end_itr = buffer.get_end_iter() end_itr = buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False) data = buffer.get_text(start_itr, end_itr, False)
@@ -67,6 +69,9 @@ class ProviderResponseCache(ProviderResponseCacheBase):
buffer = self.get_iter_correctly(context).get_buffer() buffer = self.get_iter_correctly(context).get_buffer()
word = self.get_word(context).rstrip() word = self.get_word(context).rstrip()
if not buffer in self.matchers:
self.matchers[buffer] = set()
response: list[dict] = [] response: list[dict] = []
for entry in self.matchers[buffer]: for entry in self.matchers[buffer]:
if not entry.rstrip().startswith(word): continue if not entry.rstrip().startswith(word): continue
@@ -81,7 +86,6 @@ class ProviderResponseCache(ProviderResponseCacheBase):
return response return response
def load_empty_set(self, buffer): def load_empty_set(self, buffer):
self.matchers[buffer] = set() self.matchers[buffer] = set()

View File

@@ -8,11 +8,11 @@ from gi.repository import Gtk
# Application imports # Application imports
from libs.dto.base_event import BaseEvent from libs.dto.base_event import BaseEvent
from plugins.plugin_base import PluginBase from plugins.plugin_types import PluginUI
class Plugin(PluginBase): class Plugin(PluginUI):
def __init__(self): def __init__(self):
super(Plugin, self).__init__() super(Plugin, self).__init__()
@@ -21,14 +21,14 @@ class Plugin(PluginBase):
... ...
def load(self): def load(self):
ui_element = self.requests_ui_element("plugin_control_list") ui_element = self.requests_ui_element("header-container")
ui_element.add( self.generate_plugin_element() ) ui_element.add( self.generate_plugin_element() )
def run(self): def run(self):
... ...
def generate_plugin_element(self): def generate_plugin_element(self):
button = Gtk.Button(label = self.name) button = Gtk.Button(label = "Hello, World!")
button.connect("button-release-event", self.send_message) button.connect("button-release-event", self.send_message)
button.show() button.show()
@@ -36,6 +36,5 @@ class Plugin(PluginBase):
return button return button
def send_message(self, widget = None, eve = None): def send_message(self, widget = None, eve = None):
message = "Hello, World!" logger.info("Hello, World!")
self.emit("display_message", ("warning", message, None))

View File

@@ -26,7 +26,7 @@ class FooterContainer(Gtk.Box):
self.ctx = self.get_style_context() self.ctx = self.get_style_context()
self.ctx.add_class("footer-container") self.ctx.add_class("footer-container")
self.set_orientation(Gtk.Orientation.HORIZONTAL) self.set_orientation(Gtk.Orientation.VERTICAL)
self.set_hexpand(True) self.set_hexpand(True)
def _setup_signals(self): def _setup_signals(self):

View File

@@ -37,6 +37,9 @@ class CommandSystem:
method = getattr(commands, command) method = getattr(commands, command)
return method.execute(*args) return method.execute(*args)
def add_command(self, command_name: str, command: callable):
setattr(commands, command_name, command)
def emit(self, event: Code_Event_Types.CodeEvent): def emit(self, event: Code_Event_Types.CodeEvent):
""" Monkey patch 'emit' from command controller... """ """ Monkey patch 'emit' from command controller... """

View File

@@ -9,7 +9,7 @@ __all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f"{__name__}.{module_name}") module = importlib.import_module(f"{__name__}.{module_name}")
globals()[module_name] = module # Add module to package namespace # globals()[module_name] = module # Add module to package namespace
__all__.append(module_name) __all__.append(module_name)
del pkgutil del pkgutil

View File

@@ -18,11 +18,6 @@ def execute(
uri: str uri: str
): ):
logger.debug("Command: DnD Load File To Buffer") logger.debug("Command: DnD Load File To Buffer")
file = view.command.get_file(view)
buffer = file.buffer
if not file.ftype == "buffer":
file = view.command.new_file(view) file = view.command.new_file(view)
gfile = Gio.File.new_for_uri(uri) gfile = Gio.File.new_for_uri(uri)
@@ -31,4 +26,5 @@ def execute(
(view, gfile, file) (view, gfile, file)
) )
view.set_buffer(file.buffer)
update_info_bar_if_focused(view.command, view) update_info_bar_if_focused(view.command, view)

View File

@@ -17,10 +17,10 @@ class CommandsController(ControllerBase, list):
def _controller_message(self, event: Code_Event_Types.CodeEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.GetCommandSystemEvent): if isinstance(event, Code_Event_Types.GetNewCommandSystemEvent):
event.response = self.get_command_system() event.response = self.get_new_command_system()
def get_command_system(self): def get_new_command_system(self):
command_system = CommandSystem() command_system = CommandSystem()
command_system.emit = self.emit command_system.emit = self.emit
command_system.emit_to = self.emit_to command_system.emit_to = self.emit_to

View File

@@ -27,6 +27,8 @@ class SourceViewsController(ControllerBase, list):
def _controller_message(self, event: Code_Event_Types.CodeEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.RemovedFileEvent): if isinstance(event, Code_Event_Types.RemovedFileEvent):
self._remove_file(event) self._remove_file(event)
elif isinstance(event, Code_Event_Types.RegisterCommandEvent):
self. _register_command(event)
if not self.signal_mapper.active_view: return if not self.signal_mapper.active_view: return
@@ -40,8 +42,22 @@ class SourceViewsController(ControllerBase, list):
elif isinstance(event, Code_Event_Types.TextInsertedEvent): elif isinstance(event, Code_Event_Types.TextInsertedEvent):
self.signal_mapper.insert_text(event.file, event.text) self.signal_mapper.insert_text(event.file, event.text)
def _register_command(self, event: Code_Event_Types.RegisterCommandEvent):
self.state_manager.key_mapper.map_command(
event.command_name,
{
f"{event.binding_mode}": event.binding
}
)
for view in self:
view.command.add_command(
event.command_name,
event.command
)
def _get_command_system(self): def _get_command_system(self):
event = Event_Factory.create_event("get_command_system") event = Event_Factory.create_event("get_new_command_system")
self.message_to("commands", event) self.message_to("commands", event)
command = event.response command = event.response

View File

@@ -10,6 +10,7 @@ from ...key_mapper import KeyMapper
from .states import * from .states import *
class SourceViewStateManager: class SourceViewStateManager:
def __init__(self): def __init__(self):
self.key_mapper: KeyMapper = KeyMapper() self.key_mapper: KeyMapper = KeyMapper()

View File

@@ -70,10 +70,15 @@ class KeyMapper:
with open(bindings_file, 'r') as f: with open(bindings_file, 'r') as f:
data = json.load(f)["keybindings"] data = json.load(f)["keybindings"]
for command in data: for command in data:
press_state = "held" if "held" in data[command] else "released" self.map_command( command, data[command] )
keyname = data[command][press_state]
def re_map(self):
self.states = copy.deepcopy(self._map)
def map_command(self, command, entry):
press_state = "held" if "held" in entry else "released"
keyname = entry[press_state]
state = NoKeyState state = NoKeyState
if "<Control>" in keyname: if "<Control>" in keyname:
@@ -90,9 +95,6 @@ class KeyMapper:
getattr(self.states[state], press_state)[keyname] = command getattr(self.states[state], press_state)[keyname] = command
def re_map(self):
self.states = copy.deepcopy(self._map)
def _key_press_event(self, eve): def _key_press_event(self, eve):
keyname = Gdk.keyval_name(eve.keyval).lower() keyname = Gdk.keyval_name(eve.keyval).lower()

View File

@@ -13,9 +13,14 @@ class SourceBuffer(GtkSource.Buffer):
def __init__(self): def __init__(self):
super(SourceBuffer, self).__init__() super(SourceBuffer, self).__init__()
self._handler_ids = []
self.is_processing_completion: bool = False self.is_processing_completion: bool = False
self._handler_ids = [] self.create_tag(
"search-highlight",
background = "yellow",
foreground = "black"
)
def set_signals( def set_signals(

View File

@@ -43,10 +43,26 @@ class SourceFile(GtkSource.File):
) )
def _changed(self, buffer: SourceBuffer): def _changed(self, buffer: SourceBuffer):
self.check_file_on_disk()
event = Event_Factory.create_event("text_changed", buffer = buffer) event = Event_Factory.create_event("text_changed", buffer = buffer)
event.file = self event.file = self
self.emit(event) self.emit(event)
if self.is_deleted():
print("deleted")
# event = Event_Factory.create_event("file_deleted", buffer = buffer)
# event.file = self
# self.emit(event)
return
if self.is_externally_modified():
print("is_externally_modified")
# event = Event_Factory.create_event("file_externally_modified", buffer = buffer)
# event.file = self
# self.emit(event)
return
def _insert_text( def _insert_text(
self, self,
buffer: SourceBuffer, buffer: SourceBuffer,

View File

@@ -59,6 +59,7 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
def _setup_signals(self): def _setup_signals(self):
self.connect("drag-data-received", self._on_drag_data_received) self.connect("drag-data-received", self._on_drag_data_received)
self.connect("populate-popup", self._on_populate_popup)
def _subscribe_to_events(self): def _subscribe_to_events(self):
... ...
@@ -76,6 +77,37 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
self._set_up_dnd() self._set_up_dnd()
def _on_populate_popup(self, view, menu):
buffer = self.get_buffer()
language = buffer.get_language()
if language.get_id() == "json":
self._load_prettify_json(view, menu)
menu.show_all()
def _load_prettify_json(self, view, menu):
menu.append( Gtk.SeparatorMenuItem() )
def on_prettify_json(menuitem):
import json
buffer = self.get_buffer()
start_itr, \
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False)
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
item = Gtk.MenuItem(label = "Prettify JSON")
item.connect("activate", on_prettify_json)
menu.append(item)
def clear_temp_cut_buffer_delayed(self): def clear_temp_cut_buffer_delayed(self):
if self._cut_temp_timeout_id: if self._cut_temp_timeout_id:
GLib.source_remove(self._cut_temp_timeout_id) GLib.source_remove(self._cut_temp_timeout_id)

View File

@@ -45,6 +45,7 @@ class VteWidget(Vte.Terminal):
ctx.add_class("vte-widget") ctx.add_class("vte-widget")
self.set_clear_background(False) self.set_clear_background(False)
self.set_hexpand(True)
self.set_enable_sixel(True) self.set_enable_sixel(True)
self.set_cursor_shape( Vte.CursorShape.IBEAM ) self.set_cursor_shape( Vte.CursorShape.IBEAM )

View File

@@ -5,8 +5,9 @@
from .code_event import CodeEvent from .code_event import CodeEvent
from .register_provider_event import RegisterProviderEvent from .register_provider_event import RegisterProviderEvent
from .register_command_event import RegisterCommandEvent
from .get_command_system_event import GetCommandSystemEvent from .get_new_command_system_event import GetNewCommandSystemEvent
from .request_completion_event import RequestCompletionEvent from .request_completion_event import RequestCompletionEvent
from .cursor_moved_event import CursorMovedEvent from .cursor_moved_event import CursorMovedEvent
from .modified_changed_event import ModifiedChangedEvent from .modified_changed_event import ModifiedChangedEvent

View File

@@ -9,5 +9,5 @@ from .code_event import CodeEvent
@dataclass @dataclass
class GetCommandSystemEvent(CodeEvent): class GetNewCommandSystemEvent(CodeEvent):
... ...

View File

@@ -0,0 +1,20 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
import gi
gi.require_version('GtkSource', '4')
from gi.repository import GtkSource
# Application imports
from ..base_event import BaseEvent
@dataclass
class RegisterCommandEvent(BaseEvent):
command_name: str = ""
command: callable = None
binding_mode: str = ""
binding: str = ""

View File

@@ -14,10 +14,7 @@ class InvalidPluginException(Exception):
class PluginsControllerMixin: class PluginsControllerMixin:
def requests_ui_element(self, target_id: str): def requests_ui_element(self, target_id: str):
builder = settings_manager.get_builder() if not target_id in widget_registery.objects:
ui_target = builder.get_object(target_id) raise InvalidPluginException('Unknown UI "target_id" given in requests.')
if not ui_target: return widget_registery.objects[target_id]
raise InvalidPluginException('Unknown "target_id" given in requests.')
return ui_target