Restructuring src folder

This commit is contained in:
2023-11-12 00:49:16 -06:00
parent 39959dc947
commit 2de4de6b22
234 changed files with 1275 additions and 883 deletions

View File

@@ -0,0 +1,64 @@
# Python imports
import builtins
import threading
import sys
# Lib imports
# Application imports
from utils.event_system import EventSystem
from utils.keybindings import Keybindings
from utils.logger import Logger
from utils.settings_manager.manager import SettingsManager
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded_wrapper(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
return wrapper
# NOTE: Threads WILL die with parent's destruction.
def daemon_threaded_wrapper(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
def sizeof_fmt_def(num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return f"{num:3.1f} {unit}{suffix}"
num /= 1024.0
return f"{num:.1f} Yi{suffix}"
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "SolarFM"
builtins.keybindings = Keybindings()
builtins.event_system = EventSystem()
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.threaded = threaded_wrapper
builtins.daemon_threaded = daemon_threaded_wrapper
builtins.sizeof_fmt = sizeof_fmt_def
def custom_except_hook(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
sys.excepthook = custom_except_hook

3
src/solarfm/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Base module
"""

56
src/solarfm/__main__.py Normal file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/python3
# Python imports
import argparse
import faulthandler
import locale
import traceback
from setproctitle import setproctitle
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from __builtins__ import *
from app import Application
def run():
try:
locale.setlocale(locale.LC_NUMERIC, 'C')
setproctitle(f"{app_name}")
faulthandler.enable() # For better debug info
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("--new-tab", "-t", default="", help="Open a file into new tab.")
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
# Read arguments (If any...)
args, unknownargs = parser.parse_known_args()
if args.debug == "true":
settings_manager.set_debug(True)
if args.trace_debug == "true":
settings_manager.set_trace_debug(True)
settings_manager.do_dirty_start_check()
Application(args, unknownargs)
Gtk.main()
except Exception as e:
traceback.print_exc()
quit()
if __name__ == "__main__":
""" Set process title, get arguments, and create GTK main thread. """
run()

59
src/solarfm/app.py Normal file
View File

@@ -0,0 +1,59 @@
# Python imports
import signal
import os
# Lib imports
# Application imports
from utils.debugging import debug_signal_handler
from utils.ipc_server import IPCServer
from core.window import Window
class AppLaunchException(Exception):
...
class Application(IPCServer):
""" docstring for Application. """
def __init__(self, args, unknownargs):
super(Application, self).__init__()
if not settings_manager.is_trace_debug():
self.socket_realization_check()
if not self.is_ipc_alive:
for arg in unknownargs + [args.new_tab,]:
if os.path.isdir(arg):
message = f"FILE|{arg}"
self.send_ipc_message(message)
raise AppLaunchException(f"{app_name} IPC Server Exists: Will send path(s) to it and close...")
self.setup_debug_hook()
Window(args, unknownargs)
def socket_realization_check(self):
try:
self.create_ipc_listener()
except Exception:
self.send_test_ipc_message()
try:
self.create_ipc_listener()
except Exception as e:
...
def setup_debug_hook(self):
try:
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows
signal.signal(
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"),
debug_signal_handler
)
except ValueError:
# Typically: ValueError: signal only works in main thread
...

View File

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

View File

@@ -0,0 +1,195 @@
# Python imports
import os
import time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from .controller_data import Controller_Data
from .fs_actions.file_system_actions import FileSystemActions
from .mixins.signals_mixins import SignalsMixins
from .widgets.dialogs.about_widget import AboutWidget
from .widgets.dialogs.appchooser_widget import AppchooserWidget
from .widgets.dialogs.file_exists_widget import FileExistsWidget
from .widgets.dialogs.new_file_widget import NewFileWidget
from .widgets.dialogs.rename_widget import RenameWidget
from .widgets.dialogs.save_load_widget import SaveLoadWidget
from .widgets.popups.message_popup_widget import MessagePopupWidget
from .widgets.popups.path_menu_popup_widget import PathMenuPopupWidget
from .widgets.popups.plugins_popup_widget import PluginsPopupWidget
from .widgets.popups.io_popup_widget import IOPopupWidget
from .widgets.context_menu_widget import ContextMenuWidget
from .widgets.bottom_status_info_widget import BottomStatusInfoWidget
from .ui_mixin import UIMixin
class Controller(UIMixin, SignalsMixins, Controller_Data):
""" Controller coordinates the mixins and is somewhat the root hub of it all. """
def __init__(self, args, unknownargs):
self._setup_controller_data()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._generate_file_views(self.fm_controller_data)
if args.no_plugins == "false":
self.plugins.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_signals(self):
self.window.connect("focus-out-event", self.unset_keys_and_data)
self.window.connect("key-press-event", self.on_global_key_press_controller)
self.window.connect("key-release-event", self.on_global_key_release_controller)
FileSystemActions()
def _subscribe_to_events(self):
event_system.subscribe("shutting_down", self._shutting_down)
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
event_system.subscribe("generate_file_views", self._generate_file_views)
event_system.subscribe("clear_notebooks", self.clear_notebooks)
event_system.subscribe("set_window_title", self._set_window_title)
event_system.subscribe("unset_selected_files_views", self._unset_selected_files_views)
event_system.subscribe("get_current_state", self.get_current_state)
event_system.subscribe("go_to_path", self.go_to_path)
event_system.subscribe("format_to_uris", self.format_to_uris)
event_system.subscribe("do_action_from_menu_controls", self.do_action_from_menu_controls)
event_system.subscribe("set_clipboard_data", self.set_clipboard_data)
def _load_glade_file(self):
self.builder.add_from_file( settings_manager.get_glade_file() )
self.builder.expose_object("main_window", self.window)
self.core_widget = self.builder.get_object("core_widget")
settings_manager.set_builder(self.builder)
settings_manager.register_signals_to_builder([self,], self.builder)
def get_core_widget(self):
return self.core_widget
# NOTE: Really we will move these to the UI/(New) Window 'base' controller
# after we're done cleaning and refactoring to use fewer mixins.
def _load_widgets(self):
BottomStatusInfoWidget()
IOPopupWidget()
MessagePopupWidget()
PathMenuPopupWidget()
PluginsPopupWidget()
AboutWidget()
AppchooserWidget()
ContextMenuWidget()
NewFileWidget()
RenameWidget()
FileExistsWidget()
SaveLoadWidget()
def _shutting_down(self):
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def reload_plugins(self, widget=None, eve=None):
self.plugins.reload_plugins()
def do_action_from_menu_controls(self, _action=None, eve=None):
if not _action: return
if not isinstance(_action, str):
action = _action.get_name()
else:
action = _action
event_system.emit("hide_context_menu")
event_system.emit("hide_new_file_menu")
event_system.emit("hide_rename_file_menu")
if action == "open":
event_system.emit("open_files")
if action == "open_with":
event_system.emit("show_appchooser_menu")
if action == "execute":
event_system.emit("execute_files")
if action == "execute_in_terminal":
event_system.emit("execute_files", (True,))
if action == "rename":
event_system.emit("rename_files")
if action == "cut":
event_system.emit("cut_files")
if action == "copy":
event_system.emit("copy_files")
if action == "copy_path":
event_system.emit("copy_path")
if action == "copy_name":
event_system.emit("copy_name")
if action == "copy_path_name":
event_system.emit("copy_path_name")
if action == "paste":
event_system.emit("paste_files")
if action == "create":
event_system.emit("create_files")
if action in ["save_session", "save_session_as", "load_session"]:
event_system.emit("save_load_session", (action))
if action == "about_page":
event_system.emit("show_about_page")
if action == "io_popup":
event_system.emit("show_io_popup")
if action == "plugins_popup":
event_system.emit("show_plugins_popup")
if action == "messages_popup":
event_system.emit("show_messages_popup")
if action == "ui_debug":
event_system.emit("load_interactive_debug")
if action == "tear_down":
event_system.emit("tear_down")
def go_home(self, widget=None, eve=None):
self.builder.get_object("go_home").released()
def refresh_tab(self, widget=None, eve=None):
self.builder.get_object("refresh_tab").released()
def go_up(self, widget=None, eve=None):
self.builder.get_object("go_up").released()
def grab_focus_path_entry(self, widget=None, eve=None):
self.builder.get_object("path_entry").grab_focus()
def tggl_top_main_menubar(self, widget=None, eve=None):
top_main_menubar = self.builder.get_object("top_main_menubar")
top_main_menubar.hide() if top_main_menubar.is_visible() else top_main_menubar.show()
def open_terminal(self, widget=None, eve=None):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.execute([f"{tab.terminal_app}"], start_dir=tab.get_current_directory())
def go_to_path(self, path: str):
self.builder.get_object("path_entry").set_text(path)

View File

@@ -0,0 +1,189 @@
# Python imports
import os
import subprocess
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .sfm_builder import SFMBuilder
from .widgets.dialogs.message_widget import MessageWidget
from .widgets.dialogs.user_pass_widget import UserPassWidget
from utils.types.state import State
from plugins.plugins_controller import PluginsController
from shellfm.windows.controller import WindowController
class Controller_Data:
""" Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """
__slots__ = "settings", "builder", "logger", "keybindings", "trashman", "fm_controller", "window", "window1", "window2", "window3", "window4"
def _setup_controller_data(self) -> None:
self.window = settings_manager.get_main_window()
self.builder = SFMBuilder()
self.core_widget = None
self._load_glade_file()
self.fm_controller = WindowController()
self.plugins = PluginsController()
self.fm_controller_data = self.fm_controller.get_state_from_file()
self.window1 = self.builder.get_object("window_1")
self.window2 = self.builder.get_object("window_2")
self.window3 = self.builder.get_object("window_3")
self.window4 = self.builder.get_object("window_4")
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
self.selected_files = []
self.to_copy_files = []
self.to_cut_files = []
self.soft_update_lock = {}
self.dnd_left_primed = 0
self.single_click_open = False
self.is_pane1_hidden = False
self.is_pane2_hidden = False
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.override_drop_dest = None
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
self._state = State()
self.message_dialog = MessageWidget()
self.user_pass_dialog = UserPassWidget()
def get_current_state(self) -> State:
'''
Returns the state info most useful for any given context and action intent.
Parameters:
a (obj): self
Returns:
state (obj): State
'''
# state = State()
state = self._state
state.fm_controller = self.fm_controller
state.notebooks = self.notebooks
state.wid, state.tid = self.fm_controller.get_active_wid_and_tid()
state.tab = self.get_fm_window(state.wid).get_tab_by_id(state.tid)
state.icon_grid = self.builder.get_object(f"{state.wid}|{state.tid}|icon_grid", use_gtk = False)
# state.icon_grid = event_system.emit_and_await("get_files_view_icon_grid", (state.wid, state.tid))
state.store = state.icon_grid.get_model()
# NOTE: Need to watch this as I thought we had issues with just using single reference upon closing it.
# But, I found that not doing it this way caused objects to generate upon every click... (Because we're getting state info, duh)
# Yet interactive debug view shows them just pilling on and never clearing...
state.message_dialog = self.message_dialog
state.user_pass_dialog = self.user_pass_dialog
# state.message_dialog = MessageWidget()
# state.user_pass_dialog = UserPassWidget()
selected_files = state.icon_grid.get_selected_items()
if selected_files:
state.uris = self.format_to_uris(state.store, state.wid, state.tid, selected_files, True)
state.uris_raw = self.format_to_uris(state.store, state.wid, state.tid, selected_files)
state.selected_files = event_system.emit_and_await("get_selected_files")
# if self.to_copy_files:
# state.to_copy_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_copy_files, True)
#
# if self.to_cut_files:
# state.to_cut_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_cut_files, True)
event_system.emit("update_state_info_plugins", state) # NOTE: Need to remove after we convert plugins to use emit_and_await
return state
def format_to_uris(self, store, wid, tid, treePaths, use_just_path = False):
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
uris = []
for path in treePaths:
itr = store.get_iter(path)
file = store.get(itr, 1)[0]
fpath = ""
if not use_just_path:
fpath = f"file://{dir}/{file}"
else:
fpath = f"{dir}/{file}"
uris.append(fpath)
return uris
def get_fm_window(self, wid):
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
def _unset_selected_files_views(self):
for _notebook in self.notebooks:
ctx = _notebook.get_style_context()
ctx.remove_class("notebook-selected-focus")
ctx.add_class("notebook-unselected-focus")
def _set_window_title(self, dir):
self.window.set_title(f"{app_name} ~ {dir}")
def clear_console(self) -> None:
''' Clears the terminal screen. '''
os.system('cls' if os.name == 'nt' else 'clear')
def call_method(self, _method_name: str, data: type = None) -> type:
'''
Calls a method from scope of class.
Parameters:
a (obj): self
b (str): method name to be called
c (*): Data (if any) to be passed to the method.
Note: It must be structured according to the given methods requirements.
Returns:
Return data is that which the calling method gives.
'''
method_name = str(_method_name)
method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}")
return method(data) if data else method()
def has_method(self, obj, name) -> type:
''' Checks if a given method exists. '''
return callable(getattr(obj, name, None))
def clear_notebooks(self) -> None:
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
for notebook in self.notebooks:
self.clear_children(notebook)
def clear_children(self, widget: type) -> None:
''' Clear children of a gtk widget. '''
for child in widget.get_children():
widget.remove(child)
def get_clipboard_data(self) -> str:
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
retcode = proc.wait()
data = proc.stdout.read()
return data.decode("utf-8").strip()
def set_clipboard_data(self, data: type) -> None:
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
proc.stdin.write(data.encode("utf-8"))
proc.stdin.close()
retcode = proc.wait()

View File

@@ -0,0 +1,3 @@
"""
FS Actions Module
"""

View File

@@ -0,0 +1,70 @@
# Python imports
# Lib imports
# Application imports
class CRUDMixin:
"""docstring for CRUDMixin"""
def move_files(self, files, target):
self.handle_files(files, "move", target)
def paste_files(self):
state = event_system.emit_and_await("get_current_state")
target = f"{state.tab.get_current_directory()}"
if self._to_copy_files:
self.handle_files(self._to_copy_files, "copy", target)
elif self._to_cut_files:
self.handle_files(self._to_cut_files, "move", target)
def create_files(self):
fname_field = self._builder.get_object("new_fname_field")
cancel_creation = event_system.emit_and_await("show_new_file_menu", fname_field)
if cancel_creation:
event_system.emit("hide_new_file_menu")
return
file_name = fname_field.get_text().strip()
type = self._builder.get_object("new_file_toggle_type").get_state()
state = event_system.emit_and_await("get_current_state")
target = f"{state.tab.get_current_directory()}"
if file_name:
path = f"{target}/{file_name}"
if type == True: # Create File
self.handle_files([path], "create_file")
else: # Create Folder
self.handle_files([path], "create_dir")
event_system.emit("hide_new_file_menu")
def rename_files(self):
rename_label = self._builder.get_object("file_to_rename_label")
rename_input = self._builder.get_object("rename_fname")
state = event_system.emit_and_await("get_current_state")
for uri in state.uris:
entry = uri.split("/")[-1]
rename_label.set_label(entry)
rename_input.set_text(entry)
response = event_system.emit_and_await("show_rename_file_menu", rename_input)
if response == "skip_edit":
continue
if response == "cancel_edit":
break
rname_to = rename_input.get_text().strip()
if rname_to:
target = f"{state.tab.get_current_directory()}/{rname_to}"
self.handle_files([uri], "rename", target)
event_system.emit("hide_rename_file_menu")
event_system.emit_and_await("get_selected_files").clear()

View File

@@ -0,0 +1,114 @@
# Python imports
import shlex
# Lib imports
# Application imports
from .crud_mixin import CRUDMixin
from .handler_mixin import HandlerMixin
class FileSystemActions(HandlerMixin, CRUDMixin):
"""docstring for FileSystemActions"""
def __init__(self):
super(FileSystemActions, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._selected_files = []
self._to_copy_files = []
self._to_cut_files = []
self._builder = settings_manager.get_builder()
def _setup_styling(self):
...
def _setup_signals(self):
...
def _subscribe_to_events(self):
event_system.subscribe("set_selected_files", self.set_selected_files)
event_system.subscribe("get_selected_files", self.get_selected_files)
event_system.subscribe("open_files", self.open_files)
event_system.subscribe("open_with_files", self.open_with_files)
event_system.subscribe("execute_files", self.execute_files)
event_system.subscribe("cut_files", self.cut_files)
event_system.subscribe("copy_files", self.copy_files)
event_system.subscribe("paste_files", self.paste_files)
event_system.subscribe("move_files", self.move_files)
event_system.subscribe("copy_name", self.copy_name)
event_system.subscribe("copy_path", self.copy_path)
event_system.subscribe("copy_path_name", self.copy_path_name)
event_system.subscribe("create_files", self.create_files)
event_system.subscribe("rename_files", self.rename_files)
def _load_widgets(self):
...
def set_selected_files(self, selected_files: []):
self._selected_files = selected_files
def get_selected_files(self):
return self._selected_files
def cut_files(self):
self._to_copy_files.clear()
state = event_system.emit_and_await("get_current_state")
self._to_cut_files = state.uris
def copy_files(self):
self._to_cut_files.clear()
state = event_system.emit_and_await("get_current_state")
self._to_copy_files = state.uris
def copy_path(self):
state = event_system.emit_and_await("get_current_state")
path = state.tab.get_current_directory()
event_system.emit("set_clipboard_data", (path,))
def copy_name(self):
state = event_system.emit_and_await("get_current_state")
if len(state.uris) == 1:
file_name = state.uris[0].split("/")[-1]
event_system.emit("set_clipboard_data", (file_name,))
def copy_path_name(self):
state = event_system.emit_and_await("get_current_state")
if len(state.uris) == 1:
file = state.uris[0].replace("file://", "")
event_system.emit("set_clipboard_data", (file,))
def open_files(self):
state = event_system.emit_and_await("get_current_state")
for file in state.uris:
state.tab.open_file_locally(file)
def open_with_files(self, app_info):
state = event_system.emit_and_await("get_current_state")
uris = state.uris_raw
if not state.uris_raw:
uris = [f"file://{state.tab.get_current_directory()}"]
state.tab.app_chooser_exec(app_info, uris)
def execute_files(self, in_terminal=False):
state = event_system.emit_and_await("get_current_state")
current_dir = state.tab.get_current_directory()
command = None
for path in state.uris:
command = f"{shlex.quote(path)}" if not in_terminal else f"{state.tab.terminal_app} -e {shlex.quote(path)}"
state.tab.execute(shlex.split(command), start_dir=state.tab.get_current_directory())

View File

@@ -0,0 +1,165 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import Gio
# Application imports
from ..widgets.io_widget import IOWidget
class HandlerMixinException(Exception):
...
class HandlerMixin:
"""docstring for HandlerMixin"""
# NOTE: Gtk recommends using fail flow than pre check which is more
# race condition proof. They're right; but, they can't even delete
# directories properly. So... f**k them. I'll do it my way.
def handle_files(self, paths, action, _target_path=None):
target = None
_file = None
response = None
overwrite_all = False
rename_auto_all = False
for path in paths:
try:
if "file://" in path:
path = path.split("file://")[1]
file = Gio.File.new_for_path(path)
if _target_path:
if file.get_parent().get_path() == _target_path:
raise HandlerMixinException("Parent dir of target and file locations are the same! Won't copy or move!")
if os.path.isdir(_target_path):
info = file.query_info("standard::display-name", 0, cancellable=None)
_target = f"{_target_path}/{info.get_display_name()}"
_file = Gio.File.new_for_path(_target)
else:
_file = Gio.File.new_for_path(_target_path)
else:
_file = Gio.File.new_for_path(path)
if _file.query_exists():
if not overwrite_all and not rename_auto_all:
event_system.emit("setup_exists_data", (file, _file))
response = event_system.emit_and_await("show_exists_page")
if response == "overwrite_all":
overwrite_all = True
if response == "rename_auto_all":
rename_auto_all = True
if response == "rename":
base_path = _file.get_parent().get_path()
new_name = self._builder.get_object("exists_file_field").get_text().strip()
rfPath = f"{base_path}/{new_name}"
_file = Gio.File.new_for_path(rfPath)
if response == "rename_auto" or rename_auto_all:
_file = self.rename_proc(_file)
if response == "overwrite" or overwrite_all:
type = _file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
state = event_system.emit_and_await("get_current_state")
state.tab.delete_file( _file.get_path() )
else:
_file.delete(cancellable=None)
if response == "skip":
continue
if response == "skip_all":
break
if _target_path:
target = _file
else:
file = _file
if action == "create_file":
file.create(flags=Gio.FileCreateFlags.NONE, cancellable=None)
continue
if action == "create_dir":
file.make_directory(cancellable=None)
continue
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
state = event_system.emit_and_await("get_current_state")
tab = state.tab
fPath = file.get_path()
tPath = target.get_path()
state = True
if action == "copy":
tab.copy_file(fPath, tPath)
if action == "move" or action == "rename":
tab.move_file(fPath, tPath)
else:
io_widget = IOWidget(action, file)
if action == "copy":
file.copy_async(destination=target,
flags=Gio.FileCopyFlags.BACKUP,
io_priority=98,
cancellable=io_widget.cancle_eve,
progress_callback=io_widget.update_progress,
callback=io_widget.finish_callback)
self._builder.get_object("io_list").add(io_widget)
if action == "move" or action == "rename":
file.move_async(destination=target,
flags=Gio.FileCopyFlags.BACKUP,
io_priority=98,
cancellable=io_widget.cancle_eve,
progress_callback=None,
# NOTE: progress_callback here causes seg fault when set
callback=io_widget.finish_callback)
self._builder.get_object("io_list").add(io_widget)
except GObject.GError as e:
raise OSError(e)
self._builder.get_object("exists_file_rename_bttn").set_sensitive(False)
def rename_proc(self, gio_file):
full_path = gio_file.get_path()
base_path = gio_file.get_parent().get_path()
file_name = os.path.splitext(gio_file.get_basename())[0]
extension = os.path.splitext(full_path)[-1]
target = Gio.File.new_for_path(full_path)
start = "-copy"
if settings_manager.is_debug():
logger.debug(f"Path: {full_path}")
logger.debug(f"Base Path: {base_path}")
logger.debug(f'Name: {file_name}')
logger.debug(f"Extension: {extension}")
i = 2
while target.query_exists():
try:
value = file_name[(file_name.find(start)+len(start)):]
int(value)
file_name = file_name.split(start)[0]
except HandlerMixinException as e:
pass
target = Gio.File.new_for_path(f"{base_path}/{file_name}-copy{i}{extension}")
i += 1
return target

View File

@@ -0,0 +1,3 @@
"""
Mixins module
"""

View File

@@ -0,0 +1,3 @@
"""
Signals module
"""

View File

@@ -0,0 +1,105 @@
# Python imports
import time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
# Application imports
class FileActionSignalsMixin:
"""docstring for FileActionSignalsMixin"""
def set_file_watcher(self, tab):
if tab.get_dir_watcher():
watcher = tab.get_dir_watcher()
watcher.cancel()
if settings_manager.is_debug():
logger.debug(f"Watcher Is Cancelled: {watcher.is_cancelled()}")
cur_dir = tab.get_current_directory()
dir_watcher = Gio.File.new_for_path(cur_dir) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
wid = tab.get_wid()
tid = tab.get_id()
dir_watcher.connect("changed", self.dir_watch_updates, (f"{wid}|{tid}",))
tab.set_dir_watcher(dir_watcher)
# NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab.
# Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency
def dir_watch_updates(self, file_monitor, file, other_file = None, eve_type = None, data = None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
logger.debug(eve_type)
if eve_type in [Gio.FileMonitorEvent.MOVED_IN, Gio.FileMonitorEvent.MOVED_OUT]:
self.update_on_soft_lock_end(data[0])
elif data[0] in self.soft_update_lock.keys():
self.soft_update_lock[data[0]]["last_update_time"] = time.time()
else:
self.soft_lock_countdown(data[0])
@threaded
def soft_lock_countdown(self, tab_widget):
self.soft_update_lock[tab_widget] = { "last_update_time": time.time()}
lock = True
while lock:
time.sleep(0.6)
last_update_time = self.soft_update_lock[tab_widget]["last_update_time"]
current_time = time.time()
if (current_time - last_update_time) > 0.6:
lock = False
self.soft_update_lock.pop(tab_widget, None)
GLib.idle_add(self.update_on_soft_lock_end, *(tab_widget,))
def update_on_soft_lock_end(self, tab_widget):
wid, tid = tab_widget.split("|")
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid", use_gtk = False)
store = icon_grid.get_model()
_store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab.load_directory()
icon_grid.clear_and_set_new_store()
self.load_store(tab, icon_grid.get_store())
tab_widget_label.set_label(tab.get_end_of_path())
state = self.get_current_state()
if [wid, tid] in [state.wid, state.tid]:
self.set_bottom_labels(tab)
def do_file_search(self, widget, eve = None):
if not self.ctrl_down and not self.shift_down and not self.alt_down:
target = widget.get_name()
notebook = self.builder.get_object(target)
page = notebook.get_current_page()
nth_page = notebook.get_nth_page(page)
icon_grid = nth_page.get_children()[0]
wid, tid = icon_grid.get_name().split("|")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
query = widget.get_text().lower()
icon_grid.unselect_all()
for i, file in enumerate(tab.get_files()):
if query and query in file[0].lower():
path = Gtk.TreePath().new_from_indices([i])
icon_grid.select_path(path)
items = icon_grid.get_selected_items()
if len(items) > 0:
icon_grid.scroll_to_path(items[0], False, 0.5, 0.5)

View File

@@ -0,0 +1,35 @@
# Python imports
# Lib imports
# Application imports
class IPCSignalsMixin:
""" IPCSignalsMixin handle messages from another starting solarfm process. """
def print_to_console(self, message=None):
print(message)
def handle_file_from_ipc(self, path):
window = self.builder.get_object("main_window")
window.deiconify()
window.show()
window.present()
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
if notebook.is_visible():
self.create_tab(wid, None, path)
return
if not self.is_pane4_hidden:
self.create_tab(4, None, path)
elif not self.is_pane3_hidden:
self.create_tab(3, None, path)
elif not self.is_pane2_hidden:
self.create_tab(2, None, path)
elif not self.is_pane1_hidden:
self.create_tab(1, None, path)

View File

@@ -0,0 +1,87 @@
# Python imports
import re
# 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
# Application imports
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
class KeyboardSignalsMixin:
""" KeyboardSignalsMixin keyboard hooks controller. """
# TODO: Need to set methods that use this to somehow check the keybindings state instead.
def unset_keys_and_data(self, widget=None, eve=None):
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
def on_global_key_press_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = True
if "shift" in keyname:
self.shift_down = True
if "alt" in keyname:
self.alt_down = True
def on_global_key_release_controller(self, widget, event):
"""Handler for keyboard events"""
keyname = Gdk.keyval_name(event.keyval).lower()
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = False
if "shift" in keyname:
self.shift_down = False
if "alt" in keyname:
self.alt_down = False
mapping = keybindings.lookup(event)
if mapping:
# See if in filemanager scope
try:
getattr(self, mapping)()
return True
except Exception:
# Must be plugins scope, event call, OR we forgot to add method to file manager scope
if "||" in mapping:
sender, eve_type = mapping.split("||")
else:
sender = ""
eve_type = mapping
self.handle_plugin_key_event(sender, eve_type)
else:
logger.debug(f"on_global_key_release_controller > key > {keyname}")
if self.ctrl_down:
if keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
self.builder.get_object(f"tggl_notebook_{keyname.strip('kp_')}").released()
def handle_plugin_key_event(self, sender, eve_type):
event_system.emit(eve_type)
def keyboard_close_tab(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
page = notebook.page_num(scroll)
tab = self.get_fm_window(wid).get_tab_by_id(tid)
watcher = tab.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_tab_by_id(tid)
notebook.remove_page(page)
if not trace_debug:
self.fm_controller.save_state()
self.set_window_title()

View File

@@ -0,0 +1,14 @@
# Python imports
# Lib imports
# Application imports
from .signals.file_action_signals_mixin import FileActionSignalsMixin
from .signals.ipc_signals_mixin import IPCSignalsMixin
from .signals.keyboard_signals_mixin import KeyboardSignalsMixin
class SignalsMixins(FileActionSignalsMixin, KeyboardSignalsMixin, IPCSignalsMixin):
...

View File

@@ -0,0 +1,3 @@
"""
UI module
"""

View File

@@ -0,0 +1,147 @@
# Python imports
import asyncio
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from ...widgets.tab_header_widget import TabHeaderWidget
from ...widgets.icon_grid_widget import IconGridWidget
from ...widgets.icon_tree_widget import IconTreeWidget
class GridMixin:
"""docstring for GridMixin"""
def load_store(self, tab, store, save_state = False, use_generator = False):
dir = tab.get_current_directory()
files = tab.get_files()
for file in files:
store.append([None, file[0]])
Gtk.main_iteration()
# for i, file in enumerate(files):
# self.create_icon(i, tab, store, dir, file[0])
if use_generator:
# NOTE: tab > icon > _get_system_thumbnail_gtk_thread must not be used
# as the attempted promotion back to gtk threading stalls the generator. (We're already in main gtk thread)
for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ):
self.load_icon(i, store, icon)
else:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
loop.create_task( self.create_icons(tab, store, dir, files) )
else:
asyncio.run( self.create_icons(tab, store, dir, files) )
# NOTE: Not likely called often from here but it could be useful
if save_state and not trace_debug:
self.fm_controller.save_state()
async def create_icons(self, tab, store, dir, files):
tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)]
await asyncio.gather(*tasks)
async def load_icon(self, i, store, icon):
self.update_store(i, store, icon)
async def update_store(self, i, store, dir, tab, file):
icon = tab.create_icon(dir, file)
itr = store.get_iter(i)
store.set_value(itr, 0, icon)
def create_icons_generator(self, tab, dir, files):
for file in files:
icon = tab.create_icon(dir, file[0])
yield icon
# @daemon_threaded
# def create_icon(self, i, tab, store, dir, file):
# icon = tab.create_icon(dir, file)
# GLib.idle_add(self.update_store, *(i, store, icon,))
#
# @daemon_threaded
# def load_icon(self, i, store, icon):
# GLib.idle_add(self.update_store, *(i, store, icon,))
# def update_store(self, i, store, icon):
# itr = store.get_iter(i)
# store.set_value(itr, 0, icon)
def create_tab_widget(self, tab):
return TabHeaderWidget(tab, self.close_tab)
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
scroll = Gtk.ScrolledWindow()
if not use_tree_view:
grid = self.create_icon_grid_widget()
else:
# TODO: Fix global logic to make the below work too
grid = self.create_icon_tree_widget()
scroll.add(grid)
scroll.set_name(f"{wid}|{tab.get_id()}")
grid.set_name(f"{wid}|{tab.get_id()}")
self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False)
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False)
return scroll, grid.get_store()
def create_icon_grid_widget(self):
grid = IconGridWidget()
grid._setup_additional_signals(
self.grid_icon_single_click,
self.grid_icon_double_click,
self.grid_set_selected_items,
self.grid_on_drag_set,
self.grid_on_drag_data_received,
self.grid_on_drag_motion
)
return grid
def create_icon_tree_widget(self):
grid = IconTreeWidget()
grid._setup_additional_signals(
self.grid_icon_single_click,
self.grid_icon_double_click,
self.grid_on_drag_set,
self.grid_on_drag_data_received,
self.grid_on_drag_motion
)
grid.columns_autosize()
return grid
def get_store_and_label_from_notebook(self, notebook, _name):
icon_grid = None
tab_label = None
store = None
for obj in notebook.get_children():
icon_grid = obj.get_children()[0]
name = icon_grid.get_name()
if name == _name:
store = icon_grid.get_model()
tab_label = notebook.get_tab_label(obj).get_children()[0]
return store, tab_label
def get_icon_grid_from_notebook(self, notebook, _name):
for obj in notebook.get_children():
icon_grid = obj.get_children()[0]
name = icon_grid.get_name()
if name == _name:
return icon_grid

View File

@@ -0,0 +1,58 @@
# Python imports
# Lib imports
# Application imports
# TODO: Should rewrite to try and support more windows more naturally
class PaneMixin:
"""docstring for PaneMixin"""
def toggle_pane(self, child):
if child.is_visible():
child.hide()
else:
child.show()
def run_flag_toggle(self, pane_index):
tggl_button = self.builder.get_object(f"tggl_notebook_{pane_index}")
if pane_index == 1:
self.is_pane1_hidden = not self.is_pane1_hidden
tggl_button.set_active(not self.is_pane1_hidden)
return self.is_pane1_hidden
elif pane_index == 2:
self.is_pane2_hidden = not self.is_pane2_hidden
tggl_button.set_active(not self.is_pane2_hidden)
return self.is_pane2_hidden
elif pane_index == 3:
self.is_pane3_hidden = not self.is_pane3_hidden
tggl_button.set_active(not self.is_pane3_hidden)
return self.is_pane3_hidden
elif pane_index == 4:
self.is_pane4_hidden = not self.is_pane4_hidden
tggl_button.set_active(not self.is_pane4_hidden)
return self.is_pane4_hidden
def toggle_notebook_pane(self, widget, eve=None):
name = widget.get_name()
pane_index = int(name[-1])
master_pane = self.builder.get_object("pane_master")
pane = self.builder.get_object("pane_top") if pane_index in [1, 2] else self.builder.get_object("pane_bottom")
state = self.run_flag_toggle(pane_index)
if self.is_pane1_hidden and self.is_pane2_hidden and self.is_pane3_hidden and self.is_pane4_hidden:
state = self.run_flag_toggle(pane_index)
self._set_fm_state(state, pane_index)
return
child = pane.get_child1() if pane_index in [1, 3] else pane.get_child2()
self.toggle_pane(child)
self._set_fm_state(state, pane_index)
def _set_fm_state(self, state, pane_index):
window = self.fm_controller.get_window_by_index(pane_index - 1)
window.set_is_hidden(state)

View File

@@ -0,0 +1,239 @@
# Python imports
import os
import gc
import time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from .grid_mixin import GridMixin
class TabMixin(GridMixin):
"""docstring for TabMixin"""
def create_tab(self, wid: int = None, tid: int = None, path: str = None):
if not wid:
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
path_entry = self.builder.get_object(f"path_entry")
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
tab.logger = logger
tab.set_wid(wid)
if not path:
if wid and tid:
_tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.set_path(_tab.get_current_directory())
else:
tab.set_path(path)
tab_widget = self.create_tab_widget(tab)
scroll, store = self.create_scroll_and_store(tab, wid)
index = notebook.append_page(scroll, tab_widget)
notebook.set_tab_detachable(scroll, True)
self.fm_controller.set_wid_and_tid(wid, tab.get_id())
path_entry.set_text(tab.get_current_directory())
notebook.show_all()
notebook.set_current_page(index)
ctx = notebook.get_style_context()
ctx.add_class("notebook-unselected-focus")
notebook.set_tab_reorderable(scroll, True)
self.load_store(tab, store)
self.set_window_title()
self.set_file_watcher(tab)
def close_tab(self, button, eve = None):
notebook = button.get_parent().get_parent()
if notebook.get_n_pages() == 1:
return
tab_box = button.get_parent()
wid = int(notebook.get_name()[-1])
tid = self.get_id_from_tab_box(tab_box)
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
icon_grid = scroll.get_children()[0]
store = icon_grid.get_store()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
watcher = tab.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_tab_by_id(tid)
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
self.builder.dereference_object(f"{wid}|{tid}")
store.clear()
icon_grid.destroy()
scroll.destroy()
tab_box.destroy()
del store
del icon_grid
del scroll
del tab_box
del watcher
del tab
gc.collect()
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
self.set_window_title()
# NOTE: Not actually getting called even tho set in the glade file...
def on_tab_dnded(self, notebook, page, x, y):
...
def on_tab_reorder(self, child, page_num, new_index):
wid, tid = page_num.get_name().split("|")
window = self.get_fm_window(wid)
tab = None
for i, tab in enumerate(window.get_all_tabs()):
if tab.get_id() == tid:
_tab = window.get_tab_by_id(tid)
watcher = _tab.get_dir_watcher()
watcher.cancel()
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
tab = window.get_tab_by_id(tid)
self.set_file_watcher(tab)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def on_tab_switch_update(self, notebook, content = None, index = None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
def get_id_from_tab_box(self, tab_box):
return tab_box.get_children()[2].get_text()
def get_tab_label(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
def get_tab_close(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
def get_tab_icon_grid_from_notebook(self, notebook):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data = None):
state = self.get_current_state()
state.tab.load_directory()
self.load_store(state.tab, state.store)
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
self.set_path_text(wid, tid)
char_width = len(tab.get_end_of_path())
tab_label.set_width_chars(char_width)
tab_label.set_label(tab.get_end_of_path())
self.set_window_title()
self.set_file_watcher(tab)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def do_action_from_bar_controls(self, widget, eve = None):
action = widget.get_name()
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
if action == "create_tab":
dir = tab.get_current_directory()
self.create_tab(wid, None, dir)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
return
if action == "go_up":
tab.pop_from_path()
if action == "go_home":
tab.set_to_home()
if action == "refresh_tab":
tab.load_directory()
if action == "path_entry":
focused_obj = self.window.get_focus()
dir = f"{tab.get_current_directory()}/"
path = widget.get_text()
if isinstance(focused_obj, Gtk.Entry):
self.process_path_menu(widget, tab, dir)
if path.endswith(".") or path == dir:
return
if not tab.set_path(path):
return
icon_grid = self.get_icon_grid_from_notebook(notebook, f"{wid}|{tid}")
icon_grid.clear_and_set_new_store()
self.update_tab(tab_label, tab, icon_grid.get_store(), wid, tid)
def process_path_menu(self, gtk_entry, tab, dir):
path_menu_buttons = self.builder.get_object("path_menu_buttons")
query = gtk_entry.get_text().replace(dir, "")
files = tab.get_files() + tab.get_hidden()
self.clear_children(path_menu_buttons)
show_path_menu = False
for file, hash, size in files:
if os.path.isdir(f"{dir}{file}"):
if query.lower() in file.lower():
button = Gtk.Button(label=file)
button.show()
button.connect("clicked", self.set_path_entry)
path_menu_buttons.add(button)
show_path_menu = True
if not show_path_menu:
event_system.emit("hide_path_menu")
else:
event_system.emit("show_path_menu")
buttons = path_menu_buttons.get_children()
if len(buttons) == 1:
self.slowed_focus(buttons[0])
@daemon_threaded
def slowed_focus(self, button):
time.sleep(0.05)
GLib.idle_add(self.do_focused_click, *(button,))
def do_focused_click(self, button):
button.grab_focus()
button.clicked()
def set_path_entry(self, button = None, eve = None):
self.path_auto_filled = True
state = self.get_current_state()
path = f"{state.tab.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
event_system.emit("hide_path_menu")
def show_hide_hidden_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.set_hiding_hidden(not tab.is_hiding_hidden())
tab.load_directory()
self.builder.get_object("refresh_tab").released()

View File

@@ -0,0 +1,182 @@
# Python imports
import copy
import traceback
from os.path import isdir
# 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
from .tab_mixin import TabMixin
class WindowException(Exception):
...
class WindowMixin(TabMixin):
"""docstring for WindowMixin"""
def get_fm_window(self, wid):
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
def set_bottom_labels(self, tab):
event_system.emit("set_bottom_labels", (tab,))
def set_window_title(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
for _notebook in self.notebooks:
ctx = _notebook.get_style_context()
ctx.remove_class("notebook-selected-focus")
ctx.add_class("notebook-unselected-focus")
ctx = notebook.get_style_context()
ctx.remove_class("notebook-unselected-focus")
ctx.add_class("notebook-selected-focus")
self.window.set_title(f"{app_name} ~ {dir}")
self.set_bottom_labels(tab)
def set_path_text(self, wid, tid):
path_entry = self.builder.get_object("path_entry")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
path_entry.set_text(tab.get_current_directory())
def grid_set_selected_items(self, icons_grid):
new_items = icons_grid.get_selected_items()
items_size = len(new_items)
selected_items = event_system.emit_and_await("get_selected_files")
if items_size == 1:
# NOTE: If already in selection, likely dnd else not so wont readd
if new_items[0] in selected_items:
self.dnd_left_primed += 1
# NOTE: If in selection but trying to just select an already selected item.
if self.dnd_left_primed > 1:
self.dnd_left_primed = 0
selected_items.clear()
# NOTE: Likely trying dnd, just readd to selection the former set.
# Prevents losing highlighting of grid selected.
for path in selected_items:
icons_grid.select_path(path)
if items_size > 0:
event_system.emit("set_selected_files", (new_items,))
else:
self.dnd_left_primed = 0
selected_items.clear()
def grid_icon_single_click(self, icons_grid, eve):
try:
event_system.emit("hide_path_menu")
wid, tid = icons_grid.get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
if eve.state & Gdk.ModifierType.CONTROL_MASK:
self.dnd_left_primed = 0
if self.single_click_open: # FIXME: need to find a way to pass the model index
self.grid_icon_double_click(icons_grid)
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
event_system.emit("show_context_menu")
except WindowException as e:
logger.info(repr(e))
self.display_message(settings.theming.error_color, f"{repr(e)}")
def grid_icon_double_click(self, icons_grid, item, data=None):
try:
if self.ctrl_down and self.shift_down:
self.unset_keys_and_data()
self.execute_files(in_terminal=True)
return
elif self.ctrl_down:
self.unset_keys_and_data()
self.execute_files()
return
state = self.get_current_state()
notebook = self.builder.get_object(f"window_{state.wid}")
tab_label = self.get_tab_label(notebook, state.icon_grid)
fileName = state.store[item][1]
dir = state.tab.get_current_directory()
file = f"{dir}/{fileName}"
if isdir(file):
state.tab.set_path(file)
state.icon_grid.clear_and_set_new_store()
self.update_tab(tab_label, state.tab, state.icon_grid.get_store(), state.wid, state.tid)
else:
event_system.emit("open_files")
except WindowException as e:
traceback.print_exc()
self.display_message(settings.theming.error_color, f"{repr(e)}")
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
action = icons_grid.get_name()
wid, tid = action.split("|")
store = icons_grid.get_model()
treePaths = icons_grid.get_selected_items()
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
# further down call chain when doing internal fm stuff.
uris = self.format_to_uris(store, wid, tid, treePaths)
uris_text = '\n'.join(uris)
data.set_uris(uris)
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
target = icons_grid.get_name()
wid, tid = target.split("|")
store = icons_grid.get_model()
path_at_loc = None
try:
data = icons_grid.get_dest_item_at_pos(x, y)
path_at_loc = data[0]
drop_position = data[1]
highlighted_item_path = icons_grid.get_drag_dest_item().path
if path_at_loc and path_at_loc == highlighted_item_path and drop_position == Gtk.IconViewDropPosition.DROP_INTO:
uri = self.format_to_uris(store, wid, tid, highlighted_item_path)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
else:
self.override_drop_dest = None
except Exception as e:
self.override_drop_dest = None
if target not in current:
self.fm_controller.set_wid_and_tid(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
uris = data.get_uris()
state = event_system.emit_and_await("get_current_state")
dest = f"{state.tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
if len(uris) == 0:
uris = data.get_text().split("\n")
from_uri = '/'.join(uris[0].replace("file://", "").split("/")[:-1])
if from_uri != dest:
event_system.emit("move_files", (uris, dest))
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, None, path)

View File

@@ -0,0 +1,33 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class SFMBuilder(Gtk.Builder):
"""docstring for SFMBuilder."""
def __init__(self):
super(SFMBuilder, self).__init__()
self.objects = {}
def get_object(self, id: str, use_gtk: bool = True) -> any:
if not use_gtk:
return self.objects[id]
return super(SFMBuilder, self).get_object(id)
def expose_object(self, id: str, object: any, use_gtk: bool = True) -> None:
if not use_gtk:
self.objects[id] = object
else:
super(SFMBuilder, self).expose_object(id, object)
def dereference_object(self, id: str) -> None:
del self.objects[id]

View File

@@ -0,0 +1,88 @@
# Python imports
# 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
# Application imports
from .mixins.ui.pane_mixin import PaneMixin
from .mixins.ui.window_mixin import WindowMixin
from .widgets.files_view.files_widget import FilesWidget
class UIMixinException(Exception):
...
class UIMixin(PaneMixin, WindowMixin):
# class UIMixin(PaneMixin):
def _generate_file_views(self, session_json = None):
# self._wip_hybrid_approach_loading_process(session_json)
self._current_loading_process(session_json)
def _wip_hybrid_approach_loading_process(self, session_json = None):
for session in session_json:
self.fm_controller.create_window()
FilesWidget().set_fm_controller(self.fm_controller)
for session in session_json:
nickname = session["window"]["Nickname"]
tabs = session["window"]["tabs"]
isHidden = True if session["window"]["isHidden"] == "True" else False
event_system.emit("load_files_view_state", (nickname, tabs))
@daemon_threaded
def _focus_last_visible_notebook(self, icon_grid):
import time
window = settings_manager.get_main_window()
while not window.is_visible() and not window.get_realized():
time.sleep(0.1)
icon_grid.event(Gdk.Event().new(type = Gdk.EventType.BUTTON_RELEASE))
def _current_loading_process(self, session_json = None):
if session_json:
for j, value in enumerate(session_json):
i = j + 1
notebook_tggl_button = self.builder.get_object(f"tggl_notebook_{i}")
is_hidden = True if value["window"]["isHidden"] == "True" else False
tabs = value["window"]["tabs"]
self.fm_controller.create_window()
notebook_tggl_button.set_active(True)
if tabs:
for tab in tabs:
self.create_new_tab_notebook(None, i, tab)
else:
self.create_new_tab_notebook(None, i, None)
if is_hidden:
self.toggle_notebook_pane(notebook_tggl_button)
try:
if not self.is_pane4_hidden:
notebook = self.window4
elif not self.is_pane3_hidden:
notebook = self.window3
elif not self.is_pane2_hidden:
notebook = self.window2
elif not self.is_pane1_hidden:
notebook = self.window1
scroll_win = notebook.get_children()[-1]
icon_grid = scroll_win.get_children()[0]
self._focus_last_visible_notebook(icon_grid)
except UIMixinException as e:
logger.info("\n: The saved session might be missing window data! :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n")
logger.debug(repr(e))
else:
for j in range(0, 4):
i = j + 1
self.fm_controller.create_window()
self.create_new_tab_notebook(None, i, None)

View File

@@ -0,0 +1,3 @@
"""
Widgets module
"""

View File

@@ -0,0 +1,105 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class BottomStatusInfoException(Exception):
...
class BottomStatusInfoWidget:
"""docstring for BottomStatusInfoWidget."""
def __init__(self):
super(BottomStatusInfoWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/bottom_status_info_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
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):
event_system.subscribe("set_bottom_labels", self.set_bottom_labels)
def _load_widgets(self):
builder = settings_manager.get_builder()
self.bottom_status_info = self._builder.get_object("bottom_status_info")
self.bottom_size_label = self._builder.get_object("bottom_size_label")
self.bottom_file_count_label = self._builder.get_object("bottom_file_count_label")
self.bottom_path_label = self._builder.get_object("bottom_path_label")
builder.expose_object(f"bottom_status_info", self.bottom_status_info)
builder.expose_object(f"bottom_size_label", self.bottom_size_label)
builder.expose_object(f"bottom_file_count_label", self.bottom_file_count_label)
builder.expose_object(f"bottom_path_label", self.bottom_path_label)
builder.get_object("core_widget").add(self.bottom_status_info)
def set_bottom_labels(self, tab):
state = event_system.emit_and_await("get_current_state")
selected_files = state.icon_grid.get_selected_items()
current_directory = tab.get_current_directory()
path_file = Gio.File.new_for_path(current_directory)
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
formatted_mount_free = sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
formatted_mount_size = sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) )
# NOTE: Hides empty trash and other desired buttons based on context.
if settings_manager.get_trash_files_path() == current_directory:
event_system.emit("show_trash_buttons")
else:
event_system.emit("hide_trash_buttons")
# If something selected
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
self.bottom_path_label.set_label(tab.get_current_directory())
if selected_files:
uris = event_system.emit_and_await("format_to_uris", (state.store, state.wid, state.tid, selected_files, True))
combined_size = 0
for uri in uris:
try:
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::size",
flags=Gio.FileQueryInfoFlags.NONE,
cancellable=None)
file_size = file_info.get_size()
combined_size += file_size
except BottomStatusInfoException as e:
logger.debug(repr(e))
formatted_size = sizeof_fmt(combined_size)
if tab.is_hiding_hidden():
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_files_count()} ({formatted_size})")
else:
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_not_hidden_count()} ({formatted_size})")
return
# If nothing selected
if tab.is_hiding_hidden():
if tab.get_hidden_count() > 0:
self.bottom_file_count_label.set_label(f"{tab.get_not_hidden_count()} visible ({tab.get_hidden_count()} hidden)")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")

View File

@@ -0,0 +1,93 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class ContextMenuWidget(Gtk.Menu):
"""docstring for ContextMenuWidget"""
def __init__(self):
super(ContextMenuWidget, self).__init__()
self.builder = settings_manager.get_builder()
self._builder = Gtk.Builder()
self._context_menu_data = settings_manager.get_context_menu_data()
self._window = settings_manager.get_main_window()
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("show_context_menu", self.show_context_menu)
event_system.subscribe("hide_context_menu", self.hide_context_menu)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
self.build_context_menu()
def _emit(self, menu_item, type):
event_system.emit("do_action_from_menu_controls", type)
def make_submenu(self, name, data, keys):
menu = Gtk.Menu()
menu_item = Gtk.MenuItem(name)
for key in keys:
if isinstance(data, dict):
entry = self.make_menu_item(key, data[key])
elif isinstance(data, list):
entry = self.make_menu_item(key, data)
else:
continue
menu.append(entry)
menu_item.set_submenu(menu)
return menu_item
def make_menu_item(self, name, data) -> Gtk.MenuItem:
if isinstance(data, dict):
return self.make_submenu(name, data, data.keys())
elif isinstance(data, list):
entry = Gtk.ImageMenuItem(name)
icon = getattr(Gtk, f"{data[0]}")
entry.set_image( Gtk.Image(stock=icon) )
entry.set_always_show_image(True)
entry.connect("activate", self._emit, (data[1]))
return entry
def build_context_menu(self) -> None:
data = self._context_menu_data
dkeys = data.keys()
plugins_entry = None
for dkey in dkeys:
entry = self.make_menu_item(dkey, data[dkey])
self.append(entry)
if dkey == "Plugins":
plugins_entry = entry
self.attach_to_widget(self._window, None)
self.show_all()
self.builder.expose_object("context_menu", self)
if plugins_entry:
self.builder.expose_object("context_menu_plugins", plugins_entry.get_submenu())
def show_context_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu").popup_at_pointer(None)
def hide_context_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu").popdown()

View File

@@ -0,0 +1,3 @@
"""
Dialogs module
"""

View File

@@ -0,0 +1,49 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class AboutWidget:
"""docstring for AboutWidget."""
def __init__(self):
super(AboutWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/about_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("show_about_page", self.show_about_page)
event_system.subscribe("hide_about_page", self.hide_about_page)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
builder = settings_manager.get_builder()
self.about_page = self._builder.get_object("about_page")
builder.expose_object(f"about_page", self.about_page)
def show_about_page(self, widget=None, eve=None):
response = self.about_page.run()
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
self.hide_about_page()
def hide_about_page(self, widget=None, eve=None):
self.about_page.hide()

View File

@@ -0,0 +1,61 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class AppchooserWidget:
"""docstring for AppchooserWidget."""
def __init__(self):
super(AppchooserWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/appchooser_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("show_appchooser_menu", self.show_appchooser_menu)
event_system.subscribe("hide_appchooser_menu", self.hide_appchooser_menu)
event_system.subscribe("run_appchooser_launch", self.run_appchooser_launch)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
builder = settings_manager.get_builder()
self._appchooser_menu = self._builder.get_object("appchooser_menu")
self._appchooser_widget = self._builder.get_object("appchooser_widget")
builder.expose_object(f"appchooser_menu", self._appchooser_menu)
builder.expose_object(f"appchooser_widget", self._appchooser_widget)
def show_appchooser_menu(self, widget=None, eve=None):
response = self._appchooser_menu.run()
if response == Gtk.ResponseType.OK:
app_info = self._appchooser_widget.get_app_info()
event_system.emit("open_with_files", app_info)
self.hide_appchooser_menu()
if response == Gtk.ResponseType.CANCEL:
self.hide_appchooser_menu()
def hide_appchooser_menu(self, widget=None, eve=None):
self._appchooser_menu.hide()
def run_appchooser_launch(self, widget=None, eve=None):
self._appchooser_menu.response(Gtk.ResponseType.OK)

View File

@@ -0,0 +1,130 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
class FileExistsWidget:
"""docstring for FileExistsWidget."""
def __init__(self):
super(FileExistsWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/file_exists_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("setup_exists_data", self.setup_exists_data)
event_system.subscribe("show_exists_page", self.show_exists_page)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
builder = settings_manager.get_builder()
self.file_exists_dialog = self._builder.get_object("file_exists_dialog")
self._exists_file_label = self._builder.get_object("exists_file_label")
self._exists_file_diff_from = self._builder.get_object("exists_file_diff_from")
self._exists_file_diff_to = self._builder.get_object("exists_file_diff_to")
self._exists_file_field = self._builder.get_object("exists_file_field")
self._exists_file_from = self._builder.get_object("exists_file_from")
self._exists_file_to = self._builder.get_object("exists_file_to")
self._exists_file_rename_bttn = self._builder.get_object("exists_file_rename_bttn")
builder.expose_object(f"file_exists_dialog", self.file_exists_dialog)
builder.expose_object(f"exists_file_diff_from", self._exists_file_diff_from)
builder.expose_object(f"exists_file_diff_to", self._exists_file_diff_to)
builder.expose_object(f"exists_file_field", self._exists_file_field)
builder.expose_object(f"exists_file_rename_bttn", self._exists_file_rename_bttn)
def show_exists_page(self, widget=None, eve=None):
response = self.file_exists_dialog.run()
self.file_exists_dialog.hide()
if response == Gtk.ResponseType.OK:
return "rename"
if response == Gtk.ResponseType.ACCEPT:
return "rename_auto"
if response == Gtk.ResponseType.CLOSE:
return "rename_auto_all"
if response == Gtk.ResponseType.YES:
return "overwrite"
if response == Gtk.ResponseType.APPLY:
return "overwrite_all"
if response == Gtk.ResponseType.NO:
return "skip"
if response == Gtk.ResponseType.REJECT:
return "skip_all"
def hide_exists_page_rename(self, widget=None, eve=None):
self.file_exists_dialog.response(Gtk.ResponseType.OK)
def hide_exists_page_auto_rename(self, widget=None, eve=None):
self.file_exists_dialog.response(Gtk.ResponseType.ACCEPT)
def hide_exists_page_auto_rename_all(self, widget=None, eve=None):
self.file_exists_dialog.response(Gtk.ResponseType.CLOSE)
def setup_exists_data(self, from_file, to_file):
from_info = from_file.query_info("standard::*,time::modified", 0, cancellable=None)
to_info = to_file.query_info("standard::*,time::modified", 0, cancellable=None)
from_date = from_info.get_modification_date_time()
to_date = to_info.get_modification_date_time()
from_size = from_info.get_size()
to_size = to_info.get_size()
self._exists_file_from.set_label(from_file.get_parent().get_path())
self._exists_file_to.set_label(to_file.get_parent().get_path())
self._exists_file_label.set_label(to_file.get_basename())
self._exists_file_field.set_text(to_file.get_basename())
# Returns: -1, 0 or 1 if dt1 is less than, equal to or greater than dt2.
age = GLib.DateTime.compare(from_date, to_date)
age_text = "( same time )"
if age == -1:
age_text = "older"
if age == 1:
age_text = "newer"
size_text = "( same size )"
if from_size < to_size:
size_text = "smaller"
if from_size > to_size:
size_text = "larger"
from_label_text = f"{age_text} & {size_text}"
if age_text != "( same time )" or size_text != "( same size )":
from_label_text = f"{from_date.format('%F %R')} {sizeof_fmt(from_size)} ( {from_size} bytes ) ( {age_text} & {size_text} )"
to_label_text = f"{to_date.format('%F %R')} {sizeof_fmt(to_size)} ( {to_size} bytes )"
self._exists_file_diff_from.set_text(from_label_text)
self._exists_file_diff_to.set_text(to_label_text)
def exists_rename_field_changed(self, widget):
nfile_name = widget.get_text().strip()
ofile_name = self._exists_file_label.get_label()
if nfile_name:
if nfile_name == ofile_name:
self._exists_file_rename_bttn.set_sensitive(False)
else:
self._exists_file_rename_bttn.set_sensitive(True)
else:
self._exists_file_rename_bttn.set_sensitive(False)

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 MessageWidget(Gtk.MessageDialog):
"""docstring for MessageWidget."""
def __init__(self):
super(MessageWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.type = Gtk.MessageType.WARNING
def _setup_signals(self):
...
def _subscribe_to_events(self):
...
def _load_widgets(self):
message_area = self.get_message_area()
message_area.get_children()[0].set_label("Alert!")
message_area.show_all()
self.set_image( Gtk.Image.new_from_icon_name("user-alert", 16) )
self.add_buttons(Gtk.STOCK_NO, Gtk.ResponseType.NO)
self.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES)

View File

@@ -0,0 +1,67 @@
# Python imports
# 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
# Application imports
class NewFileWidget:
"""docstring for NewFileWidget."""
def __init__(self):
super(NewFileWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/new_file_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("show_new_file_menu", self.show_new_file_menu)
event_system.subscribe("hide_new_file_menu", self.hide_new_file_menu)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
builder = settings_manager.get_builder()
self._new_file_menu = self._builder.get_object("new_file_menu")
self._new_fname_field = self._builder.get_object("new_fname_field")
self._new_file_toggle_type = self._builder.get_object("new_file_toggle_type")
builder.expose_object(f"new_file_menu", self._new_file_menu)
builder.expose_object(f"new_fname_field", self._new_fname_field)
builder.expose_object(f"new_file_toggle_type", self._new_file_toggle_type)
def show_new_file_menu(self, widget=None, eve=None):
if widget:
widget.set_text("")
widget.grab_focus()
response = self._new_file_menu.run()
if response == Gtk.ResponseType.CANCEL:
return True
else:
return False
def hide_new_file_menu(self, widget=None, eve=None):
self._builder.get_object("new_file_menu").hide()
def hide_new_file_menu_enter_key(self, widget=None, eve=None):
keyname = Gdk.keyval_name(eve.keyval).lower()
if keyname in ["return", "enter"]:
self._new_file_menu.hide()

View File

@@ -0,0 +1,81 @@
# Python imports
# 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
# Application imports
class RenameWidget:
"""docstring for RenameWidget."""
def __init__(self):
super(RenameWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/rename_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("show_rename_file_menu", self.show_rename_file_menu)
event_system.subscribe("hide_rename_file_menu", self.hide_rename_file_menu)
settings_manager.register_signals_to_builder([self,], self._builder)
def _load_widgets(self):
builder = settings_manager.get_builder()
self._rename_file_menu = self._builder.get_object("rename_file_menu")
self._rename_fname = self._builder.get_object("rename_fname")
self._file_to_rename_label = self._builder.get_object("file_to_rename_label")
builder.expose_object(f"rename_file_menu", self._rename_file_menu)
builder.expose_object(f"rename_fname", self._rename_fname)
builder.expose_object(f"file_to_rename_label", self._file_to_rename_label)
def show_rename_file_menu(self, widget=None, eve=None):
if widget:
widget.grab_focus()
response = self._rename_file_menu.run()
if response == Gtk.ResponseType.CLOSE:
return "skip_edit"
if response == Gtk.ResponseType.CANCEL:
return "cancel_edit"
return ""
def set_to_title_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().title() )
def set_to_upper_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().upper() )
def set_to_lower_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().lower() )
def set_to_invert_case(self, widget, eve=None):
self._rename_fname.set_text( self._rename_fname.get_text().swapcase() )
def hide_rename_file_menu(self, widget=None, eve=None):
self._rename_file_menu.hide()
def hide_rename_file_menu_enter_key(self, widget=None, eve=None):
keyname = Gdk.keyval_name(eve.keyval).lower()
if keyname in ["return", "enter"]:
self._rename_file_menu.hide()

View File

@@ -0,0 +1,84 @@
# Python imports
import gc
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class SaveLoadWidgetException(Exception):
...
class SaveLoadWidget:
"""docstring for SaveLoadWidget."""
def __init__(self):
super(SaveLoadWidget, self).__init__()
_GLADE_FILE = f"{settings_manager.get_ui_widgets_path()}/save_load_ui.glade"
self._builder = Gtk.Builder()
self._builder.add_from_file(_GLADE_FILE)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
event_system.subscribe("save_load_session", self.save_load_session)
def _load_widgets(self):
builder = settings_manager.get_builder()
self.save_load_dialog = self._builder.get_object("save_load_dialog")
builder.expose_object(f"save_load_dialog", self.save_load_dialog)
def save_load_session(self, action="save_session"):
state = event_system.emit_and_await("get_current_state")
if action == "save_session":
if not settings_manager.is_trace_debug():
state.fm_controller.save_state()
return
elif action == "save_session_as":
self.save_load_dialog.set_action(Gtk.FileChooserAction.SAVE)
elif action == "load_session":
self.save_load_dialog.set_action(Gtk.FileChooserAction.OPEN)
else:
raise SaveLoadWidgetException(f"Unknown action given: {action}")
self.save_load_dialog.set_current_folder(state.tab.get_current_directory())
self.save_load_dialog.set_current_name("session.json")
response = self.save_load_dialog.run()
if response == Gtk.ResponseType.OK:
if action == "save_session_as":
path = f"{self.save_load_dialog.get_current_folder()}/{self.save_load_dialog.get_current_name()}"
state.fm_controller.save_state(path)
elif action == "load_session":
path = f"{self.save_load_dialog.get_file().get_path()}"
session_json = state.fm_controller.get_state_from_file(path)
self.load_session(session_json)
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
...
self.save_load_dialog.hide()
def load_session(self, session_json):
if settings_manager.is_debug():
logger.debug(f"Session Data: {session_json}")
state = event_system.emit_and_await("get_current_state")
event_system.emit("clear_notebooks")
state.fm_controller.unload_tabs_and_windows()
event_system.emit("generate_file_views", (session_json,))
gc.collect()

View File

@@ -0,0 +1,63 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
class UserPassWidget(Gtk.Dialog):
"""docstring for UserPassWidget."""
def __init__(self):
super(UserPassWidget, self).__init__()
self.user_input = Gtk.Entry()
self.pass_input = Gtk.Entry()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_modal(True)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.user_input.set_placeholder_text("User:")
self.pass_input.set_placeholder_text("Password:")
self.pass_input.set_visibility(False)
def _setup_signals(self):
# self.connect("response", self.on_response)
...
def _subscribe_to_events(self):
...
def _load_widgets(self):
vbox = self.get_content_area()
label = Gtk.Label(label="User & Password")
vbox.add(label)
vbox.add(self.user_input)
vbox.add(self.pass_input)
vbox.show_all()
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
"Skip Input", Gtk.ResponseType.CLOSE,
"Submit Input", Gtk.ResponseType.OK
)
def do_response(self, response_id):
self.hide()
return response_id
# def on_response(self, widget, response_id):
# self.hide()
# return response_id

View File

@@ -0,0 +1,3 @@
"""
Files View module
"""

View File

@@ -0,0 +1,71 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ...mixins.signals.file_action_signals_mixin import FileActionSignalsMixin
from .window_mixin import WindowMixin
class FilesWidget(FileActionSignalsMixin, WindowMixin):
"""docstring for FilesWidget."""
ccount = 0
def __new__(cls, *args, **kwargs):
obj = super(FilesWidget, cls).__new__(cls)
cls.ccount += 1
return obj
def __init__(self):
super(FilesWidget, self).__init__()
self.INDEX = self.ccount
self.NAME = f"window_{self.INDEX}"
self.builder = Gtk.Builder()
self.files_view = None
self.fm_controller = None
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
...
def _setup_signals(self):
settings_manager.register_signals_to_builder([self,], self.builder)
def _subscribe_to_events(self):
event_system.subscribe("load_files_view_state", self._load_files_view_state)
event_system.subscribe("get_files_view_icon_grid", self._get_files_view_icon_grid)
def _load_widgets(self):
_builder = settings_manager.get_builder()
self.files_view = _builder.get_object(f"{self.NAME}")
self.files_view.set_group_name("files_widget")
self.builder.expose_object(f"{self.NAME}", self.files_view)
def _load_files_view_state(self, win_name = None, tabs = None):
if win_name == self.NAME:
if tabs:
for tab in tabs:
self.create_new_tab_notebook(None, self.INDEX, tab)
else:
self.create_new_tab_notebook(None, self.INDEX, None)
def _get_files_view_icon_grid(self, win_index = None, tid = None):
if win_index == str(self.INDEX):
return self.builder.get_object(f"{self.INDEX}|{tid}|icon_grid", use_gtk = False)
def set_fm_controller(self, _fm_controller):
self.fm_controller = _fm_controller

View File

@@ -0,0 +1,139 @@
# Python imports
import asyncio
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from ...widgets.tab_header_widget import TabHeaderWidget
from ...widgets.icon_grid_widget import IconGridWidget
from ...widgets.icon_tree_widget import IconTreeWidget
class GridMixin:
"""docstring for GridMixin"""
def load_store(self, tab, store, save_state = False, use_generator = False):
dir = tab.get_current_directory()
files = tab.get_files()
for file in files:
store.append([None, file[0]])
Gtk.main_iteration()
if use_generator:
# NOTE: tab > icon > _get_system_thumbnail_gtk_thread must not be used
# as the attempted promotion back to gtk threading stalls the generator. (We're already in main gtk thread)
for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ):
self.load_icon(i, store, icon)
else:
# for i, file in enumerate(files):
# self.create_icon(i, tab, store, dir, file[0])
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
loop.create_task( self.create_icons(tab, store, dir, files) )
else:
asyncio.run( self.create_icons(tab, store, dir, files) )
# NOTE: Not likely called often from here but it could be useful
if save_state and not trace_debug:
self.fm_controller.save_state()
async def create_icons(self, tab, store, dir, files):
tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)]
await asyncio.gather(*tasks)
async def load_icon(self, i, store, icon):
self.update_store(i, store, icon)
async def update_store(self, i, store, dir, tab, file):
icon = tab.create_icon(dir, file)
itr = store.get_iter(i)
store.set_value(itr, 0, icon)
def create_icons_generator(self, tab, dir, files):
for file in files:
icon = tab.create_icon(dir, file[0])
yield icon
# @daemon_threaded
# def create_icon(self, i, tab, store, dir, file):
# icon = tab.create_icon(dir, file)
# GLib.idle_add(self.update_store, *(i, store, icon,))
#
# @daemon_threaded
# def load_icon(self, i, store, icon):
# GLib.idle_add(self.update_store, *(i, store, icon,))
#
# def update_store(self, i, store, icon):
# itr = store.get_iter(i)
# store.set_value(itr, 0, icon)
def create_tab_widget(self, tab):
return TabHeaderWidget(tab, self.close_tab)
def create_scroll_and_store(self, tab, wid, use_tree_view = False):
scroll = Gtk.ScrolledWindow()
if not use_tree_view:
grid = self.create_icon_grid_widget()
else:
# TODO: Fix global logic to make the below work too
grid = self.create_icon_tree_widget()
scroll.add(grid)
scroll.set_name(f"{wid}|{tab.get_id()}")
grid.set_name(f"{wid}|{tab.get_id()}")
self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False)
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False)
return scroll, grid.get_store()
def create_icon_grid_widget(self):
grid = IconGridWidget()
grid._setup_additional_signals(
self.grid_icon_single_click,
self.grid_icon_double_click,
self.grid_set_selected_items,
self.grid_on_drag_set,
self.grid_on_drag_data_received,
self.grid_on_drag_motion
)
return grid
def create_icon_tree_widget(self):
grid = IconTreeWidget()
grid._setup_additional_signals(
self.grid_icon_single_click,
self.grid_icon_double_click,
self.grid_on_drag_set,
self.grid_on_drag_data_received,
self.grid_on_drag_motion
)
grid.columns_autosize()
return grid
def get_store_and_label_from_notebook(self, notebook, _name):
icon_grid = None
tab_label = None
store = None
for obj in notebook.get_children():
icon_grid = obj.get_children()[0]
name = icon_grid.get_name()
if name == _name:
store = icon_grid.get_model()
tab_label = notebook.get_tab_label(obj).get_children()[0]
return store, tab_label

View File

@@ -0,0 +1,244 @@
# Python imports
import os
import gc
import time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from .grid_mixin import GridMixin
class TabMixin(GridMixin):
"""docstring for TabMixin"""
def create_tab(self, wid: int = None, tid: int = None, path: str = None):
if not wid:
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
# path_entry = self.builder.get_object(f"path_entry")
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
# tab.logger = logger
tab.set_wid(wid)
if not path:
if wid and tid:
_tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.set_path(_tab.get_current_directory())
else:
tab.set_path(path)
tab_widget = self.create_tab_widget(tab)
scroll, store = self.create_scroll_and_store(tab, wid)
index = notebook.append_page(scroll, tab_widget)
notebook.set_tab_detachable(scroll, True)
self.fm_controller.set_wid_and_tid(wid, tab.get_id())
event_system.emit("go_to_path", (tab.get_current_directory(),)) # NOTE: Not efficent if I understand how
# path_entry.set_text(tab.get_current_directory())
notebook.show_all()
notebook.set_current_page(index)
ctx = notebook.get_style_context()
ctx.add_class("notebook-unselected-focus")
notebook.set_tab_reorderable(scroll, True)
self.load_store(tab, store)
# self.set_window_title()
event_system.emit("set_window_title", (tab.get_current_directory(),))
self.set_file_watcher(tab)
def close_tab(self, button, eve = None):
notebook = button.get_parent().get_parent()
if notebook.get_n_pages() == 1:
return
tab_box = button.get_parent()
wid = int(notebook.get_name()[-1])
tid = self.get_id_from_tab_box(tab_box)
scroll = self.builder.get_object(f"{wid}|{tid}", use_gtk = False)
icon_grid = scroll.get_children()[0]
store = icon_grid.get_model()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
watcher = tab.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_tab_by_id(tid)
self.builder.dereference_object(f"{wid}|{tid}|icon_grid")
self.builder.dereference_object(f"{wid}|{tid}")
store.clear()
# store.run_dispose()
icon_grid.destroy()
# icon_grid.run_dispose()
scroll.destroy()
#scroll.run_dispose()
tab_box.destroy()
#tab_box.run_dispose()
del store
del icon_grid
del scroll
del tab_box
del watcher
del tab
gc.collect()
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
self.set_window_title()
# NOTE: Not actually getting called even tho set in the glade file...
def on_tab_dnded(self, notebook, page, x, y):
...
def on_tab_reorder(self, child, page_num, new_index):
wid, tid = page_num.get_name().split("|")
window = self.get_fm_window(wid)
tab = None
for i, tab in enumerate(window.get_all_tabs()):
if tab.get_id() == tid:
_tab = window.get_tab_by_id(tid)
watcher = _tab.get_dir_watcher()
watcher.cancel()
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
tab = window.get_tab_by_id(tid)
self.set_file_watcher(tab)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def on_tab_switch_update(self, notebook, content = None, index = None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
def get_id_from_tab_box(self, tab_box):
return tab_box.get_children()[2].get_text()
def get_tab_label(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
def get_tab_close(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
def get_tab_icon_grid_from_notebook(self, notebook):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data = None):
state = self.get_current_state()
state.tab.load_directory()
self.load_store(state.tab, state.store)
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
self.set_path_text(wid, tid)
char_width = len(tab.get_end_of_path())
tab_label.set_width_chars(char_width)
tab_label.set_label(tab.get_end_of_path())
self.set_window_title()
self.set_file_watcher(tab)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
def do_action_from_bar_controls(self, widget, eve = None):
action = widget.get_name()
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
if action == "create_tab":
dir = tab.get_current_directory()
self.create_tab(wid, None, dir)
if not settings_manager.is_trace_debug():
self.fm_controller.save_state()
return
if action == "go_up":
tab.pop_from_path()
if action == "go_home":
tab.set_to_home()
if action == "refresh_tab":
tab.load_directory()
if action == "path_entry":
focused_obj = self.window.get_focus()
dir = f"{tab.get_current_directory()}/"
path = widget.get_text()
if isinstance(focused_obj, Gtk.Entry):
self.process_path_menu(widget, tab, dir)
if path.endswith(".") or path == dir:
return
if not tab.set_path(path):
return
icon_grid = self.get_icon_grid_from_notebook(notebook, f"{wid}|{tid}")
icon_grid.clear_and_set_new_store()
self.update_tab(tab_label, tab, store, wid, tid)
def process_path_menu(self, gtk_entry, tab, dir):
path_menu_buttons = self.builder.get_object("path_menu_buttons")
query = gtk_entry.get_text().replace(dir, "")
files = tab.get_files() + tab.get_hidden()
self.clear_children(path_menu_buttons)
show_path_menu = False
for file, hash, size in files:
if os.path.isdir(f"{dir}{file}"):
if query.lower() in file.lower():
button = Gtk.Button(label=file)
button.show()
button.connect("clicked", self.set_path_entry)
path_menu_buttons.add(button)
show_path_menu = True
if not show_path_menu:
event_system.emit("hide_path_menu")
else:
event_system.emit("show_path_menu")
buttons = path_menu_buttons.get_children()
if len(buttons) == 1:
self.slowed_focus(buttons[0])
@daemon_threaded
def slowed_focus(self, button):
time.sleep(0.05)
GLib.idle_add(self.do_focused_click, *(button,))
def do_focused_click(self, button):
button.grab_focus()
button.clicked()
def set_path_entry(self, button = None, eve = None):
self.path_auto_filled = True
state = self.get_current_state()
path = f"{state.tab.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
event_system.emit("hide_path_menu")
def show_hide_hidden_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.set_hiding_hidden(not tab.is_hiding_hidden())
tab.load_directory()
self.builder.get_object("refresh_tab").released()

View File

@@ -0,0 +1,178 @@
# Python imports
import copy
import traceback
from os.path import isdir
# 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
from .tab_mixin import TabMixin
class WindowException(Exception):
...
class WindowMixin(TabMixin):
"""docstring for WindowMixin"""
def get_fm_window(self, wid):
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
def set_bottom_labels(self, tab):
event_system.emit("set_bottom_labels", (tab,))
def set_window_title(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
event_system.emit("unset_selected_files_views")
ctx = self.files_view.get_style_context()
ctx.remove_class("notebook-unselected-focus")
ctx.add_class("notebook-selected-focus")
event_system.emit("set_window_title", (dir,))
self.set_bottom_labels(tab)
def set_path_text(self, wid, tid):
tab = self.get_fm_window(wid).get_tab_by_id(tid)
event_system.emit("go_to_path", (tab.get_current_directory(),))
def grid_set_selected_items(self, icons_grid):
new_items = icons_grid.get_selected_items()
items_size = len(new_items)
selected_items = event_system.emit_and_await("get_selected_files")
if items_size == 1:
# NOTE: If already in selection, likely dnd else not so wont readd
if new_items[0] in selected_items:
self.dnd_left_primed += 1
# NOTE: If in selection but trying to just select an already selected item.
if self.dnd_left_primed > 1:
self.dnd_left_primed = 0
selected_items.clear()
# NOTE: Likely trying dnd, just readd to selection the former set.
# Prevents losing highlighting of grid selected.
for path in selected_items:
icons_grid.select_path(path)
if items_size > 0:
event_system.emit("set_selected_files", (new_items,))
else:
self.dnd_left_primed = 0
selected_items.clear()
def grid_icon_single_click(self, icons_grid, eve):
try:
event_system.emit("hide_path_menu")
wid, tid = icons_grid.get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
if eve.state & Gdk.ModifierType.CONTROL_MASK:
self.dnd_left_primed = 0
if self.single_click_open: # FIXME: need to find a way to pass the model index
self.grid_icon_double_click(icons_grid)
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
event_system.emit("show_context_menu")
except WindowException as e:
logger.info(repr(e))
self.display_message(settings.theming.error_color, f"{repr(e)}")
def grid_icon_double_click(self, icons_grid, item, data=None):
try:
if self.ctrl_down and self.shift_down:
self.unset_keys_and_data()
self.execute_files(in_terminal=True)
return
elif self.ctrl_down:
self.unset_keys_and_data()
self.execute_files()
return
state = self.get_current_state()
notebook = self.builder.get_object(f"window_{state.wid}")
tab_label = self.get_tab_label(notebook, state.icon_grid)
fileName = state.store[item][1]
dir = state.tab.get_current_directory()
file = f"{dir}/{fileName}"
if isdir(file):
state.tab.set_path(file)
state.icon_grid.clear_and_set_new_store()
self.update_tab(tab_label, state.tab, state.icon_grid.get_store(), state.wid, state.tid)
else:
event_system.emit("open_files")
except WindowException as e:
traceback.print_exc()
self.display_message(settings.theming.error_color, f"{repr(e)}")
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
action = icons_grid.get_name()
wid, tid = action.split("|")
store = icons_grid.get_model()
treePaths = icons_grid.get_selected_items()
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
# further down call chain when doing internal fm stuff.
uris = self.format_to_uris(store, wid, tid, treePaths)
uris_text = '\n'.join(uris)
data.set_uris(uris)
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
target = icons_grid.get_name()
wid, tid = target.split("|")
store = icons_grid.get_model()
path_at_loc = None
try:
data = icons_grid.get_dest_item_at_pos(x, y)
path_at_loc = data[0]
drop_position = data[1]
highlighted_item_path = icons_grid.get_drag_dest_item().path
if path_at_loc and path_at_loc == highlighted_item_path and drop_position == Gtk.IconViewDropPosition.DROP_INTO:
uri = self.format_to_uris(store, wid, tid, highlighted_item_path)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
else:
self.override_drop_dest = None
except Exception as e:
self.override_drop_dest = None
if target not in current:
self.fm_controller.set_wid_and_tid(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
uris = data.get_uris()
state = event_system.emit_and_await("get_current_state")
dest = f"{state.tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
if len(uris) == 0:
uris = data.get_text().split("\n")
from_uri = '/'.join(uris[0].replace("file://", "").split("/")[:-1])
if from_uri != dest:
event_system.emit("move_files", (uris, dest))
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, None, path)

View File

@@ -0,0 +1,80 @@
# Python imports
# 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 GdkPixbuf
# Application imports
class IconGridWidget(Gtk.IconView):
"""docstring for IconGridWidget"""
def __init__(self):
super(IconGridWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._set_up_dnd()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_pixbuf_column(0)
self.set_text_column(1)
self.set_item_orientation(1)
self.set_selection_mode(3)
self.set_item_width(96)
self.set_item_padding(8)
self.set_margin(12)
self.set_row_spacing(18)
self.set_columns(-1)
self.set_spacing(12)
self.set_column_spacing(18)
def _setup_signals(self):
...
def _setup_additional_signals(self, grid_icon_single_click,
grid_icon_double_click,
grid_set_selected_items,
grid_on_drag_set,
grid_on_drag_data_received,
grid_on_drag_motion):
self.connect("button_release_event", grid_icon_single_click)
self.connect("item-activated", grid_icon_double_click)
self.connect("selection-changed", grid_set_selected_items)
self.connect("drag-data-get", grid_on_drag_set)
self.connect("drag-data-received", grid_on_drag_data_received)
self.connect("drag-motion", grid_on_drag_motion)
def _load_widgets(self):
self.clear_and_set_new_store()
def _set_up_dnd(self):
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
action = Gdk.DragAction.COPY
self.enable_model_drag_dest(targets, action)
self.enable_model_drag_source(0, targets, action)
def get_store(self):
return self.get_model()
def clear_and_set_new_store(self):
self.set_model(None)
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str or None)
self.set_model(store)

View File

@@ -0,0 +1,83 @@
# Python imports
# 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 GdkPixbuf
# Application imports
class IconTreeWidget(Gtk.TreeView):
"""docstring for IconTreeWidget"""
def __init__(self):
super(IconTreeWidget, self).__init__()
self._store = None
self._setup_styling()
self._setup_signals()
self._set_up_dnd()
self._load_widgets()
self.show_all()
def get_store(self):
return self._store
def _setup_styling(self):
self.set_search_column(1)
self.set_rubber_banding(True)
self.set_headers_visible(False)
self.set_enable_tree_lines(False)
def _setup_signals(self):
...
def _setup_additional_signals(self, grid_icon_single_click,
grid_icon_double_click,
grid_on_drag_set,
grid_on_drag_data_received,
grid_on_drag_motion):
self.connect("button_release_event", self.grid_icon_single_click)
self.connect("row-activated", self.grid_icon_double_click)
self.connect("drag-data-get", self.grid_on_drag_set)
self.connect("drag-data-received", self.grid_on_drag_data_received)
self.connect("drag-motion", self.grid_on_drag_motion)
def _load_widgets(self):
self._store = Gtk.TreeStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str or None)
column = Gtk.TreeViewColumn("Icons")
icon = Gtk.CellRendererPixbuf()
name = Gtk.CellRendererText()
selec = self.get_selection()
self.set_model(store)
selec.set_mode(3)
column.pack_start(icon, False)
column.pack_start(name, True)
column.add_attribute(icon, "pixbuf", 0)
column.add_attribute(name, "text", 1)
column.set_expand(False)
column.set_sizing(2)
column.set_min_width(120)
column.set_max_width(74)
self.append_column(column)
def _set_up_dnd(self):
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
action = Gdk.DragAction.COPY
self.enable_model_drag_dest(targets, action)
self.enable_model_drag_source(0, targets, action)

View File

@@ -0,0 +1,83 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
class IOWidget(Gtk.Box):
"""docstring for IOWidget"""
def __init__(self, action, file):
super(IOWidget, self).__init__()
self._action = action
self._file = file
self._basename = self._file.get_basename()
self.cancle_eve = Gio.Cancellable.new()
self.progress = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
def _setup_styling(self):
self.set_orientation(1)
def _setup_signals(self):
...
def _load_widgets(self):
stats = Gtk.Box()
label = Gtk.Label()
cncl_button = Gtk.Button(label="Cancel")
del_button = Gtk.Button(label="Clear")
self.progress = Gtk.ProgressBar()
label.set_label(self._basename)
self.progress.set_show_text(True)
self.progress.set_text(f"{self._action.upper()}ING")
stats.set_orientation(0)
stats.pack_end(del_button, False, False, 5)
del_button.connect("clicked", self.delete_self, ())
if not self._action in ("create", "rename"):
stats.pack_end(cncl_button, False, False, 5)
cncl_button.connect("clicked", self.do_cancel, *(self, self.cancle_eve))
stats.add(self.progress)
self.add(label)
self.add(stats)
def do_cancel(self, widget, container, eve):
logger.info(f"Canceling: [{self._action}] of {self._basename} ...")
eve.cancel()
def update_progress(self, current, total, eve=None):
self.progress.set_fraction(current/total)
def finish_callback(self, file, task=None, eve=None):
if self._action == "move" or self._action == "rename":
status = self._file.move_finish(task)
if self._action == "copy":
status = self._file.copy_finish(task)
if status:
self.delete_self()
else:
logger.info(f"{self._action} of {self._basename} failed...")
def delete_self(self, widget=None, eve=None):
self.get_parent().remove(self)

View File

@@ -0,0 +1,3 @@
"""
Popups module
"""

View File

@@ -0,0 +1,46 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class IOPopupWidget(Gtk.Popover):
"""docstring for IOPopupWidget."""
def __init__(self):
super(IOPopupWidget, self).__init__()
self._builder = settings_manager.get_builder()
self._builder.expose_object(f"io_popup", self)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
io_button = self._builder.get_object(f"io_button")
self.set_relative_to(io_button)
self.set_modal(True)
self.set_position(Gtk.PositionType.BOTTOM)
self.set_size_request(320, 280)
def _setup_signals(self):
event_system.subscribe("show_io_popup", self.show_io_popup)
def _load_widgets(self):
vbox = Gtk.Box()
vbox.set_orientation(Gtk.Orientation.VERTICAL)
self._builder.expose_object(f"io_list", vbox)
self.add(vbox)
def show_io_popup(self, widget=None, eve=None):
self.popup()

View File

@@ -0,0 +1,129 @@
# Python imports
import datetime
import inspect
import sys
import time
import traceback
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
class MessagePopupWidget(Gtk.Popover):
""" MessagePopupWidget custom exception hook viewer to reroute to log Gtk text area too. """
def __init__(self):
super(MessagePopupWidget, self).__init__()
self.builder = settings_manager.get_builder()
self.builder.expose_object(f"message_popup_widget", self)
self._message_buffer = None
sys.excepthook = self.custom_except_hook
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
self.set_relative_to( self.builder.get_object(f"main_menu_bar") )
self.set_modal(True)
self.set_position(Gtk.PositionType.BOTTOM)
self.set_hexpand(True)
self.set_vexpand(True)
def _setup_signals(self):
event_system.subscribe("show_messages_popup", self.show_messages_popup)
event_system.subscribe("hide_messages_popup", self.hide_messages_popup)
def _load_widgets(self):
self._message_buffer = Gtk.TextBuffer()
vbox = Gtk.Box()
scroll_window = Gtk.ScrolledWindow()
message_text_view = Gtk.TextView.new_with_buffer(self._message_buffer)
button = Gtk.Button(label="Save As")
button.set_image( Gtk.Image(stock=Gtk.STOCK_SAVE_AS) )
button.connect("released", self.save_debug_alerts)
button.set_always_show_image(True)
scroll_window.set_vexpand(True)
scroll_window.set_hexpand(True)
vbox.set_orientation(Gtk.Orientation.VERTICAL)
self.builder.expose_object(f"message_popup_widget", self)
self.builder.expose_object(f"message_text_view", message_text_view)
scroll_window.add(message_text_view)
vbox.add(button)
vbox.add(scroll_window)
vbox.show_all()
self.add(vbox)
def show_messages_popup(self):
self.popup()
def hide_messages_popup(self):
self.popup()
def custom_except_hook(self, exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
self._exception_to_ui(exc_type, exc_value, exc_traceback)
def _exception_to_ui(self, exc_type, exc_value, exc_traceback):
trace = ''.join(traceback.format_tb(exc_traceback))
current_time = datetime.datetime.now()
data = f"{current_time}\nExec Type: {exc_type} <--> Value: {exc_value}\n\n{trace}\n\n"
self.display_message(settings.theming.error_color, data)
def display_message(self, type, text, seconds=None):
# start_itr = self._message_buffer.get_start_iter()
start_itr = self._message_buffer.get_iter_at_line(0)
self._message_buffer.place_cursor(start_itr)
self._message_buffer.insert_at_cursor(text)
if seconds:
self.popup()
self.hide_message_timeout(seconds)
@threaded
def hide_message_timeout(self, seconds=3):
time.sleep(seconds)
GLib.idle_add(event_system.emit, ("hide_messages_popup"))
def save_debug_alerts(self, widget=None, eve=None):
start_itr, end_itr = self._message_buffer.get_bounds()
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", settings_manager.get_main_window(), \
action = Gtk.FileChooserAction.SAVE, \
buttons = (Gtk.STOCK_CANCEL, \
Gtk.ResponseType.CANCEL, \
Gtk.STOCK_SAVE, \
Gtk.ResponseType.OK))
text = self._message_buffer.get_text(start_itr, end_itr, False)
resp = save_location_prompt.run()
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
pass
elif resp == Gtk.ResponseType.OK:
target = save_location_prompt.get_filename();
with open(target, "w") as f:
f.write(text)
save_location_prompt.destroy()

View File

@@ -0,0 +1,58 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class PathMenuPopupWidget(Gtk.Popover):
"""docstring for PathMenuPopupWidget."""
def __init__(self):
super(PathMenuPopupWidget, self).__init__()
self.builder = settings_manager.get_builder()
self.builder.expose_object(f"path_menu", self)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
path_entry = self.builder.get_object(f"path_entry")
self.set_relative_to(path_entry)
self.set_modal(False)
self.set_position(Gtk.PositionType.BOTTOM)
self.set_size_request(240, 420)
def _setup_signals(self):
event_system.subscribe("show_path_menu", self.show_path_menu)
event_system.subscribe("hide_path_menu", self.hide_path_menu)
def _load_widgets(self):
path_menu_buttons = Gtk.ButtonBox()
scroll_window = Gtk.ScrolledWindow()
view_port = Gtk.Viewport()
scroll_window.set_vexpand(True)
scroll_window.set_hexpand(True)
path_menu_buttons.set_orientation(Gtk.Orientation.VERTICAL)
self.builder.expose_object(f"path_menu_buttons", path_menu_buttons)
view_port.add(path_menu_buttons)
scroll_window.add(view_port)
scroll_window.show_all()
self.add(scroll_window)
def show_path_menu(self, widget=None, eve=None):
self.popup()
def hide_path_menu(self, widget=None, eve=None):
self.popdown()

View File

@@ -0,0 +1,50 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class PluginsPopupWidget(Gtk.Popover):
"""docstring for PluginsPopupWidget."""
def __init__(self):
super(PluginsPopupWidget, self).__init__()
self.builder = settings_manager.get_builder()
self.builder.expose_object(f"plugin_controls", self)
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
plugins_button = self.builder.get_object(f"plugins_button")
self.set_relative_to(plugins_button)
self.set_modal(True)
self.set_position(Gtk.PositionType.BOTTOM)
def _setup_signals(self):
event_system.subscribe("show_plugins_popup", self.show_plugins_popup)
self.connect("button-release-event", self.hide_plugins_popup)
self.connect("key-release-event", self.hide_plugins_popup)
def _load_widgets(self):
vbox = Gtk.Box()
vbox.set_orientation(Gtk.Orientation.VERTICAL)
self.builder.expose_object(f"plugin_control_list", vbox)
self.add(vbox)
def show_plugins_popup(self, widget=None, eve=None):
self.popup()
def hide_plugins_popup(self, widget=None, eve=None):
self.hide()

View File

@@ -0,0 +1,56 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class TabHeaderWidget(Gtk.Box):
"""docstring for TabHeaderWidget"""
def __init__(self, tab, close_tab):
super(TabHeaderWidget, self).__init__()
self._tab = tab
self._close_tab = close_tab # NOTE: Close method in tab_mixin
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
self.set_orientation(0)
self.set_hexpand(False)
def _setup_signals(self):
...
def _load_widgets(self):
label = Gtk.Label()
tid = Gtk.Label()
close = Gtk.Button()
icon = Gtk.Image(stock=Gtk.STOCK_CLOSE)
label.set_label(f"{self._tab.get_end_of_path()}")
label.set_width_chars(len(self._tab.get_end_of_path()))
label.set_xalign(0.0)
label.set_margin_left(25)
label.set_margin_right(25)
label.set_hexpand(True)
tid.set_label(f"{self._tab.get_id()}")
close.connect("released", self._close_tab)
close.add(icon)
self.add(label)
self.add(close)
self.add(tid)
self.show_all()
tid.hide()

View File

@@ -0,0 +1,98 @@
# Python imports
import time
import signal
# Lib imports
import gi
import cairo
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 GLib
# Application imports
from core.controller import Controller
class ControllerStartException(Exception):
...
class Window(Gtk.ApplicationWindow):
"""docstring for Window."""
def __init__(self, args, unknownargs):
super(Window, self).__init__()
self._controller = None
settings_manager.set_main_window(self)
self._set_window_data()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets(args, unknownargs)
self.show()
def _setup_styling(self):
self.set_default_size(settings_manager.get_main_window_width(),
settings_manager.get_main_window_height())
self.set_title(f"{app_name}")
self.set_icon_from_file( settings_manager.get_window_icon() )
self.set_gravity(5) # 5 = CENTER
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
def _setup_signals(self):
self.connect("delete-event", self._tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._tear_down)
def _subscribe_to_events(self):
event_system.subscribe("tear_down", self._tear_down)
event_system.subscribe("load_interactive_debug", self._load_interactive_debug)
def _load_widgets(self, args, unknownargs):
if settings_manager.is_debug():
self.set_interactive_debugging(True)
self._controller = Controller(args, unknownargs)
if not self._controller:
raise ControllerStartException("Controller exited and doesn't exist...")
self.add( self._controller.get_core_widget() )
def _set_window_data(self) -> None:
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.set_visual(visual)
self.set_app_paintable(True)
self.connect("draw", self._area_draw)
# bind css file
cssProvider = Gtk.CssProvider()
cssProvider.load_from_path( settings_manager.get_css_file() )
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext()
styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None:
cr.set_source_rgba( *settings_manager.get_paint_bg_color() )
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def _load_interactive_debug(self):
self.set_interactive_debugging(True)
def _tear_down(self, widget = None, eve = None):
event_system.emit("shutting_down")
settings_manager.clear_pid()
Gtk.main_quit()

View File

@@ -0,0 +1,3 @@
"""
Gtk Bound Plugins Module
"""

View File

@@ -0,0 +1,94 @@
# Python imports
import os
import json
from os.path import join
from dataclasses import dataclass
# Lib imports
# Application imports
class ManifestProcessorException(Exception):
...
@dataclass(slots=True)
class PluginInfo:
path: str = None
name: str = None
author: str = None
version: str = None
support: str = None
requests:{} = None
reference: type = None
class ManifestProcessor:
def __init__(self, path, builder):
manifest = join(path, "manifest.json")
if not os.path.exists(manifest):
raise ManifestProcessorException("Invalid Plugin Structure: Plugin doesn't have 'manifest.json'. Aboarting load...")
self._path = path
self._builder = builder
with open(manifest) as f:
data = json.load(f)
self._manifest = data["manifest"]
self._plugin = self.collect_info()
def collect_info(self) -> PluginInfo:
plugin = PluginInfo()
plugin.path = self._path
plugin.name = self._manifest["name"]
plugin.author = self._manifest["author"]
plugin.version = self._manifest["version"]
plugin.support = self._manifest["support"]
plugin.requests = self._manifest["requests"]
return plugin
def get_loading_data(self):
loading_data = {}
requests = self._plugin.requests
keys = requests.keys()
if "ui_target" in keys:
if requests["ui_target"] in [
"none", "other", "main_Window", "main_menu_bar",
"main_menu_bttn_box_bar", "path_menu_bar", "plugin_control_list",
"context_menu", "context_menu_plugins", "window_1",
"window_2", "window_3", "window_4"
]:
if requests["ui_target"] == "other":
if "ui_target_id" in keys:
loading_data["ui_target"] = self._builder.get_object(requests["ui_target_id"])
if loading_data["ui_target"] == None:
raise ManifestProcessorException('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...')
else:
raise ManifestProcessorException('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...')
else:
loading_data["ui_target"] = self._builder.get_object(requests["ui_target"])
else:
raise ManifestProcessorException('Unknown "ui_target" given in requests.')
if "pass_fm_events" in keys:
if requests["pass_fm_events"] in ["true"]:
loading_data["pass_fm_events"] = True
if "pass_ui_objects" in keys:
if len(requests["pass_ui_objects"]) > 0:
loading_data["pass_ui_objects"] = []
for ui_id in requests["pass_ui_objects"]:
try:
loading_data["pass_ui_objects"].append( self._builder.get_object(ui_id) )
except ManifestProcessorException as e:
logger.error(repr(e))
if "bind_keys" in keys:
if isinstance(requests["bind_keys"], list):
loading_data["bind_keys"] = requests["bind_keys"]
return self._plugin, loading_data

View File

@@ -0,0 +1,96 @@
# Python imports
import os
import time
import inspect
# Lib imports
# Application imports
class PluginBaseException(Exception):
...
class PluginBase:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
# where self.name should not be needed for message comms
self._builder = None
self._ui_objects = None
self._fm_state = None
self._event_system = None
def run(self):
"""
Must define regardless if needed and can 'pass' if plugin doesn't need it.
Is intended to be used to setup internal signals or custom Gtk Builders/UI logic.
"""
raise PluginBaseException("Method hasn't been overriden...")
def generate_reference_ui_element(self):
"""
Requests Key: 'ui_target': "plugin_control_list",
Must define regardless if needed and can 'pass' if plugin doesn't use it.
Must return a widget if "ui_target" is set.
"""
raise PluginBaseException("Method hasn't been overriden...")
def set_ui_object_collection(self, ui_objects):
"""
Requests Key: "pass_ui_objects": [""]
Request reference to a UI component. Will be passed back as array to plugin.
Must define in plugin if set and an array of valid glade UI IDs is given.
"""
self._ui_objects = ui_objects
def set_fm_event_system(self, fm_event_system):
"""
Requests Key: 'pass_fm_events': "true"
Must define in plugin if "pass_fm_events" is set to "true" string.
"""
self._event_system = fm_event_system
def subscribe_to_events(self):
self._event_system.subscribe("update_state_info_plugins", self._update_fm_state_info)
def _update_fm_state_info(self, state):
self._fm_state = state
def _connect_builder_signals(self, caller_class, builder):
classes = [caller_class]
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
logger.debug(repr(e))
builder.connect_signals(handlers)
def reload_package(self, plugin_path, module_dict_main=locals()):
import importlib
from pathlib import Path
def reload_package_recursive(current_dir, module_dict):
for path in current_dir.iterdir():
if "__init__" in str(path) or path.stem not in module_dict:
continue
if path.is_file() and path.suffix == ".py":
importlib.reload(module_dict[path.stem])
elif path.is_dir():
reload_package_recursive(path, module_dict[path.stem].__dict__)
reload_package_recursive(Path(plugin_path).parent, module_dict_main["module_dict_main"])
def clear_children(self, widget: type) -> None:
""" Clear children of a gtk widget. """
for child in widget.get_children():
widget.remove(child)

View File

@@ -0,0 +1,130 @@
# Python imports
import os
import sys
import importlib
import traceback
from os.path import join
from os.path import isdir
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
# Application imports
from .manifest import PluginInfo
from .manifest import ManifestProcessor
class InvalidPluginException(Exception):
...
class PluginsController:
"""PluginsController controller"""
def __init__(self):
path = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, path) # NOTE: I think I'm not using this correctly...
self._builder = settings_manager.get_builder()
self._plugins_path = settings_manager.get_plugins_path()
self._plugins_dir_watcher = None
self._plugin_collection = []
def launch_plugins(self) -> None:
self._set_plugins_watcher()
self.load_plugins()
def _set_plugins_watcher(self) -> None:
self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ())
def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
self.reload_plugins(file)
@daemon_threaded
def load_plugins(self, file: str = None) -> None:
logger.info(f"Loading plugins...")
parent_path = os.getcwd()
for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]:
try:
target = join(path, "plugin.py")
manifest = ManifestProcessor(path, self._builder)
if not os.path.exists(target):
raise FileNotFoundError("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...")
plugin, loading_data = manifest.get_loading_data()
module = self.load_plugin_module(path, folder, target)
GLib.idle_add(self.execute_plugin, *(module, plugin, loading_data))
# self.execute_plugin(module, plugin, loading_data)
except InvalidPluginException as e:
logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !")
logger.debug("Trace: ", traceback.print_exc())
os.chdir(parent_path)
def load_plugin_module(self, path, folder, target):
os.chdir(path)
locations = []
self.collect_search_locations(path, locations)
spec = importlib.util.spec_from_file_location(folder, target, submodule_search_locations = locations)
module = importlib.util.module_from_spec(spec)
sys.modules[folder] = module
spec.loader.exec_module(module)
return module
def collect_search_locations(self, path, locations):
locations.append(path)
for file in os.listdir(path):
_path = os.path.join(path, file)
if os.path.isdir(_path):
self.collect_search_locations(_path, locations)
def execute_plugin(self, module: type, plugin: PluginInfo, loading_data: []):
plugin.reference = module.Plugin()
keys = loading_data.keys()
if "ui_target" in keys:
loading_data["ui_target"].add( plugin.reference.generate_reference_ui_element() )
loading_data["ui_target"].show_all()
if "pass_ui_objects" in keys:
plugin.reference.set_ui_object_collection( loading_data["pass_ui_objects"] )
if "pass_fm_events" in keys:
plugin.reference.set_fm_event_system(event_system)
plugin.reference.subscribe_to_events()
if "bind_keys" in keys:
keybindings.append_bindings( loading_data["bind_keys"] )
plugin.reference.run()
self._plugin_collection.append(plugin)
def reload_plugins(self, file: str = None) -> None:
logger.info(f"Reloading plugins...")
parent_path = os.getcwd()
for plugin in self._plugin_collection:
os.chdir(plugin.path)
plugin.reference.reload_package(f"{plugin.path}/plugin.py")
os.chdir(parent_path)

View File

@@ -0,0 +1,3 @@
"""
Root of ShellFM
"""

View File

@@ -0,0 +1,3 @@
"""
Window module
"""

View File

@@ -0,0 +1,180 @@
# Python imports
import threading
import json
from os import path
# Lib imports
# Application imports
from .window import Window
class WindowController:
def __init__(self):
USER_HOME: str = path.expanduser('~')
CONFIG_PATH: str = f"{USER_HOME}/.config/solarfm"
self._session_file: srr = f"{CONFIG_PATH}/session.json"
self._event_sleep_time: int = 1
self._active_window_id: str = ""
self._active_tab_id: str = ""
self._windows: list = []
def set_wid_and_tid(self, wid: int, tid: int) -> None:
self._active_window_id = str(wid)
self._active_tab_id = str(tid)
def get_active_wid_and_tid(self) -> list:
return self._active_window_id, self._active_tab_id
def create_window(self) -> Window:
window = Window()
window.set_nickname(f"window_{str(len(self._windows) + 1)}")
self._windows.append(window)
return window
def add_tab_for_window(self, win_id: str) -> None:
for window in self._windows:
if window.get_id() == win_id:
return window.create_tab()
def add_tab_for_window_by_name(self, name: str) -> None:
for window in self._windows:
if window.get_name() == name:
return window.create_tab()
def add_tab_for_window_by_nickname(self, nickname: str) -> None:
for window in self._windows:
if window.get_nickname() == nickname:
return window.create_tab()
def pop_window(self) -> None:
self._windows.pop()
def delete_window_by_id(self, win_id: str) -> None:
for window in self._windows:
if window.get_id() == win_id:
self._windows.remove(window)
break
def delete_window_by_name(self, name: str) -> str:
for window in self._windows:
if window.get_name() == name:
self._windows.remove(window)
break
def delete_window_by_nickname(self, nickname: str) -> str:
for window in self._windows:
if window.get_nickname() == nickname:
self._windows.remove(window)
break
def get_window_by_id(self, win_id: str) -> Window:
for window in self._windows:
if window.get_id() == win_id:
return window
raise(f"No Window by ID {win_id} found!")
def get_window_by_name(self, name: str) -> Window:
for window in self._windows:
if window.get_name() == name:
return window
raise(f"No Window by Name {name} found!")
def get_window_by_nickname(self, nickname: str) -> Window:
for window in self._windows:
if window.get_nickname() == nickname:
return window
raise(f"No Window by Nickname {nickname} found!")
def get_window_by_index(self, index: int) -> Window:
return self._windows[index]
def get_all_windows(self) -> list:
return self._windows
def set_window_nickname(self, win_id: str = None, nickname: str = "") -> None:
for window in self._windows:
if window.get_id() == win_id:
window.set_nickname(nickname)
def list_windows(self) -> None:
print("\n[ ---- Windows ---- ]\n")
for window in self._windows:
print(f"\nID: {window.get_id()}")
print(f"Name: {window.get_name()}")
print(f"Nickname: {window.get_nickname()}")
print(f"Is Hidden: {window.is_hidden()}")
print(f"Tab Count: {window.get_tabs_count()}")
print("\n-------------------------\n")
def list_files_from_tabs_of_window(self, win_id: str) -> None:
for window in self._windows:
if window.get_id() == win_id:
window.list_files_from_tabs()
break
def get_tabs_count(self, win_id: str) -> int:
for window in self._windows:
if window.get_id() == win_id:
return window.get_tabs_count()
def get_tabs_from_window(self, win_id: str) -> list:
for window in self._windows:
if window.get_id() == win_id:
return window.get_all_tabs()
def unload_tabs_and_windows(self) -> None:
for window in self._windows:
window.get_all_tabs().clear()
self._windows.clear()
def save_state(self, session_file: str = None) -> None:
if not session_file:
session_file = self._session_file
if len(self._windows) > 0:
windows = []
for window in self._windows:
tabs = []
for tab in window.get_all_tabs():
tabs.append(tab.get_current_directory())
windows.append(
{
'window':{
"ID": window.get_id(),
"Name": window.get_name(),
"Nickname": window.get_nickname(),
"isHidden": f"{window.is_hidden()}",
'tabs': tabs
}
}
)
with open(session_file, 'w') as outfile:
json.dump(windows, outfile, separators=(',', ':'), indent=4)
else:
raise Exception("Window data corrupted! Can not save session!")
def get_state_from_file(self, session_file: str = None) -> dict:
if not session_file:
session_file = self._session_file
if path.isfile(session_file):
with open(session_file) as infile:
return json.load(infile)

View File

@@ -0,0 +1,3 @@
"""
Tabs module
"""

View File

@@ -0,0 +1,3 @@
"""
Icons module
"""

View File

@@ -0,0 +1,204 @@
# Python imports
import os
from os.path import isfile
import hashlib
import threading
# Lib imports
import gi
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import GdkPixbuf
try:
from PIL import Image as PImage
except ModuleNotFoundError as e:
PImage = None
# Application imports
from .mixins.videoiconmixin import VideoIconMixin
from .mixins.meshsiconmixin import MeshsIconMixin
from .mixins.desktopiconmixin import DesktopIconMixin
class IconException(Exception):
...
class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin):
def create_icon(self, dir, file):
full_path = f"{dir}/{file}"
return self.get_icon_image(dir, file, full_path)
def get_icon_image(self, dir, file, full_path):
try:
thumbnl = None
if file.lower().endswith(self.fmeshs): # 3D Mesh icon
...
elif file.lower().endswith(self.fvideos): # Video icon
thumbnl = self.create_video_thumbnail(full_path)
elif file.lower().endswith(self.fimages): # Image Icon
thumbnl = self.create_scaled_image(full_path)
elif file.lower().endswith( (".blend",) ): # Blender icon
thumbnl = self.create_blender_thumbnail(full_path)
elif full_path.lower().endswith( ('.desktop',) ): # .desktop file parsing
thumbnl = self.find_thumbnail_from_desktop_file(full_path)
if not thumbnl:
# TODO: Detect if not in a thread and use directly for speed get_system_thumbnail
thumbnl = self.get_system_thumbnail(full_path, self.sys_icon_wh[0])
# thumbnl = self._get_system_thumbnail_gtk_thread(full_path, self.sys_icon_wh[0])
if not thumbnl:
raise IconException("No known icons found.")
return thumbnl
except IconException:
...
return self.get_generic_icon()
def create_blender_thumbnail(self, full_path, returnHashInstead=False):
try:
path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
if not path_exists:
self.generate_blender_thumbnail(full_path, hash_img_path)
if returnHashInstead:
return img_hash, hash_img_path
return self.create_scaled_image(hash_img_path, self.video_icon_wh)
except IconException as e:
print("Blender thumbnail generation issue:")
print( repr(e) )
return None
def create_video_thumbnail(self, full_path, scrub_percent = "65%", replace=False, returnHashInstead=False):
try:
path_exists, img_hash, hash_img_path = self.generate_hash_and_path(full_path)
if path_exists and replace:
os.remove(hash_img_path)
path_exists = False
if not path_exists:
self.generate_video_thumbnail(full_path, hash_img_path, scrub_percent)
if returnHashInstead:
return img_hash, hash_img_path
return self.create_scaled_image(hash_img_path, self.video_icon_wh)
except IconException as e:
print("Image/Video thumbnail generation issue:")
print( repr(e) )
return None
def create_scaled_image(self, full_path, wxh = None):
if not wxh:
wxh = self.video_icon_wh
if full_path:
try:
if full_path.lower().endswith(".gif"):
return GdkPixbuf.PixbufAnimation.new_from_file(full_path) \
.get_static_image() \
.scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR)
elif full_path.lower().endswith(".webp") and PImage:
return self.image2pixbuf(full_path, wxh)
pixbuf = None
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(full_path, wxh[0], wxh[1], True)
except Exception as e:
...
return pixbuf
except IconException as e:
print("Image Scaling Issue:")
print( repr(e) )
return None
def create_from_file(self, full_path):
try:
return GdkPixbuf.Pixbuf.new_from_file(full_path)
except IconException as e:
print("Image from file Issue:")
print( repr(e) )
return None
def _get_system_thumbnail_gtk_thread(self, full_path, size):
def _call_gtk_thread(event, result):
result.append( self.get_system_thumbnail(full_path, size) )
event.set()
result = []
event = threading.Event()
GLib.idle_add(_call_gtk_thread, event, result)
event.wait()
return result[0]
def get_system_thumbnail(self, full_path, size):
try:
gio_file = Gio.File.new_for_path(full_path)
info = gio_file.query_info('standard::icon' , 0, None)
icon = info.get_icon().get_names()[0]
data = settings_manager.get_icon_theme().lookup_icon(icon , size , 0)
if data:
icon_path = data.get_filename()
return GdkPixbuf.Pixbuf.new_from_file(icon_path)
raise IconException("No system icon found...")
except IconException:
...
return None
def get_generic_icon(self):
return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON)
def generate_hash_and_path(self, full_path):
img_hash = self.fast_hash(full_path)
hash_img_path = f"{self.ABS_THUMBS_PTH}/{img_hash}.jpg"
path_exists = True if isfile(hash_img_path) else False
return path_exists, img_hash, hash_img_path
def fast_hash(self, filename: str, hash_factory: callable = hashlib.md5, chunk_num_blocks: int = 128, i: int = 1) -> str:
h = hash_factory()
with open(filename,'rb') as f:
# NOTE: Jump to middle of file
f.seek(0, 2)
mid = int(f.tell() / 2)
f.seek(mid, 0)
while chunk := f.read(chunk_num_blocks * h.block_size):
h.update(chunk)
if (i == 12):
break
i += 1
return h.hexdigest()
def image2pixbuf(self, full_path, wxh):
"""Convert Pillow image to GdkPixbuf"""
im = PImage.open(full_path)
data = im.tobytes()
data = GLib.Bytes.new(data)
w, h = im.size
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
False, 8, w, h, w * 3)
return pixbuf.scale_simple(wxh[0], wxh[1], 2) # BILINEAR = 2

View File

@@ -0,0 +1,3 @@
"""
Icons mixins module
"""

View File

@@ -0,0 +1,76 @@
# Python imports
import os
from os.path import isfile
import subprocess
import hashlib
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gio
# Application imports
from .xdg.DesktopEntry import DesktopEntry
class DesktopIconMixin:
def find_thumbnail_from_desktop_file(self, full_path):
try:
xdgObj = DesktopEntry(full_path)
icon = xdgObj.getIcon()
alt_icon_path = ""
if "steam" in icon:
return self.get_steam_img(xdgObj)
elif os.path.exists(icon):
return self.create_scaled_image(icon, self.sys_icon_wh)
else:
pixbuf, alt_icon_path = self.get_icon_from_traversal(icon)
if pixbuf:
return pixbuf
return self.create_scaled_image(alt_icon_path, self.sys_icon_wh)
except Exception as e:
print(".desktop icon generation issue:")
print( repr(e) )
return None
def get_steam_img(self, xdgObj):
name = xdgObj.getName()
file_hash = hashlib.sha256(str.encode(name)).hexdigest()
hash_img_pth = f"{self.STEAM_ICONS_PTH}/{file_hash}.jpg"
if not isfile(hash_img_pth):
exec_str = xdgObj.getExec()
parts = exec_str.split("steam://rungameid/")
id = parts[len(parts) - 1]
imageLink = f"{self.STEAM_CDN_URL}{id}/header.jpg"
proc = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink])
proc.wait()
return self.create_scaled_image(hash_img_pth, self.video_icon_wh)
def get_icon_from_traversal(self, icon):
gio_icon = Gio.Icon.new_for_string(icon)
gicon = Gtk.Image.new_from_gicon(gio_icon, 32)
pixbuf = gicon.get_pixbuf()
alt_icon_path = ""
for dir in self.ICON_DIRS:
alt_icon_path = self.traverse_icons_folder(dir, icon)
if alt_icon_path != "":
break
return pixbuf, alt_icon_path
def traverse_icons_folder(self, path, icon):
for (dirpath, dirnames, filenames) in os.walk(path):
for file in filenames:
appNM = "application-x-" + icon
if icon in file or appNM in file:
return f"{dirpath}/{file}"

View File

@@ -0,0 +1,17 @@
# Python imports
import subprocess
# Lib imports
# Application imports
class MeshsIconMixin:
def generate_blender_thumbnail(self, full_path, hash_img_path):
try:
proc = subprocess.Popen([self.BLENDER_THUMBNLR, full_path, hash_img_path])
proc.wait()
except Exception as e:
self.logger.debug(repr(e))

View File

@@ -0,0 +1,55 @@
# Python imports
import subprocess
# Lib imports
# Application imports
class VideoIconMixin:
def generate_video_thumbnail(self, full_path, hash_img_path, scrub_percent = "65%"):
try:
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", scrub_percent, "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_path])
proc.wait()
except Exception as e:
self.logger.debug(repr(e))
self.ffprobe_generate_video_thumbnail(full_path, hash_img_path)
def ffprobe_generate_video_thumbnail(self, full_path, hash_img_path):
proc = None
try:
# Stream duration
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
data = subprocess.run(command, stdout=subprocess.PIPE)
duration = data.stdout.decode('utf-8')
# Format (container) duration
if "N/A" in duration:
command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
data = subprocess.run(command , stdout=subprocess.PIPE)
duration = data.stdout.decode('utf-8')
# Stream duration type: image2
if "N/A" in duration:
command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-f", "image2", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
data = subprocess.run(command, stdout=subprocess.PIPE)
duration = data.stdout.decode('utf-8')
# Format (container) duration type: image2
if "N/A" in duration:
command = ["ffprobe", "-v", "error", "-f", "image2", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", full_path]
data = subprocess.run(command , stdout=subprocess.PIPE)
duration = data.stdout.decode('utf-8')
# Get frame roughly 35% through video
grabTime = str( int( float( duration.split(".")[0] ) * 0.35) )
command = ["ffmpeg", "-ss", grabTime, "-an", "-i", full_path, "-s", "320x180", "-vframes", "1", hash_img_path]
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
proc.wait()
except Exception as e:
print("Video thumbnail generation issue in thread:")
print( repr(e) )
self.logger.debug(repr(e))

View File

@@ -0,0 +1,160 @@
"""
This module is based on a rox module (LGPL):
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log
The freedesktop.org Base Directory specification provides a way for
applications to locate shared data and configuration:
http://standards.freedesktop.org/basedir-spec/
(based on version 0.6)
This module can be used to load and save from and to these directories.
Typical usage:
from rox import basedir
for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
print "Load settings from", dir
dir = basedir.save_config_path('mydomain.org', 'MyProg')
print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"
Note: see the rox.Options module for a higher-level API for managing options.
"""
import os, stat
_home = os.path.expanduser('~')
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
os.path.join(_home, '.local', 'share')
xdg_data_dirs = [xdg_data_home] + \
(os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':')
xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
os.path.join(_home, '.config')
xdg_config_dirs = [xdg_config_home] + \
(os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':')
xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
os.path.join(_home, '.cache')
xdg_data_dirs = [x for x in xdg_data_dirs if x]
xdg_config_dirs = [x for x in xdg_config_dirs if x]
def save_config_path(*resource):
"""Ensure ``$XDG_CONFIG_HOME/<resource>/`` exists, and return its path.
'resource' should normally be the name of your application. Use this
when saving configuration settings.
"""
resource = os.path.join(*resource)
assert not resource.startswith('/')
path = os.path.join(xdg_config_home, resource)
if not os.path.isdir(path):
os.makedirs(path, 0o700)
return path
def save_data_path(*resource):
"""Ensure ``$XDG_DATA_HOME/<resource>/`` exists, and return its path.
'resource' should normally be the name of your application or a shared
resource. Use this when saving or updating application data.
"""
resource = os.path.join(*resource)
assert not resource.startswith('/')
path = os.path.join(xdg_data_home, resource)
if not os.path.isdir(path):
os.makedirs(path)
return path
def save_cache_path(*resource):
"""Ensure ``$XDG_CACHE_HOME/<resource>/`` exists, and return its path.
'resource' should normally be the name of your application or a shared
resource."""
resource = os.path.join(*resource)
assert not resource.startswith('/')
path = os.path.join(xdg_cache_home, resource)
if not os.path.isdir(path):
os.makedirs(path)
return path
def load_config_paths(*resource):
"""Returns an iterator which gives each directory named 'resource' in the
configuration search path. Information provided by earlier directories should
take precedence over later ones, and the user-specific config dir comes
first."""
resource = os.path.join(*resource)
for config_dir in xdg_config_dirs:
path = os.path.join(config_dir, resource)
if os.path.exists(path): yield path
def load_first_config(*resource):
"""Returns the first result from load_config_paths, or None if there is nothing
to load."""
for x in load_config_paths(*resource):
return x
return None
def load_data_paths(*resource):
"""Returns an iterator which gives each directory named 'resource' in the
application data search path. Information provided by earlier directories
should take precedence over later ones."""
resource = os.path.join(*resource)
for data_dir in xdg_data_dirs:
path = os.path.join(data_dir, resource)
if os.path.exists(path): yield path
def get_runtime_dir(strict=True):
"""Returns the value of $XDG_RUNTIME_DIR, a directory path.
This directory is intended for 'user-specific non-essential runtime files
and other file objects (such as sockets, named pipes, ...)', and
'communication and synchronization purposes'.
As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not
set, with ``strict=True`` (the default), a KeyError is raised. With
``strict=False``, PyXDG will create a fallback under /tmp for the current
user. This fallback does *not* provide the same guarantees as the
specification requires for the runtime directory.
The strict default is deliberately conservative, so that application
developers can make a conscious decision to allow the fallback.
"""
try:
return os.environ['XDG_RUNTIME_DIR']
except KeyError:
if strict:
raise
import getpass
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
create = False
try:
# This must be a real directory, not a symlink, so attackers can't
# point it elsewhere. So we use lstat to check it.
st = os.lstat(fallback)
except OSError as e:
import errno
if e.errno == errno.ENOENT:
create = True
else:
raise
else:
# The fallback must be a directory
if not stat.S_ISDIR(st.st_mode):
os.unlink(fallback)
create = True
# Must be owned by the user and not accessible by anyone else
elif (st.st_uid != os.getuid()) \
or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
os.rmdir(fallback)
create = True
if create:
os.mkdir(fallback, 0o700)
return fallback

View File

@@ -0,0 +1,39 @@
"""
Functions to configure Basic Settings
"""
language = "C"
windowmanager = None
icon_theme = "hicolor"
icon_size = 48
cache_time = 5
root_mode = False
def setWindowManager(wm):
global windowmanager
windowmanager = wm
def setIconTheme(theme):
global icon_theme
icon_theme = theme
import xdg.IconTheme
xdg.IconTheme.themes = []
def setIconSize(size):
global icon_size
icon_size = size
def setCacheTime(time):
global cache_time
cache_time = time
def setLocale(lang):
import locale
lang = locale.normalize(lang)
locale.setlocale(locale.LC_ALL, lang)
import xdg.Locale
xdg.Locale.update(lang)
def setRootMode(boolean):
global root_mode
root_mode = boolean

View File

@@ -0,0 +1,435 @@
"""
Complete implementation of the XDG Desktop Entry Specification
http://standards.freedesktop.org/desktop-entry-spec/
Not supported:
- Encoding: Legacy Mixed
- Does not check exec parameters
- Does not check URL's
- Does not completly validate deprecated/kde items
- Does not completly check categories
"""
from .IniFile import IniFile
from . import Locale
from .IniFile import is_ascii
from .Exceptions import ParsingError
from .util import which
import os.path
import re
import warnings
class DesktopEntry(IniFile):
"Class to parse and validate Desktop Entries"
defaultGroup = 'Desktop Entry'
def __init__(self, filename=None):
"""Create a new DesktopEntry.
If filename exists, it will be parsed as a desktop entry file. If not,
or if filename is None, a blank DesktopEntry is created.
"""
self.content = dict()
if filename and os.path.exists(filename):
self.parse(filename)
elif filename:
self.new(filename)
def __str__(self):
return self.getName()
def parse(self, file):
"""Parse a desktop entry file.
This can raise :class:`~xdg.Exceptions.ParsingError`,
:class:`~xdg.Exceptions.DuplicateGroupError` or
:class:`~xdg.Exceptions.DuplicateKeyError`.
"""
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
def findTryExec(self):
"""Looks in the PATH for the executable given in the TryExec field.
Returns the full path to the executable if it is found, None if not.
Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present.
"""
tryexec = self.get('TryExec', strict=True)
return which(tryexec)
# start standard keys
def getType(self):
return self.get('Type')
def getVersion(self):
"""deprecated, use getVersionString instead """
return self.get('Version', type="numeric")
def getVersionString(self):
return self.get('Version')
def getName(self):
return self.get('Name', locale=True)
def getGenericName(self):
return self.get('GenericName', locale=True)
def getNoDisplay(self):
return self.get('NoDisplay', type="boolean")
def getComment(self):
return self.get('Comment', locale=True)
def getIcon(self):
return self.get('Icon', locale=True)
def getHidden(self):
return self.get('Hidden', type="boolean")
def getOnlyShowIn(self):
return self.get('OnlyShowIn', list=True)
def getNotShowIn(self):
return self.get('NotShowIn', list=True)
def getTryExec(self):
return self.get('TryExec')
def getExec(self):
return self.get('Exec')
def getPath(self):
return self.get('Path')
def getTerminal(self):
return self.get('Terminal', type="boolean")
def getMimeType(self):
"""deprecated, use getMimeTypes instead """
return self.get('MimeType', list=True, type="regex")
def getMimeTypes(self):
return self.get('MimeType', list=True)
def getCategories(self):
return self.get('Categories', list=True)
def getStartupNotify(self):
return self.get('StartupNotify', type="boolean")
def getStartupWMClass(self):
return self.get('StartupWMClass')
def getURL(self):
return self.get('URL')
# end standard keys
# start kde keys
def getServiceTypes(self):
return self.get('ServiceTypes', list=True)
def getDocPath(self):
return self.get('DocPath')
def getKeywords(self):
return self.get('Keywords', list=True, locale=True)
def getInitialPreference(self):
return self.get('InitialPreference')
def getDev(self):
return self.get('Dev')
def getFSType(self):
return self.get('FSType')
def getMountPoint(self):
return self.get('MountPoint')
def getReadonly(self):
return self.get('ReadOnly', type="boolean")
def getUnmountIcon(self):
return self.get('UnmountIcon', locale=True)
# end kde keys
# start deprecated keys
def getMiniIcon(self):
return self.get('MiniIcon', locale=True)
def getTerminalOptions(self):
return self.get('TerminalOptions')
def getDefaultApp(self):
return self.get('DefaultApp')
def getProtocols(self):
return self.get('Protocols', list=True)
def getExtensions(self):
return self.get('Extensions', list=True)
def getBinaryPattern(self):
return self.get('BinaryPattern')
def getMapNotify(self):
return self.get('MapNotify')
def getEncoding(self):
return self.get('Encoding')
def getSwallowTitle(self):
return self.get('SwallowTitle', locale=True)
def getSwallowExec(self):
return self.get('SwallowExec')
def getSortOrder(self):
return self.get('SortOrder', list=True)
def getFilePattern(self):
return self.get('FilePattern', type="regex")
def getActions(self):
return self.get('Actions', list=True)
# end deprecated keys
# desktop entry edit stuff
def new(self, filename):
"""Make this instance into a new, blank desktop entry.
If filename has a .desktop extension, Type is set to Application. If it
has a .directory extension, Type is Directory. Other extensions will
cause :class:`~xdg.Exceptions.ParsingError` to be raised.
"""
if os.path.splitext(filename)[1] == ".desktop":
type = "Application"
elif os.path.splitext(filename)[1] == ".directory":
type = "Directory"
else:
raise ParsingError("Unknown extension", filename)
self.content = dict()
self.addGroup(self.defaultGroup)
self.set("Type", type)
self.filename = filename
# end desktop entry edit stuff
# validation stuff
def checkExtras(self):
# header
if self.defaultGroup == "KDE Desktop Entry":
self.warnings.append('[KDE Desktop Entry]-Header is deprecated')
# file extension
if self.fileExtension == ".kdelnk":
self.warnings.append("File extension .kdelnk is deprecated")
elif self.fileExtension != ".desktop" and self.fileExtension != ".directory":
self.warnings.append('Unknown File extension')
# Type
try:
self.type = self.content[self.defaultGroup]["Type"]
except KeyError:
self.errors.append("Key 'Type' is missing")
# Name
try:
self.name = self.content[self.defaultGroup]["Name"]
except KeyError:
self.errors.append("Key 'Name' is missing")
def checkGroup(self, group):
# check if group header is valid
if not (group == self.defaultGroup \
or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \
or (re.match("^X-", group) and is_ascii(group))):
self.errors.append("Invalid Group name: %s" % group)
else:
#OnlyShowIn and NotShowIn
if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]):
self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both")
def checkKey(self, key, value, group):
# standard keys
if key == "Type":
if value == "ServiceType" or value == "Service" or value == "FSDevice":
self.warnings.append("Type=%s is a KDE extension" % key)
elif value == "MimeType":
self.warnings.append("Type=MimeType is deprecated")
elif not (value == "Application" or value == "Link" or value == "Directory"):
self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value)
if self.fileExtension == ".directory" and not value == "Directory":
self.warnings.append("File extension is .directory, but Type is '%s'" % value)
elif self.fileExtension == ".desktop" and value == "Directory":
self.warnings.append("Files with Type=Directory should have the extension .directory")
if value == "Application":
if "Exec" not in self.content[group]:
self.warnings.append("Type=Application needs 'Exec' key")
if value == "Link":
if "URL" not in self.content[group]:
self.warnings.append("Type=Link needs 'URL' key")
elif key == "Version":
self.checkValue(key, value)
elif re.match("^Name"+xdg.Locale.regex+"$", key):
pass # locale string
elif re.match("^GenericName"+xdg.Locale.regex+"$", key):
pass # locale string
elif key == "NoDisplay":
self.checkValue(key, value, type="boolean")
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
pass # locale string
elif re.match("^Icon"+xdg.Locale.regex+"$", key):
self.checkValue(key, value)
elif key == "Hidden":
self.checkValue(key, value, type="boolean")
elif key == "OnlyShowIn":
self.checkValue(key, value, list=True)
self.checkOnlyShowIn(value)
elif key == "NotShowIn":
self.checkValue(key, value, list=True)
self.checkOnlyShowIn(value)
elif key == "TryExec":
self.checkValue(key, value)
self.checkType(key, "Application")
elif key == "Exec":
self.checkValue(key, value)
self.checkType(key, "Application")
elif key == "Path":
self.checkValue(key, value)
self.checkType(key, "Application")
elif key == "Terminal":
self.checkValue(key, value, type="boolean")
self.checkType(key, "Application")
elif key == "Actions":
self.checkValue(key, value, list=True)
self.checkType(key, "Application")
elif key == "MimeType":
self.checkValue(key, value, list=True)
self.checkType(key, "Application")
elif key == "Categories":
self.checkValue(key, value)
self.checkType(key, "Application")
self.checkCategories(value)
elif re.match("^Keywords"+xdg.Locale.regex+"$", key):
self.checkValue(key, value, type="localestring", list=True)
self.checkType(key, "Application")
elif key == "StartupNotify":
self.checkValue(key, value, type="boolean")
self.checkType(key, "Application")
elif key == "StartupWMClass":
self.checkType(key, "Application")
elif key == "URL":
self.checkValue(key, value)
self.checkType(key, "URL")
# kde extensions
elif key == "ServiceTypes":
self.checkValue(key, value, list=True)
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "DocPath":
self.checkValue(key, value)
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "InitialPreference":
self.checkValue(key, value, type="numeric")
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "Dev":
self.checkValue(key, value)
self.checkType(key, "FSDevice")
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "FSType":
self.checkValue(key, value)
self.checkType(key, "FSDevice")
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "MountPoint":
self.checkValue(key, value)
self.checkType(key, "FSDevice")
self.warnings.append("Key '%s' is a KDE extension" % key)
elif key == "ReadOnly":
self.checkValue(key, value, type="boolean")
self.checkType(key, "FSDevice")
self.warnings.append("Key '%s' is a KDE extension" % key)
elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key):
self.checkValue(key, value)
self.checkType(key, "FSDevice")
self.warnings.append("Key '%s' is a KDE extension" % key)
# deprecated keys
elif key == "Encoding":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key):
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "TerminalOptions":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "DefaultApp":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "Protocols":
self.checkValue(key, value, list=True)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "Extensions":
self.checkValue(key, value, list=True)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "BinaryPattern":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "MapNotify":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key):
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "SwallowExec":
self.checkValue(key, value)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "FilePattern":
self.checkValue(key, value, type="regex", list=True)
self.warnings.append("Key '%s' is deprecated" % key)
elif key == "SortOrder":
self.checkValue(key, value, list=True)
self.warnings.append("Key '%s' is deprecated" % key)
# "X-" extensions
elif re.match("^X-[a-zA-Z0-9-]+", key):
pass
else:
self.errors.append("Invalid key: %s" % key)
def checkType(self, key, type):
if not self.getType() == type:
self.errors.append("Key '%s' only allowed in Type=%s" % (key, type))
def checkOnlyShowIn(self, value):
values = self.getList(value)
valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity",
"XFCE", "Old"]
for item in values:
if item not in valid and item[0:2] != "X-":
self.errors.append("'%s' is not a registered OnlyShowIn value" % item);
def checkCategories(self, value):
values = self.getList(value)
main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"]
if not any(item in main for item in values):
self.errors.append("Missing main category")
additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly']
allcategories = additional + main
for item in values:
if item not in allcategories and not item.startswith("X-"):
self.errors.append("'%s' is not a registered Category" % item);
def checkCategorie(self, value):
"""Deprecated alias for checkCategories - only exists for backwards
compatibility.
"""
warnings.warn("checkCategorie is deprecated, use checkCategories",
DeprecationWarning)
return self.checkCategories(value)

View File

@@ -0,0 +1,84 @@
"""
Exception Classes for the xdg package
"""
debug = False
class Error(Exception):
"""Base class for exceptions defined here."""
def __init__(self, msg):
self.msg = msg
Exception.__init__(self, msg)
def __str__(self):
return self.msg
class ValidationError(Error):
"""Raised when a file fails to validate.
The filename is the .file attribute.
"""
def __init__(self, msg, file):
self.msg = msg
self.file = file
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
class ParsingError(Error):
"""Raised when a file cannot be parsed.
The filename is the .file attribute.
"""
def __init__(self, msg, file):
self.msg = msg
self.file = file
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
class NoKeyError(Error):
"""Raised when trying to access a nonexistant key in an INI-style file.
Attributes are .key, .group and .file.
"""
def __init__(self, key, group, file):
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
self.key = key
self.group = group
self.file = file
class DuplicateKeyError(Error):
"""Raised when the same key occurs twice in an INI-style file.
Attributes are .key, .group and .file.
"""
def __init__(self, key, group, file):
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
self.key = key
self.group = group
self.file = file
class NoGroupError(Error):
"""Raised when trying to access a nonexistant group in an INI-style file.
Attributes are .group and .file.
"""
def __init__(self, group, file):
Error.__init__(self, "No group: %s in file %s" % (group, file))
self.group = group
self.file = file
class DuplicateGroupError(Error):
"""Raised when the same key occurs twice in an INI-style file.
Attributes are .group and .file.
"""
def __init__(self, group, file):
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
self.group = group
self.file = file
class NoThemeError(Error):
"""Raised when trying to access a nonexistant icon theme.
The name of the theme is the .theme attribute.
"""
def __init__(self, theme):
Error.__init__(self, "No such icon-theme: %s" % theme)
self.theme = theme

View File

@@ -0,0 +1,443 @@
"""
Complete implementation of the XDG Icon Spec
http://standards.freedesktop.org/icon-theme-spec/
"""
import os, time
import re
from . import IniFile, Config
from .IniFile import is_ascii
from .BaseDirectory import xdg_data_dirs
from .Exceptions import NoThemeError, debug
class IconTheme(IniFile):
"Class to parse and validate IconThemes"
def __init__(self):
IniFile.__init__(self)
def __repr__(self):
return self.name
def parse(self, file):
IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"])
self.dir = os.path.dirname(file)
(nil, self.name) = os.path.split(self.dir)
def getDir(self):
return self.dir
# Standard Keys
def getName(self):
return self.get('Name', locale=True)
def getComment(self):
return self.get('Comment', locale=True)
def getInherits(self):
return self.get('Inherits', list=True)
def getDirectories(self):
return self.get('Directories', list=True)
def getScaledDirectories(self):
return self.get('ScaledDirectories', list=True)
def getHidden(self):
return self.get('Hidden', type="boolean")
def getExample(self):
return self.get('Example')
# Per Directory Keys
def getSize(self, directory):
return self.get('Size', type="integer", group=directory)
def getContext(self, directory):
return self.get('Context', group=directory)
def getType(self, directory):
value = self.get('Type', group=directory)
if value:
return value
else:
return "Threshold"
def getMaxSize(self, directory):
value = self.get('MaxSize', type="integer", group=directory)
if value or value == 0:
return value
else:
return self.getSize(directory)
def getMinSize(self, directory):
value = self.get('MinSize', type="integer", group=directory)
if value or value == 0:
return value
else:
return self.getSize(directory)
def getThreshold(self, directory):
value = self.get('Threshold', type="integer", group=directory)
if value or value == 0:
return value
else:
return 2
def getScale(self, directory):
value = self.get('Scale', type="integer", group=directory)
return value or 1
# validation stuff
def checkExtras(self):
# header
if self.defaultGroup == "KDE Icon Theme":
self.warnings.append('[KDE Icon Theme]-Header is deprecated')
# file extension
if self.fileExtension == ".theme":
pass
elif self.fileExtension == ".desktop":
self.warnings.append('.desktop fileExtension is deprecated')
else:
self.warnings.append('Unknown File extension')
# Check required keys
# Name
try:
self.name = self.content[self.defaultGroup]["Name"]
except KeyError:
self.errors.append("Key 'Name' is missing")
# Comment
try:
self.comment = self.content[self.defaultGroup]["Comment"]
except KeyError:
self.errors.append("Key 'Comment' is missing")
# Directories
try:
self.directories = self.content[self.defaultGroup]["Directories"]
except KeyError:
self.errors.append("Key 'Directories' is missing")
def checkGroup(self, group):
# check if group header is valid
if group == self.defaultGroup:
try:
self.name = self.content[group]["Name"]
except KeyError:
self.errors.append("Key 'Name' in Group '%s' is missing" % group)
try:
self.name = self.content[group]["Comment"]
except KeyError:
self.errors.append("Key 'Comment' in Group '%s' is missing" % group)
elif group in self.getDirectories():
try:
self.type = self.content[group]["Type"]
except KeyError:
self.type = "Threshold"
try:
self.name = self.content[group]["Size"]
except KeyError:
self.errors.append("Key 'Size' in Group '%s' is missing" % group)
elif not (re.match(r"^\[X-", group) and is_ascii(group)):
self.errors.append("Invalid Group name: %s" % group)
def checkKey(self, key, value, group):
# standard keys
if group == self.defaultGroup:
if re.match("^Name"+xdg.Locale.regex+"$", key):
pass
elif re.match("^Comment"+xdg.Locale.regex+"$", key):
pass
elif key == "Inherits":
self.checkValue(key, value, list=True)
elif key == "Directories":
self.checkValue(key, value, list=True)
elif key == "ScaledDirectories":
self.checkValue(key, value, list=True)
elif key == "Hidden":
self.checkValue(key, value, type="boolean")
elif key == "Example":
self.checkValue(key, value)
elif re.match("^X-[a-zA-Z0-9-]+", key):
pass
else:
self.errors.append("Invalid key: %s" % key)
elif group in self.getDirectories():
if key == "Size":
self.checkValue(key, value, type="integer")
elif key == "Context":
self.checkValue(key, value)
elif key == "Type":
self.checkValue(key, value)
if value not in ["Fixed", "Scalable", "Threshold"]:
self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value)
elif key == "MaxSize":
self.checkValue(key, value, type="integer")
if self.type != "Scalable":
self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type)
elif key == "MinSize":
self.checkValue(key, value, type="integer")
if self.type != "Scalable":
self.errors.append("Key 'MinSize' give, but Type is %s" % self.type)
elif key == "Threshold":
self.checkValue(key, value, type="integer")
if self.type != "Threshold":
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
elif key == "Scale":
self.checkValue(key, value, type="integer")
elif re.match("^X-[a-zA-Z0-9-]+", key):
pass
else:
self.errors.append("Invalid key: %s" % key)
class IconData(IniFile):
"Class to parse and validate IconData Files"
def __init__(self):
IniFile.__init__(self)
def __repr__(self):
displayname = self.getDisplayName()
if displayname:
return "<IconData: %s>" % displayname
else:
return "<IconData>"
def parse(self, file):
IniFile.parse(self, file, ["Icon Data"])
# Standard Keys
def getDisplayName(self):
"""Retrieve the display name from the icon data, if one is specified."""
return self.get('DisplayName', locale=True)
def getEmbeddedTextRectangle(self):
"""Retrieve the embedded text rectangle from the icon data as a list of
numbers (x0, y0, x1, y1), if it is specified."""
return self.get('EmbeddedTextRectangle', type="integer", list=True)
def getAttachPoints(self):
"""Retrieve the anchor points for overlays & emblems from the icon data,
as a list of co-ordinate pairs, if they are specified."""
return self.get('AttachPoints', type="point", list=True)
# validation stuff
def checkExtras(self):
# file extension
if self.fileExtension != ".icon":
self.warnings.append('Unknown File extension')
def checkGroup(self, group):
# check if group header is valid
if not (group == self.defaultGroup \
or (re.match(r"^\[X-", group) and is_ascii(group))):
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
def checkKey(self, key, value, group):
# standard keys
if re.match("^DisplayName"+xdg.Locale.regex+"$", key):
pass
elif key == "EmbeddedTextRectangle":
self.checkValue(key, value, type="integer", list=True)
elif key == "AttachPoints":
self.checkValue(key, value, type="point", list=True)
elif re.match("^X-[a-zA-Z0-9-]+", key):
pass
else:
self.errors.append("Invalid key: %s" % key)
icondirs = []
for basedir in xdg_data_dirs:
icondirs.append(os.path.join(basedir, "icons"))
icondirs.append(os.path.join(basedir, "pixmaps"))
icondirs.append(os.path.expanduser("~/.icons"))
# just cache variables, they give a 10x speed improvement
themes = []
theme_cache = {}
dir_cache = {}
icon_cache = {}
def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]):
"""Get the path to a specified icon.
size :
Icon size in pixels. Defaults to ``xdg.Config.icon_size``.
theme :
Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't
found in the specified theme, it will be looked up in the basic 'hicolor'
theme.
extensions :
List of preferred file extensions.
Example::
>>> getIconPath("inkscape", 32)
'/usr/share/icons/hicolor/32x32/apps/inkscape.png'
"""
global themes
if size == None:
size = xdg.Config.icon_size
if theme == None:
theme = xdg.Config.icon_theme
# if we have an absolute path, just return it
if os.path.isabs(iconname):
return iconname
# check if it has an extension and strip it
if os.path.splitext(iconname)[1][1:] in extensions:
iconname = os.path.splitext(iconname)[0]
# parse theme files
if (themes == []) or (themes[0].name != theme):
themes = list(__get_themes(theme))
# more caching (icon looked up in the last 5 seconds?)
tmp = (iconname, size, theme, tuple(extensions))
try:
timestamp, icon = icon_cache[tmp]
except KeyError:
pass
else:
if (time.time() - timestamp) >= xdg.Config.cache_time:
del icon_cache[tmp]
else:
return icon
for thme in themes:
icon = LookupIcon(iconname, size, thme, extensions)
if icon:
icon_cache[tmp] = (time.time(), icon)
return icon
# cache stuff again (directories looked up in the last 5 seconds?)
for directory in icondirs:
if (directory not in dir_cache \
or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \
and dir_cache[directory][2] < os.path.getmtime(directory))) \
and os.path.isdir(directory):
dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory))
for dir, values in dir_cache.items():
for extension in extensions:
try:
if iconname + "." + extension in values[0]:
icon = os.path.join(dir, iconname + "." + extension)
icon_cache[tmp] = [time.time(), icon]
return icon
except UnicodeDecodeError as e:
...
# we haven't found anything? "hicolor" is our fallback
if theme != "hicolor":
icon = getIconPath(iconname, size, "hicolor")
icon_cache[tmp] = [time.time(), icon]
return icon
def getIconData(path):
"""Retrieve the data from the .icon file corresponding to the given file. If
there is no .icon file, it returns None.
Example::
getIconData("/usr/share/icons/Tango/scalable/places/folder.svg")
"""
if os.path.isfile(path):
icon_file = os.path.splitext(path)[0] + ".icon"
if os.path.isfile(icon_file):
data = IconData()
data.parse(icon_file)
return data
def __get_themes(themename):
"""Generator yielding IconTheme objects for a specified theme and any themes
from which it inherits.
"""
for dir in icondirs:
theme_file = os.path.join(dir, themename, "index.theme")
if os.path.isfile(theme_file):
break
theme_file = os.path.join(dir, themename, "index.desktop")
if os.path.isfile(theme_file):
break
else:
if debug:
raise NoThemeError(themename)
return
theme = IconTheme()
theme.parse(theme_file)
yield theme
for subtheme in theme.getInherits():
for t in __get_themes(subtheme):
yield t
def LookupIcon(iconname, size, theme, extensions):
# look for the cache
if theme.name not in theme_cache:
theme_cache[theme.name] = []
theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup
theme_cache[theme.name].append(0) # [1] mtime
theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]]
# cache stuff (directory lookuped up the in the last 5 seconds?)
if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time:
theme_cache[theme.name][0] = time.time()
for subdir in theme.getDirectories():
for directory in icondirs:
dir = os.path.join(directory,theme.name,subdir)
if (dir not in theme_cache[theme.name][2] \
or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \
and subdir != "" \
and os.path.isdir(dir):
theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)]
theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name))
for dir, values in theme_cache[theme.name][2].items():
if DirectoryMatchesSize(values[0], size, theme):
for extension in extensions:
if iconname + "." + extension in values[1]:
return os.path.join(dir, iconname + "." + extension)
minimal_size = 2**31
closest_filename = ""
for dir, values in theme_cache[theme.name][2].items():
distance = DirectorySizeDistance(values[0], size, theme)
if distance < minimal_size:
for extension in extensions:
if iconname + "." + extension in values[1]:
closest_filename = os.path.join(dir, iconname + "." + extension)
minimal_size = distance
return closest_filename
def DirectoryMatchesSize(subdir, iconsize, theme):
Type = theme.getType(subdir)
Size = theme.getSize(subdir)
Threshold = theme.getThreshold(subdir)
MinSize = theme.getMinSize(subdir)
MaxSize = theme.getMaxSize(subdir)
if Type == "Fixed":
return Size == iconsize
elif Type == "Scaleable":
return MinSize <= iconsize <= MaxSize
elif Type == "Threshold":
return Size - Threshold <= iconsize <= Size + Threshold
def DirectorySizeDistance(subdir, iconsize, theme):
Type = theme.getType(subdir)
Size = theme.getSize(subdir)
Threshold = theme.getThreshold(subdir)
MinSize = theme.getMinSize(subdir)
MaxSize = theme.getMaxSize(subdir)
if Type == "Fixed":
return abs(Size - iconsize)
elif Type == "Scalable":
if iconsize < MinSize:
return MinSize - iconsize
elif iconsize > MaxSize:
return MaxSize - iconsize
return 0
elif Type == "Threshold":
if iconsize < Size - Threshold:
return MinSize - iconsize
elif iconsize > Size + Threshold:
return iconsize - MaxSize
return 0

View File

@@ -0,0 +1,419 @@
"""
Base Class for DesktopEntry, IconTheme and IconData
"""
import re, os, stat, io
from .Exceptions import (ParsingError, DuplicateGroupError, NoGroupError,
NoKeyError, DuplicateKeyError, ValidationError,
debug)
# import xdg.Locale
from . import Locale
from .util import u
def is_ascii(s):
"""Return True if a string consists entirely of ASCII characters."""
try:
s.encode('ascii', 'strict')
return True
except UnicodeError:
return False
class IniFile:
defaultGroup = ''
fileExtension = ''
filename = ''
tainted = False
def __init__(self, filename=None):
self.content = dict()
if filename:
self.parse(filename)
def __cmp__(self, other):
return cmp(self.content, other.content)
def parse(self, filename, headers=None):
'''Parse an INI file.
headers -- list of headers the parser will try to select as a default header
'''
# for performance reasons
content = self.content
if not os.path.isfile(filename):
raise ParsingError("File not found", filename)
try:
# The content should be UTF-8, but legacy files can have other
# encodings, including mixed encodings in one file. We don't attempt
# to decode them, but we silence the errors.
fd = io.open(filename, 'r', encoding='utf-8', errors='replace')
except IOError as e:
if debug:
raise e
else:
return
# parse file
for line in fd:
line = line.strip()
# empty line
if not line:
continue
# comment
elif line[0] == '#':
continue
# new group
elif line[0] == '[':
currentGroup = line.lstrip("[").rstrip("]")
if debug and self.hasGroup(currentGroup):
raise DuplicateGroupError(currentGroup, filename)
else:
content[currentGroup] = {}
# key
else:
try:
key, value = line.split("=", 1)
except ValueError:
raise ParsingError("Invalid line: " + line, filename)
key = key.strip() # Spaces before/after '=' should be ignored
try:
if debug and self.hasKey(key, currentGroup):
raise DuplicateKeyError(key, currentGroup, filename)
else:
content[currentGroup][key] = value.strip()
except (IndexError, UnboundLocalError):
raise ParsingError("Parsing error on key, group missing", filename)
fd.close()
self.filename = filename
self.tainted = False
# check header
if headers:
for header in headers:
if header in content:
self.defaultGroup = header
break
else:
raise ParsingError("[%s]-Header missing" % headers[0], filename)
# start stuff to access the keys
def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
# set default group
if not group:
group = self.defaultGroup
# return key (with locale)
if (group in self.content) and (key in self.content[group]):
if locale:
value = self.content[group][self.__addLocale(key, group)]
else:
value = self.content[group][key]
else:
if strict or debug:
if group not in self.content:
raise NoGroupError(group, self.filename)
elif key not in self.content[group]:
raise NoKeyError(key, group, self.filename)
else:
value = ""
if list == True:
values = self.getList(value)
result = []
else:
values = [value]
for value in values:
if type == "boolean":
value = self.__getBoolean(value)
elif type == "integer":
try:
value = int(value)
except ValueError:
value = 0
elif type == "numeric":
try:
value = float(value)
except ValueError:
value = 0.0
elif type == "regex":
value = re.compile(value)
elif type == "point":
x, y = value.split(",")
value = int(x), int(y)
if list == True:
result.append(value)
else:
result = value
return result
# end stuff to access the keys
# start subget
def getList(self, string):
if re.search(r"(?<!\\)\;", string):
list = re.split(r"(?<!\\);", string)
elif re.search(r"(?<!\\)\|", string):
list = re.split(r"(?<!\\)\|", string)
elif re.search(r"(?<!\\),", string):
list = re.split(r"(?<!\\),", string)
else:
list = [string]
if list[-1] == "":
list.pop()
return list
def __getBoolean(self, boolean):
if boolean == 1 or boolean == "true" or boolean == "True":
return True
elif boolean == 0 or boolean == "false" or boolean == "False":
return False
return False
# end subget
def __addLocale(self, key, group=None):
"add locale to key according the current lc_messages"
# set default group
if not group:
group = self.defaultGroup
for lang in Locale.langs:
langkey = "%s[%s]" % (key, lang)
if langkey in self.content[group]:
return langkey
return key
# start validation stuff
def validate(self, report="All"):
"""Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
if there is anything amiss.
report can be 'All' / 'Warnings' / 'Errors'
"""
self.warnings = []
self.errors = []
# get file extension
self.fileExtension = os.path.splitext(self.filename)[1]
# overwrite this for own checkings
self.checkExtras()
# check all keys
for group in self.content:
self.checkGroup(group)
for key in self.content[group]:
self.checkKey(key, self.content[group][key], group)
# check if value is empty
if self.content[group][key] == "":
self.warnings.append("Value of Key '%s' is empty" % key)
# raise Warnings / Errors
msg = ""
if report == "All" or report == "Warnings":
for line in self.warnings:
msg += "\n- " + line
if report == "All" or report == "Errors":
for line in self.errors:
msg += "\n- " + line
if msg:
raise ValidationError(msg, self.filename)
# check if group header is valid
def checkGroup(self, group):
pass
# check if key is valid
def checkKey(self, key, value, group):
pass
# check random stuff
def checkValue(self, key, value, type="string", list=False):
if list == True:
values = self.getList(value)
else:
values = [value]
for value in values:
if type == "string":
code = self.checkString(value)
if type == "localestring":
continue
elif type == "boolean":
code = self.checkBoolean(value)
elif type == "numeric":
code = self.checkNumber(value)
elif type == "integer":
code = self.checkInteger(value)
elif type == "regex":
code = self.checkRegex(value)
elif type == "point":
code = self.checkPoint(value)
if code == 1:
self.errors.append("'%s' is not a valid %s" % (value, type))
elif code == 2:
self.warnings.append("Value of key '%s' is deprecated" % key)
def checkExtras(self):
pass
def checkBoolean(self, value):
# 1 or 0 : deprecated
if (value == "1" or value == "0"):
return 2
# true or false: ok
elif not (value == "true" or value == "false"):
return 1
def checkNumber(self, value):
# float() ValueError
try:
float(value)
except:
return 1
def checkInteger(self, value):
# int() ValueError
try:
int(value)
except:
return 1
def checkPoint(self, value):
if not re.match("^[0-9]+,[0-9]+$", value):
return 1
def checkString(self, value):
return 0 if is_ascii(value) else 1
def checkRegex(self, value):
try:
re.compile(value)
except:
return 1
# write support
def write(self, filename=None, trusted=False):
if not filename and not self.filename:
raise ParsingError("File not found", "")
if filename:
self.filename = filename
else:
filename = self.filename
if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
os.makedirs(os.path.dirname(filename))
with io.open(filename, 'w', encoding='utf-8') as fp:
# An executable bit signifies that the desktop file is
# trusted, but then the file can be executed. Add hashbang to
# make sure that the file is opened by something that
# understands desktop files.
if trusted:
fp.write(u("#!/usr/bin/env xdg-open\n"))
if self.defaultGroup:
fp.write(u("[%s]\n") % self.defaultGroup)
for (key, value) in self.content[self.defaultGroup].items():
fp.write(u("%s=%s\n") % (key, value))
fp.write(u("\n"))
for (name, group) in self.content.items():
if name != self.defaultGroup:
fp.write(u("[%s]\n") % name)
for (key, value) in group.items():
fp.write(u("%s=%s\n") % (key, value))
fp.write(u("\n"))
# Add executable bits to the file to show that it's trusted.
if trusted:
oldmode = os.stat(filename).st_mode
mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
os.chmod(filename, mode)
self.tainted = False
def set(self, key, value, group=None, locale=False):
# set default group
if not group:
group = self.defaultGroup
if locale == True and len(xdg.Locale.langs) > 0:
key = key + "[" + xdg.Locale.langs[0] + "]"
try:
self.content[group][key] = value
except KeyError:
raise NoGroupError(group, self.filename)
self.tainted = (value == self.get(key, group))
def addGroup(self, group):
if self.hasGroup(group):
if debug:
raise DuplicateGroupError(group, self.filename)
else:
self.content[group] = {}
self.tainted = True
def removeGroup(self, group):
existed = group in self.content
if existed:
del self.content[group]
self.tainted = True
else:
if debug:
raise NoGroupError(group, self.filename)
return existed
def removeKey(self, key, group=None, locales=True):
# set default group
if not group:
group = self.defaultGroup
try:
if locales:
for name in list(self.content[group]):
if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key:
del self.content[group][name]
value = self.content[group].pop(key)
self.tainted = True
return value
except KeyError as e:
if debug:
if e == group:
raise NoGroupError(group, self.filename)
else:
raise NoKeyError(key, group, self.filename)
else:
return ""
# misc
def groups(self):
return self.content.keys()
def hasGroup(self, group):
return group in self.content
def hasKey(self, key, group=None):
# set default group
if not group:
group = self.defaultGroup
return key in self.content[group]
def getFileName(self):
return self.filename

View File

@@ -0,0 +1,79 @@
"""
Helper Module for Locale settings
This module is based on a ROX module (LGPL):
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log
"""
import os
from locale import normalize
regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?"
def _expand_lang(locale):
locale = normalize(locale)
COMPONENT_CODESET = 1 << 0
COMPONENT_MODIFIER = 1 << 1
COMPONENT_TERRITORY = 1 << 2
# split up the locale into its base components
mask = 0
pos = locale.find('@')
if pos >= 0:
modifier = locale[pos:]
locale = locale[:pos]
mask |= COMPONENT_MODIFIER
else:
modifier = ''
pos = locale.find('.')
codeset = ''
if pos >= 0:
locale = locale[:pos]
pos = locale.find('_')
if pos >= 0:
territory = locale[pos:]
locale = locale[:pos]
mask |= COMPONENT_TERRITORY
else:
territory = ''
language = locale
ret = []
for i in range(mask+1):
if not (i & ~mask): # if all components for this combo exist ...
val = language
if i & COMPONENT_TERRITORY: val += territory
if i & COMPONENT_CODESET: val += codeset
if i & COMPONENT_MODIFIER: val += modifier
ret.append(val)
ret.reverse()
return ret
def expand_languages(languages=None):
# Get some reasonable defaults for arguments that were not supplied
if languages is None:
languages = []
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
val = os.environ.get(envar)
if val:
languages = val.split(':')
break
#if 'C' not in languages:
# languages.append('C')
# now normalize and expand the languages
nelangs = []
for lang in languages:
for nelang in _expand_lang(lang):
if nelang not in nelangs:
nelangs.append(nelang)
return nelangs
def update(language=None):
global langs
if language:
langs = expand_languages([language])
else:
langs = expand_languages()
langs = []
update()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,541 @@
""" CLass to edit XDG Menus """
import os
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from .Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder
from .BaseDirectory import xdg_config_dirs, xdg_data_dirs
from .Exceptions import ParsingError
from .Config import setRootMode
# XML-Cleanups: Move / Exclude
# FIXME: proper reverte/delete
# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions
# FIXME: catch Exceptions
# FIXME: copy functions
# FIXME: More Layout stuff
# FIXME: unod/redo function / remove menu...
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
class MenuEditor(object):
def __init__(self, menu=None, filename=None, root=False):
self.menu = None
self.filename = None
self.tree = None
self.parser = XMLMenuBuilder()
self.parse(menu, filename, root)
# fix for creating two menus with the same name on the fly
self.filenames = []
def parse(self, menu=None, filename=None, root=False):
if root:
setRootMode(True)
if isinstance(menu, Menu):
self.menu = menu
elif menu:
self.menu = self.parser.parse(menu)
else:
self.menu = self.parser.parse()
if root:
self.filename = self.menu.Filename
elif filename:
self.filename = filename
else:
self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
try:
self.tree = etree.parse(self.filename)
except IOError:
root = etree.fromtring("""
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
<Menu>
<Name>Applications</Name>
<MergeFile type="parent">%s</MergeFile>
</Menu>
""" % self.menu.Filename)
self.tree = etree.ElementTree(root)
except ParsingError:
raise ParsingError('Not a valid .menu file', self.filename)
#FIXME: is this needed with etree ?
self.__remove_whitespace_nodes(self.tree)
def save(self):
self.__saveEntries(self.menu)
self.__saveMenu()
def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None):
menuentry = MenuEntry(self.__getFileName(name, ".desktop"))
menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal)
self.__addEntry(parent, menuentry, after, before)
self.menu.sort()
return menuentry
def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None):
menu = Menu()
menu.Parent = parent
menu.Depth = parent.Depth + 1
menu.Layout = parent.DefaultLayout
menu.DefaultLayout = parent.DefaultLayout
menu = self.editMenu(menu, name, genericname, comment, icon)
self.__addEntry(parent, menu, after, before)
self.menu.sort()
return menu
def createSeparator(self, parent, after=None, before=None):
separator = Separator(parent)
self.__addEntry(parent, separator, after, before)
self.menu.sort()
return separator
def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__deleteEntry(oldparent, menuentry, after, before)
self.__addEntry(newparent, menuentry, after, before)
self.menu.sort()
return menuentry
def moveMenu(self, menu, oldparent, newparent, after=None, before=None):
self.__deleteEntry(oldparent, menu, after, before)
self.__addEntry(newparent, menu, after, before)
root_menu = self.__getXmlMenu(self.menu.Name)
if oldparent.getPath(True) != newparent.getPath(True):
self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name))
self.menu.sort()
return menu
def moveSeparator(self, separator, parent, after=None, before=None):
self.__deleteEntry(parent, separator, after, before)
self.__addEntry(parent, separator, after, before)
self.menu.sort()
return separator
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__addEntry(newparent, menuentry, after, before)
self.menu.sort()
return menuentry
def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None):
deskentry = menuentry.DesktopEntry
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale=True)
if genericname:
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale=True)
if command:
deskentry.set("Exec", command)
if icon:
deskentry.set("Icon", icon)
if terminal:
deskentry.set("Terminal", "true")
elif not terminal:
deskentry.set("Terminal", "false")
if nodisplay is True:
deskentry.set("NoDisplay", "true")
elif nodisplay is False:
deskentry.set("NoDisplay", "false")
if hidden is True:
deskentry.set("Hidden", "true")
elif hidden is False:
deskentry.set("Hidden", "false")
menuentry.updateAttributes()
if len(menuentry.Parents) > 0:
self.menu.sort()
return menuentry
def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None):
# Hack for legacy dirs
if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory":
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory")
menu.Directory.setAttributes(menu.Name + ".directory")
# Hack for New Entries
elif not isinstance(menu.Directory, MenuEntry):
if not name:
name = menu.Name
filename = self.__getFileName(name, ".directory").replace("/", "")
if not menu.Name:
menu.Name = filename.replace(".directory", "")
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
self.__addXmlTextElement(xml_menu, 'Directory', filename)
menu.Directory = MenuEntry(filename)
deskentry = menu.Directory.DesktopEntry
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale=True)
if genericname:
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale=True)
if icon:
deskentry.set("Icon", icon)
if nodisplay is True:
deskentry.set("NoDisplay", "true")
elif nodisplay is False:
deskentry.set("NoDisplay", "false")
if hidden is True:
deskentry.set("Hidden", "true")
elif hidden is False:
deskentry.set("Hidden", "false")
menu.Directory.updateAttributes()
if isinstance(menu.Parent, Menu):
self.menu.sort()
return menu
def hideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay=True)
def unhideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
def hideMenu(self, menu):
self.editMenu(menu, nodisplay=True)
def unhideMenu(self, menu):
self.editMenu(menu, nodisplay=False, hidden=False)
xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
deleted = xml_menu.findall('Deleted')
not_deleted = xml_menu.findall('NotDeleted')
for node in deleted + not_deleted:
xml_menu.remove(node)
def deleteMenuEntry(self, menuentry):
if self.getAction(menuentry) == "delete":
self.__deleteFile(menuentry.DesktopEntry.filename)
for parent in menuentry.Parents:
self.__deleteEntry(parent, menuentry)
self.menu.sort()
return menuentry
def revertMenuEntry(self, menuentry):
if self.getAction(menuentry) == "revert":
self.__deleteFile(menuentry.DesktopEntry.filename)
menuentry.Original.Parents = []
for parent in menuentry.Parents:
index = parent.Entries.index(menuentry)
parent.Entries[index] = menuentry.Original
index = parent.MenuEntries.index(menuentry)
parent.MenuEntries[index] = menuentry.Original
menuentry.Original.Parents.append(parent)
self.menu.sort()
return menuentry
def deleteMenu(self, menu):
if self.getAction(menu) == "delete":
self.__deleteFile(menu.Directory.DesktopEntry.filename)
self.__deleteEntry(menu.Parent, menu)
xml_menu = self.__getXmlMenu(menu.getPath(True, True))
parent = self.__get_parent_node(xml_menu)
parent.remove(xml_menu)
self.menu.sort()
return menu
def revertMenu(self, menu):
if self.getAction(menu) == "revert":
self.__deleteFile(menu.Directory.DesktopEntry.filename)
menu.Directory = menu.Directory.Original
self.menu.sort()
return menu
def deleteSeparator(self, separator):
self.__deleteEntry(separator.Parent, separator, after=True)
self.menu.sort()
return separator
""" Private Stuff """
def getAction(self, entry):
if isinstance(entry, Menu):
if not isinstance(entry.Directory, MenuEntry):
return "none"
elif entry.Directory.getType() == "Both":
return "revert"
elif entry.Directory.getType() == "User" and (
len(entry.Submenus) + len(entry.MenuEntries)
) == 0:
return "delete"
elif isinstance(entry, MenuEntry):
if entry.getType() == "Both":
return "revert"
elif entry.getType() == "User":
return "delete"
else:
return "none"
return "none"
def __saveEntries(self, menu):
if not menu:
menu = self.menu
if isinstance(menu.Directory, MenuEntry):
menu.Directory.save()
for entry in menu.getEntries(hidden=True):
if isinstance(entry, MenuEntry):
entry.save()
elif isinstance(entry, Menu):
self.__saveEntries(entry)
def __saveMenu(self):
if not os.path.isdir(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename))
self.tree.write(self.filename, encoding='utf-8')
def __getFileName(self, name, extension):
postfix = 0
while 1:
if postfix == 0:
filename = name + extension
else:
filename = name + "-" + str(postfix) + extension
if extension == ".desktop":
dir = "applications"
elif extension == ".directory":
dir = "desktop-directories"
if not filename in self.filenames and not os.path.isfile(
os.path.join(xdg_data_dirs[0], dir, filename)
):
self.filenames.append(filename)
break
else:
postfix += 1
return filename
def __getXmlMenu(self, path, create=True, element=None):
# FIXME: we should also return the menu's parent,
# to avoid looking for it later on
# @see Element.getiterator()
if not element:
element = self.tree
if "/" in path:
(name, path) = path.split("/", 1)
else:
name = path
path = ""
found = None
for node in element.findall("Menu"):
name_node = node.find('Name')
if name_node.text == name:
if path:
found = self.__getXmlMenu(path, create, node)
else:
found = node
if found:
break
if not found and create:
node = self.__addXmlMenuElement(element, name)
if path:
found = self.__getXmlMenu(path, create, node)
else:
found = node
return found
def __addXmlMenuElement(self, element, name):
menu_node = etree.SubElement('Menu', element)
name_node = etree.SubElement('Name', menu_node)
name_node.text = name
return menu_node
def __addXmlTextElement(self, element, name, text):
node = etree.SubElement(name, element)
node.text = text
return node
def __addXmlFilename(self, element, filename, type_="Include"):
# remove old filenames
includes = element.findall('Include')
excludes = element.findall('Exclude')
rules = includes + excludes
for rule in rules:
#FIXME: this finds only Rules whose FIRST child is a Filename element
if rule[0].tag == "Filename" and rule[0].text == filename:
element.remove(rule)
# shouldn't it remove all occurences, like the following:
#filename_nodes = rule.findall('.//Filename'):
#for fn in filename_nodes:
#if fn.text == filename:
##element.remove(rule)
#parent = self.__get_parent_node(fn)
#parent.remove(fn)
# add new filename
node = etree.SubElement(type_, element)
self.__addXmlTextElement(node, 'Filename', filename)
return node
def __addXmlMove(self, element, old, new):
node = etree.SubElement("Move", element)
self.__addXmlTextElement(node, 'Old', old)
self.__addXmlTextElement(node, 'New', new)
return node
def __addXmlLayout(self, element, layout):
# remove old layout
for node in element.findall("Layout"):
element.remove(node)
# add new layout
node = etree.SubElement("Layout", element)
for order in layout.order:
if order[0] == "Separator":
child = etree.SubElement("Separator", node)
elif order[0] == "Filename":
child = self.__addXmlTextElement(node, "Filename", order[1])
elif order[0] == "Menuname":
child = self.__addXmlTextElement(node, "Menuname", order[1])
elif order[0] == "Merge":
child = etree.SubElement("Merge", node)
child.attrib["type"] = order[1]
return node
def __addLayout(self, parent):
layout = Layout()
layout.order = []
layout.show_empty = parent.Layout.show_empty
layout.inline = parent.Layout.inline
layout.inline_header = parent.Layout.inline_header
layout.inline_alias = parent.Layout.inline_alias
layout.inline_limit = parent.Layout.inline_limit
layout.order.append(["Merge", "menus"])
for entry in parent.Entries:
if isinstance(entry, Menu):
layout.parseMenuname(entry.Name)
elif isinstance(entry, MenuEntry):
layout.parseFilename(entry.DesktopFileID)
elif isinstance(entry, Separator):
layout.parseSeparator()
layout.order.append(["Merge", "files"])
parent.Layout = layout
return layout
def __addEntry(self, parent, entry, after=None, before=None):
if after or before:
if after:
index = parent.Entries.index(after) + 1
elif before:
index = parent.Entries.index(before)
parent.Entries.insert(index, entry)
else:
parent.Entries.append(entry)
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
if isinstance(entry, MenuEntry):
parent.MenuEntries.append(entry)
entry.Parents.append(parent)
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include")
elif isinstance(entry, Menu):
parent.addSubmenu(entry)
if after or before:
self.__addLayout(parent)
self.__addXmlLayout(xml_parent, parent.Layout)
def __deleteEntry(self, parent, entry, after=None, before=None):
parent.Entries.remove(entry)
xml_parent = self.__getXmlMenu(parent.getPath(True, True))
if isinstance(entry, MenuEntry):
entry.Parents.remove(parent)
parent.MenuEntries.remove(entry)
self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude")
elif isinstance(entry, Menu):
parent.Submenus.remove(entry)
if after or before:
self.__addLayout(parent)
self.__addXmlLayout(xml_parent, parent.Layout)
def __deleteFile(self, filename):
try:
os.remove(filename)
except OSError:
pass
try:
self.filenames.remove(filename)
except ValueError:
pass
def __remove_whitespace_nodes(self, node):
for child in node:
text = child.text.strip()
if not text:
child.text = ''
tail = child.tail.strip()
if not tail:
child.tail = ''
if len(child):
self.__remove_whilespace_nodes(child)
def __get_parent_node(self, node):
# elements in ElementTree doesn't hold a reference to their parent
for parent, child in self.__iter_parent():
if child is node:
return child
def __iter_parent(self):
for parent in self.tree.getiterator():
for child in parent:
yield parent, child

View File

@@ -0,0 +1,780 @@
"""
This module is based on a rox module (LGPL):
http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log
This module provides access to the shared MIME database.
types is a dictionary of all known MIME types, indexed by the type name, e.g.
types['application/x-python']
Applications can install information about MIME types by storing an
XML file as <MIME>/packages/<application>.xml and running the
update-mime-database command, which is provided by the freedesktop.org
shared mime database package.
See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
information about the format of these files.
(based on version 0.13)
"""
import os
import re
import stat
import sys
import fnmatch
from . import BaseDirectory, Locale
from .dom import minidom, XML_NAMESPACE
from collections import defaultdict
FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'
types = {} # Maps MIME names to type objects
exts = None # Maps extensions to types
globs = None # List of (glob, type) pairs
literals = None # Maps liternal names to types
magic = None
PY3 = (sys.version_info[0] >= 3)
def _get_node_data(node):
"""Get text of XML node"""
return ''.join([n.nodeValue for n in node.childNodes]).strip()
def lookup(media, subtype = None):
"""Get the MIMEtype object for the given type.
This remains for backwards compatibility; calling MIMEtype now does
the same thing.
The name can either be passed as one part ('text/plain'), or as two
('text', 'plain').
"""
return MIMEtype(media, subtype)
class MIMEtype(object):
"""Class holding data about a MIME type.
Calling the class will return a cached instance, so there is only one
instance for each MIME type. The name can either be passed as one part
('text/plain'), or as two ('text', 'plain').
"""
def __new__(cls, media, subtype=None):
if subtype is None and '/' in media:
media, subtype = media.split('/', 1)
assert '/' not in subtype
media = media.lower()
subtype = subtype.lower()
try:
return types[(media, subtype)]
except KeyError:
mtype = super(MIMEtype, cls).__new__(cls)
mtype._init(media, subtype)
types[(media, subtype)] = mtype
return mtype
# If this is done in __init__, it is automatically called again each time
# the MIMEtype is returned by __new__, which we don't want. So we call it
# explicitly only when we construct a new instance.
def _init(self, media, subtype):
self.media = media
self.subtype = subtype
self._comment = None
def _load(self):
"Loads comment for current language. Use get_comment() instead."
resource = os.path.join('mime', self.media, self.subtype + '.xml')
for path in BaseDirectory.load_data_paths(resource):
doc = minidom.parse(path)
if doc is None:
continue
for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
goodness = 1 + (lang in xdg.Locale.langs)
if goodness > self._comment[0]:
self._comment = (goodness, _get_node_data(comment))
if goodness == 2: return
# FIXME: add get_icon method
def get_comment(self):
"""Returns comment for current language, loading it if needed."""
# Should we ever reload?
if self._comment is None:
self._comment = (0, str(self))
self._load()
return self._comment[1]
def canonical(self):
"""Returns the canonical MimeType object if this is an alias."""
update_cache()
s = str(self)
if s in aliases:
return lookup(aliases[s])
return self
def inherits_from(self):
"""Returns a set of Mime types which this inherits from."""
update_cache()
return set(lookup(t) for t in inheritance[str(self)])
def __str__(self):
return self.media + '/' + self.subtype
def __repr__(self):
return 'MIMEtype(%r, %r)' % (self.media, self.subtype)
def __hash__(self):
return hash(self.media) ^ hash(self.subtype)
class UnknownMagicRuleFormat(ValueError):
pass
class DiscardMagicRules(Exception):
"Raised when __NOMAGIC__ is found, and caught to discard previous rules."
pass
class MagicRule:
also = None
def __init__(self, start, value, mask, word, range):
self.start = start
self.value = value
self.mask = mask
self.word = word
self.range = range
rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$')
@classmethod
def from_file(cls, f):
"""Read a rule from the binary magics file. Returns a 2-tuple of
the nesting depth and the MagicRule."""
line = f.readline()
#print line
# [indent] '>'
nest_depth, line = line.split(b'>', 1)
nest_depth = int(nest_depth) if nest_depth else 0
# start-offset '='
start, line = line.split(b'=', 1)
start = int(start)
if line == b'__NOMAGIC__\n':
raise DiscardMagicRules
# value length (2 bytes, big endian)
if sys.version_info[0] >= 3:
lenvalue = int.from_bytes(line[:2], byteorder='big')
else:
lenvalue = (ord(line[0])<<8)+ord(line[1])
line = line[2:]
# value
# This can contain newlines, so we may need to read more lines
while len(line) <= lenvalue:
line += f.readline()
value, line = line[:lenvalue], line[lenvalue:]
# ['&' mask]
if line.startswith(b'&'):
# This can contain newlines, so we may need to read more lines
while len(line) <= lenvalue:
line += f.readline()
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
else:
mask = None
# ['~' word-size] ['+' range-length]
ending = cls.rule_ending_re.match(line)
if not ending:
# Per the spec, this will be caught and ignored, to allow
# for future extensions.
raise UnknownMagicRuleFormat(repr(line))
word, range = ending.groups()
word = int(word) if (word is not None) else 1
range = int(range) if (range is not None) else 1
return nest_depth, cls(start, value, mask, word, range)
def maxlen(self):
l = self.start + len(self.value) + self.range
if self.also:
return max(l, self.also.maxlen())
return l
def match(self, buffer):
if self.match0(buffer):
if self.also:
return self.also.match(buffer)
return True
def match0(self, buffer):
l=len(buffer)
lenvalue = len(self.value)
for o in range(self.range):
s=self.start+o
e=s+lenvalue
if l<e:
return False
if self.mask:
test=''
for i in range(lenvalue):
if PY3:
c = buffer[s+i] & self.mask[i]
else:
c = ord(buffer[s+i]) & ord(self.mask[i])
test += chr(c)
else:
test = buffer[s:e]
if test==self.value:
return True
def __repr__(self):
return 'MagicRule(start=%r, value=%r, mask=%r, word=%r, range=%r)' %(
self.start,
self.value,
self.mask,
self.word,
self.range)
class MagicMatchAny(object):
"""Match any of a set of magic rules.
This has a similar interface to MagicRule objects (i.e. its match() and
maxlen() methods), to allow for duck typing.
"""
def __init__(self, rules):
self.rules = rules
def match(self, buffer):
return any(r.match(buffer) for r in self.rules)
def maxlen(self):
return max(r.maxlen() for r in self.rules)
@classmethod
def from_file(cls, f):
"""Read a set of rules from the binary magic file."""
c=f.read(1)
f.seek(-1, 1)
depths_rules = []
while c and c != b'[':
try:
depths_rules.append(MagicRule.from_file(f))
except UnknownMagicRuleFormat:
# Ignored to allow for extensions to the rule format.
pass
c=f.read(1)
if c:
f.seek(-1, 1)
# Build the rule tree
tree = [] # (rule, [(subrule,[subsubrule,...]), ...])
insert_points = {0:tree}
for depth, rule in depths_rules:
subrules = []
insert_points[depth].append((rule, subrules))
insert_points[depth+1] = subrules
return cls.from_rule_tree(tree)
@classmethod
def from_rule_tree(cls, tree):
"""From a nested list of (rule, subrules) pairs, build a MagicMatchAny
instance, recursing down the tree.
Where there's only one top-level rule, this is returned directly,
to simplify the nested structure. Returns None if no rules were read.
"""
rules = []
for rule, subrules in tree:
if subrules:
rule.also = cls.from_rule_tree(subrules)
rules.append(rule)
if len(rules)==0:
return None
if len(rules)==1:
return rules[0]
return cls(rules)
class MagicDB:
def __init__(self):
self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
def merge_file(self, fname):
"""Read a magic binary file, and add its rules to this MagicDB."""
with open(fname, 'rb') as f:
line = f.readline()
if line != b'MIME-Magic\0\n':
raise IOError('Not a MIME magic file')
while True:
shead = f.readline().decode('ascii')
#print(shead)
if not shead:
break
if shead[0] != '[' or shead[-2:] != ']\n':
raise ValueError('Malformed section heading', shead)
pri, tname = shead[1:-2].split(':')
#print shead[1:-2]
pri = int(pri)
mtype = lookup(tname)
try:
rule = MagicMatchAny.from_file(f)
except DiscardMagicRules:
self.bytype.pop(mtype, None)
rule = MagicMatchAny.from_file(f)
if rule is None:
continue
#print rule
self.bytype[mtype].append((pri, rule))
def finalise(self):
"""Prepare the MagicDB for matching.
This should be called after all rules have been merged into it.
"""
maxlen = 0
self.alltypes = [] # (priority, mimetype, rule)
for mtype, rules in self.bytype.items():
for pri, rule in rules:
self.alltypes.append((pri, mtype, rule))
maxlen = max(maxlen, rule.maxlen())
self.maxlen = maxlen # Number of bytes to read from files
self.alltypes.sort(key=lambda x: x[0], reverse=True)
def match_data(self, data, max_pri=100, min_pri=0, possible=None):
"""Do magic sniffing on some bytes.
max_pri & min_pri can be used to specify the maximum & minimum priority
rules to look for. possible can be a list of mimetypes to check, or None
(the default) to check all mimetypes until one matches.
Returns the MIMEtype found, or None if no entries match.
"""
if possible is not None:
types = []
for mt in possible:
for pri, rule in self.bytype[mt]:
types.append((pri, mt, rule))
types.sort(key=lambda x: x[0])
else:
types = self.alltypes
for priority, mimetype, rule in types:
#print priority, max_pri, min_pri
if priority > max_pri:
continue
if priority < min_pri:
break
if rule.match(data):
return mimetype
def match(self, path, max_pri=100, min_pri=0, possible=None):
"""Read data from the file and do magic sniffing on it.
max_pri & min_pri can be used to specify the maximum & minimum priority
rules to look for. possible can be a list of mimetypes to check, or None
(the default) to check all mimetypes until one matches.
Returns the MIMEtype found, or None if no entries match. Raises IOError
if the file can't be opened.
"""
with open(path, 'rb') as f:
buf = f.read(self.maxlen)
return self.match_data(buf, max_pri, min_pri, possible)
def __repr__(self):
return '<MagicDB (%d types)>' % len(self.alltypes)
class GlobDB(object):
def __init__(self):
"""Prepare the GlobDB. It can't actually be used until .finalise() is
called, but merge_file() can be used to add data before that.
"""
# Maps mimetype to {(weight, glob, flags), ...}
self.allglobs = defaultdict(set)
def merge_file(self, path):
"""Loads name matching information from a globs2 file."""#
allglobs = self.allglobs
with open(path) as f:
for line in f:
if line.startswith('#'): continue # Comment
fields = line[:-1].split(':')
weight, type_name, pattern = fields[:3]
weight = int(weight)
mtype = lookup(type_name)
if len(fields) > 3:
flags = fields[3].split(',')
else:
flags = ()
if pattern == '__NOGLOBS__':
# This signals to discard any previous globs
allglobs.pop(mtype, None)
continue
allglobs[mtype].add((weight, pattern, tuple(flags)))
def finalise(self):
"""Prepare the GlobDB for matching.
This should be called after all files have been merged into it.
"""
self.exts = defaultdict(list) # Maps extensions to [(type, weight),...]
self.cased_exts = defaultdict(list)
self.globs = [] # List of (regex, type, weight) triplets
self.literals = {} # Maps literal names to (type, weight)
self.cased_literals = {}
for mtype, globs in self.allglobs.items():
mtype = mtype.canonical()
for weight, pattern, flags in globs:
cased = 'cs' in flags
if pattern.startswith('*.'):
# *.foo -- extension pattern
rest = pattern[2:]
if not ('*' in rest or '[' in rest or '?' in rest):
if cased:
self.cased_exts[rest].append((mtype, weight))
else:
self.exts[rest.lower()].append((mtype, weight))
continue
if ('*' in pattern or '[' in pattern or '?' in pattern):
# Translate the glob pattern to a regex & compile it
re_flags = 0 if cased else re.I
pattern = re.compile(fnmatch.translate(pattern), flags=re_flags)
self.globs.append((pattern, mtype, weight))
else:
# No wildcards - literal pattern
if cased:
self.cased_literals[pattern] = (mtype, weight)
else:
self.literals[pattern.lower()] = (mtype, weight)
# Sort globs by weight & length
self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) )
def first_match(self, path):
"""Return the first match found for a given path, or None if no match
is found."""
try:
return next(self._match_path(path))[0]
except StopIteration:
return None
def all_matches(self, path):
"""Return a list of (MIMEtype, glob weight) pairs for the path."""
return list(self._match_path(path))
def _match_path(self, path):
"""Yields pairs of (mimetype, glob weight)."""
leaf = os.path.basename(path)
# Literals (no wildcards)
if leaf in self.cased_literals:
yield self.cased_literals[leaf]
lleaf = leaf.lower()
if lleaf in self.literals:
yield self.literals[lleaf]
# Extensions
ext = leaf
while 1:
p = ext.find('.')
if p < 0: break
ext = ext[p + 1:]
if ext in self.cased_exts:
for res in self.cased_exts[ext]:
yield res
ext = lleaf
while 1:
p = ext.find('.')
if p < 0: break
ext = ext[p+1:]
if ext in self.exts:
for res in self.exts[ext]:
yield res
# Other globs
for (regex, mime_type, weight) in self.globs:
if regex.match(leaf):
yield (mime_type, weight)
# Some well-known types
text = lookup('text', 'plain')
octet_stream = lookup('application', 'octet-stream')
inode_block = lookup('inode', 'blockdevice')
inode_char = lookup('inode', 'chardevice')
inode_dir = lookup('inode', 'directory')
inode_fifo = lookup('inode', 'fifo')
inode_socket = lookup('inode', 'socket')
inode_symlink = lookup('inode', 'symlink')
inode_door = lookup('inode', 'door')
app_exe = lookup('application', 'executable')
_cache_uptodate = False
def _cache_database():
global globs, magic, aliases, inheritance, _cache_uptodate
_cache_uptodate = True
aliases = {} # Maps alias Mime types to canonical names
inheritance = defaultdict(set) # Maps to sets of parent mime types.
# Load aliases
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')):
with open(path, 'r') as f:
for line in f:
alias, canonical = line.strip().split(None, 1)
aliases[alias] = canonical
# Load filename patterns (globs)
globs = GlobDB()
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')):
globs.merge_file(path)
globs.finalise()
# Load magic sniffing data
magic = MagicDB()
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
magic.merge_file(path)
magic.finalise()
# Load subclasses
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
with open(path, 'r') as f:
for line in f:
sub, parent = line.strip().split(None, 1)
inheritance[sub].add(parent)
def update_cache():
if not _cache_uptodate:
_cache_database()
def get_type_by_name(path):
"""Returns type of file by its name, or None if not known"""
update_cache()
return globs.first_match(path)
def get_type_by_contents(path, max_pri=100, min_pri=0):
"""Returns type of file by its contents, or None if not known"""
update_cache()
return magic.match(path, max_pri, min_pri)
def get_type_by_data(data, max_pri=100, min_pri=0):
"""Returns type of the data, which should be bytes."""
update_cache()
return magic.match_data(data, max_pri, min_pri)
def _get_type_by_stat(st_mode):
"""Match special filesystem objects to Mimetypes."""
if stat.S_ISDIR(st_mode): return inode_dir
elif stat.S_ISCHR(st_mode): return inode_char
elif stat.S_ISBLK(st_mode): return inode_block
elif stat.S_ISFIFO(st_mode): return inode_fifo
elif stat.S_ISLNK(st_mode): return inode_symlink
elif stat.S_ISSOCK(st_mode): return inode_socket
return inode_door
def get_type(path, follow=True, name_pri=100):
"""Returns type of file indicated by path.
This function is *deprecated* - :func:`get_type2` is more accurate.
:param path: pathname to check (need not exist)
:param follow: when reading file, follow symbolic links
:param name_pri: Priority to do name matches. 100=override magic
This tries to use the contents of the file, and falls back to the name. It
can also handle special filesystem objects like directories and sockets.
"""
update_cache()
try:
if follow:
st = os.stat(path)
else:
st = os.lstat(path)
except:
t = get_type_by_name(path)
return t or text
if stat.S_ISREG(st.st_mode):
# Regular file
t = get_type_by_contents(path, min_pri=name_pri)
if not t: t = get_type_by_name(path)
if not t: t = get_type_by_contents(path, max_pri=name_pri)
if t is None:
if stat.S_IMODE(st.st_mode) & 0o111:
return app_exe
else:
return text
return t
else:
return _get_type_by_stat(st.st_mode)
def get_type2(path, follow=True):
"""Find the MIMEtype of a file using the XDG recommended checking order.
This first checks the filename, then uses file contents if the name doesn't
give an unambiguous MIMEtype. It can also handle special filesystem objects
like directories and sockets.
:param path: file path to examine (need not exist)
:param follow: whether to follow symlinks
:rtype: :class:`MIMEtype`
.. versionadded:: 1.0
"""
update_cache()
try:
st = os.stat(path) if follow else os.lstat(path)
except OSError:
return get_type_by_name(path) or octet_stream
if not stat.S_ISREG(st.st_mode):
# Special filesystem objects
return _get_type_by_stat(st.st_mode)
mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True)
if mtypes:
max_weight = mtypes[0][1]
i = 1
for mt, w in mtypes[1:]:
if w < max_weight:
break
i += 1
mtypes = mtypes[:i]
if len(mtypes) == 1:
return mtypes[0][0]
possible = [mt for mt,w in mtypes]
else:
possible = None # Try all magic matches
try:
t = magic.match(path, possible=possible)
except IOError:
t = None
if t:
return t
elif mtypes:
return mtypes[0][0]
elif stat.S_IMODE(st.st_mode) & 0o111:
return app_exe
else:
return text if is_text_file(path) else octet_stream
def is_text_file(path):
"""Guess whether a file contains text or binary data.
Heuristic: binary if the first 32 bytes include ASCII control characters.
This rule may change in future versions.
.. versionadded:: 1.0
"""
try:
f = open(path, 'rb')
except IOError:
return False
with f:
return _is_text(f.read(32))
if PY3:
def _is_text(data):
return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data)
else:
def _is_text(data):
return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \
for b in data)
_mime2ext_cache = None
_mime2ext_cache_uptodate = False
def get_extensions(mimetype):
"""Retrieve the set of filename extensions matching a given MIMEtype.
Extensions are returned without a leading dot, e.g. 'py'. If no extensions
are registered for the MIMEtype, returns an empty set.
The extensions are stored in a cache the first time this is called.
.. versionadded:: 1.0
"""
global _mime2ext_cache, _mime2ext_cache_uptodate
update_cache()
if not _mime2ext_cache_uptodate:
_mime2ext_cache = defaultdict(set)
for ext, mtypes in globs.exts.items():
for mtype, prio in mtypes:
_mime2ext_cache[mtype].add(ext)
_mime2ext_cache_uptodate = True
return _mime2ext_cache[mimetype]
def install_mime_info(application, package_file):
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``
If package_file is None, install ``<app_dir>/<application>.xml``.
If already installed, does nothing. May overwrite an existing
file with the same name (if the contents are different)"""
application += '.xml'
new_data = open(package_file).read()
# See if the file is already installed
package_dir = os.path.join('mime', 'packages')
resource = os.path.join(package_dir, application)
for x in BaseDirectory.load_data_paths(resource):
try:
old_data = open(x).read()
except:
continue
if old_data == new_data:
return # Already installed
global _cache_uptodate
_cache_uptodate = False
# Not already installed; add a new copy
# Create the directory structure...
new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application)
# Write the file...
open(new_file, 'w').write(new_data)
# Update the database...
command = 'update-mime-database'
if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')):
os.unlink(new_file)
raise Exception("The '%s' command returned an error code!\n" \
"Make sure you have the freedesktop.org shared MIME package:\n" \
"http://standards.freedesktop.org/shared-mime-info/" % command)

View File

@@ -0,0 +1,181 @@
"""
Implementation of the XDG Recent File Storage Specification
http://standards.freedesktop.org/recent-file-spec
"""
import xml.dom.minidom, xml.sax.saxutils
import os, time, fcntl
from .Exceptions import ParsingError
class RecentFiles:
def __init__(self):
self.RecentFiles = []
self.filename = ""
def parse(self, filename=None):
"""Parse a list of recently used files.
filename defaults to ``~/.recently-used``.
"""
if not filename:
filename = os.path.join(os.getenv("HOME"), ".recently-used")
try:
doc = xml.dom.minidom.parse(filename)
except IOError:
raise ParsingError('File not found', filename)
except xml.parsers.expat.ExpatError:
raise ParsingError('Not a valid .menu file', filename)
self.filename = filename
for child in doc.childNodes:
if child.nodeType == xml.dom.Node.ELEMENT_NODE:
if child.tagName == "RecentFiles":
for recent in child.childNodes:
if recent.nodeType == xml.dom.Node.ELEMENT_NODE:
if recent.tagName == "RecentItem":
self.__parseRecentItem(recent)
self.sort()
def __parseRecentItem(self, item):
recent = RecentFile()
self.RecentFiles.append(recent)
for attribute in item.childNodes:
if attribute.nodeType == xml.dom.Node.ELEMENT_NODE:
if attribute.tagName == "URI":
recent.URI = attribute.childNodes[0].nodeValue
elif attribute.tagName == "Mime-Type":
recent.MimeType = attribute.childNodes[0].nodeValue
elif attribute.tagName == "Timestamp":
recent.Timestamp = int(attribute.childNodes[0].nodeValue)
elif attribute.tagName == "Private":
recent.Prviate = True
elif attribute.tagName == "Groups":
for group in attribute.childNodes:
if group.nodeType == xml.dom.Node.ELEMENT_NODE:
if group.tagName == "Group":
recent.Groups.append(group.childNodes[0].nodeValue)
def write(self, filename=None):
"""Write the list of recently used files to disk.
If the instance is already associated with a file, filename can be
omitted to save it there again.
"""
if not filename and not self.filename:
raise ParsingError('File not found', filename)
elif not filename:
filename = self.filename
f = open(filename, "w")
fcntl.lockf(f, fcntl.LOCK_EX)
f.write('<?xml version="1.0"?>\n')
f.write("<RecentFiles>\n")
for r in self.RecentFiles:
f.write(" <RecentItem>\n")
f.write(" <URI>%s</URI>\n" % xml.sax.saxutils.escape(r.URI))
f.write(" <Mime-Type>%s</Mime-Type>\n" % r.MimeType)
f.write(" <Timestamp>%s</Timestamp>\n" % r.Timestamp)
if r.Private == True:
f.write(" <Private/>\n")
if len(r.Groups) > 0:
f.write(" <Groups>\n")
for group in r.Groups:
f.write(" <Group>%s</Group>\n" % group)
f.write(" </Groups>\n")
f.write(" </RecentItem>\n")
f.write("</RecentFiles>\n")
fcntl.lockf(f, fcntl.LOCK_UN)
f.close()
def getFiles(self, mimetypes=None, groups=None, limit=0):
"""Get a list of recently used files.
The parameters can be used to filter by mime types, by group, or to
limit the number of items returned. By default, the entire list is
returned, except for items marked private.
"""
tmp = []
i = 0
for item in self.RecentFiles:
if groups:
for group in groups:
if group in item.Groups:
tmp.append(item)
i += 1
elif mimetypes:
for mimetype in mimetypes:
if mimetype == item.MimeType:
tmp.append(item)
i += 1
else:
if item.Private == False:
tmp.append(item)
i += 1
if limit != 0 and i == limit:
break
return tmp
def addFile(self, item, mimetype, groups=None, private=False):
"""Add a recently used file.
item should be the URI of the file, typically starting with ``file:///``.
"""
# check if entry already there
if item in self.RecentFiles:
index = self.RecentFiles.index(item)
recent = self.RecentFiles[index]
else:
# delete if more then 500 files
if len(self.RecentFiles) == 500:
self.RecentFiles.pop()
# add entry
recent = RecentFile()
self.RecentFiles.append(recent)
recent.URI = item
recent.MimeType = mimetype
recent.Timestamp = int(time.time())
recent.Private = private
if groups:
recent.Groups = groups
self.sort()
def deleteFile(self, item):
"""Remove a recently used file, by URI, from the list.
"""
if item in self.RecentFiles:
self.RecentFiles.remove(item)
def sort(self):
self.RecentFiles.sort()
self.RecentFiles.reverse()
class RecentFile:
def __init__(self):
self.URI = ""
self.MimeType = ""
self.Timestamp = ""
self.Private = False
self.Groups = []
def __cmp__(self, other):
return cmp(self.Timestamp, other.Timestamp)
def __lt__ (self, other):
return self.Timestamp < other.Timestamp
def __eq__(self, other):
return self.URI == str(other)
def __str__(self):
return self.URI

View File

@@ -0,0 +1,3 @@
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
__version__ = "0.26"

View File

@@ -0,0 +1,75 @@
import sys
PY3 = sys.version_info[0] >= 3
if PY3:
def u(s):
return s
else:
# Unicode-like literals
def u(s):
return s.decode('utf-8')
try:
# which() is available from Python 3.3
from shutil import which
except ImportError:
import os
# This is a copy of which() from Python 3.3
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None

View File

@@ -0,0 +1,64 @@
# Python imports
import os
# Lib imports
# Application imports
class Path:
def get_home(self) -> str:
return os.path.expanduser("~") + self.subpath
def get_path(self) -> str:
return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}"
def get_path_list(self) -> list:
return self.path
def push_to_path(self, dir: str):
self.path.append(dir)
self.load_directory()
def pop_from_path(self) -> None:
try:
self.path.pop()
if not self.go_past_home:
if self.get_home() not in self.get_path():
self.set_to_home()
self.load_directory()
except Exception as e:
pass
def set_path(self, path: str) -> bool:
if path == self.get_path():
return False
if os.path.isdir(path):
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
self.load_directory()
return True
return False
def set_path_with_sub_path(self, sub_path: str) -> bool:
path = os.path.join(self.get_home(), sub_path)
if path == self.get_path():
return False
if os.path.isdir(path):
self.path = list( filter(None, path.replace("\\", "/").split('/')) )
self.load_directory()
return True
return False
def set_to_home(self) -> None:
home = os.path.expanduser("~") + self.subpath
path = list( filter(None, home.replace("\\", "/").split('/')) )
self.path = path
self.load_directory()

View File

@@ -0,0 +1,291 @@
# Python imports
import os
import hashlib
import re
from os import listdir
from os.path import isdir
from os.path import isfile
from os.path import join
from random import randint
# Lib imports
# Application imports
from .utils.settings import Settings
from .utils.launcher import Launcher
from .utils.filehandler import FileHandler
from .icons.icon import Icon
from .path import Path
try:
get_file_size("/")
except Exception as e:
import os
def sizeof_fmt_def(num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return f"{num:3.1f} {unit}{suffix}"
num /= 1024.0
return f"{num:.1f} Yi{suffix}"
def _get_file_size(file):
try:
return "4K" if isdir(file) else sizeof_fmt_def(os.path.getsize(file))
except Exception as e:
return "0K"
get_file_size = _get_file_size
class Tab(Settings, FileHandler, Launcher, Icon, Path):
def __init__(self):
self.logger = None
self._id_length: int = 10
self._id: str = ""
self._wid: str = None
self.error_message: str = None
self._dir_watcher = None
self._hide_hidden: bool = self.HIDE_HIDDEN_FILES
self._files: list = []
self._dirs: list = []
self._vids: list = []
self._images: list = []
self._desktop: list = []
self._ungrouped: list = []
self._hidden: list = []
self._generate_id()
self.set_to_home()
def load_directory(self) -> None:
path = self.get_path()
self._dirs = []
self._vids = []
self._images = []
self._desktop = []
self._ungrouped = []
self._hidden = []
self._files = []
if not isdir(path):
self._set_error_message("Path can not be accessed.")
self.set_to_home()
return ""
for f in listdir(path):
file = join(path, f)
if self._hide_hidden:
if f.startswith('.'):
self._hidden.append(f)
continue
if isfile(file):
lowerName = file.lower()
if lowerName.endswith(self.fvideos):
self._vids.append(f)
elif lowerName.endswith(self.fimages):
self._images.append(f)
elif lowerName.endswith((".desktop",)):
self._desktop.append(f)
else:
self._ungrouped.append(f)
else:
self._dirs.append(f)
self._dirs.sort(key=self._natural_keys)
self._vids.sort(key=self._natural_keys)
self._images.sort(key=self._natural_keys)
self._desktop.sort(key=self._natural_keys)
self._ungrouped.sort(key=self._natural_keys)
self._files = self._dirs + self._vids + self._images + self._desktop + self._ungrouped
def is_folder_locked(self, hash):
if self.lock_folder:
path_parts = self.get_path().split('/')
file = self.get_path_part_from_hash(hash)
# Insure chilren folders are locked too.
lockedFolderInPath = False
for folder in self.locked_folders:
if folder in path_parts:
lockedFolderInPath = True
break
return (file in self.locked_folders or lockedFolderInPath)
else:
return False
def get_not_hidden_count(self) -> int:
return len(self._files) + \
len(self._dirs) + \
len(self._vids) + \
len(self._images) + \
len(self._desktop) + \
len(self._ungrouped)
def get_hidden_count(self) -> int:
return len(self._hidden)
def get_files_count(self) -> int:
return len(self._files)
def get_path_part_from_hash(self, hash: str) -> str:
files = self.get_files()
file = None
for f in files:
if hash == f[1]:
file = f[0]
break
return file
def get_files_formatted(self) -> dict:
files = self._hash_set(self._files),
dirs = self._hash_set(self._dirs),
videos = self.get_videos(),
images = self._hash_set(self._images),
desktops = self._hash_set(self._desktop),
ungrouped = self._hash_set(self._ungrouped)
hidden = self._hash_set(self._hidden)
return {
'path_head': self.get_path(),
'list': {
'files': files,
'dirs': dirs,
'videos': videos,
'images': images,
'desktops': desktops,
'ungrouped': ungrouped,
'hidden': hidden
}
}
def get_video_icons(self) -> list:
data = []
dir = self.get_current_directory()
for file in self._vids:
img_hash, hash_img_path = self.create_video_thumbnail(full_path=f"{dir}/{file}", returnHashInstead=True)
data.append([img_hash, hash_img_path])
return data
def get_pixbuf_icon_str_combo(self):
data = []
dir = self.get_current_directory()
for file in self._files:
icon = self.create_icon(dir, file).get_pixbuf()
data.append([icon, file])
return data
def get_gtk_icon_str_combo(self) -> list:
data = []
dir = self.get_current_directory()
for file in self._files:
icon = self.create_icon(dir, file)
data.append([icon, file[0]])
return data
def get_current_directory(self) -> str:
return self.get_path()
def get_current_sub_path(self) -> str:
path = self.get_path()
home = f"{self.get_home()}/"
return path.replace(home, "")
def get_end_of_path(self) -> str:
parts = self.get_current_directory().split("/")
size = len(parts)
return parts[size - 1]
def set_hiding_hidden(self, state: bool) -> None:
self._hide_hidden = state
def is_hiding_hidden(self) -> bool:
return self._hide_hidden
def get_dot_dots(self) -> list:
return self._hash_set(['.', '..'])
def get_files(self) -> list:
return self._hash_set(self._files)
def get_dirs(self) -> list:
return self._hash_set(self._dirs)
def get_videos(self) -> list:
return self._hash_set(self._vids)
def get_images(self) -> list:
return self._hash_set(self._images)
def get_desktops(self) -> list:
return self._hash_set(self._desktop)
def get_ungrouped(self) -> list:
return self._hash_set(self._ungrouped)
def get_hidden(self) -> list:
return self._hash_set(self._hidden)
def get_id(self) -> str:
return self._id
def set_wid(self, _wid: str) -> None:
self._wid = _wid
def get_wid(self) -> str:
return self._wid
def set_dir_watcher(self, watcher):
self._dir_watcher = watcher
def get_dir_watcher(self):
return self._dir_watcher
def get_error_message(self):
return self.error_message
def unset_error_message(self):
self.error_message = None
def _atoi(self, text):
return int(text) if text.isdigit() else text
def _natural_keys(self, text):
return [ self._atoi(c) for c in re.split('(\d+)',text) ]
def _hash_text(self, text) -> str:
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
def _hash_set(self, arry: list) -> list:
path = self.get_current_directory()
data = []
for arr in arry:
file = f"{path}/{arr}"
size = get_file_size(file)
data.append([arr, self._hash_text(arr), size])
return data
def _random_with_N_digits(self, n: int) -> int:
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def _generate_id(self) -> str:
self._id = str(self._random_with_N_digits(self._id_length))
def _set_error_message(self, text: str):
self.error_message = text

View File

@@ -0,0 +1,3 @@
"""
Utils module
"""

View File

@@ -0,0 +1,87 @@
# Python imports
import os
import shutil
# Lib imports
# Application imports
class FileHandler:
def create_file(self, nFile, type):
try:
if type == "dir":
os.mkdir(nFile)
elif type == "file":
open(nFile, 'a').close()
except Exception as e:
print("An error occured creating the file/dir:")
print(repr(e))
return False
return True
def update_file(self, oFile, nFile):
try:
print(f"Renaming: {oFile} --> {nFile}")
os.rename(oFile, nFile)
except Exception as e:
print("An error occured renaming the file:")
print(repr(e))
return False
return True
def delete_file(self, to_delete_file):
try:
print(f"Deleting: {to_delete_file}")
if os.path.exists(to_delete_file):
if os.path.isfile(to_delete_file):
os.remove(to_delete_file)
elif os.path.isdir(to_delete_file):
shutil.rmtree(to_delete_file)
else:
print("An error occured deleting the file:")
return False
else:
print("The folder/file does not exist")
return False
except Exception as e:
print("An error occured deleting the file:")
print(repr(e))
return False
return True
def move_file(self, fFile, tFile):
try:
print(f"Moving: {fFile} --> {tFile}")
if os.path.exists(fFile) and not os.path.exists(tFile):
if not tFile.endswith("/"):
tFile += "/"
shutil.move(fFile, tFile)
else:
print("The folder/file does not exist")
return False
except Exception as e:
print("An error occured moving the file:")
print(repr(e))
return False
return True
def copy_file(self, fFile, tFile, symlinks = False, ignore = None):
try:
if os.path.isdir(fFile):
shutil.copytree(fFile, tFile, symlinks, ignore)
else:
shutil.copy2(fFile, tFile)
except Exception as e:
print("An error occured copying the file:")
print(repr(e))
return False
return True

View File

@@ -0,0 +1,116 @@
# Python imports
import os
import subprocess
# Lib imports
# Apoplication imports
class ShellFMLauncherException(Exception):
...
class Launcher:
def open_file_locally(self, file):
lowerName = file.lower()
command = []
if lowerName.endswith(self.fvideos):
command = [self.media_app, file]
elif lowerName.endswith(self.fimages):
command = [self.image_app, file]
elif lowerName.endswith(self.fmusic):
command = [self.music_app, file]
elif lowerName.endswith(self.foffice):
command = [self.office_app, file]
elif lowerName.endswith(self.fcode):
command = [self.code_app, file]
elif lowerName.endswith(self.ftext):
command = [self.text_app, file]
elif lowerName.endswith(self.fpdf):
command = [self.pdf_app, file]
elif lowerName.endswith("placeholder-until-i-can-get-a-use-pref-fm-flag"):
command = [self.file_manager_app, file]
else:
command = ["xdg-open", file]
self.execute(command)
def execute(self, command, start_dir=os.getenv("HOME"), use_shell=False):
try:
self.logger.debug(command)
subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=True, stdout=None, stderr=None, close_fds=True)
except ShellFMLauncherException as e:
self.logger.error(f"Couldn't execute: {command}")
self.logger.error(e)
# TODO: Return std(out/in/err) handlers along with subprocess instead of sinking to null
def execute_and_return_thread_handler(self, command, start_dir=os.getenv("HOME"), use_shell=False):
try:
DEVNULL = open(os.devnull, 'w')
return subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=False, stdout=DEVNULL, stderr=DEVNULL, close_fds=False)
except ShellFMLauncherException as e:
self.logger.error(f"Couldn't execute and return thread: {command}")
self.logger.error(e)
return None
@threaded
def app_chooser_exec(self, app_info, uris):
app_info.launch_uris_async(uris)
def remux_video(self, hash, file):
remux_vid_pth = "{self.REMUX_FOLDER}/{hash}.mp4"
self.logger.debug(remux_vid_pth)
if not os.path.isfile(remux_vid_pth):
self.check_remux_space()
command = ["ffmpeg", "-i", file, "-hide_banner", "-movflags", "+faststart"]
if file.endswith("mkv"):
command += ["-codec", "copy", "-strict", "-2"]
if file.endswith("avi"):
command += ["-c:v", "libx264", "-crf", "21", "-c:a", "aac", "-b:a", "192k", "-ac", "2"]
if file.endswith("wmv"):
command += ["-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-strict", "-2", "-q:a", "100"]
if file.endswith("f4v") or file.endswith("flv"):
command += ["-vcodec", "copy"]
command += [remux_vid_pth]
try:
proc = subprocess.Popen(command)
proc.wait()
except ShellFMLauncherException as e:
self.logger.error(message)
self.logger.error(e)
return False
return True
def check_remux_space(self):
limit = self.remux_folder_max_disk_usage
try:
limit = int(limit)
except ShellFMLauncherException as e:
self.logger.debug(e)
return
usage = self.get_remux_folder_usage(self.REMUX_FOLDER)
if usage > limit:
files = os.listdir(self.REMUX_FOLDER)
for file in files:
fp = os.path.join(self.REMUX_FOLDER, file)
os.unlink(fp)
def get_remux_folder_usage(self, start_path = "."):
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp): # Skip if it is symbolic link
total_size += os.path.getsize(fp)
return total_size

