Compare commits

..

9 Commits

25 changed files with 483 additions and 54 deletions

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...
@@ -54,6 +66,7 @@ builtins.logger = Logger(
builtins.threaded = threaded_wrapper
builtins.daemon_threaded = daemon_threaded_wrapper
builtins.call_chain = call_chain_wrapper
@@ -64,4 +77,4 @@ def custom_except_hook(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

@@ -28,7 +28,9 @@ def main():
settings_manager.set_trace_debug(True)
settings_manager.do_dirty_start_check()
Application()
app = Application()
app.run()

View File

@@ -1,4 +1,5 @@
# Python imports
from contextlib import suppress
import signal
import os
@@ -26,9 +27,12 @@ class Application:
self.load_ipc()
self.setup_debug_hook()
Window().main()
def run(self):
win = Window()
win.start()
def load_ipc(self):
args, \
unknownargs = settings_manager.get_starting_args()
@@ -49,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 <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

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

@@ -26,6 +26,10 @@ class CenterContainer(Gtk.Box):
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")

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

View File

@@ -23,6 +23,9 @@ class LeftContainer(Gtk.Box):
def _setup_styling(self):
self.set_orientation(Gtk.Orientation.VERTICAL)
self.set_vexpand(True)
ctx = self.get_style_context()
ctx.add_class("left-container")

View File

@@ -24,6 +24,9 @@ class RightContainer(Gtk.Box):
def _setup_styling(self):
self.set_orientation(Gtk.Orientation.VERTICAL)
self.set_vexpand(True)
ctx = self.get_style_context()
ctx.add_class("right-container")

View File

@@ -17,6 +17,8 @@ from .bridge_controller import BridgeController
class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData):
""" docstring for BaseController. """
def __init__(self):
self._setup_controller_data()
@@ -52,8 +54,6 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerData):
args, unknownargs = settings_manager.get_starting_args()
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():

View File

@@ -37,14 +37,19 @@ class BaseControllerData:
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
@@ -80,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()
retcode = proc.wait()

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

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

@@ -93,13 +93,16 @@ class VteWidget(Vte.Terminal):
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.debud(e)
logger.debug(e)
return
if not command_ran[0:3].encode() in self.cd_cmd_prefix:
@@ -114,12 +117,12 @@ class VteWidget(Vte.Terminal):
event = Event("pty_path_updated", "", target_path)
event_system.emit("handle_bridge_event", (event,))
def update_term_path(self, fpath):
def update_term_path(self, fpath: str):
self.dont_process = True
cmds = [f"cd '{fpath}'\n", "clear\n"]
for i in cmds:
self.run_command(i)
for cmd in cmds:
self.run_command(cmd)
def run_command(self, cmd):
def run_command(self, cmd: str):
self.feed_child_binary(bytes(cmd, 'utf8'))

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
@@ -31,6 +36,9 @@ class Window(Gtk.ApplicationWindow):
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()
@@ -38,6 +46,7 @@ class Window(Gtk.ApplicationWindow):
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,11 +68,11 @@ 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):
@@ -74,6 +86,15 @@ class Window(Gtk.ApplicationWindow):
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

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

View File

@@ -91,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)}" )
@@ -155,6 +157,7 @@ class SettingsManager(StartCheckMixin, Singleton):
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

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

@@ -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,18 +1,37 @@
/* ---- Make most desired things base transparent ---- */
/* ---- Make most desired things base transparent ---- */
popover,
popover > box
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;
}