Initial commit

This commit is contained in:
Victor Fuentes 2022-08-26 17:48:43 -04:00
commit 175e7c23e9
No known key found for this signature in database
GPG Key ID: 0A88B68D6A9ACAE0
52 changed files with 11578 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.envrc
/.direnv
/.vscode
/result
/src/config.rs
/target

3089
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

41
Cargo.toml Normal file
View File

@ -0,0 +1,41 @@
[package]
name = "nix-software-center"
version = "0.0.1"
edition = "2021"
default-run = "nix-software-center"
[dependencies]
relm4 = { git = "https://github.com/Relm4/Relm4", tag = "v0.5.0-beta.2", features = ["all"] }
relm4-components = { package = "relm4-components", git = "https://github.com/Relm4/Relm4", tag = "v0.5.0-beta.2"}
adw = { package = "libadwaita", git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", features = ["v1_2"] }
tokio = { version = "1.20", features = ["rt", "macros", "time", "rt-multi-thread", "sync", "process"] }
sourceview5 = { git = "https://gitlab.gnome.org/World/Rust/sourceview5-rs/", rev = "6082210f7d1fc32b100bd9c714e9521eecacb3f7", features = ["v5_4"] }
tracker = "0.1"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
simd-json = { version = "0.6", features = ["allow-non-simd"] }
nix-editor = "0.2.12"
html2pango = "0.4"
brotli = "3.3"
log = "0.4"
pretty_env_logger = "0.4"
flate2 = "1.0"
quick-xml = { version = "0.23", features = ["serialize"] }
rand = "0.8"
reqwest = { version = "0.11", features = ["blocking"] }
sha256 = "1.0"
image = "0.24"
spdx = "0.9"
# { git = "https://github.com/EmbarkStudios/spdx", version = "0.8" }
edit-distance = "2.1"
ijson = "0.1"
strum = "0.24"
strum_macros = "0.24"
[workspace]
members = [".", "nsc-helper"]
default-members = [".", "nsc-helper"]

9
build-aux/dist-vendor.sh Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
export SOURCE_ROOT="$1"
export DIST="$2"
cd "$SOURCE_ROOT"
mkdir "$DIST"/.cargo
cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config
# Move vendor into dist tarball directory
mv vendor "$DIST"

View File

@ -0,0 +1,12 @@
[Desktop Entry]
Name=Software Center
Comment=Install Applications
Type=Application
Exec=nix-software-center
Terminal=false
Categories=Settings;System;Utility;
# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
Keywords=Nix;Nixos;nix;nixos;software;package;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=@icon@
StartupNotify=true

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema path="/dev/vlinkz/NixSoftwareCenter/" id="@app-id@" gettext-domain="@gettext-package@">
</schema>
</schemalist>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>@app-id@</id>
<metadata_license>CC-BY-SA-4.0</metadata_license>
<project_license>MIT</project_license>
<name>Nix Software Center</name>
<summary>A simple application to manage your NixOS packages.</summary>
<description>
<p>A simple software center to install and manage Nix packages, built with libadwaita, GTK4, and Relm4.</p>
</description>
<screenshots>
<!-- <screenshot type="default">
<image>https://raw.githubusercontent.com/vlinkz/nixos-conf-editor/main/data/screenshots/listviewlight.png</image>
<caption>Main window</caption>
</screenshot>
<screenshot type="default">
<image>https://raw.githubusercontent.com/vlinkz/nixos-conf-editor/main/data/screenshots/optionlight2.png</image>
<caption>Option Selection</caption>
</screenshot> -->
</screenshots>
<url type="homepage">https://github.com/vlinkz/nix-software-center</url>
<url type="bugtracker">https://github.com/vlinkz/nix-software-center/issues</url>
<developer_name>Victor Fuentes</developer_name>
<update_contact>vmfuentes64@gmail.com</update_contact>
<translation type="gettext">@gettext-package@</translation>
<launchable type="desktop-id">@app-id@.desktop</launchable>
</component>

View File

@ -0,0 +1,17 @@
<?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>Victor Fuentes</vendor>
<vendor_url>https://github.com/vlinkz</vendor_url>
<action id="dev.vlinkz.NixSoftwareCenter">
<description>Give Nix Software Center root access</description>
<message>Authentication is required to install NixOS system packages</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">@pkglibexecdir@/nsc-helper</annotate>
</action>
</policyconfig>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#99b2e5"/>
<stop offset="0.243452" stop-color="#a9c2e5"/>
<stop offset="1" stop-color="#c2cdeb"/>
</linearGradient>
<linearGradient id="b" x1="29.258924657" x2="52.12594020815" xlink:href="#a" y1="63.99513142558" y2="103.54451872902"/>
<linearGradient id="c" gradientTransform="matrix(0.127763 0.221291 -0.221291 0.127763 96.086827 -56.897127)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="d" gradientTransform="matrix(0.127763 -0.221291 0.221291 0.127763 -54.103341 92.070101)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="e" gradientTransform="matrix(-0.255525 0 0 -0.255525 150.106936 147.648442)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="f" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#636378"/>
<stop offset="0.231686" stop-color="#6a6b8f"/>
<stop offset="1" stop-color="#444465"/>
</linearGradient>
<linearGradient id="g" x1="31.62242739098" x2="54.0836659203" xlink:href="#f" y1="62.6498143668" y2="102.35286903923"/>
<linearGradient id="h" gradientTransform="matrix(-0.127763 0.221291 -0.221291 -0.127763 177.158345 261.09881)" x1="-584.199341" x2="-496.297028" xlink:href="#f" y1="782.335632" y2="937.713989"/>
<linearGradient id="i" gradientTransform="matrix(-0.127763 -0.221291 0.221291 -0.127763 -166.023458 58.751427)" x1="-584.199341" x2="-496.297028" xlink:href="#f" y1="782.335632" y2="937.713989"/>
<clipPath id="j">
<rect height="128" width="128"/>
</clipPath>
<clipPath id="k">
<rect height="128" width="128"/>
</clipPath>
<filter id="l" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="m">
<g clip-path="url(#k)" filter="url(#l)">
<g clip-path="url(#j)">
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#b)" fill-rule="evenodd"/>
<path d="m 89.023438 65.707031 l -31.222657 -54.089843 l 0.039063 -6.152344 l 22.644531 20.550781 l 19.214844 -14.296875 l -0.035157 6.152344 l -11.960937 20.566406 l 8.527344 8.625 l -0.039063 6.152344 z m 0 0" fill="#d7dded" fill-rule="evenodd"/>
<path d="m 75.277344 44.613281 l -62.457032 -0.003906 l -0.007812 -6.140625 l 23.820312 -6.304688 l -8.320312 -14.5 l -0.007812 -6.140624 l 10.867187 -0.039063 l 11.828125 20.640625 l 24.265625 6.347656 z m 0 0" fill="#3d3846" fill-rule="evenodd"/>
<path d="m 50.277344 46.167969 l -31.226563 54.085937 l -7.292969 -12.359375 l 0.039063 -6.152343 l 8.378906 -8.332032 l -16.714843 -0.042968 l -3.5625 -6.175782 l 0.0351558 -6.152344 l 27.3945312 -0.089843 l 22.988281 -20.933594 z m 0 0" fill="#d7dded" fill-rule="evenodd"/>
<path d="m 52.675781 89.402344 l 0.035157 -6.152344 l 62.453124 0.003906 l -0.035156 6.152344 l -7.058594 12.496094 l -16.753906 -0.046875 l 8.359375 8.34375 l -0.039062 6.152343 l -3.566407 6.175782 l -7.289062 0.007812 l -11.832031 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="#d7dded" fill-rule="evenodd"/>
<path d="m 31.878906 74.785156 l 38.390625 41.597656 l -0.035156 6.15625 l -14.351563 0.132813 l -8.335937 -14.53125 l -8.394531 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 0.035156 -6.152344 l 11.925781 -14.414062 l -8.492187 -14.777344 z m 0 0" fill="#3d3846" fill-rule="evenodd"/>
<path d="m 77.710938 87.972656 l 0.011718 -6.148437 l 38.519532 -41.726563 l -0.007813 6.148438 l -8.417969 14.488281 l 20.285156 0.070313 l -0.007812 6.148437 l -3.636719 6.316406 l -23.792969 -0.074219 l -8.550781 14.742188 z m 0 0" fill="#3d3846" fill-rule="evenodd"/>
<path d="m 50.316406 40.015625 l -31.230468 54.085937 l -7.289063 -12.359374 l 8.414063 -14.484376 l -16.714844 -0.042968 l -3.5625002 -6.175782 l 3.6367182 -6.320312 l 23.792969 0.078125 l 8.550781 -14.742187 z m 0 0" fill="url(#c)" fill-rule="evenodd"/>
<path d="m 52.710938 83.25 l 62.453124 0.003906 l -7.054687 12.496094 l -16.753906 -0.046875 l 8.320312 14.496094 l -3.570312 6.175781 l -7.289063 0.007812 l -11.828125 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="url(#d)" fill-rule="evenodd"/>
<path d="m 89.0625 59.554688 l -31.222656 -54.089844 l 14.347656 -0.136719 l 8.335938 14.535156 l 8.394531 -14.457031 l 7.132812 0.003906 l 3.648438 6.308594 l -11.960938 20.566406 l 8.492188 14.777344 z m 0 0" fill="url(#e)" fill-rule="evenodd"/>
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#g)" fill-rule="evenodd"/>
<path d="m 75.265625 38.472656 l -62.453125 -0.003906 l 7.058594 -12.492188 l 16.753906 0.046876 l -8.320312 -14.5 l 3.566406 -6.171876 l 7.289062 -0.007812 l 11.832032 20.640625 l 17.042968 0.035156 z m 0 0" fill="url(#h)" fill-rule="evenodd"/>
<path d="m 77.722656 81.824219 l 31.226563 -54.085938 l 7.292969 12.359375 l -8.417969 14.484375 l 16.714843 0.046875 l 3.5625 6.175782 l -3.636718 6.316406 l -23.792969 -0.074219 l -8.550781 14.738281 z m 0 0" fill="url(#i)" fill-rule="evenodd"/>
</g>
</g>
</mask>
<mask id="n">
<g filter="url(#l)">
<rect fill-opacity="0.8" height="128" width="128"/>
</g>
</mask>
<linearGradient id="o" gradientTransform="matrix(0 0.37 -0.98462 0 295.38501 -30.360001)" gradientUnits="userSpaceOnUse" x1="300" x2="428" y1="235" y2="235">
<stop offset="0" stop-color="#f9f06b"/>
<stop offset="1" stop-color="#f5c211"/>
</linearGradient>
<clipPath id="p">
<rect height="128" width="128"/>
</clipPath>
<clipPath id="q">
<rect height="128" width="128"/>
</clipPath>
<g fill-rule="evenodd">
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#b)"/>
<path d="m 89.023438 65.707031 l -31.222657 -54.089843 l 0.039063 -6.152344 l 22.644531 20.550781 l 19.214844 -14.296875 l -0.035157 6.152344 l -11.960937 20.566406 l 8.527344 8.625 l -0.039063 6.152344 z m 0 0" fill="#d7dded"/>
<path d="m 75.277344 44.613281 l -62.457032 -0.003906 l -0.007812 -6.140625 l 23.820312 -6.304688 l -8.320312 -14.5 l -0.007812 -6.140624 l 10.867187 -0.039063 l 11.828125 20.640625 l 24.265625 6.347656 z m 0 0" fill="#3d3846"/>
<path d="m 50.277344 46.167969 l -31.226563 54.085937 l -7.292969 -12.359375 l 0.039063 -6.152343 l 8.378906 -8.332032 l -16.714843 -0.042968 l -3.5625 -6.175782 l 0.0351558 -6.152344 l 27.3945312 -0.089843 l 22.988281 -20.933594 z m 0 0" fill="#d7dded"/>
<path d="m 52.675781 89.402344 l 0.035157 -6.152344 l 62.453124 0.003906 l -0.035156 6.152344 l -7.058594 12.496094 l -16.753906 -0.046875 l 8.359375 8.34375 l -0.039062 6.152343 l -3.566407 6.175782 l -7.289062 0.007812 l -11.832031 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="#d7dded"/>
<path d="m 31.878906 74.785156 l 38.390625 41.597656 l -0.035156 6.15625 l -14.351563 0.132813 l -8.335937 -14.53125 l -8.394531 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 0.035156 -6.152344 l 11.925781 -14.414062 l -8.492187 -14.777344 z m 0 0" fill="#3d3846"/>
<path d="m 77.710938 87.972656 l 0.011718 -6.148437 l 38.519532 -41.726563 l -0.007813 6.148438 l -8.417969 14.488281 l 20.285156 0.070313 l -0.007812 6.148437 l -3.636719 6.316406 l -23.792969 -0.074219 l -8.550781 14.742188 z m 0 0" fill="#3d3846"/>
<path d="m 50.316406 40.015625 l -31.230468 54.085937 l -7.289063 -12.359374 l 8.414063 -14.484376 l -16.714844 -0.042968 l -3.5625002 -6.175782 l 3.6367182 -6.320312 l 23.792969 0.078125 l 8.550781 -14.742187 z m 0 0" fill="url(#c)"/>
<path d="m 52.710938 83.25 l 62.453124 0.003906 l -7.054687 12.496094 l -16.753906 -0.046875 l 8.320312 14.496094 l -3.570312 6.175781 l -7.289063 0.007812 l -11.828125 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="url(#d)"/>
<path d="m 89.0625 59.554688 l -31.222656 -54.089844 l 14.347656 -0.136719 l 8.335938 14.535156 l 8.394531 -14.457031 l 7.132812 0.003906 l 3.648438 6.308594 l -11.960938 20.566406 l 8.492188 14.777344 z m 0 0" fill="url(#e)"/>
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#g)"/>
<path d="m 75.265625 38.472656 l -62.453125 -0.003906 l 7.058594 -12.492188 l 16.753906 0.046876 l -8.320312 -14.5 l 3.566406 -6.171876 l 7.289062 -0.007812 l 11.832032 20.640625 l 17.042968 0.035156 z m 0 0" fill="url(#h)"/>
<path d="m 77.722656 81.824219 l 31.226563 -54.085938 l 7.292969 12.359375 l -8.417969 14.484375 l 16.714843 0.046875 l 3.5625 6.175782 l -3.636718 6.316406 l -23.792969 -0.074219 l -8.550781 14.738281 z m 0 0" fill="url(#i)"/>
</g>
<g clip-path="url(#q)" mask="url(#m)">
<g clip-path="url(#p)" mask="url(#n)">
<path d="m 128 80.640625 v 47.359375 h -128 v -47.359375 z m 0 0" fill="url(#o)"/>
<path d="m 13.308594 80.640625 l 47.355468 47.359375 h 21.214844 l -47.359375 -47.359375 z m 42.421875 0 l 47.363281 47.359375 h 21.214844 l -47.363282 -47.359375 z m 42.429687 0 l 29.839844 29.839844 v -21.210938 l -8.628906 -8.628906 z m -98.160156 7.90625 v 21.214844 l 18.238281 18.238281 h 21.214844 z m 0 0"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#99b2e5"/>
<stop offset="0.243452" stop-color="#a9c2e5"/>
<stop offset="1" stop-color="#c2cdeb"/>
</linearGradient>
<linearGradient id="b" x1="29.258924657" x2="52.12594020815" xlink:href="#a" y1="63.99513142558" y2="103.54451872902"/>
<linearGradient id="c" gradientTransform="matrix(0.127763 0.221291 -0.221291 0.127763 96.086827 -56.897127)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="d" gradientTransform="matrix(0.127763 -0.221291 0.221291 0.127763 -54.103341 92.070101)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="e" gradientTransform="matrix(-0.255525 0 0 -0.255525 150.106936 147.648442)" x1="200.59668" x2="290.087006" xlink:href="#a" y1="351.411163" y2="506.188141"/>
<linearGradient id="f" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#636378"/>
<stop offset="0.231686" stop-color="#6a6b8f"/>
<stop offset="1" stop-color="#444465"/>
</linearGradient>
<linearGradient id="g" x1="31.62242739098" x2="54.0836659203" xlink:href="#f" y1="62.6498143668" y2="102.35286903923"/>
<linearGradient id="h" gradientTransform="matrix(-0.127763 0.221291 -0.221291 -0.127763 177.158345 261.09881)" x1="-584.199341" x2="-496.297028" xlink:href="#f" y1="782.335632" y2="937.713989"/>
<linearGradient id="i" gradientTransform="matrix(-0.127763 -0.221291 0.221291 -0.127763 -166.023458 58.751427)" x1="-584.199341" x2="-496.297028" xlink:href="#f" y1="782.335632" y2="937.713989"/>
<g fill-rule="evenodd">
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#b)"/>
<path d="m 89.023438 65.707031 l -31.222657 -54.089843 l 0.039063 -6.152344 l 22.644531 20.550781 l 19.214844 -14.296875 l -0.035157 6.152344 l -11.960937 20.566406 l 8.527344 8.625 l -0.039063 6.152344 z m 0 0" fill="#d7dded"/>
<path d="m 75.277344 44.613281 l -62.457032 -0.003906 l -0.007812 -6.140625 l 23.820312 -6.304688 l -8.320312 -14.5 l -0.007812 -6.140624 l 10.867187 -0.039063 l 11.828125 20.640625 l 24.265625 6.347656 z m 0 0" fill="#3d3846"/>
<path d="m 50.277344 46.167969 l -31.226563 54.085937 l -7.292969 -12.359375 l 0.039063 -6.152343 l 8.378906 -8.332032 l -16.714843 -0.042968 l -3.5625 -6.175782 l 0.0351558 -6.152344 l 27.3945312 -0.089843 l 22.988281 -20.933594 z m 0 0" fill="#d7dded"/>
<path d="m 52.675781 89.402344 l 0.035157 -6.152344 l 62.453124 0.003906 l -0.035156 6.152344 l -7.058594 12.496094 l -16.753906 -0.046875 l 8.359375 8.34375 l -0.039062 6.152343 l -3.566407 6.175782 l -7.289062 0.007812 l -11.832031 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="#d7dded"/>
<path d="m 31.878906 74.785156 l 38.390625 41.597656 l -0.035156 6.15625 l -14.351563 0.132813 l -8.335937 -14.53125 l -8.394531 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 0.035156 -6.152344 l 11.925781 -14.414062 l -8.492187 -14.777344 z m 0 0" fill="#3d3846"/>
<path d="m 77.710938 87.972656 l 0.011718 -6.148437 l 38.519532 -41.726563 l -0.007813 6.148438 l -8.417969 14.488281 l 20.285156 0.070313 l -0.007812 6.148437 l -3.636719 6.316406 l -23.792969 -0.074219 l -8.550781 14.742188 z m 0 0" fill="#3d3846"/>
<path d="m 50.316406 40.015625 l -31.230468 54.085937 l -7.289063 -12.359374 l 8.414063 -14.484376 l -16.714844 -0.042968 l -3.5625002 -6.175782 l 3.6367182 -6.320312 l 23.792969 0.078125 l 8.550781 -14.742187 z m 0 0" fill="url(#c)"/>
<path d="m 52.710938 83.25 l 62.453124 0.003906 l -7.054687 12.496094 l -16.753906 -0.046875 l 8.320312 14.496094 l -3.570312 6.175781 l -7.289063 0.007812 l -11.828125 -20.644531 l -17.042969 -0.035156 z m 0 0" fill="url(#d)"/>
<path d="m 89.0625 59.554688 l -31.222656 -54.089844 l 14.347656 -0.136719 l 8.335938 14.535156 l 8.394531 -14.457031 l 7.132812 0.003906 l 3.648438 6.308594 l -11.960938 20.566406 l 8.492188 14.777344 z m 0 0" fill="url(#e)"/>
<path d="m 39.046875 62.296875 l 31.222656 54.085937 l -14.347656 0.136719 l -8.335937 -14.53125 l -8.398438 14.453125 l -7.128906 -0.003906 l -3.652344 -6.308594 l 11.960938 -20.566406 l -8.488282 -14.777344 z m 0 0" fill="url(#g)"/>
<path d="m 75.265625 38.472656 l -62.453125 -0.003906 l 7.058594 -12.492188 l 16.753906 0.046876 l -8.320312 -14.5 l 3.566406 -6.171876 l 7.289062 -0.007812 l 11.832032 20.640625 l 17.042968 0.035156 z m 0 0" fill="url(#h)"/>
<path d="m 77.722656 81.824219 l 31.226563 -54.085938 l 7.292969 12.359375 l -8.417969 14.484375 l 16.714843 0.046875 l 3.5625 6.175782 l -3.636718 6.316406 l -23.792969 -0.074219 l -8.550781 14.738281 z m 0 0" fill="url(#i)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,825 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
inkscape:export-ydpi="96"
inkscape:export-xdpi="96"
inkscape:export-filename="Template.png"
width="128"
height="128"
id="svg11300"
sodipodi:version="0.32"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="template.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0"
style="display:inline;enable-background:new"
viewBox="0 0 128 128"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title4162">Adwaita Icon Template</title>
<defs
id="defs3">
<linearGradient
y2="236"
x2="96"
y1="236"
x1="32"
gradientTransform="translate(604.81684,170.58641)"
gradientUnits="userSpaceOnUse"
id="linearGradient1099"
xlink:href="#linearGradient1036" />
<linearGradient
id="linearGradient1036">
<stop
id="stop1032"
offset="0"
style="stop-color:#d5d3cf;stop-opacity:1;" />
<stop
id="stop1034"
offset="1"
style="stop-color:#f6f5f4;stop-opacity:1" />
</linearGradient>
<radialGradient
r="32"
fy="-76"
fx="-244"
cy="-76"
cx="-244"
gradientTransform="matrix(0.88333331,0,0,0.88333331,-460.35018,463.11973)"
gradientUnits="userSpaceOnUse"
id="radialGradient1103"
xlink:href="#linearGradient1069" />
<linearGradient
id="linearGradient1069">
<stop
id="stop1065"
offset="0"
style="stop-color:#d5d3cf;stop-opacity:1" />
<stop
id="stop1067-1"
offset="1"
style="stop-color:#949390;stop-opacity:1" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="232"
x2="64"
y1="262.5"
x1="64"
id="linearGradient1027"
xlink:href="#linearGradient1025"
gradientTransform="translate(-470.5864,432.81685)" />
<linearGradient
id="linearGradient1025">
<stop
id="stop1021"
offset="0"
style="stop-color:#9a9996;stop-opacity:1" />
<stop
id="stop1023"
offset="1"
style="stop-color:#77767b;stop-opacity:1" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect35304-9"
is_visible="true" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1609-7">
<path
sodipodi:nodetypes="cccccc"
inkscape:connector-curvature="0"
id="path1611-5"
d="m 252,116 28,-28 v -8 h -36 v 36 z"
style="fill:#e74747;stroke:none;stroke-width:0.25px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</clipPath>
<linearGradient
y2="515.97058"
x2="282.26105"
y1="338.62445"
x1="213.95642"
gradientTransform="translate(983.36076,601.38885)"
gradientUnits="userSpaceOnUse"
id="linearGradient5855"
xlink:href="#linearGradient5960"
inkscape:collect="always" />
<linearGradient
id="linearGradient5960"
inkscape:collect="always">
<stop
id="stop5962"
offset="0"
style="stop-color:#637ddf;stop-opacity:1" />
<stop
style="stop-color:#649afa;stop-opacity:1"
offset="0.23168644"
id="stop5964" />
<stop
id="stop5966"
offset="1"
style="stop-color:#719efa;stop-opacity:1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5562"
id="linearGradient4328"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(70.650339,-1055.1511)"
x1="200.59668"
y1="351.41116"
x2="290.08701"
y2="506.18814" />
<linearGradient
inkscape:collect="always"
id="linearGradient5562">
<stop
style="stop-color:#99b2e5;stop-opacity:1;"
offset="0"
id="stop5564" />
<stop
id="stop5566"
offset="0.24345198"
style="stop-color:#a9c2e5;stop-opacity:1;" />
<stop
style="stop-color:#c2cdeb;stop-opacity:1;"
offset="1"
id="stop5568" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5053"
id="linearGradient4330"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(864.69589,-1491.3405)"
x1="-584.19934"
y1="782.33563"
x2="-496.29703"
y2="937.71399" />
<linearGradient
inkscape:collect="always"
id="linearGradient5053">
<stop
style="stop-color:#636378;stop-opacity:1;"
offset="0"
id="stop5055" />
<stop
id="stop5057"
offset="0.23168644"
style="stop-color:#6a6b8f;stop-opacity:1;" />
<stop
style="stop-color:#444465;stop-opacity:1;"
offset="1"
id="stop5059" />
</linearGradient>
</defs>
<sodipodi:namedview
stroke="#ef2929"
fill="#f57900"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="0.25490196"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.3387241"
inkscape:cx="64.880825"
inkscape:cy="82.051772"
inkscape:current-layer="layer4"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:showpageshadow="false"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
width="400px"
height="300px"
inkscape:snap-nodes="true"
inkscape:snap-bbox="true"
objecttolerance="7"
gridtolerance="12"
guidetolerance="13"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false"
showguides="false"
inkscape:guide-bbox="true"
inkscape:locked="false"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:object-nodes="true"
inkscape:bbox-nodes="true"
inkscape:snap-global="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showborder="false"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-midpoints="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-text-baseline="true"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid5883"
spacingx="2"
spacingy="2"
enabled="true"
visible="true"
empspacing="4"
originx="0"
originy="0" />
<sodipodi:guide
position="64,8"
orientation="0,1"
id="guide1073"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="12,64"
orientation="1,0"
id="guide1075"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,104"
orientation="0,1"
id="guide1099"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,128"
orientation="0,1"
id="guide993"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="104,64"
orientation="1,0"
id="guide995"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="9.2651362e-08,64"
orientation="1,0"
id="guide867"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="120,64"
orientation="1,0"
id="guide869"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,116"
orientation="0,1"
id="guide871"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<inkscape:grid
type="xygrid"
id="grid873"
spacingx="1"
spacingy="1"
empspacing="8"
color="#000000"
opacity="0.49019608"
empcolor="#000000"
empopacity="0.08627451"
dotted="true" />
<sodipodi:guide
position="24,64"
orientation="1,0"
id="guide877"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="116,64"
orientation="1,0"
id="guide879"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,120"
orientation="0,1"
id="guide881"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,12"
orientation="0,1"
id="guide883"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="8,64"
orientation="1,0"
id="guide885"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="128,64"
orientation="1,0"
id="guide887"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,0"
orientation="0,1"
id="guide897"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,24"
orientation="0,1"
id="guide899"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="256,256"
orientation="-0.70710678,0.70710678"
id="guide950"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,64"
orientation="0.70710678,0.70710678"
id="guide952"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>GNOME Design Team</dc:title>
</cc:Agent>
</dc:creator>
<dc:source />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
<dc:title>Adwaita Icon Template</dc:title>
<dc:subject>
<rdf:Bag />
</dc:subject>
<dc:date />
<dc:rights>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:relation />
<dc:language />
<dc:coverage />
<dc:description />
<dc:contributor>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="App Icon"
inkscape:groupmode="layer"
style="display:inline"
transform="translate(0,-172)">
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="baseplate"
style="display:inline"
sodipodi:insensitive="true">
<g
style="display:inline;fill:#000000;enable-background:new"
transform="matrix(7.9911709,0,0,8.0036407,-167.7909,-4846.0776)"
id="g12027"
inkscape:export-xdpi="12"
inkscape:export-ydpi="12" />
<rect
style="display:inline;overflow:visible;visibility:visible;fill:#f0f0f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;enable-background:accumulate"
id="rect13805"
width="128"
height="128"
x="9.2651362e-08"
y="172"
inkscape:label="512x512" />
<g
id="g883"
style="fill:none;fill-opacity:0.25098039;stroke:#a579b3;stroke-opacity:1"
transform="translate(-24,24)" />
<g
id="g900"
style="fill:none;fill-opacity:0.25098039;stroke:#a579b3;stroke-opacity:1"
transform="translate(-24,24)" />
<rect
inkscape:label="512x512"
y="172"
x="160"
height="16"
width="16"
id="rect859"
style="display:inline;overflow:visible;visibility:visible;fill:#f0f0f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;enable-background:accumulate" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4px;line-height:125%;font-family:Cantarell;-inkscape-font-specification:'Cantarell, Bold';text-align:start;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.332649;enable-background:new"
x="0"
y="164"
id="text863"
inkscape:label="icon-name"><tspan
style="font-size:4px;stroke-width:0.332649"
sodipodi:role="line"
id="tspan861"
x="0"
y="164">Hicolor</tspan></text>
<text
inkscape:label="icon-name"
id="text867"
y="164"
x="160"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4px;line-height:125%;font-family:Cantarell;-inkscape-font-specification:'Cantarell, Bold';text-align:start;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.332649;enable-background:new"
xml:space="preserve"><tspan
y="164"
x="160"
id="tspan865"
sodipodi:role="line"
style="font-size:4px;stroke-width:0.332649">Symbolic</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer9"
inkscape:label="icons"
style="display:inline" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="hairlines"
style="display:inline"
sodipodi:insensitive="true">
<circle
cx="64.000031"
cy="236"
r="59.504131"
id="circle2892"
style="display:inline;opacity:0.1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.99000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.99000001, 0.99000001;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<rect
ry="7.9292889"
rx="8.701004"
y="180.49496"
x="20.495007"
height="111.01005"
width="87.009987"
id="rect2894"
style="display:inline;opacity:0.1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.99000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.99000001, 0.99000001;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<rect
ry="7.9238095"
rx="7.9238095"
y="184.49524"
x="12.495266"
height="103.00952"
width="103.00952"
id="rect2896"
style="display:inline;opacity:0.1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.99000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.99000001, 0.99000001;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<rect
ry="8.701005"
rx="7.9292889"
y="200.49496"
x="8.4950066"
height="87.010048"
width="111.01004"
id="rect2898"
style="display:inline;opacity:0.1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.99000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.99000001, 0.99000001;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<path
inkscape:connector-curvature="0"
id="path2900"
d="M 2.6203015e-5,288.99999 H 128.00003"
style="display:inline;fill:none;stroke:#62a0ea;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="devicon"
style="display:inline;enable-background:new"
transform="matrix(0.23955471,0,0,0.23955471,31.935221,217.79694)">
<g
inkscape:groupmode="layer"
id="layer7"
inkscape:label="bg"
style="display:none"
transform="matrix(1.0666667,0,0,1.0666667,-159.08108,-195.28677)">
<rect
transform="translate(-132.5822,958.04022)"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect5389"
width="1543.4283"
height="483.7439"
x="132.5822"
y="-957.77832" />
</g>
<g
inkscape:groupmode="layer"
id="layer6"
inkscape:label="logo-guide"
style="display:none"
transform="matrix(1.0666667,0,0,1.0666667,-300.5021,826.62283)">
<rect
y="-958.02759"
x="132.65129"
height="484.30399"
width="550.41602"
id="rect5379"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5c201e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
inkscape:export-filename="/home/tim/dev/nix/homepage/logo/nix-wiki.png"
inkscape:export-xdpi="22.07"
inkscape:export-ydpi="22.07" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c24a46;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect5372"
width="501.94415"
height="434.30405"
x="156.12303"
y="-933.02759"
inkscape:export-filename="/home/tim/dev/nix/homepage/logo/nixos-logo-only-hires-print.png"
inkscape:export-xdpi="212.2"
inkscape:export-ydpi="212.2" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d98d8a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect5381"
width="24.939611"
height="24.939611"
x="658.02826"
y="-958.04022" />
</g>
<path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="path3336-6"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
style="display:inline;enable-background:new;opacity:1;fill:url(#linearGradient4328);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:label="path3336-6"
transform="matrix(1.0666667,0,0,1.0666667,-300.5021,826.62282)" />
<g
inkscape:label="print-logo"
inkscape:groupmode="layer"
id="layer1-5"
style="display:inline"
transform="matrix(1.0666667,0,0,1.0666667,-300.50356,852.16062)"
sodipodi:insensitive="true">
<path
style="color:#000000;clip-rule:nonzero;display:none;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 309.40365,-710.2521 122.34347,187.59727 -0.14664,24.07783 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 0.14664,-24.07783 46.66383,-56.41237 -33.22946,-57.8256 0.14664,-24.07773 z"
id="path4861"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d7dded;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 505.14318,-720.9886 -122.19683,-211.6751 0.14663,-24.07782 88.63403,80.42022 75.19631,-55.94252 -0.14663,24.07782 -46.81047,80.4902 33.37609,33.74778 -0.14663,24.07782 z"
id="use4867"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc"
transform="translate(3.5325942e-7,-1.2250463e-5)" />
<path
sodipodi:nodetypes="cccccccccc"
inkscape:connector-curvature="0"
id="use4875"
d="m 451.3364,-803.53264 -244.4144,-0.012 -0.0331,-24.02908 93.21726,-24.68602 -32.55875,-56.73717 -0.033,-24.02908 42.52119,-0.16093 46.3013,80.78414 94.96643,24.84107 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
transform="translate(-1.7899967e-6)" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d7dded;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 353.50926,-797.4433 -122.21756,211.6631 -28.53477,-48.37 0.14666,-24.07787 32.79173,-32.60963 -65.41521,-0.1719 -13.9414,-24.1698 0.14638,-24.07767 107.20176,-0.34943 89.96911,-81.91465 z"
id="use4863"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d7dded;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 362.88537,-628.243 0.14658,-24.07778 244.41444,0.012 -0.14663,24.07778 -27.62229,48.8968 -65.56199,-0.1817 32.70539,32.6594 -0.14663,24.0777 -13.96098,24.1585 -28.52722,0.032 -46.3013,-80.7841 -66.69317,-0.1353 z"
id="use4865"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
<path
sodipodi:nodetypes="cccccccccccc"
inkscape:connector-curvature="0"
id="path4873"
d="m 281.49779,-685.44833 150.24933,162.7935 -0.14664,24.07783 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 0.14664,-24.07783 46.66383,-56.41237 -33.22946,-57.8256 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
sodipodi:nodetypes="ccccccccccc"
inkscape:connector-curvature="0"
id="path3633"
d="m 460.87178,-633.8425 0.0347,-24.06524 150.75237,-163.29308 -0.0349,24.06531 -32.93839,56.68751 79.39183,0.27618 -0.0353,24.06542 -14.23636,24.7211 -93.11177,-0.294 -33.46371,57.6904 z"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#3d3846;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<g
id="layer2-3"
inkscape:label="guides"
style="display:none"
transform="translate(72.039038,-1799.4476)">
<path
d="M 460.60629,594.72881 209.74183,594.7288 84.309616,377.4738 209.74185,160.21882 l 250.86446,1e-5 125.43222,217.255 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="1.5707963"
sodipodi:arg1="1.0471976"
sodipodi:r2="217.25499"
sodipodi:r1="250.86446"
sodipodi:cy="377.47382"
sodipodi:cx="335.17407"
sodipodi:sides="6"
id="path6032"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.236;fill:#4e4d52;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
sodipodi:type="star" />
<path
transform="translate(0,-308.26772)"
sodipodi:type="star"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#4e4d52;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
id="path5875"
sodipodi:sides="6"
sodipodi:cx="335.17407"
sodipodi:cy="685.74158"
sodipodi:r1="100.83495"
sodipodi:r2="87.32563"
sodipodi:arg1="1.0471976"
sodipodi:arg2="1.5707963"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 385.59154,773.06721 -100.83495,0 -50.41747,-87.32564 50.41748,-87.32563 100.83495,10e-6 50.41748,87.32563 z" />
<path
transform="translate(0,-308.26772)"
sodipodi:nodetypes="ccccccccc"
inkscape:connector-curvature="0"
id="path5851"
d="m 1216.5591,938.53395 123.0545,228.14035 -42.6807,-1.2616 -43.4823,-79.7725 -39.6506,80.3267 -32.6875,-19.7984 53.4737,-100.2848 -37.1157,-73.88955 z"
style="fill:url(#linearGradient5855);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.415;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c53a3a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect5884"
width="48.834862"
height="226.22897"
x="-34.74221"
y="446.17056"
transform="rotate(-30)" />
<path
transform="translate(0,-308.26772)"
sodipodi:type="star"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.509;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="path3428"
sodipodi:sides="6"
sodipodi:cx="223.93674"
sodipodi:cy="878.63831"
sodipodi:r1="28.048939"
sodipodi:r2="24.291094"
sodipodi:arg1="0"
sodipodi:arg2="0.52359878"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 251.98568,878.63831 -14.02447,24.29109 h -28.04894 l -14.02447,-24.29109 14.02447,-24.2911 h 28.04894 z" />
<use
x="0"
y="0"
xlink:href="#rect5884"
id="use4252"
transform="rotate(60,268.29786,489.4515)"
width="100%"
height="100%" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:0.650794;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect4254"
width="5.3947482"
height="115.12564"
x="545.71014"
y="467.07007"
transform="rotate(30,575.23539,-154.13386)" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3-5"
inkscape:label="gradient-logo"
style="display:inline"
transform="matrix(1.0666667,0,0,1.0666667,-300.5021,826.62283)"
sodipodi:insensitive="true">
<use
height="100%"
width="100%"
transform="matrix(0.46874999,0.81189879,-0.81189879,0.46874999,395.66027,-853.96471)"
id="use3439-6"
inkscape:transform-center-y="151.59082"
inkscape:transform-center-x="124.43045"
xlink:href="#path3336-6"
y="0"
x="0"
style="display:inline" />
<use
height="100%"
width="100%"
transform="matrix(0.46874999,-0.81189879,0.81189879,0.46874999,293.19668,-636.56447)"
id="use3445-0"
inkscape:transform-center-y="75.573958"
inkscape:transform-center-x="-168.20651"
xlink:href="#path3336-6"
y="0"
x="0"
style="display:inline" />
<use
height="100%"
width="100%"
transform="matrix(-0.93749997,0,0,-0.93749997,533.11665,-656.55412)"
id="use3449-5"
inkscape:transform-center-y="-139.94592"
inkscape:transform-center-x="59.669705"
xlink:href="#path3336-6"
y="0"
x="0"
style="display:inline" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4330);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8256 z"
id="path4260-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<use
height="100%"
width="100%"
transform="rotate(120,407.33916,-716.08356)"
id="use4354-5"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline" />
<use
height="100%"
width="100%"
transform="rotate(-120,407.28823,-715.86995)"
id="use4362-2"
xlink:href="#path4260-0"
y="0"
x="0"
style="display:inline;fill:#77767b"
inkscape:label="use4362-2"
inkscape:highlight-color="#5a3be3" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 37 KiB

10
data/icons/meson.build Normal file
View File

@ -0,0 +1,10 @@
install_data(
'@0@.svg'.format(application_id),
install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps'
)
install_data(
'@0@-symbolic.svg'.format(base_id),
install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps',
rename: '@0@-symbolic.svg'.format(application_id)
)

93
data/meson.build Normal file
View File

@ -0,0 +1,93 @@
subdir('icons')
# Desktop file
desktop_conf = configuration_data()
desktop_conf.set('icon', application_id)
desktop_file = i18n.merge_file(
type: 'desktop',
input: configure_file(
input: '@0@.desktop.in.in'.format(base_id),
output: '@BASENAME@',
configuration: desktop_conf
),
output: '@0@.desktop'.format(application_id),
po_dir: podir,
install: true,
install_dir: datadir / 'applications'
)
# Validate Desktop file
if desktop_file_validate.found()
test(
'validate-desktop',
desktop_file_validate,
args: [
desktop_file.full_path()
],
depends: desktop_file,
)
endif
# Appdata
appdata_conf = configuration_data()
appdata_conf.set('app-id', application_id)
appdata_conf.set('gettext-package', gettext_package)
appdata_file = i18n.merge_file(
input: configure_file(
input: '@0@.metainfo.xml.in.in'.format(base_id),
output: '@BASENAME@',
configuration: appdata_conf
),
output: '@0@.metainfo.xml'.format(application_id),
po_dir: podir,
install: true,
install_dir: datadir / 'metainfo'
)
# Validate Appdata
if appstream_util.found()
test(
'validate-appdata', appstream_util,
args: [
'validate', '--nonet', appdata_file.full_path()
],
depends: appdata_file,
)
endif
# Policy file
dataconf = configuration_data()
dataconf.set('pkglibexecdir',
libexecdir
)
i18n.merge_file(
input : configure_file(
configuration: dataconf,
input : '@0@.policy.in.in'.format(base_id),
output: '@BASENAME@'
),
output: '@0@.policy'.format(base_id),
po_dir: podir,
install: true,
install_dir: datadir / 'polkit-1' / 'actions'
)
# GSchema
gschema_conf = configuration_data()
gschema_conf.set('app-id', application_id)
gschema_conf.set('gettext-package', gettext_package)
configure_file(
input: '@0@.gschema.xml.in'.format(base_id),
output: '@0@.gschema.xml'.format(application_id),
configuration: gschema_conf,
install: true,
install_dir: datadir / 'glib-2.0' / 'schemas'
)
# Validata GSchema
if glib_compile_schemas.found()
test(
'validate-gschema', glib_compile_schemas,
args: [
'--strict', '--dry-run', meson.current_build_dir()
],
)
endif

71
default.nix Normal file
View File

@ -0,0 +1,71 @@
{
pkgs ? import <nixpkgs> {},
lib ? import <nixpkgs/lib>,
}:
let
libadwaita-git = pkgs.libadwaita.overrideAttrs (oldAttrs: rec {
version = "1.2.beta";
src = pkgs.fetchFromGitLab {
domain = "gitlab.gnome.org";
owner = "GNOME";
repo = "libadwaita";
rev = version;
hash = "sha256-QBblkeNAgfHi5YQxaV9ceqNDyDIGu8d6pvLcT6apm6o=";
};
});
nixos-appstream-data = (import (pkgs.fetchFromGitHub {
owner = "vlinkz";
repo = "nixos-appstream-data";
rev = "66b3399e6d81017c10265611a151d1109ff1af1b";
hash = "sha256-oiEZD4sMpb2djxReg99GUo0RHWAehxSyQBbiz8Z4DJk=";
}) {stdenv = pkgs.stdenv; lib = pkgs.lib; pkgs = pkgs; });
in pkgs.stdenv.mkDerivation rec {
pname = "nix-software-center";
version = "0.0.1";
src = [ ./. ];
cargoDeps = pkgs.rustPlatform.fetchCargoTarball {
inherit src;
name = "${pname}-${version}";
hash = "sha256-EI9zULrlN+GvtDO0PvtAEA1YjJAbK+SDZ8NSRZf+2Rw=";
};
nativeBuildInputs = with pkgs; [
appstream-glib
polkit
gettext
desktop-file-utils
meson
ninja
pkg-config
git
wrapGAppsHook4
] ++ (with pkgs.rustPlatform; [
cargoSetupHook
rust.cargo
rust.rustc
]);
buildInputs = with pkgs; [
gdk-pixbuf
glib
gtk4
gtksourceview5
libadwaita-git
openssl
wayland
gnome.adwaita-icon-theme
desktop-file-utils
nixos-appstream-data
];
# mesonFlags = [
# "-Dprofile=development"
# ];
patchPhase = ''
substituteInPlace ./src/lib.rs \
--replace "/usr/share/app-info" "${nixos-appstream-data}/share/app-info"
'';
}

109
flake.lock Normal file
View File

@ -0,0 +1,109 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"mach-nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"pypi-deps-db": "pypi-deps-db"
},
"locked": {
"lastModified": 1661359648,
"narHash": "sha256-BRz30Xg/g535oRJA3xEcXf0KM6GTJPugt2lgaom3D6g=",
"owner": "DavHau",
"repo": "mach-nix",
"rev": "6cd3929b1561c3eef68f5fc6a08b57cf95c41ec1",
"type": "github"
},
"original": {
"id": "mach-nix",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1643805626,
"narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "554d2d8aa25b6e583575459c297ec23750adb6cb",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1661361016,
"narHash": "sha256-Bjf6ZDnDc6glTwIIItvwfcaeJ5zWFM6GYfPajSArdUY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b784c5ae63dd288375af1b4d37b8a27dd8061887",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pypi-deps-db": {
"flake": false,
"locked": {
"lastModified": 1661155889,
"narHash": "sha256-t00mBTZhmZBT4jteO6pJbU0wyRS6/ep4pKmQNeztEms=",
"owner": "DavHau",
"repo": "pypi-deps-db",
"rev": "49c620f3de2b557c9d5c44f58a00fee59f27d1b0",
"type": "github"
},
"original": {
"owner": "DavHau",
"repo": "pypi-deps-db",
"type": "github"
}
},
"root": {
"inputs": {
"mach-nix": "mach-nix",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

71
flake.nix Normal file
View File

@ -0,0 +1,71 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils, mach-nix }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
libadwaita-git = pkgs.libadwaita.overrideAttrs (oldAttrs: rec {
version = "1.2.beta";
src = pkgs.fetchFromGitLab {
domain = "gitlab.gnome.org";
owner = "GNOME";
repo = "libadwaita";
rev = version;
hash = "sha256-QBblkeNAgfHi5YQxaV9ceqNDyDIGu8d6pvLcT6apm6o=";
};
});
nixos-appstream-data = pkgs.fetchFromGitHub {
owner = "vlinkz";
repo = "nixos-appstream-data";
rev = "66b3399e6d81017c10265611a151d1109ff1af1b";
hash = "sha256-oiEZD4sMpb2djxReg99GUo0RHWAehxSyQBbiz8Z4DJk=";
};
name = "nix-software-center";
in
rec
{
packages.${name} = pkgs.callPackage ./default.nix {
inherit (inputs);
};
# `nix build`
defaultPackage = packages.${name};
# `nix run`
apps.${name} = utils.lib.mkApp {
inherit name;
drv = packages.${name};
};
defaultApp = packages.${name};
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
cargo
clippy
rust-analyzer
rustc
rustfmt
cairo
gdk-pixbuf
gobject-introspection
graphene
gtk4
gtksourceview5
libadwaita-git
openssl
pandoc
pango
pkgconfig
wrapGAppsHook4
nixos-appstream-data
];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
};
});
}