View File

@@ -0,0 +1,101 @@
# Python imports
import json
import os
from os import path
# Lib imports
# Apoplication imports
class ShellFMSettingsException(Exception):
...
class Settings:
logger = None
# NOTE: app_name should be defined using python 'builtins'
app_name_exists = False
try:
app_name
app_name_exists = True
except Exception as e:
...
APP_CONTEXT = f"{app_name.lower()}" if app_name_exists else "shellfm"
USR_APP_CONTEXT = f"/usr/share/{APP_CONTEXT}"
USER_HOME = path.expanduser('~')
CONFIG_PATH = f"{USER_HOME}/.config/{APP_CONTEXT}"
CONFIG_FILE = f"{CONFIG_PATH}/settings.json"
HIDE_HIDDEN_FILES = True
DEFAULT_ICONS = f"{CONFIG_PATH}/icons"
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
BLENDER_THUMBNLR = f"{CONFIG_PATH}/blender-thumbnailer" # Blender thumbnail generator binary
REMUX_FOLDER = f"{USER_HOME}/.remuxs" # Remuxed files folder
ICON_DIRS = ["/usr/share/icons", f"{USER_HOME}/.icons" "/usr/share/pixmaps"]
BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails"
ABS_THUMBS_PTH = f"{BASE_THUMBS_PTH}/normal"
STEAM_ICONS_PTH = f"{BASE_THUMBS_PTH}/steam_icons"
if not os.path.exists(CONFIG_PATH) or not os.path.exists(CONFIG_FILE):
msg = f"No config file located! Aborting loading ShellFM library...\nExpected: {CONFIG_FILE}"
raise ShellFMSettingsException(msg)
if not path.isdir(REMUX_FOLDER):
os.mkdir(REMUX_FOLDER)
if not path.isdir(BASE_THUMBS_PTH):
os.mkdir(BASE_THUMBS_PTH)
if not path.isdir(ABS_THUMBS_PTH):
os.mkdir(ABS_THUMBS_PTH)
if not path.isdir(STEAM_ICONS_PTH):
os.mkdir(STEAM_ICONS_PTH)
if not os.path.exists(DEFAULT_ICONS):
DEFAULT_ICONS = f"{USR_APP_CONTEXT}/icons"
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
with open(CONFIG_FILE) as f:
settings = json.load(f)
config = settings["config"]
subpath = config["base_of_home"]
STEAM_CDN_URL = config["steam_cdn_url"]
FFMPG_THUMBNLR = FFMPG_THUMBNLR if config["thumbnailer_path"] == "" else config["thumbnailer_path"]
BLENDER_THUMBNLR = BLENDER_THUMBNLR if config["blender_thumbnailer_path"] == "" else config["blender_thumbnailer_path"]
HIDE_HIDDEN_FILES = True if config["hide_hidden_files"] in ["true", ""] else False
go_past_home = True if config["go_past_home"] in ["true", ""] else False
lock_folder = False if config["lock_folder"] in ["false", ""] else True
locked_folders = config["locked_folders"].split("::::")
mplayer_options = config["mplayer_options"].split()
music_app = config["music_app"]
media_app = config["media_app"]
image_app = config["image_app"]
office_app = config["office_app"]
pdf_app = config["pdf_app"]
code_app = config["code_app"]
text_app = config["text_app"]
terminal_app = config["terminal_app"]
container_icon_wh = config["container_icon_wh"]
video_icon_wh = config["video_icon_wh"]
sys_icon_wh = config["sys_icon_wh"]
file_manager_app = config["file_manager_app"]
remux_folder_max_disk_usage = config["remux_folder_max_disk_usage"]
# Filters
filters = settings["filters"]
fmeshs = tuple(filters["meshs"])
fcode = tuple(filters["code"])
fvideos = tuple(filters["videos"])
foffice = tuple(filters["office"])
fimages = tuple(filters["images"])
ftext = tuple(filters["text"])
fmusic = tuple(filters["music"])
fpdf = tuple(filters["pdf"])

