Compare commits

..

16 Commits

Author SHA1 Message Date
2c453bc621 Fixed IPC issues with passing paths back to instance 2025-12-10 20:09:32 -06:00
530fe7c3ab Updated jeybinding and restructured css 2025-11-30 19:51:04 -06:00
5e5cc50ba8 Fixed empty vte commit error 2025-11-30 00:59:37 -06:00
22736147e6 added clock, pager, and task list widgets for ref; added footer container; added option for key combo o universally toggle window with key combo if set 2025-11-29 23:22:03 -06:00
4c25ce297c Corrected css for popover box 2025-10-21 22:21:06 -05:00
60b55da973 Moved to use contextlib suppress pattern than empty exception blocks 2025-10-21 22:20:06 -05:00
7c0d87fd20 Slight call start restructuring, fixing css for popover box 2025-08-25 00:22:42 -05:00
4cd5a1f089 Fixing readme n requirements 2025-07-13 14:17:52 -05:00
18dc71dcb0 Adding generic doc string basecontroller 2025-07-13 14:15:25 -05:00
dd3e87f636 Adding call chain wrapper; cleanup; optimization 2024-12-22 00:50:32 -06:00
cca007db76 Added VTE widget; css changes; format cleanup 2024-11-08 22:46:07 -06:00
33c0827ca2 Moved args info to settings and restructured calls 2024-10-20 16:03:19 -05:00
f2b33066af Added stronger typing in settings; logging loading times; css changes to transparency 2024-10-20 15:20:18 -05:00
fafc1a985f Updated __init__ files; theming css changes, other 2024-10-14 21:57:18 -05:00
e2e9dc8c1f made default hiding transparency controls easier with restructured show calls 2024-08-31 22:27:11 -05:00
25b6b5305b update readme 2024-07-26 19:53:04 -05:00
46 changed files with 832 additions and 199 deletions

View File

@@ -9,6 +9,7 @@ A template project for Python with Gtk applications.
* 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/<app name> can go to ~/.config folder if prefered.
* In additiion, place the plugins folder in the same app folder you moved to /usr/share/<app name> or ~/.config/<app name> .
There are a "\<change_me\>" strings and files that need to be set according to your app's name located at:
@@ -21,4 +22,4 @@ There are a "\<change_me\>" 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.
The logic follows Debian Dpkg packaging and its placement logic.

13
pyrightconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"reportUndefinedVariable": false,
"reportUnusedVariable": false,
"reportUnusedImport": true,
"reportDuplicateImport": true,
"executionEnvironments": [
{
"root": "./src/versions/solarfm-0.0.1/solarfm"
}
],
"venvPath": ".",
"venv": ".venv"
}

View File

@@ -4,4 +4,4 @@ setproctitle==1.2.2
pyxdg==0.27
psutil==5.8.0
pycryptodome==3.20.0
sqlmodel==0.0.19
sqlmodel==0.0.19

View File

@@ -1,5 +1,6 @@
# Python imports
import builtins
import traceback
import threading
import sys
@@ -31,6 +32,17 @@ 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...
@@ -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
sys.excepthook = custom_except_hook

View File

@@ -1,3 +1,3 @@
"""
Start of package.
"""
Src Package.
"""

View File

@@ -17,8 +17,9 @@ from app import Application
def main(args, unknownargs):
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()

View File

@@ -1,4 +1,5 @@
# Python imports
from contextlib import suppress
import signal
import os
@@ -19,27 +20,40 @@ 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.setup_debug_hook()
Window(args, unknownargs).main()
def load_ipc(self, args, unknownargs):
ipc_server = IPCServer()
def run(self):
if not settings_manager.is_trace_debug():
if not self.load_ipc():
return
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 ipc_server.is_ipc_alive:
return True
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)
logger.warning(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...")
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...")
if os.path.isdir(arg):
message = f"DIR|{arg}"
ipc_server.send_ipc_message(message)
return False
def ipc_realization_check(self, ipc_server):
try:
@@ -47,18 +61,12 @@ class Application:
except Exception:
ipc_server.send_test_ipc_message()
try:
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 <pid> from Linux/Unix or SIGBREAK signal from Windows
signal.signal(
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR2"),
debug_signal_handler
)
except ValueError:
# Typically: ValueError: signal only works in main thread
...

View File

@@ -1,3 +1,3 @@
"""
Core Module
Core Package
"""

View File

@@ -1,3 +1,3 @@
"""
Containers Module
"""
Containers Package
"""