70
meson.build Normal file
View File

@ -0,0 +1,70 @@
project(
'nix-software-center',
'rust',
version: '0.0.1',
meson_version: '>= 0.59',
license: 'MIT',
)
i18n = import('i18n')
gnome = import('gnome')
base_id = 'dev.vlinkz.NixSoftwareCenter'
dependency('openssl', version: '>= 1.0')
dependency('glib-2.0', version: '>= 2.66')
dependency('gio-2.0', version: '>= 2.66')
dependency('gtk4', version: '>= 4.0.0')
dependency('libadwaita-1', version: '>=1.2.alpha')
dependency('polkit-gobject-1', version: '>= 0.103')
glib_compile_resources = find_program('glib-compile-resources', required: true)
glib_compile_schemas = find_program('glib-compile-schemas', required: true)
desktop_file_validate = find_program('desktop-file-validate', required: false)
appstream_util = find_program('appstream-util', required: false)
cargo = find_program('cargo', required: true)
version = meson.project_version()
prefix = get_option('prefix')
bindir = prefix / get_option('bindir')
libexecdir = prefix / get_option('libexecdir')
localedir = prefix / get_option('localedir')
datadir = prefix / get_option('datadir')
pkgdatadir = datadir / meson.project_name()
iconsdir = datadir / 'icons'
podir = meson.project_source_root() / 'po'
gettext_package = meson.project_name()
if get_option('profile') == 'development'
profile = 'Devel'
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip()
if vcs_tag == ''
version_suffix = '-devel'
else
version_suffix = '-@0@'.format(vcs_tag)
endif
application_id = '@0@.@1@'.format(base_id, profile)
else
profile = ''
version_suffix = ''
application_id = base_id
endif
meson.add_dist_script(
'build-aux/dist-vendor.sh',
meson.project_build_root() / 'meson-dist' / meson.project_name() + '-' + version,
meson.project_source_root()
)
subdir('data')
subdir('po')
subdir('src')
subdir('nsc-helper/src')
gnome.post_install(
gtk_update_icon_cache: true,
glib_compile_schemas: true,
update_desktop_database: true,
)

