Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions data/artist-packs/dev.sinty.desktop.artist-pack-install.policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>Singularity</vendor>
<action id="dev.sinty.desktop.artist-pack-install">
<description>Install an Artist Pack</description>
<message>Authentication is required to install wallpaper packages</message>
<icon_name>preferences-desktop-wallpaper-symbolic</icon_name>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
35 changes: 35 additions & 0 deletions data/artist-packs/singularity-artist-pack-install
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh
# Usage: singularity-artist-pack-install PACKAGE SOURCE_URI (via pkexec).
# Root has no dconf session, so apt's own policy must confirm SOURCE_URI.
set -eu

PACKAGE="${1:-}"
SOURCE_URI="${2:-}"
[ -n "$PACKAGE" ] && [ -n "$SOURCE_URI" ] || {
echo "usage: $0 PACKAGE SOURCE_URI" >&2
exit 2
}
case "$PACKAGE" in
*[!a-zA-Z0-9.+-]*|"")
echo "refusing: '$PACKAGE' is not a valid package name" >&2
exit 1
;;
esac

command -v apt-cache >/dev/null 2>&1 && command -v apt-get >/dev/null 2>&1 || {
echo "apt is not available on this system" >&2
exit 1
}

POLICY=$(apt-cache policy "$PACKAGE" 2>/dev/null || true)
CANDIDATE=$(printf '%s' "$POLICY" | awk '/Candidate:/{print $2; exit}')
[ -n "$CANDIDATE" ] && [ "$CANDIDATE" != "(none)" ] || {
echo "refusing: '$PACKAGE' has no installation candidate" >&2
exit 1
}
printf '%s' "$POLICY" | grep -F "$SOURCE_URI" >/dev/null || {
echo "refusing: '$PACKAGE' candidate $CANDIDATE is not associated with source $SOURCE_URI" >&2
exit 1
}

exec apt-get install -y --no-install-recommends "$PACKAGE"
68 changes: 68 additions & 0 deletions data/artist-packs/singularity-artist-pack-inventory
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/sh
# Unprivileged. Prints a JSON array of
# {package,title,summary,version,source,installed} for every package the
# dev.sinty.desktop artist-pack-apt-sources entries publish.
set -eu

json_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e ':a;N;$!ba;s/\n/\\n/g'
}

SOURCES_RAW=$(gsettings get dev.sinty.desktop artist-pack-apt-sources 2>/dev/null || printf '@as []')
# gsettings prints a GVariant literal: ['deb https://...', ...]
SOURCE_LINES=$(printf '%s\n' "$SOURCES_RAW" \
| sed -e "s/^@as //" -e "s/^\[//" -e "s/\]\s*$//" \
| tr ',' '\n' \
| sed -e "s/^[[:space:]]*'//" -e "s/'[[:space:]]*$//" \
| grep -v '^[[:space:]]*$' || true)

[ -n "$SOURCE_LINES" ] || { printf '[]\n'; exit 0; }
command -v apt-cache >/dev/null 2>&1 || { printf '[]\n'; exit 0; }
command -v apt-get >/dev/null 2>&1 || { printf '[]\n'; exit 0; }

first=1
printf '['
printf '%s\n' "$SOURCE_LINES" | while IFS= read -r source_line; do
[ -n "$source_line" ] || continue
uri=$(printf '%s' "$source_line" | awk '{print $2}')
suite=$(printf '%s' "$source_line" | awk '{print $3}')
[ -n "$uri" ] && [ -n "$suite" ] || continue

# Ask apt which index it resolved, rather than guessing the lists/ name.
index_file=$(apt-get indextargets --format '$(FILENAME)' \
"Repo-URI: $uri" "Codename: $suite" 2>/dev/null | head -1)
[ -n "$index_file" ] && [ -r "$index_file" ] || continue