View File

@@ -0,0 +1,93 @@
# Python imports
from random import randint
# Lib imports
# Application imports
from .tabs.tab import Tab
class Window:
def __init__(self):
self._id_length: int = 10
self._id: str = ""
self._name: str = ""
self._nickname:str = ""
self._isHidden: bool = False
self._active_tab: int = 0
self._tabs: list = []
self._generate_id()
self._set_name()
def create_tab(self) -> Tab:
tab = Tab()
self._tabs.append(tab)
return tab
def pop_tab(self) -> None:
self._tabs.pop()
def delete_tab_by_id(self, tid: str):
for tab in self._tabs:
if tab.get_id() == tid:
self._tabs.remove(tab)
break
def get_tab_by_id(self, tid: str) -> Tab:
for tab in self._tabs:
if tab.get_id() == tid:
return tab
def get_tab_by_index(self, index) -> Tab:
return self._tabs[index]
def get_tabs_count(self) -> int:
return len(self._tabs)
def get_all_tabs(self) -> list:
return self._tabs
def get_id(self) -> str:
return self._id
def get_name(self) -> str:
return self._name
def get_nickname(self) -> str:
return self._nickname
def is_hidden(self) -> bool:
return self._isHidden
def list_files_from_tabs(self) -> None:
for tab in self._tabs:
print(tab.get_files())
def set_active_tab(self, index: int):
self._active_tab = index
def get_active_tab(self) -> Tab:
return self._tabs[self._active_tab]
def set_nickname(self, nickname):
self._nickname = f"{nickname}"
def set_is_hidden(self, state):
self._isHidden = f"{state}"
def _set_name(self):
self._name = "window_" + self.get_id()
def _random_with_N_digits(self, n):
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def _generate_id(self):
self._id = str(self._random_with_N_digits(self._id_length))