9
meson_options.txt Normal file
View File

@ -0,0 +1,9 @@
option(
'profile',
type: 'combo',
choices: [
'default',
'development'
],
value: 'default',
)

2
nsc-helper/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/.vscode

12
nsc-helper/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "nsc-helper"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "3.2", features = ["derive"] }
users = "0.11"
[[bin]]
name = "nsc-helper"
path = "src/main.rs"

134
nsc-helper/src/main.rs Normal file
View File

@ -0,0 +1,134 @@
use clap::{self, FromArgMatches, Subcommand};
use std::{
error::Error,
fs::{File, self},
io::{self, Read, Write},
process::Command,
};
#[derive(clap::Subcommand)]
enum SubCommands {
Config {
/// Write stdin to file in path output
#[clap(short, long)]
output: String,
/// Run `nixos-rebuild` with the given arguments
arguments: Vec<String>,
},
Rebuild {
/// Run `nixos-rebuild` with the given arguments
arguments: Vec<String>,
},
Channel {
/// Whether to rebuild the system after updating channels
#[clap(short, long)]
rebuild: bool,
/// Run `nixos-rebuild` with the given arguments
arguments: Vec<String>,
}
}
fn main() {
let cli = SubCommands::augment_subcommands(clap::Command::new(
"Helper binary for Nix Software Center",
));
let matches = cli.get_matches();
let derived_subcommands = SubCommands::from_arg_matches(&matches)
.map_err(|err| err.exit())
.unwrap();
if users::get_effective_uid() != 0 {
eprintln!("nsc-helper must be run as root");
std::process::exit(1);
}
match derived_subcommands {
SubCommands::Config { output, arguments } => {
let old = fs::read_to_string(&output);
match write_file(&output) {
Ok(_) => match rebuild(arguments) {
Ok(_) => {}
Err(err) => {
eprintln!("{}", err);
if let Ok(o) = old {
if fs::write(&output, o).is_err() {
eprintln!("Could not restore old file");
}
}
std::process::exit(1);
}
},
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
};
}
SubCommands::Rebuild { arguments } => match rebuild(arguments) {
Ok(_) => (),
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
},
SubCommands::Channel { rebuild: dorebuild, arguments } => {
match dorebuild {
true => match rebuild(arguments) {
Ok(_) => (),
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
},
false => match channel() {
Ok(_) => (),
Err(err) => {
eprintln!("{}", err);
std::process::exit(1);
}
},
}
}
}
}
fn write_file(path: &str) -> Result<(), Box<dyn Error>> {
let stdin = io::stdin();
let mut buf = String::new();
stdin.lock().read_to_string(&mut buf)?;
let mut file = File::create(path)?;
write!(file, "{}", &buf)?;
Ok(())
}
fn rebuild(args: Vec<String>) -> Result<(), Box<dyn Error>> {
let mut cmd = Command::new("nixos-rebuild")
.args(args)
.spawn()?;
let x = cmd.wait()?;
if x.success() {
Ok(())
} else {
eprintln!("nixos-rebuild failed with exit code {}", x.code().unwrap());
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"nixos-rebuild failed",
)))
}
}
fn channel() -> Result<(), Box<dyn Error>> {
let mut cmd = Command::new("nix-channel")
.arg("--update")
.spawn()?;
let x = cmd.wait()?;
if x.success() {
Ok(())
} else {
eprintln!("nixos-rebuild failed with exit code {}", x.code().unwrap());
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"nix-channel failed",
)))
}
}

View File

@ -0,0 +1,32 @@
global_conf = configuration_data()
cargo_options = [ '--manifest-path', meson.project_source_root() / 'nsc-helper' / 'Cargo.toml' ]
cargo_options += [ '--target-dir', meson.project_build_root() / 'nsc-helper' / 'src' ]
if get_option('profile') == 'default'
cargo_options += [ '--release' ]
rust_target = 'release'
message('Building in release mode')
else
rust_target = 'debug'
message('Building in debug mode')
endif
cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'nsc-helper' / 'cargo-home' ]
cargo_build = custom_target(
'cargo-build',
build_by_default: true,
build_always_stale: true,
output: 'nsc-helper',
console: true,
install: true,
install_dir: get_option('libexecdir'),
command: [
'env',
cargo_env,
cargo, 'build',
cargo_options,
'&&',
'cp', 'src' / rust_target / 'nsc-helper', '@OUTPUT@',
]
)

0
po/LINGUAS Normal file
View File

4
po/POTFILES.in Normal file
View File

@ -0,0 +1,4 @@
data/dev.vlinkz.NixSoftwareCenter.policy.in.in
data/dev.vlinkz.NixSoftwareCenter.desktop.in.in
data/dev.vlinkz.NixSoftwareCenter.metainfo.xml.in.in
data/dev.vlinkz.NixSoftwareCenter.metainfo.gschema.xml.in

1
po/meson.build Normal file
View File

@ -0,0 +1 @@
i18n.gettext(gettext_package, preset: 'glib')

7
src/config.rs.in Normal file
View File

@ -0,0 +1,7 @@
pub const APP_ID: &str = @APP_ID@;
pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;
pub const LOCALEDIR: &str = @LOCALEDIR@;
pub const PKGDATADIR: &str = @PKGDATADIR@;
pub const PROFILE: &str = @PROFILE@;
pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource");
pub const VERSION: &str = @VERSION@;

4
src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod ui;
pub mod parse;
pub mod config;
static APPINFO: &str = "/usr/share/app-info";

8
src/main.rs Normal file
View File

@ -0,0 +1,8 @@
use nix_software_center::ui::window::AppModel;
use relm4::*;
fn main() {
pretty_env_logger::init();
let app = RelmApp::new("dev.vlinkz.NixSoftwareCenter");
let application = app.app.clone();
app.run::<AppModel>(application);
}

51
src/meson.build Normal file
View File

@ -0,0 +1,51 @@
global_conf = configuration_data()
global_conf.set_quoted('APP_ID', application_id)
global_conf.set_quoted('PKGDATADIR', pkgdatadir)
global_conf.set_quoted('PROFILE', profile)
global_conf.set_quoted('VERSION', version + version_suffix)
global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package)
global_conf.set_quoted('LOCALEDIR', localedir)
config = configure_file(
input: 'config.rs.in',
output: 'config.rs',
configuration: global_conf
)
# Copy the config.rs output to the source directory.
run_command(
'cp',
meson.project_build_root() / 'src' / 'config.rs',
meson.project_source_root() / 'src' / 'config.rs',
check: true
)
cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
if get_option('profile') == 'default'
cargo_options += [ '--release' ]
rust_target = 'release'
message('Building in release mode')
else
rust_target = 'debug'
message('Building in debug mode')
endif
cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
cargo_build = custom_target(
'cargo-build',
build_by_default: true,
build_always_stale: true,
output: meson.project_name(),
console: true,
install: true,
install_dir: bindir,
command: [
'env',
cargo_env,
cargo, 'build',
cargo_options,
'&&',
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
]
)

263
src/parse/cache.rs Normal file
View File