View File

@@ -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):
@@ -39,6 +40,7 @@ class BaseContainer(Gtk.Box):
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}")

View File

@@ -23,7 +23,7 @@ class BodyContainer(Gtk.Box):
self._subscribe_to_events()
self._load_widgets()
self.show_all()
self.show()
def _setup_styling(self):

View File

@@ -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")
@@ -39,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() )

View File

@@ -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):
...

View File

@@ -22,11 +22,14 @@ 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):
@@ -48,4 +51,4 @@ class HeaderContainer(Gtk.Box):
event_system.emit("load-interactive-debug")
def tggl_top_main_menubar(self):
self.hide() if self.is_visible() else self.show()
self.hide() if self.is_visible() else self.show_all()

View File

@@ -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")
@@ -31,4 +36,4 @@ class LeftContainer(Gtk.Box):
...
def _load_widgets(self):
...
...

View File

@@ -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")
@@ -31,4 +37,5 @@ class RightContainer(Gtk.Box):
...
def _load_widgets(self):
...
vte_widget = VteWidget()
self.add( vte_widget )

View File

@@ -1,3 +1,3 @@
"""
Controllers Module
Controllers Package
"""

View File

@@ -17,26 +17,20 @@ from .bridge_controller import BridgeController
class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData):
def __init__(self, args, unknownargs):
self.collect_files_dirs(args, unknownargs)
""" docstring for BaseController. """
self.setup_controller_data()
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_controller.pre_launch_plugins()
if args.no_plugins == "false":
self.plugins_controller.post_launch_plugins()
for file in settings_manager.get_starting_files():
event_system.emit("post-file-to-ipc", file)
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):
@@ -56,15 +50,23 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData):
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 _load_glade_file(self):
self.builder.add_from_file(settings_manager.get_glade_file())
self.builder.add_from_file( settings_manager.get_glade_file() )
self.builder.expose_object("main_window", self.window)
settings_manager.set_builder(self.builder)
self.base_container = BaseContainer()
settings_manager.register_signals_to_builder([self, self.base_container])
settings_manager.register_signals_to_builder([self, self.base_container])

View File

@@ -14,7 +14,7 @@ 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:
def _setup_controller_data(self) -> None:
self.window = settings_manager.get_main_window()
self.builder = BuilderWrapper()
self.plugins_controller = PluginsController()
@@ -25,22 +25,31 @@ class BaseControllerData:
self.shift_down = False
self.alt_down = False
self._collect_files_dirs()
self._load_glade_file()
def collect_files_dirs(self, args, unknownargs):
files = []
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
if len(files) > 0:
settings_manager.set_is_starting_with_file(True)
settings_manager.set_starting_files(files)
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
@@ -76,24 +85,22 @@ 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()

View File

@@ -10,8 +10,6 @@ import base64
class BridgeController:
def __init__(self):
self.opened_files = {}
self._setup_signals()
self._subscribe_to_events()

View File

@@ -1,3 +1,3 @@
"""
Widgets Module
"""
Widgets Package
"""

View File

@@ -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()

View File

@@ -1,3 +1,3 @@
"""
Widgets.Controls Module
"""
Widgets.Controls Package
"""

View File

@@ -1,12 +1,11 @@
# Python imports
from contextlib import suppress
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Gio
# Application imports
@@ -60,11 +59,9 @@ class OpenFilesButton(Gtk.Button):
chooser.set_select_multiple(True)
try:
with suppress(Exception):
folder = widget.get_current_file().get_parent() if not start_dir else start_dir
chooser.set_current_folder( folder.get_path() )
except Exception as e:
...
response = chooser.run()
if not response == Gtk.ResponseType.OK:

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -0,0 +1,128 @@
# 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()
if not text: return
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'))

View File

@@ -1,3 +1,3 @@
"""
WebKit2 UI Module
WebKit2 UI Package
"""

View File

@@ -10,6 +10,11 @@ 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
@@ -24,20 +29,24 @@ 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._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()
@@ -45,6 +54,9 @@ class Window(Gtk.ApplicationWindow):
def _setup_styling(self):
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
@@ -56,24 +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("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
@@ -118,8 +139,53 @@ class Window(Gtk.ApplicationWindow):
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):
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()
@@ -133,6 +199,3 @@ class Window(Gtk.ApplicationWindow):
settings_manager.clear_pid()
Gtk.main_quit()
def main(self):
Gtk.main()