View File

@@ -0,0 +1,3 @@
"""
Utils module
"""

View File

@@ -0,0 +1,52 @@
# Python imports
# Lib imports
# Application imports
# Break into a Python console upon SIGUSR1 (Linux) or SIGBREAK (Windows:
# CTRL+Pause/Break). To be included in all production code, just in case.
def debug_signal_handler(signal, frame):
del signal
del frame
try:
import rpdb2
logger.debug("\n\nStarting embedded RPDB2 debugger. Password is 'foobar'\n\n")
rpdb2.start_embedded_debugger("foobar", True, True)
rpdb2.setbreak(depth=1)
return
except StandardError:
...
try:
from rfoo.utils import rconsole
logger.debug("\n\nStarting embedded rconsole debugger...\n\n")
rconsole.spawn_server()
return
except StandardError as ex:
...
try:
from pudb import set_trace
logger.debug("\n\nStarting PuDB debugger...\n\n")
set_trace(paused = True)
return
except StandardError as ex:
...
try:
import pdb
logger.debug("\n\nStarting embedded PDB debugger...\n\n")
pdb.Pdb(skip=['gi.*']).set_trace()
return
except StandardError as ex:
...
try:
import code
code.interact()
except StandardError as ex:
logger.debug(f"{ex}, returning to normal program flow...")