@ -0,0 +1,263 @@
use ijson::IString;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
env,
error::Error,
fs::{self, File},
io::{BufReader, Read, Write},
path::Path,
process::Command,
};
#[derive(Serialize, Deserialize, Debug)]
struct NewPackageBase {
packages: HashMap<String, NewPackage>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct NewPackage {
version: IString,
}
pub fn checkcache() -> Result<(), Box<dyn Error>> {
setuppkgscache()?;
setupupdatecache()?;
setupnewestver()?;
Ok(())
}
pub fn uptodate() -> Result<Option<(String, String)>, Box<dyn Error>> {
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
let oldversion = fs::read_to_string(format!("{}/sysver.txt", cachedir))?
.trim()
.to_string();
let newversion = fs::read_to_string(format!("{}/chnver.txt", cachedir))?
.trim()
.to_string();
if oldversion == newversion {
println!("System is up to date");
Ok(None)
} else {
println!("OLD {:?} != NEW {:?}", oldversion, newversion);
Ok(Some((oldversion, newversion)))
}
}
pub fn channelver() -> Result<Option<(String, String)>, Box<dyn Error>> {
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
let oldversion = fs::read_to_string(format!("{}/chnver.txt", cachedir))?
.trim()
.to_string();
let newversion = fs::read_to_string(format!("{}/newver.txt", cachedir))?
.trim()
.to_string();
if oldversion == newversion {
println!("Channels match");
Ok(None)
} else {
println!("chnver {:?} != newver {:?}", oldversion, newversion);
Ok(Some((oldversion, newversion)))
}
}
fn setuppkgscache() -> Result<(), Box<dyn Error>> {
let vout = Command::new("nix-instantiate")
.arg("-I")
.arg("nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos")
.arg("<nixpkgs/lib>")
.arg("-A")
.arg("version")
.arg("--eval")
.arg("--json")
.output()?;
let dlver = String::from_utf8_lossy(&vout.stdout)
.to_string()
.replace('"', "");
let mut relver = dlver.split('.').collect::<Vec<&str>>().join(".")[0..5].to_string();
if dlver.len() >= 8 && &dlver[5..8] == "pre" {
relver = "unstable".to_string();
}
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
let url = format!(
"https://releases.nixos.org/nixos/{}/nixos-{}/packages.json.br",
relver, dlver
);
println!("VERSION {}", relver);
// let response = reqwest::blocking::get(url)?;
// if let Some(latest) = response.url().to_string().split('/').last() {
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
if !Path::new(&cachedir).exists() {
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
}
if !Path::new(&format!("{}/chnver.txt", &cachedir)).exists() {
let mut sysver = fs::File::create(format!("{}/chnver.txt", &cachedir))?;
sysver.write_all(dlver.as_bytes())?;
}
if Path::new(format!("{}/chnver.txt", &cachedir).as_str()).exists()
&& fs::read_to_string(&Path::new(format!("{}/chnver.txt", &cachedir).as_str()))? == dlver
&& Path::new(format!("{}/packages.json", &cachedir).as_str()).exists()
{
return Ok(());
} else {
let oldver = fs::read_to_string(&Path::new(format!("{}/chnver.txt", &cachedir).as_str()))?;
let sysver = &dlver;
// Change to debug msg
println!("OLD: {}, != NEW: {}", oldver, sysver);
}
if Path::new(format!("{}/chnver.txt", &cachedir).as_str()).exists() {
fs::remove_file(format!("{}/chnver.txt", &cachedir).as_str())?;
}
let mut sysver = fs::File::create(format!("{}/chnver.txt", &cachedir))?;
sysver.write_all(dlver.as_bytes())?;
let outfile = format!("{}/packages.json", &cachedir);
dlfile(&url, &outfile)?;
// }
Ok(())
}
fn setupupdatecache() -> Result<(), Box<dyn Error>> {
let dlver = fs::read_to_string("/run/current-system/nixos-version")?;
let mut relver = dlver.split('.').collect::<Vec<&str>>().join(".")[0..5].to_string();
if dlver.len() >= 8 && &dlver[5..8] == "pre" {
relver = "unstable".to_string();
}
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
let url = format!(
"https://releases.nixos.org/nixos/{}/nixos-{}/packages.json.br",
relver, dlver
);
println!("VERSION {}", relver);
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
if !Path::new(&cachedir).exists() {
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
}
if !Path::new(&format!("{}/sysver.txt", &cachedir)).exists() {
let mut sysver = fs::File::create(format!("{}/sysver.txt", &cachedir))?;
sysver.write_all(dlver.as_bytes())?;
}
if Path::new(format!("{}/sysver.txt", &cachedir).as_str()).exists()
&& fs::read_to_string(&Path::new(format!("{}/sysver.txt", &cachedir).as_str()))? == dlver
&& Path::new(format!("{}/syspackages.json", &cachedir).as_str()).exists()
{
return Ok(());
} else {
let oldver = fs::read_to_string(&Path::new(format!("{}/sysver.txt", &cachedir).as_str()))?;
let sysver = &dlver;
// Change to debug msg
println!("OLD: {}, != NEW: {}", oldver, sysver);
}
if Path::new(format!("{}/sysver.txt", &cachedir).as_str()).exists() {
fs::remove_file(format!("{}/sysver.txt", &cachedir).as_str())?;
}
let mut sysver = fs::File::create(format!("{}/sysver.txt", &cachedir))?;
sysver.write_all(dlver.as_bytes())?;
let outfile = format!("{}/syspackages.json", &cachedir);
dlfile(&url, &outfile)?;
let file = File::open(&outfile)?;
let reader = BufReader::new(file);
let pkgbase: NewPackageBase = simd_json::serde::from_reader(reader).unwrap();
let mut outbase = HashMap::new();
for (pkg, ver) in pkgbase.packages {
outbase.insert(pkg.clone(), ver.version.clone());
}
let out = simd_json::serde::to_string(&outbase)?;
fs::write(&outfile, out)?;
Ok(())
}
fn setupnewestver() -> Result<(), Box<dyn Error>> {
let version = fs::read_to_string("/run/current-system/nixos-version")?;
let mut relver = version.split('.').collect::<Vec<&str>>().join(".")[0..5].to_string();
if version.len() >= 8 && &version[5..8] == "pre" {
relver = "unstable".to_string();
}
println!("VERSION {}", relver);
let response = reqwest::blocking::get(format!("https://channels.nixos.org/nixos-{}", relver))?;
if let Some(latest) = response.url().to_string().split('/').last() {
let latest = latest.strip_prefix("nixos-").unwrap_or(latest);
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
if !Path::new(&cachedir).exists() {
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
}
if !Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists() {
let mut newver = fs::File::create(format!("{}/newver.txt", &cachedir))?;
newver.write_all(latest.as_bytes())?;
}
if Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists()
&& fs::read_to_string(&Path::new(format!("{}/newver.txt", &cachedir).as_str()))?
== latest
{
return Ok(());
} else {
let oldver =
fs::read_to_string(&Path::new(format!("{}/newver.txt", &cachedir).as_str()))?;
let newver = latest;
// Change to debug msg
println!("OLD: {}, != NEW: {}", oldver, newver);
}
if Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists() {
fs::remove_file(format!("{}/newver.txt", &cachedir).as_str())?;
}
let mut newver = fs::File::create(format!("{}/newver.txt", &cachedir))?;
newver.write_all(latest.as_bytes())?;
}
Ok(())
}
fn dlfile(url: &str, path: &str) -> Result<(), Box<dyn Error>> {
println!("Downloading {}", url);
let response = reqwest::blocking::get(url)?;
if response.status().is_success() {
let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?);
if !Path::new(&cachedir).exists() {
fs::create_dir_all(&cachedir).expect("Failed to create cache directory");
}
let dst: Vec<u8> = response.bytes()?.to_vec();
{
let mut file = File::create(path)?;
let mut reader = brotli::Decompressor::new(
dst.as_slice(),
4096, // buffer size
);
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf[..]) {
Err(e) => {
if let std::io::ErrorKind::Interrupted = e.kind() {
continue;
}
return Err(Box::new(e));
}
Ok(size) => {
if size == 0 {
break;
}
file.write_all(&buf[..size])?
}
}
}
}
}
Ok(())
}

98
src/parse/config.rs Normal file
View File

@ -0,0 +1,98 @@
use std::{error::Error, env, path::Path, fs::{self, File}, io::Write};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct NscConfig {
pub systemconfig: String,
pub flake: Option<String>,
}
pub fn getconfig() -> NscConfig {
if let Ok(c) = getconfigval() {
c
} else {
NscConfig {
systemconfig: String::from("/etc/nixos/configuration.nix"),
flake: None,
}
}
}
fn getconfigval() -> Result<NscConfig, Box<dyn Error>> {
let configfile = checkconfig()?;
let config: NscConfig = serde_json::from_reader(File::open(format!("{}/config.json", configfile))?)?;
Ok(config)
}
fn checkconfig() -> Result<String, Box<dyn Error>> {
let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?);
if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) {
if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) {
createdefaultconfig()?;
Ok(cfgdir)
} else {
Ok("/etc/nix-software-center/".to_string())
}
} else {
Ok(cfgdir)
}
}
// fn configexists() -> Result<bool, Box<dyn Error>> {
// let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?);
// if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) {
// if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) {
// Ok(false)
// } else {
// Ok(true)
// }
// } else {
// Ok(true)
// }
// }
pub fn editconfig(config: NscConfig) -> Result<(), Box<dyn Error>> {
let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?);
fs::create_dir_all(&cfgdir)?;
let json = serde_json::to_string_pretty(&config)?;
let mut file = File::create(format!("{}/config.json", cfgdir))?;
file.write_all(json.as_bytes())?;
Ok(())
}
fn createdefaultconfig() -> Result<(), Box<dyn Error>> {
let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?);
fs::create_dir_all(&cfgdir)?;
let config = NscConfig {
systemconfig: "/etc/nixos/configuration.nix".to_string(),
flake: None,
};
let json = serde_json::to_string_pretty(&config)?;
let mut file = File::create(format!("{}/config.json", cfgdir))?;
file.write_all(json.as_bytes())?;
Ok(())
}
// pub fn readconfig(cfg: String) -> Result<(String, Option<String>), Box<dyn Error>> {
// let file = fs::read_to_string(cfg)?;
// let config: NscConfig = match serde_json::from_str(&file) {
// Ok(x) => x,
// Err(_) => {
// createdefaultconfig()?;
// return Ok((
// "/etc/nixos/configuration.nix".to_string(),
// None,
// ));
// }
// };
// if Path::is_file(Path::new(&config.systemconfig)) {
// Ok((config.systemconfig, config.flake))
// } else {
// Ok((
// "/etc/nixos/configuration.nix".to_string(),
// None,
// ))
// }
// }

3
src/parse/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod cache;
pub mod packages;
pub mod config;

190
src/parse/packages.rs Normal file
View File

