develop #11
| @@ -4,11 +4,18 @@ SolarFM is a Gtk+ Python file manager. | ||||
| # Notes | ||||
| <b>Still Work in  progress! Use at own risk!</b> | ||||
|  | ||||
| Additionally, if not building a .deb then just move the contents of user_config to their respective folders. | ||||
| Copy the share/solarfm folder to your user .config/ directory too. | ||||
|  | ||||
| <h6>Install Setup</h6> | ||||
| ``` | ||||
| sudo apt-get install python3.8 wget python3-setproctitle python3-gi ffmpegthumbnailer steamcmd | ||||
| ``` | ||||
|  | ||||
| # Known Issues | ||||
| Doing Ctrl+D when in Terminator (maybe other terminals too) somehow propagates the signal to SolarFM too. | ||||
| A selected file in the active quad-pane will move to trash since it is the defaul keybinding for that action. | ||||
|  | ||||
| # TODO | ||||
| <ul> | ||||
| <li>Add simpleish preview plugin for various file types.</li> | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| Remove me... | ||||
| @@ -1,12 +1,11 @@ | ||||
| # Python imports | ||||
| import ctypes | ||||
| import time | ||||
| import threading | ||||
| import subprocess | ||||
| import signal | ||||
| import json | ||||
| import shlex | ||||
| from datetime import datetime | ||||
| libgcc_s = ctypes.CDLL('libgcc_s.so.1') | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
|   | ||||
| @@ -67,21 +67,26 @@ def grep_search(target=None, query=None): | ||||
|     collection = {} | ||||
|  | ||||
|     for line in proc_data: | ||||
|         file, line_no, data = line.split(":", 2) | ||||
|         b64_file = base64.urlsafe_b64encode(file.encode('utf-8')).decode('utf-8') | ||||
|         b64_data = base64.urlsafe_b64encode(data.encode('utf-8')).decode('utf-8') | ||||
|         try: | ||||
|             parts    = line.split(":", 2) | ||||
|             if not len(parts) == 3: | ||||
|                 continue | ||||
|  | ||||
|         if b64_file in collection.keys(): | ||||
|             collection[f"{b64_file}"][f"{line_no}"] = b64_data | ||||
|         else: | ||||
|             collection[f"{b64_file}"] = {} | ||||
|             collection[f"{b64_file}"] = { f"{line_no}": b64_data} | ||||
|             file, line_no, data = parts | ||||
|             b64_file = base64.urlsafe_b64encode(file.encode('utf-8')).decode('utf-8') | ||||
|             b64_data = base64.urlsafe_b64encode(data.encode('utf-8')).decode('utf-8') | ||||
|  | ||||
|     try: | ||||
|         data = f"GREP|{ts}|{json.dumps(collection, separators=(',', ':'), indent=4)}" | ||||
|         send_ipc_message(data) | ||||
|     except Exception as e: | ||||
|         ... | ||||
|             if b64_file in collection.keys(): | ||||
|                 collection[f"{b64_file}"][f"{line_no}"] = b64_data | ||||
|             else: | ||||
|                 collection[f"{b64_file}"] = {} | ||||
|                 collection[f"{b64_file}"] = { f"{line_no}": b64_data} | ||||
|  | ||||
|  | ||||
|             data = f"GREP|{ts}|{json.dumps(collection, separators=(',', ':'), indent=4)}" | ||||
|             send_ipc_message(data) | ||||
|         except Exception as e: | ||||
|             traceback.print_exc() | ||||
|  | ||||
|     collection = {} | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| #!/bin/bash | ||||
| #postrm (script executed after uninstalling the package) | ||||
| #set -e | ||||
| # Note: postrm (script executed after uninstalling the package) | ||||
| # set -e | ||||
|  | ||||
|  | ||||
| if [ -f /bin/solarfm ]; then | ||||
|     rm /bin/solarfm | ||||
| fi | ||||
|  | ||||
| if [ -d /opt/SolarFM ]; then | ||||
|     rm -rf /opt/SolarFM | ||||
| if [ -f /opt/solarfm.zip ]; then | ||||
|     rm /opt/solarfm.zip | ||||
| fi | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/bin/solarfm
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -1,74 +0,0 @@ | ||||
| # Python imports | ||||
| import builtins | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
| from ipc_server import IPCServer | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class EventSystem(IPCServer): | ||||
|     """ Inheret IPCServerMixin. Create an pub/sub systems. """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         super(EventSystem, self).__init__() | ||||
|  | ||||
|         # NOTE: The format used is list of [type, target, (data,)] Where: | ||||
|         #             type is useful context for control flow, | ||||
|         #             target is the method to call, | ||||
|         #             data is the method parameters to give | ||||
|         #       Where data may be any kind of data | ||||
|         self._gui_events    = [] | ||||
|         self._module_events = [] | ||||
|  | ||||
|  | ||||
|  | ||||
|     # Makeshift fake "events" type system FIFO | ||||
|     def _pop_gui_event(self): | ||||
|         if len(self._gui_events) > 0: | ||||
|             return self._gui_events.pop(0) | ||||
|         return None | ||||
|  | ||||
|     def _pop_module_event(self): | ||||
|         if len(self._module_events) > 0: | ||||
|             return self._module_events.pop(0) | ||||
|         return None | ||||
|  | ||||
|  | ||||
|     def push_gui_event(self, event): | ||||
|         if len(event) == 3: | ||||
|             self._gui_events.append(event) | ||||
|             return None | ||||
|  | ||||
|         raise Exception("Invald event format! Please do:  [type, target, (data,)]") | ||||
|  | ||||
|     def push_module_event(self, event): | ||||
|         if len(event) == 3: | ||||
|             self._module_events.append(event) | ||||
|             return None | ||||
|  | ||||
|         raise Exception("Invald event format! Please do:  [type, target, (data,)]") | ||||
|  | ||||
|     def read_gui_event(self): | ||||
|         return self._gui_events[0] | ||||
|  | ||||
|     def read_module_event(self): | ||||
|         return self._module_events[0] | ||||
|  | ||||
|     def consume_gui_event(self): | ||||
|         return self._pop_gui_event() | ||||
|  | ||||
|     def consume_module_event(self): | ||||
|         return self._pop_module_event() | ||||
|  | ||||
|  | ||||
|  | ||||
| # NOTE: Just reminding myself we can add to builtins two different ways... | ||||
| # __builtins__.update({"event_system": Builtins()}) | ||||
| builtins.app_name          = "SolarFM" | ||||
| builtins.event_system      = EventSystem() | ||||
| builtins.event_sleep_time  = 0.2 | ||||
| builtins.debug             = False | ||||
| builtins.trace_debug       = False | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Base module | ||||
| """ | ||||
| @@ -1,42 +0,0 @@ | ||||
| #!/usr/bin/python3 | ||||
|  | ||||
|  | ||||
| # Python imports | ||||
| import argparse, faulthandler, traceback | ||||
| from setproctitle import setproctitle | ||||
|  | ||||
| import tracemalloc | ||||
| tracemalloc.start() | ||||
|  | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk | ||||
|  | ||||
| # Application imports | ||||
| from app import Application | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     """ Set process title, get arguments, and create GTK main thread. """ | ||||
|  | ||||
|     try: | ||||
|         # import web_pdb | ||||
|         # web_pdb.set_trace() | ||||
|  | ||||
|         setproctitle('SolarFM') | ||||
|         faulthandler.enable()  # For better debug info | ||||
|         parser = argparse.ArgumentParser() | ||||
|         # Add long and short arguments | ||||
|         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() | ||||
|  | ||||
|         Application(args, unknownargs) | ||||
|         Gtk.main() | ||||
|     except Exception as e: | ||||
|         traceback.print_exc() | ||||
|         quit() | ||||
| @@ -1,55 +0,0 @@ | ||||
| # Python imports | ||||
| import os, inspect, time | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
| from utils.settings import Settings | ||||
| from context.controller import Controller | ||||
| from __builtins__ import EventSystem | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class Application(EventSystem): | ||||
|     """ Create Settings and Controller classes. Bind signal to Builder. Inherit from Builtins to bind global methods and classes. """ | ||||
|  | ||||
|     def __init__(self, args, unknownargs): | ||||
|         if not trace_debug: | ||||
|             event_system.create_ipc_server() | ||||
|             time.sleep(0.1) | ||||
|  | ||||
|             if not event_system.is_ipc_alive: | ||||
|                 if unknownargs: | ||||
|                     for arg in unknownargs: | ||||
|                         if os.path.isdir(arg): | ||||
|                             message = f"FILE|{arg}" | ||||
|                             event_system.send_ipc_message(message) | ||||
|  | ||||
|                 if args.new_tab and os.path.isdir(args.new_tab): | ||||
|                     message = f"FILE|{args.new_tab}" | ||||
|                     event_system.send_ipc_message(message) | ||||
|  | ||||
|                 raise Exception("IPC Server Exists: Will send path(s) to it and close...") | ||||
|  | ||||
|  | ||||
|         settings = Settings() | ||||
|         settings.create_window() | ||||
|  | ||||
|         controller = Controller(args, unknownargs, settings) | ||||
|         if not controller: | ||||
|             raise Exception("Controller exited and doesn't exist...") | ||||
|  | ||||
|         # Gets the methods from the classes and sets to handler. | ||||
|         # Then, builder connects to any signals it needs. | ||||
|         classes  = [controller] | ||||
|         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)) | ||||
|  | ||||
|         settings.builder.connect_signals(handlers) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
|     Gtk Bound Signal Module | ||||
| """ | ||||
| @@ -1,171 +0,0 @@ | ||||
| # Python imports | ||||
| import os, gc, threading, time | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk, GLib | ||||
|  | ||||
| # Application imports | ||||
| from .mixins.exception_hook_mixin import ExceptionHookMixin | ||||
| from .mixins.ui_mixin import UIMixin | ||||
| from .signals.ipc_signals_mixin import IPCSignalsMixin | ||||
| from .signals.keyboard_signals_mixin import KeyboardSignalsMixin | ||||
| from .controller_data import Controller_Data | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, ExceptionHookMixin, Controller_Data): | ||||
|     """ Controller coordinates the mixins and is somewhat the root hub of it all. """ | ||||
|     def __init__(self, args, unknownargs, _settings): | ||||
|         self.setup_controller_data(_settings) | ||||
|         self.window.show() | ||||
|  | ||||
|         self.generate_windows(self.state) | ||||
|         self.plugins.launch_plugins() | ||||
|  | ||||
|         if debug: | ||||
|             self.window.set_interactive_debugging(True) | ||||
|  | ||||
|         if not trace_debug: | ||||
|             self.gui_event_observer() | ||||
|  | ||||
|             if unknownargs: | ||||
|                 for arg in unknownargs: | ||||
|                     if os.path.isdir(arg): | ||||
|                         message = f"FILE|{arg}" | ||||
|                         event_system.send_ipc_message(message) | ||||
|  | ||||
|             if args.new_tab and os.path.isdir(args.new_tab): | ||||
|                 message = f"FILE|{args.new_tab}" | ||||
|                 event_system.send_ipc_message(message) | ||||
|  | ||||
|  | ||||
|     def tear_down(self, widget=None, eve=None): | ||||
|         event_system.send_ipc_message("close server") | ||||
|         self.fm_controller.save_state() | ||||
|         time.sleep(event_sleep_time) | ||||
|         Gtk.main_quit() | ||||
|  | ||||
|  | ||||
|     @threaded | ||||
|     def gui_event_observer(self): | ||||
|         while True: | ||||
|             time.sleep(event_sleep_time) | ||||
|             event = event_system.consume_gui_event() | ||||
|             if event: | ||||
|                 try: | ||||
|                     type, target, data = event | ||||
|                     if type: | ||||
|                         method = getattr(self.__class__, "handle_gui_event_and_set_message") | ||||
|                         GLib.idle_add(method, *(self, type, target, data)) | ||||
|                     else: | ||||
|                         method = getattr(self.__class__, target) | ||||
|                         GLib.idle_add(method, *(self, *data,)) | ||||
|                 except Exception as e: | ||||
|                     print(repr(e)) | ||||
|  | ||||
|     def handle_gui_event_and_set_message(self, type, target, parameters): | ||||
|         method = getattr(self.__class__, f"{target}") | ||||
|         data   = method(*(self, *parameters)) | ||||
|         self.plugins.send_message_to_plugin(type, data) | ||||
|  | ||||
|     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) | ||||
|         dir      = tab.get_current_directory() | ||||
|         tab.execute(f"{tab.terminal_app}", dir) | ||||
|  | ||||
|     def save_load_session(self, action="save_session"): | ||||
|         wid, tid          = self.fm_controller.get_active_wid_and_tid() | ||||
|         tab               = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|         save_load_dialog  = self.builder.get_object("save_load_dialog") | ||||
|  | ||||
|         if action == "save_session": | ||||
|             self.fm_controller.save_state() | ||||
|             return | ||||
|         elif action == "save_session_as": | ||||
|             save_load_dialog.set_action(Gtk.FileChooserAction.SAVE) | ||||
|         elif action == "load_session": | ||||
|             save_load_dialog.set_action(Gtk.FileChooserAction.OPEN) | ||||
|         else: | ||||
|             raise Exception(f"Unknown action given:  {action}") | ||||
|  | ||||
|         save_load_dialog.set_current_folder(tab.get_current_directory()) | ||||
|         save_load_dialog.set_current_name("session.json") | ||||
|         response = save_load_dialog.run() | ||||
|         if response == Gtk.ResponseType.OK: | ||||
|             if action == "save_session_as": | ||||
|                 path = f"{save_load_dialog.get_current_folder()}/{save_load_dialog.get_current_name()}" | ||||
|                 self.fm_controller.save_state(path) | ||||
|             elif action == "load_session": | ||||
|                 path         = f"{save_load_dialog.get_file().get_path()}" | ||||
|                 session_json = self.fm_controller.load_state(path) | ||||
|                 self.load_session(session_json) | ||||
|         if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT): | ||||
|             pass | ||||
|  | ||||
|         save_load_dialog.hide() | ||||
|  | ||||
|     def load_session(self, session_json): | ||||
|         if debug: | ||||
|             print(f"Session Data: {session_json}") | ||||
|  | ||||
|         self.ctrl_down  = False | ||||
|         self.shift_down = False | ||||
|         self.alt_down   = False | ||||
|         for notebook in self.notebooks: | ||||
|             self.clear_children(notebook) | ||||
|  | ||||
|         self.fm_controller.unload_tabs_and_windows() | ||||
|         self.generate_windows(session_json) | ||||
|         gc.collect() | ||||
|  | ||||
|  | ||||
|     def do_action_from_menu_controls(self, widget, event_button): | ||||
|         action = widget.get_name() | ||||
|         self.hide_context_menu() | ||||
|         self.hide_new_file_menu() | ||||
|         self.hide_edit_file_menu() | ||||
|  | ||||
|         if action == "open": | ||||
|             self.open_files() | ||||
|         if action == "open_with": | ||||
|             self.show_appchooser_menu() | ||||
|         if action == "execute": | ||||
|             self.execute_files() | ||||
|         if action == "execute_in_terminal": | ||||
|             self.execute_files(in_terminal=True) | ||||
|         if action == "rename": | ||||
|             self.rename_files() | ||||
|         if action == "cut": | ||||
|             self.to_copy_files.clear() | ||||
|             self.cut_files() | ||||
|         if action == "copy": | ||||
|             self.to_cut_files.clear() | ||||
|             self.copy_files() | ||||
|         if action == "paste": | ||||
|             self.paste_files() | ||||
|         if action == "archive": | ||||
|             self.show_archiver_dialogue() | ||||
|         if action == "delete": | ||||
|             self.delete_files() | ||||
|         if action == "trash": | ||||
|             self.trash_files() | ||||
|         if action == "go_to_trash": | ||||
|             self.path_entry.set_text(self.trash_files_path) | ||||
|         if action == "restore_from_trash": | ||||
|             self.restore_trash_files() | ||||
|         if action == "empty_trash": | ||||
|             self.empty_trash() | ||||
|         if action == "create": | ||||
|             self.show_new_file_menu() | ||||
|         if action in ["save_session", "save_session_as", "load_session"]: | ||||
|             self.save_load_session(action) | ||||
| @@ -1,157 +0,0 @@ | ||||
| # Python imports | ||||
| import sys, os, signal | ||||
|  | ||||
| # Lib imports | ||||
| from gi.repository import GLib | ||||
|  | ||||
| # Application imports | ||||
| from trasher.xdgtrash import XDGTrash | ||||
| from shellfm.windows.controller import WindowController | ||||
| from plugins.plugins import Plugins | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class Controller_Data: | ||||
|     """ Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """ | ||||
|  | ||||
|     def setup_controller_data(self, _settings): | ||||
|         self.trashman           = XDGTrash() | ||||
|         self.fm_controller      = WindowController() | ||||
|         self.plugins            = Plugins(_settings) | ||||
|         self.state              = self.fm_controller.load_state() | ||||
|         self.trashman.regenerate() | ||||
|  | ||||
|         self.settings           = _settings | ||||
|         self.builder            = self.settings.get_builder() | ||||
|         self.logger             = self.settings.get_logger() | ||||
|  | ||||
|         self.window             = self.settings.get_main_window() | ||||
|         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.message_popup_widget = self.builder.get_object("message_popup_widget") | ||||
|         self.message_text_view  = self.builder.get_object("message_text_view") | ||||
|         self.message_buffer     = self.builder.get_object("message_buffer") | ||||
|         self.arc_command_buffer = self.builder.get_object("arc_command_buffer") | ||||
|  | ||||
|         self.exists_file_rename_bttn = self.builder.get_object("exists_file_rename_bttn") | ||||
|         self.warning_alert      = self.builder.get_object("warning_alert") | ||||
|         self.edit_file_menu     = self.builder.get_object("edit_file_menu") | ||||
|         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_field  = self.builder.get_object("exists_file_field") | ||||
|         self.path_menu          = self.builder.get_object("path_menu") | ||||
|         self.path_entry         = self.builder.get_object("path_entry") | ||||
|  | ||||
|         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") | ||||
|  | ||||
|         self.trash_files_path        = GLib.get_user_data_dir() + "/Trash/files" | ||||
|         self.trash_info_path         = GLib.get_user_data_dir() + "/Trash/info" | ||||
|  | ||||
|         # In compress commands: | ||||
|         #    %n: First selected filename/dir to archive | ||||
|         #    %N: All selected filenames/dirs to archive, or (with %O) a single filename | ||||
|         #    %o: Resulting single archive file | ||||
|         #    %O: Resulting archive per source file/directory (use changes %N meaning) | ||||
|         # | ||||
|         #  In extract commands: | ||||
|         #    %x: Archive file to extract | ||||
|         #    %g: Unique extraction target filename with optional subfolder | ||||
|         #    %G: Unique extraction target filename, never with subfolder | ||||
|         # | ||||
|         #  In list commands: | ||||
|         #      %x: Archive to list | ||||
|         # | ||||
|         #  Plus standard bash variables are accepted. | ||||
|         self.arc_commands            = [ '$(which 7za || echo 7zr) a %o %N', | ||||
|                                                                 'zip -r %o %N', | ||||
|                                                                 'rar a -r %o %N', | ||||
|                                                                 'tar -cvf %o %N', | ||||
|                                                                 'tar -cvjf %o %N', | ||||
|                                                                 'tar -cvzf %o %N', | ||||
|                                                                 'tar -cvJf %o %N', | ||||
|                                                                 'gzip -c %N > %O', | ||||
|                                                                 'xz -cz %N > %O' | ||||
|                                         ] | ||||
|  | ||||
|         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.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.is_searching       = False | ||||
|         self.search_icon_grid   = None | ||||
|         self.search_tab         = None | ||||
|  | ||||
|         self.skip_edit          = False | ||||
|         self.cancel_edit        = False | ||||
|         self.ctrl_down          = False | ||||
|         self.shift_down         = False | ||||
|         self.alt_down           = False | ||||
|  | ||||
|         self.success_color      = self.settings.get_success_color() | ||||
|         self.warning_color      = self.settings.get_warning_color() | ||||
|         self.error_color        = self.settings.get_error_color() | ||||
|  | ||||
|         sys.excepthook = self.custom_except_hook | ||||
|         self.window.connect("delete-event", self.tear_down) | ||||
|         GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down) | ||||
|  | ||||
|     def get_current_state(self): | ||||
|         ''' | ||||
|         Returns the state info most useful for any given context and action intent. | ||||
|  | ||||
|                 Parameters: | ||||
|                         a (obj): self | ||||
|  | ||||
|                 Returns: | ||||
|                         wid, tid, tab, icon_grid, store | ||||
|         ''' | ||||
|         wid, tid     = self.fm_controller.get_active_wid_and_tid() | ||||
|         tab          = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|         icon_grid    = self.builder.get_object(f"{wid}|{tid}|icon_grid") | ||||
|         store        = icon_grid.get_model() | ||||
|         return wid, tid, tab, icon_grid, store | ||||
|  | ||||
|  | ||||
|     def clear_console(self): | ||||
|         ''' Clears the terminal screen. ''' | ||||
|         os.system('cls' if os.name == 'nt' else 'clear') | ||||
|  | ||||
|     def call_method(self, _method_name, data = None): | ||||
|         ''' | ||||
|         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): | ||||
|         ''' Checks if a given method exists. ''' | ||||
|         return callable(getattr(obj, name, None)) | ||||
|  | ||||
|     def clear_children(self, widget): | ||||
|         ''' Clear children of a gtk widget. ''' | ||||
|         for child in widget.get_children(): | ||||
|             widget.remove(child) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Mixins module | ||||
| """ | ||||
| @@ -1,62 +0,0 @@ | ||||
| # Python imports | ||||
| import traceback, threading, time | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk, GLib | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| class ExceptionHookMixin: | ||||
|     """ ExceptionHookMixin custom exception hook to reroute to a Gtk text area. """ | ||||
|  | ||||
|     def custom_except_hook(self, exec_type, value, _traceback): | ||||
|         trace     = ''.join(traceback.format_tb(_traceback)) | ||||
|         data      = f"Exec Type:  {exec_type}  <-->  Value:  {value}\n\n{trace}\n\n\n\n" | ||||
|         start_itr = self.message_buffer.get_start_iter() | ||||
|         self.message_buffer.place_cursor(start_itr) | ||||
|         self.display_message(self.error, data) | ||||
|  | ||||
|     def display_message(self, type, text, seconds=None): | ||||
|         self.message_buffer.insert_at_cursor(text) | ||||
|         self.message_popup_widget.popup() | ||||
|         if seconds: | ||||
|             self.hide_message_timeout(seconds) | ||||
|  | ||||
|     @threaded | ||||
|     def hide_message_timeout(self, seconds=3): | ||||
|         time.sleep(seconds) | ||||
|         GLib.idle_add(self.message_popup_widget.popdown) | ||||
|  | ||||
|     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", self.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() | ||||
|  | ||||
|  | ||||
|     def set_arc_buffer_text(self, widget=None, eve=None): | ||||
|         sid = widget.get_active_id() | ||||
|         self.arc_command_buffer.set_text(self.arc_commands[int(sid)]) | ||||
| @@ -1,147 +0,0 @@ | ||||
| # Python imports | ||||
|  | ||||
| # Gtk imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| gi.require_version('Gdk', '3.0') | ||||
| from gi.repository import Gtk, Gdk | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class ShowHideMixin: | ||||
|     def show_messages_popup(self, type, text, seconds=None): | ||||
|         self.message_popup_widget.popup() | ||||
|  | ||||
|     def stop_file_searching(self, widget=None, eve=None): | ||||
|         self.is_searching = False | ||||
|  | ||||
|     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 show_about_page(self, widget=None, eve=None): | ||||
|         about_page = self.builder.get_object("about_page") | ||||
|         response   = 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.builder.get_object("about_page").hide() | ||||
|  | ||||
|  | ||||
|     def show_archiver_dialogue(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) | ||||
|         archiver_dialogue = self.builder.get_object("archiver_dialogue") | ||||
|         archiver_dialogue.set_action(Gtk.FileChooserAction.SAVE) | ||||
|         archiver_dialogue.set_current_folder(tab.get_current_directory()) | ||||
|         archiver_dialogue.set_current_name("arc.7z") | ||||
|  | ||||
|         response = archiver_dialogue.run() | ||||
|         if response == Gtk.ResponseType.OK: | ||||
|             self.archive_files(archiver_dialogue) | ||||
|         if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT): | ||||
|             pass | ||||
|  | ||||
|         archiver_dialogue.hide() | ||||
|  | ||||
|     def hide_archiver_dialogue(self, widget=None, eve=None): | ||||
|         self.builder.get_object("archiver_dialogue").hide() | ||||
|  | ||||
|  | ||||
|     def show_appchooser_menu(self, widget=None, eve=None): | ||||
|         appchooser_menu   = self.builder.get_object("appchooser_menu") | ||||
|         appchooser_widget = self.builder.get_object("appchooser_widget") | ||||
|         response          = appchooser_menu.run() | ||||
|  | ||||
|         if response == Gtk.ResponseType.OK: | ||||
|             self.open_with_files(appchooser_widget) | ||||
|             self.hide_appchooser_menu() | ||||
|  | ||||
|         if response == Gtk.ResponseType.CANCEL: | ||||
|             self.hide_appchooser_menu() | ||||
|  | ||||
|  | ||||
|     def hide_appchooser_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("appchooser_menu").hide() | ||||
|  | ||||
|     def run_appchooser_launch(self, widget=None, eve=None): | ||||
|         dialog = widget.get_parent().get_parent() | ||||
|         dialog.response(Gtk.ResponseType.OK) | ||||
|  | ||||
|  | ||||
|     def show_plugins_popup(self, widget=None, eve=None): | ||||
|         self.builder.get_object("plugin_list").popup() | ||||
|  | ||||
|     def hide_plugins_popup(self, widget=None, eve=None): | ||||
|         self.builder.get_object("plugin_list").hide() | ||||
|  | ||||
|     def show_context_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("context_menu").run() | ||||
|  | ||||
|     def hide_context_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("context_menu").hide() | ||||
|  | ||||
|  | ||||
|     def show_new_file_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("context_menu_fname").set_text("") | ||||
|  | ||||
|         new_file_menu = self.builder.get_object("new_file_menu") | ||||
|         response      = new_file_menu.run() | ||||
|         if response == Gtk.ResponseType.APPLY: | ||||
|             self.create_files() | ||||
|         if response == Gtk.ResponseType.CANCEL: | ||||
|             self.hide_new_file_menu() | ||||
|  | ||||
|     def hide_new_file_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("new_file_menu").hide() | ||||
|  | ||||
|     def show_edit_file_menu(self, widget=None, eve=None): | ||||
|         if widget: | ||||
|             widget.grab_focus() | ||||
|  | ||||
|         response = self.edit_file_menu.run() | ||||
|         if response == Gtk.ResponseType.CLOSE: | ||||
|             self.skip_edit   = True | ||||
|         if response == Gtk.ResponseType.CANCEL: | ||||
|             self.cancel_edit = True | ||||
|  | ||||
|     def hide_edit_file_menu(self, widget=None, eve=None): | ||||
|         self.builder.get_object("edit_file_menu").hide() | ||||
|  | ||||
|     def hide_edit_file_menu_enter_key(self, widget=None, eve=None): | ||||
|         keyname = Gdk.keyval_name(eve.keyval).lower() | ||||
|         if keyname in ["return", "enter"]: | ||||
|             self.builder.get_object("edit_file_menu").hide() | ||||
|  | ||||
|     def hide_edit_file_menu_skip(self, widget=None, eve=None): | ||||
|         self.edit_file_menu.response(Gtk.ResponseType.CLOSE) | ||||
|  | ||||
|     def hide_edit_file_menu_cancel(self, widget=None, eve=None): | ||||
|         self.edit_file_menu.response(Gtk.ResponseType.CANCEL) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| UI module | ||||
| """ | ||||
| @@ -1,59 +0,0 @@ | ||||
| # 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._save_state(state, pane_index) | ||||
|             return | ||||
|  | ||||
|         child = pane.get_child1() if pane_index in [1, 3] else pane.get_child2() | ||||
|  | ||||
|         self.toggle_pane(child) | ||||
|         self._save_state(state, pane_index) | ||||
|  | ||||
|     def _save_state(self, state, pane_index): | ||||
|         window = self.fm_controller.get_window_by_index(pane_index - 1) | ||||
|         window.set_is_hidden(state) | ||||
|         self.fm_controller.save_state() | ||||
| @@ -1,202 +0,0 @@ | ||||
| # Python imports | ||||
| import os | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk | ||||
|  | ||||
| # Application imports | ||||
| from .widget_mixin import WidgetMixin | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class TabMixin(WidgetMixin): | ||||
|     """docstring for TabMixin""" | ||||
|  | ||||
|     def create_tab(self, wid, path=None): | ||||
|         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  = self.logger | ||||
|  | ||||
|         tab.set_wid(wid) | ||||
|         if path: tab.set_path(path) | ||||
|  | ||||
|         tab_widget    = self.create_tab_widget(tab) | ||||
|         scroll, store = self.create_icon_grid_widget(tab, wid) | ||||
|         # TODO: Fix global logic to make the below work too | ||||
|         # scroll, store = self.create_icon_tree_widget(tab, wid) | ||||
|         index         = notebook.append_page(scroll, tab_widget) | ||||
|  | ||||
|         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() | ||||
|         wid      = int(notebook.get_name()[-1]) | ||||
|         tid      = self.get_id_from_tab_box(button.get_parent()) | ||||
|         scroll   = self.builder.get_object(f"{wid}|{tid}") | ||||
|         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) | ||||
|         self.fm_controller.save_state() | ||||
|         self.set_window_title() | ||||
|  | ||||
|     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) | ||||
|         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): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         tab.load_directory() | ||||
|         self.load_store(tab, 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) | ||||
|         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, dir) | ||||
|             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): | ||||
|                 path_menu_buttons  = self.builder.get_object("path_menu_buttons") | ||||
|                 query              = widget.get_text().replace(dir, "") | ||||
|                 files              = tab.get_files() + tab.get_hidden() | ||||
|  | ||||
|                 self.clear_children(path_menu_buttons) | ||||
|                 show_path_menu = False | ||||
|                 for file, hash 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: | ||||
|                     self.path_menu.popdown() | ||||
|                 else: | ||||
|                     self.path_menu.popup() | ||||
|                     widget.grab_focus_without_selecting() | ||||
|                     widget.set_position(-1) | ||||
|  | ||||
|             if path.endswith(".") or path == dir: | ||||
|                 return | ||||
|  | ||||
|             if not tab.set_path(path): | ||||
|                 return | ||||
|  | ||||
|         self.update_tab(tab_label, tab, store, wid, tid) | ||||
|  | ||||
|         try: | ||||
|             widget.grab_focus_without_selecting() | ||||
|             widget.set_position(-1) | ||||
|         except Exception as e: | ||||
|             pass | ||||
|  | ||||
|     def set_path_entry(self, button=None, eve=None): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         path       = f"{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) | ||||
|         self.path_menu.popdown() | ||||
|  | ||||
|     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}") | ||||
|         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) | ||||
|         self.fm_controller.save_state() | ||||
|         self.set_window_title() | ||||
|  | ||||
|     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() | ||||
| @@ -1,457 +0,0 @@ | ||||
| # Python imports | ||||
| import os, time, threading | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk, GObject, GLib, Gio | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| class WidgetFileActionMixin: | ||||
|     """docstring for WidgetFileActionMixin""" | ||||
|  | ||||
|     def sizeof_fmt(self, 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_dir_size(self, sdir): | ||||
|         """Get the size of a directory.  Based on code found online.""" | ||||
|         size = os.path.getsize(sdir) | ||||
|  | ||||
|         for item in os.listdir(sdir): | ||||
|             item = os.path.join(sdir, item) | ||||
|  | ||||
|             if os.path.isfile(item): | ||||
|                 size = size + os.path.getsize(item) | ||||
|             elif os.path.isdir(item): | ||||
|                 size = size + self.get_dir_size(item) | ||||
|  | ||||
|         return size | ||||
|  | ||||
|  | ||||
|     def set_file_watcher(self, tab): | ||||
|         if tab.get_dir_watcher(): | ||||
|             watcher = tab.get_dir_watcher() | ||||
|             watcher.cancel() | ||||
|             if debug: | ||||
|                 print(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]: | ||||
|                 if debug: | ||||
|                     print(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") | ||||
|         store     = icon_grid.get_model() | ||||
|         _store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}") | ||||
|  | ||||
|         tab.load_directory() | ||||
|         self.load_store(tab, store) | ||||
|  | ||||
|         tab_widget_label.set_label(tab.get_end_of_path()) | ||||
|  | ||||
|         _wid, _tid, _tab, _icon_grid, _store = self.get_current_state() | ||||
|  | ||||
|         if [wid, tid] in [_wid, _tid]: | ||||
|             self.set_bottom_labels(tab) | ||||
|  | ||||
|  | ||||
|     def popup_search_files(self, wid, keyname): | ||||
|         entry = self.builder.get_object(f"win{wid}_search_field") | ||||
|         self.builder.get_object(f"win{wid}_search").popup() | ||||
|         entry.set_text(keyname) | ||||
|         entry.grab_focus_without_selecting() | ||||
|         entry.set_position(-1) | ||||
|  | ||||
|     def do_file_search(self, widget, eve=None): | ||||
|         query = widget.get_text() | ||||
|         self.search_icon_grid.unselect_all() | ||||
|         for i, file in enumerate(self.search_tab.get_files()): | ||||
|             if query and query in file[0].lower(): | ||||
|                 path = Gtk.TreePath().new_from_indices([i]) | ||||
|                 self.search_icon_grid.select_path(path) | ||||
|  | ||||
|         items = self.search_icon_grid.get_selected_items() | ||||
|         if len(items) == 1: | ||||
|             self.search_icon_grid.scroll_to_path(items[0], True, 0.5, 0.5) | ||||
|  | ||||
|  | ||||
|     def open_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|  | ||||
|         for file in uris: | ||||
|             tab.open_file_locally(file) | ||||
|  | ||||
|     def open_with_files(self, appchooser_widget): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         app_info  = appchooser_widget.get_app_info() | ||||
|         uris      = self.format_to_uris(store, wid, tid, self.selected_files) | ||||
|  | ||||
|         tab.app_chooser_exec(app_info, uris) | ||||
|  | ||||
|     def execute_files(self, in_terminal=False): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         paths       = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         current_dir = tab.get_current_directory() | ||||
|         command     = None | ||||
|  | ||||
|         for path in paths: | ||||
|             command = f"exec '{path}'" if not in_terminal else f"{tab.terminal_app} -e '{path}'" | ||||
|             tab.execute(command, start_dir=tab.get_current_directory(), use_os_system=False) | ||||
|  | ||||
|     def archive_files(self, archiver_dialogue): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         paths = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|  | ||||
|         save_target = archiver_dialogue.get_filename(); | ||||
|         sItr, eItr  = self.arc_command_buffer.get_bounds() | ||||
|         pre_command = self.arc_command_buffer.get_text(sItr, eItr, False) | ||||
|         pre_command = pre_command.replace("%o", save_target) | ||||
|         pre_command = pre_command.replace("%N", ' '.join(paths)) | ||||
|         command     = f"{tab.terminal_app} -e '{pre_command}'" | ||||
|  | ||||
|         tab.execute(command, start_dir=None, use_os_system=True) | ||||
|  | ||||
|     def rename_files(self): | ||||
|         rename_label = self.builder.get_object("file_to_rename_label") | ||||
|         rename_input = self.builder.get_object("new_rename_fname") | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris         = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|  | ||||
|         for uri in uris: | ||||
|             entry = uri.split("/")[-1] | ||||
|             rename_label.set_label(entry) | ||||
|             rename_input.set_text(entry) | ||||
|  | ||||
|             self.show_edit_file_menu(rename_input) | ||||
|             if self.skip_edit: | ||||
|                 self.skip_edit   = False | ||||
|                 continue | ||||
|             if self.cancel_edit: | ||||
|                 self.cancel_edit = False | ||||
|                 break | ||||
|  | ||||
|             rname_to = rename_input.get_text().strip() | ||||
|             target   = f"{tab.get_current_directory()}/{rname_to}" | ||||
|             self.handle_files([uri], "rename", target) | ||||
|  | ||||
|  | ||||
|         self.skip_edit   = False | ||||
|         self.cancel_edit = False | ||||
|         self.hide_edit_file_menu() | ||||
|         self.selected_files.clear() | ||||
|  | ||||
|     def cut_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         self.to_cut_files = uris | ||||
|  | ||||
|     def copy_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         self.to_copy_files = uris | ||||
|  | ||||
|     def paste_files(self): | ||||
|         wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|         target    = f"{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 delete_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris     = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         response = None | ||||
|  | ||||
|         self.warning_alert.format_secondary_text(f"Do you really want to delete the {len(uris)} file(s)?") | ||||
|         for uri in uris: | ||||
|             file = Gio.File.new_for_path(uri) | ||||
|  | ||||
|             if not response: | ||||
|                 response = self.warning_alert.run() | ||||
|                 self.warning_alert.hide() | ||||
|             if response == Gtk.ResponseType.YES: | ||||
|                 type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE) | ||||
|  | ||||
|                 if type == Gio.FileType.DIRECTORY: | ||||
|                     tab.delete_file( file.get_path() ) | ||||
|                 else: | ||||
|                     file.delete(cancellable=None) | ||||
|             else: | ||||
|                 break | ||||
|  | ||||
|  | ||||
|     def trash_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris      = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         for uri in uris: | ||||
|             self.trashman.trash(uri, False) | ||||
|  | ||||
|     def restore_trash_files(self): | ||||
|         wid, tid, tab, icon_grid, store = self.get_current_state() | ||||
|         uris      = self.format_to_uris(store, wid, tid, self.selected_files, True) | ||||
|         for uri in uris: | ||||
|             self.trashman.restore(filename=uri.split("/")[-1], verbose=False) | ||||
|  | ||||
|     def empty_trash(self): | ||||
|         self.trashman.empty(verbose=False) | ||||
|  | ||||
|  | ||||
|     def create_files(self): | ||||
|         fname_field = self.builder.get_object("context_menu_fname") | ||||
|         file_name   = fname_field.get_text().strip() | ||||
|         type        = self.builder.get_object("context_menu_type_toggle").get_state() | ||||
|  | ||||
|         wid, tid    = self.fm_controller.get_active_wid_and_tid() | ||||
|         tab         = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|         target      = f"{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") | ||||
|  | ||||
|         self.hide_new_file_menu() | ||||
|  | ||||
|     def move_files(self, files, target): | ||||
|         self.handle_files(files, "move", target) | ||||
|  | ||||
|     # 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 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: | ||||
|                         self.setup_exists_data(file, _file) | ||||
|                         response = self.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.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: | ||||
|                             wid, tid = self.fm_controller.get_active_wid_and_tid() | ||||
|                             tab      = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|                             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: | ||||
|                     wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||
|                     tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|                     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: | ||||
|                     if action == "copy": | ||||
|                         file.copy(target, flags=Gio.FileCopyFlags.BACKUP, cancellable=None) | ||||
|                     if action == "move" or action == "rename": | ||||
|                         file.move(target, flags=Gio.FileCopyFlags.BACKUP, cancellable=None) | ||||
|  | ||||
|             except GObject.GError as e: | ||||
|                 raise OSError(e) | ||||
|  | ||||
|         self.exists_file_rename_bttn.set_sensitive(False) | ||||
|  | ||||
|  | ||||
|     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) | ||||
|         exists_file_diff_from = self.builder.get_object("exists_file_diff_from") | ||||
|         exists_file_diff_to   = self.builder.get_object("exists_file_diff_to") | ||||
|         exists_file_from      = self.builder.get_object("exists_file_from") | ||||
|         exists_file_to        = self.builder.get_object("exists_file_to") | ||||
|         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() | ||||
|  | ||||
|         exists_file_from.set_label(from_file.get_parent().get_path()) | ||||
|         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')}     {self.sizeof_fmt(from_size)}     ( {from_size} bytes )  ( {age_text} & {size_text} )" | ||||
|         to_label_text = f"{to_date.format('%F %R')}     {self.sizeof_fmt(to_size)}     ( {to_size} bytes )" | ||||
|  | ||||
|         exists_file_diff_from.set_text(from_label_text) | ||||
|         exists_file_diff_to.set_text(to_label_text) | ||||
|  | ||||
|  | ||||
|     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 debug: | ||||
|             print(f"Path:  {full_path}") | ||||
|             print(f"Base Path:  {base_path}") | ||||
|             print(f'Name:  {file_name}') | ||||
|             print(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 Exception as e: | ||||
|                 pass | ||||
|  | ||||
|             target = Gio.File.new_for_path(f"{base_path}/{file_name}-copy{i}{extension}") | ||||
|             i += 1 | ||||
|  | ||||
|         return target | ||||
|  | ||||
|  | ||||
|     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) | ||||
| @@ -1,218 +0,0 @@ | ||||
| # Python imports | ||||
| import os, threading, subprocess, time | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
|  | ||||
| gi.require_version("Gtk", "3.0") | ||||
| gi.require_version('Gdk', '3.0') | ||||
| from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class WidgetMixin: | ||||
|     """docstring for WidgetMixin""" | ||||
|  | ||||
|     def load_store(self, tab, store, save_state=False): | ||||
|         store.clear() | ||||
|         dir   = tab.get_current_directory() | ||||
|         files = tab.get_files() | ||||
|  | ||||
|         for i, file in enumerate(files): | ||||
|             store.append([None, file[0]]) | ||||
|             self.create_icon(i, tab, store, dir, file[0]) | ||||
|  | ||||
|         # NOTE: Not likely called often from here but it could be useful | ||||
|         if save_state: | ||||
|             self.fm_controller.save_state() | ||||
|  | ||||
|     @threaded | ||||
|     def create_icon(self, i, tab, store, dir, file): | ||||
|         icon  = tab.create_icon(dir, file) | ||||
|         fpath = f"{dir}/{file}" | ||||
|         GLib.idle_add(self.update_store, (i, store, icon, tab, fpath,)) | ||||
|  | ||||
|     # NOTE: Might need to keep an eye on this throwing invalid iters when too | ||||
|     #       many updates are happening to a folder. Example: /tmp | ||||
|     def update_store(self, item): | ||||
|         i, store, icon, tab, fpath = item | ||||
|         itr = None | ||||
|  | ||||
|         try: | ||||
|             itr = store.get_iter(i) | ||||
|         except Exception as e: | ||||
|             try: | ||||
|                 time.sleep(0.2) | ||||
|                 itr = store.get_iter(i) | ||||
|             except Exception as e: | ||||
|                 print(":Invalid Itr detected: (Potential race condition...)") | ||||
|                 print(f"Index Requested:  {i}") | ||||
|                 print(f"Store Size:  {len(store)}") | ||||
|                 return | ||||
|  | ||||
|         if not icon: | ||||
|             icon = self.get_system_thumbnail(fpath, tab.SYS_ICON_WH[0]) | ||||
|             if not icon: | ||||
|                 if fpath.endswith(".gif"): | ||||
|                     icon = GdkPixbuf.PixbufAnimation.get_static_image(fpath) | ||||
|                 else: | ||||
|                     icon = GdkPixbuf.Pixbuf.new_from_file(tab.DEFAULT_ICON) | ||||
|  | ||||
|         store.set_value(itr, 0, icon) | ||||
|  | ||||
|  | ||||
|     def get_system_thumbnail(self, filename, size): | ||||
|         try: | ||||
|             if os.path.exists(filename): | ||||
|                 gioFile   = Gio.File.new_for_path(filename) | ||||
|                 info      = gioFile.query_info('standard::icon' , 0, Gio.Cancellable()) | ||||
|                 icon      = info.get_icon().get_names()[0] | ||||
|                 iconTheme = Gtk.IconTheme.get_default() | ||||
|                 iconData  = iconTheme.lookup_icon(icon , size , 0) | ||||
|                 if iconData: | ||||
|                     iconPath  = iconData.get_filename() | ||||
|                     return GdkPixbuf.Pixbuf.new_from_file(iconPath) | ||||
|                 else: | ||||
|                     return None | ||||
|             else: | ||||
|                 return None | ||||
|         except Exception as e: | ||||
|             print("System icon generation issue:") | ||||
|             return None | ||||
|  | ||||
|  | ||||
|     def create_tab_widget(self, tab): | ||||
|         tab_widget = Gtk.ButtonBox() | ||||
|         label = Gtk.Label() | ||||
|         tid   = Gtk.Label() | ||||
|         close = Gtk.Button() | ||||
|         icon  = Gtk.Image(stock=Gtk.STOCK_CLOSE) | ||||
|  | ||||
|         label.set_label(f"{tab.get_end_of_path()}") | ||||
|         label.set_width_chars(len(tab.get_end_of_path())) | ||||
|         label.set_xalign(0.0) | ||||
|         tid.set_label(f"{tab.get_id()}") | ||||
|  | ||||
|         close.add(icon) | ||||
|         tab_widget.add(label) | ||||
|         tab_widget.add(close) | ||||
|         tab_widget.add(tid) | ||||
|  | ||||
|         close.connect("released", self.close_tab) | ||||
|         tab_widget.show_all() | ||||
|         tid.hide() | ||||
|         return tab_widget | ||||
|  | ||||
|     def create_icon_grid_widget(self, tab, wid): | ||||
|         scroll = Gtk.ScrolledWindow() | ||||
|         grid   = Gtk.IconView() | ||||
|         store  = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str) | ||||
|  | ||||
|         grid.set_model(store) | ||||
|         grid.set_pixbuf_column(0) | ||||
|         grid.set_text_column(1) | ||||
|  | ||||
|         grid.set_item_orientation(1) | ||||
|         grid.set_selection_mode(3) | ||||
|         grid.set_item_width(96) | ||||
|         grid.set_item_padding(8) | ||||
|         grid.set_margin(12) | ||||
|         grid.set_row_spacing(18) | ||||
|         grid.set_columns(-1) | ||||
|         grid.set_spacing(12) | ||||
|         grid.set_column_spacing(18) | ||||
|  | ||||
|         grid.connect("button_release_event", self.grid_icon_single_click) | ||||
|         grid.connect("item-activated",       self.grid_icon_double_click) | ||||
|         grid.connect("selection-changed",    self.grid_set_selected_items) | ||||
|         grid.connect("drag-data-get",        self.grid_on_drag_set) | ||||
|         grid.connect("drag-data-received",   self.grid_on_drag_data_received) | ||||
|         grid.connect("drag-motion",          self.grid_on_drag_motion) | ||||
|  | ||||
|  | ||||
|         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 | ||||
|         grid.enable_model_drag_dest(targets, action) | ||||
|         grid.enable_model_drag_source(0, targets, action) | ||||
|  | ||||
|         grid.show_all() | ||||
|         scroll.add(grid) | ||||
|         grid.set_name(f"{wid}|{tab.get_id()}") | ||||
|         scroll.set_name(f"{wid}|{tab.get_id()}") | ||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid) | ||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll) | ||||
|         return scroll, store | ||||
|  | ||||
|     def create_icon_tree_widget(self, tab, wid): | ||||
|         scroll = Gtk.ScrolledWindow() | ||||
|         grid   = Gtk.TreeView() | ||||
|         store  = Gtk.TreeStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str) | ||||
|         column = Gtk.TreeViewColumn("Icons") | ||||
|         icon   = Gtk.CellRendererPixbuf() | ||||
|         name   = Gtk.CellRendererText() | ||||
|         selec  = grid.get_selection() | ||||
|  | ||||
|         grid.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) | ||||
|  | ||||
|         grid.append_column(column) | ||||
|         grid.set_search_column(1) | ||||
|         grid.set_rubber_banding(True) | ||||
|         grid.set_headers_visible(False) | ||||
|         grid.set_enable_tree_lines(False) | ||||
|  | ||||
|         grid.connect("button_release_event", self.grid_icon_single_click) | ||||
|         grid.connect("row-activated",        self.grid_icon_double_click) | ||||
|         grid.connect("drag-data-get",        self.grid_on_drag_set) | ||||
|         grid.connect("drag-data-received",   self.grid_on_drag_data_received) | ||||
|         grid.connect("drag-motion",          self.grid_on_drag_motion) | ||||
|  | ||||
|         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 | ||||
|         grid.enable_model_drag_dest(targets, action) | ||||
|         grid.enable_model_drag_source(0, targets, action) | ||||
|  | ||||
|  | ||||
|         grid.show_all() | ||||
|         scroll.add(grid) | ||||
|         grid.set_name(f"{wid}|{tab.get_id()}") | ||||
|         scroll.set_name(f"{wid}|{tab.get_id()}") | ||||
|         grid.columns_autosize() | ||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll) | ||||
|         return scroll, store | ||||
|  | ||||
|  | ||||
|     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 | ||||
| @@ -1,256 +0,0 @@ | ||||
| # Python imports | ||||
| import copy | ||||
| from os.path import isdir, isfile | ||||
|  | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gdk', '3.0') | ||||
| from gi.repository import Gdk, Gio | ||||
|  | ||||
| # Application imports | ||||
| from .tab_mixin import TabMixin | ||||
|  | ||||
|  | ||||
| class WindowMixin(TabMixin): | ||||
|     """docstring for WindowMixin""" | ||||
|  | ||||
|     def generate_windows(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[0]["window"]["isHidden"] == "True" else False | ||||
|                 tabs      = value[0]["window"]["tabs"] | ||||
|                 self.fm_controller.create_window() | ||||
|                 notebook_tggl_button.set_active(True) | ||||
|  | ||||
|                 for tab in tabs: | ||||
|                     self.create_new_tab_notebook(None, i, tab) | ||||
|  | ||||
|                 if is_hidden: | ||||
|                     self.toggle_notebook_pane(notebook_tggl_button) | ||||
|  | ||||
|             try: | ||||
|                 if not self.is_pane4_hidden: | ||||
|                     icon_grid = self.window4.get_children()[1].get_children()[0] | ||||
|                 elif not self.is_pane3_hidden: | ||||
|                     icon_grid = self.window3.get_children()[1].get_children()[0] | ||||
|                 elif not self.is_pane2_hidden: | ||||
|                     icon_grid = self.window2.get_children()[1].get_children()[0] | ||||
|                 elif not self.is_pane1_hidden: | ||||
|                     icon_grid = self.window1.get_children()[1].get_children()[0] | ||||
|  | ||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) | ||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) | ||||
|             except Exception as e: | ||||
|                 print("\n:  The saved session might be missing window data!  :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n") | ||||
|                 print(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) | ||||
|  | ||||
|  | ||||
|     def get_fm_window(self, wid): | ||||
|         return self.fm_controller.get_window_by_nickname(f"window_{wid}") | ||||
|  | ||||
|     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 set_bottom_labels(self, tab): | ||||
|         _wid, _tid, _tab, icon_grid, store = self.get_current_state() | ||||
|         selected_files       = 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 = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) ) | ||||
|         formatted_mount_size = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::size")) ) | ||||
|  | ||||
|         if self.trash_files_path == current_directory: | ||||
|             self.builder.get_object("restore_from_trash").show() | ||||
|             self.builder.get_object("empty_trash").show() | ||||
|         else: | ||||
|             self.builder.get_object("restore_from_trash").hide() | ||||
|             self.builder.get_object("empty_trash").hide() | ||||
|  | ||||
|         # 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          = self.format_to_uris(store, _wid, _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 Exception as e: | ||||
|                     if debug: | ||||
|                         print(repr(e)) | ||||
|  | ||||
|  | ||||
|             formatted_size = self.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.get_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") | ||||
|  | ||||
|  | ||||
|  | ||||
|     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"SolarFM ~ {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): | ||||
|         self.selected_files = icons_grid.get_selected_items() | ||||
|  | ||||
|     def grid_cursor_toggled(self, icons_grid): | ||||
|         print("wat...") | ||||
|  | ||||
|     def grid_icon_single_click(self, icons_grid, eve): | ||||
|         try: | ||||
|             self.path_menu.popdown() | ||||
|             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 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 | ||||
|                 self.show_context_menu() | ||||
|  | ||||
|         except Exception as e: | ||||
|             print(repr(e)) | ||||
|             self.display_message(self.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 | ||||
|  | ||||
|  | ||||
|             wid, tid, tab, _icons_grid, store = self.get_current_state() | ||||
|             notebook   = self.builder.get_object(f"window_{wid}") | ||||
|             tab_label  = self.get_tab_label(notebook, icons_grid) | ||||
|  | ||||
|             fileName   = store[item][1] | ||||
|             dir        = tab.get_current_directory() | ||||
|             file       = f"{dir}/{fileName}" | ||||
|  | ||||
|             if isdir(file): | ||||
|                 tab.set_path(file) | ||||
|                 self.update_tab(tab_label, tab, store, wid, tid) | ||||
|             else: | ||||
|                 self.open_files() | ||||
|         except Exception as e: | ||||
|             self.display_message(self.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() | ||||
|         treePath  = icons_grid.get_drag_dest_item().path | ||||
|  | ||||
|         if treePath: | ||||
|             uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "") | ||||
|             self.override_drop_dest = uri if isdir(uri) else 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: | ||||
|             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) | ||||
|  | ||||
|             uris = data.get_uris() | ||||
|             dest = f"{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: | ||||
|                 self.move_files(uris, dest) | ||||
|  | ||||
|  | ||||
|     def create_new_tab_notebook(self, widget=None, wid=None, path=None): | ||||
|         self.create_tab(wid, path) | ||||
| @@ -1,14 +0,0 @@ | ||||
| # Python imports | ||||
|  | ||||
| # Gtk imports | ||||
|  | ||||
| # Application imports | ||||
| from .show_hide_mixin import ShowHideMixin | ||||
| from .ui.widget_file_action_mixin import WidgetFileActionMixin | ||||
| from .ui.pane_mixin import PaneMixin | ||||
| from .ui.window_mixin import WindowMixin | ||||
| from .show_hide_mixin import ShowHideMixin | ||||
|  | ||||
|  | ||||
| class UIMixin(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin): | ||||
|     pass | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Signals module | ||||
| """ | ||||
| @@ -1,29 +0,0 @@ | ||||
| # Python imports | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class IPCSignalsMixin: | ||||
|     """ IPCSignalsMixin handle messages from another starting solarfm process. """ | ||||
|  | ||||
|     def print_to_console(self, message=None): | ||||
|         print(self) | ||||
|         print(message) | ||||
|  | ||||
|     def handle_file_from_ipc(self, path): | ||||
|         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, path) | ||||
|             return | ||||
|  | ||||
|         if not self.is_pane4_hidden: | ||||
|             self.create_tab(4, path) | ||||
|         elif not self.is_pane3_hidden: | ||||
|             self.create_tab(3, path) | ||||
|         elif not self.is_pane2_hidden: | ||||
|             self.create_tab(2, path) | ||||
|         elif not self.is_pane1_hidden: | ||||
|             self.create_tab(1, path) | ||||
| @@ -1,114 +0,0 @@ | ||||
| # 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, Gdk | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| valid_keyvalue_pat    = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]") | ||||
|  | ||||
|  | ||||
| class KeyboardSignalsMixin: | ||||
|     """ KeyboardSignalsMixin keyboard hooks controller. """ | ||||
|  | ||||
|     def unset_keys_and_data(self, widget=None, eve=None): | ||||
|         self.ctrl_down    = False | ||||
|         self.shift_down   = False | ||||
|         self.alt_down     = False | ||||
|         self.is_searching = False | ||||
|  | ||||
|     def 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 | ||||
|  | ||||
|     # NOTE: Yes, this should actually be mapped to some key controller setting | ||||
|     #       file or something. Sue me. | ||||
|     def global_key_release_controller(self, eve, user_data): | ||||
|         keyname = Gdk.keyval_name(user_data.keyval).lower() | ||||
|         if debug: | ||||
|             print(f"global_key_release_controller > key > {keyname}") | ||||
|  | ||||
|         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 | ||||
|  | ||||
|         if self.ctrl_down and self.shift_down and keyname == "t": | ||||
|             self.unset_keys_and_data() | ||||
|             self.trash_files() | ||||
|  | ||||
|         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() | ||||
|             if keyname == "q": | ||||
|                 self.tear_down() | ||||
|             if keyname == "slash" or keyname == "home": | ||||
|                 self.builder.get_object("go_home").released() | ||||
|             if keyname == "r" or keyname == "f5": | ||||
|                 self.builder.get_object("refresh_tab").released() | ||||
|             if keyname == "up" or keyname == "u": | ||||
|                 self.builder.get_object("go_up").released() | ||||
|             if keyname == "l": | ||||
|                 self.unset_keys_and_data() | ||||
|                 self.builder.get_object("path_entry").grab_focus() | ||||
|             if keyname == "t": | ||||
|                 self.builder.get_object("create_tab").released() | ||||
|             if keyname == "o": | ||||
|                 self.unset_keys_and_data() | ||||
|                 self.open_files() | ||||
|             if keyname == "w": | ||||
|                 self.keyboard_close_tab() | ||||
|             if keyname == "h": | ||||
|                 self.show_hide_hidden_files() | ||||
|             if keyname == "e": | ||||
|                 self.unset_keys_and_data() | ||||
|                 self.rename_files() | ||||
|             if keyname == "c": | ||||
|                 self.copy_files() | ||||
|                 self.to_cut_files.clear() | ||||
|             if keyname == "x": | ||||
|                 self.to_copy_files.clear() | ||||
|                 self.cut_files() | ||||
|             if keyname == "v": | ||||
|                 self.paste_files() | ||||
|             if keyname == "n": | ||||
|                 self.unset_keys_and_data() | ||||
|                 self.show_new_file_menu() | ||||
|  | ||||
|         if keyname == "delete": | ||||
|             self.unset_keys_and_data() | ||||
|             self.delete_files() | ||||
|         if keyname == "f2": | ||||
|             self.unset_keys_and_data() | ||||
|             self.rename_files() | ||||
|         if keyname == "f4": | ||||
|             self.unset_keys_and_data() | ||||
|             self.open_terminal() | ||||
|         if keyname in ["alt_l", "alt_r"]: | ||||
|             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() | ||||
|  | ||||
|         if re.fullmatch(valid_keyvalue_pat, keyname): | ||||
|             if not self.is_searching and not self.ctrl_down \ | ||||
|                 and not self.shift_down and not self.alt_down: | ||||
|                     focused_obj = self.window.get_focus() | ||||
|                     if isinstance(focused_obj, Gtk.IconView): | ||||
|                         self.is_searching = True | ||||
|                         wid, tid, self.search_tab, self.search_icon_grid, store = self.get_current_state() | ||||
|                         self.unset_keys_and_data() | ||||
|                         self.popup_search_files(wid, keyname) | ||||
|                         return | ||||
| @@ -1,90 +0,0 @@ | ||||
| # Python imports | ||||
| import os, threading, time | ||||
| from multiprocessing.connection import Listener, Client | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class IPCServer: | ||||
|     """ Create a listener so that other SolarFM instances send requests back to existing instance. """ | ||||
|     def __init__(self, conn_type="socket"): | ||||
|         self.is_ipc_alive   = False | ||||
|         self._conn_type     = conn_type | ||||
|         self.ipc_authkey    = b'solarfm-ipc' | ||||
|         self.ipc_timeout    = 15.0 | ||||
|  | ||||
|         if conn_type == "socket": | ||||
|             self.ipc_address    = '/tmp/solarfm-ipc.sock' | ||||
|         else: | ||||
|             self.ipc_address    = '127.0.0.1' | ||||
|             self.ipc_port       = 4848 | ||||
|  | ||||
|  | ||||
|     @threaded | ||||
|     def create_ipc_server(self): | ||||
|         if self._conn_type == "socket": | ||||
|             if os.path.exists(self.ipc_address): | ||||
|                 return | ||||
|  | ||||
|             listener = Listener(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey) | ||||
|         else: | ||||
|             listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) | ||||
|  | ||||
|  | ||||
|         self.is_ipc_alive = True | ||||
|         while True: | ||||
|             conn       = listener.accept() | ||||
|             start_time = time.time() | ||||
|  | ||||
|             print(f"New Connection: {listener.last_accepted}") | ||||
|             while True: | ||||
|                 msg = conn.recv() | ||||
|                 if debug: | ||||
|                     print(msg) | ||||
|  | ||||
|                 if "FILE|" in msg: | ||||
|                     file = msg.split("FILE|")[1].strip() | ||||
|                     if file: | ||||
|                         event_system.push_gui_event([None, "handle_file_from_ipc", (file,)]) | ||||
|  | ||||
|                     conn.close() | ||||
|                     break | ||||
|  | ||||
|  | ||||
|                 if msg == 'close connection': | ||||
|                     conn.close() | ||||
|                     break | ||||
|                 if msg == 'close server': | ||||
|                     conn.close() | ||||
|                     break | ||||
|  | ||||
|                 # NOTE: Not perfect but insures we don't lock up the connection for too long. | ||||
|                 end_time = time.time() | ||||
|                 if (end - start) > self.ipc_timeout: | ||||
|                     conn.close() | ||||
|  | ||||
|         listener.close() | ||||
|  | ||||
|  | ||||
|     def send_ipc_message(self, message="Empty Data..."): | ||||
|         try: | ||||
|             if self._conn_type == "socket": | ||||
|                 conn = Client(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey) | ||||
|             else: | ||||
|                 conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey) | ||||
|  | ||||
|  | ||||
|             conn.send(message) | ||||
|             conn.send('close connection') | ||||
|         except Exception as e: | ||||
|             print(repr(e)) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
|     Gtk Bound Plugins Module | ||||
| """ | ||||
| @@ -1,83 +0,0 @@ | ||||
| # Python imports | ||||
| import os, sys, importlib, traceback | ||||
| from os.path import join, isdir | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| gi.require_version('Gtk', '3.0') | ||||
| from gi.repository import Gtk, Gio | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class Plugin: | ||||
|     name          = None | ||||
|     module        = None | ||||
|     reference     = None | ||||
|  | ||||
|  | ||||
| class Plugins: | ||||
|     """Plugins controller""" | ||||
|  | ||||
|     def __init__(self, settings): | ||||
|         self._settings            = settings | ||||
|         self._builder             = self._settings.get_builder() | ||||
|         self._plugins_path        = self._settings.get_plugins_path() | ||||
|         self._plugins_dir_watcher = None | ||||
|         self._plugin_collection   = [] | ||||
|  | ||||
|  | ||||
|     def launch_plugins(self): | ||||
|         self._set_plugins_watcher() | ||||
|         self.load_plugins() | ||||
|  | ||||
|     def _set_plugins_watcher(self): | ||||
|         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) | ||||
|  | ||||
|     # @threaded | ||||
|     def load_plugins(self, file=None): | ||||
|         print(f"Loading plugins...") | ||||
|         parent_path = os.getcwd() | ||||
|  | ||||
|         for file in os.listdir(self._plugins_path): | ||||
|             try: | ||||
|                 path = join(self._plugins_path, file) | ||||
|                 if isdir(path): | ||||
|                     os.chdir(path) | ||||
|  | ||||
|                     sys.path.insert(0, path) | ||||
|                     spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py")) | ||||
|                     app  = importlib.util.module_from_spec(spec) | ||||
|                     spec.loader.exec_module(app) | ||||
|  | ||||
|                     plugin_reference = app.Plugin(self._builder, event_system) | ||||
|                     plugin           = Plugin() | ||||
|                     plugin.name      = plugin_reference.get_plugin_name() | ||||
|                     plugin.module    = path | ||||
|                     plugin.reference = plugin_reference | ||||
|  | ||||
|                     self._plugin_collection.append(plugin) | ||||
|             except Exception as e: | ||||
|                 print("Malformed plugin! Not loading!") | ||||
|                 traceback.print_exc() | ||||
|  | ||||
|         os.chdir(parent_path) | ||||
|  | ||||
|  | ||||
|     def reload_plugins(self, file=None): | ||||
|         print(f"Reloading plugins... stub.") | ||||
|  | ||||
|     def send_message_to_plugin(self, type, data): | ||||
|         print("Trying to send message to plugin...") | ||||
|         for plugin in self._plugin_collection: | ||||
|             if type in plugin.name: | ||||
|                 print('Found plugin; posting message...') | ||||
|                 plugin.reference.set_message(data) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Root of ShellFM | ||||
| """ | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Window module | ||||
| """ | ||||
| @@ -1,185 +0,0 @@ | ||||
| # Python imports | ||||
| import threading, json | ||||
| from os import path | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
| from .window import Window | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| class WindowController: | ||||
|     def __init__(self): | ||||
|         USER_HOME               = path.expanduser('~') | ||||
|         CONFIG_PATH             = USER_HOME   + "/.config/solarfm" | ||||
|         self._session_file      = CONFIG_PATH + "/session.json" | ||||
|  | ||||
|         self._event_sleep_time  = 1 | ||||
|         self._active_window_id  = "" | ||||
|         self._active_tab_id     = "" | ||||
|         self._windows           = [] | ||||
|  | ||||
|  | ||||
|     def set__wid_and_tid(self, wid, tid): | ||||
|         self._active_window_id = str(wid) | ||||
|         self._active_tab_id    = str(tid) | ||||
|  | ||||
|     def get_active_wid_and_tid(self): | ||||
|         return self._active_window_id, self._active_tab_id | ||||
|  | ||||
|     def create_window(self): | ||||
|         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): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 return window.create_tab() | ||||
|  | ||||
|     def add_tab_for_window_by_name(self, name): | ||||
|         for window in self._windows: | ||||
|             if window.get_name() == name: | ||||
|                 return window.create_tab() | ||||
|  | ||||
|     def add_tab_for_window_by_nickname(self, nickname): | ||||
|         for window in self._windows: | ||||
|             if window.get_nickname() == nickname: | ||||
|                 return window.create_tab() | ||||
|  | ||||
|     def pop_window(self): | ||||
|         self._windows.pop() | ||||
|  | ||||
|     def delete_window_by_id(self, win_id): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 self._windows.remove(window) | ||||
|                 break | ||||
|  | ||||
|     def delete_window_by_name(self, name): | ||||
|         for window in self._windows: | ||||
|             if window.get_name() == name: | ||||
|                 self._windows.remove(window) | ||||
|                 break | ||||
|  | ||||
|     def delete_window_by_nickname(self, nickname): | ||||
|         for window in self._windows: | ||||
|             if window.get_nickname() == nickname: | ||||
|                 self._windows.remove(window) | ||||
|                 break | ||||
|  | ||||
|     def get_window_by_id(self, win_id): | ||||
|         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): | ||||
|         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): | ||||
|         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): | ||||
|         return self._windows[index] | ||||
|  | ||||
|     def get_all_windows(self): | ||||
|         return self._windows | ||||
|  | ||||
|  | ||||
|     def set_window_nickname(self, win_id = None, nickname = ""): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 window.set_nickname(nickname) | ||||
|  | ||||
|     def list_windows(self): | ||||
|         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): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 window.list_files_from_tabs() | ||||
|                 break | ||||
|  | ||||
|     def get_tabs_count(self, win_id): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 return window.get_tabs_count() | ||||
|  | ||||
|     def get_tabs_from_window(self, win_id): | ||||
|         for window in self._windows: | ||||
|             if window.get_id() == win_id: | ||||
|                 return window.get_all_tabs() | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     def unload_tabs_and_windows(self): | ||||
|         for window in self._windows: | ||||
|             window.get_all_tabs().clear() | ||||
|  | ||||
|         self._windows.clear() | ||||
|  | ||||
|     def save_state(self, session_file = 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 load_state(self, session_file = None): | ||||
|         if not session_file: | ||||
|             session_file = self._session_file | ||||
|  | ||||
|         if path.isfile(session_file): | ||||
|             with open(session_file) as infile: | ||||
|                 return json.load(infile) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Tabs module | ||||
| """ | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Icons module | ||||
| """ | ||||
| @@ -1,82 +0,0 @@ | ||||
| # Python Imports | ||||
| import os, subprocess, threading, hashlib | ||||
| from os.path import isfile | ||||
|  | ||||
| # Gtk imports | ||||
| import gi | ||||
| gi.require_version('GdkPixbuf', '2.0') | ||||
| from gi.repository import GdkPixbuf | ||||
|  | ||||
| # Application imports | ||||
| from .mixins.desktopiconmixin import DesktopIconMixin | ||||
| from .mixins.videoiconmixin import VideoIconMixin | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| class Icon(DesktopIconMixin, VideoIconMixin): | ||||
|     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.fvideos):              # Video icon | ||||
|                 thumbnl = self.create_thumbnail(dir, file) | ||||
|             elif file.lower().endswith(self.fimages):            # Image Icon | ||||
|                 thumbnl = self.create_scaled_image(full_path, self.VIDEO_ICON_WH) | ||||
|             elif full_path.lower().endswith( ('.desktop',) ):    # .desktop file parsing | ||||
|                 thumbnl = self.parse_desktop_files(full_path) | ||||
|  | ||||
|             return thumbnl | ||||
|         except Exception as e: | ||||
|             return None | ||||
|  | ||||
|     def create_thumbnail(self, dir, file): | ||||
|         full_path = f"{dir}/{file}" | ||||
|         try: | ||||
|             file_hash    = hashlib.sha256(str.encode(full_path)).hexdigest() | ||||
|             hash_img_pth = f"{self.ABS_THUMBS_PTH}/{file_hash}.jpg" | ||||
|             if isfile(hash_img_pth) == False: | ||||
|                 self.generate_video_thumbnail(full_path, hash_img_pth) | ||||
|  | ||||
|             thumbnl = self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) | ||||
|             if thumbnl == None: # If no icon whatsoever, return internal default | ||||
|                 thumbnl = GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") | ||||
|  | ||||
|             return thumbnl | ||||
|         except Exception as e: | ||||
|             print("Thumbnail generation issue:") | ||||
|             print( repr(e) ) | ||||
|             return GdkPixbuf.Pixbuf.new_from_file(f"{self.DEFAULT_ICONS}/video.png") | ||||
|  | ||||
|  | ||||
|     def create_scaled_image(self, path, wxh): | ||||
|         try: | ||||
|                 if path.lower().endswith(".gif"): | ||||
|                     return  GdkPixbuf.PixbufAnimation.new_from_file(path) \ | ||||
|                                                         .get_static_image() \ | ||||
|                                                         .scale_simple(wxh[0], wxh[1], GdkPixbuf.InterpType.BILINEAR) | ||||
|                 else: | ||||
|                     return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, wxh[0], wxh[1], True) | ||||
|         except Exception as e: | ||||
|             print("Image Scaling Issue:") | ||||
|             print( repr(e) ) | ||||
|             return None | ||||
|  | ||||
|     def create_from_file(self, path): | ||||
|         try: | ||||
|             return GdkPixbuf.Pixbuf.new_from_file(path) | ||||
|         except Exception as e: | ||||
|             print("Image from file Issue:") | ||||
|             print( repr(e) ) | ||||
|             return None | ||||
|  | ||||
|     def return_generic_icon(self): | ||||
|         return GdkPixbuf.Pixbuf.new_from_file(self.DEFAULT_ICON) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Icons mixins module | ||||
| """ | ||||
| @@ -1,62 +0,0 @@ | ||||
| # Python Imports | ||||
| import os, subprocess, hashlib | ||||
| from os.path import isfile | ||||
|  | ||||
| # Gtk imports | ||||
|  | ||||
| # Application imports | ||||
| from .xdg.DesktopEntry import DesktopEntry | ||||
|  | ||||
|  | ||||
| class DesktopIconMixin: | ||||
|     def parse_desktop_files(self, full_path): | ||||
|         try: | ||||
|             xdgObj        = DesktopEntry(full_path) | ||||
|             icon          = xdgObj.getIcon() | ||||
|             alt_icon_path = "" | ||||
|  | ||||
|             if "steam" in icon: | ||||
|                 name         = xdgObj.getName() | ||||
|                 file_hash    = hashlib.sha256(str.encode(name)).hexdigest() | ||||
|                 hash_img_pth = self.STEAM_ICONS_PTH + "/" + file_hash + ".jpg" | ||||
|  | ||||
|                 if isfile(hash_img_pth) == True: | ||||
|                     # Use video sizes since headers are bigger | ||||
|                     return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) | ||||
|  | ||||
|                 exec_str  = xdgObj.getExec() | ||||
|                 parts     = exec_str.split("steam://rungameid/") | ||||
|                 id        = parts[len(parts) - 1] | ||||
|                 imageLink = self.STEAM_BASE_URL + id + "/header.jpg" | ||||
|                 proc      = subprocess.Popen(["wget", "-O", hash_img_pth, imageLink]) | ||||
|                 proc.wait() | ||||
|  | ||||
|                 # Use video thumbnail sizes since headers are bigger | ||||
|                 return self.create_scaled_image(hash_img_pth, self.VIDEO_ICON_WH) | ||||
|             elif os.path.exists(icon): | ||||
|                 return self.create_scaled_image(icon, self.SYS_ICON_WH) | ||||
|             else: | ||||
|                 alt_icon_path = "" | ||||
|  | ||||
|                 for dir in self.ICON_DIRS: | ||||
|                     alt_icon_path = self.traverse_icons_folder(dir, icon) | ||||
|                     if alt_icon_path != "": | ||||
|                         break | ||||
|  | ||||
|                 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 traverse_icons_folder(self, path, icon): | ||||
|         alt_icon_path = "" | ||||
|  | ||||
|         for (dirpath, dirnames, filenames) in os.walk(path): | ||||
|             for file in filenames: | ||||
|                 appNM = "application-x-" + icon | ||||
|                 if icon in file or appNM in file: | ||||
|                     alt_icon_path = dirpath + "/" + file | ||||
|                     break | ||||
|  | ||||
|         return alt_icon_path | ||||
| @@ -1,53 +0,0 @@ | ||||
| # Python Imports | ||||
| import subprocess | ||||
|  | ||||
| # Gtk imports | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class VideoIconMixin: | ||||
|     def generate_video_thumbnail(self, full_path, hash_img_pth): | ||||
|         try: | ||||
|             proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", full_path, "-o", hash_img_pth]) | ||||
|             proc.wait() | ||||
|         except Exception as e: | ||||
|             self.logger.debug(repr(e)) | ||||
|             self.ffprobe_generate_video_thumbnail(full_path, hash_img_pth) | ||||
|  | ||||
|  | ||||
|     def ffprobe_generate_video_thumbnail(self, full_path, hash_img_pth): | ||||
|         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_pth] | ||||
|             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)) | ||||
| @@ -1,160 +0,0 @@ | ||||
| """ | ||||
| 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 | ||||
| @@ -1,39 +0,0 @@ | ||||
| """ | ||||
| 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 | ||||
| @@ -1,435 +0,0 @@ | ||||
| """ | ||||
| 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) | ||||
| @@ -1,84 +0,0 @@ | ||||
| """ | ||||
| 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 | ||||
| @@ -1,445 +0,0 @@ | ||||
| """ | ||||
| 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: | ||||
|                 if debug: | ||||
|                     raise e | ||||
|                 else: | ||||
|                     pass | ||||
|  | ||||
|     # 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 | ||||
| @@ -1,419 +0,0 @@ | ||||
| """ | ||||
| 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 | ||||
| @@ -1,79 +0,0 @@ | ||||
| """ | ||||
| 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() | ||||
| @@ -1,541 +0,0 @@ | ||||
| """ 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 | ||||
| @@ -1,780 +0,0 @@ | ||||
| """ | ||||
| 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) | ||||
| @@ -1,181 +0,0 @@ | ||||
| """ | ||||
| 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 | ||||
| @@ -1,3 +0,0 @@ | ||||
| __all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] | ||||
|  | ||||
| __version__ = "0.26" | ||||
| @@ -1,75 +0,0 @@ | ||||
| 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 | ||||
| @@ -1,62 +0,0 @@ | ||||
| # Python imports | ||||
| import os | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class Path: | ||||
|     def get_home(self): | ||||
|         return os.path.expanduser("~") + self.subpath | ||||
|  | ||||
|     def get_path(self): | ||||
|         return f"/{'/'.join(self.path)}" if self.path else f"/{''.join(self.path)}" | ||||
|  | ||||
|     def get_path_list(self): | ||||
|         return self.path | ||||
|  | ||||
|     def push_to_path(self, dir): | ||||
|         self.path.append(dir) | ||||
|         self.load_directory() | ||||
|  | ||||
|     def pop_from_path(self): | ||||
|         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): | ||||
|         if path == self.get_path(): | ||||
|             return | ||||
|  | ||||
|         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): | ||||
|         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): | ||||
|         home = os.path.expanduser("~") + self.subpath | ||||
|         path = list( filter(None, home.replace("\\", "/").split('/')) ) | ||||
|         self.path = path | ||||
|         self.load_directory() | ||||
| @@ -1,246 +0,0 @@ | ||||
| # Python imports | ||||
| import hashlib, re | ||||
| from os import listdir | ||||
| from os.path import isdir, isfile, 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 | ||||
|  | ||||
|  | ||||
| class Tab(Settings, FileHandler, Launcher, Icon, Path): | ||||
|     def __init__(self): | ||||
|         self.logger      = None | ||||
|         self._id_length   = 10 | ||||
|  | ||||
|         self._id          = "" | ||||
|         self._wid         = None | ||||
|         self._dir_watcher = None | ||||
|         self._hide_hidden = self.HIDE_HIDDEN_FILES | ||||
|         self._files       = [] | ||||
|         self._dirs        = [] | ||||
|         self._vids        = [] | ||||
|         self._images      = [] | ||||
|         self._desktop     = [] | ||||
|         self._ungrouped   = [] | ||||
|         self._hidden      = [] | ||||
|  | ||||
|         self._generate_id() | ||||
|         self.set_to_home() | ||||
|  | ||||
|     def load_directory(self): | ||||
|         path            = self.get_path() | ||||
|         self._dirs      = [] | ||||
|         self._vids      = [] | ||||
|         self._images    = [] | ||||
|         self._desktop   = [] | ||||
|         self._ungrouped = [] | ||||
|         self._hidden    = [] | ||||
|         self._files     = [] | ||||
|  | ||||
|         if not isdir(path): | ||||
|             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): | ||||
|         return len(self._files)    + \ | ||||
|                 len(self._dirs)    + \ | ||||
|                 len(self._vids)    + \ | ||||
|                 len(self._images)  + \ | ||||
|                 len(self._desktop) + \ | ||||
|                 len(self._ungrouped) | ||||
|  | ||||
|     def get_hidden_count(self): | ||||
|         return len(self._hidden) | ||||
|  | ||||
|     def get_files_count(self): | ||||
|         return len(self._files) | ||||
|  | ||||
|     def get_path_part_from_hash(self, hash): | ||||
|         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): | ||||
|         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_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): | ||||
|         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): | ||||
|         return self.get_path() | ||||
|  | ||||
|     def get_current_sub_path(self): | ||||
|         path = self.get_path() | ||||
|         home = f"{self.get_home()}/" | ||||
|         return path.replace(home, "") | ||||
|  | ||||
|     def get_end_of_path(self): | ||||
|         parts = self.get_current_directory().split("/") | ||||
|         size  = len(parts) | ||||
|         return parts[size - 1] | ||||
|  | ||||
|  | ||||
|     def set_hiding_hidden(self, state): | ||||
|         self._hide_hidden = state | ||||
|  | ||||
|     def is_hiding_hidden(self): | ||||
|         return self._hide_hidden | ||||
|  | ||||
|     def get_dot_dots(self): | ||||
|         return self._hash_set(['.', '..']) | ||||
|  | ||||
|     def get_files(self): | ||||
|         return self._hash_set(self._files) | ||||
|  | ||||
|     def get_dirs(self): | ||||
|         return self._hash_set(self._dirs) | ||||
|  | ||||
|     def get_videos(self): | ||||
|         return self._hash_set(self._vids) | ||||
|  | ||||
|     def get_images(self): | ||||
|         return self._hash_set(self._images) | ||||
|  | ||||
|     def get_desktops(self): | ||||
|         return self._hash_set(self._desktop) | ||||
|  | ||||
|     def get_ungrouped(self): | ||||
|         return self._hash_set(self._ungrouped) | ||||
|  | ||||
|     def get_hidden(self): | ||||
|         return self._hash_set(self._hidden) | ||||
|  | ||||
|     def get_id(self): | ||||
|         return self._id | ||||
|  | ||||
|     def set_wid(self, _wid): | ||||
|         self._wid = _wid | ||||
|  | ||||
|     def get_wid(self): | ||||
|         return self._wid | ||||
|  | ||||
|     def set_dir_watcher(self, watcher): | ||||
|         self._dir_watcher = watcher | ||||
|  | ||||
|     def get_dir_watcher(self): | ||||
|         return self._dir_watcher | ||||
|  | ||||
|     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): | ||||
|         return hashlib.sha256(str.encode(text)).hexdigest()[:18] | ||||
|  | ||||
|     def _hash_set(self, arry): | ||||
|         data = [] | ||||
|         for arr in arry: | ||||
|             data.append([arr, self._hash_text(arr)]) | ||||
|         return data | ||||
|  | ||||
|     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)) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Utils module | ||||
| """ | ||||
| @@ -1,86 +0,0 @@ | ||||
| # Python imports | ||||
| import os, 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, toDeleteFile): | ||||
|         try: | ||||
|             print(f"Deleting:  {toDeleteFile}") | ||||
|             if os.path.exists(toDeleteFile): | ||||
|                 if os.path.isfile(toDeleteFile): | ||||
|                     os.remove(toDeleteFile) | ||||
|                 elif os.path.isdir(toDeleteFile): | ||||
|                     shutil.rmtree(toDeleteFile) | ||||
|                 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 | ||||
| @@ -1,114 +0,0 @@ | ||||
| # System import | ||||
| import os, threading, subprocess | ||||
|  | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
|  | ||||
| # Apoplication imports | ||||
|  | ||||
|  | ||||
| def threaded(fn): | ||||
|     def wrapper(*args, **kwargs): | ||||
|         threading.Thread(target=fn, args=args, kwargs=kwargs).start() | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| class Launcher: | ||||
|     def open_file_locally(self, file): | ||||
|         lowerName = file.lower() | ||||
|         command   = [] | ||||
|  | ||||
|         if lowerName.endswith(self.fvideos): | ||||
|             command = [self.media_app] | ||||
|  | ||||
|             if "mplayer" in self.media_app: | ||||
|                 command += self.mplayer_options | ||||
|  | ||||
|             command += [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.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, use_shell=False) | ||||
|  | ||||
|  | ||||
|     def execute(self, command, start_dir=os.getenv("HOME"), use_os_system=None, use_shell=True): | ||||
|         self.logger.debug(command) | ||||
|         if use_os_system: | ||||
|             os.system(command) | ||||
|         else: | ||||
|             subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=True, stdout=None, stderr=None, close_fds=True) | ||||
|  | ||||
|     def execute_and_return_thread_handler(self, command, start_dir=os.getenv("HOME"), use_shell=True): | ||||
|         DEVNULL = open(os.devnull, 'w') | ||||
|         return subprocess.Popen(command, cwd=start_dir, shell=use_shell, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True) | ||||
|  | ||||
|     @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 Exception as e: | ||||
|                 self.logger.debug(message) | ||||
|                 self.logger.debug(e) | ||||
|                 return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def check_remux_space(self): | ||||
|         limit = self.remux_folder_max_disk_usage | ||||
|         try: | ||||
|             limit = int(limit) | ||||
|         except Exception 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 | ||||
| @@ -1,100 +0,0 @@ | ||||
| # System import | ||||
| import json | ||||
| import os | ||||
| from os import path | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
|  | ||||
| # Apoplication imports | ||||
|  | ||||
|  | ||||
|  | ||||
| class Settings: | ||||
|     logger            = None | ||||
|  | ||||
|     USR_SOLARFM       = "/usr/share/solarfm" | ||||
|     USER_HOME         = path.expanduser('~') | ||||
|     CONFIG_PATH       = f"{USER_HOME}/.config/solarfm" | ||||
|     CONFIG_FILE       = f"{CONFIG_PATH}/settings.json" | ||||
|     HIDE_HIDDEN_FILES = True | ||||
|  | ||||
|     GTK_ORIENTATION   = 1      # HORIZONTAL (0) VERTICAL (1) | ||||
|     DEFAULT_ICONS     = f"{CONFIG_PATH}/icons" | ||||
|     DEFAULT_ICON      = f"{DEFAULT_ICONS}/text.png" | ||||
|     FFMPG_THUMBNLR    = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary | ||||
|     REMUX_FOLDER      = f"{USER_HOME}/.remuxs"             # Remuxed files folder | ||||
|  | ||||
|     STEAM_BASE_URL    = "https://steamcdn-a.akamaihd.net/steam/apps/" | ||||
|     ICON_DIRS         = ["/usr/share/pixmaps", "/usr/share/icons", f"{USER_HOME}/.icons" ,] | ||||
|     BASE_THUMBS_PTH   = f"{USER_HOME}/.thumbnails"         # Used for thumbnail generation | ||||
|     ABS_THUMBS_PTH    = f"{BASE_THUMBS_PTH}/normal"        # Used for thumbnail generation | ||||
|     STEAM_ICONS_PTH   = f"{BASE_THUMBS_PTH}/steam_icons" | ||||
|     CONTAINER_ICON_WH = [128, 128] | ||||
|     VIDEO_ICON_WH     = [128, 64] | ||||
|     SYS_ICON_WH       = [56, 56] | ||||
|  | ||||
|     # CONTAINER_ICON_WH = [128, 128] | ||||
|     # VIDEO_ICON_WH     = [96, 48] | ||||
|     # SYS_ICON_WH       = [96, 96] | ||||
|  | ||||
|     subpath           = "" | ||||
|     go_past_home      = None | ||||
|     lock_folder       = None | ||||
|     locked_folders    = None | ||||
|     mplayer_options   = None | ||||
|     music_app         = None | ||||
|     media_app         = None | ||||
|     image_app         = None | ||||
|     office_app        = None | ||||
|     pdf_app           = None | ||||
|     text_app          = None | ||||
|     file_manager_app  = None | ||||
|     remux_folder_max_disk_usage = None | ||||
|  | ||||
|     if path.isfile(CONFIG_FILE): | ||||
|         with open(CONFIG_FILE) as infile: | ||||
|             settings          = json.load(infile)["settings"] | ||||
|  | ||||
|             subpath           = settings["base_of_home"] | ||||
|             HIDE_HIDDEN_FILES = True if settings["hide_hidden_files"] == "true" else False | ||||
|             FFMPG_THUMBNLR    = FFMPG_THUMBNLR if settings["thumbnailer_path"] == "" else settings["thumbnailer_path"] | ||||
|             go_past_home      = True if settings["go_past_home"] == "" else settings["go_past_home"]  | ||||
|             lock_folder       = True if settings["lock_folder"] == "true" else False | ||||
|             locked_folders    = settings["locked_folders"].split("::::") | ||||
|             mplayer_options   = settings["mplayer_options"].split() | ||||
|             music_app         = settings["music_app"] | ||||
|             media_app         = settings["media_app"] | ||||
|             image_app         = settings["image_app"] | ||||
|             office_app        = settings["office_app"] | ||||
|             pdf_app           = settings["pdf_app"] | ||||
|             text_app          = settings["text_app"] | ||||
|             file_manager_app  = settings["file_manager_app"] | ||||
|             terminal_app      = settings["terminal_app"] | ||||
|             remux_folder_max_disk_usage = settings["remux_folder_max_disk_usage"] | ||||
|  | ||||
|     # Filters | ||||
|     fvideos = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm') | ||||
|     foffice = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf') | ||||
|     fimages = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga') | ||||
|     ftext   = ('.txt', '.text', '.sh', '.cfg', '.conf') | ||||
|     fmusic  = ('.psf', '.mp3', '.ogg', '.flac', '.m4a') | ||||
|     fpdf    = ('.pdf') | ||||
|  | ||||
|  | ||||
|     # Dir structure check | ||||
|     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_SOLARFM}/icons" | ||||
|         DEFAULT_ICON  = f"{DEFAULT_ICONS}/text.png" | ||||
| @@ -1,87 +0,0 @@ | ||||
| # Python imports | ||||
| from random import randint | ||||
|  | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
|  | ||||
| # Application imports | ||||
| from .tabs.tab import Tab | ||||
|  | ||||
|  | ||||
| class Window: | ||||
|     def __init__(self): | ||||
|         self._id_length = 10 | ||||
|         self._id        = "" | ||||
|         self._name      = "" | ||||
|         self._nickname  = "" | ||||
|         self._isHidden  = False | ||||
|         self._tabs      = [] | ||||
|  | ||||
|         self._generate_id() | ||||
|         self._set_name() | ||||
|  | ||||
|  | ||||
|     def create_tab(self): | ||||
|         tab = Tab() | ||||
|         self._tabs.append(tab) | ||||
|         return tab | ||||
|  | ||||
|     def pop_tab(self): | ||||
|         self._tabs.pop() | ||||
|  | ||||
|     def delete_tab_by_id(self, vid): | ||||
|         for tab in self._tabs: | ||||
|             if tab.get_id() == vid: | ||||
|                 self._tabs.remove(tab) | ||||
|                 break | ||||
|  | ||||
|  | ||||
|     def get_tab_by_id(self, vid): | ||||
|         for tab in self._tabs: | ||||
|             if tab.get_id() == vid: | ||||
|                 return tab | ||||
|  | ||||
|     def get_tab_by_index(self, index): | ||||
|         return self._tabs[index] | ||||
|  | ||||
|     def get_tabs_count(self): | ||||
|         return len(self._tabs) | ||||
|  | ||||
|     def get_all_tabs(self): | ||||
|         return self._tabs | ||||
|  | ||||
|     def get_id(self): | ||||
|         return self._id | ||||
|  | ||||
|     def get_name(self): | ||||
|         return self._name | ||||
|  | ||||
|     def get_nickname(self): | ||||
|         return self._nickname | ||||
|  | ||||
|     def is_hidden(self): | ||||
|         return self._isHidden | ||||
|  | ||||
|     def list_files_from_tabs(self): | ||||
|         for tab in self._tabs: | ||||
|             print(tab.get_files()) | ||||
|  | ||||
|  | ||||
|     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)) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
| Trasher module | ||||
| """ | ||||
| @@ -1,46 +0,0 @@ | ||||
| # Python imports | ||||
| import os | ||||
|  | ||||
| # Lib imports | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class Trash(object): | ||||
|     """Base Trash class.""" | ||||
|  | ||||
|     def size_dir(self, sdir): | ||||
|         """Get the size of a directory.  Based on code found online.""" | ||||
|         size = os.path.getsize(sdir) | ||||
|  | ||||
|         for item in os.listdir(sdir): | ||||
|             item = os.path.join(sdir, item) | ||||
|  | ||||
|             if os.path.isfile(item): | ||||
|                 size = size + os.path.getsize(item) | ||||
|             elif os.path.isdir(item): | ||||
|                 size = size + self.size_dir(item) | ||||
|  | ||||
|         return size | ||||
|  | ||||
|     def regenerate(self): | ||||
|         """Regenerate the trash and recreate metadata.""" | ||||
|         pass  # Some backends don’t need regeneration. | ||||
|  | ||||
|     def empty(self, verbose): | ||||
|         """Empty the trash.""" | ||||
|         raise NotImplementedError(_('Backend didn’t implement this functionality')) | ||||
|  | ||||
|     def list(self, human=True): | ||||
|         """List the trash contents.""" | ||||
|         raise NotImplementedError(_('Backend didn’t implement this functionality')) | ||||
|  | ||||
|     def trash(self, filepath, verbose): | ||||
|         """Move specified file to trash.""" | ||||
|         raise NotImplementedError(_('Backend didn’t implement this functionality')) | ||||
|  | ||||
|     def restore(self, filename, verbose): | ||||
|         """Restore a file from trash.""" | ||||
|         raise NotImplementedError(_('Backend didn’t \ implement this functionality')) | ||||
| @@ -1,161 +0,0 @@ | ||||
| from .trash import Trash | ||||
| import shutil | ||||
| import os | ||||
| import os.path | ||||
| import datetime | ||||
| import sys | ||||
| import logging | ||||
|  | ||||
| try: | ||||
|     import configparser | ||||
| except ImportError: | ||||
|     import ConfigParser as configparser | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| class XDGTrash(Trash): | ||||
|     """XDG trash backend.""" | ||||
|     def __init__(self): | ||||
|         self.trashdir = None | ||||
|         self.filedir  = None | ||||
|         self.infodir  = None | ||||
|  | ||||
|         if os.getenv('XDG_DATA_HOME') is None: | ||||
|             self.trashdir = os.path.expanduser('~/.local/share/Trash') | ||||
|         else: | ||||
|             self.trashdir = os.getenv('XDG_DATA_HOME') + '/Trash' | ||||
|  | ||||
|         try: | ||||
|             if not os.path.exists(self.trashdir): | ||||
|                 os.mkdir(self.trashdir) | ||||
|         except OSError: | ||||
|             self.trashdir = os.path.join('tmp' 'TRASH') | ||||
|             raise('Couldn’t access the proper directory, temporary trash  is in in /tmp/TRASH') | ||||
|  | ||||
|         self.filedir = self.trashdir + '/files/' | ||||
|         self.infodir = self.trashdir + '/info/' | ||||
|  | ||||
|     def regenerate(self): | ||||
|         """Regenerate the trash and recreate metadata.""" | ||||
|         print('Regenerating the trash and recreating metadata...') | ||||
|         zerosize = False | ||||
|  | ||||
|         if not os.path.exists(self.trashdir): | ||||
|             os.mkdir(self.trashdir) | ||||
|             zerosize = True | ||||
|  | ||||
|         if ((not os.path.exists(self.filedir)) or | ||||
|                 (not os.path.exists(self.infodir))): | ||||
|             os.mkdir(self.filedir) | ||||
|             os.mkdir(self.infodir) | ||||
|             zerosize = True | ||||
|         if not zerosize: | ||||
|             trashsize = (self.size_dir(self.filedir) + self.size_dir(self.infodir)) | ||||
|         else: | ||||
|             trashsize = 0 | ||||
|  | ||||
|         infofile = '[Cached]\nSize=' + str(trashsize) + '\n' | ||||
|         fh = open(os.path.join(self.trashdir, 'metadata'), 'w') | ||||
|         fh.write(infofile) | ||||
|         fh.close() | ||||
|  | ||||
|     def empty(self, verbose): | ||||
|         """Empty the trash.""" | ||||
|         print('emptying (verbose={})'.format(verbose)) | ||||
|         shutil.rmtree(self.filedir) | ||||
|         shutil.rmtree(self.infodir) | ||||
|         self.regenerate() | ||||
|         if verbose: | ||||
|             sys.stderr.write(_('emptied the trash\n')) | ||||
|  | ||||
|     def list(self, human=True): | ||||
|         """List the trash contents.""" | ||||
|         if human: | ||||
|             print('listing contents (on stdout; human=True)') | ||||
|         else: | ||||
|             print('listing contents (return; human=False)') | ||||
|         dirs  = [] | ||||
|         files = [] | ||||
|         for f in os.listdir(self.filedir): | ||||
|             if os.path.isdir(self.filedir + f): | ||||
|                 dirs.append(f) | ||||
|             else: | ||||
|                 files.append(f) | ||||
|  | ||||
|         dirs.sort() | ||||
|         files.sort() | ||||
|  | ||||
|         allfiles = [] | ||||
|         for i in dirs: | ||||
|             allfiles.append(i + '/') | ||||
|         for i in files: | ||||
|             allfiles.append(i) | ||||
|         if human: | ||||
|             if allfiles != []: | ||||
|                 print('\n'.join(allfiles)) | ||||
|         else: | ||||
|             return allfiles | ||||
|  | ||||
|     def trash(self, filepath, verbose): | ||||
|         """Move specified file to trash.""" | ||||
|         print('trashing file {} (verbose={})'.format(filepath, verbose)) | ||||
|         # Filename alteration, a big mess. | ||||
|         filename = os.path.basename(filepath) | ||||
|         fileext  = os.path.splitext(filename) | ||||
|  | ||||
|         tomove = filename | ||||
|         collision = True | ||||
|         i = 1 | ||||
|  | ||||
|         while collision: | ||||
|             if os.path.lexists(self.filedir + tomove): | ||||
|                 tomove = fileext[0] + ' ' + str(i) + fileext[1] | ||||
|                 i = i + 1 | ||||
|             else: | ||||
|                 collision = False | ||||
|  | ||||
|         infofile = """[Trash Info] | ||||
| Path={} | ||||
| DeletionDate={} | ||||
| """.format(os.path.realpath(filepath), | ||||
|            datetime.datetime.now().strftime('%Y-%m-%dT%H:%m:%S')) | ||||
|  | ||||
|         os.rename(filepath, self.filedir + tomove) | ||||
|  | ||||
|         f = open(os.path.join(self.infodir, tomove + '.trashinfo'), 'w') | ||||
|         f.write(infofile) | ||||
|         f.close() | ||||
|  | ||||
|         self.regenerate() | ||||
|  | ||||
|         if verbose: | ||||
|             sys.stderr.write(_('trashed \'{}\'\n').format(filename)) | ||||
|  | ||||
|     def restore(self, filename, verbose, tocwd=False): | ||||
|         """Restore a file from trash.""" | ||||
|         print('restoring file {} (verbose={}, tocwd={})'.format(filename, verbose, tocwd)) | ||||
|         info = configparser.ConfigParser() | ||||
|         if os.path.exists(os.path.join(self.filedir, filename)): | ||||
|             info.read(os.path.join(self.infodir, filename + '.trashinfo')) | ||||
|             restname = os.path.basename(info.get('Trash Info', 'Path')) | ||||
|  | ||||
|             if tocwd: | ||||
|                 restdir = os.path.abspath('.') | ||||
|             else: | ||||
|                 restdir = os.path.dirname(info.get('Trash Info', 'Path')) | ||||
|  | ||||
|             restfile = os.path.join(restdir, restname) | ||||
|             if not os.path.exists(restdir): | ||||
|                 raise TMError('restore', 'nodir', _('no such directory: {}' | ||||
|                               ' -- cannot restore').format(restdir)) | ||||
|             os.rename(os.path.join(self.filedir, filename), restfile) | ||||
|             os.remove(os.path.join(self.infodir, filename + '.trashinfo')) | ||||
|             self.regenerate() | ||||
|             print('restored {} to {}'.format(filename, restfile)) | ||||
|             if verbose: | ||||
|                 sys.stderr.write(_('restored {} to {}\n').format(filename, restfile)) | ||||
|  | ||||
|         else: | ||||
|             print('couldn\'t find {} in trash'.format(filename)) | ||||
|             raise TMError('restore', 'nofile', _('no such file in trash')) | ||||
| @@ -1,3 +0,0 @@ | ||||
| """ | ||||
|     Utils module | ||||
| """ | ||||
| @@ -1,56 +0,0 @@ | ||||
| # Python imports | ||||
| import os, logging | ||||
|  | ||||
| # Application imports | ||||
|  | ||||
|  | ||||
| class Logger: | ||||
|     def __init__(self, config_path): | ||||
|         self._CONFIG_PATH = config_path | ||||
|  | ||||
|     def get_logger(self, loggerName = "NO_LOGGER_NAME_PASSED", createFile = True): | ||||
|         """ | ||||
|             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 | ||||
|         """ | ||||
|  | ||||
|         globalLogLvl = logging.DEBUG    # Keep this at highest so that handlers can filter to their desired levels | ||||
|         chLogLevel   = logging.CRITICAL # Prety musch the only one we change ever | ||||
|         fhLogLevel   = logging.DEBUG | ||||
|         log          = logging.getLogger(loggerName) | ||||
|         log.setLevel(globalLogLvl) | ||||
|  | ||||
|         # 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=chLogLevel) | ||||
|         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=fhLogLevel) | ||||
|             fh.setFormatter(fFormatter) | ||||
|             log.addHandler(fh) | ||||
|  | ||||
|         return log | ||||
| @@ -1,102 +0,0 @@ | ||||
| # Python imports | ||||
| import os | ||||
| from os import path | ||||
|  | ||||
| # Gtk imports | ||||
| import gi, cairo | ||||
| 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 .logger import Logger | ||||
|  | ||||
|  | ||||
| class Settings: | ||||
|     def __init__(self): | ||||
|         self._SCRIPT_PTH    = os.path.dirname(os.path.realpath(__file__)) | ||||
|         self._USER_HOME     = path.expanduser('~') | ||||
|         self._CONFIG_PATH   = f"{self._USER_HOME}/.config/{app_name.lower()}" | ||||
|         self._PLUGINS_PATH  = f"{self._CONFIG_PATH}/plugins" | ||||
|         self._USR_SOLARFM   = f"/usr/share/{app_name.lower()}" | ||||
|  | ||||
|         self._CSS_FILE      = f"{self._CONFIG_PATH}/stylesheet.css" | ||||
|         self._WINDOWS_GLADE = f"{self._CONFIG_PATH}/Main_Window.glade" | ||||
|         self._DEFAULT_ICONS = f"{self._CONFIG_PATH}/icons" | ||||
|         self._WINDOW_ICON   = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png" | ||||
|  | ||||
|         if not os.path.exists(self._CONFIG_PATH): | ||||
|             os.mkdir(self._CONFIG_PATH) | ||||
|         if not os.path.exists(self._PLUGINS_PATH): | ||||
|             os.mkdir(self._PLUGINS_PATH) | ||||
|  | ||||
|         if not os.path.exists(self._WINDOWS_GLADE): | ||||
|             self._WINDOWS_GLADE = f"{self._USR_SOLARFM}/Main_Window.glade" | ||||
|         if not os.path.exists(self._CSS_FILE): | ||||
|             self._CSS_FILE      = f"{self._USR_SOLARFM}/stylesheet.css" | ||||
|         if not os.path.exists(self._WINDOW_ICON): | ||||
|             self._WINDOW_ICON   = f"{self._USR_SOLARFM}/icons/{app_name.lower()}.png" | ||||
|         if not os.path.exists(self._DEFAULT_ICONS): | ||||
|             self._DEFAULT_ICONS = f"{self._USR_SOLARFM}/icons" | ||||
|  | ||||
|         self._success_color = "#88cc27" | ||||
|         self._warning_color = "#ffa800" | ||||
|         self._error_color   = "#ff0000" | ||||
|  | ||||
|         self.main_window    = None | ||||
|         self.logger         = Logger(self._CONFIG_PATH).get_logger() | ||||
|         self.builder        = Gtk.Builder() | ||||
|         self.builder.add_from_file(self._WINDOWS_GLADE) | ||||
|  | ||||
|  | ||||
|  | ||||
|     def create_window(self): | ||||
|         # Get window and connect signals | ||||
|         self.main_window = self.builder.get_object("Main_Window") | ||||
|         self._set_window_data() | ||||
|  | ||||
|     def _set_window_data(self): | ||||
|         self.main_window.set_icon_from_file(self._WINDOW_ICON) | ||||
|         screen = self.main_window.get_screen() | ||||
|         visual = screen.get_rgba_visual() | ||||
|  | ||||
|         if visual != None and screen.is_composited(): | ||||
|             self.main_window.set_visual(visual) | ||||
|             self.main_window.set_app_paintable(True) | ||||
|             self.main_window.connect("draw", self._area_draw) | ||||
|  | ||||
|         # bind css file | ||||
|         cssProvider  = Gtk.CssProvider() | ||||
|         cssProvider.load_from_path(self._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, cr): | ||||
|         cr.set_source_rgba(0, 0, 0, 0.54) | ||||
|         cr.set_operator(cairo.OPERATOR_SOURCE) | ||||
|         cr.paint() | ||||
|         cr.set_operator(cairo.OPERATOR_OVER) | ||||
|  | ||||
|     def get_monitor_data(self): | ||||
|         screen = self.builder.get_object("Main_Window").get_screen() | ||||
|         monitors = [] | ||||
|         for m in range(screen.get_n_monitors()): | ||||
|             monitors.append(screen.get_monitor_geometry(m)) | ||||
|  | ||||
|         for monitor in monitors: | ||||
|             print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y)) | ||||
|  | ||||
|         return monitors | ||||
|  | ||||
|     def get_builder(self):       return self.builder | ||||
|     def get_logger(self):        return self.logger | ||||
|     def get_main_window(self):   return self.main_window | ||||
|     def get_plugins_path(self):  return self._PLUGINS_PATH | ||||
|  | ||||
|     def get_success_color(self):  return self._success_color | ||||
|     def get_warning_color(self):  return self._warning_color | ||||
|     def get_error_color(self):    return self._error_color | ||||
| @@ -2,9 +2,8 @@ | ||||
| Name=SolarFM | ||||
| GenericName=File Manager | ||||
| Comment=A file manager built with Python and GObject introspection. | ||||
| Path=/home/abaddon/.local/share/solarfm | ||||
| Exec=/home/abaddon/Portable_Apps/py-venvs/flask-apps-venv/venv/bin/python /home/abaddon/.local/share/solarfm %F | ||||
| Icon=/home/abaddon/.local/share/solarfm/resources/solarfm-64x64.png | ||||
| Exec=/bin/solarfm %F | ||||
| Icon=/usr/share/solarfm/icons/solarfm.png | ||||
| Type=Application | ||||
| StartupNotify=true | ||||
| Categories=System;FileTools;Utility;Core;GTK;FileManager; | ||||
| @@ -0,0 +1,17 @@ | ||||
| { | ||||
|     "Open Actions": { | ||||
|         "Open":      ["STOCK_OPEN", "open"], | ||||
|         "Open With": ["STOCK_OPEN", "open_with"], | ||||
|         "Execute":   ["STOCK_EXECUTE", "execute"], | ||||
|         "Execute in Terminal": ["STOCK_EXECUTE", "execute_in_terminal"] | ||||
|     }, | ||||
|     "File Actions": { | ||||
|         "New":       ["STOCK_ADD", "create"], | ||||
|         "Rename":    ["STOCK_EDIT", "rename"], | ||||
|         "Cut":       ["STOCK_CUT", "cut"], | ||||
|         "Copy":      ["STOCK_COPY", "copy"], | ||||
|         "Copy Name": ["STOCK_COPY", "copy_name"], | ||||
|         "Paste":     ["STOCK_PASTE", "paste"] | ||||
|     }, | ||||
|     "Plugins": {} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/3g2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/3gp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/ai.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/air.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/asf.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/avi.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/bib.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/cls.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/csv.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/deb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/djvu.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/dmg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/doc.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/docx.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/dwf.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/dwg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/eps.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/epub.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/exe.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/f.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/f77.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/f90.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/flac.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/flv.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/gif.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/gz.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/ico.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/indd.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/iso.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/jpeg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/debs/solarfm-0-0-1-x64/usr/share/solarfm/fileicons/jpg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.2 KiB |