View File

@@ -0,0 +1,73 @@
# Python imports
from collections import defaultdict
# Lib imports
# Application imports
from .singleton import Singleton
class EventSystem(Singleton):
""" Create event system. """
def __init__(self):
self.subscribers = defaultdict(list)
self._is_paused = False
self._subscribe_to_events()
def _subscribe_to_events(self):
self.subscribe("pause_event_processing", self._pause_processing_events)
self.subscribe("resume_event_processing", self._resume_processing_events)
def _pause_processing_events(self):
self._is_paused = True
def _resume_processing_events(self):
self._is_paused = False
def subscribe(self, event_type, fn):
self.subscribers[event_type].append(fn)
def unsubscribe(self, event_type, fn):
self.subscribers[event_type].remove(fn)
def unsubscribe_all(self, event_type):
self.subscribers.pop(event_type, None)
def emit(self, event_type, data = None):
if self._is_paused and event_type != "resume_event_processing":
return
if event_type in self.subscribers:
for fn in self.subscribers[event_type]:
if data:
if hasattr(data, '__iter__') and not type(data) is str:
fn(*data)
else:
fn(data)
else:
fn()
def emit_and_await(self, event_type, data = None):
if self._is_paused and event_type != "resume_event_processing":
return
""" NOTE: Should be used when signal has only one listener and vis-a-vis """
if event_type in self.subscribers:
response = None
for fn in self.subscribers[event_type]:
if data:
if hasattr(data, '__iter__') and not type(data) is str:
response = fn(*data)
else:
response = fn(data)
else:
response = fn()
if not response in (None, ''):
break
return response