@ -0,0 +1,190 @@
use flate2::bufread::GzDecoder;
use ijson::IString;
use serde::{Deserialize, Serialize};
use std::io::Read;
use std::{self, fs::File, collections::HashMap, error::Error, env, io::BufReader};
use crate::APPINFO;
#[derive(Serialize, Deserialize, Debug)]
pub struct PackageBase {
packages: HashMap<String, Package>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Package {
pub system: IString,
pub pname: IString,
pub meta: Meta,
pub version: IString,
#[serde(skip_deserializing)]
pub appdata: Option<AppData>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Meta {
pub broken: Option<bool>,
pub insecure: Option<bool>,
pub unsupported: Option<bool>,
pub unfree: Option<bool>,
pub description: Option<IString>,
#[serde(rename = "longDescription")]
pub longdescription: Option<IString>,
pub homepage: Option<StrOrVec>,
pub maintainers: Option<Vec<PkgMaintainer>>,
pub position: Option<IString>,
pub license: Option<LicenseEnum>,
pub platforms: Option<Platform>
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
#[serde(untagged)]
pub enum StrOrVec {
Single(IString),
List(Vec<IString>),
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
#[serde(untagged)]
pub enum Platform {
Single(IString),
List(Vec<IString>),
ListList(Vec<Vec<IString>>),
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
#[serde(untagged)]
pub enum LicenseEnum {
Single(License),
List(Vec<License>),
SingleStr(IString),
VecStr(Vec<IString>),
Mixed(Vec<LicenseEnum>)
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct License {
pub free: Option<bool>,
#[serde(rename = "fullName")]
pub fullname: Option<IString>,
#[serde(rename = "spdxId")]
pub spdxid: Option<IString>,
pub url: Option<IString>,
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct PkgMaintainer {
pub email: IString,
pub github: IString,
pub matrix: Option<IString>,
pub name: Option<IString>
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct AppData {
#[serde(rename = "Type")]
pub metatype: IString,
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Package")]
pub package: String,
#[serde(rename = "Name")]
pub name: Option<HashMap<String, String>>,
#[serde(rename = "Description")]
pub description: Option<HashMap<String, String>>,
#[serde(rename = "Summary")]
pub summary: Option<HashMap<String, String>>,
#[serde(rename = "Url")]
pub url: Option<AppUrl>,
#[serde(rename = "Icon")]
pub icon: Option<AppIconList>,
#[serde(rename = "Launchable")]
pub launchable: Option<AppLaunchable>,
#[serde(rename = "Provides")]
pub provides: Option<AppProvides>,
#[serde(rename = "Screenshots")]
pub screenshots: Option<Vec<AppScreenshot>>,
#[serde(rename = "Categories")]
pub categories: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppUrl {
pub homepage: Option<String>,
pub bugtracker: Option<String>,
pub help: Option<String>,
pub donation: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppIconList {
pub cached: Option<Vec<AppIcon>>,
pub stock: Option<String>,
// TODO: add support for other icon types
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct AppIcon {
pub name: String,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppLaunchable {
#[serde(rename = "desktop-id")]
pub desktopid: Vec<String>
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppProvides {
pub binaries: Option<Vec<String>>,
pub ids: Option<Vec<String>>,
pub mediatypes: Option<Vec<String>>,
pub libraries: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppScreenshot {
pub default: Option<bool>,
pub thumbnails: Option<Vec<String>>,
#[serde(rename = "source-image")]
pub sourceimage: Option<AppScreenshotImage>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AppScreenshotImage {
pub url: String,
}
pub async fn readpkgs() -> Result<HashMap<String, Package>, Box<dyn Error + Send + Sync>> {
let cachedir = format!("{}/.cache/nix-software-center/", env::var("HOME")?);
let cachefile = format!("{}/packages.json", cachedir);
let file = File::open(cachefile).unwrap();
let reader = BufReader::new(file);
let pkgbase: PackageBase = simd_json::serde::from_reader(reader).unwrap();
let mut pkgs = pkgbase.packages;
println!("APPDATADIR {}", APPINFO);
let appdata = File::open(&format!("{}/xmls/nixos_x86_64_linux.yml.gz", APPINFO)).unwrap();
let appreader = BufReader::new(appdata);
let mut d = GzDecoder::new(appreader);
let mut s = String::new();
d.read_to_string(&mut s).unwrap();
let mut files = s.split("\n---\n").collect::<Vec<_>>();
files.remove(0);
for f in files {
let appstream: AppData = serde_yaml::from_str(f).unwrap();
if let Some(p) = pkgs.get_mut(&appstream.package.to_string()) {
p.appdata = Some(appstream);
}
}
Ok(pkgs)
}
pub fn readsyspkgs() -> Result<HashMap<String, String>, Box<dyn Error + Send + Sync>> {
let cachedir = format!("{}/.cache/nix-software-center/", env::var("HOME")?);
let cachefile = format!("{}/syspackages.json", cachedir);
let file = File::open(cachefile)?;
let reader = BufReader::new(file);
let newpkgs: HashMap<String, String> = simd_json::serde::from_reader(reader).unwrap();
Ok(newpkgs)
}

64
src/ui/about.rs Normal file
View File

@ -0,0 +1,64 @@
use adw::prelude::*;
use relm4::*;
use crate::config;
use super::window::AppMsg;
#[derive(Debug)]
pub struct AboutPageModel {
hidden: bool,
}
#[derive(Debug)]
pub enum AboutPageMsg {
Show,
}
#[relm4::component(pub)]
impl SimpleComponent for AboutPageModel {
type InitParams = gtk::Window;
type Input = AboutPageMsg;
type Output = AppMsg;
type Widgets = AboutPageWidgets;
view! {
adw::AboutWindow {
#[watch]
set_visible: !model.hidden,
set_transient_for: Some(&parent_window),
set_modal: true,
set_application_name: "Nix Software Center",
set_application_icon: config::APP_ID,
set_developer_name: "Victor Fuentes",
set_version: env!("CARGO_PKG_VERSION"),
set_issue_url: "https://github.com/vlinkz/nix-software-center/issues",
set_license_type: gtk::License::MitX11,
set_website: "https://github.com/vlinkz/nix-software-center",
set_developers: &["Victor Fuentes https://github.com/vlinkz"],
}
}
fn init(
parent_window: Self::InitParams,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = AboutPageModel {
hidden: true,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
AboutPageMsg::Show => {
self.hidden = false;
}
}
}
}

105
src/ui/categories.rs Normal file
View File

@ -0,0 +1,105 @@
use relm4::adw::prelude::*;
use relm4::gtk::pango;
use relm4::{factory::*, *};
use strum_macros::{EnumIter, Display};
use super::window::AppMsg;
#[derive(Debug, PartialEq)]
pub struct PkgGroup {
pub category: PkgCategory,
}
#[derive(Debug, Display, Hash, EnumIter, Eq, PartialEq, Clone)]
pub enum PkgCategory {
Audio,
Development,
Games,
Graphics,
Network,
Video,
}
#[derive(Debug)]
pub enum PkgCategoryMsg {
Open(PkgCategory),
}
#[relm4::factory(pub)]
impl FactoryComponent for PkgGroup {
type CommandOutput = ();
type Init = PkgCategory;
type Input = ();
type Output = PkgCategoryMsg;
type Widgets = PkgGroupWidgets;
type ParentWidget = gtk::FlowBox;
type ParentMsg = AppMsg;
view! {
gtk::FlowBoxChild {
set_width_request: 210,
set_height_request: 70,
gtk::Button {
add_css_class: "card",
gtk::Box {
set_margin_start: 15,
set_margin_end: 15,
set_margin_top: 10,
set_margin_bottom: 10,
set_spacing: 10,
set_halign: gtk::Align::Center,
gtk::Image {
add_css_class: "icon-dropshadow",
set_icon_name: match self.category {
PkgCategory::Audio => Some("audio-x-generic"),
PkgCategory::Development => Some("computer"),
PkgCategory::Games => Some("input-gaming"),
PkgCategory::Graphics => Some("image-x-generic"),
PkgCategory::Network => Some("network-server"),
PkgCategory::Video => Some("video-x-generic"),
},
set_pixel_size: 32,
},
gtk::Label {
add_css_class: "title-2",
set_valign: gtk::Align::Center,
set_hexpand: true,
set_label: match self.category {
PkgCategory::Audio => "Audio",
PkgCategory::Development => "Development",
PkgCategory::Games => "Games",
PkgCategory::Graphics => "Graphics",
PkgCategory::Network => "Network",
PkgCategory::Video => "Video",
},
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
}
},
connect_clicked[sender, category = self.category.clone()] => move |_| {
println!("CLICKED {}", category);
sender.output(PkgCategoryMsg::Open(category.clone()));
}
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
Self {
category: parent,
}
}
fn output_to_parent_msg(output: Self::Output) -> Option<AppMsg> {
Some(match output {
PkgCategoryMsg::Open(x) => AppMsg::OpenCategoryPage(x),
})
}
}

204
src/ui/categorypage.rs Normal file
View File

@ -0,0 +1,204 @@
use super::{window::*, categories::PkgCategory, categorytile::CategoryTile};
use adw::prelude::*;
use relm4::{factory::*, *};
#[tracker::track]
#[derive(Debug)]
pub struct CategoryPageModel {
category: PkgCategory,
#[tracker::no_eq]
recommendedapps: FactoryVecDeque<CategoryTile>,
#[tracker::no_eq]
apps: FactoryVecDeque<CategoryTile>,
busy: bool,
}
#[derive(Debug)]
pub enum CategoryPageMsg {
Close,
OpenPkg(String),
Open(PkgCategory, Vec<CategoryTile>, Vec<CategoryTile>),
Loading(PkgCategory),
UpdateInstalled(Vec<String>, Vec<String>)
}
#[relm4::component(pub)]
impl SimpleComponent for CategoryPageModel {
type InitParams = ();
type Input = CategoryPageMsg;
type Output = AppMsg;
type Widgets = CategoryPageWidgets;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
adw::HeaderBar {
pack_start = &gtk::Button {
add_css_class: "flat",
gtk::Image {
set_icon_name: Some("go-previous-symbolic"),
},
connect_clicked[sender] => move |_| {
sender.input(CategoryPageMsg::Close)
},
},
#[wrap(Some)]
set_title_widget = &gtk::Label {
#[watch]
set_label: &model.category.to_string(),
},
},
gtk::ScrolledWindow {
set_vexpand: true,
set_hexpand: true,
set_hscrollbar_policy: gtk::PolicyType::Never,
set_vscrollbar_policy: gtk::PolicyType::Automatic,
#[track(model.changed(CategoryPageModel::category()))]
set_vadjustment: gtk::Adjustment::NONE,
adw::Clamp {
set_maximum_size: 1000,
set_tightening_threshold: 750,
if model.busy {
gtk::Spinner {
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
set_spinning: true,
set_height_request: 32,
}
} else {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Start,
set_margin_all: 15,
set_spacing: 15,
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "Recommended",
},
#[local_ref]
recbox -> gtk::FlowBox {
set_halign: gtk::Align::Fill,
set_hexpand: true,
set_valign: gtk::Align::Center,
set_orientation: gtk::Orientation::Horizontal,
set_selection_mode: gtk::SelectionMode::None,
set_homogeneous: true,
set_max_children_per_line: 3,
set_min_children_per_line: 1,
set_column_spacing: 14,
set_row_spacing: 14,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "Other",
},
#[local_ref]
allbox -> gtk::FlowBox {
set_halign: gtk::Align::Fill,
set_hexpand: true,
set_valign: gtk::Align::Center,
set_orientation: gtk::Orientation::Horizontal,
set_selection_mode: gtk::SelectionMode::None,
set_homogeneous: true,
set_max_children_per_line: 3,
set_min_children_per_line: 1,
set_column_spacing: 14,
set_row_spacing: 14,
}
}
}
}
}
}
}
fn init(
(): Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = CategoryPageModel {
category: PkgCategory::Audio,
recommendedapps: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input),
apps: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input),
busy: true,
tracker: 0
};
let recbox = model.recommendedapps.widget();
let allbox = model.apps.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match msg {
CategoryPageMsg::Close => {
let mut recapps_guard = self.recommendedapps.guard();
let mut apps_guard = self.apps.guard();
recapps_guard.clear();
apps_guard.clear();
sender.output(AppMsg::FrontFrontPage)
}
CategoryPageMsg::OpenPkg(pkg) => {
sender.output(AppMsg::OpenPkg(pkg))
}
CategoryPageMsg::Open(category, catrec, catall) => {
println!("CategoryPageMsg::Open");
self.set_category(category);
let mut recapps_guard = self.recommendedapps.guard();
recapps_guard.clear();
for app in catrec {
recapps_guard.push_back(app);
}
let mut apps_guard = self.apps.guard();
apps_guard.clear();
for app in catall {
apps_guard.push_back(app);
}
self.busy = false;
}
CategoryPageMsg::Loading(category) => {
println!("CATEGORY PAGE LOADING {}", category);
self.set_category(category);
self.busy = true;
}
CategoryPageMsg::UpdateInstalled(installeduserpkgs, installedsystempkgs) => {
let mut recapps_guard = self.recommendedapps.guard();
for i in 0..recapps_guard.len() {
let app = recapps_guard.get_mut(i).unwrap();
if installeduserpkgs.contains(&app.pname) {
app.installeduser = true;
} else {
app.installeduser = false;
}
if installedsystempkgs.contains(&app.pkg) {
app.installedsystem = true;
} else {
app.installedsystem = false;
}
}
let mut apps_guard = self.apps.guard();
for i in 0..apps_guard.len() {
let app = apps_guard.get_mut(i).unwrap();
if installeduserpkgs.contains(&app.pname) {
app.installeduser = true;
} else {
app.installeduser = false;
}
if installedsystempkgs.contains(&app.pkg) {
app.installedsystem = true;
} else {
app.installedsystem = false;
}
}
}
}
}
}

185
src/ui/categorytile.rs Normal file
View File

@ -0,0 +1,185 @@
use std::path::Path;
use crate::APPINFO;
use super::categorypage::CategoryPageMsg;
use relm4::adw::prelude::*;
use relm4::gtk::pango;
use relm4::{factory::*, *};
#[derive(Default, Debug, PartialEq, Clone)]
pub struct CategoryTile {
pub name: String,
pub pkg: String,
pub pname: String,
pub summary: Option<String>,
pub icon: Option<String>,
pub installeduser: bool,
pub installedsystem: bool,
}
#[derive(Debug)]
pub enum CategoryTileMsg {
Open(String),
}
#[relm4::factory(pub)]
impl FactoryComponent for CategoryTile {
type CommandOutput = ();
type Init = CategoryTile;
type Input = ();
type Output = CategoryTileMsg;
type Widgets = CategoryTileWidgets;
type ParentWidget = gtk::FlowBox;
type ParentMsg = CategoryPageMsg;
view! {
gtk::FlowBoxChild {
set_width_request: 270,
gtk::Overlay {
add_overlay = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
gtk::Image {
add_css_class: "accent",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_pixel_size: 16,
set_margin_top: 8,
set_margin_end: 8,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.installeduser,
},
gtk::Image {
add_css_class: "success",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_pixel_size: 16,
set_margin_top: 8,
set_margin_end: 8,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.installedsystem,
}
},
gtk::Button {
add_css_class: "card",
connect_clicked[sender, pkg = self.pkg.clone()] => move |_| {
println!("CLICKED");
sender.output(CategoryTileMsg::Open(pkg.to_string()))
},
set_can_focus: false,
gtk::Box {
set_margin_start: 15,
set_margin_end: 15,
set_margin_top: 10,
set_margin_bottom: 10,
set_spacing: 20,
append = if self.icon.is_some() {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_from_file: {
if let Some(i) = &self.icon {
let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i);
let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i);
if Path::new(&iconpath).is_file() {
Some(iconpath)
} else if Path::new(&iconpath64).is_file() {
Some(iconpath64)
} else {
None
}
} else {
None
}
},
set_pixel_size: 64,
}
} else {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("package-x-generic"),
set_pixel_size: 64,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 3,
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "heading",
set_label: &self.name,
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: &self.pkg,
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
// add_css_class: "dim-label",
#[watch]
set_visible: self.summary.is_some(),
set_label: &(if let Some(s) = &self.summary { s.to_string() } else { String::default() }),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 2,
set_wrap: true,
set_max_width_chars: 0,
}
}
}
}
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
let mut sum = parent.summary;
sum = sum.map(|mut s| {
s.trim().to_string();
while s.contains('\n') {
s = s.replace('\n', " ");
}
while s.contains(" ") {
s = s.replace(" ", " ");
}
s
});
Self {
name: parent.name,
pkg: parent.pkg,
pname: parent.pname,
summary: sum,
icon: parent.icon,
installeduser: parent.installeduser,
installedsystem: parent.installedsystem,
}
}
fn output_to_parent_msg(output: Self::Output) -> Option<CategoryPageMsg> {
Some(match output {
CategoryTileMsg::Open(x) => CategoryPageMsg::OpenPkg(x),
})
}
}

1
src/ui/installdialog.rs Normal file
View File

@ -0,0 +1 @@
// TODO add a dialog where user can view `nix-env` and `nixos-rebuild` output

378
src/ui/installedpage.rs Normal file
View File

@ -0,0 +1,378 @@
use std::path::Path;
use crate::APPINFO;
use super::{window::*, pkgpage::{InstallType, WorkPkg, PkgAction, NotifyPage}};
use adw::prelude::*;
use relm4::{factory::*, *, gtk::pango};
#[tracker::track]
#[derive(Debug)]
pub struct InstalledPageModel {
#[tracker::no_eq]
installeduserlist: FactoryVecDeque<InstalledItemModel>,
#[tracker::no_eq]
installedsystemlist: FactoryVecDeque<InstalledItemModel>,
updatetracker: u8,
}
#[derive(Debug)]
pub enum InstalledPageMsg {
Update(Vec<InstalledItem>, Vec<InstalledItem>),
OpenRow(usize, InstallType),
Remove(InstalledItem),
UnsetBusy(WorkPkg),
}
#[relm4::component(pub)]
impl SimpleComponent for InstalledPageModel {
type InitParams = ();
type Input = InstalledPageMsg;
type Output = AppMsg;
type Widgets = InstalledPageWidgets;
view! {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
#[track(model.changed(InstalledPageModel::updatetracker()))]
set_vadjustment: gtk::Adjustment::NONE,
adw::Clamp {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Start,
set_margin_all: 15,
set_spacing: 15,
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "User (nix-env)",
},
#[local_ref]
installeduserlist -> gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |listbox, row| {
if let Some(i) = listbox.index_of_child(row) {
sender.input(InstalledPageMsg::OpenRow(i as usize, InstallType::User))
}
// sender.input(InstalledPageMsg::OpenRow(row.clone(), InstallType::User));
}
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "System (configuration.nix)",
},
#[local_ref]
installedsystemlist -> gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |listbox, row| {
if let Some(i) = listbox.index_of_child(row) {
sender.input(InstalledPageMsg::OpenRow(i as usize, InstallType::System))
}
// sender.input(InstalledPageMsg::OpenRow(row.clone(), InstallType::System));
}
}
}
}
}
}
fn init(
(): Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = InstalledPageModel {
installeduserlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input),
installedsystemlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input),
updatetracker: 0,
tracker: 0
};
let installeduserlist = model.installeduserlist.widget();
let installedsystemlist = model.installedsystemlist.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match msg {
InstalledPageMsg::Update(installeduserlist, installedsystemlist) => {
self.update_updatetracker(|_| ());
let mut installeduserlist_guard = self.installeduserlist.guard();
installeduserlist_guard.clear();
for installeduser in installeduserlist {
installeduserlist_guard.push_back(installeduser);
}
let mut installedsystemlist_guard = self.installedsystemlist.guard();
installedsystemlist_guard.clear();
for installedsystem in installedsystemlist {
installedsystemlist_guard.push_back(installedsystem);
}
}
InstalledPageMsg::OpenRow(row, pkgtype) => {
match pkgtype {
InstallType::User => {
let installeduserlist_guard = self.installeduserlist.guard();
if let Some(item) = installeduserlist_guard.get(row) {
if let Some(pkg) = &item.item.pkg {
sender.output(AppMsg::OpenPkg(pkg.to_string()));
}
}
// for (i, child) in installeduserlist_guard.widget().iter_children().enumerate() {
// if child == row {
// if let Some(item) = installeduserlist_guard.get(i) {
// if let Some(pkg) = &item.item.pkg {
// sender.output(AppMsg::OpenPkg(pkg.to_string()));
// }
// }
// }
// }
}
InstallType::System => {
let installedsystemlist_guard = self.installedsystemlist.guard();
if let Some(item) = installedsystemlist_guard.get(row) {
if let Some(pkg) = &item.item.pkg {
sender.output(AppMsg::OpenPkg(pkg.to_string()));
}
}
// for (i, child) in installedsystemlist_guard.widget().iter_children().enumerate() {
// if child == row {
// if let Some(item) = installedsystemlist_guard.get(i) {
// if let Some(pkg) = &item.item.pkg {
// sender.output(AppMsg::OpenPkg(pkg.to_string()));
// }
// }
// }
// }
}
}
}
InstalledPageMsg::Remove(item) => {
let work = WorkPkg {
pkg: item.pkg.unwrap_or_default(),
pname: item.pname,
pkgtype: item.pkgtype,
action: PkgAction::Remove,
block: false,
notify: Some(NotifyPage::Installed)
};
sender.output(AppMsg::AddInstalledToWorkQueue(work))
}
InstalledPageMsg::UnsetBusy(work) => {
match work.pkgtype {
InstallType::User => {
let mut installeduserlist_guard = self.installeduserlist.guard();
for i in 0..installeduserlist_guard.len() {
if let Some(item) = installeduserlist_guard.get_mut(i) {
if item.item.pname == work.pname && item.item.pkgtype == work.pkgtype {
item.item.busy = false;
}
}
}
}
InstallType::System => {
let mut installedsystemlist_guard = self.installedsystemlist.guard();
for i in 0..installedsystemlist_guard.len() {
if let Some(item) = installedsystemlist_guard.get_mut(i) {
if item.item.pkg == Some(work.pkg.clone()) && item.item.pkgtype == work.pkgtype {
item.item.busy = false;
}
}
}
}
}
}
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct InstalledItem {
pub name: String,
pub pkg: Option<String>,
pub pname: String,
pub summary: Option<String>,
pub icon: Option<String>,
pub pkgtype: InstallType,
pub busy: bool,
}
#[derive(Debug, PartialEq)]
pub struct InstalledItemModel {
pub item: InstalledItem,
}
#[derive(Debug)]
pub enum InstalledItemMsg {
Delete(InstalledItem),
}
#[derive(Debug)]
pub enum InstalledItemInputMsg {
Busy(bool),
}
#[relm4::factory(pub)]
impl FactoryComponent for InstalledItemModel {
type CommandOutput = ();
type Init = InstalledItem;
type Input = InstalledItemInputMsg;
type Output = InstalledItemMsg;
type Widgets = InstalledItemWidgets;
type ParentWidget = adw::gtk::ListBox;
type ParentMsg = InstalledPageMsg;
view! {
adw::PreferencesRow {
set_activatable: self.item.pkg.is_some(),
set_can_focus: false,
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_spacing: 10,
set_margin_all: 10,
adw::Bin {
set_valign: gtk::Align::Center,
#[wrap(Some)]
set_child = if self.item.icon.is_some() {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_from_file: {
if let Some(i) = &self.item.icon {
let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i);
let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i);
if Path::new(&iconpath).is_file() {
Some(iconpath)
} else if Path::new(&iconpath64).is_file() {
Some(iconpath64)
} else {
None
}
} else {
None
}
},
set_pixel_size: 64,
}
} else {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("package-x-generic"),
set_pixel_size: 64,
}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 2,
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.name.as_str(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: if let Some(p) = &self.item.pkg { p } else { &self.item.pname },
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.summary.as_deref().unwrap_or(""),
set_visible: self.item.summary.is_some(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
},
if self.item.busy {
gtk::Spinner {
set_spinning: true,
}
} else {
gtk::Button {
add_css_class: "destructive-action",
set_valign: gtk::Align::Center,
set_halign: gtk::Align::End,
set_icon_name: "user-trash-symbolic",
set_can_focus: false,
connect_clicked[sender, item = self.item.clone()] => move |_| {
sender.input(InstalledItemInputMsg::Busy(true));
sender.output(InstalledItemMsg::Delete(item.clone()))
}
}
}
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
let sum = if let Some(s) = parent.summary {
let mut sum = s.trim().to_string();
while sum.contains('\n') {
sum = sum.replace('\n', " ");
}
while sum.contains(" ") {
sum = sum.replace(" ", " ");
}
Some(sum)
} else {
None
};
let item = InstalledItem {
name: parent.name,
pkg: parent.pkg,
pname: parent.pname,
summary: sum,
icon: parent.icon,
pkgtype: parent.pkgtype,
busy: parent.busy,
};
Self {
item,
}
}
fn output_to_parent_msg(output: Self::Output) -> Option<InstalledPageMsg> {
Some(match output {
InstalledItemMsg::Delete(item) => InstalledPageMsg::Remove(item),
})
}
fn update(&mut self, msg: Self::Input, _sender: FactoryComponentSender<Self>) {
match msg {
InstalledItemInputMsg::Busy(b) => self.item.busy = b,
}
}
}

346
src/ui/installworker.rs Normal file
View File

@ -0,0 +1,346 @@
use crate::parse::config::NscConfig;
use super::pkgpage::{InstallType, PkgAction, PkgMsg, WorkPkg};
use relm4::*;
use std::error::Error;
use std::path::Path;
use std::process::Stdio;
use std::{fs, io};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
#[tracker::track]
#[derive(Debug)]
pub struct InstallAsyncHandler {
#[tracker::no_eq]
process: Option<JoinHandle<()>>,
work: Option<WorkPkg>,
systemconfig: String,
flakeargs: Option<String>,
pid: Option<u32>,
}
#[derive(Debug)]
pub enum InstallAsyncHandlerMsg {
SetConfig(NscConfig),
Process(WorkPkg),
CancelProcess,
SetPid(Option<u32>),
}
impl Worker for InstallAsyncHandler {
type InitParams = ();
type Input = InstallAsyncHandlerMsg;
type Output = PkgMsg;
fn init(_params: Self::InitParams, _sender: relm4::ComponentSender<Self>) -> Self {
Self {
process: None,
work: None,
systemconfig: String::new(),
flakeargs: None,
pid: None,
tracker: 0,
}
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match msg {
InstallAsyncHandlerMsg::SetConfig(config) => {
self.systemconfig = config.systemconfig;
self.flakeargs = config.flake;
}
InstallAsyncHandlerMsg::Process(work) => {
if work.block {
return;
}
let systemconfig = self.systemconfig.clone();
let rebuildargs = self.flakeargs.clone();
match work.pkgtype {
InstallType::User => {
match work.action {
PkgAction::Install => {
println!("Installing user package: {}", work.pkg);
self.process = Some(relm4::spawn(async move {
let mut p = tokio::process::Command::new("nix-env")
.arg("-iA")
.arg(format!("nixos.{}", work.pkg))
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to run nix-env");
let stderr = p.stderr.take().unwrap();
let reader = tokio::io::BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
println!("CAUGHT LINE: {}", line);
}
match p.wait().await {
Ok(o) => {
if o.success() {
println!(
"Removed user package: {} success",
work.pkg
);
// println!("{}", String::from_utf8_lossy(&pstdout));
sender.output(PkgMsg::FinishedProcess(work))
} else {
println!(
"Removed user package: {} failed",
work.pkg
);
// println!("{}", String::from_utf8_lossy(&p.stderr));
sender.output(PkgMsg::FailedProcess(work));
}
}
Err(e) => {
println!("Error removing user package: {}", e);
sender.output(PkgMsg::FailedProcess(work));
}
}
}));
}
PkgAction::Remove => {
println!("Removing user package: {}", work.pkg);
self.process = Some(relm4::spawn(async move {
let mut p = tokio::process::Command::new("nix-env")
.arg("-e")
.arg(&work.pname)
.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to run nix-env");
let stderr = p.stderr.take().unwrap();
let reader = tokio::io::BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
println!("CAUGHT LINE: {}", line);
}
match p.wait().await {
Ok(o) => {
if o.success() {
println!(
"Removed user package: {} success",
work.pkg
);
// println!("{}", String::from_utf8_lossy(&pstdout));
sender.output(PkgMsg::FinishedProcess(work))
} else {
println!(
"Removed user package: {} failed",
work.pkg
);
// println!("{}", String::from_utf8_lossy(&p.stderr));
sender.output(PkgMsg::FailedProcess(work));
}
}
Err(e) => {
println!("Error removing user package: {}", e);
sender.output(PkgMsg::FailedProcess(work));
}
}
}));
}
}
}
InstallType::System => match work.action {
PkgAction::Install => {
println!("Installing system package: {}", work.pkg);
self.process = Some(relm4::spawn(async move {
match installsys(
work.pkg.to_string(),
work.action.clone(),
systemconfig,
rebuildargs,
sender.clone(),
)
.await
{
Ok(b) => {
if b {
sender.output(PkgMsg::FinishedProcess(work))
} else {
sender.output(PkgMsg::FailedProcess(work))
}
}
Err(e) => {
sender.output(PkgMsg::FailedProcess(work));
println!("Error installing system package: {}", e);
}
}
}));
}
PkgAction::Remove => {
println!("Removing system package: {}", work.pkg);
self.process = Some(relm4::spawn(async move {
match installsys(
work.pkg.to_string(),
work.action.clone(),
systemconfig,
rebuildargs,
sender.clone(),
)
.await
{
Ok(b) => {
if b {
sender.output(PkgMsg::FinishedProcess(work))
} else {
sender.output(PkgMsg::FailedProcess(work))
}
}
Err(e) => {
sender.output(PkgMsg::FailedProcess(work));
println!("Error removing system package: {}", e);
}
}
}));
}
},
}
}
InstallAsyncHandlerMsg::CancelProcess => {
println!("CANCELING PROCESS");
// if let Some(p) = self.pid {
// println!("Killing process: {}", p);
// Command::new("pkexec")
// .arg("kill")
// .arg("-INT")
// .arg(p.to_string())
// .spawn()
// .expect("Failed to kill process");
// }
if let Some(p) = &mut self.process {
p.abort()
}
self.process = None;
self.pid = None;
sender.output(PkgMsg::CancelFinished);
}
InstallAsyncHandlerMsg::SetPid(p) => self.pid = p,
}
}
}
async fn installsys(
pkg: String,
action: PkgAction,
systemconfig: String,
flakeargs: Option<String>,
_sender: ComponentSender<InstallAsyncHandler>,
) -> Result<bool, Box<dyn Error>> {
let mut p = pkg;
let f = fs::read_to_string(&systemconfig)?;
if let Ok(s) = nix_editor::read::getwithvalue(&f, "environment.systemPackages") {
if !s.contains(&"pkgs".to_string()) {
p = format!("pkgs.{}", p);
}
} else {
return Err(Box::new(io::Error::new(
io::ErrorKind::InvalidData,
"Failed to write configuration.nix",
)));
}
let out = match action {
PkgAction::Install => {
match nix_editor::write::addtoarr(&f, "environment.systemPackages", vec![p]) {
Ok(x) => x,
Err(_) => {
return Err(Box::new(io::Error::new(
io::ErrorKind::InvalidData,
"Failed to write configuration.nix",
)))
}
}
}
PkgAction::Remove => {
match nix_editor::write::rmarr(&f, "environment.systemPackages", vec![p]) {
Ok(x) => x,
Err(_) => {
return Err(Box::new(io::Error::new(
io::ErrorKind::InvalidData,
"Failed to write configuration.nix",
)))
}
}
}
};
let exe = match std::env::current_exe() {
Ok(mut e) => {
e.pop(); // root/bin
// e.pop(); // root/
// e.push("libexec"); // root/libexec
e.push("nsc-helper");
let x = e.to_string_lossy().to_string();
println!("CURRENT PATH {}", x);
if Path::new(&x).is_file() {
x
} else {
String::from("nsc-helper")
}
}
Err(_) => String::from("nsc-helper"),
};
println!("EXECUTING {}", exe);
// let rebuildargs = match flakeargs {
// Some(x) => format!("--flake {}", x),//.split(' ').map(|x| x.to_string()).collect::<Vec<String>>(),
// None => String::default(),
// };
let rebuildargs = if let Some(x) = flakeargs {
let mut v = vec![String::from("--flake")];
for arg in x.split(' ') {
if !arg.is_empty() {
v.push(String::from(arg));
}
}
v
} else {
vec![]
};
println!("Rebuild args: {:?}", rebuildargs);
let mut cmd = tokio::process::Command::new("pkexec")
.arg(&exe)
.arg("config")
.arg("--output")
.arg(&systemconfig)
.arg("--")
.arg("switch")
.args(&rebuildargs)
.stderr(Stdio::piped())
.stdin(Stdio::piped())
.spawn()?;
// sender.input(InstallAsyncHandlerMsg::SetPid(cmd.id()));
cmd.stdin.take().unwrap().write_all(out.as_bytes()).await?;
println!("SENT INPUT");
let stderr = cmd.stderr.take().unwrap();
let reader = tokio::io::BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
println!("CAUGHT LINE: {}", line);
}
println!("READER DONE");
if cmd.wait().await?.success() {
println!("SUCCESS");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(true)
} else {
println!("FAILURE");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(false)
}
}

16
src/ui/mod.rs Normal file
View File

@ -0,0 +1,16 @@
pub mod window;
pub mod windowloading;
pub mod pkgtile;
pub mod categories;
pub mod pkgpage;
pub mod screenshotfactory;
pub mod installworker;
pub mod searchpage;
pub mod installedpage;
pub mod updatepage;
pub mod updatedialog;
pub mod updateworker;
pub mod about;
pub mod preferencespage;
pub mod categorypage;
pub mod categorytile;

1472
src/ui/pkgpage.rs Normal file

File diff suppressed because it is too large Load Diff

180
src/ui/pkgtile.rs Normal file
View File

@ -0,0 +1,180 @@
use std::path::Path;
use relm4::adw::prelude::*;
use relm4::gtk::pango;
use relm4::{factory::*, *};
use crate::APPINFO;
use super::window::AppMsg;
#[derive(Default, Debug, PartialEq)]
pub struct PkgTile {
pub name: String,
pub pkg: String,
pub pname: String,
pub summary: String,
pub icon: Option<String>,
pub installeduser: bool,
pub installedsystem: bool,
}
#[derive(Debug)]
pub enum PkgTileMsg {
Open(String),
}
#[relm4::factory(pub)]
impl FactoryComponent for PkgTile {
type CommandOutput = ();
type Init = PkgTile;
type Input = ();
type Output = PkgTileMsg;
type Widgets = PkgTileWidgets;
type ParentWidget = gtk::FlowBox;
type ParentMsg = AppMsg;
view! {
gtk::FlowBoxChild {
set_width_request: 270,
gtk::Overlay {
add_overlay = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
gtk::Image {
add_css_class: "accent",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_pixel_size: 16,
set_margin_top: 8,
set_margin_end: 8,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.installeduser,
},
gtk::Image {
add_css_class: "success",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_pixel_size: 16,
set_margin_top: 8,
set_margin_end: 8,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.installedsystem,
}
},
gtk::Button {
add_css_class: "card",
connect_clicked[sender, pkg = self.pkg.clone()] => move |_| {
println!("CLICKED");
sender.output(PkgTileMsg::Open(pkg.to_string()))
},
gtk::Box {
set_margin_start: 15,
set_margin_end: 15,
set_margin_top: 10,
set_margin_bottom: 10,
set_spacing: 20,
append = if self.icon.is_some() {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_from_file: {
if let Some(i) = &self.icon {
let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i);
let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i);
if Path::new(&iconpath).is_file() {
Some(iconpath)
} else if Path::new(&iconpath64).is_file() {
Some(iconpath64)
} else {
None
}
} else {
None
}
},
set_pixel_size: 64,
}
} else {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("package-x-generic"),
set_pixel_size: 64,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 3,
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "heading",
set_label: &self.name,
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: &self.pkg,
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
// add_css_class: "dim-label",
set_label: &self.summary,
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 2,
set_wrap: true,
set_max_width_chars: 0,
}
}
}
}
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
let mut sum = parent.summary.trim().to_string();
while sum.contains('\n') {
sum = sum.replace('\n', " ");
}
while sum.contains(" ") {
sum = sum.replace(" ", " ");
}
Self {
name: parent.name,
pkg: parent.pkg,
pname: parent.pname,
summary: sum,
icon: parent.icon,
installeduser: parent.installeduser,
installedsystem: parent.installedsystem,
}
}
fn output_to_parent_msg(output: Self::Output) -> Option<AppMsg> {
Some(match output {
PkgTileMsg::Open(x) => AppMsg::OpenPkg(x),
})
}
}

