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