View File

@@ -0,0 +1,126 @@
# Python imports
import os
import time
from multiprocessing.connection import Client
from multiprocessing.connection import Listener
# Lib imports
from gi.repository import GLib
# Application imports
from .singleton import Singleton
class IPCServer(Singleton):
""" 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 = "socket"):
self.is_ipc_alive = False
self._ipc_port = 4848
self._ipc_address = ipc_address
self._conn_type = conn_type
self._ipc_authkey = b'' + bytes(f'{app_name}-ipc', 'utf-8')
self._ipc_timeout = 15.0
if conn_type == "socket":
self._ipc_address = f'/tmp/{app_name}-ipc.sock'
elif conn_type == "full_network":
self._ipc_address = '0.0.0.0'
elif conn_type == "full_network_unsecured":
self._ipc_authkey = None
self._ipc_address = '0.0.0.0'
elif conn_type == "local_network_unsecured":
self._ipc_authkey = None
self._subscribe_to_events()
def _subscribe_to_events(self):
event_system.subscribe("post_file_to_ipc", self.send_ipc_message)
def create_ipc_listener(self) -> None:
if self._conn_type == "socket":
if os.path.exists(self._ipc_address) and settings_manager.is_dirty_start():
os.unlink(self._ipc_address)
listener = Listener(address = self._ipc_address, family = "AF_UNIX", authkey = self._ipc_authkey)
elif "unsecured" not in self._conn_type:
listener = Listener((self._ipc_address, self._ipc_port), authkey = self._ipc_authkey)
else:
listener = Listener((self._ipc_address, self._ipc_port))
self.is_ipc_alive = True
self._run_ipc_loop(listener)
@daemon_threaded
def _run_ipc_loop(self, listener) -> None:
while True:
try:
conn = listener.accept()
start_time = time.perf_counter()
GLib.idle_add(self._handle_ipc_message, *(conn, start_time,))
except Exception as e:
logger.debug( repr(e) )
listener.close()
def _handle_ipc_message(self, conn, start_time) -> None:
while True:
msg = conn.recv()
logger.debug(msg)
if "FILE|" in msg:
file = msg.split("FILE|")[1].strip()
if file:
event_system.emit("handle_file_from_ipc", file)
conn.close()
break
if msg in ['close connection', 'close server']:
conn.close()
break
# NOTE: Not perfect but insures we don't lock up the connection for too long.
end_time = time.perf_counter()
if (end_time - start_time) > self._ipc_timeout:
conn.close()
break
def send_ipc_message(self, message: str = "Empty Data...") -> None:
try:
if self._conn_type == "socket":
conn = Client(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey)
elif "unsecured" not in self._conn_type:
conn = Client((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey)
else:
conn = Client((self._ipc_address, self._ipc_port))
conn.send(message)
conn.close()
except ConnectionRefusedError as e:
logger.error("Connection refused...")
except Exception as e:
logger.error( repr(e) )
def send_test_ipc_message(self, message: str = "Empty Data...") -> None:
try:
if self._conn_type == "socket":
conn = Client(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey)
elif "unsecured" not in self._conn_type:
conn = Client((self._ipc_address, self._ipc_port), authkey=self._ipc_authkey)
else:
conn = Client((self._ipc_address, self._ipc_port))
conn.send(message)
conn.close()
except ConnectionRefusedError as e:
if self._conn_type == "socket":
logger.error("IPC Socket no longer valid.... Removing.")
os.unlink(self._ipc_address)
except Exception as e:
logger.error( repr(e) )

View File

@@ -0,0 +1,138 @@
# Python imports
import re
# Gtk imports
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
# Application imports
from .singleton import Singleton
def logger(log = ""):
print(log)
class KeymapError(Exception):
""" Custom exception for errors in keybinding configurations """
MODIFIER = re.compile('<([^<]+)>')
class Keybindings(Singleton):
""" Class to handle loading and lookup of Terminator keybindings """
modifiers = {
'ctrl': Gdk.ModifierType.CONTROL_MASK,
'control': Gdk.ModifierType.CONTROL_MASK,
'primary': Gdk.ModifierType.CONTROL_MASK,
'shift': Gdk.ModifierType.SHIFT_MASK,
'alt': Gdk.ModifierType.MOD1_MASK,
'super': Gdk.ModifierType.SUPER_MASK,
'hyper': Gdk.ModifierType.HYPER_MASK,
'mod2': Gdk.ModifierType.MOD2_MASK
}
empty = {}
keys = None
_masks = None
_lookup = None
def __init__(self):
self.keymap = Gdk.Keymap.get_default()
self.configure({})
def print_keys(self):
print(self.keys)
def append_bindings(self, combos):
""" Accept new binding(s) and reload """
for item in combos:
method, keys = item.split(":")
self.keys[method] = keys
self.reload()
def configure(self, bindings):
""" Accept new bindings and reconfigure with them """
self.keys = bindings
self.reload()
def reload(self):
""" Parse bindings and mangle into an appropriate form """
self._lookup = {}
self._masks = 0
for action, bindings in list(self.keys.items()):
if isinstance(bindings, list):
bindings = (*bindings,)
elif not isinstance(bindings, tuple):
bindings = (bindings,)
for binding in bindings:
if not binding or binding == "None":
continue
try:
keyval, mask = self._parsebinding(binding)
# Does much the same, but with poorer error handling.
# keyval, mask = Gtk.accelerator_parse(binding)
except KeymapError as e:
logger(f"Keybinding reload failed to parse binding '{binding}': {e}")
else:
if mask & Gdk.ModifierType.SHIFT_MASK:
if keyval == Gdk.KEY_Tab:
keyval = Gdk.KEY_ISO_Left_Tab
mask &= ~Gdk.ModifierType.SHIFT_MASK
else:
keyvals = Gdk.keyval_convert_case(keyval)
if keyvals[0] != keyvals[1]:
keyval = keyvals[1]
mask &= ~Gdk.ModifierType.SHIFT_MASK
else:
keyval = Gdk.keyval_to_lower(keyval)
self._lookup.setdefault(mask, {})
self._lookup[mask][keyval] = action
self._masks |= mask
def _parsebinding(self, binding):
""" Parse an individual binding using Gtk's binding function """
mask = 0
modifiers = re.findall(MODIFIER, binding)
if modifiers:
for modifier in modifiers:
mask |= self._lookup_modifier(modifier)
key = re.sub(MODIFIER, '', binding)
if key == '':
raise KeymapError('No key found!')
keyval = Gdk.keyval_from_name(key)
if keyval == 0:
raise KeymapError(f"Key '{key}' is unrecognised...")
return (keyval, mask)
def _lookup_modifier(self, modifier):
""" Map modifier names to gtk values """
try:
return self.modifiers[modifier.lower()]
except KeyError:
raise KeymapError(f"Unhandled modifier '<{modifier}>'")
def lookup(self, event):
""" Translate a keyboard event into a mapped key """
try:
_found, keyval, _egp, _lvl, consumed = self.keymap.translate_keyboard_state(
event.hardware_keycode,
Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK),
event.group)
except TypeError:
logger("Keybinding lookup failed to translate keyboard event: {dir(event)}")
return None
mask = (event.get_state() & ~consumed) & self._masks
return self._lookup.get(mask, self.empty).get(keyval, None)