236
src/ui/preferencespage.rs Normal file
View File

@ -0,0 +1,236 @@
use std::path::PathBuf;
use super::window::AppMsg;
use adw::prelude::*;
use relm4::*;
use relm4_components::open_dialog::*;
#[derive(Debug)]
pub struct PreferencesPageModel {
hidden: bool,
configpath: PathBuf,
flake: Option<(PathBuf, String)>,
open_dialog: Controller<OpenDialog>,
flake_file_dialog: Controller<OpenDialog>,
}
#[derive(Debug)]
pub enum PreferencesPageMsg {
Show(PathBuf, Option<(PathBuf, String)>),
Open,
OpenFlake,
SetConfigPath(PathBuf),
SetFlake(Option<(PathBuf, String)>),
SetFlakePath(PathBuf),
SetFlakeArg(String),
ModifyFlake,
Ignore,
}
#[relm4::component(pub)]
impl SimpleComponent for PreferencesPageModel {
type InitParams = gtk::Window;
type Input = PreferencesPageMsg;
type Output = AppMsg;
type Widgets = PreferencesPageWidgets;
view! {
adw::PreferencesWindow {
#[watch]
set_visible: !model.hidden,
set_transient_for: Some(&parent_window),
set_modal: true,
set_search_enabled: false,
add = &adw::PreferencesPage {
add = &adw::PreferencesGroup {
// set_title: "Preferences",
add = &adw::ActionRow {
set_title: "Configuration file",
add_suffix = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_halign: gtk::Align::End,
set_valign: gtk::Align::Center,
set_spacing: 10,
gtk::Button {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
gtk::Image {
set_icon_name: Some("document-open-symbolic"),
},
gtk::Label {
#[watch]
set_label: {
let x = model.configpath.file_name().unwrap_or_default().to_str().unwrap_or_default();
if x.is_empty() {
"(None)"
} else {
x
}
}
}
},
connect_clicked[sender] => move |_| {
sender.input(PreferencesPageMsg::Open);
}
},
gtk::Button {
add_css_class: "flat",
set_icon_name: "view-refresh-symbolic",
connect_clicked[sender] => move |_| {
sender.input(PreferencesPageMsg::SetConfigPath(PathBuf::from("/etc/nixos/configuration.nix")));
}
}
}
},
add = &adw::ActionRow {
set_title: "Use nix flakes",
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center,
connect_state_set[sender] => move |_, b| {
if b {
sender.input(PreferencesPageMsg::SetFlake(Some((PathBuf::new(), String::default()))));
} else {
sender.input(PreferencesPageMsg::SetFlake(None));
}
gtk::Inhibit(false)
}
}
},
add = &adw::ActionRow {
set_title: "Flake file",
#[watch]
set_visible: model.flake.is_some(),
add_suffix = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_halign: gtk::Align::End,
set_valign: gtk::Align::Center,
set_spacing: 10,
gtk::Button {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
gtk::Image {
set_icon_name: Some("document-open-symbolic"),
},
gtk::Label {
#[watch]
set_label: {
let x = if let Some((f, _)) = &model.flake {
f.file_name().unwrap_or_default().to_str().unwrap_or_default()
} else {
""
};
if x.is_empty() {
"(None)"
} else {
x
}
}
}
},
connect_clicked[sender] => move |_| {
sender.input(PreferencesPageMsg::OpenFlake);
}
},
gtk::Button {
add_css_class: "flat",
set_icon_name: "user-trash-symbolic",
connect_clicked[sender] => move |_| {
sender.input(PreferencesPageMsg::SetFlakePath(PathBuf::new()));
}
}
}
},
add = &adw::EntryRow {
#[watch]
set_visible: model.flake.is_some(),
set_title: "Flake arguments (--flake path/to/flake.nix#<THIS ENTRY>)",
connect_changed[sender] => move |x| {
sender.input(PreferencesPageMsg::SetFlakeArg(x.text().to_string()));
}
}
}
}
}
}
fn init(
parent_window: Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let open_dialog = OpenDialog::builder()
.transient_for_native(root)
.launch(OpenDialogSettings::default())
.forward(&sender.input, |response| match response {
OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetConfigPath(path),
OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore,
});
let flake_file_dialog = OpenDialog::builder()
.transient_for_native(root)
.launch(OpenDialogSettings::default())
.forward(&sender.input, |response| match response {
OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetFlakePath(path),
OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore,
});
let model = PreferencesPageModel {
hidden: true,
configpath: PathBuf::new(),
flake: None,
open_dialog,
flake_file_dialog,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
match msg {
PreferencesPageMsg::Show(path, flake) => {
self.configpath = path;
self.flake = flake;
self.hidden = false;
println!("FLAKE {:?}", self.flake);
}
PreferencesPageMsg::Open => self.open_dialog.emit(OpenDialogMsg::Open),
PreferencesPageMsg::OpenFlake => self.flake_file_dialog.emit(OpenDialogMsg::Open),
PreferencesPageMsg::SetConfigPath(path) => {
self.configpath = path.clone();
sender.output(AppMsg::UpdateSysconfig(path.to_string_lossy().to_string()));
}
PreferencesPageMsg::SetFlake(flake) => {
self.flake = flake;
println!("FLAKE {:?}", self.flake);
sender.input(PreferencesPageMsg::ModifyFlake)
}
PreferencesPageMsg::SetFlakePath(path) => {
self.flake = Some((
path,
self.flake.as_ref().map(|x| x.1.clone()).unwrap_or_default(),
));
println!("FLAKE {:?}", self.flake);
sender.input(PreferencesPageMsg::ModifyFlake)
}
PreferencesPageMsg::SetFlakeArg(arg) => {
self.flake = Some((
self.flake.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
arg,
));
println!("FLAKE {:?}", self.flake);
sender.input(PreferencesPageMsg::ModifyFlake)
}
PreferencesPageMsg::ModifyFlake => {
let out = self
.flake
.as_ref()
.map(|(path, arg)| format!("{}#{}", path.to_string_lossy(), arg));
sender.output(AppMsg::UpdateFlake(out.map(|x| x.strip_suffix('#').unwrap_or(&x).to_string())));
}
_ => {}
}
}
}

View File

@ -0,0 +1,73 @@
use relm4::adw::prelude::*;
use relm4::{factory::*, *};
use super::pkgpage::PkgMsg;
#[derive(Default, Debug, PartialEq)]
pub struct ScreenshotItem {
pub path: Option<String>,
pub error: bool,
}
#[derive(Debug)]
pub enum ScreenshotItemMsg {}
#[relm4::factory(pub)]
impl FactoryComponent for ScreenshotItem {
type CommandOutput = ();
type Init = ();
type Input = ();
type Output = ScreenshotItemMsg;
type Widgets = PkgTileWidgets;
type ParentWidget = adw::Carousel;
type ParentMsg = PkgMsg;
view! {
gtk::Box {
set_margin_all: 15,
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Fill,
set_vexpand: true,
gtk::Picture {
#[watch]
set_visible: self.path.is_some() && !self.error,
#[watch]
set_filename: self.path.as_ref(),
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_vexpand: true,
},
gtk::Spinner {
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_vexpand: true,
#[watch]
set_visible: self.path.is_none() && !self.error,
set_spinning: true,
set_height_request: 80,
set_width_request: 80,
set_margin_all: 30,
},
gtk::Image {
add_css_class: "error",
set_pixel_size: 64,
set_icon_name: Some("dialog-error-symbolic"),
#[watch]
set_visible: self.error,
}
}
}
fn init_model(
_parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
Self {
path: None,
error: false,
}
}
}

274
src/ui/searchpage.rs Normal file
View File

