From 0ef34ac42c5fe75c6466d38423bd6fb0ecc150e2 Mon Sep 17 00:00:00 2001 From: Rick Calixte <10281587+rcalixte@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:03:46 -0500 Subject: [PATCH] Add Actions module (#11878) --- ...hicolor_categories_scalable_cs-actions.svg | 359 ++++++++++++++++++ files/usr/bin/xlet-about-dialog | 64 ++-- .../cinnamon-settings-actions.desktop | 9 + .../cinnamon-settings/bin/ExtensionCore.py | 184 ++++----- .../cinnamon/cinnamon-settings/bin/Spices.py | 249 +++++++----- .../cinnamon-settings/modules/cs_actions.py | 64 ++++ python3/cinnamon/harvester.py | 158 ++++---- python3/cinnamon/logger.py | 7 +- python3/cinnamon/updates.py | 24 +- 9 files changed, 824 insertions(+), 294 deletions(-) create mode 100644 data/icons/hicolor_categories_scalable_cs-actions.svg create mode 100644 files/usr/share/applications/cinnamon-settings-actions.desktop create mode 100644 files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py diff --git a/data/icons/hicolor_categories_scalable_cs-actions.svg b/data/icons/hicolor_categories_scalable_cs-actions.svg new file mode 100644 index 0000000000..62711b3b30 --- /dev/null +++ b/data/icons/hicolor_categories_scalable_cs-actions.svg @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/usr/bin/xlet-about-dialog b/files/usr/bin/xlet-about-dialog index 23ecdfa441..79c6425184 100755 --- a/files/usr/bin/xlet-about-dialog +++ b/files/usr/bin/xlet-about-dialog @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import argparse import sys import os import json @@ -9,17 +10,14 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GdkPixbuf, GLib -usage = """ -usage : xlet-about-dialog applets/desklets/extensions uuid -""" - gettext.install('cinnamon', '/usr/share/locale') home = os.path.expanduser('~') + class AboutDialog(Gtk.AboutDialog): def __init__(self, metadata, stype): - super(AboutDialog, self).__init__() + super().__init__() self.metadata = metadata self.stype = stype @@ -33,25 +31,28 @@ class AboutDialog(Gtk.AboutDialog): else: self.props.version = _("Version %s") % version elif 'last-edited' in metadata: - timestamp = datetime.datetime.fromtimestamp(self.metadata['last-edited']).isoformat(' ') + timestamp = datetime.datetime.fromtimestamp(int(self.metadata['last-edited'])).isoformat(' ') self.props.version = _("Updated on %s") % timestamp - comments = self._(self.metadata['description']) + comments = self._(self.metadata.get('description', '')) + if 'comments' in self.metadata: comments += '\n\n' + self._(self.metadata['comments']) self.props.comments = comments s_id = self.get_spices_id() if s_id: - self.props.website = 'https://cinnamon-spices.linuxmint.com/%s/view/%s' % (self.stype, s_id) + self.props.website = f'https://cinnamon-spices.linuxmint.com/{self.stype}/view/{s_id}' self.props.website_label = _("More info") if 'contributors' in self.metadata: self.props.authors = self.metadata['contributors'].split(',') - self.props.program_name = '%s (%s)' % (self._(self.metadata['name']), self.metadata['uuid']) + xlet_name = self._(self.metadata.get('name')) + self.props.program_name = f"{xlet_name} ({self.metadata['uuid']})" self.connect('response', self.close) + self.set_title(_("About %s") % self.props.program_name) self.show() def close(self, *args): @@ -59,7 +60,7 @@ class AboutDialog(Gtk.AboutDialog): def set_icon(self): # Use the generic type icon - self.set_logo_icon_name('cs-%s' % self.metadata['type']) + self.set_logo_icon_name(f"cs-{self.metadata['type']}") # Override with metadata if set.. if 'icon' in self.metadata: self.set_logo_icon_name(self.metadata['icon']) @@ -68,7 +69,7 @@ class AboutDialog(Gtk.AboutDialog): if os.path.exists(icon_path): icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 48, 48) self.set_logo(icon) - self.set_default_icon_name('cs-%s' % self.metadata['type']) + self.set_default_icon_name(f"cs-{self.metadata['type']}") def _(self, msg): gettext.bindtextdomain(self.metadata['uuid'], home + '/.local/share/locale') @@ -81,9 +82,9 @@ class AboutDialog(Gtk.AboutDialog): def get_spices_id(self): try: - cache_path = os.path.join(GLib.get_user_cache_dir(), 'cinnamon', 'spices', self.stype[0:-1], 'index.json'), + cache_path = os.path.join(GLib.get_user_cache_dir(), 'cinnamon', 'spices', self.stype[0:-1], 'index.json') if os.path.exists(cache_path): - with open(cache_path, 'r') as cache_file: + with open(cache_path, 'r', encoding='utf-8') as cache_file: index_cache = json.load(cache_file) if self.metadata['uuid'] in index_cache: return index_cache[self.metadata['uuid']]['spices-id'] @@ -93,19 +94,22 @@ class AboutDialog(Gtk.AboutDialog): print(e) return None + if __name__ == "__main__": - if len(sys.argv) < 3: - print(usage) - quit() + parser = argparse.ArgumentParser() + parser.description = 'Arguments for xlet-about-dialog' + parser.add_argument('xlet_type', type=str, help=_('the type of the Spice'), + choices=['applets', 'desklets', 'extensions', + 'actions', 'themes']) + parser.add_argument('uuid', type=str, metavar='UUID', nargs=1, + help=_('the UUID of the Spice')) + _args = parser.parse_args() - xlet_type = sys.argv[1] - uuid = sys.argv[2] + xlet_type = _args.xlet_type + uuid = _args.uuid[0] - suffix = 'share/cinnamon/%s/%s' % (xlet_type, uuid) - prefixes = [ - os.path.join(home, '.local'), - '/usr' - ] + suffix = f'share/nemo/actions/{uuid}' if xlet_type == 'actions' else f'share/cinnamon/{xlet_type}/{uuid}' + prefixes = [os.path.join(home, '.local'), '/usr'] path = None for prefix in prefixes: @@ -114,15 +118,15 @@ if __name__ == "__main__": break if path is None: - print('Unable to locate %s %s' % (xlet_type, uuid)) - quit() + print(_('Unable to locate %s %s') % (xlet_type, uuid)) + sys.exit(1) - with open(os.path.join(path, 'metadata.json')) as meta_file: - metadata = json.load(meta_file) + with open(os.path.join(path, 'metadata.json'), encoding='utf-8') as meta: + _metadata = json.load(meta) - metadata['path'] = path - metadata['type'] = xlet_type + _metadata['path'] = path + _metadata['type'] = xlet_type - AboutDialog(metadata, xlet_type) + AboutDialog(_metadata, xlet_type) Gtk.main() diff --git a/files/usr/share/applications/cinnamon-settings-actions.desktop b/files/usr/share/applications/cinnamon-settings-actions.desktop new file mode 100644 index 0000000000..9d631c5794 --- /dev/null +++ b/files/usr/share/applications/cinnamon-settings-actions.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Icon=cs-actions +Exec=cinnamon-settings actions +Type=Application +OnlyShowIn=X-Cinnamon; +Categories=Settings; +Name=Actions +Comment=Manage your Actions +Keywords=action; diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py index d3aff5ef55..63b626b2c5 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py @@ -7,7 +7,7 @@ import subprocess import gettext from html.parser import HTMLParser -import html.entities as entities +from html import entities import locale import gi @@ -57,18 +57,23 @@ def find_extension_subdir(directory): subdir_a = [int(part) for part in subdir.split(".")] - if subdir_a <= curr_a and largest < subdir_a: + if largest < subdir_a <= curr_a: largest = subdir_a if largest == [0]: return directory - else: - return os.path.join(directory, ".".join(map(str, largest))) + return os.path.join(directory, ".".join(map(str, largest))) + translations = {} + def translate(uuid, string): - #check for a translation for this xlet + # do not translate whitespaces + if not string.strip(): + return string + + # check for a translation for this xlet if uuid not in translations: try: translations[uuid] = gettext.translation(uuid, home + '/.local/share/locale').gettext @@ -78,20 +83,18 @@ def translate(uuid, string): except IOError: translations[uuid] = None - #do not translate whitespaces - if not string.strip(): - return string - if translations[uuid]: result = translations[uuid](string) if result != string: return result return _(string) + def list_header_func(row, before, user_data): if before and not row.get_header(): row.set_header(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) + def filter_row(row, entry): search_string = entry.get_text().lower() for row_part in [row.name, row.description, row.uuid, row.author]: @@ -99,11 +102,12 @@ def filter_row(row, entry): return True return False + def show_prompt(msg, window=None): - dialog = Gtk.MessageDialog(transient_for = window, - destroy_with_parent = True, - message_type = Gtk.MessageType.QUESTION, - buttons = Gtk.ButtonsType.YES_NO) + dialog = Gtk.MessageDialog(transient_for=window, + destroy_with_parent=True, + message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.YES_NO) esc = html.escape(msg) dialog.set_markup(esc) dialog.show_all() @@ -111,17 +115,19 @@ def show_prompt(msg, window=None): dialog.destroy() return response == Gtk.ResponseType.YES + def show_message(msg, window=None): - dialog = Gtk.MessageDialog(transient_for = window, - destroy_with_parent = True, - message_type = Gtk.MessageType.ERROR, - buttons = Gtk.ButtonsType.OK) + dialog = Gtk.MessageDialog(transient_for=window, + destroy_with_parent=True, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK) esc = html.escape(msg) dialog.set_markup(esc) dialog.show_all() dialog.run() dialog.destroy() + background_work_queue = ThreadedTaskManager(5) @@ -144,6 +150,7 @@ def handle_entityref(self, name): def get_text(self): return ''.join(self.strings) + def sanitize_html(string): parser = MyHTMLParser() parser.feed(string) @@ -153,25 +160,25 @@ def sanitize_html(string): class ManageSpicesRow(Gtk.ListBoxRow): def __init__(self, extension_type, metadata, size_groups): - super(ManageSpicesRow, self).__init__() + super().__init__() self.extension_type = extension_type self.metadata = metadata self.status_ids = {} - self.writable = metadata['writable'] + self.writable = bool(metadata.get('writable')) self.uuid = self.metadata['uuid'] + self.name = translate(self.metadata['uuid'], self.metadata['name']) self.description = translate(self.metadata['uuid'], self.metadata['description']) + icon_path = os.path.join(self.metadata['path'], 'icon.png') self.author = "" if 'author' in metadata: if metadata['author'].lower() != "none" and metadata['author'].lower() != "unknown": self.author = metadata['author'] - icon_path = os.path.join(self.metadata['path'], 'icon.png') - try: self.max_instances = int(self.metadata['max-instances']) if self.max_instances < -1: @@ -184,19 +191,16 @@ def __init__(self, extension_type, metadata, size_groups): except (KeyError, ValueError): self.role = None - try: - last_edited = self.metadata['last-edited'] - except (KeyError, ValueError): - last_edited = -1 - - # Check for the right version subdir (if the spice is multi-versioned, it won't necessarily be in its root directory) + # Check for the right version subdir (if the spice is multi-versioned, + # it won't necessarily be in its root directory) self.metadata['path'] = find_extension_subdir(self.metadata['path']) # "hide-configuration": true in metadata trumps all - # otherwise we check for "external-configuration-app" in metadata and settings-schema.json in settings + # otherwise we check for "external-configuration-app" in metadata + # and settings-schema.json in settings self.has_config = False self.ext_config_app = None - if 'hide-configuration' not in self.metadata or self.metadata['hide-configuration'] != True: + if not self.metadata.get('hide-configuration'): if 'external-configuration-app' in self.metadata: self.ext_config_app = os.path.join(self.metadata['path'], self.metadata['external-configuration-app']) @@ -206,7 +210,7 @@ def __init__(self, extension_type, metadata, size_groups): else: self.ext_config_app = None - if self.ext_config_app is None and os.path.exists('%s/settings-schema.json' % self.metadata['path']): + if self.ext_config_app is None and os.path.exists(f"{self.metadata['path']}/settings-schema.json"): self.has_config = True widget = SettingsWidget() @@ -245,7 +249,7 @@ def __init__(self, extension_type, metadata, size_groups): icon = None if icon is None: - icon = Gtk.Image.new_from_icon_name('cs-%ss' % extension_type, 3) + icon = Gtk.Image.new_from_icon_name(f'cs-{extension_type}s', 3) grid.attach_next_to(icon, enabled_box, Gtk.PositionType.RIGHT, 1, 1) @@ -256,23 +260,23 @@ def __init__(self, extension_type, metadata, size_groups): name_label = Gtk.Label() name_markup = GLib.markup_escape_text(self.name) - if self.author == "": - name_label.set_markup('{}'.format(name_markup)) + if not self.author: + name_label.set_markup(f'{name_markup}') else: by_author = _("by %s") % self.author - name_label.set_markup('{} {}'.format(name_markup, by_author)) + name_label.set_markup(f'{name_markup} {by_author}') name_label.props.xalign = 0.0 desc_box.add(name_label) uuid_label = Gtk.Label() uuid_markup = GLib.markup_escape_text(self.uuid) - uuid_label.set_markup('{}'.format(uuid_markup)) + uuid_label.set_markup(f'{uuid_markup}') uuid_label.props.xalign = 0.0 desc_box.add(uuid_label) description_label = SettingsLabel() description_markup = GLib.markup_escape_text(sanitize_html(self.description)) - description_label.set_markup('{}'.format(description_markup)) + description_label.set_markup(f'{description_markup}') description_label.set_margin_top(2) desc_box.add(description_label) @@ -304,12 +308,7 @@ def __init__(self, extension_type, metadata, size_groups): elif self.extension_type == "extension": self.add_status('locked', 'changes-prevent-symbolic', _("This is a system extension. It cannot be removed.")) - try: - schema_filename = self.metadata['schema-file'] - except (KeyError, ValueError): - schema_filename = '' - - if self.writable: + if self.writable and self.extension_type != 'action': self.scan_extension_for_danger(self.metadata['path']) self.version_supported = self.is_compatible_with_cinnamon_version() @@ -317,7 +316,8 @@ def __init__(self, extension_type, metadata, size_groups): def is_compatible_with_cinnamon_version(self): try: # Treat "cinnamon-version" as a list of minimum required versions - # if any version in there is lower than our Cinnamon version, then the spice is compatible. + # if any version in there is lower than our Cinnamon version, + # then the spice is compatible. curr_ver = get_cinnamon_version() for version in self.metadata['cinnamon-version']: @@ -327,10 +327,9 @@ def is_compatible_with_cinnamon_version(self): path = os.path.join(self.metadata['path'], self.extension_type + ".js") if os.path.exists(path): return True - else: - print ("The %s %s is not properly structured. Path not found: '%s'" % (self.uuid, self.extension_type, path)) - return False - print ("The %s %s is not compatible with this version of Cinnamon." % (self.uuid, self.extension_type)) + print(f"The {self.uuid} {self.extension_type} is not properly structured. Path not found: '{path}'") + return False + print(f"The {self.uuid} {self.extension_type} is not compatible with this version of Cinnamon.") return False except: # If cinnamon-version is not specified or if the version check goes wrong, assume compatibility @@ -384,12 +383,11 @@ def scan_extension_thread(self, directory): def scan_item(item): if item.endswith('.js'): - f = open(item) - contents = f.read() - for unsafe_item in UNSAFE_ITEMS: - if unsafe_item in contents: - raise Exception('unsafe') - f.close() + with open(item, encoding="utf-8") as scan_file: + contents = scan_file.read() + for unsafe_item in UNSAFE_ITEMS: + if unsafe_item in contents: + raise Exception('unsafe') def scan_dir(subdir): for item in os.listdir(subdir): @@ -415,6 +413,7 @@ def on_scan_complete(self, is_dangerous): elif self.extension_type == "extension": self.add_status('dangerous', 'dialog-warning-symbolic', _("This extension contains function calls that could potentially cause Cinnamon to crash or freeze. If you are experiencing crashes or freezing, please try removing it.")) + class ManageSpicesPage(SettingsPage): def __init__(self, parent, collection_type, spices, window): super().__init__() @@ -465,14 +464,12 @@ def sort_rows(row1, row2): name2 = row2.name.lower() if name1 < name2: return -1 - elif name2 < name1: + if name2 < name1: return 1 - else: - return 0 - elif row1.writable: + return 0 + if row1.writable: return -1 - else: - return 1 + return 1 self.list_box = Gtk.ListBox() self.list_box.set_selection_mode(Gtk.SelectionMode.SINGLE) @@ -576,7 +573,7 @@ def add_instance(self, *args): extension_row = self.list_box.get_selected_row() self.enable_extension(extension_row.uuid, extension_row.name, extension_row.version_supported) - def enable_extension(self, uuid, name, version_check = True): + def enable_extension(self, uuid, name, version_check=True): if not version_check: show_message(_("Extension %s is not compatible with your version of Cinnamon.") % uuid, self.window) return @@ -600,22 +597,32 @@ def uninstall_extension(self, *args): extension_row = self.list_box.get_selected_row() if not show_prompt(_("Are you sure you want to completely remove %s?") % extension_row.uuid, self.window): return - self.spices.disable_extension(extension_row.uuid) + self.spices.disable_extension(extension_row.uuid) self.spices.uninstall(extension_row.uuid) def restore_to_default(self, *args): - if self.collection_type == 'applet': - msg = _("This will restore the default set of enabled applets. Are you sure you want to do this?") - elif self.collection_type == 'desklet': - msg = _("This will restore the default set of enabled desklets. Are you sure you want to do this?") - elif self.collection_type == 'extension': - msg = _("This will disable all active extensions. Are you sure you want to do this?") + collection_msgs = {'applet': _("This will restore the default set of enabled applets. Are you sure you want to do this?"), + 'desklet': _("This will restore the default set of enabled desklets. Are you sure you want to do this?"), + 'extension': _("This will disable all active extensions. Are you sure you want to do this?"), + 'action': _("This will remove all Actions. Are you sure you want to do this?")} + msg = collection_msgs.get(self.collection_type) + if show_prompt(msg, self.window): - sett = Gio.Settings.new('org.cinnamon') + gio_sett = 'org.nemo.plugins' if self.collection_type == 'action' else 'org.cinnamon' + sett = Gio.Settings.new(f'{gio_sett}') + if self.collection_type == 'action': + for uuid in self.spices.get_installed(): + disableds = sett.get_strv('disabled-actions') + uuid_name = f'{uuid}.nemo_action' + if uuid_name in disableds: + disableds.remove(f'{uuid_name}') + sett.set_strv('disabled-actions', disableds) + self.spices.uninstall(uuid) + return if self.collection_type != 'extension': - sett.reset('next-%s-id' % self.collection_type) - sett.reset('enabled-%ss' % self.collection_type) + sett.reset(f'next-{self.collection_type}-id') + sett.reset(f'enabled-{self.collection_type}s') def about(self, *args): row = self.list_box.get_selected_row() @@ -636,7 +643,7 @@ def load_extensions(self, *args): self.extension_rows.append(extension_row) extension_row.set_enabled(self.spices.get_enabled(uuid)) except Exception as msg: - print("Failed to load extension %s: %s" % (uuid, msg)) + print(f"Failed to load extension {uuid}: {msg}") self.list_box.show_all() @@ -644,7 +651,7 @@ def update_status(self, *args): for row in self.extension_rows: enabled = self.spices.get_enabled(row.uuid) row.set_enabled(enabled) - if enabled and not self.spices.get_is_running(row.uuid): + if enabled and not self.spices.get_is_running(row.uuid) and self.collection_type != 'action': row.add_status('error', 'dialog-error-symbolic', _("Something went wrong while loading %s. Please make sure you are using the latest version, and then report the issue to its developer.") % row.uuid) else: row.remove_status('error') @@ -673,10 +680,10 @@ def __init__(self, uuid, data, spices, size_groups): self.author = data['author_user'] if 'translations' in data.keys(): - key = 'name_%s' % LANGUAGE_CODE + key = f'name_{LANGUAGE_CODE}' if key in data['translations'].keys(): self.name = data['translations'][key] - key = 'description_%s' % LANGUAGE_CODE + key = f'description_{LANGUAGE_CODE}' if key in data['translations'].keys(): self.description = data['translations'][key] @@ -714,23 +721,23 @@ def __init__(self, uuid, data, spices, size_groups): name_label = Gtk.Label() name_markup = GLib.markup_escape_text(self.name) if self.author == "": - name_label.set_markup('{}'.format(name_markup)) + name_label.set_markup(f'{name_markup}') else: by_author = _("by %s") % self.author - name_label.set_markup('{} {}'.format(name_markup, by_author)) + name_label.set_markup(f'{name_markup} {by_author}') name_label.set_hexpand(True) name_label.set_halign(Gtk.Align.START) desc_box.pack_start(name_label, False, False, 0) uuid_label = Gtk.Label() uuid_markup = GLib.markup_escape_text(self.uuid) - uuid_label.set_markup('{}'.format(uuid_markup)) + uuid_label.set_markup(f'{uuid_markup}') uuid_label.props.xalign = 0.0 desc_box.add(uuid_label) description_label = SettingsLabel() description_markup = GLib.markup_escape_text(sanitize_html(self.description)) - description_label.set_markup('{}'.format(description_markup)) + description_label.set_markup(f'{description_markup}') description_label.set_margin_top(2) desc_box.pack_start(description_label, False, False, 0) @@ -812,7 +819,7 @@ def __init__(self, parent, collection_type, spices, window): self.top_box.pack_start(sort_label, False, False, 4) self.sort_combo = Gtk.ComboBox() - sort_types=Gtk.ListStore(str, str) + sort_types = Gtk.ListStore(str, str) self.sort_combo.set_model(sort_types) renderer_text = Gtk.CellRendererText() self.sort_combo.pack_start(renderer_text, True) @@ -823,7 +830,7 @@ def __init__(self, parent, collection_type, spices, window): sort_types.append(['date', _("Date")]) sort_types.append(['installed', _("Installed")]) sort_types.append(['update', _("Upgradable")]) - self.sort_combo.set_active(1) #Rating + self.sort_combo.set_active(1) # Rating self.sort_combo.connect('changed', self.sort_changed) self.top_box.pack_start(self.sort_combo, False, False, 4) @@ -932,10 +939,9 @@ def sort_changed(self, *args): def sort_name(row1, row2): if row2.name.lower() == row1.name.lower(): return 0 - elif row2.name.lower() < row1.name.lower(): + if row2.name.lower() < row1.name.lower(): return 1 - else: - return -1 + return -1 def sort_score(row1, row2): return row2.score - row1.score @@ -946,20 +952,18 @@ def sort_date(row1, row2): def sort_installed(row1, row2): if row1.installed == row2.installed: return 0 - elif row1.installed: + if row1.installed: return -1 - else: - return 1 + return 1 def sort_update(row1, row2): if row1.has_update == row2.has_update: if not row1.has_update: return row2.timestamp - row1.timestamp return 0 - elif row1.has_update: + if row1.has_update: return -1 - else: - return 1 + return 1 sort_type = self.sort_combo.get_active_id() if sort_type == 'name': @@ -1034,7 +1038,7 @@ def on_page_shown(self, *args): if not self.extension_rows: self.build_list() - if (not self.initial_refresh_done) and (not self.spices.processing_jobs): + if not self.initial_refresh_done and not self.spices.processing_jobs: self.initial_refresh_done = True self.spices.refresh_cache() diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py index 04b20e09eb..510844db97 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py @@ -1,10 +1,11 @@ #!/usr/bin/python3 +import os +import sys + try: from gi.repository import Gio, Gtk, GObject, Gdk, GdkPixbuf, GLib import tempfile - import os - import sys import zipfile import shutil import html @@ -13,8 +14,8 @@ from PIL import Image import datetime import time -except Exception as detail: - print(detail) +except Exception as error_message: + print(error_message) sys.exit(1) try: @@ -23,24 +24,27 @@ import simplejson as json home = os.path.expanduser("~") -locale_inst = '%s/.local/share/locale' % home +locale_inst = f'{home}/.local/share/locale' settings_dir = os.path.join(GLib.get_user_config_dir(), 'cinnamon', 'spices') -old_settings_dir = '%s/.cinnamon/configs/' % home +old_settings_dir = f'{home}/.cinnamon/configs/' URL_SPICES_HOME = "https://cinnamon-spices.linuxmint.com" URL_MAP = { 'applet': URL_SPICES_HOME + "/json/applets.json", 'theme': URL_SPICES_HOME + "/json/themes.json", 'desklet': URL_SPICES_HOME + "/json/desklets.json", - 'extension': URL_SPICES_HOME + "/json/extensions.json" + 'extension': URL_SPICES_HOME + "/json/extensions.json", + 'action': URL_SPICES_HOME + "/json/actions.json", } ABORT_NONE = 0 ABORT_ERROR = 1 ABORT_USER = 2 + def ui_thread_do(callback, *args): - GLib.idle_add (callback, *args, priority=GLib.PRIORITY_DEFAULT) + GLib.idle_add(callback, *args, priority=GLib.PRIORITY_DEFAULT) + def removeEmptyFolders(path): if not os.path.isdir(path): @@ -60,9 +64,10 @@ def removeEmptyFolders(path): print("Removing empty folder:", path) os.rmdir(path) + class ThreadedTaskManager(GObject.GObject): def __init__(self, max_threads): - super(ThreadedTaskManager, self).__init__() + super().__init__() self.max_threads = max_threads self.abort_status = False self.jobs = [] @@ -121,6 +126,7 @@ def abort(self): self.abort_status = True del self.jobs[:] + class Spice_Harvester(GObject.Object): __gsignals__ = { 'installed-changed': (GObject.SignalFlags.RUN_FIRST, None, ()), @@ -129,11 +135,12 @@ class Spice_Harvester(GObject.Object): } def __init__(self, collection_type, window=None): - super(Spice_Harvester, self).__init__() + super().__init__() self.collection_type = collection_type self.window = window self.themes = collection_type == 'theme' + self.actions = collection_type == 'action' self.index_cache = {} self.meta_map = {} self.download_manager = ThreadedTaskManager(10) @@ -155,18 +162,26 @@ def __init__(self, collection_type, window=None): if self.themes: self.settings = Gio.Settings.new('org.cinnamon.theme') self.enabled_key = 'name' + elif self.actions: + self.settings = Gio.Settings.new('org.nemo.plugins') + self.enabled_key = 'disabled-actions' else: self.settings = Gio.Settings.new('org.cinnamon') - self.enabled_key = 'enabled-%ss' % self.collection_type + self.enabled_key = f'enabled-{self.collection_type}s' + + if not self.themes: + self.settings.connect(f'changed::{self.enabled_key}', self._update_status) if self.themes: - self.install_folder = '%s/.themes/' % home + self.install_folder = f'{home}/.themes/' old_install_folder = os.path.join(GLib.get_user_data_dir(), 'themes') self.spices_directories = (self.install_folder, old_install_folder) + elif self.actions: + self.install_folder = f'{home}/.local/share/nemo/actions/' + self.spices_directories = (self.install_folder, ) else: - self.install_folder = '%s/.local/share/cinnamon/%ss/' % (home, self.collection_type) - self.spices_directories = ('/usr/share/cinnamon/%ss/' % self.collection_type, self.install_folder) - self.settings.connect('changed::%s' % self.enabled_key, self._update_status) + self.install_folder = f'{home}/.local/share/cinnamon/{self.collection_type}s/' + self.spices_directories = (f'/usr/share/cinnamon/{self.collection_type}s/', self.install_folder) self._update_status() @@ -187,12 +202,18 @@ def __init__(self, collection_type, window=None): print(e) try: + if not self.actions: + dbus_path = 'org.Cinnamon' + gsetting = '/org/Cinnamon' + else: + dbus_path = 'org.Nemo' + gsetting = '/org/Nemo' Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, - 'org.Cinnamon', '/org/Cinnamon', 'org.Cinnamon', None, self._on_proxy_ready, None) + f'{dbus_path}', f'{gsetting}', f'{dbus_path}', None, self._on_proxy_ready, None) except GLib.Error as e: - print(e.message) + print(e) - def _on_proxy_ready (self, object, result, data=None): + def _on_proxy_ready(self, obj, result, data=None): try: self._proxy = Gio.DBusProxy.new_for_bus_finish(result) self._proxy.connect('g-signal', self._on_signal) @@ -202,7 +223,7 @@ def _on_proxy_ready (self, object, result, data=None): else: print("org.Cinnamon proxy created, but no owner - is Cinnamon running?") except GLib.Error as e: - print("Could not establish proxy for org.Cinnamon: %s" % e.message) + print(f"Could not establish proxy for org.Cinnamon: {e}") self.connect_proxy('XletAddedComplete', self._update_status) self._update_status() @@ -247,7 +268,7 @@ def send_proxy_signal(self, command, *args): def _update_status(self, *args): try: - if self._proxy and self._proxy.get_name_owner(): + if self._proxy and self._proxy.get_name_owner() and not self.actions: self.running_uuids = self._proxy.GetRunningXletUUIDs('(s)', self.collection_type) else: self.running_uuids = [] @@ -258,7 +279,9 @@ def _update_status(self, *args): def open_spice_page(self, uuid): """ opens to the web page of the given uuid""" id = self.index_cache[uuid]['spices-id'] - os.system('xdg-open "%s/%ss/view/%s"' % (URL_SPICES_HOME, self.collection_type, id)) + subprocess.run(['/usr/bin/xdg-open', + f"{URL_SPICES_HOME}/{self.collection_type}s/view/{id}"], + check=True) def get_progressbar(self): """ returns a Gtk.Widget that can be added to the application. This widget will show the @@ -299,7 +322,7 @@ def _update_progress(self, count, blockSize, totalSize): total = self.download_total_files current = total - self.download_manager.get_n_jobs() fraction = float(current) / float(total) - text = "%s %i/%i" % (_("Downloading images:"), current, total) + text = _("Downloading images:") + f" {current}/{total}" self._set_progressbar_text(text) else: fraction = count * blockSize / float((totalSize / blockSize + 1) * blockSize) @@ -309,7 +332,8 @@ def _update_progress(self, count, blockSize, totalSize): while Gtk.events_pending(): Gtk.main_iteration() - # Jobs are added by calling _push_job. _process_job and _advance_queue form a wrapper that runs the job in it's own thread. + # Jobs are added by calling _push_job. _process_job and _advance_queue + # from a wrapper that runs the job in it's own thread. def _push_job(self, job): self.total_jobs += 1 job['job_number'] = self.total_jobs @@ -319,7 +343,7 @@ def _push_job(self, job): def _process_job(self, job): job['result'] = job['func'](job) - if 'callback' in job: + if job.get('callback'): GLib.idle_add(job['callback'], job) GLib.idle_add(self._advance_queue) @@ -340,7 +364,7 @@ def _advance_queue(self): self.current_job = job text = job['progress_text'] if self.total_jobs > 1: - text += " (%i/%i)" % (job['job_number'], self.total_jobs) + text += f" ({job['job_number']}/{self.total_jobs})" self._set_progressbar_text(text) job_thread = threading.Thread(target=self._process_job, args=(job,)) job_thread.start() @@ -360,8 +384,8 @@ def _advance_queue(self): def _download(self, out_file, url, binary=True): timestamp = round(time.time()) - url = "%s?time=%d" % (url, timestamp) - print("Downloading from %s" % url) + url = f"{url}?time={timestamp}" + print(f"Downloading from {url}") try: open_args = 'wb' if binary else 'w' with open(out_file, open_args) as outfd: @@ -379,9 +403,9 @@ def _download(self, out_file, url, binary=True): return out_file def _url_retrieve(self, url, outfd, reporthook, binary): - #Like the one in urllib. Unlike urllib.retrieve url_retrieve - #can be interrupted. KeyboardInterrupt exception is raised when - #interrupted. + # Like the one in urllib. Unlike urllib.retrieve url_retrieve + # can be interrupted. KeyboardInterrupt exception is raised when + # interrupted. import proxygsettings import requests @@ -409,24 +433,25 @@ def _url_retrieve(self, url, outfd, reporthook, binary): def _load_metadata(self): self.meta_map = {} - for directory in self.spices_directories: - if os.path.exists(directory): - extensions = os.listdir(directory) - + for file_path in self.spices_directories: + if os.path.exists(file_path): + extensions = os.listdir(file_path) for uuid in extensions: - subdirectory = os.path.join(directory, uuid) + subdirectory = os.path.join(file_path, uuid) + if uuid.endswith('.nemo_action'): + continue try: - json_data = open(os.path.join(subdirectory, 'metadata.json')).read() - metadata = json.loads(json_data) - metadata['path'] = subdirectory - metadata['writable'] = os.access(subdirectory, os.W_OK) - self.meta_map[uuid] = metadata - except Exception as detail: + with open(f"{subdirectory}/metadata.json", encoding='utf-8') as json_data: + metadata = json.load(json_data) + metadata['path'] = subdirectory + metadata['writable'] = os.access(subdirectory, os.W_OK) + self.meta_map[uuid] = metadata + except Exception as error: if not self.themes: - print(detail) - print("Skipping %s: there was a problem trying to read metadata.json" % uuid) + print(error) + print(_("Skipping %s: there was a problem trying to read metadata.json") % uuid) else: - print("%s does not exist! Skipping" % directory) + print(f"{file_path} does not exist! Skipping") def _directory_changed(self, *args): self._load_metadata() @@ -448,18 +473,23 @@ def get_has_update(self, uuid): try: return int(self.meta_map[uuid]["last-edited"]) < self.index_cache[uuid]["last_edited"] - except Exception as e: + except Exception: return False def get_enabled(self, uuid): """ returns the number of instances currently enabled""" enabled_count = 0 - if not self.themes: + if not self.themes and not self.actions: enabled_list = self.settings.get_strv(self.enabled_key) for item in enabled_list: item = item.replace("!", "") if uuid in item.split(":"): enabled_count += 1 + elif self.actions: + disabled_list = self.settings.get_strv(self.enabled_key) + uuid_name = f"{uuid}.nemo_action" + if uuid_name not in disabled_list: + enabled_count = 1 elif self.settings.get_string(self.enabled_key) == uuid: enabled_count = 1 @@ -490,18 +520,17 @@ def _load_cache(self): if not os.path.exists(filename): self.has_cache = False return - else: - self.has_cache = True + self.has_cache = True - f = open(filename, 'r') - try: - self.index_cache = json.load(f) - except ValueError as detail: + with open(filename, 'r', encoding='utf-8') as f: try: - os.remove(filename) - except: - pass - self.errorMessage(_("Something went wrong with the spices download. Please try refreshing the list again."), str(detail)) + self.index_cache = json.load(f) + except ValueError as detail: + try: + os.remove(filename) + except: + pass + self.errorMessage(_("Something went wrong with the spices download. Please try refreshing the list again."), str(detail)) self._generate_update_list() @@ -537,7 +566,7 @@ def _download_image_cache(self): self.download_total_files = 0 self.download_current_file = 0 - for uuid, info in self.index_cache.items(): + for uuid, _ in self.index_cache.items(): if self.themes: icon_basename = self._sanitize_thumb(os.path.basename(self.index_cache[uuid]['screenshot'])) download_url = URL_SPICES_HOME + "/uploads/themes/thumbs/" + icon_basename @@ -594,7 +623,8 @@ def _sanitize_thumb(basename): def install(self, uuid): """ downloads and installs the given extension""" - job = {'uuid': uuid, 'func': self._install, 'callback': self._install_finished} + _callback = None if self.actions else self._install_finished + job = {'uuid': uuid, 'func': self._install, 'callback': _callback} job['progress_text'] = _("Installing %s") % uuid self._push_job(job) @@ -604,20 +634,19 @@ def _install(self, job): download_url = URL_SPICES_HOME + self.index_cache[uuid]['file'] self.current_uuid = uuid - fd, ziptempfile = tempfile.mkstemp() + _, ziptempfile = tempfile.mkstemp() if self._download(ziptempfile, download_url) is None: return try: - zip = zipfile.ZipFile(ziptempfile) - - tempfolder = tempfile.mkdtemp() - zip.extractall(tempfolder) + with zipfile.ZipFile(ziptempfile) as _zip: + tempfolder = tempfile.mkdtemp() + _zip.extractall(tempfolder) - uuidfolder = os.path.join(tempfolder, uuid) + uuidfolder = tempfolder if self.actions else os.path.join(tempfolder, uuid) - self.install_from_folder(uuidfolder, uuid, True) + self.install_from_folder(uuidfolder, uuid, True) except Exception as detail: if not self.abort_download: self.errorMessage(_("An error occurred during the installation of %s. Please report this incident to its developer.") % uuid, str(detail)) @@ -631,31 +660,47 @@ def _install(self, job): def install_from_folder(self, folder, uuid, from_spices=False): """ installs a spice from a specified folder""" - contents = os.listdir(folder) + _folder = f"{folder}/{uuid}" if self.actions else folder + contents = os.listdir(_folder) if not self.themes: # Install spice localization files, if any if 'po' in contents: - po_dir = os.path.join(folder, 'po') + po_dir = os.path.join(_folder, 'po') for file in os.listdir(po_dir): if file.endswith('.po'): lang = file.split(".")[0] locale_dir = os.path.join(locale_inst, lang, 'LC_MESSAGES') os.makedirs(locale_dir, mode=0o755, exist_ok=True) - subprocess.call(['msgfmt', '-c', os.path.join(po_dir, file), '-o', os.path.join(locale_dir, '%s.mo' % uuid)]) - + subprocess.run(['/usr/bin/msgfmt', '-c', + os.path.join(po_dir, file), '-o', + os.path.join(locale_dir, f'{uuid}.mo')], + check=True) + # Create install folder on demand if not os.path.exists(self.install_folder): - subprocess.call(["mkdir", "-p", self.install_folder]) + subprocess.run(["/usr/bin/mkdir", "-p", self.install_folder], check=True) dest = os.path.join(self.install_folder, uuid) if os.path.exists(dest): shutil.rmtree(dest) - shutil.copytree(folder, dest) + if self.actions and os.path.exists(dest + '.nemo_action'): + os.remove(dest + '.nemo_action') + if not self.actions: + shutil.copytree(folder, dest) + else: + shutil.copytree(folder, self.install_folder, dirs_exist_ok=True) + + if self.actions and uuid not in self.updates_available: + disabled_list = self.settings.get_strv(self.enabled_key) + uuid_name = f"{uuid}.nemo_action" + if uuid_name not in disabled_list: + disabled_list.append(uuid_name) + self.settings.set_strv(self.enabled_key, disabled_list) if not self.themes: # ensure proper file permissions - for root, dirs, files in os.walk(dest): + for root, _, files in os.walk(dest): for file in files: os.chmod(os.path.join(root, file), 0o755) @@ -663,10 +708,8 @@ def install_from_folder(self, folder, uuid, from_spices=False): if self.themes and not os.path.exists(meta_path): md = {} else: - file = open(meta_path, 'r') - raw_meta = file.read() - file.close() - md = json.loads(raw_meta) + with open(meta_path, 'r', encoding='utf-8') as file: + md = json.load(file) if from_spices and uuid in self.index_cache: md['last-edited'] = self.index_cache[uuid]['last_edited'] @@ -674,9 +717,8 @@ def install_from_folder(self, folder, uuid, from_spices=False): md['last-edited'] = int(datetime.datetime.utcnow().timestamp()) raw_meta = json.dumps(md, indent=4) - file = open(meta_path, 'w+') - file.write(raw_meta) - file.close() + with open(meta_path, 'w+', encoding='utf-8') as file: + file.write(raw_meta) def _install_finished(self, job): uuid = job['uuid'] @@ -711,8 +753,18 @@ def _uninstall(self, job): shutil.rmtree(os.path.join(old_settings_dir, uuid)) for folder in self.spices_directories: shutil.rmtree(os.path.join(folder, uuid), ignore_errors=True) - except Exception as detail: - self.errorMessage(_("A problem occurred while removing %s.") % job['uuid'], str(detail)) + if self.actions: + disabled_list = self.settings.get_strv(self.enabled_key) + uuid_name = f"{uuid}.nemo_action" + if uuid_name in disabled_list: + disabled_list.remove(uuid_name) + self.settings.set_strv(self.enabled_key, disabled_list) + try: + os.remove(os.path.join(folder, f'{uuid}.nemo_action')) + except FileNotFoundError: + pass + except Exception as error: + self.errorMessage(_("A problem occurred while removing %s.") % job['uuid'], str(error)) def update_all(self): """ applies all available updates""" @@ -727,18 +779,18 @@ def abort(self, abort_type=ABORT_USER): def _is_aborted(self): return self.download_manager.abort_status - def _ui_error_message(self, msg, detail = None): - dialog = Gtk.MessageDialog(transient_for = self.window, - modal = True, - message_type = Gtk.MessageType.ERROR, - buttons = Gtk.ButtonsType.OK) + def _ui_error_message(self, msg, detail=None): + dialog = Gtk.MessageDialog(transient_for=self.window, + modal=True, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK) markup = msg if detail is not None: markup += _("\n\nDetails: %s") % (str(detail)) esc = html.escape(markup) dialog.set_markup(esc) dialog.show_all() - response = dialog.run() + _ = dialog.run() dialog.destroy() def errorMessage(self, msg, detail=None): @@ -753,13 +805,13 @@ def enable_extension(self, uuid, panel=1, box='right', position=0): for entry in self.settings.get_strv(self.enabled_key): info = entry.split(':') pos = int(info[2]) - if info[0] == 'panel%d' % panel and info[1] == box and position <= pos: + if info[0] == f'panel{panel}' and info[1] == box and position <= pos: info[2] = str(pos+1) entries.append(':'.join(info)) else: entries.append(entry) - entries.append('panel%d:%s:%d:%s:%d' % (panel, box, position, uuid, applet_id)) + entries.append(f'panel{panel}:{box}:{position}:{uuid}:{applet_id}') self.settings.set_strv(self.enabled_key, entries) elif self.collection_type == 'desklet': @@ -770,10 +822,15 @@ def enable_extension(self, uuid, panel=1, box='right', position=0): screen = Gdk.Screen.get_default() primary = screen.get_primary_monitor() primary_rect = screen.get_monitor_geometry(primary) - enabled.append('%s:%d:%d:%d' % (uuid, desklet_id, primary_rect.x + 100, primary_rect.y + 100)) + enabled.append(f'{uuid}:{desklet_id}:{primary_rect.x + 100}:{primary_rect.y + 100}') self.settings.set_strv(self.enabled_key, enabled) - + elif self.actions: + disabled_extensions = self.settings.get_strv(self.enabled_key) + uuid_name = f"{uuid}.nemo_action" + if uuid_name in disabled_extensions: + disabled_extensions.remove(uuid_name) + self.settings.set_strv(self.enabled_key, disabled_extensions) else: enabled = self.settings.get_strv(self.enabled_key) enabled.append(uuid) @@ -783,6 +840,14 @@ def disable_extension(self, uuid): if self.themes: return + if self.actions: + disabled_extensions = self.settings.get_strv(self.enabled_key) + uuid_name = f"{uuid}.nemo_action" + if uuid_name not in disabled_extensions: + disabled_extensions.append(uuid_name) + self.settings.set_strv(self.enabled_key, disabled_extensions) + return + enabled_extensions = self.settings.get_strv(self.enabled_key) new_list = [] for enabled_extension in enabled_extensions: @@ -808,6 +873,6 @@ def get_icon(self, uuid): pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file_path, 24, 24, True) return Gtk.Image.new_from_pixbuf(pixbuf) - except Exception as e: + except Exception: print("There was an error processing one of the images. Try refreshing the cache.") return Gtk.Image.new_from_icon_name('image-missing', 2) diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py new file mode 100644 index 0000000000..173379a0a4 --- /dev/null +++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_actions.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +from ExtensionCore import ManageSpicesPage, DownloadSpicesPage +from Spices import Spice_Harvester +from SettingsWidgets import SidePage +from xapp.GSettingsWidgets import * +from gi.repository import GLib + + +class Module: + comment = _("Manage your Actions") + name = "actions" + category = "prefs" + + def __init__(self, content_box): + self.window = None + self.sidePage = ActionsViewSidePage(content_box, self) + + def on_module_selected(self): + if not self.loaded: + print(_("Loading Actions module")) + self.sidePage.load(self.window) + + def _setParentRef(self, window): + self.window = window + + +class ActionsViewSidePage(SidePage): + collection_type = "action" + + def __init__(self, content_box, module): + self.RemoveString = "" + keywords = _("action") + + super().__init__(_("Actions"), "cs-actions", keywords, + content_box, module=module) + + def load(self, window): + self.window = window + + self.spices = Spice_Harvester(self.collection_type, self.window) + + self.stack = SettingsStack() + self.add_widget(self.stack) + self.stack.expand = True + + manage_extensions_page = ManageActionsPage(self, self.spices, self.window) + self.stack.add_titled(manage_extensions_page, 'installed', _("Manage")) + + download_actions_page = DownloadSpicesPage(self, self.collection_type, self.spices, self.window) + self.stack.add_titled(download_actions_page, 'more', _("Download")) + + +class ManageActionsPage(ManageSpicesPage): + directories = [f"{GLib.get_home_dir()}/.local/share/nemo/actions"] + collection_type = "action" + instance_button_text = _("Enable") + remove_button_text = _("Disable") + installed_page_title = _("Installed Actions") + uninstall_button_text = _("Uninstall") + restore_button_text = _("Remove all") + + def __init__(self, parent, spices, window): + super().__init__(parent, self.collection_type, spices, window) diff --git a/python3/cinnamon/harvester.py b/python3/cinnamon/harvester.py index e8ff27f847..a8cee9325c 100644 --- a/python3/cinnamon/harvester.py +++ b/python3/cinnamon/harvester.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -import requests import os import subprocess import json @@ -14,6 +13,7 @@ import threading from concurrent.futures import ThreadPoolExecutor from PIL import Image +import requests import gi gi.require_version('Gtk', '3.0') @@ -25,17 +25,19 @@ from . import proxygsettings DEBUG = os.getenv("DEBUG") is not None + + def debug(msg): if DEBUG: print(msg) + LANGUAGE_CODE = "C" try: LANGUAGE_CODE = locale.getlocale()[0].split("_")[0] except: pass -URL_GITHUB_SPICES = "https://cinnamon-spices.linuxmint.com" URL_SPICES_HOME = "https://cinnamon-spices.linuxmint.com" SPICE_MAP = { @@ -54,6 +56,11 @@ def debug(msg): "enabled-schema": "org.cinnamon", "enabled-key": "enabled-extensions" }, + "action": { + "url": URL_SPICES_HOME + "/json/actions.json", + "enabled-schema": "org.nemo.plugins", + "enabled-key": "disabled-actions" + }, "theme": { "url": URL_SPICES_HOME + "/json/themes.json", "enabled-schema": "org.cinnamon.theme", @@ -66,7 +73,7 @@ def debug(msg): TIMEOUT_DOWNLOAD_ZIP = 120 home = os.path.expanduser("~") -locale_inst = '%s/.local/share/locale' % home +locale_inst = f'{home}/.local/share/locale' settings_dir = os.path.join(GLib.get_user_config_dir(), 'cinnamon', 'spices') activity_logger = logger.ActivityLogger() @@ -75,6 +82,8 @@ def debug(msg): # This gives us a unique value every 10 minutes to allow # the server cache to be utilized. TIMESTAMP_LIFETIME_MINUTES = 10 + + def get_current_timestamp(): seconds = datetime.datetime.utcnow().timestamp() return int(seconds // (TIMESTAMP_LIFETIME_MINUTES * 60)) @@ -88,18 +97,18 @@ def __init__(self, spice_type, uuid, index_node, meta_node): self.author = "" try: author = index_node["author_user"] - if author not in ("none", "unknown"): + if author not in {"none", "unknown"}: self.author = author except: pass try: - self.name = index_node["translations"]["name_%s" % LANGUAGE_CODE] + self.name = index_node["translations"][f"name_{LANGUAGE_CODE}"] except: self.name = index_node["name"] try: - self.description = index_node["translations"]["description_%s" % LANGUAGE_CODE] + self.description = index_node["translations"][f"description_{LANGUAGE_CODE}"] except: self.description = index_node["description"] @@ -116,9 +125,10 @@ def __init__(self, spice_type, uuid, index_node, meta_node): self.commit_id = index_node['last_commit'] self.commit_msg = index_node['last_commit_subject'] - self.link = "%s/%ss/view/%s" % (URL_SPICES_HOME, spice_type, index_node["spices-id"]) + self.link = f"{URL_SPICES_HOME}/{spice_type}s/view/{index_node['spices-id']}" self.size = index_node['file_size'] + class SpicePathSet: def __init__(self, cache_item, spice_type): cache_folder = Path(os.path.join(GLib.get_user_cache_dir(), 'cinnamon', 'spices', spice_type)) @@ -137,11 +147,13 @@ def __init__(self, cache_item, spice_type): self.thumb_local_path = cache_folder.joinpath(self.thumb_basename) self.zip_download_url = URL_SPICES_HOME + cache_item['file'] + class Harvester: def __init__(self, spice_type): self.spice_type = spice_type self.themes = self.spice_type == "theme" + self.actions = self.spice_type == "action" self.has_cache = False self.updates = [] @@ -153,10 +165,10 @@ def __init__(self, spice_type): self.index_file = os.path.join(self.cache_folder, "index.json") - self.install_folder = os.path.join(home, ".local/share/cinnamon", "%ss" % self.spice_type) + self.install_folder = f"{home}/.local/share/nemo/actions" if self.actions else os.path.join(home, ".local/share/cinnamon", f"{self.spice_type}s") if self.themes: - old_install_folder = '%s/.themes/' % home + old_install_folder = f'{home}/.themes/' self.spices_directories = (old_install_folder, self.install_folder) else: self.spices_directories = (self.install_folder, ) @@ -178,10 +190,10 @@ def anything_installed(self): if not path.exists(): continue - for spice in path.iterdir(): + for _ in path.iterdir(): return True - debug("No additional %ss installed" % self.spice_type) + debug(f"No additional {self.spice_type}s installed") return False def refresh(self, full): @@ -190,7 +202,7 @@ def refresh(self, full): if self.disabled: return - debug("Cache stamp: %d" % get_current_timestamp()) + debug(f"Cache stamp: {get_current_timestamp()}") os.makedirs(self.cache_folder, mode=0o755, exist_ok=True) self._update_local_json() @@ -218,6 +230,13 @@ def get_enabled(self, uuid): else: enabled_list = settings.get_strv(SPICE_MAP[self.spice_type]["enabled-key"]) + if self.actions: + # enabled_list is really disabled_list + uuid_name = f'{uuid}.nemo_action' + if uuid_name not in enabled_list: + enabled_count = 1 + return enabled_count + for item in enabled_list: item = item.replace("!", "") if uuid in item.split(":"): @@ -226,24 +245,21 @@ def get_enabled(self, uuid): return enabled_count def _update_local_json(self): - debug("harvester: Downloading new list of available %ss" % self.spice_type) + debug(f"harvester: Downloading new list of available {self.spice_type}s") url = SPICE_MAP[self.spice_type]["url"] try: r = requests.get(url, timeout=TIMEOUT_DOWNLOAD_JSON, proxies=self.proxy_info, - params={ "time" : get_current_timestamp() }) - debug("Downloading from %s" % r.request.url) + params={"time": get_current_timestamp()}) + debug(f"Downloading from {r.request.url}") + r.raise_for_status() except Exception as e: - print("Could not refresh json data for %s: %s" % (self.spice_type, e)) + print(f"Could not refresh json data for {self.spice_type}: {e}") return - if r.status_code != requests.codes.ok: - debug("Can't download spices json: ", r.status_code) - return - - with open(self.index_file, "w") as f: + with open(self.index_file, "w", encoding="utf-8") as f: f.write(r.text) self._load_cache() @@ -257,35 +273,32 @@ def thumb_job(uuid, item): with self.cache_lock: for uuid, item in self.index_cache.items(): - debug("Submitting thumb_job for %s" % uuid) + debug(f"Submitting thumb_job for {uuid}") tpe.submit(thumb_job, copy.copy(uuid), copy.deepcopy(item)) def _download_thumb(self, uuid, item): paths = SpicePathSet(item, spice_type=self.spice_type) - if (not os.path.isfile(paths.thumb_local_path)) or self._is_bad_image(paths.thumb_local_path) or self._spice_has_update(uuid): - debug("Downloading thumbnail for %s: %s" % (uuid, paths.thumb_download_url)) + if not os.path.isfile(paths.thumb_local_path) or self._is_bad_image(paths.thumb_local_path) or self._spice_has_update(uuid): + debug(f"Downloading thumbnail for {uuid}: {paths.thumb_download_url}") try: r = requests.get(paths.thumb_download_url, timeout=TIMEOUT_DOWNLOAD_THUMB, proxies=self.proxy_info, - params={ "time" : get_current_timestamp() }) + params={"time": get_current_timestamp()}) + r.raise_for_status() except Exception as e: - print("Could not get thumbnail for %s: %s" % (uuid, e)) + print(f"Could not get thumbnail for {uuid}: {e}") return - if r.status_code != requests.codes.ok: - debug("Can't download thumbnail for %s: %s" % (uuid, r.status_code)) - return - - with open(paths.thumb_local_path, "wb") as f: + with open(paths.thumb_local_path, "wb", encoding="utf-8") as f: f.write(r.content) def _load_metadata(self): if self.disabled: return - debug("harvester: Loading metadata on installed %ss" % self.spice_type) + debug(f"harvester: Loading metadata on installed {self.spice_type}s") self.meta_map = {} for directory in self.spices_directories: @@ -294,8 +307,10 @@ def _load_metadata(self): for uuid in extensions: subdirectory = os.path.join(directory, uuid) + if uuid.endswith('.nemo_action'): + continue try: - with open(os.path.join(subdirectory, "metadata.json"), "r") as f: + with open(os.path.join(subdirectory, "metadata.json"), "r", encoding="utf-8") as f: metadata = json.load(f) metadata['path'] = subdirectory @@ -303,7 +318,7 @@ def _load_metadata(self): self.meta_map[uuid] = metadata except Exception as detail: debug(detail) - debug("Skipping %s: there was a problem trying to read metadata.json" % uuid) + debug(f"Skipping {uuid}: there was a problem trying to read metadata.json") except FileNotFoundError: # debug("%s does not exist! Creating it now." % directory) try: @@ -315,11 +330,11 @@ def _load_cache(self): if self.disabled: return - debug("harvester: Loading local %s cache" % self.spice_type) + debug(f"harvester: Loading local {self.spice_type} cache") self.index_cache = {} try: - with open(self.index_file, "r") as f: + with open(self.index_file, "r", encoding="utf-8") as f: self.index_cache = json.load(f) self.has_cache = True except FileNotFoundError: @@ -336,12 +351,12 @@ def _generate_update_list(self): if self.disabled: return [] - debug("harvester: Generating list of updated %ss" % self.spice_type) + debug(f"harvester: Generating list of updated {self.spice_type}s") self.updates = [] - for uuid in self.index_cache.keys(): - if uuid in self.meta_map.keys() and self._spice_has_update(uuid): + for uuid in self.index_cache: + if uuid in self.meta_map and self._spice_has_update(uuid): update = SpiceUpdate(self.spice_type, uuid, self.index_cache[uuid], self.meta_map[uuid]) self.updates.append(update) @@ -350,16 +365,16 @@ def _generate_update_list(self): def _spice_has_update(self, uuid): try: return int(self.meta_map[uuid]["last-edited"]) < self.index_cache[uuid]["last_edited"] - except Exception as e: + except Exception: return False def _install_by_uuid(self, uuid): - action = "upgrade" if uuid in self.meta_map.keys() else "install" + action = "upgrade" if uuid in self.meta_map else "install" try: item = self.index_cache[uuid] except KeyError: - debug("Can't install %s - it doesn't seem to exist on the server" % uuid) + debug(f"Can't install {uuid} - it doesn't seem to exist on the server") return paths = SpicePathSet(item, spice_type=self.spice_type) @@ -368,13 +383,10 @@ def _install_by_uuid(self, uuid): r = requests.get(paths.zip_download_url, timeout=TIMEOUT_DOWNLOAD_ZIP, proxies=self.proxy_info, - params={ "time" : get_current_timestamp() }) + params={"time": get_current_timestamp()}) + r.raise_for_status() except Exception as e: - print("Could not download zip for %s: %s" % (uuid, e)) - return - - if r.status_code != requests.codes.ok: - debug("couldn't download") + print(f"Could not download zip for {uuid}: {e}") return try: @@ -383,17 +395,17 @@ def _install_by_uuid(self, uuid): tmp_name = f.name f.write(r.content) - zip = zipfile.ZipFile(tmp_name) - os.remove(tmp_name) + with zipfile.ZipFile(tmp_name) as _zip: + os.remove(tmp_name) - with tempfile.TemporaryDirectory() as d: - zip.extractall(d) - self._install_from_folder(os.path.join(d, uuid), uuid, from_spices=True) - self.write_to_log(uuid, action) + with tempfile.TemporaryDirectory() as d: + _zip.extractall(d) + self._install_from_folder(os.path.join(d, uuid), uuid, from_spices=True) + self.write_to_log(uuid, action) self._load_metadata() except Exception as e: - debug("couldn't install", e) + debug(f"couldn't install: {e}") def _install_from_folder(self, folder, uuid, from_spices=False): contents = os.listdir(folder) @@ -407,16 +419,24 @@ def _install_from_folder(self, folder, uuid, from_spices=False): lang = file.split(".")[0] locale_dir = os.path.join(locale_inst, lang, 'LC_MESSAGES') os.makedirs(locale_dir, mode=0o755, exist_ok=True) - subprocess.call(['msgfmt', '-c', os.path.join(po_dir, file), '-o', os.path.join(locale_dir, '%s.mo' % uuid)]) + subprocess.run(['/usr/bin/msgfmt', '-c', + os.path.join(po_dir, file), '-o', + os.path.join(locale_dir, f'{uuid}.mo')], + check=True) dest = os.path.join(self.install_folder, uuid) if os.path.exists(dest): shutil.rmtree(dest) - shutil.copytree(folder, dest) + if self.actions and os.path.exists(dest + '.nemo_action'): + os.remove(dest + '.nemo_action') + if not self.actions: + shutil.copytree(folder, dest) + else: + shutil.copytree(folder, self.install_folder, dirs_exist_ok=True) if not self.themes: # ensure proper file permissions - for root, dirs, files in os.walk(dest): + for root, _, files in os.walk(dest): for file in files: os.chmod(os.path.join(root, file), 0o755) @@ -425,7 +445,7 @@ def _install_from_folder(self, folder, uuid, from_spices=False): if self.themes and not os.path.exists(meta_path): md = {} else: - with open(meta_path, "r") as f: + with open(meta_path, "r", encoding='utf-8') as f: md = json.load(f) if from_spices and uuid in self.index_cache: @@ -433,7 +453,7 @@ def _install_from_folder(self, folder, uuid, from_spices=False): else: md['last-edited'] = int(datetime.datetime.utcnow().timestamp()) - with open(meta_path, "w+") as f: + with open(meta_path, "w+", encoding='utf-8') as f: json.dump(md, f, indent=4) def write_to_log(self, uuid, action): @@ -445,20 +465,20 @@ def write_to_log(self, uuid, action): new_version = datetime.datetime.fromtimestamp(remote_item["last_edited"]).strftime("%Y.%m.%d") except KeyError: if action in ("upgrade", "install"): - debug("Upgrading %s with no local metadata - something's wrong" % uuid) + debug(f"Upgrading {uuid} with no local metadata - something's wrong") try: local_item = self.meta_map[uuid] old_version = datetime.datetime.fromtimestamp(local_item["last-edited"]).strftime("%Y.%m.%d") except KeyError: if action in ("upgrade", "remove"): - debug("Upgrading or removing %s with no local metadata - something's wrong" % uuid) + debug(f"Upgrading or removing {uuid} with no local metadata - something's wrong") log_timestamp = datetime.datetime.now().strftime("%F %T") - activity_logger.log("%s %s %s %s %s %s" % (log_timestamp, self.spice_type, action, uuid, old_version, new_version)) + activity_logger.log(f"{log_timestamp} {self.spice_type} {action} {uuid} {old_version} {new_version}") def get_icon_surface(self, uuid, ui_scale): - """ gets the icon for a given uuid""" + """ gets the icon for a given uuid""" try: pixbuf = None @@ -473,15 +493,15 @@ def get_icon_surface(self, uuid, ui_scale): raise Exception surf = Gdk.cairo_surface_create_from_pixbuf(pixbuf, ui_scale, None) - return Gtk.Image.new_from_surface (surf) - except Exception as e: + return Gtk.Image.new_from_surface(surf) + except Exception: debug("There was an error processing one of the images. Try refreshing the cache.") return Gtk.Image.new_from_icon_name('image-missing', Gtk.IconSize.LARGE_TOOLBAR) def _is_bad_image(self, path): try: Image.open(path) - except IOError as detail: + except IOError: return True return False @@ -500,7 +520,7 @@ def _clean_old_thumbs(self): if f == "index.json": continue try: - debug("removing old thumb", f) + debug(f"removing old thumb: {f}") os.remove(os.path.join(self.cache_folder, f)) except: - pass \ No newline at end of file + pass diff --git a/python3/cinnamon/logger.py b/python3/cinnamon/logger.py index 73944ebc12..4a028062cf 100644 --- a/python3/cinnamon/logger.py +++ b/python3/cinnamon/logger.py @@ -10,7 +10,8 @@ try: logfile = os.path.join(GLib.get_user_state_dir(), 'cinnamon', 'harvester.log') except AttributeError: - logfile = '%s/.cinnamon/harvester.log' % os.path.expanduser("~") + logfile = f'{os.path.expanduser("~")}/.cinnamon/harvester.log' + class ActivityLogger: def __init__(self): @@ -27,5 +28,5 @@ def write_to_file_thread(self): os.makedirs(directory) while True: entry = self.queue.get() - with open(logfile, "a") as f: - f.write("%s\n" % entry) + with open(logfile, "a", encoding='utf-8') as f: + f.write(f"{entry}\n") diff --git a/python3/cinnamon/updates.py b/python3/cinnamon/updates.py index aeed2367c6..ef35cd7e0c 100644 --- a/python3/cinnamon/updates.py +++ b/python3/cinnamon/updates.py @@ -1,19 +1,23 @@ #!/usr/bin/python3 -import gi import gettext +import gi + +from . import harvester gi.require_version('Gtk', '3.0') gettext.install("cinnamon", "/usr/share/locale", names=["ngettext"]) -from . import harvester - SPICE_TYPE_APPLET = "applet" SPICE_TYPE_DESKLET = "desklet" SPICE_TYPE_THEME = "theme" SPICE_TYPE_EXTENSION = "extension" -SPICE_TYPES = [SPICE_TYPE_APPLET, SPICE_TYPE_DESKLET, SPICE_TYPE_THEME, SPICE_TYPE_EXTENSION] +SPICE_TYPE_ACTION = "action" + +SPICE_TYPES = [SPICE_TYPE_APPLET, SPICE_TYPE_DESKLET, SPICE_TYPE_THEME, + SPICE_TYPE_EXTENSION, SPICE_TYPE_ACTION] + class UpdateManager: def __init__(self): @@ -32,19 +36,19 @@ def refresh_all_caches(self, full=False): self.refresh_cache_for_type(spice_type, full) def refresh_cache_for_type(self, spice_type, full=False): - harvester = self.harvesters[spice_type] - return harvester.refresh(full) + _harvester = self.harvesters[spice_type] + return _harvester.refresh(full) def get_updates_of_type(self, spice_type): - harvester = self.harvesters[spice_type] - return harvester.get_updates() + _harvester = self.harvesters[spice_type] + return _harvester.get_updates() def upgrade(self, update): self.upgrade_uuid(update.uuid, update.spice_type) def upgrade_uuid(self, uuid, spice_type): - harvester = self.harvesters[spice_type] - harvester.install(uuid) + _harvester = self.harvesters[spice_type] + _harvester.install(uuid) def spice_is_enabled(self, update): return self.harvesters[update.spice_type].get_enabled(update.uuid) > 0