View File

@@ -0,0 +1,59 @@
# Python imports
import os
import logging
# Application imports
from .singleton import Singleton
class Logger(Singleton):
"""
Create a new logging object and return it.
:note:
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
Log Levels (From least to most)
Type Value
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
:param loggerName: Sets the name of the logger object. (Used in log lines)
:param createFile: Whether we create a log file or just pump to terminal
:return: the logging object we created
"""
def __init__(self, config_path: str, _ch_log_lvl = logging.CRITICAL, _fh_log_lvl = logging.INFO):
self._CONFIG_PATH = config_path
self.global_lvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
self.ch_log_lvl = _ch_log_lvl # Prety much the only one we ever change
self.fh_log_lvl = _fh_log_lvl
def get_logger(self, loggerName: str = "NO_LOGGER_NAME_PASSED", createFile: bool = True) -> logging.Logger:
log = logging.getLogger(loggerName)
log.setLevel(self.global_lvl)
# Set our log output styles
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setLevel(level=self.ch_log_lvl)
ch.setFormatter(cFormatter)
log.addHandler(ch)
if createFile:
folder = self._CONFIG_PATH
file = f"{folder}/application.log"
if not os.path.exists(folder):
os.mkdir(folder)
fh = logging.FileHandler(file)
fh.setLevel(level=self.fh_log_lvl)
fh.setFormatter(fFormatter)
log.addHandler(fh)
return log