@ -0,0 +1,274 @@
use std::{path::Path, collections::HashSet};
use crate::APPINFO;
use super::window::*;
use adw::prelude::*;
use relm4::{factory::*, *, gtk::pango};
#[tracker::track]
#[derive(Debug)]
pub struct SearchPageModel {
#[tracker::no_eq]
searchitems: FactoryVecDeque<SearchItemModel>,
searchitemtracker: u8,
}
#[derive(Debug)]
pub enum SearchPageMsg {
Open,
Search(Vec<SearchItem>),
UpdateInstalled(HashSet<String>, HashSet<String>),
Close,
OpenRow(gtk::ListBoxRow)
}
#[relm4::component(pub)]
impl SimpleComponent for SearchPageModel {
type InitParams = ();
type Input = SearchPageMsg;
type Output = AppMsg;
type Widgets = SearchPageWidgets;
view! {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
#[track(model.changed(SearchPageModel::searchitemtracker()))]
set_vadjustment: gtk::Adjustment::NONE,
adw::Clamp {
gtk::Stack {
set_margin_all: 20,
#[local_ref]
searchlist -> gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |_, row| {
sender.input(SearchPageMsg::OpenRow(row.clone()));
}
}
}
}
}
}
fn init(
(): Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = SearchPageModel {
searchitems: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input),
searchitemtracker: 0,
tracker: 0,
};
let searchlist = model.searchitems.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match msg {
SearchPageMsg::Search(items) => {
let mut searchitem_guard = self.searchitems.guard();
searchitem_guard.clear();
for item in items {
searchitem_guard.push_back(item);
}
searchitem_guard.drop();
self.update_searchitemtracker(|_| ());
}
SearchPageMsg::Open => {}
SearchPageMsg::Close => {}
SearchPageMsg::OpenRow(row) => {
let searchitem_guard = self.searchitems.guard();
for (i, child) in searchitem_guard.widget().iter_children().enumerate() {
if child == row {
if let Some(item) = searchitem_guard.get(i) {
let pkg = &item.get_item().pkg;
sender.output(AppMsg::OpenPkg(pkg.to_string()));
}
}
}
}
SearchPageMsg::UpdateInstalled(installeduserpkgs, installedsystempkgs) => {
let mut searchitem_guard = self.searchitems.guard();
for i in 0..searchitem_guard.len() {
if let Some(item) = searchitem_guard.get_mut(i) {
let mut pkgitem = item.get_mut_item();
pkgitem.installeduser = installeduserpkgs.contains(&pkgitem.pname.to_string());
pkgitem.installedsystem = installedsystempkgs.contains(&pkgitem.pkg.to_string());
}
}
}
}
}
}
#[derive(Default, Debug, PartialEq)]
pub struct SearchItem {
pub name: String,
pub pkg: String,
pub pname: String,
pub summary: Option<String>,
pub icon: Option<String>,
pub installeduser: bool,
pub installedsystem: bool,
}
#[tracker::track]
#[derive(Default, Debug, PartialEq)]
pub struct SearchItemModel {
pub item: SearchItem,
}
#[derive(Debug)]
pub enum SearchItemMsg {}
#[relm4::factory(pub)]
impl FactoryComponent for SearchItemModel {
type CommandOutput = ();
type Init = SearchItem;
type Input = ();
type Output = SearchItemMsg;
type Widgets = SearchItemWidgets;
type ParentWidget = adw::gtk::ListBox;
type ParentMsg = SearchPageMsg;
view! {
adw::PreferencesRow {
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_spacing: 10,
set_margin_all: 10,
adw::Bin {
set_valign: gtk::Align::Center,
#[wrap(Some)]
set_child = if self.item.icon.is_some() {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_from_file: {
if let Some(i) = &self.item.icon {
let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i);
let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i);
if Path::new(&iconpath).is_file() {
Some(iconpath)
} else if Path::new(&iconpath64).is_file() {
Some(iconpath64)
} else {
None
}
} else {
None
}
},
set_pixel_size: 64,
}
} else {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("package-x-generic"),
set_pixel_size: 64,
}
}
},
gtk::Overlay {
add_overlay = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
gtk::Image {
add_css_class: "accent",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.item.installeduser,
},
gtk::Image {
add_css_class: "success",
set_valign: gtk::Align::Start,
set_halign: gtk::Align::End,
set_icon_name: Some("emblem-default-symbolic"),
#[watch]
set_visible: self.item.installedsystem,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 2,
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.name.as_str(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: self.item.pkg.as_str(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.summary.as_deref().unwrap_or(""),
set_visible: self.item.summary.is_some(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
}
}
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
let sum = if let Some(s) = parent.summary {
let mut sum = s.trim().to_string();
while sum.contains('\n') {
sum = sum.replace('\n', " ");
}
while sum.contains(" ") {
sum = sum.replace(" ", " ");
}
Some(sum)
} else {
None
};
let item = SearchItem {
name: parent.name,
pkg: parent.pkg,
pname: parent.pname,
summary: sum,
icon: parent.icon,
installeduser: parent.installeduser,
installedsystem: parent.installedsystem,
};
Self { item, tracker: 0 }
}
}

137
src/ui/updatedialog.rs Normal file
View File

@ -0,0 +1,137 @@
use adw::prelude::*;
use relm4::*;
use super::updatepage::UpdatePageMsg;
#[derive(Debug)]
pub struct UpdateDialogModel {
hidden: bool,
message: String,
done: bool,
failed: bool,
}
#[derive(Debug)]
pub enum UpdateDialogMsg {
Show(String),
Close,
Done,
Failed,
}
#[relm4::component(pub)]
impl SimpleComponent for UpdateDialogModel {
type InitParams = gtk::Window;
type Input = UpdateDialogMsg;
type Output = UpdatePageMsg;
type Widgets = UpdateDialogWidgets;
view! {
dialog = adw::Window {
#[watch]
set_visible: !model.hidden,
set_transient_for: Some(&parent_window),
set_modal: true,
set_resizable: false,
set_default_width: 500,
set_default_height: 200,
add_css_class: "dialog",
add_css_class: "message",
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Fill,
set_hexpand: true,
set_vexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_vexpand: true,
set_margin_all: 15,
set_spacing: 10,
gtk::Label {
#[watch]
set_visible: !model.message.is_empty(),
add_css_class: "title-1",
#[watch]
set_label: &model.message,
},
if model.done {
gtk::Image {
add_css_class: "success",
set_icon_name: Some("emblem-ok-symbolic"),
set_pixel_size: 128,
}
} else if model.failed {
gtk::Image {
add_css_class: "error",
set_icon_name: Some("dialog-error-symbolic"),
set_pixel_size: 128,
}
} else {
gtk::Spinner {
#[watch]
set_visible: !model.done,
#[watch]
set_spinning: !model.done,
}
}
},
gtk::Box {
#[watch]
set_visible: model.done || model.failed,
add_css_class: "dialog-action-area",
set_valign: gtk::Align::End,
set_vexpand: true,
set_orientation: gtk::Orientation::Horizontal,
set_homogeneous: true,
gtk::Button {
set_label: "Close",
connect_clicked[sender] => move |_| {
sender.input(UpdateDialogMsg::Close);
}
}
},
}
}
}
fn init(
parent_window: Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = UpdateDialogModel {
hidden: true,
done: false,
failed: false,
message: String::default(),
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
UpdateDialogMsg::Show(desc) => {
self.message = desc;
self.hidden = false;
self.done = false;
self.failed = false;
}
UpdateDialogMsg::Close => {
self.hidden = true;
}
UpdateDialogMsg::Done => {
self.done = true;
}
UpdateDialogMsg::Failed => {
self.failed = true;
}
}
}
}

537
src/ui/updatepage.rs Normal file
View File

@ -0,0 +1,537 @@
use crate::{parse::{cache::channelver, config::{getconfig, NscConfig}}, APPINFO};
use super::{pkgpage::InstallType, window::*, updatedialog::{UpdateDialogModel, UpdateDialogMsg}, updateworker::{UpdateAsyncHandler, UpdateAsyncHandlerMsg}};
use adw::prelude::*;
use relm4::{factory::*, gtk::pango, *};
use std::{path::Path, convert::identity};
#[tracker::track]
#[derive(Debug)]
pub struct UpdatePageModel {
#[tracker::no_eq]
updateuserlist: FactoryVecDeque<UpdateItemModel>,
#[tracker::no_eq]
updatesystemlist: FactoryVecDeque<UpdateItemModel>,
channelupdate: Option<(String, String)>,
#[tracker::no_eq]
updatedialog: Controller<UpdateDialogModel>,
#[tracker::no_eq]
updateworker: WorkerController<UpdateAsyncHandler>,
config: NscConfig,
updatetracker: u8,
}
#[derive(Debug)]
pub enum UpdatePageMsg {
UpdateConfig(NscConfig),
Update(Vec<UpdateItem>, Vec<UpdateItem>),
OpenRow(usize, InstallType),
UpdateSystem,
UpdateAllUser,
UpdateUser(String),
UpdateChannels,
UpdateSystemAndChannels,
UpdateAll,
DoneWorking,
DoneLoading,
FailedWorking,
}
#[relm4::component(pub)]
impl SimpleComponent for UpdatePageModel {
type InitParams = gtk::Window;
type Input = UpdatePageMsg;
type Output = AppMsg;
type Widgets = UpdatePageWidgets;
view! {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
#[track(model.changed(UpdatePageModel::updatetracker()))]
set_vadjustment: gtk::Adjustment::NONE,
adw::Clamp {
if model.channelupdate.is_some() || !model.updateuserlist.is_empty() || !model.updatesystemlist.is_empty() {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Start,
set_margin_all: 15,
set_spacing: 15,
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-2",
set_label: "Updates",
},
gtk::Button {
add_css_class: "suggested-action",
set_halign: gtk::Align::End,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_label: "Update Everything",
connect_clicked[sender] => move |_| {
sender.input(UpdatePageMsg::UpdateAll);
}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
#[watch]
set_visible: model.channelupdate.is_some(),
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "Channels",
},
},
gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
#[watch]
set_visible: model.channelupdate.is_some(),
adw::PreferencesRow {
set_activatable: false,
set_can_focus: false,
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_spacing: 10,
set_margin_all: 10,
adw::Bin {
set_valign: gtk::Align::Center,
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("application-x-addon"),
set_pixel_size: 64,
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 2,
gtk::Label {
set_halign: gtk::Align::Start,
set_label: "nixos",
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: {
&(if let Some((old, new)) = &model.channelupdate {
format!("{}{}", old, new)
} else {
String::default()
})
},
set_visible: model.channelupdate.is_some(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_halign: gtk::Align::End,
set_valign: gtk::Align::Center,
gtk::Button {
add_css_class: "suggested-action",
set_valign: gtk::Align::Center,
set_halign: gtk::Align::End,
set_label: "Update channel and system",
set_can_focus: false,
connect_clicked[sender] => move |_| {
sender.input(UpdatePageMsg::UpdateSystemAndChannels);
}
},
gtk::Button {
set_valign: gtk::Align::Center,
set_halign: gtk::Align::End,
set_label: "Update channel only",
set_can_focus: false,
connect_clicked[sender] => move |_| {
sender.input(UpdatePageMsg::UpdateChannels);
}
},
}
}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
#[watch]
set_visible: !model.updateuserlist.is_empty(),
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "User (nix-env)",
},
gtk::Button {
add_css_class: "suggested-action",
set_halign: gtk::Align::End,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_label: "Update All",
connect_clicked[sender] => move |_| {
sender.input(UpdatePageMsg::UpdateAllUser);
}
}
},
#[local_ref]
updateuserlist -> gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |listbox, row| {
if let Some(i) = listbox.index_of_child(row) {
sender.input(UpdatePageMsg::OpenRow(i as usize, InstallType::User));
}
},
#[watch]
set_visible: !model.updateuserlist.is_empty(),
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
#[watch]
set_visible: !model.updatesystemlist.is_empty(),
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "title-4",
set_label: "System (configuration.nix)",
},
gtk::Button {
add_css_class: "suggested-action",
set_halign: gtk::Align::End,
set_hexpand: true,
set_valign: gtk::Align::Center,
set_label: "Update All",
connect_clicked[sender] => move |_|{
sender.input(UpdatePageMsg::UpdateSystem);
},
}
},
#[local_ref]
updatesystemlist -> gtk::ListBox {
set_valign: gtk::Align::Start,
add_css_class: "boxed-list",
set_selection_mode: gtk::SelectionMode::None,
connect_row_activated[sender] => move |listbox, row| {
if let Some(i) = listbox.index_of_child(row) {
sender.input(UpdatePageMsg::OpenRow(i as usize, InstallType::System));
}
},
#[watch]
set_visible: !model.updatesystemlist.is_empty(),
}
}
} else {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
set_hexpand: true,
set_vexpand: true,
set_spacing: 10,
gtk::Image {
add_css_class: "success",
set_icon_name: Some("emblem-ok-symbolic"),
set_pixel_size: 256,
},
gtk::Label {
add_css_class: "title-1",
set_label: "Everything is up to date!"
}
}
}
}
}
}
fn init(
parent_window: Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let updatedialog = UpdateDialogModel::builder()
.launch(parent_window.upcast())
.forward(sender.input_sender(), identity);
let updateworker = UpdateAsyncHandler::builder()
.detach_worker(())
.forward(sender.input_sender(), identity);
let config = getconfig();
updateworker.emit(UpdateAsyncHandlerMsg::UpdateConfig(config.clone()));
let model = UpdatePageModel {
updateuserlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input),
updatesystemlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input),
channelupdate: None,
updatetracker: 0,
updatedialog,
updateworker,
config,
tracker: 0,
};
let updateuserlist = model.updateuserlist.widget();
let updatesystemlist = model.updatesystemlist.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match msg {
UpdatePageMsg::UpdateConfig(config) => {
self.config = config;
self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateConfig(self.config.clone()));
}
UpdatePageMsg::Update(updateuserlist, updatesystemlist) => {
self.channelupdate = channelver().unwrap_or(None);
self.update_updatetracker(|_| ());
let mut updateuserlist_guard = self.updateuserlist.guard();
updateuserlist_guard.clear();
for updateuser in updateuserlist {
updateuserlist_guard.push_back(updateuser);
}
let mut updatesystemlist_guard = self.updatesystemlist.guard();
updatesystemlist_guard.clear();
for updatesystem in updatesystemlist {
updatesystemlist_guard.push_back(updatesystem);
}
}
UpdatePageMsg::OpenRow(row, pkgtype) => match pkgtype {
InstallType::User => {
let updateuserlist_guard = self.updateuserlist.guard();
if let Some(item) = updateuserlist_guard.get(row) {
if let Some(pkg) = &item.item.pkg {
sender.output(AppMsg::OpenPkg(pkg.to_string()));
}
}
}
InstallType::System => {
let updatesystemlist_guard = self.updatesystemlist.guard();
if let Some(item) = updatesystemlist_guard.get(row) {
if let Some(pkg) = &item.item.pkg {
sender.output(AppMsg::OpenPkg(pkg.to_string()));
}
}
}
},
UpdatePageMsg::UpdateChannels => {
self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating channels...")));
self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateChannels);
}
UpdatePageMsg::UpdateSystemAndChannels => {
self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating system and channels...")));
self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateChannelsAndSystem);
}
UpdatePageMsg::UpdateSystem => {
self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating system...")));
self.updateworker.emit(UpdateAsyncHandlerMsg::RebuildSystem);
}
UpdatePageMsg::UpdateUser(pkg) => {
println!("UPDATE USER PKG: {}", pkg);
eprintln!("unimplemented");
// self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating user...")));
// self.updateworker.emit(UpdateAsyncHandlerMsg::RebuildUser);
}
UpdatePageMsg::UpdateAllUser => {
self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating all user packages...")));
self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateUserPkgs);
}
UpdatePageMsg::UpdateAll => {
self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating everything...")));
self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateAll);
}
UpdatePageMsg::DoneWorking => {
sender.output(AppMsg::ReloadUpdate);
}
UpdatePageMsg::DoneLoading => {
self.updatedialog.emit(UpdateDialogMsg::Done);
}
UpdatePageMsg::FailedWorking => {
self.updatedialog.emit(UpdateDialogMsg::Failed);
}
}
}
}
#[derive(Debug, PartialEq)]
pub struct UpdateItem {
pub name: String,
pub pkg: Option<String>,
pub pname: String,
pub summary: Option<String>,
pub icon: Option<String>,
pub pkgtype: InstallType,
pub verfrom: Option<String>,
pub verto: Option<String>,
}
#[derive(Debug, PartialEq)]
pub struct UpdateItemModel {
item: UpdateItem,
}
#[derive(Debug)]
pub enum UpdateItemMsg {}
#[relm4::factory(pub)]
impl FactoryComponent for UpdateItemModel {
type CommandOutput = ();
type Init = UpdateItem;
type Input = ();
type Output = UpdateItemMsg;
type Widgets = UpdateItemWidgets;
type ParentWidget = adw::gtk::ListBox;
type ParentMsg = UpdatePageMsg;
view! {
adw::PreferencesRow {
set_activatable: self.item.pkg.is_some(),
set_can_focus: false,
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_spacing: 10,
set_margin_all: 10,
adw::Bin {
set_valign: gtk::Align::Center,
#[wrap(Some)]
set_child = if self.item.icon.is_some() {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_from_file: {
if let Some(i) = &self.item.icon {
let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i);
let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i);
if Path::new(&iconpath).is_file() {
Some(iconpath)
} else if Path::new(&iconpath64).is_file() {
Some(iconpath64)
} else {
None
}
} else {
None
}
},
set_pixel_size: 64,
}
} else {
gtk::Image {
add_css_class: "icon-dropshadow",
set_halign: gtk::Align::Start,
set_icon_name: Some("package-x-generic"),
set_pixel_size: 64,
}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Center,
set_hexpand: true,
set_spacing: 2,
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.name.as_str(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
add_css_class: "dim-label",
add_css_class: "caption",
set_label: {
&(if let Some(old) = &self.item.verfrom {
if let Some(new) = &self.item.verto {
format!("{}{}", old, new)
} else {
String::default()
}
} else {
String::default()
})
},
set_visible: self.item.verfrom.is_some() && self.item.verto.is_some(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
gtk::Label {
set_halign: gtk::Align::Start,
set_label: self.item.summary.as_deref().unwrap_or(""),
set_visible: self.item.summary.is_some(),
set_ellipsize: pango::EllipsizeMode::End,
set_lines: 1,
set_wrap: true,
set_max_width_chars: 0,
},
},
// gtk::Button {
// set_visible: self.item.pkgtype == InstallType::User,
// set_valign: gtk::Align::Center,
// set_halign: gtk::Align::End,
// set_label: "Update",
// set_can_focus: false,
// }
}
}
}
fn init_model(
parent: Self::Init,
_index: &DynamicIndex,
_sender: FactoryComponentSender<Self>,
) -> Self {
let sum = if let Some(s) = parent.summary {
let mut sum = s.trim().to_string();
while sum.contains('\n') {
sum = sum.replace('\n', " ");
}
while sum.contains(" ") {
sum = sum.replace(" ", " ");
}
Some(sum)
} else {
None
};
let item = UpdateItem {
name: parent.name,
pkg: parent.pkg,
pname: parent.pname,
summary: sum,
icon: parent.icon,
pkgtype: parent.pkgtype,
verfrom: parent.verfrom,
verto: parent.verto,
};
Self { item }
}
}

