23 Commits

Author SHA1 Message Date
53b604e299 Merge pull request 'develop' (#14) from develop into master
Reviewed-on: #14
2025-12-28 04:37:28 +00:00
5e9354ba58 Updated README file 2025-12-27 22:34:16 -06:00
6a3bfbeb13 Fixed missing steam cdn url setting for thumbnailer plugin; fixed vod_thumbnailer plugin not expanding return values correctly 2025-12-27 16:36:13 -06:00
ad06ea50ad Commented out WIP memreaper import 2025-12-27 16:01:17 -06:00
9932f34782 Fixed backspaceing in path_entry causing cursor to jump to start of entry 2025-12-27 15:56:48 -06:00
76a4b2e0e2 Enhanced rename functionality to target selection bounds f present; WIP cbindings for memreaper effort 2025-12-27 15:26:43 -06:00
0942af249b Fixed IPC issues; cleaned up arg passthrough flow
Fixing rapid refresh when a lot of file activity happens
WIP memory reaper for Gtk Objects
2025-12-11 00:47:59 -06:00
28fefafa6f Fixed IPC issues; cleaned up arg passthrough flow 2025-12-10 19:27:06 -06:00
d96ace1504 Fixing vod thumbnailer; handling signal removal on tab close
+ more attempts at leak fixes; thumbnailer plugin updates and corrections
2025-11-24 17:14:57 -06:00
e0723e7b9e Updating file filters 2025-06-10 19:54:46 -05:00
2f47614eaf Merge pull request 'develop' (#13) from develop into master
Reviewed-on: #13
2023-11-12 06:51:23 +00:00
a5f864b802 Merge pull request 'develop' (#12) from develop into master
Reviewed-on: #12
2023-09-30 19:36:35 +00:00
5e9fe86cd6 Merge pull request 'develop' (#11) from develop into master
Reviewed-on: #11
2023-09-19 01:11:23 +00:00
da70244a54 Merge pull request 'Externalized items to new glade files and classes' (#10) from develop into master
Reviewed-on: #10
2022-12-04 08:49:44 +00:00
c01e81af27 Merge pull request 'Merge Stable Changesto Master' (#9) from develop into master
Reviewed-on: #9
2022-11-29 04:58:09 +00:00
41f39ba8cc Merge pull request 'Plugin rework/fixes' (#8) from develop into master
Reviewed-on: #8
2022-09-06 02:24:35 +00:00
bebe0c7cba Merge pull request 'plugin work' (#7) from develop into master
Reviewed-on: #7
2022-09-05 06:09:39 +00:00
74d53690e2 Merge pull request 'Updated VOD Thumbnailer, added icon gen logic' (#6) from develop into master
Reviewed-on: #6
2022-09-03 22:03:24 +00:00
061dbf19ad Merge pull request 'develop' (#5) from develop into master
Reviewed-on: #5
2022-09-03 05:46:08 +00:00
1798213bfc Merge pull request 'develop' (#4) from develop into master
Reviewed-on: #4
2022-09-02 03:25:44 +00:00
6bd4d97db2 Merge pull request 'Bringing to latest changes' (#3) from develop into master
Reviewed-on: #3
2022-07-16 19:14:29 +00:00
7737e3ad6d Merge pull request 'develop' (#2) from develop into master
Reviewed-on: #2
2022-01-31 02:34:18 +00:00
3c914e64dd Merge pull request 'develop' (#1) from develop into master
Reviewed-on: #1
2022-01-30 00:22:36 +00:00
22 changed files with 314 additions and 105 deletions

View File

@@ -2,9 +2,7 @@
SolarFM is a Gtk+ Python file manager. SolarFM is a Gtk+ Python file manager.
# Notes # Notes
<b>Still Work in progress! Use at own risk!</b> If not building a .deb then just move the contents of user_config to their respective folders.
Additionally, if not building a .deb then just move the contents of user_config to their respective folders.
Copy the share/solarfm folder to your user .config/ directory too. Copy the share/solarfm folder to your user .config/ directory too.
`pyrightconfig.json` `pyrightconfig.json`
@@ -17,7 +15,7 @@ sudo apt-get install xclip python3.8 python3-setproctitle python3-gi wget ffmpeg
# Known Issues # Known Issues
<ul> <ul>
<li>The memory leak has now been mostly mitigated in that it doesn't balloon to 2GB'. Still working out where the smaller one(s) is/are.</li> <li>There is a memory leak that has been slowed down but can get to 2GB over a long enough time period OR active accessing image based dirs.</li>
<li>Doing Ctrl+D when in Terminator (maybe other terminals too) somehow propagates the signal to SolarFM too. <li>Doing Ctrl+D when in Terminator (maybe other terminals too) somehow propagates the signal to SolarFM too.
A selected file in the active quad-pane will move to trash since it is the default key-binding for that action.</li> A selected file in the active quad-pane will move to trash since it is the default key-binding for that action.</li>
</ul> </ul>
@@ -25,7 +23,6 @@ A selected file in the active quad-pane will move to trash since it is the defau
# TODO # TODO
<ul> <ul>
<li>Add simpleish preview plugin for various file types.</li> <li>Add simpleish preview plugin for various file types.</li>
<li>Add simpleish bulk-renamer.</li>
</ul> </ul>
# Images # Images

View File

@@ -38,6 +38,7 @@ class IconController(Icon):
self.BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails" self.BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails"
self.ABS_THUMBS_PTH = f"{self.BASE_THUMBS_PTH}/normal" self.ABS_THUMBS_PTH = f"{self.BASE_THUMBS_PTH}/normal"
self.STEAM_ICONS_PTH = f"{self.BASE_THUMBS_PTH}/steam_icons" self.STEAM_ICONS_PTH = f"{self.BASE_THUMBS_PTH}/steam_icons"
self.STEAM_CDN_URL = ""
if not path.isdir(self.BASE_THUMBS_PTH): if not path.isdir(self.BASE_THUMBS_PTH):
os.mkdir(self.BASE_THUMBS_PTH) os.mkdir(self.BASE_THUMBS_PTH)
@@ -60,6 +61,7 @@ class IconController(Icon):
self.container_icon_wh = config["container_icon_wh"] self.container_icon_wh = config["container_icon_wh"]
self.video_icon_wh = config["video_icon_wh"] self.video_icon_wh = config["video_icon_wh"]
self.sys_icon_wh = config["sys_icon_wh"] self.sys_icon_wh = config["sys_icon_wh"]
self.STEAM_CDN_URL = config["steam_cdn_url"]
# Filters # Filters
filters = settings["filters"] filters = settings["filters"]

View File

@@ -14,7 +14,7 @@ class VideoIconMixin:
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", scrub_percent, "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_path]) proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", scrub_percent, "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_path])
proc.wait() proc.wait()
except Exception as e: except Exception as e:
logger.info(repr(e)) logger.info(e)
self.ffprobe_generate_video_thumbnail(full_path, hash_img_path) self.ffprobe_generate_video_thumbnail(full_path, hash_img_path)

View File

@@ -117,8 +117,8 @@ class Plugin(PluginBase):
uri = self._fm_state.uris[0] uri = self._fm_state.uris[0]
path = self._fm_state.tab.get_current_directory() path = self._fm_state.tab.get_current_directory()
parts = uri.split("/") parts = uri.split("/")
path_exists, path_exists, \
img_hash, img_hash, \
hash_img_pth = self._event_system.emit_and_await("get-thumbnail-hash", (uri,)) hash_img_pth = self._event_system.emit_and_await("get-thumbnail-hash", (uri,))
if not path_exists: if not path_exists:

View File

@@ -25,7 +25,6 @@ def limit_memory(maxsize):
def main(args, unknownargs): def main(args, unknownargs):
setproctitle(f'{app_name}') setproctitle(f'{app_name}')
# limit_memory(248 * (1024 * 1024 * 42))
if args.debug == "true": if args.debug == "true":
settings_manager.set_debug(True) settings_manager.set_debug(True)
@@ -34,7 +33,9 @@ def main(args, unknownargs):
settings_manager.set_trace_debug(True) settings_manager.set_trace_debug(True)
settings_manager.do_dirty_start_check() settings_manager.do_dirty_start_check()
Application(args, unknownargs)
app = Application()
app.run()
@@ -52,6 +53,7 @@ if __name__ == "__main__":
# Read arguments (If any...) # Read arguments (If any...)
args, unknownargs = parser.parse_known_args() args, unknownargs = parser.parse_known_args()
settings_manager.set_starting_args( args, unknownargs )
try: try:
faulthandler.enable() # For better debug info faulthandler.enable() # For better debug info

View File

@@ -19,40 +19,48 @@ class AppLaunchException(Exception):
class Application: class Application:
""" docstring for Application. """ """ docstring for Application. """
def __init__(self, args, unknownargs): def __init__(self):
super(Application, self).__init__() super(Application, self).__init__()
if not settings_manager.is_trace_debug():
self.load_ipc(args, unknownargs)
self.setup_debug_hook() self.setup_debug_hook()
Window(args, unknownargs).main()
def load_ipc(self, args, unknownargs): def run(self):
ipc_server = IPCServer() 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) self.ipc_realization_check(ipc_server)
if ipc_server.is_ipc_alive:
return True
if not ipc_server.is_ipc_alive: logger.warning(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...")
for arg in unknownargs + [args.new_tab,]: for arg in unknownargs + [args.new_tab,]:
if os.path.isfile(arg): if os.path.isfile(arg):
message = f"FILE|{arg}" message = f"FILE|{arg}"
ipc_server.send_ipc_message(message) ipc_server.send_ipc_message(message)
if os.path.isdir(arg):
message = f"DIR|{arg}"
ipc_server.send_ipc_message(message)
return False
raise AppLaunchException(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...")
def ipc_realization_check(self, ipc_server): def ipc_realization_check(self, ipc_server):
try: try:
ipc_server.create_ipc_listener() ipc_server.create_ipc_listener()
except Exception as e: except Exception as e:
print(e)
ipc_server.send_test_ipc_message() ipc_server.send_test_ipc_message()
try:
ipc_server.create_ipc_listener()
except Exception as e:
...
def setup_debug_hook(self): def setup_debug_hook(self):
try: try:
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows # kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows

View File

@@ -36,26 +36,14 @@ from .ui_mixin import UIMixin
class Controller(UIMixin, SignalsMixins, Controller_Data): class Controller(UIMixin, SignalsMixins, Controller_Data):
""" Controller coordinates the mixins and is somewhat the root hub of it all. """ """ Controller coordinates the mixins and is somewhat the root hub of it all. """
def __init__(self, args, unknownargs): def __init__(self):
self._setup_controller_data() self._setup_controller_data()
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
self._subscribe_to_events() self._subscribe_to_events()
self._load_widgets() self._load_widgets()
self._load_plugins_and_files()
if args.no_plugins == "false":
self.plugins_controller.pre_launch_plugins()
self._generate_file_views(self.fm_controller_data)
if args.no_plugins == "false":
self.plugins_controller.post_launch_plugins()
for arg in unknownargs + [args.new_tab,]:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.emit("post_file_to_ipc", message)
def _setup_styling(self): def _setup_styling(self):
@@ -69,7 +57,7 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("shutting_down", self._shutting_down) event_system.subscribe("shutting_down", self._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("generate_file_views", self._generate_file_views) event_system.subscribe("generate_file_views", self._generate_file_views)
event_system.subscribe("clear_notebooks", self.clear_notebooks) event_system.subscribe("clear_notebooks", self.clear_notebooks)
@@ -113,6 +101,22 @@ class Controller(UIMixin, SignalsMixins, Controller_Data):
FileExistsWidget() FileExistsWidget()
SaveLoadWidget() SaveLoadWidget()
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._generate_file_views(self.fm_controller_data)
if args.no_plugins == "false":
self.plugins_controller.post_launch_plugins()
for arg in unknownargs + [args.new_tab,]:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.emit("post_file_to_ipc", message)
def _shutting_down(self): def _shutting_down(self):
if not settings_manager.is_trace_debug(): if not settings_manager.is_trace_debug():
self.fm_controller.save_state() self.fm_controller.save_state()

View File

@@ -47,13 +47,26 @@ class FileActionSignalsMixin:
if tab_widget_id in self.soft_update_lock: if tab_widget_id in self.soft_update_lock:
timeout_id = self.soft_update_lock[tab_widget_id]["timeout_id"] timeout_id = self.soft_update_lock[tab_widget_id]["timeout_id"]
GLib.source_remove(timeout_id) GLib.source_remove(timeout_id)
self.soft_update_lock[tab_widget_id]["call_count"] += 1
else:
self.soft_update_lock[tab_widget_id] = {}
self.soft_update_lock[tab_widget_id]["call_count"] = 0
timeout_id = GLib.timeout_add(0, self.update_on_soft_lock_end, 600, *(tab_widget_id,)) timeout_id = GLib.timeout_add(
500 if self.soft_update_lock[tab_widget_id]["call_count"] <= 5 else 1000,
self.update_on_soft_lock_end,
tab_widget_id
)
self.soft_update_lock[tab_widget_id]["timeout_id"] = timeout_id
def update_on_soft_lock_end(self, timout_ms, tab_widget_id): def update_on_soft_lock_end(self, tab_widget_id):
GLib.idle_add(self._update_interface, tab_widget_id)
self.soft_update_lock.pop(tab_widget_id, None) self.soft_update_lock.pop(tab_widget_id, None)
return False
def _update_interface(self, tab_widget_id):
wid, tid = tab_widget_id.split("|") wid, tid = tab_widget_id.split("|")
notebook = self.builder.get_object(f"window_{wid}") notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid) tab = self.get_fm_window(wid).get_tab_by_id(tid)
@@ -70,8 +83,6 @@ class FileActionSignalsMixin:
if [wid, tid] in [state.wid, state.tid]: if [wid, tid] in [state.wid, state.tid]:
self.set_bottom_labels(tab) self.set_bottom_labels(tab)
return False
def do_file_search(self, widget, eve = None): def do_file_search(self, widget, eve = None):
if not self.ctrl_down and not self.shift_down and not self.alt_down: if not self.ctrl_down and not self.shift_down and not self.alt_down:
target = widget.get_name() target = widget.get_name()

View File

@@ -13,7 +13,7 @@ class IPCSignalsMixin:
def print_to_console(self, message=None): def print_to_console(self, message=None):
print(message) print(message)
def handle_file_from_ipc(self, path): def handle_dir_from_ipc(self, path):
window = self.builder.get_object("main_window") window = self.builder.get_object("main_window")
window.deiconify() window.deiconify()
window.show() window.show()

View File

@@ -64,16 +64,56 @@ class RenameWidget:
def set_to_title_case(self, widget, eve=None): def set_to_title_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().title() ) bounds = self._rename_fname.get_selection_bounds()
fname = self._rename_fname.get_text()
if bounds:
start, end = bounds
replace = fname[start:end]
fname = fname.replace(replace, replace.title())
else:
fname = fname.title()
self._rename_fname.set_text(fname)
def set_to_upper_case(self, widget, eve=None): def set_to_upper_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().upper() ) bounds = self._rename_fname.get_selection_bounds()
fname = self._rename_fname.get_text()
if bounds:
start, end = bounds
replace = fname[start:end]
fname = fname.replace(replace, replace.upper())
else:
fname = fname.upper()
self._rename_fname.set_text(fname)
def set_to_lower_case(self, widget, eve=None): def set_to_lower_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().lower() ) bounds = self._rename_fname.get_selection_bounds()
fname = self._rename_fname.get_text()
if bounds:
start, end = bounds
replace = fname[start:end]
fname = fname.replace(replace, replace.lower())
else:
fname = fname.lower()
self._rename_fname.set_text(fname)
def set_to_invert_case(self, widget, eve=None): def set_to_invert_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().swapcase() ) bounds = self._rename_fname.get_selection_bounds()
fname = self._rename_fname.get_text()
if bounds:
start, end = bounds
replace = fname[start:end]
fname = fname.replace(replace, replace.swapcase())
else:
fname = fname.swapcase()
self._rename_fname.set_text(fname)
def hide_rename_file_menu(self, widget=None, eve=None): def hide_rename_file_menu(self, widget=None, eve=None):
self._rename_file_menu.hide() self._rename_file_menu.hide()

View File

@@ -100,13 +100,15 @@ class TabMixin(GridMixin):
icon_grid.unparent() icon_grid.unparent()
scroll.unparent() scroll.unparent()
Gtk.main_iteration_do(False)
gc.collect()
logger.debug(f"Reference count for tab_box is: {tab_box.__grefcount__}") logger.debug(f"Reference count for tab_box is: {tab_box.__grefcount__}")
logger.debug(f"Reference count for icon_grid is: {icon_grid.__grefcount__}") logger.debug(f"Reference count for icon_grid is: {icon_grid.__grefcount__}")
logger.debug(f"Reference count for scroll is: {scroll.__grefcount__}") logger.debug(f"Reference count for scroll is: {scroll.__grefcount__}")
del tab_box
del icon_grid
del scroll
gc.collect()
if not settings_manager.is_trace_debug(): if not settings_manager.is_trace_debug():
self.fm_controller.save_state() self.fm_controller.save_state()

View File

@@ -55,6 +55,7 @@ class WindowMixin(TabMixin):
path_entry = self.builder.get_object("path_entry") path_entry = self.builder.get_object("path_entry")
tab = self.get_fm_window(wid).get_tab_by_id(tid) tab = self.get_fm_window(wid).get_tab_by_id(tid)
path_entry.set_text(tab.get_current_directory()) path_entry.set_text(tab.get_current_directory())
path_entry.set_position(-1)
path_entry = None path_entry = None
tab = None tab = None

View File

@@ -9,8 +9,8 @@ from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
# Application imports # Application imports
# from utils.cbindings import gtkmemreaper
@@ -48,19 +48,23 @@ class IconGridWidget(Gtk.IconView):
def _setup_signals(self): def _setup_signals(self):
... ...
def _setup_additional_signals(self, grid_icon_single_click, def _setup_additional_signals(self,
grid_icon_double_click, grid_icon_single_click,
grid_set_selected_items, grid_icon_double_click,
grid_on_drag_set, grid_set_selected_items,
grid_on_drag_data_received, grid_on_drag_set,
grid_on_drag_motion): grid_on_drag_data_received,
grid_on_drag_motion
):
self._handler_ids.append(self.connect("button_release_event", grid_icon_single_click)) self._handler_ids = [
self._handler_ids.append(self.connect("item-activated", grid_icon_double_click)) self.connect("button_release_event", grid_icon_single_click),
self._handler_ids.append(self.connect("selection-changed", grid_set_selected_items)) self.connect("item-activated", grid_icon_double_click),
self._handler_ids.append(self.connect("drag-data-get", grid_on_drag_set)) self.connect("selection-changed", grid_set_selected_items),
self._handler_ids.append(self.connect("drag-data-received", grid_on_drag_data_received)) self.connect("drag-data-get", grid_on_drag_set),
self._handler_ids.append(self.connect("drag-motion", grid_on_drag_motion)) self.connect("drag-data-received", grid_on_drag_data_received),
self.connect("drag-motion", grid_on_drag_motion)
]
def _load_widgets(self): def _load_widgets(self):
self.clear_and_set_new_store() self.clear_and_set_new_store()
@@ -113,10 +117,14 @@ class IconGridWidget(Gtk.IconView):
if icon: if icon:
logger.debug(f"Reference count for icon is: {icon.__grefcount__}") logger.debug(f"Reference count for icon is: {icon.__grefcount__}")
icon.run_dispose() icon.run_dispose()
# icon_ptr = int(hash(icon)) # WARNING: not stable across runs
# gtkmemreaper.free_pixbuf(icon_ptr)
del icon del icon
store.clear() store.clear()
store.run_dispose() store.run_dispose()
# store_ptr = int(hash(store)) # WARNING: not stable across runs
# gtkmemreaper.free_list_store(store)
del store del store
gc.collect() gc.collect()

View File

@@ -22,9 +22,8 @@ class ControllerStartException(Exception):
class Window(Gtk.ApplicationWindow): class Window(Gtk.ApplicationWindow):
"""docstring for Window.""" """docstring for Window."""
def __init__(self, args, unknownargs): def __init__(self):
Gtk.ApplicationWindow.__init__(self) Gtk.ApplicationWindow.__init__(self)
# super(Window, self).__init__()
settings_manager.set_main_window(self) settings_manager.set_main_window(self)
@@ -33,7 +32,7 @@ class Window(Gtk.ApplicationWindow):
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
self._subscribe_to_events() self._subscribe_to_events()
self._load_widgets(args, unknownargs) self._load_widgets()
self._set_window_data() self._set_window_data()
self._set_size_constraints() self._set_size_constraints()
@@ -56,11 +55,11 @@ class Window(Gtk.ApplicationWindow):
event_system.subscribe("tear_down", self._tear_down) event_system.subscribe("tear_down", self._tear_down)
event_system.subscribe("load_interactive_debug", self._load_interactive_debug) 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(): if settings_manager.is_debug():
self.set_interactive_debugging(True) self.set_interactive_debugging(True)
self._controller = Controller(args, unknownargs) self._controller = Controller()
if not self._controller: if not self._controller:
raise ControllerStartException("Controller exited and doesn't exist...") raise ControllerStartException("Controller exited and doesn't exist...")
@@ -120,5 +119,5 @@ class Window(Gtk.ApplicationWindow):
settings_manager.clear_pid() settings_manager.clear_pid()
Gtk.main_quit() Gtk.main_quit()
def main(self): def start(self):
Gtk.main() Gtk.main()

View File

View File

@@ -0,0 +1,20 @@
# Python imports
import pkg_resources
import importlib.util
# Lib imports
# Application imports
def __bootstrap__():
__file__ = pkg_resources.resource_filename(__name__, 'gtkmemreaper.cpython-313-x86_64-linux-gnu.so')
spec = importlib.util.spec_from_file_location(
__name__,
__file__
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
__bootstrap__()

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# . CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
cd "$(dirname "")"
echo "Working Dir: " $(pwd)
python3 setup.py build && python3 setup.py install --user
}
main "$@";

View File

@@ -0,0 +1,62 @@
#include <Python.h>
#include <gtk/gtk.h>
#include <cairo.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <stdlib.h>
// static PyObject* free_pixbuf(PyObject* self, PyObject* args) {
static PyObject* free_pixbuf(PyObject* self, PyObject* args) {
PyObject *py_pixbuf;
if (!PyArg_ParseTuple(args, "O", &py_pixbuf)) {
Py_RETURN_NONE;
}
GdkPixbuf *pixbuf = (GdkPixbuf *) PyLong_AsVoidPtr(py_pixbuf);
if (!GDK_IS_PIXBUF(pixbuf)) {
PyErr_SetString(PyExc_TypeError, "Invalid GdkPixbuf pointer.");
Py_RETURN_NONE;
}
g_object_unref(pixbuf);
g_assert_null(pixbuf);
Py_RETURN_NONE;
}
static PyObject* free_list_store(PyObject* self, PyObject* args) {
PyObject *py_list_store;
if (!PyArg_ParseTuple(args, "O", &py_list_store)) {
Py_RETURN_NONE;
}
GtkListStore *list_store = (GtkListStore *) PyLong_AsVoidPtr(py_list_store);
if (!GTK_IS_LIST_STORE(list_store)) {
PyErr_SetString(PyExc_TypeError, "Invalid Gtk.ListStore pointer.");
Py_RETURN_NONE;
}
g_object_unref(list_store);
g_assert_null(list_store);
Py_RETURN_NONE;
}
static PyMethodDef Methods[] = {
{"free_pixbuf", free_pixbuf, METH_VARARGS, "Clear GdkPixbuf* ."},
{"free_list_store", free_list_store, METH_VARARGS, "Clear GtkListStore* ."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"gtkmemreaper",
NULL,
-1,
Methods
};
PyMODINIT_FUNC PyInit_gtkmemreaper(void) {
return PyModule_Create(&moduledef);
}

View File

@@ -0,0 +1,28 @@
# Python imports
from setuptools import setup, Extension
from subprocess import check_output
# Lib imports
# Application imports
pkg_config_args = ["gdk-pixbuf-2.0", "cairo", "gtk+-3.0"]
def get_pkgconfig_flags(flag_type):
return check_output(["pkg-config", flag_type] + pkg_config_args).decode().split()
ext = Extension(
"gtkmemreaper",
sources = ["gtkmemreaper.c"],
include_dirs = [],
extra_compile_args = get_pkgconfig_flags("--cflags"),
extra_link_args = get_pkgconfig_flags("--libs")
)
setup(
name = "gtkmemreaper",
version = "0.1",
ext_modules = [ext]
)

View File

@@ -5,7 +5,6 @@ from multiprocessing.connection import Client
from multiprocessing.connection import Listener from multiprocessing.connection import Listener
# Lib imports # Lib imports
from gi.repository import GLib
# Application imports # Application imports
from .singleton import Singleton from .singleton import Singleton
@@ -14,7 +13,7 @@ from .singleton import Singleton
class IPCServer(Singleton): class IPCServer(Singleton):
""" Create a listener so that other SolarFM instances send requests back to existing instance. """ """ Create a listener so that other SolarFM instances send requests back to existing instance. """
def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "local_network_unsecured"): def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"):
self.is_ipc_alive = False self.is_ipc_alive = False
self._ipc_port = 0 # Use 0 to let Listener chose port self._ipc_port = 0 # Use 0 to let Listener chose port
self._ipc_address = ipc_address self._ipc_address = ipc_address
@@ -43,30 +42,29 @@ class IPCServer(Singleton):
if os.path.exists(self._ipc_address) and settings_manager.is_dirty_start(): if os.path.exists(self._ipc_address) and settings_manager.is_dirty_start():
os.unlink(self._ipc_address) os.unlink(self._ipc_address)
listener = Listener(address = self._ipc_address, family = "AF_UNIX", authkey = self._ipc_authkey) listener = Listener(self._ipc_address, family = "AF_UNIX", authkey = self._ipc_authkey)
elif "unsecured" not in self._conn_type: elif "unsecured" not in self._conn_type:
listener = Listener((self._ipc_address, self._ipc_port), authkey = self._ipc_authkey) listener = Listener((self._ipc_address, self._ipc_port), authkey = self._ipc_authkey)
else: else:
listener = Listener((self._ipc_address, self._ipc_port)) listener = Listener((self._ipc_address, self._ipc_port))
self.is_ipc_alive = True self.is_ipc_alive = True
# self._run_ipc_loop(listener) self._run_ipc_loop(listener)
GLib.Thread("", self._run_ipc_loop, listener)
# @daemon_threaded @daemon_threaded
def _run_ipc_loop(self, listener) -> None: def _run_ipc_loop(self, listener) -> None:
while True: while True:
try: try:
conn = listener.accept() conn = listener.accept()
start_time = time.perf_counter() start_time = time.perf_counter()
GLib.idle_add(self._handle_ipc_message, *(conn, start_time,)) self._handle_ipc_message(conn, start_time)
except EOFError as e:
conn = None logger.debug( repr(e) )
start_time = None
except Exception as e: except Exception as e:
logger.debug( repr(e) ) logger.debug( repr(e) )
finally:
conn.close()
listener.close() listener.close()
@@ -80,27 +78,28 @@ class IPCServer(Singleton):
if file: if file:
event_system.emit_and_await("handle_file_from_ipc", file) event_system.emit_and_await("handle_file_from_ipc", file)
msg = None conn.close()
file = None break
if "DIR|" in msg:
file = msg.split("DIR|")[1].strip()
if file:
event_system.emit_and_await("handle_dir_from_ipc", file)
conn.close() conn.close()
break break
if msg in ['close connection', 'close server']: if msg in ['close connection', 'close server', 'Empty Data...']:
msg = None
conn.close() conn.close()
break break
# NOTE: Not perfect but insures we don't lock up the connection for too long. # NOTE: Not perfect but insures we don't lock up the connection for too long.
end_time = time.perf_counter() end_time = time.perf_counter()
if (end_time - start_time) > self._ipc_timeout: if (end_time - start_time) > self._ipc_timeout:
msg = None
end_time = None
conn.close() conn.close()
break break
return False
def send_ipc_message(self, message: str = "Empty Data...") -> None: def send_ipc_message(self, message: str = "Empty Data...") -> None:
try: try:

View File

@@ -87,17 +87,18 @@ class SettingsManager(StartCheckMixin, Singleton):
print( f"Settings: {self._CONTEXT_MENU}\n\t\t{repr(e)}" ) print( f"Settings: {self._CONTEXT_MENU}\n\t\t{repr(e)}" )
self.settings: Settings = None self.settings: Settings = None
self._main_window = None self._main_window = None
self._main_window_w = 1670 self._main_window_w = 1670
self._main_window_h = 830 self._main_window_h = 830
self._builder = None self._builder = None
self.PAINT_BG_COLOR = (0, 0, 0, 0.0) self.PAINT_BG_COLOR = (0, 0, 0, 0.0)
self._trace_debug = False
self._debug = False
self._dirty_start = False
self._trace_debug = False
self._debug = False
self._dirty_start = False
self._passed_in_file: bool = False
self._starting_files: list = []
def register_signals_to_builder(self, classes=None, builder=None): def register_signals_to_builder(self, classes=None, builder=None):
handlers = {} handlers = {}
@@ -143,6 +144,12 @@ class SettingsManager(StartCheckMixin, Singleton):
def get_trash_info_path(self) -> str: return self._TRASH_INFO_PATH def get_trash_info_path(self) -> str: return self._TRASH_INFO_PATH
def get_plugins_path(self) -> str: return self._PLUGINS_PATH def get_plugins_path(self) -> str: return self._PLUGINS_PATH
def get_starting_args(self):
return self.args, self.unknownargs
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) -> bool: return self._trace_debug def is_trace_debug(self) -> bool: return self._trace_debug
def is_debug(self) -> bool: return self._debug def is_debug(self) -> bool: return self._debug
@@ -153,6 +160,10 @@ class SettingsManager(StartCheckMixin, Singleton):
def set_main_window_min_width(self, width = 720): self.settings.config.main_window_min_width = width 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_main_window_min_height(self, height = 480): self.settings.config.main_window_min_height = height
def set_starting_args(self, args, unknownargs):
self.args = args
self.unknownargs = unknownargs
def set_trace_debug(self, trace_debug: bool): def set_trace_debug(self, trace_debug: bool):
self._trace_debug = trace_debug self._trace_debug = trace_debug

View File

@@ -29,7 +29,7 @@ class StartCheckMixin:
pid = f.readline().strip() pid = f.readline().strip()
if pid not in ("", None): if pid not in ("", None):
if self.is_pid_alive( int(pid) ): if self.is_pid_alive( int(pid) ):
print("PID file exists and PID is alive... Letting downstream errors (sans debug args) handle app closure propigation.") print("PID file exists and PID is alive...")
return return
self._write_new_pid() self._write_new_pid()