View File

@@ -1,3 +1,3 @@
"""
Utils module
"""
Libs Package
"""

View File

@@ -1,6 +1,6 @@
"""
DB module
DB Package
"""
from .models import User
from .db import DB
from .db import DB

View File

@@ -1,5 +1,5 @@
"""
Dasta Class module
Dasta Class Package
"""
from .event import Event

View File

@@ -16,7 +16,7 @@ class IPCServer(Singleton):
""" 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')
@@ -56,18 +56,22 @@ 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()
self._handle_ipc_message(conn, start_time)
except EOFError as e:
logger.debug( repr(e) )
except Exception as e:
logger.debug( repr(e) )
finally:
conn.close()
listener.close()
def _handle_ipc_message(self, conn, start_time) -> None:
while True:
while self.is_ipc_alive:
msg = conn.recv()
logger.debug(msg)
@@ -76,6 +80,9 @@ class IPCServer(Singleton):
if file:
event_system.emit("handle-file-from-ipc", file)
conn.close()
break
if "DIR|" in msg:
file = msg.split("DIR|")[1].strip()
if file:
@@ -85,7 +92,7 @@ class IPCServer(Singleton):
break
if msg in ['close connection', 'close server']:
if msg in ['close connection', 'close server', 'Empty Data...']:
conn.close()
break
@@ -129,4 +136,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) )

View File

@@ -1,3 +1,3 @@
"""
Utils/Mixins module
Libs.Mixins Package
"""

View File

@@ -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}")
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)

View File

@@ -1,4 +1,4 @@
"""
Settings module
Settings Package
"""
from .manager import SettingsManager
from .manager import SettingsManager

View File

@@ -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:
@@ -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,25 +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._passed_in_file = False
self._starting_files = []
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:
...
@@ -153,34 +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) -> []: return self._starting_files
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_starting_files(self, files: []) -> None: self._starting_files = files
def set_trace_debug(self, trace_debug):
self._trace_debug = trace_debug
def set_debug(self, debug):
self._debug = debug
def set_is_starting_with_file(self, is_passed_in_file: False):
self._passed_in_file = is_passed_in_file
def load_settings(self):
if not path.exists(self._CONFIG_FILE):
self.settings = Settings()
@@ -193,4 +208,4 @@ class SettingsManager(StartCheckMixin, Singleton):
def save_settings(self):
with open(self._CONFIG_FILE, 'w') as outfile:
json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4)
json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4)

View File

@@ -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

View File

@@ -1,3 +1,3 @@
"""
Settings Other module
"""
Settings.Other Package
"""

View File

@@ -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

View File

@@ -69,3 +69,7 @@ class ManifestProcessor:
loading_data["bind_keys"] = requests["bind_keys"]
return self._plugin, loading_data
def is_pre_launch(self):
return self._plugin.pre_launch

View File

@@ -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); }

View File

@@ -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;
}

View File

@@ -1,10 +1,12 @@
{
"keybindings": {
"help" : "F1",
"guake_key" : "",
"rename-files" : ["F2", "<Control>e"],
"open-terminal" : "F4",
"refresh-tab" : ["F5", "<Control>r"],
"delete-files" : "Delete",
"load-interactive-debug" : "<Control>d",
"tggl-top-main-menubar" : "Alt_L",
"tggl-top-main-menubar" : "<Control>0",
"trash-files" : "<Shift><Control>t",

View File

@@ -1,16 +1,37 @@
/* ---- Make most desired things base transparent ---- */
popover,
popover > *,
scrolledwindow > *,
textview > *,
.main-window > .base-container > .body-container,
.main-window > .base-container > .body-container > .left-container,
.main-window > .base-container > .body-container > .center-container,
.main-window > .base-container > .body-container > .right-container {
popover > box,
notebook,
header,
stack,
scrolledwindow,
viewport {
background: rgba(0, 0, 0, 0.0);
color: rgba(255, 255, 255, 1);
}
tab {
color: rgba(255, 255, 255, 1);
}
tab:checked {
border-bottom-color: rgba(125, 125, 125, 1);
}
.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;
}