261
src/ui/updateworker.rs Normal file
View File

@ -0,0 +1,261 @@
use std::{error::Error, path::Path, process::Stdio};
use relm4::*;
use tokio::io::AsyncBufReadExt;
use crate::parse::config::NscConfig;
use super::updatepage::UpdatePageMsg;
#[tracker::track]
#[derive(Debug)]
pub struct UpdateAsyncHandler {
#[tracker::no_eq]
process: Option<JoinHandle<()>>,
systemconfig: String,
flakeargs: Option<String>,
}
#[derive(Debug)]
pub enum UpdateAsyncHandlerMsg {
UpdateConfig(NscConfig),
UpdateChannels,
UpdateChannelsAndSystem,
RebuildSystem,
UpdateUserPkgs,
UpdateAll,
}
enum NscCmd {
Rebuild,
Channel,
All,
}
impl Worker for UpdateAsyncHandler {
type InitParams = ();
type Input = UpdateAsyncHandlerMsg;
type Output = UpdatePageMsg;
fn init(_params: Self::InitParams, _sender: relm4::ComponentSender<Self>) -> Self {
Self {
process: None,
systemconfig: String::default(),
flakeargs: None,
tracker: 0,
}
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
match msg {
UpdateAsyncHandlerMsg::UpdateConfig(config) => {
self.systemconfig = config.systemconfig;
self.flakeargs = config.flake;
}
UpdateAsyncHandlerMsg::UpdateChannels => {
let systenconfig = self.systemconfig.clone();
let flakeargs = self.flakeargs.clone();
relm4::spawn(async move {
println!("STARTED");
let result = runcmd(NscCmd::Channel, systenconfig, flakeargs).await;
match result {
Ok(true) => {
println!("CHANNEL DONE");
sender.output(UpdatePageMsg::DoneWorking);
}
_ => {
println!("CHANNEL FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
});
}
UpdateAsyncHandlerMsg::UpdateChannelsAndSystem => {
let systenconfig = self.systemconfig.clone();
let flakeargs = self.flakeargs.clone();
relm4::spawn(async move {
println!("STARTED");
let result = runcmd(NscCmd::All, systenconfig, flakeargs).await;
match result {
Ok(true) => {
println!("ALL DONE");
sender.output(UpdatePageMsg::DoneWorking);
}
_ => {
println!("ALL FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
});
}
UpdateAsyncHandlerMsg::RebuildSystem => {
let systenconfig = self.systemconfig.clone();
let flakeargs = self.flakeargs.clone();
relm4::spawn(async move {
println!("STARTED");
let result = runcmd(NscCmd::Rebuild, systenconfig, flakeargs).await;
match result {
Ok(true) => {
println!("REBUILD DONE");
sender.output(UpdatePageMsg::DoneWorking);
}
_ => {
println!("REBUILD FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
});
}
UpdateAsyncHandlerMsg::UpdateUserPkgs => {
relm4::spawn(async move {
println!("STARTED");
let result = updateenv().await;
match result {
Ok(true) => {
println!("USER DONE");
sender.output(UpdatePageMsg::DoneWorking);
}
_ => {
println!("USER FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
});
}
UpdateAsyncHandlerMsg::UpdateAll => {
let systenconfig = self.systemconfig.clone();
let flakeargs = self.flakeargs.clone();
relm4::spawn(async move {
println!("STARTED");
let result = runcmd(NscCmd::All, systenconfig, flakeargs).await;
match result {
Ok(true) => {
println!("ALL pkexec DONE");
match updateenv().await {
Ok(true) => {
println!("ALL DONE");
sender.output(UpdatePageMsg::DoneWorking);
}
_ => {
println!("ALL FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
}
_ => {
println!("ALL FAILED");
sender.output(UpdatePageMsg::FailedWorking);
}
}
});
}
}
}
}
async fn runcmd(
cmd: NscCmd,
_systemconfig: String,
flakeargs: Option<String>,
) -> Result<bool, Box<dyn Error + Send + Sync>> {
let exe = match std::env::current_exe() {
Ok(mut e) => {
e.pop(); // root/bin
// e.pop(); // root/
// e.push("libexec"); // root/libexec
e.push("nsc-helper");
let x = e.to_string_lossy().to_string();
println!("CURRENT PATH {}", x);
if Path::new(&x).is_file() {
x
} else {
String::from("nsc-helper")
}
}
Err(_) => String::from("nsc-helper"),
};
let rebuildargs = if let Some(x) = flakeargs {
let mut v = vec![String::from("--flake")];
for arg in x.split(' ') {
if !arg.is_empty() {
v.push(String::from(arg));
}
}
v
} else {
vec![]
};
let mut cmd = match cmd {
NscCmd::Rebuild => tokio::process::Command::new("pkexec")
.arg(&exe)
.arg("rebuild")
.arg("--")
.arg("switch")
.args(&rebuildargs)
.stderr(Stdio::piped())
.spawn()?,
NscCmd::Channel => tokio::process::Command::new("pkexec")
.arg(&exe)
.arg("channel")
.stderr(Stdio::piped())
.spawn()?,
NscCmd::All => tokio::process::Command::new("pkexec")
.arg(&exe)
.arg("channel")
.arg("--rebuild")
.arg("--")
.arg("switch")
.args(&rebuildargs)
.stderr(Stdio::piped())
.spawn()?,
};
println!("SENT INPUT");
let stderr = cmd.stderr.take().unwrap();
let reader = tokio::io::BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
println!("CAUGHT REBUILD LINE: {}", line);
}
println!("READER DONE");
if cmd.wait().await?.success() {
println!("SUCCESS");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(true)
} else {
println!("FAILURE");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(false)
}
}
async fn updateenv() -> Result<bool, Box<dyn Error + Send + Sync>> {
let mut cmd = tokio::process::Command::new("nix-env")
.arg("-u")
.stderr(Stdio::piped())
.spawn()?;
println!("SENT INPUT");
let stderr = cmd.stderr.take().unwrap();
let reader = tokio::io::BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
println!("CAUGHT NIXENV LINE: {}", line);
}
println!("READER DONE");
if cmd.wait().await?.success() {
println!("SUCCESS");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(true)
} else {
println!("FAILURE");
// sender.input(InstallAsyncHandlerMsg::SetPid(None));
Ok(false)
}
}

1236
src/ui/window.rs Normal file

File diff suppressed because it is too large Load Diff

481
src/ui/windowloading.rs Normal file
View File

@ -0,0 +1,481 @@
use super::window::AppMsg;
use crate::parse::cache::checkcache;
use crate::parse::packages::readpkgs;
use crate::parse::packages::readsyspkgs;
use crate::parse::packages::Package;
use crate::ui::categories::PkgCategory;
use rand::prelude::SliceRandom;
use rand::thread_rng;
use relm4::adw::prelude::*;
use relm4::*;
use std::{collections::HashMap, env};
use strum::IntoEnumIterator;
pub struct WindowAsyncHandler;
#[derive(Debug)]
pub enum WindowAsyncHandlerMsg {
CheckCache(CacheReturn),
}
#[derive(Debug, PartialEq)]
pub enum CacheReturn {
Init,
Update,
}
impl Worker for WindowAsyncHandler {
type InitParams = ();
type Input = WindowAsyncHandlerMsg;
type Output = AppMsg;
fn init(_params: Self::InitParams, _sender: relm4::ComponentSender<Self>) -> Self {
Self
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
match msg {
WindowAsyncHandlerMsg::CheckCache(cr) => {
println!("CHECK CACHE");
relm4::spawn(async move {
match checkcache() {
Ok(_) => {}
Err(_) => {
println!("FAILED TO CHECK CACHE");
sender.output(AppMsg::LoadError(
String::from("Could not load cache"),
String::from(
"Try connecting to the internet or launching the application again",
),
));
return;
}
}
let pkgs = match readpkgs().await {
Ok(pkgs) => pkgs,
Err(_) => {
println!("FAILED TO LOAD PKGS");
sender.output(AppMsg::LoadError(
String::from("Could not load packages"),
String::from(
"Try connecting to the internet or launching the application again",
),
));
return;
}
};
let newpkgs = match readsyspkgs() {
Ok(newpkgs) => newpkgs,
Err(_) => {
println!("FAILED TO LOAD NEW PKGS");
sender.output(AppMsg::LoadError(
String::from("Could not load new packages"),
String::from(
"Try connecting to the internet or launching the application again",
),
));
return;
}
};
println!("GOT PKGS");
let mut recpicks = vec![];
let mut catpicks: HashMap<PkgCategory, Vec<String>> = HashMap::new();
let mut catpkgs: HashMap<PkgCategory, Vec<String>> = HashMap::new();
if cr == CacheReturn::Init {
println!("INIT");
let desktopenv = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let appdatapkgs = pkgs
.iter()
.filter(|(x, _)| {
if let Some(p) = pkgs.get(*x) {
if let Some(data) = &p.appdata {
(if let Some(i) = &data.icon {
i.cached.is_some()
} else {
false
}) && data.description.is_some()
&& data.name.is_some()
&& data.launchable.is_some()
&& data.screenshots.is_some()
&& (!x.starts_with("gnome.") || desktopenv == "GNOME")
&& (!x.starts_with("xfce.") || desktopenv == "XFCE")
&& (!x.starts_with("mate.") || desktopenv == "MATE")
&& (!x.starts_with("cinnamon.")
|| desktopenv == "X-Cinnamon")
&& (!x.starts_with("libsForQt5") || desktopenv == "KDE")
&& (!x.starts_with("pantheon.")
|| desktopenv == "Pantheon")
} else {
false
}
} else {
false
}
})
.collect::<HashMap<_, _>>();
let mut recommendedpkgs = appdatapkgs
.keys()
.map(|x| x.to_string())
.collect::<Vec<_>>();
let mut rng = thread_rng();
recommendedpkgs.shuffle(&mut rng);
let mut desktoppicks = recommendedpkgs
.iter()
.filter(|x| {
if desktopenv == "GNOME" {
x.starts_with("gnome.") || x.starts_with("gnome-")
} else if desktopenv == "XFCE" {
x.starts_with("xfce.")
} else if desktopenv == "MATE" {
x.starts_with("mate.")
} else if desktopenv == "X-Cinnamon" {
x.starts_with("cinnamon.")
} else if desktopenv == "KDE" {
x.starts_with("libsForQt5")
} else if desktopenv == "Pantheon" {
x.starts_with("pantheon.")
} else {
false
}
})
.collect::<Vec<_>>();
for p in desktoppicks.iter().take(3) {
recpicks.push(p.to_string());
}
for category in PkgCategory::iter() {
println!("CATEGORY: {}", category);
desktoppicks.shuffle(&mut rng);
let mut cvec = vec![];
let mut allvec = vec![];
let mut rpkgs = recommendedpkgs.clone();
fn checkpkgs(
pkg: String,
pkgs: &HashMap<&String, &Package>,
category: PkgCategory,
) -> bool {
match category {
PkgCategory::Audio => {
// Audio:
// - pkgs/applications/audio
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/applications/audio") {
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories.contains(&String::from("Audio")) {
return true;
}
}
}
}
false
}
PkgCategory::Development => {
// Development:
// - pkgs/development
// - pkgs/applications/terminal-emulators
// - xdg: Development
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/development")
|| pos.starts_with(
"pkgs/applications/terminal-emulators",
)
{
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories
.contains(&String::from("Development"))
{
return true;
}
}
}
}
false
}
PkgCategory::Games => {
// Games:
// - pkgs/games
// - pkgs/applications/emulators
// - pkgs/tools/games
// - xdg::Games
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/games")
|| pos.starts_with(
"pkgs/applications/emulators",
)
|| pos.starts_with("pkgs/tools/games")
{
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories.contains(&String::from("Games")) {
return true;
}
}
}
}
false
}
PkgCategory::Graphics => {
// Graphics:
// - pkgs/applications/graphics
// - xdg: Graphics
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/applications/graphics")
|| pos.starts_with("xdg:Graphics")
{
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories.contains(&String::from("Graphics")) {
return true;
}
}
}
}
false
}
PkgCategory::Network => {
// Network:
// - pkgs/applications/networking
// - xdg: Network
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/applications/networking")
|| pos.starts_with("xdg:Network")
{
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories.contains(&String::from("Network")) {
return true;
}
}
}
}
false
}
PkgCategory::Video => {
// Video:
// - pkgs/applications/video
// - xdg: Video
if let Some(p) = pkgs.get(&pkg) {
if let Some(pos) = &p.meta.position {
if pos.starts_with("pkgs/applications/video")
|| pos.starts_with("xdg:Video")
{
return true;
}
}
if let Some(data) = &p.appdata {
if let Some(categories) = &data.categories {
if categories.contains(&String::from("Video")) {
return true;
}
}
}
}
false
}
}
}
for pkg in desktoppicks.iter().take(3) {
if checkpkgs(pkg.to_string(), &appdatapkgs, category.clone()) {
cvec.push(pkg.to_string());
}
}
while cvec.len() < 12 {
if let Some(pkg) = rpkgs.pop() {
if !cvec.contains(&pkg.to_string())
&& checkpkgs(
pkg.to_string(),
&appdatapkgs,
category.clone(),
)
{
cvec.push(pkg.to_string());
}
} else {
break;
}
}
println!("{} PICKS {:#?}", category, cvec);
let catagortypkgs = pkgs
.iter()
.filter(|(x, _)| {
if let Some(p) = appdatapkgs.get(*x) {
if let Some(position) = &p.meta.position {
(position.starts_with("pkgs/applications/audio") && category == PkgCategory::Audio)
|| (position.starts_with("pkgs/applications/terminal-emulators") && category == PkgCategory::Development)
|| (position.starts_with("pkgs/applications/emulators") && category == PkgCategory::Games)
|| (position.starts_with("pkgs/applications/graphics") && category == PkgCategory::Graphics)
|| (position.starts_with("pkgs/applications/networking") && category == PkgCategory::Network)
|| (position.starts_with("pkgs/applications/video") && category == PkgCategory::Video)
|| (position.starts_with("pkgs/tools/games") && category == PkgCategory::Games)
|| (position.starts_with("pkgs/games") && category == PkgCategory::Games)
|| (position.starts_with("pkgs/development") && category == PkgCategory::Development)
|| appdatapkgs.contains_key(x)
} else {
false
}
} else {
false
}
})
.collect::<HashMap<_, _>>();
for pkg in catagortypkgs.keys() {
if checkpkgs(pkg.to_string(), &catagortypkgs, category.clone()) {
allvec.push(pkg.to_string());
}
}
println!("{} ALL {:#?}", category, allvec);
cvec.shuffle(&mut rng);
allvec.sort_by_key(|x| x.to_lowercase());
catpicks.insert(category.clone(), cvec);
catpkgs.insert(category.clone(), allvec);
}
while recpicks.len() < 12 {
if let Some(p) = recommendedpkgs.pop() {
if !recpicks.contains(&p.to_string()) {
recpicks.push(p);
}
} else {
break;
}
}
recpicks.shuffle(&mut rng);
}
println!("SEND INIT");
match cr {
CacheReturn::Init => {
sender.output(AppMsg::Initialize(pkgs, recpicks, newpkgs, catpicks, catpkgs));
}
CacheReturn::Update => {
sender.output(AppMsg::ReloadUpdateItems(pkgs, newpkgs));
}
}
});
}
}
}
}
pub struct LoadErrorModel {
hidden: bool,
msg: String,
msg2: String,
}
#[derive(Debug)]
pub enum LoadErrorMsg {
Show(String, String),
Retry,
Close,
// Preferences,
}
#[relm4::component(pub)]
impl SimpleComponent for LoadErrorModel {
type InitParams = gtk::Window;
type Input = LoadErrorMsg;
type Output = AppMsg;
type Widgets = LoadErrorWidgets;
view! {
dialog = gtk::MessageDialog {
set_transient_for: Some(&parent_window),
set_modal: true,
#[watch]
set_visible: !model.hidden,
#[watch]
set_text: Some(&model.msg),
#[watch]
set_secondary_text: Some(&model.msg2),
set_use_markup: true,
set_secondary_use_markup: true,
add_button: ("Retry", gtk::ResponseType::Accept),
// add_button: ("Preferences", gtk::ResponseType::Help),
add_button: ("Quit", gtk::ResponseType::Close),
connect_response[sender] => move |_, resp| {
sender.input(match resp {
gtk::ResponseType::Accept => LoadErrorMsg::Retry,
gtk::ResponseType::Close => LoadErrorMsg::Close,
// gtk::ResponseType::Help => LoadErrorMsg::Preferences,
_ => unreachable!(),
});
},
}
}
fn init(
parent_window: Self::InitParams,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = LoadErrorModel {
hidden: true,
msg: String::default(),
msg2: String::default(),
};
let widgets = view_output!();
let accept_widget = widgets
.dialog
.widget_for_response(gtk::ResponseType::Accept)
.expect("No button for accept response set");
accept_widget.add_css_class("warning");
// let pref_widget = widgets
// .dialog
// .widget_for_response(gtk::ResponseType::Help)
// .expect("No button for help response set");
// pref_widget.add_css_class("suggested-action");
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
match msg {
LoadErrorMsg::Show(s, s2) => {
self.hidden = false;
self.msg = s;
self.msg2 = s2;
}
LoadErrorMsg::Retry => {
self.hidden = true;
sender.output(AppMsg::TryLoad)
}
LoadErrorMsg::Close => sender.output(AppMsg::Close),
// LoadErrorMsg::Preferences => sender.output(AppMsg::ShowPrefMenu),
}
}
}