diff --git a/plugins/README.md b/plugins/README.md index 9495829..2602c94 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,17 +1,17 @@ ### Note -Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' permissions data but don't have to if not directly interacted with. +Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with. Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use. -### Manifest Example (All are required.) +### Manifest Example (All are required even if empty.) ``` class Manifest: - path: str = os.path.dirname(os.path.realpath(__file__)) - name: str = "Example Plugin" - author: str = "John Doe" - version: str = "0.0.1" - support: str = "" - permissions: {} = { + path: str = os.path.dirname(os.path.realpath(__file__)) + name: str = "Example Plugin" + author: str = "John Doe" + version: str = "0.0.1" + support: str = "" + requests: {} = { 'ui_target': "plugin_control_list", 'pass_fm_events': "true" @@ -19,9 +19,9 @@ class Manifest: ``` -### Permissions +### Requests ``` -permissions: {} = { +requests: {} = { 'ui_target': "plugin_control_list", 'ui_target_id': "" # Only needed if using "other" in "ui_target". See below for predefined "ui_target" options... 'pass_fm_events': "true" # If empty or not present will be ignored. diff --git a/plugins/file_properties/file_properties.glade b/plugins/file_properties/file_properties.glade new file mode 100644 index 0000000..7390e9e --- /dev/null +++ b/plugins/file_properties/file_properties.glade @@ -0,0 +1,685 @@ + + + + + + False + 6 + File Properties + True + center-on-parent + 420 + True + dialog + True + True + center + + + + True + False + 12 + + + True + False + end + + + gtk-cancel + True + True + True + False + True + + + True + True + 0 + + + + + gtk-ok + True + True + True + False + True + + + True + True + 1 + + + + + False + False + end + 0 + + + + + True + True + 6 + + + True + False + 6 + 6 + 12 + + + True + False + 4 + 7 + 2 + 12 + 6 + + + True + False + <b>File _Name:</b> + True + True + file_name + 0 + + + GTK_FILL + + + + + + True + True + + + 1 + 2 + + + + + + True + False + <b>_Location:</b> + True + True + file_location + 0 + + + 1 + 2 + GTK_FILL + + + + + + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + True + False + <b>Link _Target:</b> + True + True + file_target + 0 + + + 2 + 3 + GTK_FILL + + + + + + True + True + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + + True + False + <b>Type:</b> + True + True + 0 + 0 + + + 3 + 4 + GTK_FILL + GTK_FILL + + + + + True + True + True + end + 0 + 0 + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + + True + False + <b>Size:</b> + True + True + 0 + + + 4 + 5 + GTK_FILL + + + + + + True + True + True + 0 + + + 1 + 2 + 4 + 5 + GTK_FILL + + + + + + True + False + <b>_Modified:</b> + True + True + 0 + + + 5 + 6 + GTK_FILL + + + + + + True + False + <b>_Accessed:</b> + True + True + 0 + + + 6 + 7 + GTK_FILL + + + + + + True + True + + + 1 + 2 + 5 + 6 + GTK_FILL + + + + + + True + True + + + 1 + 2 + 6 + 7 + GTK_FILL + + + + + + + + + + True + False + _Info + True + + + False + + + + + True + False + 6 + 6 + 12 + + + True + False + 6 + + + True + False + 2 + 2 + 2 + 12 + 6 + + + True + False + <b>_Owner:</b> + True + True + file_owner + 0 + + + GTK_FILL + + + + + + True + False + <b>_Group:</b> + True + True + file_group + 0 + + + 1 + 2 + GTK_FILL + + + + + + True + True + + + 1 + 2 + + + + + + True + True + + + 1 + 2 + 1 + 2 + + + + + + False + False + 0 + + + + + True + False + + + False + False + 1 + + + + + True + False + 4 + 3 + 5 + 12 + 6 + + + True + False + <b>Owner:</b> + True + True + 0 + + + GTK_FILL + + + + + + True + False + <b>Group:</b> + True + True + 0 + + + 1 + 2 + GTK_FILL + + + + + + True + False + <b>Other:</b> + True + True + 0 + + + 2 + 3 + GTK_FILL + + + + + + Read + True + True + False + 2 + True + True + + + 1 + 2 + GTK_FILL + + + + + + Read + True + True + False + 2 + True + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + + Read + True + True + False + 2 + True + True + + + 1 + 2 + 2 + 3 + GTK_FILL + + + + + + Write + True + True + False + 2 + True + True + + + 2 + 3 + GTK_FILL + + + + + + Write + True + True + False + 2 + True + True + + + 2 + 3 + 1 + 2 + GTK_FILL + + + + + + Write + True + True + False + 2 + True + True + + + 2 + 3 + 2 + 3 + GTK_FILL + + + + + + Execute + True + True + False + 2 + True + True + + + 3 + 4 + GTK_FILL + + + + + + Execute + True + True + False + 2 + True + True + + + 3 + 4 + 1 + 2 + GTK_FILL + + + + + + Execute + True + True + False + 2 + True + True + + + 3 + 4 + 2 + 3 + GTK_FILL + + + + + + True + False + + + 4 + 5 + 3 + GTK_FILL + GTK_FILL + + + + + False + True + 2 + + + + + + + 1 + + + + + True + False + _Permissions + True + + + 1 + False + + + + + False + True + 2 + + + + + + cancel_button + ok_button + + + diff --git a/plugins/file_properties/plugin.py b/plugins/file_properties/plugin.py new file mode 100644 index 0000000..72c74a1 --- /dev/null +++ b/plugins/file_properties/plugin.py @@ -0,0 +1,263 @@ +# Python imports +import os, threading, subprocess, time +from datetime import datetime + +# Gtk imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GLib, Gio + +# Application imports + + +# NOTE: Threads WILL NOT die with parent's destruction. +def threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start() + return wrapper + +# NOTE: Threads WILL die with parent's destruction. +def daemon_threaded(fn): + def wrapper(*args, **kwargs): + threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() + return wrapper + + + + +class Manifest: + path: str = os.path.dirname(os.path.realpath(__file__)) + name: str = "Properties" + author: str = "ITDominator" + version: str = "0.0.1" + support: str = "" + requests: {} = { + 'ui_target': "context_menu", + 'pass_fm_events': "true" + } + +class Properties: + file_uri: str = None + file_name: str = None + file_location: str = None + file_target: str = None + mime_type: str = None + file_size: str = None + mtime: int = None + atime: int = None + file_owner: str = None + file_group: str = None + chmod_stat: str = None + + +class Plugin(Manifest): + def __init__(self): + self._GLADE_FILE = f"{self.path}/file_properties.glade" + self._builder = None + self._properties_dialog = None + + self._event_system = None + self._event_sleep_time = .5 + self._event_message = None + + self._file_name = None + self._file_location = None + self._file_target = None + self._mime_type = None + self._file_size = None + self._mtime = None + self._atime = None + self._file_owner = None + self._file_group = None + + self._chmod_map: {} = { + "7": "rwx", + "6": "rw", + "5": "rx", + "4": "r", + "3": "wx", + "2": "w", + "1": "x", + "0": "" + } + + self._chmod_map_counter: {} = { + "rwx": "7", + "rw": "6", + "rx": "5", + "r": "4", + "wx": "3", + "w": "2", + "x": "1", + "": "0" + } + + + def get_ui_element(self): + self._builder = Gtk.Builder() + self._builder.add_from_file(self._GLADE_FILE) + + self._properties_dialog = self._builder.get_object("file_properties_dialog") + self._file_name = self._builder.get_object("file_name") + self._file_location = self._builder.get_object("file_location") + self._file_target = self._builder.get_object("file_target") + self._mime_type = self._builder.get_object("mime_type") + self._file_size = self._builder.get_object("file_size") + self._mtime = self._builder.get_object("mtime") + self._atime = self._builder.get_object("atime") + self._file_owner = self._builder.get_object("file_owner") + self._file_group = self._builder.get_object("file_group") + + button = Gtk.Button(label=self.name) + button.connect("button-release-event", self._show_properties_page) + return button + + def set_fm_event_system(self, fm_event_system): + self._event_system = fm_event_system + + def run(self): + self._module_event_observer() + + + + + @threaded + def _show_properties_page(self, widget=None, eve=None): + self._event_system.push_gui_event([self.name, "get_current_state", ()]) + self.wait_for_fm_message() + + state = self._event_message + self._event_message = None + + GLib.idle_add(self._process_changes, (state)) + + def _process_changes(self, state): + if len(state.selected_files) == 1: + uri = state.selected_files[0] + path = state.tab.get_current_directory() + + + properties = self._set_ui_data(uri, path) + response = self._properties_dialog.run() + if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]: + self._properties_dialog.hide() + + self._update_file(properties) + self._properties_dialog.hide() + + + def _update_file(self, properties): + chmod_stat = self._get_check_boxes() + + if chmod_stat is not properties.chmod_stat: + try: + print("\nNew chmod flags...") + print(f"Old: {''.join(properties.chmod_stat)}") + print(f"New: {chmod_stat}") + # self._guest_fs.chmod(int(chmod_stat), properties.file_uri) + except Exception as e: + print(f"Couldn't chmod\nFile: {properties.file_uri}") + print( repr(e) ) + + + def _set_ui_data(self, uri, path): + properties = Properties() + file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::*,owner::*,time::access,time::changed", + flags=Gio.FileQueryInfoFlags.NONE, + cancellable=None) + + is_symlink = file_info.get_attribute_as_string("standard::is-symlink") + properties.file_uri = uri + properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink else "" + properties.file_name = file_info.get_display_name() + properties.file_location = path + properties.mime_type = file_info.get_content_type() + properties.file_size = self._sizeof_fmt(file_info.get_size()) + properties.mtime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::changed")) ).strftime("%A, %B %d, %Y %I:%M:%S") + properties.atime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::access")) ).strftime("%A, %B %d, %Y %I:%M:%S") + properties.file_owner = file_info.get_attribute_as_string("owner::user") + properties.file_group = file_info.get_attribute_as_string("owner::group") + + # NOTE: Read = 4, Write = 2, Exec = 1 + command = ["stat", "-c", "%a", uri] + with subprocess.Popen(command, stdout=subprocess.PIPE) as proc: + properties.chmod_stat = list(proc.stdout.read().decode("UTF-8").strip()) + owner = self._chmod_map[f"{properties.chmod_stat[0]}"] + group = self._chmod_map[f"{properties.chmod_stat[1]}"] + others = self._chmod_map[f"{properties.chmod_stat[2]}"] + + self._reset_check_boxes() + self._set_check_boxes([["owner", owner], ["group", group], ["others", others]]) + + self._file_name.set_text(properties.file_name) + self._file_location.set_text(properties.file_location) + self._file_target.set_text(properties.file_target) + self._mime_type.set_label(properties.mime_type) + self._file_size.set_label(properties.file_size) + self._mtime.set_text(properties.mtime) + self._atime.set_text(properties.atime) + self._file_owner.set_text(properties.file_owner) + self._file_group.set_text(properties.file_group) + + return properties + + + + + def _get_check_boxes(self): + perms = [[], [], []] + + for i, target in enumerate(["owner", "group", "others"]): + for type in ["r", "w", "x"]: + is_active = self._builder.get_object(f"{target}_{type}").get_active() + if is_active: + perms[i].append(type) + + digits = [] + for perm in perms: + digits.append(self._chmod_map_counter[ ''.join(perm) ]) + + return ''.join(digits) + + def _set_check_boxes(self, targets): + for name, target in targets: + for type in list(target): + obj = f"{name}_{type}" + self._builder.get_object(obj).set_active(True) + + def _reset_check_boxes(self): + for target in ["owner", "group", "others"]: + for type in ["r", "w", "x"]: + self._builder.get_object(f"{target}_{type}").set_active(False) + + 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 wait_for_fm_message(self): + while not self._event_message: + pass + + @daemon_threaded + def _module_event_observer(self): + while True: + time.sleep(self._event_sleep_time) + event = self._event_system.read_module_event() + if event: + try: + if event[0] == self.name: + target_id, method_target, data = self._event_system.consume_module_event() + + if not method_target: + self._event_message = data + else: + method = getattr(self.__class__, f"{method_target}") + if data: + data = method(*(self, *data)) + else: + method(*(self,)) + except Exception as e: + print(repr(e)) diff --git a/plugins/template/plugin.py b/plugins/template/plugin.py index e514ec5..294aadc 100644 --- a/plugins/template/plugin.py +++ b/plugins/template/plugin.py @@ -25,12 +25,12 @@ def daemon_threaded(fn): class Manifest: - path: str = os.path.dirname(os.path.realpath(__file__)) - name: str = "Example Plugin" - author: str = "John Doe" - version: str = "0.0.1" - support: str = "" - permissions: {} = { + path: str = os.path.dirname(os.path.realpath(__file__)) + name: str = "Example Plugin" + author: str = "John Doe" + version: str = "0.0.1" + support: str = "" + requests: {} = { 'ui_target': "plugin_control_list", 'pass_fm_events': "true", 'bind_keys': [f"{name}||send_message:f"] @@ -39,7 +39,7 @@ class Manifest: class Plugin(Manifest): def __init__(self): - self._event_system = event_system + self._event_system = None self._event_sleep_time = .5 self._event_message = None @@ -50,7 +50,7 @@ class Plugin(Manifest): return button def set_fm_event_system(self, fm_event_system): - self.event_system = fm_event_system + self._event_system = fm_event_system def run(self): self._module_event_observer() @@ -62,6 +62,10 @@ class Plugin(Manifest): self._event_system.push_gui_event([self.name, "display_message", ("warning", message, None)]) + def wait_for_fm_message(self): + while not self._event_message: + pass + @daemon_threaded def _module_event_observer(self): while True: @@ -81,5 +85,4 @@ class Plugin(Manifest): else: method(*(self,)) except Exception as e: - print("ewww here") print(repr(e)) diff --git a/plugins/youtube_download/plugin.py b/plugins/youtube_download/plugin.py index e183c4f..a7a6c44 100644 --- a/plugins/youtube_download/plugin.py +++ b/plugins/youtube_download/plugin.py @@ -25,12 +25,12 @@ def daemon_threaded(fn): class Manifest: - path: str = os.path.dirname(os.path.realpath(__file__)) - name: str = "Youtube Download" - author: str = "ITDominator" - version: str = "0.0.1" - support: str = "" - permissions: {} = { + path: str = os.path.dirname(os.path.realpath(__file__)) + name: str = "Youtube Download" + author: str = "ITDominator" + version: str = "0.0.1" + support: str = "" + requests: {} = { 'ui_target': "plugin_control_list", 'pass_fm_events': "true" @@ -39,9 +39,9 @@ class Manifest: class Plugin(Manifest): def __init__(self): - self._fm_event_system = None - self._event_sleep_time = .5 - self._fm_event_message = None + self._event_system = None + self._event_sleep_time = .5 + self._event_message = None def get_ui_element(self): @@ -50,24 +50,38 @@ class Plugin(Manifest): return button def set_fm_event_system(self, fm_event_system): - self._fm_event_system = fm_event_system + self._event_system = fm_event_system def run(self): self._module_event_observer() + @threaded + def _do_download(self, widget=None, eve=None): + self._event_system.push_gui_event([self.name, "get_current_state", ()]) + self.wait_for_fm_message() + + state = self._event_message + subprocess.Popen([f'{self.path}/download.sh' , state.tab.get_current_directory()]) + self._event_message = None + + + def wait_for_fm_message(self): + while not self._event_message: + pass + @daemon_threaded def _module_event_observer(self): while True: time.sleep(self._event_sleep_time) - event = self._fm_event_system.read_module_event() + event = self._event_system.read_module_event() if event: try: if event[0] == self.name: - target_id, method_target, data = self._fm_event_system.consume_module_event() + target_id, method_target, data = self._event_system.consume_module_event() if not method_target: - self._fm_event_message = data + self._event_message = data else: method = getattr(self.__class__, f"{method_target}") if data: @@ -76,14 +90,3 @@ class Plugin(Manifest): method(*(self,)) except Exception as e: print(repr(e)) - - - @threaded - def _do_download(self, widget=None, eve=None): - self._fm_event_system.push_gui_event([self.name, "get_current_state", ()]) - while not self._fm_event_message: - pass - - state = self._fm_event_message - subprocess.Popen([f'{self.path}/download.sh' , state.tab.get_current_directory()]) - self._fm_event_message = None diff --git a/src/versions/solarfm-0.0.1/SolarFM/solarfm/core/controller_data.py b/src/versions/solarfm-0.0.1/SolarFM/solarfm/core/controller_data.py index b94766b..6a787f4 100644 --- a/src/versions/solarfm-0.0.1/SolarFM/solarfm/core/controller_data.py +++ b/src/versions/solarfm-0.0.1/SolarFM/solarfm/core/controller_data.py @@ -19,6 +19,9 @@ class State: tab: type = None icon_grid: gi.overrides.Gtk.IconView = None store: gi.overrides.Gtk.ListStore = None + selected_files: [] = None + to_copy_files: [] = None + to_cut_files: [] = None class Controller_Data: @@ -138,6 +141,18 @@ class Controller_Data: state.icon_grid = self.builder.get_object(f"{state.wid}|{state.tid}|icon_grid") state.store = state.icon_grid.get_model() + + selected_files = state.icon_grid.get_selected_items() + # if self.selected_files: + if selected_files: + state.selected_files = self.format_to_uris(state.store, state.wid, state.tid, selected_files, True) + + # if self.to_copy_files: + # state.to_copy_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_copy_files, True) + # + # if self.to_cut_files: + # state.to_cut_files = self.format_to_uris(state.store, state.wid, state.tid, self.to_cut_files, True) + return state diff --git a/src/versions/solarfm-0.0.1/SolarFM/solarfm/plugins/plugins.py b/src/versions/solarfm-0.0.1/SolarFM/solarfm/plugins/plugins.py index 0a81be1..2d547b4 100644 --- a/src/versions/solarfm-0.0.1/SolarFM/solarfm/plugins/plugins.py +++ b/src/versions/solarfm-0.0.1/SolarFM/solarfm/plugins/plugins.py @@ -16,7 +16,7 @@ class Plugin: author: str = None version: str = None support: str = None - permissions:{} = None + requests:{} = None reference: type = None @@ -53,72 +53,76 @@ class Plugins: print(f"Loading plugins...") parent_path = os.getcwd() - for file in os.listdir(self._plugins_path): + for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]: try: - path = join(self._plugins_path, file) - if isdir(path): - module = self.load_plugin_module(path, file) - plugin = self.collect_info(module, path) - loading_data = self.parse_permissions(plugin) + target = join(path, "plugin.py") + if not os.path.exists(target): + raise Exception("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") - self.execute_plugin(module, plugin, loading_data) + module = self.load_plugin_module(path, folder, target) + plugin = self.collect_info(module, path) + loading_data = self.parse_requests(plugin) + + self.execute_plugin(module, plugin, loading_data) except Exception as e: - print("Malformed plugin! Not loading!") + print(f"Malformed Plugin: Not loading -->: '{folder}' !") traceback.print_exc() + # if debug: + # traceback.print_exc() os.chdir(parent_path) - def load_plugin_module(self, path, file): + def load_plugin_module(self, path, folder, target): os.chdir(path) sys.path.insert(0, path) - spec = importlib.util.spec_from_file_location(file, join(path, "plugin.py")) + spec = importlib.util.spec_from_file_location(folder, target) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def collect_info(self, module, path) -> Plugin: - plugin = Plugin() - plugin.path = module.Manifest.path - plugin.name = module.Manifest.name - plugin.author = module.Manifest.author - plugin.version = module.Manifest.version - plugin.support = module.Manifest.support - plugin.permissions = module.Manifest.permissions + plugin = Plugin() + plugin.path = module.Manifest.path + plugin.name = module.Manifest.name + plugin.author = module.Manifest.author + plugin.version = module.Manifest.version + plugin.support = module.Manifest.support + plugin.requests = module.Manifest.requests return plugin - def parse_permissions(self, plugin): + def parse_requests(self, plugin): loading_data = {} - permissions = plugin.permissions - keys = permissions.keys() + requests = plugin.requests + keys = requests.keys() if "ui_target" in keys: - if permissions["ui_target"] in [ - "none", "other", "main_Window", "main_menu_bar", "path_menu_bar", "plugin_control_list", - "context_menu", "window_1", "window_2", "window_3", "window_4" - ]: - if permissions["ui_target"] == "other": + if requests["ui_target"] in [ + "none", "other", "main_Window", "main_menu_bar", "path_menu_bar", "plugin_control_list", + "context_menu", "window_1", "window_2", "window_3", "window_4" + ]: + if requests["ui_target"] == "other": if "ui_target_id" in keys: - loading_data["ui_target"] = self._builder.get_object(permissions["ui_target_id"]) + loading_data["ui_target"] = self._builder.get_object(requests["ui_target_id"]) if loading_data["ui_target"] == None: - raise Exception('Invalid "ui_target_id" given in permissions. Must have one if setting "ui_target" to "other"...') + raise Exception('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...') else: - raise Exception('Invalid "ui_target_id" given in permissions. Must have one if setting "ui_target" to "other"...') + raise Exception('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...') else: - loading_data["ui_target"] = self._builder.get_object(permissions["ui_target"]) + loading_data["ui_target"] = self._builder.get_object(requests["ui_target"]) else: - raise Exception('Unknown "ui_target" given in permissions.') + raise Exception('Unknown "ui_target" given in requests.') if "pass_fm_events" in keys: - if permissions["pass_fm_events"] in ["true"]: + if requests["pass_fm_events"] in ["true"]: loading_data["pass_fm_events"] = True if "bind_keys" in keys: - if isinstance(permissions["bind_keys"], list): - loading_data["bind_keys"] = permissions["bind_keys"] + if isinstance(requests["bind_keys"], list): + loading_data["bind_keys"] = requests["bind_keys"] return loading_data