diff --git a/README.md b/README.md index cb2c302..6d2b8f3 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ A template project for Python with Gtk applications. ### Requirements * PyGObject (Gtk introspection library) +* pygobject-stubs (For actually getting pylsp or python-language-server to auto complete in LSPs. Do if GTK3 --no-cache-dir --config-settings=config=Gtk3,Gdk3,Soup2) * pyxdg (Desktop ".desktop" file parser) * setproctitle (Define process title to search and kill more easily) * sqlmodel (SQL databases and is powered by Pydantic and SQLAlchemy) ### Note +* pyrightconfig.json can prompt IDEs that use pyright lsp on where imports are located- look at venvPath and venv. "venvPath" is parent path of "venv" where "venv" is just the name of the folder under the parent path that is the python created venv. * Move respetive sub folder content under user_config to the same places in Linux. Though, user/share/ can go to ~/.config folder if prefered. * In additiion, place the plugins folder in the same app folder you moved to /usr/share/ or ~/.config/ . There are a "\" strings and files that need to be set according to your app's name located at: @@ -20,4 +22,4 @@ There are a "\" strings and files that need to be set according to y For the user_config, after changing names and files, copy all content to their respective destinations. -The logic follows Debian Dpkg packaging and its placement logic. \ No newline at end of file +The logic follows Debian Dpkg packaging and its placement logic. diff --git a/plugins/README.txt b/plugins/README.txt index 4173ddd..dfbb0f9 100644 --- a/plugins/README.txt +++ b/plugins/README.txt @@ -1,2 +1,31 @@ ### Note -Copy the example and rename it to your desired name. The Main class and passed in arguments are required. You don't necessarily need to use the passed in socket_id or event_system. +Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with. +Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use. + + +### Manifest Example (All are required even if empty.) +``` +class Manifest: + name: str = "Example Plugin" + author: str = "John Doe" + version: str = "0.0.1" + support: str = "" + requests: {} = { + 'pass_ui_objects': ["plugin_control_list"], + 'pass_events': "true", + 'bind_keys': [] + } + pre_launch: bool = False +``` + + +### Requests +``` +requests: {} = { + 'pass_events': "true", # If empty or not present will be ignored. + "pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin. + 'bind_keys': [f"{name}||send_message:f"], + f"{name}||do_save:s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right. + +} +``` diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..4d8b8ec --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,13 @@ +{ + "reportUndefinedVariable": false, + "reportUnusedVariable": false, + "reportUnusedImport": true, + "reportDuplicateImport": true, + "executionEnvironments": [ + { + "root": "./src/versions/solarfm-0.0.1/solarfm" + } + ], + "venvPath": ".", + "venv": ".venv" +} diff --git a/requirements.txt b/requirements.txt index 4f65d82..c055f2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ -PyGObject -pyxdg -setproctitle -sqlmodel +PyGObject==3.40.1 +pygobject-stubs --no-cache-dir --config-settings=config=Gtk3,Gdk3,Soup2 +setproctitle==1.2.2 +pyxdg==0.27 +psutil==5.8.0 +pycryptodome==3.20.0 +sqlmodel==0.0.19 diff --git a/src/__builtins__.py b/src/__builtins__.py index 827ee12..3642a81 100644 --- a/src/__builtins__.py +++ b/src/__builtins__.py @@ -1,5 +1,6 @@ # Python imports import builtins +import traceback import threading import sys @@ -11,7 +12,7 @@ from libs.event_system import EventSystem from libs.endpoint_registry import EndpointRegistry from libs.keybindings import Keybindings from libs.logger import Logger -from libs.settings_manager.manager import SettingsManager +from libs.settings.manager import SettingsManager @@ -31,11 +32,22 @@ def daemon_threaded_wrapper(fn): return thread return wrapper +def call_chain_wrapper(fn): + def wrapper(*args, **kwargs): + print() + print() + for line in traceback.format_stack(): + print( line.strip() ) + print() + print() + + return fn(*args, **kwargs) + return wrapper # NOTE: Just reminding myself we can add to builtins two different ways... # __builtins__.update({"event_system": Builtins()}) -builtins.app_name = "" +builtins.APP_NAME = "" builtins.keybindings = Keybindings() builtins.event_system = EventSystem() @@ -46,12 +58,15 @@ builtins.settings_manager = SettingsManager() settings_manager.load_settings() builtins.settings = settings_manager.settings -builtins.logger = Logger(settings_manager.get_home_config_path(), \ - _ch_log_lvl=settings.debugging.ch_log_lvl, \ - _fh_log_lvl=settings.debugging.fh_log_lvl).get_logger() +builtins.logger = Logger( + settings_manager.get_home_config_path(), \ + _ch_log_lvl = settings.debugging.ch_log_lvl, \ + _fh_log_lvl = settings.debugging.fh_log_lvl + ).get_logger() builtins.threaded = threaded_wrapper builtins.daemon_threaded = daemon_threaded_wrapper +builtins.call_chain = call_chain_wrapper @@ -60,6 +75,6 @@ def custom_except_hook(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return - logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + logger.error("Uncaught exception", exc_info = (exc_type, exc_value, exc_traceback)) -sys.excepthook = custom_except_hook \ No newline at end of file +sys.excepthook = custom_except_hook diff --git a/src/__init__.py b/src/__init__.py index 90dc8da..6de34b8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,3 @@ """ - Start of package. -""" + Src Package. +""" \ No newline at end of file diff --git a/src/__main__.py b/src/__main__.py index f5121f8..b30240b 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -17,8 +17,9 @@ from app import Application -def main(args, unknownargs): - setproctitle(f'{app_name}') +def main(): + setproctitle(f'{APP_NAME}') + settings_manager.set_start_load_time() if args.debug == "true": settings_manager.set_debug(True) @@ -27,7 +28,9 @@ def main(args, unknownargs): settings_manager.set_trace_debug(True) settings_manager.do_dirty_start_check() - Application(args, unknownargs) + + app = Application() + app.run() @@ -36,19 +39,20 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() # Add long and short arguments - parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.") - parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.") - parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.") + parser.add_argument("--debug", "-d", default = "false", help = "Do extra console messaging.") + parser.add_argument("--trace-debug", "-td", default = "false", help = "Disable saves, ignore IPC lock, do extra console messaging.") + parser.add_argument("--no-plugins", "-np", default = "false", help = "Do not load plugins.") - parser.add_argument("--new-tab", "-nt", default="false", help="Opens a 'New Tab' if a handler is set for it.") - parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.") + parser.add_argument("--new-tab", "-nt", default = "false", help = "Opens a 'New Tab' if a handler is set for it.") + parser.add_argument("--file", "-f", default = "default", help = "JUST SOME FILE ARG.") # Read arguments (If any...) args, unknownargs = parser.parse_known_args() + settings_manager.set_starting_args( args, unknownargs ) try: faulthandler.enable() # For better debug info - main(args, unknownargs) + main() except Exception as e: traceback.print_exc() quit() \ No newline at end of file diff --git a/src/app.py b/src/app.py index b945b22..14eca5a 100644 --- a/src/app.py +++ b/src/app.py @@ -1,4 +1,5 @@ # Python imports +from contextlib import suppress import signal import os @@ -19,27 +20,32 @@ class AppLaunchException(Exception): class Application: """ docstring for Application. """ - def __init__(self, args, unknownargs): + def __init__(self): super(Application, self).__init__() if not settings_manager.is_trace_debug(): - self.load_ipc(args, unknownargs) + self.load_ipc() self.setup_debug_hook() - Window(args, unknownargs).main() - def load_ipc(self, args, unknownargs): - ipc_server = IPCServer() + def run(self): + win = Window() + win.start() + + def load_ipc(self): + args, \ + unknownargs = settings_manager.get_starting_args() + ipc_server = IPCServer() + self.ipc_realization_check(ipc_server) - if not ipc_server.is_ipc_alive: for arg in unknownargs + [args.new_tab,]: if os.path.isfile(arg): message = f"FILE|{arg}" ipc_server.send_ipc_message(message) - raise AppLaunchException(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...") + raise AppLaunchException(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...") def ipc_realization_check(self, ipc_server): try: @@ -47,18 +53,15 @@ class Application: except Exception: ipc_server.send_test_ipc_message() - try: + with suppress(Exception): ipc_server.create_ipc_listener() - except Exception as e: - ... def setup_debug_hook(self): - try: + # Typically: ValueError: signal only works in main thread + with suppress(ValueError): # kill -SIGUSR2 from Linux/Unix or SIGBREAK signal from Windows signal.signal( - vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"), + vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR2"), debug_signal_handler ) - except ValueError: - # Typically: ValueError: signal only works in main thread - ... \ No newline at end of file + diff --git a/src/core/__init__.py b/src/core/__init__.py index f1af95c..6c4fff7 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,3 +1,3 @@ """ - Core Module + Core Package """ \ No newline at end of file diff --git a/src/core/builder_wrapper.py b/src/core/builder_wrapper.py new file mode 100644 index 0000000..9245da9 --- /dev/null +++ b/src/core/builder_wrapper.py @@ -0,0 +1,33 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports + + + +class BuilderWrapper(Gtk.Builder): + """docstring for BuilderWrapper.""" + + def __init__(self): + super(BuilderWrapper, self).__init__() + + self.objects = {} + + def get_object(self, id: str, use_gtk: bool = True) -> any: + if not use_gtk: + return self.objects[id] + + return super(BuilderWrapper, self).get_object(id) + + def expose_object(self, id: str, object: any, use_gtk: bool = True) -> None: + if not use_gtk: + self.objects[id] = object + else: + super(BuilderWrapper, self).expose_object(id, object) + + def dereference_object(self, id: str) -> None: + del self.objects[id] diff --git a/src/core/containers/__init__.py b/src/core/containers/__init__.py index 4efd4b9..c59e952 100644 --- a/src/core/containers/__init__.py +++ b/src/core/containers/__init__.py @@ -1,3 +1,3 @@ """ - Containers Module -""" + Containers Package +""" \ No newline at end of file diff --git a/src/core/containers/base_container.py b/src/core/containers/base_container.py index 387cbbd..d725c88 100644 --- a/src/core/containers/base_container.py +++ b/src/core/containers/base_container.py @@ -8,6 +8,7 @@ from gi.repository import Gtk # Application imports from .header_container import HeaderContainer from .body_container import BodyContainer +from .footer_container import FooterContainer @@ -22,7 +23,7 @@ class BaseContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() - self.show_all() + self.show() def _setup_styling(self): @@ -33,12 +34,13 @@ class BaseContainer(Gtk.Box): ... def _subscribe_to_events(self): - event_system.subscribe("update_transparency", self._update_transparency) - event_system.subscribe("remove_transparency", self._remove_transparency) + event_system.subscribe("update-transparency", self._update_transparency) + event_system.subscribe("remove-transparency", self._remove_transparency) def _load_widgets(self): self.add(HeaderContainer()) self.add(BodyContainer()) + self.add(FooterContainer()) def _update_transparency(self): self.ctx.add_class(f"mw_transparency_{settings.theming.transparency}") diff --git a/src/core/containers/body_container.py b/src/core/containers/body_container.py index e1d94b2..ffe3043 100644 --- a/src/core/containers/body_container.py +++ b/src/core/containers/body_container.py @@ -23,7 +23,7 @@ class BodyContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() - self.show_all() + self.show() def _setup_styling(self): diff --git a/src/core/containers/center_container.py b/src/core/containers/center_container.py index 7d51eb2..2fa8887 100644 --- a/src/core/containers/center_container.py +++ b/src/core/containers/center_container.py @@ -21,9 +21,15 @@ class CenterContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() + self.show() + def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_hexpand(True) + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("center-container") @@ -31,7 +37,6 @@ class CenterContainer(Gtk.Box): ... def _subscribe_to_events(self): - # event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc) ... def _load_widgets(self): @@ -40,6 +45,9 @@ class CenterContainer(Gtk.Box): button.connect("clicked", self._hello_world) + button.show() + glade_box.show() + self.add(button) self.add(glade_box) self.add( WebkitUI() ) diff --git a/src/core/containers/footer_container.py b/src/core/containers/footer_container.py new file mode 100644 index 0000000..4e21cea --- /dev/null +++ b/src/core/containers/footer_container.py @@ -0,0 +1,41 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports + + + +class FooterContainer(Gtk.Box): + def __init__(self): + super(FooterContainer, self).__init__() + + self.ctx = self.get_style_context() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + self.show() + + + def _setup_styling(self): + self.set_orientation(Gtk.Orientation.HORIZONTAL) + + self.set_hexpand(True) + + self.ctx.add_class("footer-container") + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + + def _load_widgets(self): + ... diff --git a/src/core/containers/header_container.py b/src/core/containers/header_container.py index 857cbeb..7da0a75 100644 --- a/src/core/containers/header_container.py +++ b/src/core/containers/header_container.py @@ -6,7 +6,8 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # Application imports -from ..widgets.transparency_scale import TransparencyScale +from ..widgets.controls.open_files_button import OpenFilesButton +from ..widgets.controls.transparency_scale import TransparencyScale @@ -21,26 +22,33 @@ class HeaderContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() - self.show_all() + self.show() def _setup_styling(self): self.set_orientation(Gtk.Orientation.HORIZONTAL) + + self.set_hexpand(True) + self.ctx.add_class("header-container") def _setup_signals(self): ... def _subscribe_to_events(self): - ... + event_system.subscribe("tggl-top-main-menubar", self.tggl_top_main_menubar) def _load_widgets(self): - button = Gtk.Button(label = "Interactive Debug") + button = Gtk.Button(label = "Interactive Debug") button.connect("clicked", self._interactive_debug) - self.add(TransparencyScale()) + self.add( OpenFilesButton() ) + self.add( TransparencyScale() ) self.add(button) def _interactive_debug(self, widget = None, eve = None): - event_system.emit("load_interactive_debug") + event_system.emit("load-interactive-debug") + + def tggl_top_main_menubar(self): + self.hide() if self.is_visible() else self.show_all() \ No newline at end of file diff --git a/src/core/containers/left_container.py b/src/core/containers/left_container.py index 77cd490..45e6a37 100644 --- a/src/core/containers/left_container.py +++ b/src/core/containers/left_container.py @@ -18,9 +18,14 @@ class LeftContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() + self.show() + def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("left-container") @@ -28,8 +33,7 @@ class LeftContainer(Gtk.Box): ... def _subscribe_to_events(self): - # event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc) ... def _load_widgets(self): - ... + ... \ No newline at end of file diff --git a/src/core/containers/right_container.py b/src/core/containers/right_container.py index f0ac75d..6e760a7 100644 --- a/src/core/containers/right_container.py +++ b/src/core/containers/right_container.py @@ -6,6 +6,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # Application imports +from ..widgets.vte_widget import VteWidget @@ -18,9 +19,14 @@ class RightContainer(Gtk.Box): self._subscribe_to_events() self._load_widgets() + self.show() + def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) + + self.set_vexpand(True) + ctx = self.get_style_context() ctx.add_class("right-container") @@ -28,8 +34,8 @@ class RightContainer(Gtk.Box): ... def _subscribe_to_events(self): - # event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc) ... def _load_widgets(self): - ... + vte_widget = VteWidget() + self.add( vte_widget ) \ No newline at end of file diff --git a/src/core/controllers/__init__.py b/src/core/controllers/__init__.py index c004b70..0f53a83 100644 --- a/src/core/controllers/__init__.py +++ b/src/core/controllers/__init__.py @@ -1,3 +1,3 @@ """ - Controllers Module + Controllers Package """ \ No newline at end of file diff --git a/src/core/controllers/base_controller.py b/src/core/controllers/base_controller.py index a42387f..474e200 100644 --- a/src/core/controllers/base_controller.py +++ b/src/core/controllers/base_controller.py @@ -1,5 +1,4 @@ # Python imports -import os # Lib imports import gi @@ -18,27 +17,20 @@ from .bridge_controller import BridgeController class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData): - def __init__(self, args, unknownargs): - self.setup_controller_data() + """ docstring for BaseController. """ + def __init__(self): + + self._setup_controller_data() self._setup_styling() self._setup_signals() self._subscribe_to_events() self._load_controllers() - - if args.no_plugins == "false": - self.plugins.launch_plugins() - - for arg in unknownargs + [args.new_tab,]: - if os.path.isfile(arg): - message = f"FILE|{arg}" - event_system.emit("post_file_to_ipc", message) - - if os.path.isdir(arg): - message = f"DIR|{arg}" - event_system.emit("post_file_to_ipc", message) + self._load_plugins_and_files() logger.info(f"Made it past {self.__class__} loading...") + settings_manager.set_end_load_time() + settings_manager.log_load_time() def _setup_styling(self): @@ -50,20 +42,28 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData): self.window.connect("key-release-event", self.on_global_key_release_controller) def _subscribe_to_events(self): - event_system.subscribe("shutting_down", lambda: print("Shutting down...")) - event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc) - event_system.subscribe("handle_dir_from_ipc", self.handle_dir_from_ipc) - event_system.subscribe("tggl_top_main_menubar", self._tggl_top_main_menubar) + event_system.subscribe("shutting-down", lambda: print("Shutting down...")) + event_system.subscribe("handle-file-from-ipc", self.handle_file_from_ipc) + event_system.subscribe("handle-dir-from-ipc", self.handle_dir_from_ipc) + event_system.subscribe("tggl-top-main-menubar", self._tggl_top_main_menubar) def _load_controllers(self): BridgeController() + def _load_plugins_and_files(self): + args, unknownargs = settings_manager.get_starting_args() + if args.no_plugins == "false": + self.plugins_controller.pre_launch_plugins() + self.plugins_controller.post_launch_plugins() + + for file in settings_manager.get_starting_files(): + event_system.emit("post-file-to-ipc", file) + def _tggl_top_main_menubar(self): logger.debug("_tggl_top_main_menubar > stub...") - def setup_builder_and_container(self): - self.builder = Gtk.Builder() - self.builder.add_from_file(settings_manager.get_glade_file()) + def _load_glade_file(self): + self.builder.add_from_file( settings_manager.get_glade_file() ) self.builder.expose_object("main_window", self.window) settings_manager.set_builder(self.builder) diff --git a/src/core/controllers/base_controller_data.py b/src/core/controllers/base_controller_data.py index 937ba88..8b85498 100644 --- a/src/core/controllers/base_controller_data.py +++ b/src/core/controllers/base_controller_data.py @@ -7,23 +7,49 @@ from shutil import which # Application imports from plugins.plugins_controller import PluginsController +from ..builder_wrapper import BuilderWrapper class BaseControllerData: ''' BaseControllerData contains most of the state of the app at ay given time. It also has some support methods. ''' - def setup_controller_data(self) -> None: - self.window = settings_manager.get_main_window() - self.builder = None - self.base_container = None - self.was_midified_key = False - self.ctrl_down = False - self.shift_down = False - self.alt_down = False + def _setup_controller_data(self) -> None: + self.window = settings_manager.get_main_window() + self.builder = BuilderWrapper() + self.plugins_controller = PluginsController() - self.setup_builder_and_container() - self.plugins = PluginsController() + self.base_container = None + self.was_midified_key = False + self.ctrl_down = False + self.shift_down = False + self.alt_down = False + + self._collect_files_dirs() + self._load_glade_file() + + + def _collect_files_dirs(self): + args, \ + unknownargs = settings_manager.get_starting_args() + files = [] + + for arg in unknownargs + [args.new_tab,]: + if os.path.isdir( arg.replace("file://", "") ): + files.append( f"DIR|{arg.replace('file://', '')}" ) + continue + + # NOTE: If passing line number with file split against : + if os.path.isfile( arg.replace("file://", "").split(":")[0] ): + files.append( f"FILE|{arg.replace('file://', '')}" ) + continue + + logger.info(f"Not a File: {arg}") + + if len(files) == 0: return + + settings_manager.set_is_starting_with_file(True) + settings_manager.set_starting_files(files) def get_base_container(self): return self.base_container @@ -59,25 +85,23 @@ class BaseControllerData: widget.remove(child) def get_clipboard_data(self, encoding = "utf-8") -> str: - if which("xclip"): - command = ['xclip','-selection','clipboard'] - else: + if not which("xclip"): logger.info('xclip not found...') return + command = ['xclip','-selection','clipboard'] proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout = subprocess.PIPE) retcode = proc.wait() data = proc.stdout.read() return data.decode(encoding).strip() def set_clipboard_data(self, data: type, encoding = "utf-8") -> None: - if which("xclip"): - command = ['xclip','-selection','clipboard'] - else: + if not which("xclip"): logger.info('xclip not found...') return + command = ['xclip','-selection','clipboard'] proc = subprocess.Popen(command, stdin = subprocess.PIPE) proc.stdin.write(data.encode(encoding)) proc.stdin.close() - retcode = proc.wait() \ No newline at end of file + retcode = proc.wait() diff --git a/src/core/controllers/bridge_controller.py b/src/core/controllers/bridge_controller.py index 648139e..bc2c542 100644 --- a/src/core/controllers/bridge_controller.py +++ b/src/core/controllers/bridge_controller.py @@ -10,8 +10,6 @@ import base64 class BridgeController: def __init__(self): - self.opened_files = {} - self._setup_signals() self._subscribe_to_events() @@ -20,19 +18,19 @@ class BridgeController: ... def _subscribe_to_events(self): - event_system.subscribe("handle_bridge_event", self.handle_bridge_event) + event_system.subscribe("handle-bridge-event", self.handle_bridge_event) def handle_bridge_event(self, event): match event.topic: case "save": - event_system.emit(f"handle_file_event_{event.originator}", (event,)) + event_system.emit(f"handle-file-event-{event.originator}", (event,)) case "close": - event_system.emit(f"handle_file_event_{event.originator}", (event,)) + event_system.emit(f"handle-file-event-{event.originator}", (event,)) case "load_buffer": - event_system.emit(f"handle_file_event_{event.originator}", (event,)) + event_system.emit(f"handle-file-event-{event.originator}", (event,)) case "load_file": - event_system.emit(f"handle_file_event_{event.originator}", (event,)) + event_system.emit(f"handle-file-event-{event.originator}", (event,)) case "alert": content = base64.b64decode( event.content.encode() ).decode("utf-8") logger.info(f"\nMessage Topic: {event.topic}\nMessage Content: {content}") diff --git a/src/core/widgets/__init__.py b/src/core/widgets/__init__.py index 72b072b..a379fc5 100644 --- a/src/core/widgets/__init__.py +++ b/src/core/widgets/__init__.py @@ -1,3 +1,3 @@ """ - Widgets Module -""" + Widgets Package +""" \ No newline at end of file diff --git a/src/core/widgets/clock_widget.py b/src/core/widgets/clock_widget.py new file mode 100644 index 0000000..bda1842 --- /dev/null +++ b/src/core/widgets/clock_widget.py @@ -0,0 +1,110 @@ +# Python imports +from datetime import datetime + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import GObject + +# Application imports + + + +class CalendarWidget(Gtk.Popover): + def __init__(self): + super(CalendarWidget, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + self.body = Gtk.Calendar() + + self.body.show() + self.add(self.body) + + +class ClockWidget(Gtk.EventBox): + def __init__(self): + super(ClockWidget, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + self.show_all() + + + def _setup_styling(self): + self.set_size_request(180, -1) + + ctx = self.get_style_context() + ctx.add_class("clock-widget") + + def _setup_signals(self): + self.connect("button_release_event", self._toggle_cal_popover) + + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + self.calendar = CalendarWidget() + self.label = Gtk.Label() + + self.calendar.set_relative_to(self) + + self.label.set_justify(Gtk.Justification.CENTER) + self.label.set_margin_top(15) + self.label.set_margin_bottom(15) + self.label.set_margin_left(15) + self.label.set_margin_right(15) + + self._update_face() + self.add(self.label) + + GObject.timeout_add(59000, self._update_face) + + def _update_face(self): + dt_now = datetime.now() + hours_mins_sec = dt_now.strftime("%I:%M %p") + month_day_year = dt_now.strftime("%m/%d/%Y") + time_str = hours_mins_sec + "\n" + month_day_year + + self.label.set_label(time_str) + + def _toggle_cal_popover(self, widget, eve): + if (self.calendar.get_visible() == True): + self.calendar.popdown() + return + + now = datetime.now() + timeStr = now.strftime("%m/%d/%Y") + parts = timeStr.split("/") + month = int(parts[0]) - 1 + day = int(parts[1]) + year = int(parts[2]) + + self.calendar.body.select_day(day) + self.calendar.body.select_month(month, year) + + self.calendar.popup() + + + + + diff --git a/src/core/widgets/controls/__init__.py b/src/core/widgets/controls/__init__.py new file mode 100644 index 0000000..a82161f --- /dev/null +++ b/src/core/widgets/controls/__init__.py @@ -0,0 +1,3 @@ +""" + Widgets.Controls Package +""" \ No newline at end of file diff --git a/src/core/widgets/controls/open_files_button.py b/src/core/widgets/controls/open_files_button.py new file mode 100644 index 0000000..d29eaea --- /dev/null +++ b/src/core/widgets/controls/open_files_button.py @@ -0,0 +1,83 @@ +# Python imports +from contextlib import suppress +import os + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Gio + +# Application imports + + + +class OpenFilesButton(Gtk.Button): + """docstring for OpenFilesButton.""" + + def __init__(self): + super(OpenFilesButton, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + self.set_label("Open File(s)...") + self.set_image( Gtk.Image.new_from_icon_name("gtk-open", 4) ) + self.set_always_show_image(True) + self.set_image_position(1) # Left - 0, Right = 1 + self.set_hexpand(False) + + def _setup_signals(self): + self.connect("button-release-event", self._open_files) + + def _subscribe_to_events(self): + event_system.subscribe("open_files", self._open_files) + + def _load_widgets(self): + ... + + def _open_files(self, widget = None, eve = None, gfile = None): + start_dir = None + _gfiles = [] + + if gfile and gfile.query_exists(): + start_dir = gfile.get_parent() + + chooser = Gtk.FileChooserDialog("Open File(s)...", None, + Gtk.FileChooserAction.OPEN, + ( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, + Gtk.ResponseType.OK + ) + ) + + chooser.set_select_multiple(True) + + with suppress(Exception): + folder = widget.get_current_file().get_parent() if not start_dir else start_dir + chooser.set_current_folder( folder.get_path() ) + + response = chooser.run() + if not response == Gtk.ResponseType.OK: + chooser.destroy() + return _gfiles + + filenames = chooser.get_filenames() + if not filenames: + chooser.destroy() + return _gfiles + + for file in filenames: + path = file if os.path.isabs(file) else os.path.abspath(file) + _gfiles.append( Gio.File.new_for_path(path) ) + + chooser.destroy() + + logger.debug(_gfiles) + return _gfiles \ No newline at end of file diff --git a/src/core/widgets/controls/save_as_button.py b/src/core/widgets/controls/save_as_button.py new file mode 100644 index 0000000..aa81ae0 --- /dev/null +++ b/src/core/widgets/controls/save_as_button.py @@ -0,0 +1,72 @@ +# Python imports +import os + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Gio + +# Application imports + + + +class SaveAsButton(Gtk.Button): + def __init__(self): + super(SaveAsButton, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + self.set_label("Save As") + self.set_image( Gtk.Image.new_from_icon_name("gtk-save-as", 4) ) + self.set_always_show_image(True) + self.set_image_position(1) # Left - 0, Right = 1 + self.set_hexpand(False) + + def _setup_signals(self): + self.connect("released", self._save_as) + + def _subscribe_to_events(self): + event_system.subscribe("save-as", self._save_as) + + def _load_widgets(self): + ... + + def _save_as(self, widget = None, eve = None, gfile = None): + start_dir = None + _gfile = None + + chooser = Gtk.FileChooserDialog("Save File As...", None, + Gtk.FileChooserAction.SAVE, + ( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE_AS, + Gtk.ResponseType.OK + ) + ) + + # chooser.set_select_multiple(False) + + response = chooser.run() + if not response == Gtk.ResponseType.OK: + chooser.destroy() + return _gfile + + file = chooser.get_filename() + if not file: + chooser.destroy() + return _gfile + + path = file if os.path.isabs(file) else os.path.abspath(file) + _gfile = Gio.File.new_for_path(path) + + chooser.destroy() + + logger.debug(f"File To Save As: {_gfile}") + return _gfile diff --git a/src/core/widgets/transparency_scale.py b/src/core/widgets/controls/transparency_scale.py similarity index 89% rename from src/core/widgets/transparency_scale.py rename to src/core/widgets/controls/transparency_scale.py index 1e48177..223b59a 100644 --- a/src/core/widgets/transparency_scale.py +++ b/src/core/widgets/controls/transparency_scale.py @@ -37,12 +37,12 @@ class TransparencyScale(Gtk.Scale): def _load_widgets(self): adjust = self.get_adjustment() adjust.set_lower(0) - adjust.set_upper(99) + adjust.set_upper(100) adjust.set_value(settings.theming.transparency) adjust.set_step_increment(1.0) def _update_transparency(self, range): - event_system.emit("remove_transparency") + event_system.emit("remove-transparency") tp = int(range.get_value()) settings.theming.transparency = tp - event_system.emit("update_transparency") \ No newline at end of file + event_system.emit("update-transparency") \ No newline at end of file diff --git a/src/core/widgets/pager_widget.py b/src/core/widgets/pager_widget.py new file mode 100644 index 0000000..9736ee5 --- /dev/null +++ b/src/core/widgets/pager_widget.py @@ -0,0 +1,35 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Wnck', '3.0') +from gi.repository import Wnck + +# Application imports + + + +class PagerWidget: + def __init__(self): + super(PagerWidget, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + ... + + def get_widget(self): + return Wnck.Pager.new() diff --git a/src/core/widgets/task_list_widget.py b/src/core/widgets/task_list_widget.py new file mode 100644 index 0000000..244a092 --- /dev/null +++ b/src/core/widgets/task_list_widget.py @@ -0,0 +1,54 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Wnck', '3.0') +from gi.repository import Gtk +from gi.repository import Wnck + +# Application imports + + + +class TaskListWidget(Gtk.ScrolledWindow): + def __init__(self): + super(TaskListWidget, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + self.show_all() + + + def _setup_styling(self): + self.set_hexpand(False) + self.set_size_request(180, -1) + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + viewport = Gtk.Viewport() + task_list = Wnck.Tasklist.new() + vbox = Gtk.Box() + + vbox.set_orientation(Gtk.Orientation.VERTICAL) + + + task_list.set_scroll_enabled(False) + task_list.set_button_relief(2) # 0 = normal relief, 2 = no relief + task_list.set_grouping(1) # 0 = mever group, 1 auto group, 2 = always group + + task_list.set_vexpand(True) + task_list.set_include_all_workspaces(False) + task_list.set_orientation(1) # 0 = horizontal, 1 = vertical + + vbox.add(task_list) + viewport.add(vbox) + self.add(viewport) diff --git a/src/core/widgets/vte_widget.py b/src/core/widgets/vte_widget.py new file mode 100644 index 0000000..7a5f18d --- /dev/null +++ b/src/core/widgets/vte_widget.py @@ -0,0 +1,125 @@ +# Python imports +import os + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +gi.require_version('Vte', '2.91') +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import Vte + +# Application imports +from libs.dto.event import Event + + + +class VteWidgetException(Exception): + ... + + + +class VteWidget(Vte.Terminal): + """ + https://stackoverflow.com/questions/60454326/how-to-implement-a-linux-terminal-in-a-pygtk-app-like-vscode-and-pycharm-has + """ + + def __init__(self): + super(VteWidget, self).__init__() + + self.cd_cmd_prefix = ("cd".encode(), "cd ".encode()) + self.dont_process = False + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + self._do_session_spawn() + + self.show() + + + def _setup_styling(self): + ctx = self.get_style_context() + ctx.add_class("vte-widget") + + self.set_clear_background(False) + self.set_enable_sixel(True) + self.set_cursor_shape( Vte.CursorShape.IBEAM ) + + def _setup_signals(self): + self.connect("commit", self._commit) + + def _subscribe_to_events(self): + event_system.subscribe("update_term_path", self.update_term_path) + + def _load_widgets(self): + ... + + def _do_session_spawn(self): + self.spawn_sync( + Vte.PtyFlags.DEFAULT, + settings_manager.get_home_path(), + ["/bin/bash"], + [], + GLib.SpawnFlags.DEFAULT, + None, None, + ) + + # Note: '-->:' is used as a delimiter to split on to get command actual. + # !!! DO NOT REMOVE UNLESS CODE UPDATED ACCORDINGLY !!! + startup_cmds = [ + "env -i /bin/bash --noprofile --norc\n", + "export TERM='xterm-256color'\n", + "export LC_ALL=C\n", + "export XDG_RUNTIME_DIR='/run/user/1000'\n", + "export DISPLAY=:0\n", + f"export XAUTHORITY='{settings_manager.get_home_path()}/.Xauthority'\n", + f"\nexport HOME='{settings_manager.get_home_path()}'\n", + "export PS1='\\h@\\u \\W -->: '\n", + "clear\n" + ] + + for i in startup_cmds: + self.run_command(i) + + def _commit(self, terminal, text, size): + if self.dont_process: + self.dont_process = False + return + + if not text.encode() == "\r".encode(): return + + text, attributes = self.get_text() + lines = text.strip().splitlines() + command_ran = None + + try: + command_ran = lines[-1].split("-->:")[1].strip() + except VteWidgetException as e: + logger.debug(e) + return + + if not command_ran[0:3].encode() in self.cd_cmd_prefix: + return + + target_path = command_ran.split( command_ran[0:3] )[1] + if target_path in (".", "./"): return + + if not target_path: + target_path = settings_manager.get_home_path() + + event = Event("pty_path_updated", "", target_path) + event_system.emit("handle_bridge_event", (event,)) + + def update_term_path(self, fpath: str): + self.dont_process = True + + cmds = [f"cd '{fpath}'\n", "clear\n"] + for cmd in cmds: + self.run_command(cmd) + + def run_command(self, cmd: str): + self.feed_child_binary(bytes(cmd, 'utf8')) \ No newline at end of file diff --git a/src/core/widgets/webkit/__init__.py b/src/core/widgets/webkit/__init__.py index a0264af..1b77b51 100644 --- a/src/core/widgets/webkit/__init__.py +++ b/src/core/widgets/webkit/__init__.py @@ -1,3 +1,3 @@ """ - WebKit2 UI Module + WebKit2 UI Package """ \ No newline at end of file diff --git a/src/core/widgets/webkit/webkit_ui.py b/src/core/widgets/webkit/webkit_ui.py index b16bebc..263e491 100644 --- a/src/core/widgets/webkit/webkit_ui.py +++ b/src/core/widgets/webkit/webkit_ui.py @@ -9,17 +9,14 @@ from gi.repository import Gdk from gi.repository import WebKit2 # Application imports -from libs.data_types import Event - +from libs.settings.other.webkit_ui_settings import WebkitUISettings +from libs.dto.event import Event class WebkitUI(WebKit2.WebView): def __init__(self): super(WebkitUI, self).__init__() - # self.get_context().set_sandbox_enabled(False) - - self._load_settings() self._setup_styling() self._subscribe_to_events() self._load_view() @@ -27,10 +24,6 @@ class WebkitUI(WebKit2.WebView): self.show_all() - if settings_manager.is_debug(): - inspector = self.get_inspector() - inspector.show() - def _setup_styling(self): self.set_vexpand(True) @@ -38,8 +31,8 @@ class WebkitUI(WebKit2.WebView): self.set_background_color( Gdk.RGBA(0, 0, 0, 0.0) ) def _subscribe_to_events(self): - event_system.subscribe(f"ui_message", self.ui_message) - + event_system.subscribe(f"ui-message", self.ui_message) + def _load_settings(self): self.set_settings( WebkitUISettings() ) @@ -54,7 +47,6 @@ class WebkitUI(WebKit2.WebView): def _setup_content_manager(self): content_manager = self.get_user_content_manager() - content_manager.connect("script-message-received", self._process_js_message) content_manager.register_script_message_handler("backend") @@ -64,45 +56,14 @@ class WebkitUI(WebKit2.WebView): try: event = Event( **json.loads(message) ) - event_system.emit("handle_bridge_event", (event,)) + event_system.emit("handle-bridge-event", (event,)) except Exception as e: logger.info(e) - def ui_message(self, mtype, message): + def ui_message(self, message, mtype): command = f"displayMessage('{message}', '{mtype}', '3')" self.run_javascript(command, None, None) - -class WebkitUISettings(WebKit2.Settings): - def __init__(self): - super(WebkitUISettings, self).__init__() - - self._set_default_settings() - - - # Note: Highly insecure setup but most "app" like setup I could think of. - # Audit heavily any scripts/links ran/clicked under this setup! - def _set_default_settings(self): - # self.set_enable_xss_auditor(True) - # self.set_enable_hyperlink_auditing(True) - self.set_enable_xss_auditor(False) - self.set_enable_hyperlink_auditing(False) - self.set_enable_dns_prefetching(False) - self.set_allow_file_access_from_file_urls(True) - self.set_allow_universal_access_from_file_urls(True) - # self.set_enable_java(True) - - self.set_enable_page_cache(False) - self.set_enable_offline_web_application_cache(False) - self.set_enable_html5_local_storage(False) - self.set_enable_html5_database(False) - - self.set_print_backgrounds(False) - self.set_enable_tabs_to_links(False) - self.set_enable_fullscreen(True) - self.set_enable_developer_extras(True) - self.set_enable_webrtc(True) - self.set_enable_webaudio(True) - self.set_enable_accelerated_2d_canvas(True) - - self.set_user_agent(f"Mozilla/5.0 {app_name}") \ No newline at end of file + def run_javascript(self, script, cancellable, callback): + logger.debug(script) + super().run_javascript(script, cancellable, callback) diff --git a/src/core/widgets/webkit_ui.py b/src/core/widgets/webkit_ui.py deleted file mode 100644 index 074adf0..0000000 --- a/src/core/widgets/webkit_ui.py +++ /dev/null @@ -1,64 +0,0 @@ -# Python imports - -# Lib imports -import gi -gi.require_version('Gdk', '3.0') -gi.require_version('WebKit2', '4.0') -from gi.repository import Gdk -from gi.repository import WebKit2 - -# Application imports -from libs.settings_manager.other.webkit_ui_settings import WebkitUISettings - - -class WebkitUI(WebKit2.WebView): - def __init__(self): - super(WebkitUI, self).__init__() - - self._setup_styling() - self._subscribe_to_events() - self._load_view() - self._setup_content_manager() - - self.show_all() - - - def _setup_styling(self): - self.set_vexpand(True) - self.set_hexpand(True) - self.set_background_color( Gdk.RGBA(0, 0, 0, 0.0) ) - - def _subscribe_to_events(self): - event_system.subscribe(f"ui_message", self.ui_message) - - def _load_settings(self): - self.set_settings( WebkitUISettings() ) - - def _load_view(self): - path = settings_manager.get_context_path() - data = None - - with open(f"{path}/index.html", "r") as f: - data = f.read() - - self.load_html(content = data, base_uri = f"file://{path}/") - - def _setup_content_manager(self): - content_manager = self.get_user_content_manager() - content_manager.connect("script-message-received", self._process_js_message) - content_manager.register_script_message_handler("backend") - - def _process_js_message(self, user_content_manager, js_result): - js_value = js_result.get_js_value() - message = js_value.to_string() - - try: - event = Event( **json.loads(message) ) - event_system.emit("handle_bridge_event", (event,)) - except Exception as e: - logger.info(e) - - def ui_message(self, message, mtype): - command = f"displayMessage('{message}', '{mtype}', '3')" - self.run_javascript(command, None, None) - diff --git a/src/core/window.py b/src/core/window.py index 7fad7b8..8199215 100644 --- a/src/core/window.py +++ b/src/core/window.py @@ -10,7 +10,13 @@ from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib +try: + from gi.repository import GdkX11 +except ImportError: + logger.debug("Could not import X11 gir module...") + # Application imports +from libs.status_icon import StatusIcon from core.controllers.base_controller import BaseController @@ -23,26 +29,34 @@ class ControllerStartExceptiom(Exception): class Window(Gtk.ApplicationWindow): """ docstring for Window. """ - def __init__(self, args, unknownargs): + def __init__(self): super(Window, self).__init__() settings_manager.set_main_window(self) - self._controller = None + self._status_icon = None + self._controller = None + + self.guake_key = settings_manager.get_guake_key() + self.hidefunc = None self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets(args, unknownargs) + self._load_widgets() self._set_window_data() self._set_size_constraints() + self._setup_window_toggle_event() self.show() def _setup_styling(self): - self.set_title(f"{app_name}") + self.set_title(f"{APP_NAME}") self.set_icon_from_file( settings_manager.get_window_icon() ) + self.set_decorated(True) + self.set_skip_pager_hint(False) + self.set_skip_taskbar_hint(False) self.set_gravity(5) # 5 = CENTER self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS @@ -54,23 +68,33 @@ class Window(Gtk.ApplicationWindow): self.connect("focus-in-event", self._on_focus_in_event) self.connect("focus-out-event", self._on_focus_out_event) - self.connect("delete-event", self._tear_down) - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._tear_down) + self.connect("delete-event", self.stop) + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.stop) def _subscribe_to_events(self): - event_system.subscribe("tear_down", self._tear_down) - event_system.subscribe("load_interactive_debug", self._load_interactive_debug) + event_system.subscribe("tear-down", self.stop) + event_system.subscribe("load-interactive-debug", self._load_interactive_debug) - def _load_widgets(self, args, unknownargs): + def _load_widgets(self): if settings_manager.is_debug(): self.set_interactive_debugging(True) - self._controller = BaseController(args, unknownargs) + self._controller = BaseController() + self._status_icon = StatusIcon() if not self._controller: raise ControllerStartException("BaseController exited and doesn't exist...") self.add( self._controller.get_base_container() ) + def _display_manager(self): + """ Try to detect which display manager we are running under... """ + + import os + if os.environ.get('WAYLAND_DISPLAY'): + return 'WAYLAND' + + return 'X11' + def _set_size_constraints(self): _window_x = settings.config.main_window_x _window_y = settings.config.main_window_y @@ -90,7 +114,7 @@ class Window(Gtk.ApplicationWindow): if visual and screen.is_composited() and settings.config.make_transparent == 0: self.set_visual(visual) self.set_app_paintable(True) - self.connect("draw", self._area_draw) + # self.connect("draw", self._area_draw) # bind css file cssProvider = Gtk.CssProvider() @@ -107,17 +131,62 @@ class Window(Gtk.ApplicationWindow): def _on_focus_in_event(self, widget, event): - event_system.emit("pause_dnd_signals") + event_system.emit("pause-dnd-signals") def _on_focus_out_event(self, widget, event): - event_system.emit("listen_dnd_signals") + event_system.emit("listen-dnd-signals") def _load_interactive_debug(self): self.set_interactive_debugging(True) + def _setup_window_toggle_event(self) -> None: + hidebound = None + if not self.guake_key or not self._display_manager() == 'X11': + return - def _tear_down(self, widget = None, eve = None): - event_system.emit("shutting_down") + try: + import gi + gi.require_version('Keybinder', '3.0') + from gi.repository import Keybinder + + Keybinder.init() + Keybinder.set_use_cooked_accelerators(False) + except (ImportError, ValueError) as e: + logger.warning(e) + logger.warning('Unable to load Keybinder module. This means the hide_window shortcut will be unavailable') + + return + + # Attempt to grab a global hotkey for hiding the window. + # If we fail, we'll never hide the window, iconifying instead. + try: + hidebound = Keybinder.bind(self.guake_key, self._on_toggle_window, self) + except (KeyError, NameError) as e: + logger.warning(e) + + if not hidebound: + logger.debug('Unable to bind hide_window key, another instance/window has it.') + self.hidefunc = self.iconify + else: + self.hidefunc = self.hide + + def _on_toggle_window(self, data, window): + """Handle a request to hide/show the window""" + if not window.get_property('visible'): + window.show() + # Note: Needed to properly grab widget focus when set_skip_taskbar_hint set to True + window.present() + # NOTE: Need here to enforce sticky after hide and reshow. + window.stick() + else: + self.hidefunc() + + + def start(self): + Gtk.main() + + def stop(self, widget = None, eve = None): + event_system.emit("shutting-down") size = self.get_size() pos = self.get_position() @@ -130,6 +199,3 @@ class Window(Gtk.ApplicationWindow): settings_manager.clear_pid() Gtk.main_quit() - - def main(self): - Gtk.main() \ No newline at end of file diff --git a/src/libs/__init__.py b/src/libs/__init__.py index a8e5edd..620f163 100644 --- a/src/libs/__init__.py +++ b/src/libs/__init__.py @@ -1,3 +1,3 @@ """ - Utils module -""" + Libs Package +""" \ No newline at end of file diff --git a/src/libs/data_types/__init__.py b/src/libs/data_types/__init__.py deleted file mode 100644 index 16675ec..0000000 --- a/src/libs/data_types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - Dasta Class module -""" - -from .event import Event \ No newline at end of file diff --git a/src/libs/db/__init__.py b/src/libs/db/__init__.py new file mode 100644 index 0000000..d20f589 --- /dev/null +++ b/src/libs/db/__init__.py @@ -0,0 +1,6 @@ +""" + DB Package +""" + +from .models import User +from .db import DB \ No newline at end of file diff --git a/src/libs/db.py b/src/libs/db/db.py similarity index 100% rename from src/libs/db.py rename to src/libs/db/db.py diff --git a/src/libs/models.py b/src/libs/db/models.py similarity index 100% rename from src/libs/models.py rename to src/libs/db/models.py diff --git a/src/libs/debugging.py b/src/libs/debugging.py index b84193a..5eaa286 100644 --- a/src/libs/debugging.py +++ b/src/libs/debugging.py @@ -18,7 +18,7 @@ def debug_signal_handler(signal, frame): rpdb2.start_embedded_debugger("foobar", True, True) rpdb2.setbreak(depth=1) return - except StandardError: + except Exception: ... try: @@ -26,7 +26,7 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting embedded rconsole debugger...\n\n") rconsole.spawn_server() return - except StandardError as ex: + except Exception as ex: ... try: @@ -34,7 +34,15 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting PuDB debugger...\n\n") set_trace(paused = True) return - except StandardError as ex: + except Exception as ex: + ... + + try: + import ipdb + logger.debug("\n\nStarting IPDB debugger...\n\n") + ipdb.set_trace() + return + except Exception as ex: ... try: @@ -42,11 +50,11 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting embedded PDB debugger...\n\n") pdb.Pdb(skip=['gi.*']).set_trace() return - except StandardError as ex: + except Exception as ex: ... try: import code code.interact() - except StandardError as ex: - logger.debug(f"{ex}, returning to normal program flow...") + except Exception as ex: + logger.debug(f"{ex}, returning to normal program flow...") \ No newline at end of file diff --git a/src/libs/dto/__init__.py b/src/libs/dto/__init__.py new file mode 100644 index 0000000..8c55071 --- /dev/null +++ b/src/libs/dto/__init__.py @@ -0,0 +1,5 @@ +""" + Dasta Class Package +""" + +from .event import Event \ No newline at end of file diff --git a/src/libs/data_types/event.py b/src/libs/dto/event.py similarity index 100% rename from src/libs/data_types/event.py rename to src/libs/dto/event.py diff --git a/src/libs/ipc_server.py b/src/libs/ipc_server.py index a351b66..eacde83 100644 --- a/src/libs/ipc_server.py +++ b/src/libs/ipc_server.py @@ -13,17 +13,17 @@ from .singleton import Singleton class IPCServer(Singleton): - """ Create a listener so that other {app_name} instances send requests back to existing instance. """ + """ Create a listener so that other {APP_NAME} instances send requests back to existing instance. """ def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"): self.is_ipc_alive = False - self._ipc_port = 4848 + self._ipc_port = 0 # Use 0 to let Listener chose port self._ipc_address = ipc_address self._conn_type = conn_type - self._ipc_authkey = b'' + bytes(f'{app_name}-ipc', 'utf-8') + self._ipc_authkey = b'' + bytes(f'{APP_NAME}-ipc', 'utf-8') self._ipc_timeout = 15.0 if conn_type == "socket": - self._ipc_address = f'/tmp/{app_name}-ipc.sock' + self._ipc_address = f'/tmp/{APP_NAME}-ipc.sock' elif conn_type == "full_network": self._ipc_address = '0.0.0.0' elif conn_type == "full_network_unsecured": @@ -35,7 +35,7 @@ class IPCServer(Singleton): self._subscribe_to_events() def _subscribe_to_events(self): - event_system.subscribe("post_file_to_ipc", self.send_ipc_message) + event_system.subscribe("post-file-to-ipc", self.send_ipc_message) def create_ipc_listener(self) -> None: @@ -56,7 +56,7 @@ class IPCServer(Singleton): @daemon_threaded def _run_ipc_loop(self, listener) -> None: # NOTE: Not thread safe if using with Gtk. Need to import GLib and use idle_add - while True: + while self.is_ipc_alive: try: conn = listener.accept() start_time = time.perf_counter() @@ -67,19 +67,22 @@ class IPCServer(Singleton): listener.close() def _handle_ipc_message(self, conn, start_time) -> None: - while True: + while self.is_ipc_alive: msg = conn.recv() logger.debug(msg) if "FILE|" in msg: file = msg.split("FILE|")[1].strip() if file: - event_system.emit("handle_file_from_ipc", file) + event_system.emit("handle-file-from-ipc", file) + + conn.close() + break if "DIR|" in msg: file = msg.split("DIR|")[1].strip() if file: - event_system.emit("handle_dir_from_ipc", file) + event_system.emit("handle-dir-from-ipc", file) conn.close() break @@ -129,4 +132,4 @@ class IPCServer(Singleton): logger.error("IPC Socket no longer valid.... Removing.") os.unlink(self._ipc_address) except Exception as e: - logger.error( repr(e) ) + logger.error( repr(e) ) \ No newline at end of file diff --git a/src/libs/mixins/__init__.py b/src/libs/mixins/__init__.py index 6eb3b43..e852849 100644 --- a/src/libs/mixins/__init__.py +++ b/src/libs/mixins/__init__.py @@ -1,3 +1,3 @@ """ - Utils/Mixins module + Libs.Mixins Package """ \ No newline at end of file diff --git a/src/libs/mixins/dnd_mixin.py b/src/libs/mixins/dnd_mixin.py index e4c9eed..4e231d3 100644 --- a/src/libs/mixins/dnd_mixin.py +++ b/src/libs/mixins/dnd_mixin.py @@ -67,4 +67,4 @@ class DnDMixin: files.append(gfile) - event_system.emit('set_pre_drop_dnd', (files,)) \ No newline at end of file + event_system.emit('set-pre-drop-dnd', (files,)) \ No newline at end of file diff --git a/src/libs/mixins/ipc_signals_mixin.py b/src/libs/mixins/ipc_signals_mixin.py index bbabd1e..880266d 100644 --- a/src/libs/mixins/ipc_signals_mixin.py +++ b/src/libs/mixins/ipc_signals_mixin.py @@ -1,6 +1,8 @@ # Python imports # Lib imports +import gi +from gi.repository import GLib # Application imports @@ -8,13 +10,22 @@ class IPCSignalsMixin: - """ IPCSignalsMixin handle messages from another starting solarfm process. """ + """ IPCSignalsMixin handle messages from another starting {APP_NAME} process. """ - def print_to_console(self, message=None): + def print_to_console(self, message = None): logger.debug(message) - def handle_file_from_ipc(self, path: str) -> None: - logger.debug(f"File From IPC: {path}") + def handle_file_from_ipc(self, fpath: str) -> None: + logger.debug(f"File From IPC: {fpath}") + GLib.idle_add( + self.broadcast_message, "handle-file", (fpath,) + ) - def handle_dir_from_ipc(self, path: str) -> None: - logger.debug(f"Dir From IPC: {path}") \ No newline at end of file + def handle_dir_from_ipc(self, fpath: str) -> None: + logger.debug(f"Dir From IPC: {fpath}") + GLib.idle_add( + self.broadcast_message, "handle-folder", (fpath,) + ) + + def broadcast_message(self, message_type: str = "none", data: () = ()) -> None: + event_system.emit(message_type, data) \ No newline at end of file diff --git a/src/libs/mixins/keyboard_signals_mixin.py b/src/libs/mixins/keyboard_signals_mixin.py index 4659adf..03446d0 100644 --- a/src/libs/mixins/keyboard_signals_mixin.py +++ b/src/libs/mixins/keyboard_signals_mixin.py @@ -68,11 +68,11 @@ class KeyboardSignalsMixin: if mapping: self.handle_mapped_key_event(mapping) else: - self.handle_as_key_event_scope(mapping) + self.handle_as_key_event_scope(keyname) def handle_mapped_key_event(self, mapping): try: - self.handle_as_controller_scope() + self.handle_as_controller_scope(mapping) except Exception: self.handle_as_plugin_scope(mapping) @@ -86,13 +86,11 @@ class KeyboardSignalsMixin: sender = "" eve_type = mapping - self.handle_as_key_event_system(sender, eve_type) - - def handle_as_key_event_scope(self, mapping): - logger.debug(f"on_global_key_release_controller > key > {keyname}") + self.handle_key_event_system(sender, eve_type) + def handle_as_key_event_scope(self, keyname): if self.ctrl_down and not keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]: - self.handle_key_event_system(None, mapping) + self.handle_key_event_system(None, keyname) def handle_key_event_system(self, sender, eve_type): event_system.emit(eve_type) \ No newline at end of file diff --git a/src/libs/settings/__init__.py b/src/libs/settings/__init__.py new file mode 100644 index 0000000..228a75d --- /dev/null +++ b/src/libs/settings/__init__.py @@ -0,0 +1,4 @@ +""" + Settings Package +""" +from .manager import SettingsManager \ No newline at end of file diff --git a/src/libs/settings_manager/manager.py b/src/libs/settings/manager.py similarity index 54% rename from src/libs/settings_manager/manager.py rename to src/libs/settings/manager.py index 6aa0fb7..e4e1c87 100644 --- a/src/libs/settings_manager/manager.py +++ b/src/libs/settings/manager.py @@ -1,5 +1,6 @@ # Python imports import inspect +import time import json import zipfile @@ -22,35 +23,35 @@ class MissingConfigError(Exception): class SettingsManager(StartCheckMixin, Singleton): def __init__(self): - self._SCRIPT_PTH = path.dirname(path.realpath(__file__)) - self._USER_HOME = path.expanduser('~') - self._HOME_CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}" - self._USR_PATH = f"/usr/share/{app_name.lower()}" - self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json" + self._SCRIPT_PTH: str = path.dirname(path.realpath(__file__)) + self._USER_HOME: str = path.expanduser('~') + self._HOME_CONFIG_PATH: str = f"{self._USER_HOME}/.config/{APP_NAME.lower()}" + self._USR_PATH: str = f"/usr/share/{APP_NAME.lower()}" + self._USR_CONFIG_FILE: str = f"{self._USR_PATH}/settings.json" - self._CONTEXT_PATH = f"{self._HOME_CONFIG_PATH}/context_path" - self._PLUGINS_PATH = f"{self._HOME_CONFIG_PATH}/plugins" - self._DEFAULT_ICONS = f"{self._HOME_CONFIG_PATH}/icons" - self._CONFIG_FILE = f"{self._HOME_CONFIG_PATH}/settings.json" - self._GLADE_FILE = f"{self._HOME_CONFIG_PATH}/Main_Window.glade" - self._CSS_FILE = f"{self._HOME_CONFIG_PATH}/stylesheet.css" - self._KEY_BINDINGS_FILE = f"{self._HOME_CONFIG_PATH}/key-bindings.json" - self._PID_FILE = f"{self._HOME_CONFIG_PATH}/{app_name.lower()}.pid" - self._UI_WIDEGTS_PATH = f"{self._HOME_CONFIG_PATH}/ui_widgets" - self._CONTEXT_MENU = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" - self._WINDOW_ICON = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png" + self._CONTEXT_PATH: str = f"{self._HOME_CONFIG_PATH}/context_path" + self._PLUGINS_PATH: str = f"{self._HOME_CONFIG_PATH}/plugins" + self._DEFAULT_ICONS: str = f"{self._HOME_CONFIG_PATH}/icons" + self._CONFIG_FILE: str = f"{self._HOME_CONFIG_PATH}/settings.json" + self._GLADE_FILE: str = f"{self._HOME_CONFIG_PATH}/Main_Window.glade" + self._CSS_FILE: str = f"{self._HOME_CONFIG_PATH}/stylesheet.css" + self._KEY_BINDINGS_FILE: str = f"{self._HOME_CONFIG_PATH}/key-bindings.json" + self._PID_FILE: str = f"{self._HOME_CONFIG_PATH}/{APP_NAME.lower()}.pid" + self._UI_WIDEGTS_PATH: str = f"{self._HOME_CONFIG_PATH}/ui_widgets" + self._CONTEXT_MENU: str = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" + self._WINDOW_ICON: str = f"{self._DEFAULT_ICONS}/{APP_NAME.lower()}.png" - # self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json" - # self._PLUGINS_PATH = f"plugins" - # self._CONFIG_FILE = f"settings.json" - # self._GLADE_FILE = f"Main_Window.glade" - # self._CSS_FILE = f"stylesheet.css" - # self._KEY_BINDINGS_FILE = f"key-bindings.json" - # self._PID_FILE = f"{app_name.lower()}.pid" - # self._WINDOW_ICON = f"{app_name.lower()}.png" - # self._UI_WIDEGTS_PATH = f"ui_widgets" - # self._CONTEXT_MENU = f"contexct_menu.json" - # self._DEFAULT_ICONS = f"icons" + # self._USR_CONFIG_FILE: str = f"{self._USR_PATH}/settings.json" + # self._PLUGINS_PATH: str = f"plugins" + # self._CONFIG_FILE: str = f"settings.json" + # self._GLADE_FILE: str = f"Main_Window.glade" + # self._CSS_FILE: str = f"stylesheet.css" + # self._KEY_BINDINGS_FILE: str = f"key-bindings.json" + # self._PID_FILE: str = f"{APP_NAME.lower()}.pid" + # self._WINDOW_ICON: str = f"{APP_NAME.lower()}.png" + # self._UI_WIDEGTS_PATH: str = f"ui_widgets" + # self._CONTEXT_MENU: str = f"contexct_menu.json" + # self._DEFAULT_ICONS: str = f"icons" # with zipfile.ZipFile("files.zip", mode="r", allowZip64=True) as zf: @@ -79,7 +80,7 @@ class SettingsManager(StartCheckMixin, Singleton): if not path.exists(self._CSS_FILE): raise MissingConfigError("Unable to find the application Stylesheet file.") if not path.exists(self._WINDOW_ICON): - self._WINDOW_ICON = f"{self._USR_PATH}/icons/{app_name.lower()}.png" + self._WINDOW_ICON = f"{self._USR_PATH}/icons/{APP_NAME.lower()}.png" if not path.exists(self._WINDOW_ICON): raise MissingConfigError("Unable to find the application icon.") if not path.exists(self._UI_WIDEGTS_PATH): @@ -90,7 +91,9 @@ class SettingsManager(StartCheckMixin, Singleton): try: with open(self._KEY_BINDINGS_FILE) as file: - bindings = json.load(file)["keybindings"] + bindings = json.load(file)["keybindings"] + self._guake_key = bindings["guake_key"] + keybindings.configure(bindings) except Exception as e: print( f"Settings Manager: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" ) @@ -102,23 +105,25 @@ class SettingsManager(StartCheckMixin, Singleton): print( f"Settings Manager: {self._CONTEXT_MENU}\n\t\t{repr(e)}" ) - self.settings: Settings = None - self._main_window = None - self._builder = None - self.PAINT_BG_COLOR = (0, 0, 0, 0.0) + self.settings: Settings = None + self._main_window = None + self._builder = None + self.PAINT_BG_COLOR: tuple = (0, 0, 0, 0.0) - self._trace_debug = False - self._debug = False - self._dirty_start = False + self._trace_debug: bool = False + self._debug: bool = False + self._dirty_start: bool = False + self._passed_in_file: bool = False + self._starting_files: list = [] - def register_signals_to_builder(self, classes=None): + def register_signals_to_builder(self, classes = None): handlers = {} for c in classes: methods = None try: - methods = inspect.getmembers(c, predicate=inspect.ismethod) + methods = inspect.getmembers(c, predicate = inspect.ismethod) handlers.update(methods) except Exception as e: ... @@ -128,7 +133,6 @@ class SettingsManager(StartCheckMixin, Singleton): def set_main_window(self, window): self._main_window = window def set_builder(self, builder) -> any: self._builder = builder - def get_monitor_data(self) -> list: screen = self._main_window.get_screen() monitors = [] @@ -152,29 +156,46 @@ class SettingsManager(StartCheckMixin, Singleton): def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH def get_window_icon(self) -> str: return self._WINDOW_ICON def get_home_path(self) -> str: return self._USER_HOME + def get_starting_files(self) -> list: return self._starting_files + def get_guake_key(self) -> tuple: return self._guake_key + + def get_starting_args(self): + return self.args, self.unknownargs + + def set_main_window_x(self, x: int = 0): self.settings.config.main_window_x = x + def set_main_window_y(self, y: int = 0): self.settings.config.main_window_y = y + def set_main_window_width(self, width: int = 800): self.settings.config.main_window_width = width + def set_main_window_height(self, height: int = 600): self.settings.config.main_window_height = height + def set_main_window_min_width(self, width: int = 720): self.settings.config.main_window_min_width = width + def set_main_window_min_height(self, height: int = 480): self.settings.config.main_window_min_height = height + def set_starting_files(self, files: list): self._starting_files = files + def set_start_load_time(self): self._start_load_time = time.perf_counter() + def set_end_load_time(self): self._end_load_time = time.perf_counter() + + def set_starting_args(self, args, unknownargs): + self.args = args + self.unknownargs = unknownargs + + def set_trace_debug(self, trace_debug: bool): + self._trace_debug = trace_debug + + def set_debug(self, debug: bool): + self._debug = debug + + def set_is_starting_with_file(self, is_passed_in_file: bool = False): + self._passed_in_file = is_passed_in_file def is_trace_debug(self) -> str: return self._trace_debug def is_debug(self) -> str: return self._debug + def is_starting_with_file(self) -> bool: return self._passed_in_file - def call_method(self, target_class = None, _method_name = None, data = None): + def log_load_time(self): logger.info( f"Load Time: {self._end_load_time - self._start_load_time}" ) + + def call_method(self, target_class: any = None, _method_name: str = "", data: any = None): method_name = str(_method_name) method = getattr(target_class, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") return method(data) if data else method() - def set_main_window_x(self, x = 0): self.settings.config.main_window_x = x - def set_main_window_y(self, y = 0): self.settings.config.main_window_y = y - def set_main_window_width(self, width = 800): self.settings.config.main_window_width = width - def set_main_window_height(self, height = 600): self.settings.config.main_window_height = height - def set_main_window_min_width(self, width = 720): self.settings.config.main_window_min_width = width - def set_main_window_min_height(self, height = 480): self.settings.config.main_window_min_height = height - - def set_trace_debug(self, trace_debug): - self._trace_debug = trace_debug - - def set_debug(self, debug): - self._debug = debug - - def load_settings(self): if not path.exists(self._CONFIG_FILE): self.settings = Settings() diff --git a/src/libs/settings_manager/options/__init__.py b/src/libs/settings/options/__init__.py similarity index 67% rename from src/libs/settings_manager/options/__init__.py rename to src/libs/settings/options/__init__.py index 0046efd..e06487a 100644 --- a/src/libs/settings_manager/options/__init__.py +++ b/src/libs/settings/options/__init__.py @@ -1,8 +1,8 @@ """ - Options module + Settings.Options Package """ from .settings import Settings from .config import Config from .filters import Filters from .theming import Theming -from .debugging import Debugging +from .debugging import Debugging \ No newline at end of file diff --git a/src/libs/settings_manager/options/config.py b/src/libs/settings/options/config.py similarity index 100% rename from src/libs/settings_manager/options/config.py rename to src/libs/settings/options/config.py diff --git a/src/libs/settings_manager/options/debugging.py b/src/libs/settings/options/debugging.py similarity index 100% rename from src/libs/settings_manager/options/debugging.py rename to src/libs/settings/options/debugging.py diff --git a/src/libs/settings_manager/options/filters.py b/src/libs/settings/options/filters.py similarity index 100% rename from src/libs/settings_manager/options/filters.py rename to src/libs/settings/options/filters.py diff --git a/src/libs/settings_manager/options/settings.py b/src/libs/settings/options/settings.py similarity index 100% rename from src/libs/settings_manager/options/settings.py rename to src/libs/settings/options/settings.py diff --git a/src/libs/settings_manager/options/theming.py b/src/libs/settings/options/theming.py similarity index 100% rename from src/libs/settings_manager/options/theming.py rename to src/libs/settings/options/theming.py diff --git a/src/libs/settings/other/__init__.py b/src/libs/settings/other/__init__.py new file mode 100644 index 0000000..c38a726 --- /dev/null +++ b/src/libs/settings/other/__init__.py @@ -0,0 +1,3 @@ +""" + Settings.Other Package +""" \ No newline at end of file diff --git a/src/libs/settings_manager/other/webkit_ui_settings.py b/src/libs/settings/other/webkit_ui_settings.py similarity index 96% rename from src/libs/settings_manager/other/webkit_ui_settings.py rename to src/libs/settings/other/webkit_ui_settings.py index 962fe60..981ea49 100644 --- a/src/libs/settings_manager/other/webkit_ui_settings.py +++ b/src/libs/settings/other/webkit_ui_settings.py @@ -39,4 +39,4 @@ class WebkitUISettings(WebKit2.Settings): self.set_enable_webaudio(True) self.set_enable_accelerated_2d_canvas(True) - self.set_user_agent(f"{app_name}") \ No newline at end of file + self.set_user_agent(f"{APP_NAME}") \ No newline at end of file diff --git a/src/libs/settings_manager/start_check_mixin.py b/src/libs/settings/start_check_mixin.py similarity index 91% rename from src/libs/settings_manager/start_check_mixin.py rename to src/libs/settings/start_check_mixin.py index 6fc8208..b47d9bd 100644 --- a/src/libs/settings_manager/start_check_mixin.py +++ b/src/libs/settings/start_check_mixin.py @@ -41,7 +41,7 @@ class StartCheckMixin: try: os.kill(pid, 0) except OSError: - print(f"{app_name} PID file exists but PID is irrelevant; starting dirty...") + print(f"{APP_NAME} PID file exists but PID is irrelevant; starting dirty...") self._dirty_start = True return False @@ -53,7 +53,7 @@ class StartCheckMixin: self._print_pid(pid) def _print_pid(self, pid): - print(f"{app_name} PID: {pid}") + print(f"{APP_NAME} PID: {pid}") def _clean_pid(self): os.unlink(self._PID_FILE) diff --git a/src/libs/settings_manager/__init__.py b/src/libs/settings_manager/__init__.py deleted file mode 100644 index a0b3452..0000000 --- a/src/libs/settings_manager/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" - Settings module -""" -from .manager import SettingsManager diff --git a/src/libs/settings_manager/other/__init__.py b/src/libs/settings_manager/other/__init__.py deleted file mode 100644 index e1b5377..0000000 --- a/src/libs/settings_manager/other/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" - Settings Other module -""" diff --git a/src/libs/singleton.py b/src/libs/singleton.py index 23b7191..b484b28 100644 --- a/src/libs/singleton.py +++ b/src/libs/singleton.py @@ -12,13 +12,11 @@ class SingletonError(Exception): class Singleton: - ccount = 0 + _instance = None def __new__(cls, *args, **kwargs): - obj = super(Singleton, cls).__new__(cls) - cls.ccount += 1 + if cls._instance: + raise SingletonError(f"'{cls.__name__}' is a Singleton. Cannot create a new instance...") - if cls.ccount == 2: - raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...") - - return obj + cls._instance = super(Singleton, cls).__new__(cls) + return cls._instance diff --git a/src/libs/status_icon.py b/src/libs/status_icon.py new file mode 100644 index 0000000..2769a05 --- /dev/null +++ b/src/libs/status_icon.py @@ -0,0 +1,67 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('AppIndicator3', '0.1') +from gi.repository import Gtk +from gi.repository import GLib +from gi.repository import AppIndicator3 + +# Application imports + + + +class StatusIcon(): + """ StatusIcon for Application to go to Status Tray. """ + + def __init__(self): + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + status_menu = Gtk.Menu() + icon_theme = Gtk.IconTheme.get_default() + check_menu_item = Gtk.CheckMenuItem.new_with_label("Update icon") + quit_menu_item = Gtk.MenuItem.new_with_label("Quit") + + # Create StatusNotifierItem + self.indicator = AppIndicator3.Indicator.new( + f"{APP_NAME}-statusicon", + "gtk-info", + AppIndicator3.IndicatorCategory.APPLICATION_STATUS) + self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + + check_menu_item.connect("activate", self.check_menu_item_cb) + quit_menu_item.connect("activate", self.quit_menu_item_cb) + icon_theme.connect('changed', self.icon_theme_changed_cb) + + self.indicator.set_menu(status_menu) + status_menu.append(check_menu_item) + status_menu.append(quit_menu_item) + status_menu.show_all() + + def update_icon(self, icon_name): + self.indicator.set_icon(icon_name) + + def check_menu_item_cb(self, widget, data = None): + icon_name = "parole" if widget.get_active() else "gtk-info" + self.update_icon(icon_name) + + def icon_theme_changed_cb(self, theme): + self.update_icon("gtk-info") + + def quit_menu_item_cb(self, widget, data = None): + event_system.emit("tear-down") diff --git a/src/plugins/manifest.py b/src/plugins/manifest.py index 1b93f34..7cb701c 100644 --- a/src/plugins/manifest.py +++ b/src/plugins/manifest.py @@ -15,13 +15,14 @@ class ManifestProcessor(Exception): class Plugin: - path: str = None - name: str = None - author: str = None - version: str = None - support: str = None - requests:{} = None - reference: type = None + path: str = None + name: str = None + author: str = None + version: str = None + support: str = None + requests:{} = None + reference: type = None + pre_launch: bool = False class ManifestProcessor: @@ -46,23 +47,29 @@ class ManifestProcessor: plugin.support = self._manifest["support"] plugin.requests = self._manifest["requests"] + if "pre_launch" in self._manifest.keys(): + plugin.pre_launch = True if self._manifest["pre_launch"] == "true" else False + return plugin def get_loading_data(self): loading_data = {} requests = self._plugin.requests - keys = requests.keys() - if "pass_events" in keys: + if "pass_events" in requests: if requests["pass_events"] in ["true"]: loading_data["pass_events"] = True - if "bind_keys" in keys: - if isinstance(requests["bind_keys"], list): - loading_data["bind_keys"] = requests["bind_keys"] - - if "pass_ui_objects" in keys: + if "pass_ui_objects" in requests: if isinstance(requests["pass_ui_objects"], list): loading_data["pass_ui_objects"] = [ self._builder.get_object(obj) for obj in requests["pass_ui_objects"] ] + if "bind_keys" in requests: + if isinstance(requests["bind_keys"], list): + loading_data["bind_keys"] = requests["bind_keys"] + return self._plugin, loading_data + + def is_pre_launch(self): + return self._plugin.pre_launch + diff --git a/src/plugins/plugins_controller.py b/src/plugins/plugins_controller.py index a77186c..10d5dc2 100644 --- a/src/plugins/plugins_controller.py +++ b/src/plugins/plugins_controller.py @@ -10,6 +10,7 @@ from os.path import isdir import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk +from gi.repository import GLib from gi.repository import Gio # Application imports @@ -35,11 +36,23 @@ class PluginsController: self._plugins_dir_watcher = None self._plugin_collection = [] + self._plugin_manifests = {} + + self._load_manifests() - def launch_plugins(self) -> None: + def _load_manifests(self): + logger.info(f"Loading manifests...") + + for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]: + manifest = ManifestProcessor(path, self._builder) + self._plugin_manifests[path] = { + "path": path, + "folder": folder, + "manifest": manifest + } + self._set_plugins_watcher() - self.load_plugins() def _set_plugins_watcher(self) -> None: self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \ @@ -52,21 +65,47 @@ class PluginsController: Gio.FileMonitorEvent.MOVED_OUT]: self.reload_plugins(file) - def load_plugins(self, file: str = None) -> None: - logger.info(f"Loading plugins...") + def pre_launch_plugins(self) -> None: + logger.info(f"Loading pre-launch plugins...") + plugin_manifests: {} = {} + + for key in self._plugin_manifests: + target_manifest = self._plugin_manifests[key]["manifest"] + if target_manifest.is_pre_launch(): + plugin_manifests[key] = self._plugin_manifests[key] + + self._load_plugins(plugin_manifests, is_pre_launch = True) + + def post_launch_plugins(self) -> None: + logger.info(f"Loading post-launch plugins...") + plugin_manifests: {} = {} + + for key in self._plugin_manifests: + target_manifest = self._plugin_manifests[key]["manifest"] + if not target_manifest.is_pre_launch(): + plugin_manifests[key] = self._plugin_manifests[key] + + self._load_plugins(plugin_manifests) + + def _load_plugins(self, plugin_manifests: {} = {}, is_pre_launch: bool = False) -> None: parent_path = os.getcwd() - for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]: - try: - target = join(path, "plugin.py") - manifest = ManifestProcessor(path, self._builder) + for key in plugin_manifests: + target_manifest = plugin_manifests[key] + path, folder, manifest = target_manifest["path"], target_manifest["folder"], target_manifest["manifest"] + try: + target = join(path, "plugin.py") if not os.path.exists(target): raise InvalidPluginException("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") plugin, loading_data = manifest.get_loading_data() module = self.load_plugin_module(path, folder, target) - self.execute_plugin(module, plugin, loading_data) + + if is_pre_launch: + self.execute_plugin(module, plugin, loading_data) + else: + GLib.idle_add(self.execute_plugin, *(module, plugin, loading_data)) except Exception as e: logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !") logger.debug("Trace: ", traceback.print_exc()) diff --git a/user_config/usr/share/app_name/context_path/resources/css/main.css b/user_config/usr/share/app_name/context_path/resources/css/main.css index 7264b0e..d7dea7c 100644 --- a/user_config/usr/share/app_name/context_path/resources/css/main.css +++ b/user_config/usr/share/app_name/context_path/resources/css/main.css @@ -43,6 +43,6 @@ /* Other message text colors */ -.errorTxt { color: rgb(170, 18, 18); } -.warningTxt { color: rgb(255, 168, 0); } -.successTxt { color: rgb(136, 204, 39); } +.error-txt { color: rgb(170, 18, 18); } +.warning-txt { color: rgb(255, 168, 0); } +.success=txt { color: rgb(136, 204, 39); } \ No newline at end of file diff --git a/user_config/usr/share/app_name/context_path/resources/css/overrides.css b/user_config/usr/share/app_name/context_path/resources/css/overrides.css index 8501c90..950c496 100644 --- a/user_config/usr/share/app_name/context_path/resources/css/overrides.css +++ b/user_config/usr/share/app_name/context_path/resources/css/overrides.css @@ -1,6 +1,7 @@ html, body { display: block; - background-color: #32383e00; +// background-color: #32383e00; + background-color: rgba(39, 43, 52, 0.64); color: #ffffff; text-wrap: wrap; } diff --git a/user_config/usr/share/app_name/icons/app_name-64x64.png b/user_config/usr/share/app_name/icons/-64x64.png similarity index 100% rename from user_config/usr/share/app_name/icons/app_name-64x64.png rename to user_config/usr/share/app_name/icons/-64x64.png diff --git a/user_config/usr/share/app_name/icons/app_name.png b/user_config/usr/share/app_name/icons/.png similarity index 100% rename from user_config/usr/share/app_name/icons/app_name.png rename to user_config/usr/share/app_name/icons/.png diff --git a/user_config/usr/share/app_name/key-bindings.json b/user_config/usr/share/app_name/key-bindings.json index a172eea..9e47bfb 100644 --- a/user_config/usr/share/app_name/key-bindings.json +++ b/user_config/usr/share/app_name/key-bindings.json @@ -1,23 +1,25 @@ { "keybindings": { "help" : "F1", - "rename_files" : ["F2", "e"], - "open_terminal" : "F4", - "refresh_tab" : ["F5", "r"], - "delete_files" : "Delete", - "tggl_top_main_menubar" : "Alt_L", - "trash_files" : "t", - "tear_down" : "q", - "go_up" : "Up", - "go_home" : "slash", - "grab_focus_path_entry" : "l", - "open_files" : "o", - "show_hide_hidden_files" : "h", - "keyboard_create_tab" : "t", - "keyboard_close_tab" : "w", - "keyboard_copy_files" : "c", - "keyboard_cut_files" : "x", - "paste_files" : "v", - "show_new_file_menu" : "n" + "guake_key" : "", + "rename-files" : ["F2", "e"], + "open-terminal" : "F4", + "refresh-tab" : ["F5", "r"], + "delete-files" : "Delete", + "tggl-top-main-menubar" : "Alt_L", + "tggl-top-main-menubar" : "0", + "trash-files" : "t", + "tear-down" : "q", + "go-up" : "Up", + "go-home" : "slash", + "grab-focus-path-entry" : "l", + "open-files" : "o", + "show-hide-hidden-files" : "h", + "keyboard-create-tab" : "t", + "keyboard-close-tab" : "w", + "keyboard-copy-files" : "c", + "keyboard-cut-files" : "x", + "paste-files" : "v", + "show-new-file-menu" : "n" } } diff --git a/user_config/usr/share/app_name/stylesheet.css b/user_config/usr/share/app_name/stylesheet.css index ce012b1..7d3c240 100644 --- a/user_config/usr/share/app_name/stylesheet.css +++ b/user_config/usr/share/app_name/stylesheet.css @@ -1,3 +1,18 @@ +/* ---- Make most desired things base transparent ---- */ +popover, +popover > box, +.main-window, +.base-container, +.body-container, +.center-container, +.header-container, +.footer-container, +.left-containerm, +.right-container { + background: rgba(0, 0, 0, 0.0); + color: rgba(255, 255, 255, 1); +} + .base-container { margin: 10px; } @@ -128,3 +143,4 @@ XfdesktopIconView.view:active { .mw_transparency_97 { background: rgba(39, 43, 52, 0.97); } .mw_transparency_98 { background: rgba(39, 43, 52, 0.98); } .mw_transparency_99 { background: rgba(39, 43, 52, 0.99); } +.mw_transparency_100 { background: rgba(39, 43, 52, 1.00); } \ No newline at end of file