View File

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

View File

@@ -0,0 +1,167 @@
# Python imports
import inspect
import json
from os import path
from os import mkdir
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
# Application imports
from ..singleton import Singleton
from .start_check_mixin import StartCheckMixin
from .options.settings import Settings
class MissingConfigError(Exception):
pass
class SettingsManager(StartCheckMixin, Singleton):
def __init__(self):
self._SCRIPT_PTH = path.dirname(path.realpath(__file__))
self._USER_HOME = path.expanduser('~')
self._USR_PATH = f"/usr/share/{app_name.lower()}"
self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json"
self._HOME_CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}"
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._WINDOW_ICON = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png"
self._UI_WIDEGTS_PATH = f"{self._HOME_CONFIG_PATH}/ui_widgets"
self._CONTEXT_MENU = f"{self._HOME_CONFIG_PATH}/contexct_menu.json"
self._TRASH_FILES_PATH = f"{GLib.get_user_data_dir()}/Trash/files"
self._TRASH_INFO_PATH = f"{GLib.get_user_data_dir()}/Trash/info"
self._ICON_THEME = Gtk.IconTheme.get_default()
if not path.exists(self._HOME_CONFIG_PATH):
mkdir(self._HOME_CONFIG_PATH)
if not path.exists(self._PLUGINS_PATH):
mkdir(self._PLUGINS_PATH)
if not path.exists(self._DEFAULT_ICONS):
self._DEFAULT_ICONS = f"{self._USR_PATH}/icons"
if not path.exists(self._DEFAULT_ICONS):
raise MissingConfigError("Unable to find the application icons directory.")
if not path.exists(self._GLADE_FILE):
self._GLADE_FILE = f"{self._USR_PATH}/Main_Window.glade"
if not path.exists(self._GLADE_FILE):
raise MissingConfigError("Unable to find the application Glade file.")
if not path.exists(self._KEY_BINDINGS_FILE):
self._KEY_BINDINGS_FILE = f"{self._USR_PATH}/key-bindings.json"
if not path.exists(self._KEY_BINDINGS_FILE):
raise MissingConfigError("Unable to find the application Keybindings file.")
if not path.exists(self._CSS_FILE):
self._CSS_FILE = f"{self._USR_PATH}/stylesheet.css"
if not path.exists(self._CSS_FILE):
raise MissingConfigError("Unable to find the application Stylesheet file.")
if not path.exists(self._WINDOW_ICON):
self._WINDOW_ICON = f"{self._USR_PATH}/icons/{app_name.lower()}.png"
if not path.exists(self._WINDOW_ICON):
raise MissingConfigError("Unable to find the application icon.")
if not path.exists(self._UI_WIDEGTS_PATH):
self._UI_WIDEGTS_PATH = f"{self._USR_PATH}/ui_widgets"
if not path.exists(self._CONTEXT_MENU):
self._CONTEXT_MENU = f"{self._USR_PATH}/contexct_menu.json"
try:
with open(self._KEY_BINDINGS_FILE) as file:
bindings = json.load(file)["keybindings"]
keybindings.configure(bindings)
except Exception as e:
print( f"Settings: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" )
try:
with open(self._CONTEXT_MENU) as file:
self._context_menu_data = json.load(file)
except Exception as e:
print( f"Settings: {self._CONTEXT_MENU}\n\t\t{repr(e)}" )
self.settings: Settings = None
self._main_window = None
self._main_window_w = 1670
self._main_window_h = 830
self._builder = None
self.PAINT_BG_COLOR = (0, 0, 0, 0.0)
self._trace_debug = False
self._debug = False
self._dirty_start = False
def register_signals_to_builder(self, classes=None, builder=None):
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
print(repr(e))
builder.connect_signals(handlers)
def get_monitor_data(self) -> list:
screen = self._main_window.get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
monitors.append(screen.get_monitor_geometry(m))
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
return monitors
def set_builder(self, builder) -> any: self._builder = builder
def set_main_window(self, window): self._main_window = window
def get_main_window(self) -> Gtk.ApplicationWindow: return self._main_window
def get_main_window_width(self) -> Gtk.ApplicationWindow: return self._main_window_w
def get_main_window_height(self) -> Gtk.ApplicationWindow: return self._main_window_h
def get_builder(self) -> Gtk.Builder: return self._builder
def get_paint_bg_color(self) -> list: return self.PAINT_BG_COLOR
def get_glade_file(self) -> str: return self._GLADE_FILE
def get_icon_theme(self) -> str: return self._ICON_THEME
def get_css_file(self) -> str: return self._CSS_FILE
def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH
def get_window_icon(self) -> str: return self._WINDOW_ICON
def get_context_menu_data(self) -> str: return self._context_menu_data
def get_home_path(self) -> str: return self._USER_HOME
def get_ui_widgets_path(self) -> str: return self._UI_WIDEGTS_PATH
def get_trash_files_path(self) -> str: return self._TRASH_FILES_PATH
def get_trash_info_path(self) -> str: return self._TRASH_INFO_PATH
def get_plugins_path(self) -> str: return self._PLUGINS_PATH
def is_trace_debug(self) -> bool: return self._trace_debug
def is_debug(self) -> bool: return self._debug
def set_trace_debug(self, trace_debug: bool):
self._trace_debug = trace_debug
def set_debug(self, debug: bool):
self._debug = debug
def load_settings(self):
if not path.exists(self._CONFIG_FILE):
self.settings = Settings()
return
with open(self._CONFIG_FILE) as file:
data = json.load(file)
data["load_defaults"] = False
self.settings = Settings(**data)
def save_settings(self):
with open(self._CONFIG_FILE, 'w') as outfile:
json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4)

View File

@@ -0,0 +1,8 @@
"""
Options module
"""
from .settings import Settings
from .config import Config
from .filters import Filters
from .theming import Theming
from .debugging import Debugging

View File

@@ -0,0 +1,36 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
@dataclass
class Config:
base_of_home: str = ""
hide_hidden_files: str = "true"
thumbnailer_path: str = "ffmpegthumbnailer"
blender_thumbnailer_path: str = ""
go_past_home: str = "true"
lock_folder: str = "false"
locked_folders: list = field(default_factory=lambda: ["venv", "flasks"])
mplayer_options: str = "-quiet -really-quiet -xy 1600 -geometry 50%:50%"
music_app: str = "deadbeef"
media_app: str = "mpv"
image_app: str = "mirage"
office_app: str = "libreoffice"
pdf_app: str = "evince"
code_app: str = "atom"
text_app: str = "mousepad"
terminal_app: str = "terminator"
file_manager_app: str = "solarfm"
container_icon_wh: list = field(default_factory=lambda: [128, 128])
video_icon_wh: list = field(default_factory=lambda: [128, 64])
sys_icon_wh: list = field(default_factory=lambda: [56, 56])
steam_cdn_url: str = "https://steamcdn-a.akamaihd.net/steam/apps/"
remux_folder_max_disk_usage: str = "8589934592"
application_dirs: list = field(default_factory=lambda: [
"/usr/share/applications",
f"{settings_manager.get_home_path()}/.local/share/applications"
])

View File

@@ -0,0 +1,12 @@
# Python imports
from dataclasses import dataclass
# Lib imports
# Application imports
@dataclass
class Debugging:
ch_log_lvl: int = 10
fh_log_lvl: int = 20

View File

@@ -0,0 +1,90 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
@dataclass
class Filters:
meshs: list = field(default_factory=lambda: [
".blend",
".dae",
".fbx",
".gltf",
".obj",
".stl"
])
code: list = field(default_factory=lambda: [
".cpp",
".css",
".c",
".go",
".html",
".htm",
".java",
".js",
".json",
".lua",
".md",
".py",
".rs",
".toml",
".xml",
".pom"
])
videos: list = field(default_factory=lambda:[
".mkv",
".mp4",
".webm",
".avi",
".mov",
".m4v",
".mpg",
".mpeg",
".wmv",
".flv"
])
office: list = field(default_factory=lambda: [
".doc",
".docx",
".xls",
".xlsx",
".xlt",
".xltx",
".xlm",
".ppt",
".pptx",
".pps",
".ppsx",
".odt",
".rtf"
])
images: list = field(default_factory=lambda: [
".png",
".jpg",
".jpeg",
".gif",
".ico",
".tga",
".webp"
])
text: list = field(default_factory=lambda: [
".txt",
".text",
".sh",
".cfg",
".conf",
".log"
])
music: list = field(default_factory=lambda: [
".psf",
".mp3",
".ogg",
".flac",
".m4a"
])
pdf: list = field(default_factory=lambda: [
".pdf"
])

View File

@@ -0,0 +1,31 @@
# Python imports
from dataclasses import dataclass, field
from dataclasses import asdict
# Gtk imports
# Application imports
from .config import Config
from .filters import Filters
from .theming import Theming
from .debugging import Debugging
@dataclass
class Settings:
load_defaults: bool = True
config: Config = field(default_factory=lambda: Config())
filters: Filters = field(default_factory=lambda: Filters())
theming: Theming = field(default_factory=lambda: Theming())
debugging: Debugging = field(default_factory=lambda: Debugging())
def __post_init__(self):
if not self.load_defaults:
self.load_defaults = False
self.config = Config(**self.config)
self.filters = Filters(**self.filters)
self.theming = Theming(**self.theming)
self.debugging = Debugging(**self.debugging)
def as_dict(self):
return asdict(self)

View File

@@ -0,0 +1,13 @@
# Python imports
from dataclasses import dataclass
# Lib imports
# Application imports
@dataclass
class Theming:
success_color: str = "#88cc27"
warning_color: str = "#ffa800"
error_color: str = "#ff0000"

View File

@@ -0,0 +1,51 @@
# Python imports
import os
import json
import inspect
# Lib imports
# Application imports
class StartCheckMixin:
def is_dirty_start(self) -> bool: return self._dirty_start
def clear_pid(self): self._clean_pid()
def do_dirty_start_check(self):
if not os.path.exists(self._PID_FILE):
self._write_new_pid()
else:
with open(self._PID_FILE, "r") as _pid:
pid = _pid.readline().strip()
if pid not in ("", None):
self._check_alive_status(int(pid))
else:
self._write_new_pid()
""" Check For the existence of a unix pid. """
def _check_alive_status(self, pid):
print(f"PID Found: {pid}")
try:
os.kill(pid, 0)
except OSError:
print(f"{app_name} is starting dirty...")
self._dirty_start = True
self._write_new_pid()
return
print("PID is alive... Let downstream errors (sans debug args) handle app closure propigation.")
def _write_new_pid(self):
pid = os.getpid()
self._write_pid(pid)
print(f"{app_name} PID: {pid}")
def _clean_pid(self):
os.unlink(self._PID_FILE)
def _write_pid(self, pid):
with open(self._PID_FILE, "w") as _pid:
_pid.write(f"{pid}")

Some files were not shown because too many files have changed in this diff Show More