pkg=""
version=""
summary=""
while IFS= read -r line; do
case "$line" in
"Package: "*) pkg=${line#Package: } ;;
"Version: "*) version=${line#Version: } ;;
"Description: "*|"Description-en: "*) summary=${line#*: } ;;
"")
[ -n "$pkg" ] || continue
installed=false
dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q '^install ok installed$' && installed=true
[ "$first" = 1 ] || printf ','
first=0
printf '{"package":"%s","title":"%s","summary":"%s","version":"%s","source":"%s","installed":%s}' \
"$(json_escape "$pkg")" "$(json_escape "$pkg")" "$(json_escape "$summary")" \
"$(json_escape "$version")" "$(json_escape "$uri")" "$installed"
pkg=""; version=""; summary=""
;;
esac
done < "$index_file"
# A Packages file without a trailing blank line leaves the last stanza unflushed.
if [ -n "$pkg" ]; then
installed=false
dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q '^install ok installed$' && installed=true
[ "$first" = 1 ] || printf ','
first=0
printf '{"package":"%s","title":"%s","summary":"%s","version":"%s","source":"%s","installed":%s}' \
"$(json_escape "$pkg")" "$(json_escape "$pkg")" "$(json_escape "$summary")" \
"$(json_escape "$version")" "$(json_escape "$uri")" "$installed"
fi
done
printf ']\n'
8 changes: 8 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ singularity_core_sources = files(
'src/core/wallpaper_gallery.vala',
'src/core/wallpaper_rotation_state.vala',
'src/core/wallpaper_rotator.vala',
'src/core/artist_pack_manager.vala',
'src/core/wallpaper_sidecar.vala',
'src/core/wallpaper_ocs.vala',
'src/core/wallpaper_browse_cache.vala',
Expand Down Expand Up @@ -490,6 +491,13 @@ install_data('data/fan-control/dev.sinty.FanControl.conf',
install_data('data/fan-control/dev.sinty.fan-control.policy',
install_dir: get_option('datadir') / 'polkit-1' / 'actions')

install_data('data/artist-packs/singularity-artist-pack-inventory',
install_dir: '/usr/local/bin', install_mode: 'rwxr-xr-x')
install_data('data/artist-packs/singularity-artist-pack-install',
install_dir: '/usr/local/bin', install_mode: 'rwxr-xr-x')
install_data('data/artist-packs/dev.sinty.desktop.artist-pack-install.policy',
install_dir: get_option('datadir') / 'polkit-1' / 'actions')

wallpaper_collections_test = executable('wallpaper-collections-test',
sources: ['src/core/wallpaper_collections.vala', 'tests/wallpaper_collections_test.vala'],
dependencies: [dependency('gobject-2.0'), dependency('glib-2.0'), dependency('gio-2.0'), gee_dep, json_dep],
Expand Down
103 changes: 103 additions & 0 deletions src/components/sidebar/pages/desktop_page.vala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ namespace Singularity {
private int wallpaper_accent_generation = 0;
private string cached_wallpaper_accent = "#3584e4";
private static bool wallpaper_css_loaded = false;
private PreferencesGroup? artist_pack_group = null;
private int artist_pack_refresh_generation = 0;
// In-flight installs; survives the row teardown a refresh performs.
private HashSet<string> artist_packs_installing = new HashSet<string>();

// Label for a stored rotate-interval that doesn't match a fixed
// preset. Hours when it divides evenly, else minutes, else seconds
Expand Down Expand Up @@ -432,6 +436,19 @@ namespace Singularity {

add_group(grid_group);
GLib.Idle.add(() => { populate_grid(); return GLib.Source.REMOVE; });

if (ArtistPackManager.get_default().is_available()) {
artist_pack_group = new PreferencesGroup(
_("Artist Packs"),
_("Curated wallpaper packs, installed through the system package manager."));
var artist_pack_refresh_btn = new Button.from_icon_name("view-refresh-symbolic");
artist_pack_refresh_btn.has_frame = false;
artist_pack_refresh_btn.tooltip_text = _("Refresh");
artist_pack_refresh_btn.clicked.connect(() => { populate_artist_packs_async.begin(); });
artist_pack_group.add_header_suffix(artist_pack_refresh_btn);
add_group(artist_pack_group);
populate_artist_packs_async.begin();
}
refresh_wallpaper_accent_async();
update_preview_async();
var wm = WallpaperManager.get_default();
Expand Down Expand Up @@ -2223,6 +2240,92 @@ namespace Singularity {
}
}

private async void populate_artist_packs_async() {
if (artist_pack_group == null) return;
int gen = ++artist_pack_refresh_generation;

artist_pack_group.clear();
var loading_row = new ActionRow(_("Loading..."));
loading_row.activatable = false;
artist_pack_group.add_row(loading_row);

Gee.ArrayList<ArtistPackInfo> packs;
try {
packs = yield ArtistPackManager.get_default().fetch_inventory_async();
} catch (Error e) {
if (gen != artist_pack_refresh_generation) return;
artist_pack_group.clear();
var error_row = new ActionRow(_("Could not list Artist Packs"), e.message, "dialog-error-symbolic");
error_row.activatable = false;
artist_pack_group.add_row(error_row);
return;
}
if (gen != artist_pack_refresh_generation) return;

artist_pack_group.clear();
if (packs.size == 0) {
var empty_row = new ActionRow(
_("No Artist Packs available"),
_("None of the configured apt sources currently offer one, or none are configured."));
empty_row.activatable = false;
artist_pack_group.add_row(empty_row);
return;
}

foreach (var pack in packs) {
var row = new ActionRow(pack.title, pack.summary);
row.activatable = false;
// A transaction started before this refresh is not yet in pack.installed.
bool installing = artist_packs_installing.contains(pack.package);
var install_btn = new Button.with_label(
installing ? _("Installing...") : (pack.installed ? _("Installed") : _("Install")));
install_btn.sensitive = !installing && !pack.installed;
string captured_package = pack.package;
string captured_source = pack.source;
Button captured_btn = install_btn;
install_btn.clicked.connect(() => {
start_artist_pack_install(captured_package, captured_source, captured_btn);
});
row.add_suffix(install_btn);
artist_pack_group.add_row(row);
}
}

private void start_artist_pack_install(string package, string source, Button btn) {
if (!artist_packs_installing.add(package)) return;
// A refresh mid-install detaches btn's row, so relabel only if gen holds.
int gen = artist_pack_refresh_generation;
btn.sensitive = false;
btn.label = _("Installing...");
ArtistPackManager.get_default().install_async.begin(package, source, null, (obj, res) => {
artist_packs_installing.remove(package);
bool row_alive = (gen == artist_pack_refresh_generation);
try {
ArtistPackManager.get_default().install_async.end(res);
if (row_alive) {
btn.label = _("Installed");
} else {
populate_artist_packs_async.begin();
}
// The new pack's .collection file is only re-read here.
populate_grid();
} catch (Error e) {
warning("Artist Pack install of %s failed: %s", package, e.message);
if (!row_alive) {
populate_artist_packs_async.begin();
return;
}
btn.label = _("Install Failed");
GLib.Timeout.add_seconds(4, () => {
if (gen != artist_pack_refresh_generation) return GLib.Source.REMOVE;
btn.label = _("Install");
btn.sensitive = true;
return GLib.Source.REMOVE;
});
}
});
}

private void populate_grid() {
int gen = ++wallpaper_grid_generation;
wallpaper_grid.remove_all();
Expand Down
Loading
Loading