diff --git a/AGENTS.md b/AGENTS.md index 278114162c..80022b1f61 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,11 +116,11 @@ there; the CLI needs no server and is the first choice. Auto-generated from the Prowl index, refreshed on each `overview`/`init`. Prefer retrieving from Prowl (and reading the cited files) over grepping or relying on training memory; this is the current shape of the repo. -- size: 6256 files, 492641 symbols, 26729 edges (resolved 12811, external deps 7969, unresolved 5949) -- languages: qml:2468 go:2055 bash:577 javascript:305 json:255 markdown:141 cpp:112 python:75 -- subsystems: ryoku/shell(2185,qml) · ryoku/shell(71,cpp) · ryoku/hub(66,qml) · ryoku/ui(59,qml) · ryoku/apps(57,qml) · ryoku/rashin(25,typescript) · tests/ui(21,qml) · ryoku/shell(18,css) -- entrypoints: ryoku/shell/quickshell/inir/modules/background/Background.qml · ryoku/shell/quickshell/shell/shell.qml · ryoku/shell/quickshell/shell/modules/dock/ii/settings/DockConfig.qml · ryoku/hub/quickshell/pages/InputPage.qml · ryoku/shell/quickshell/shell/modules/dock/ii/settings/DockUtilitiesConfig.qml · ryoku/shell/quickshell/stage/modules/ii/background/widgets/clock/WearOSArcClock.qml · ryoku/hub/quickshell/pages/AddonsPage.qml · ryoku/shell/quickshell/shell/modules/dock/ii/draw/LiveDrawWindow.qml · (+561 more) -- central files (most depended-on): ryoku/shell/quickshell/shell/modules/bar/barstyles/python/quickactions/actions/Timer.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/I18n.qml · ryoku/ui/Singletons/Tokens.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/theme/ThemeBackend.qml +- size: 6773 files, 539735 symbols, 31135 edges (resolved 15835, external deps 8793, unresolved 6507) +- languages: qml:2775 go:2149 bash:616 javascript:317 json:271 markdown:149 cpp:123 python:100 +- subsystems: ryoku/shell(2484,qml) · ryoku/hub(80,qml) · ryoku/shell(75,cpp) · ryoku/apps(58,qml) · ryoku/ui(56,qml) · ryoku/rashin(25,typescript) · tests/ui(22,qml) · ryoku/shell(20,css) +- entrypoints: ryoku/shell/quickshell/inir/modules/background/Background.qml · ryoku/shell/quickshell/shell/shell.qml · ryoku/shell/quickshell/stage/modules/ii/background/widgets/media/AndroidMediaWidget.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/media/MusicPopup.qml · ryoku/shell/quickshell/stage/modules/ii/background/widgets/clock/ConcentricClockWidget.qml · ryoku/hub/quickshell/pages/InputPage.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/syspanel/SystemPanel.qml · ryoku/shell/quickshell/stage/modules/ii/background/widgets/media/CircularMediaWidget.qml · (+742 more) +- central files (most depended-on): ryoku/shell/quickshell/shell/modules/bar/barstyles/python/quickactions/actions/Timer.qml · ryoku/ui/Singletons/Tokens.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/I18n.qml · ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/theme/ThemeBackend.qml - read these guides first: README.md · AGENTS.md · CONTRIBUTING.md · docs/development.md · docs/structure.md Depth on demand: `prowl find|def|outline|references `, `search `, `context search ""`, `sketch `. diff --git a/VERSION b/VERSION index 90a27f9cea..af0b7ddbff 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.5 +1.0.6 diff --git a/docs/store.md b/docs/store.md index 981ca9316c..5c50b5cc83 100644 --- a/docs/store.md +++ b/docs/store.md @@ -246,6 +246,7 @@ The public command surface is deliberately small: ```text ryostore catalog [--refresh] ryostore install +ryostore search ryostore open ryostore settings [id] ``` @@ -287,6 +288,38 @@ and installation, not a second runtime copy. the category offers Popular, New, and Verified orderings and global search. Its thousands of preview and gallery URLs deliberately bypass eager asset warming; only visible delegates request their remote media. +- **Apps (Flatpak):** list up to 300 of the most popular desktop applications + from the Flathub collection API, joined with `flatpak list`, so an installed + Flathub application outside that ranking is still listed and removable. + Flathub is a third-party source: it does not pass through the `ryostore` + registry or its screening. The detail labels Flathub's verified or + unverified state when known (or unknown for installed-only items) and warns + before installation; verification is not a Ryostore safety review. New apps + use the system Flatpak installation and its shared runtimes when its + `flathub` remote is valid; if only the user installation has a valid remote, + they install there instead. System installs may ask for authorization. + The store never adds a remote. When neither installation has a valid remote, + the app is unavailable; if the system remote is missing, `ryoku doctor` can + set it up. Before showing an installed app as a Flathub install, or installing + or updating it, the store checks that the selected installation's `flathub` + remote points at `https://dl.flathub.org/repo/`; a same-name remote elsewhere + is not trusted. Existing user-installed Flathub apps remain updateable and + removable there. If the same application exists in both installations, those + actions target the user copy. Removal refuses apps from other remotes, keeps + application data, and leaves unrelated runtimes alone. + An installed application with an update waiting (asked of each + installation that holds one, with `flatpak remote-ls --updates`, best effort + and bounded) shows UPDATE, which runs `flatpak update` in the installation + that holds it. The ranking is cached on disk and served flagged offline when + Flathub is unreachable. A search started from the Apps category also asks + Flathub's own search (`ryostore search flatpak `, live, up to 30 + results, queries of two characters or more, debounced in the UI), so an + application outside the ranking is still found; the catalogue's own copy + retains update state and matches a loose live result by ID. + Flathub matches loosely (typos, keywords, descriptions), so a result need not + contain the query text. Only Flathub CDN icons load lazily, like the Omarchy marketplace's + media. A host without the `flatpak` command lists the apps greyed out with the + reason. - **Bundles:** fetch the `ryostore` bundle registry and join it with `ryostore-install status`. Partial counts are first-class. Settings owns installed bundle status and removal. diff --git a/fedora/packages/rpm/ryoku-shell.spec b/fedora/packages/rpm/ryoku-shell.spec index 9f5269c3cf..718e8a3bbe 100644 --- a/fedora/packages/rpm/ryoku-shell.spec +++ b/fedora/packages/rpm/ryoku-shell.spec @@ -42,6 +42,7 @@ Requires: inotify-tools Requires: easyeffects Requires: lsp-plugins-lv2 Requires: lv2-calf-plugins +Suggests: kde-connect %description ryoku-shell, built from the shared Ryoku source and package payload recipe. diff --git a/fedora/packages/translations.tsv b/fedora/packages/translations.tsv index 8cb42dee99..1b3129d59e 100644 --- a/fedora/packages/translations.tsv +++ b/fedora/packages/translations.tsv @@ -79,9 +79,12 @@ hyprgrass hyprgrass hyprland Ryoku-owned RPM. hypridle hypridle desktop Provided by a restricted COPR. hyprland hyprland hyprland Provided by a restricted COPR. hyprland-preview-share-picker hyprland-preview-share-picker hyprland Ryoku-owned RPM. +hyprlang hyprlang hyprland Provided by a restricted COPR. hyprpicker hyprpicker desktop Provided by a restricted COPR. hyprpolkitagent hyprpolkitagent hyprland Provided by a restricted COPR. hyprsunset hyprsunset hyprland Provided by a restricted COPR. +hyprtoolkit hyprtoolkit hyprland Provided by a restricted COPR. +hyprutils hyprutils hyprland Provided by a restricted COPR. imagemagick ImageMagick desktop imgborders imgborders hyprland Ryoku-owned RPM. inetutils telnet optional @@ -113,6 +116,7 @@ libnotify libnotify desktop libpulse pulseaudio-libs pulseaudio-utils desktop libqalculate libqalculate desktop librsvg librsvg2-tools desktop +libstdc++ libstdc++ hyprland libusb libusb1 optional libva libva desktop libvirt libvirt optional @@ -219,6 +223,7 @@ ryoku-shell ryoku-shell desktop Ryoku-owned RPM. ryomanager ryomanager optional Ryoku-owned RPM. ryotunes ryotunes optional Ryoku-owned RPM. scrcpy scrcpy optional Provided by a restricted COPR. +sdbus-cpp sdbus-cpp hyprland sddm sddm desktop shaderc glslc desktop skwd-paper-bin skwd-paper-bin desktop Ryoku-owned RPM. @@ -249,6 +254,7 @@ ttf-readex-pro ttf-readex-pro desktop Ryoku-owned RPM. ttf-rubik-vf google-rubik-fonts desktop udev systemd-udev desktop upower upower desktop +util-linux-libs util-linux-libs hyprland uv uv desktop v4l2loopback-dkms v4l2loopback v4l2loopback-dkms optional Provided by a restricted COPR. vdirsyncer vdirsyncer desktop @@ -274,6 +280,7 @@ wlsunset wlsunset desktop wpa_supplicant wpa_supplicant desktop wtype wtype desktop xcb-util-cursor xcb-util-cursor desktop +xdg-desktop-portal xdg-desktop-portal hyprland xdg-desktop-portal-gnome xdg-desktop-portal-gnome desktop xdg-desktop-portal-gtk xdg-desktop-portal-gtk desktop xdg-desktop-portal-hyprland xdg-desktop-portal-hyprland hyprland Provided by a restricted COPR. diff --git a/installation/CHANGELOG.md b/installation/CHANGELOG.md index 1ad2be0c15..d05742996e 100644 --- a/installation/CHANGELOG.md +++ b/installation/CHANGELOG.md @@ -18,6 +18,11 @@ (`installerplan/components.tsv`, `tui/main.go`, `backend/lib/deploy.sh`). ### Fixed +- **Fresh installs receive newer Ryoku package overrides.** After the Arch or + CachyOS desktop transaction, the installer aligns every installed package + that `[ryoku]` serves at a newer version. Both offline ISO repositories + compute and bake the same overrides instead of keeping an earlier + repository's build (`backend/lib/deploy.sh`, `iso/offline-repo.sh`). - **The Void package check sees what a real install gets.** The Void container image skips `/etc/skel` and other paths through noextract patterns, so the clean-container install could not find the per-user services `ryoku-desktop` diff --git a/installation/backend/lib/deploy.sh b/installation/backend/lib/deploy.sh index 853887dd52..f3492cf50b 100755 --- a/installation/backend/lib/deploy.sh +++ b/installation/backend/lib/deploy.sh @@ -117,6 +117,24 @@ ryoku_repo_keyring() { run rm -f "$kd/ryoku.gpg" "$kd/ryoku-trusted" "$kd/ryoku-revoked" } +# Print the installed packages for which [ryoku] serves a newer build. The +# qualified targets bypass pacman's first-repository rule without downgrading a +# package when an official or CachyOS repository has moved ahead. +ryoku_newer_repo_targets() { + local installed_out served_out repo name version comparison + local -A installed=() + installed_out=$(arch-chroot /mnt pacman -Q --noconfirm) || return + served_out=$(arch-chroot /mnt pacman -Sl --noconfirm ryoku) || return + while read -r name version _; do + [[ -n $name && -n $version ]] && installed["$name"]=$version + done <<<"$installed_out" + while read -r repo name version _; do + [[ $repo == ryoku && -n ${installed[$name]:-} ]] || continue + comparison=$(arch-chroot /mnt vercmp "$version" "${installed[$name]}") || return + (( comparison > 0 )) && printf 'ryoku/%s\n' "$name" + done <<<"$served_out" +} + # packages: pacman -S the desktop set from [ryoku] inside the chroot. The # umbrella version-pins every monorepo component; hardware-only providers are # added from the live machine's detector. Lends the live resolv.conf for DNS @@ -159,6 +177,7 @@ ryoku_deploy_packages() { else log "DRYRUN: arch-chroot /mnt pacman -Sy --noprogressbar" log "DRYRUN: arch-chroot /mnt pacman -S --noconfirm --needed --noprogressbar ${pkgs[*]} (one retry on a network flake)" + log "DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version" log "DRYRUN: would warn if $RYOKU_REPO/.payload version differs from pacman -Si ryoku-desktop" fi return 0 @@ -207,6 +226,21 @@ ryoku_deploy_packages() { else rc=1 fi + if (( rc == 0 )); then + case ${RYOKU_VARIANT:-} in + plain | cachyos) + local target_output + local -a align_targets=() + if ! target_output=$(ryoku_newer_repo_targets); then + rc=1 + elif [[ -n $target_output ]]; then + mapfile -t align_targets <<<"$target_output" + log 'aligning newer [ryoku] package overrides: %s' "${align_targets[*]}" + arch-chroot /mnt pacman -S --noconfirm --needed --noprogressbar "${align_targets[@]}" || rc=$? + fi + ;; + esac + fi if (( made_resolv == 1 )); then rm -f /mnt/etc/resolv.conf diff --git a/installation/iso/offline-repo.sh b/installation/iso/offline-repo.sh index 91c2335e2a..6b21697054 100755 --- a/installation/iso/offline-repo.sh +++ b/installation/iso/offline-repo.sh @@ -34,6 +34,11 @@ die() { printf 'offline-repo: error: %s\n' "$*" >&2; exit 1; } command -v pacman >/dev/null 2>&1 || die "pacman not found" command -v repo-add >/dev/null 2>&1 || die "repo-add not found (pacman package)" +case $VARIANT in + plain | cachyos) ;; + *) die "offline pacman closure supports only plain or cachyos, got: $VARIANT" ;; +esac + # read a plain one-per-line list, dropping comments + blanks. read_list() { [[ -f $1 ]] && grep -vE '^[[:space:]]*(#|$)' "$1" || true; } @@ -155,17 +160,19 @@ log "syncing repo databases (throwaway db)" "${PAC[@]}" -Sy --config "$conf" --dbpath "$work/db" --noconfirm >/dev/null # SERVED: every name the configured repos serve, mapped to the version pacman -# resolves it to: the first repo in config order that has the name, as on the -# target, whichever build is newer. RESOLVED keeps every "name version" pacman +# resolves it to: the first repo in config order that has the name, even when a +# later repo has a newer build. RESOLVED keeps every "name version" pacman # resolved during this run, so a retry's db re-sync does not orphan what an # earlier sync already fetched. Re-read after every db sync. -declare -A SERVED=() RESOLVED=() +declare -A SERVED=() SERVED_REPO=() RESOLVED=() read_served() { - local _repo name ver + local repo name ver SERVED=() - while read -r _repo name ver _; do + SERVED_REPO=() + while read -r repo name ver _; do [[ -n ${SERVED[$name]:-} ]] && continue SERVED[$name]=$ver + SERVED_REPO[$name]=$repo RESOLVED["$name $ver"]=1 # A package file names its version without the epoch when the repo that # publishes it does (ryotunes is 1:1.2.3-1 in a ryotunes-1.2.3-1 file), and @@ -174,6 +181,20 @@ read_served() { done < <("${PAC[@]}" -Sl --config "$conf" --dbpath "$work/db") } read_served +# Pacman resolves duplicate names from the first repository, regardless of +# version. Qualify every newer [ryoku] duplicate so the flat offline repository +# contains the same overrides `ryoku update` would select, without downgrades. +ryoku_pins=() +while read -r _repo name ver _; do + first_repo=${SERVED_REPO[$name]:-} + [[ -n $first_repo && $first_repo != ryoku ]] || continue + comparison=$(vercmp "$ver" "${SERVED[$name]}") || die "could not compare repository versions for $name" + (( comparison > 0 )) && ryoku_pins+=("$name") +done < <("${PAC[@]}" -Sl --config "$conf" --dbpath "$work/db" ryoku) +for name in "${ryoku_pins[@]}"; do + PKGS+=("ryoku/$name") +done +log "qualified ${#ryoku_pins[@]} newer [ryoku] duplicate(s) in the offline closure" # An aur.packages name a configured repo serves (CachyOS ships zen-browser-bin) # is that repo's package: fetch its build like any other, so the box updates it @@ -505,7 +526,7 @@ done # (a retry's re-sync, an older AUR build), `repo-add -p` indexes the newest # whatever order it reads them in. stage_offline_repo() { - local f base name + local f base name url candidate pinned local -a keep=() for f in "$CACHE"/*.pkg.tar.zst "$CACHE"/*.pkg.tar.xz; do [[ -e $f ]] || continue @@ -516,6 +537,28 @@ stage_offline_repo() { done (( ${#keep[@]} )) || die "no packages in $CACHE after download" cp -a --reflink=auto "${keep[@]}" "$DEST"/ + + # The configured repo order is deliberately the installed system's order, + # where official/CachyOS repos precede [ryoku]. Resolve each override by its + # qualified target, then replace any earlier-repo build staged above. + for name in "${ryoku_pins[@]}"; do + pinned= + while IFS= read -r url; do + candidate=${url%%\?*}; candidate=${candidate##*/} + base=${candidate%.pkg.tar.*}; base=${base%-*}; base=${base%-*}; base=${base%-*} + if [[ $base == "$name" ]]; then + pinned="$CACHE/$candidate" + break + fi + done < <("${PAC[@]}" -Sp --config "$conf" --dbpath "$work/db" --print-format '%l' "ryoku/$name") + [[ -f $pinned ]] || die "qualified [ryoku] package '$name' was not downloaded" + for f in "$DEST/$name"-*.pkg.tar.*; do + [[ -e $f ]] || continue + base=${f##*/}; base=${base%.pkg.tar.*}; base=${base%-*}; base=${base%-*}; base=${base%-*} + [[ $base == "$name" ]] && rm -f "$f" + done + cp -a --reflink=auto "$pinned" "$DEST"/ + done } # assemble the [offline] repo, then build the db. diff --git a/release/CHANGELOG.md b/release/CHANGELOG.md index 08c7461a02..2837b44520 100644 --- a/release/CHANGELOG.md +++ b/release/CHANGELOG.md @@ -152,6 +152,9 @@ the other plugin packages. ### Fixed +- **Hyprland screen sharing survives DMA-BUF renegotiation.** `[ryoku]` now + carries the post-1.4.1 portal fixes that preserve the capture callback and + bound PipeWire stream startup instead of wedging every later share. - **Fedora applies Bluetooth tuning during the package transaction.** The desktop RPM now restarts a running Bluetooth service after tuning BlueZ, as the Arch, CachyOS, and Void package hooks already do. diff --git a/release/packages/README.md b/release/packages/README.md index 011edd3727..37ab91a9bc 100644 --- a/release/packages/README.md +++ b/release/packages/README.md @@ -35,6 +35,10 @@ repo. Packages publish only from `main` release tags, never from `unstable-dev`. few commits past `v0.8.2` because that release regressed override-redirect popup focus (Steam and Wine menus close on sight under niri). A pinned commit, not a monorepo build; a later official `0.8.3` sorts above it and takes over. +- `xdg-desktop-portal-hyprland` -- the Hyprland portal pinned ten commits past + `v1.4.1`: it preserves the screencopy callback across DMA-BUF renegotiation + and bounds PipeWire stream startup instead of hanging forever. A later + official `1.4.2` sorts above it and takes over. - `ryoku-desktop` -- the umbrella. Depends on the packages above plus the user-facing desktop runtime, lays the base configuration under `/usr/share/ryoku/config`, and installs the helper scripts (`ryoku-cmd-*`, the hardware `ryoku-*`, diff --git a/release/packages/ryoku-desktop-hyprland/PKGBUILD b/release/packages/ryoku-desktop-hyprland/PKGBUILD index b6e7de4c34..ccbf1c9743 100644 --- a/release/packages/ryoku-desktop-hyprland/PKGBUILD +++ b/release/packages/ryoku-desktop-hyprland/PKGBUILD @@ -33,6 +33,8 @@ depends=( "ryoku-keysounds=$pkgver" "hyprgrass=$pkgver" 'hyprpolkitagent' + # [ryoku] pins the post-1.4.1 DMA-BUF renegotiation and Start-timeout fixes; + # `ryoku update` repo-qualifies this drop-in even though extra is listed first. 'xdg-desktop-portal-hyprland' # XDPH names /usr/bin/hyprland-preview-share-picker for the screen-share source # chooser, so a hard depend keeps the picker atomic with the portal. diff --git a/release/packages/ryoku-shell/PKGBUILD b/release/packages/ryoku-shell/PKGBUILD index e5d58c35d6..99a15d48c0 100644 --- a/release/packages/ryoku-shell/PKGBUILD +++ b/release/packages/ryoku-shell/PKGBUILD @@ -18,6 +18,7 @@ license=('GPL-3.0-or-later') # Python's built-in equalizer needs the processor and its LV2 effects at runtime. depends=('quickshell' 'ffmpeg' 'wayland' 'jq' 'python' 'uv' 'socat' 'gum' 'qrencode' 'upower' 'brightnessctl' 'ddcutil' 'wtype' 'wl-clipboard' 'zenity' 'power-profiles-daemon' 'networkmanager' 'curl' 'libnotify' 'inotify-tools' 'easyeffects' 'lsp-plugins-lv2' 'calf') makedepends=('go' 'wayland' 'wayland-protocols' 'ffmpeg') +optdepends=('kdeconnect: send ryoshot captures to paired devices') source=() _repo="$startdir/../../.." diff --git a/release/packages/xdg-desktop-portal-hyprland/PKGBUILD b/release/packages/xdg-desktop-portal-hyprland/PKGBUILD new file mode 100644 index 0000000000..3fbcdc39b2 --- /dev/null +++ b/release/packages/xdg-desktop-portal-hyprland/PKGBUILD @@ -0,0 +1,76 @@ +# Maintainer: Ryoku +# +# Arch's 1.4.1 build can lose its screencopy callback during DMA-BUF +# renegotiation (#403), killing hardware-encoded shares, and its Start handler +# then waits forever for an invalid PipeWire node. Upstream 59d429bf preserves +# the replacement callback, and 167adef9 bounds stream initialization to five +# seconds. This pinned build carries both fixes against Arch's stable Hyprland +# libraries until an official 1.4.2 or newer sorts above it and takes over. +# +# Fixed version, not RYOKU_PKGVER: build-repo.sh builds it once and adopts the +# published bytes later. Bump pkgrel if the pinned commit changes. +pkgname=xdg-desktop-portal-hyprland +pkgver=1.4.1.r10.g167adef +pkgrel=1 +_commit=167adef947a6067d1b90317daa0c6625641b27fb +_protocolver=044a434ec371f985655389bcef09286b1af3d77c +pkgdesc='xdg-desktop-portal backend for Hyprland (Ryoku build with screencopy fixes)' +url="https://github.com/hyprwm/$pkgname" +arch=('x86_64') +license=('BSD-3-Clause') +provides=('xdg-desktop-portal-impl') +depends=( + 'glibc' + 'hyprlang' + 'hyprtoolkit' + 'hyprutils' + 'libdrm' + 'libgcc' + 'libstdc++' + 'pipewire' + 'sdbus-cpp' + 'util-linux-libs' + 'wayland' + 'xdg-desktop-portal' +) +makedepends=( + 'cmake' + 'git' + 'hyprland-protocols' + 'hyprwayland-scanner' + 'wayland-protocols' +) +optdepends=( + 'grim: required for the screenshot portal to function' + 'slurp: support for interactive mode for the screenshot portal' +) +source=( + "$pkgname::git+$url.git#commit=$_commit" + "hyprland-protocols-$_protocolver.tar.gz::https://github.com/hyprwm/hyprland-protocols/archive/$_protocolver.tar.gz" +) +sha256sums=( + 'SKIP' + 'da075667ab2aecb271f6cbd49490f14c725f417d7522c49269decf7f14431d3e' +) + +prepare() { + cd "$pkgname/subprojects" + rm -rf hyprland-protocols sdbus-cpp + ln -s "$srcdir/hyprland-protocols-$_protocolver" hyprland-protocols +} + +build() { + cd "$pkgname" + cmake -B build \ + -D CMAKE_INSTALL_PREFIX=/usr \ + -D CMAKE_INSTALL_LIBEXECDIR=/usr/lib \ + -D CMAKE_BUILD_TYPE=Release \ + -D BUILD_TESTING=OFF + cmake --build build +} + +package() { + cd "$pkgname" + DESTDIR="$pkgdir" cmake --install build + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE +} diff --git a/release/repo/build-toolchain.packages b/release/repo/build-toolchain.packages index 330ef12579..979f754b53 100644 --- a/release/repo/build-toolchain.packages +++ b/release/repo/build-toolchain.packages @@ -71,6 +71,18 @@ gtk4 gtk4-layer-shell glm +# xdg-desktop-portal-hyprland builds its portal and HyprToolkit picker against +# the stable Hyprland libraries. The protocol source is pinned by the recipe, +# but keeping its package here also covers CMake's system lookup in CI. +hyprlang +hyprtoolkit +hyprutils +hyprland-protocols +hyprwayland-scanner +pipewire +sdbus-cpp +util-linux-libs + # ryoku-cursor-material compiles a Bibata variant at build time: fish + jq run # the vendored recipe, and python + librsvg (rsvg-convert) + xorg-xcursorgen # (xcursorgen) plus xcur2png (the recipe's -x PNG export) rasterise the cursors. diff --git a/ryoku-shell-installer/CHANGELOG.md b/ryoku-shell-installer/CHANGELOG.md index 55e355a53b..314605c884 100644 --- a/ryoku-shell-installer/CHANGELOG.md +++ b/ryoku-shell-installer/CHANGELOG.md @@ -72,6 +72,10 @@ steps, so a stuck stable install can be finished on unstable. ### Fixed +- **Fresh Arch-based conversions receive newer Ryoku package overrides.** + After the desktop transaction, the installer aligns every installed package + that `[ryoku]` serves at a newer version without downgrading packages whose + official or CachyOS build is already ahead. - **Uninstalling on Fedora stops trusting Ryoku's signing key.** Removing the repository files now also deletes the key rpm imported, so the machine no longer accepts packages signed by Ryoku. The key file itself leaves with the diff --git a/ryoku-shell-installer/distro_test.go b/ryoku-shell-installer/distro_test.go index b4594c7b7f..704e2ff173 100644 --- a/ryoku-shell-installer/distro_test.go +++ b/ryoku-shell-installer/distro_test.go @@ -32,6 +32,48 @@ func TestDetectDistro(t *testing.T) { } } +func TestNewerRyokuRepoTargets(t *testing.T) { + previousCompare := comparePackageVersions + comparePackageVersions = func(available, installed string) (int, error) { + return strings.Compare(available, installed), nil + } + t.Cleanup(func() { comparePackageVersions = previousCompare }) + + installed := map[string]string{ + "upgrade": "1.0-1", + "equal": "2.0-1", + "repo-older": "3.0-1", + "not-served": "1.0-1", + } + served := map[string]string{ + "upgrade": "1.1-1", + "equal": "2.0-1", + "repo-older": "2.9-1", + "not-installed": "4.0-1", + } + cachyos := detectDistro("cachyos", "arch") + for _, test := range []struct { + name string + distro *distro + want string + }{ + {name: "arch", distro: archLinux, want: "ryoku/upgrade"}, + {name: "cachyos", distro: cachyos, want: "ryoku/upgrade"}, + {name: "fedora", distro: fedoraLinux}, + {name: "void", distro: voidLinux}, + } { + t.Run(test.name, func(t *testing.T) { + got, err := newerRyokuRepoTargets(test.distro, installed, served) + if err != nil { + t.Fatal(err) + } + if joined := strings.Join(got, ","); joined != test.want { + t.Fatalf("newerRyokuRepoTargets() = %q, want %q", joined, test.want) + } + }) + } +} + func TestLocalAllRenamesAndDrops(t *testing.T) { in := []string{"git", "networkmanager", "fd", "matugen", "limine", "kitty"} got := debianLinux.localAll(in) diff --git a/ryoku-shell-installer/engine.go b/ryoku-shell-installer/engine.go index 736e4a0195..4e9573a633 100644 --- a/ryoku-shell-installer/engine.go +++ b/ryoku-shell-installer/engine.go @@ -17,6 +17,8 @@ import ( "os/exec" "path/filepath" "regexp" + "sort" + "strconv" "strings" "sync" "time" @@ -149,6 +151,84 @@ func setRyokuServer(conf, server string) (string, string) { var ryokuPkgs = []string{"ryoku-keyring", "ryoku-desktop"} var fedoraRyokuPkgs = []string{"ryoku", "ryoku-keyring", "ryoku-desktop", "ryoku-desktop-hyprland", "ryoku-desktop-niri"} +var comparePackageVersions = func(available, installed string) (int, error) { + out, err := exec.Command("vercmp", available, installed).Output() + if err != nil { + return 0, err + } + return strconv.Atoi(strings.TrimSpace(string(out))) +} + +func newerRyokuRepoTargets(d *distro, installed, served map[string]string) ([]string, error) { + if d.id != "arch" { + return nil, nil + } + names := make([]string, 0, len(served)) + for name := range served { + names = append(names, name) + } + sort.Strings(names) + + var targets []string + for _, name := range names { + installedVersion, ok := installed[name] + if !ok { + continue + } + comparison, err := comparePackageVersions(served[name], installedVersion) + if err != nil { + return nil, fmt.Errorf("compare package version for %s: %w", name, err) + } + if comparison > 0 { + targets = append(targets, "ryoku/"+name) + } + } + return targets, nil +} + +func pacmanVersionMap(args []string, nameField, versionField int) (map[string]string, error) { + out, err := exec.Command("pacman", args...).Output() + if err != nil { + return nil, err + } + versions := map[string]string{} + scanner := bufio.NewScanner(strings.NewReader(string(out))) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) > versionField { + versions[fields[nameField]] = fields[versionField] + } + } + return versions, scanner.Err() +} + +func alignNewerRyokuPackages(e *engine, d *distro) error { + if d.id != "arch" { + return nil + } + if e.dry { + e.say(i18n.T("DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version")) + return nil + } + installed, err := pacmanVersionMap([]string{"-Q"}, 0, 1) + if err != nil { + return fmt.Errorf("read installed package versions: %w", err) + } + served, err := pacmanVersionMap([]string{"-Sl", "ryoku"}, 1, 2) + if err != nil { + return fmt.Errorf("read [ryoku] package versions: %w", err) + } + targets, err := newerRyokuRepoTargets(d, installed, served) + if err != nil { + return err + } + if len(targets) == 0 { + return nil + } + e.say(i18n.Tf("Aligning %d newer package(s) from [ryoku]", len(targets))) + args := append([]string{"pacman", "-S", "--noconfirm", "--needed"}, targets...) + return e.sudo(args...) +} // ryokuOverwriteGlob names the ryoku-desktop-owned paths that a prior partial // install, a dev deploy, or the ISO installer can leave unowned on disk: the @@ -1985,7 +2065,10 @@ func stepPackages(e *engine) error { } // -Syu, not -S: a resumed run holds the db its first attempt synced, and // a publish in between replaces or prunes the files that db points at. - return installPackagePlan(e, d, pkgs) + if err := installPackagePlan(e, d, pkgs); err != nil { + return err + } + return alignNewerRyokuPackages(e, d) } func cachyosV3Supported() bool { diff --git a/ryoku-shell-installer/ryoku-shell-install b/ryoku-shell-installer/ryoku-shell-install index c55459ab1f..2eab7e3d95 100755 Binary files a/ryoku-shell-installer/ryoku-shell-install and b/ryoku-shell-installer/ryoku-shell-install differ diff --git a/ryoku-shell-installer/ryoku-shell-install.sha256 b/ryoku-shell-installer/ryoku-shell-install.sha256 index 176daf6eea..c1ffae7d9b 100644 --- a/ryoku-shell-installer/ryoku-shell-install.sha256 +++ b/ryoku-shell-installer/ryoku-shell-install.sha256 @@ -1 +1 @@ -a185e22107f8b15361b6f5c65bbbfd204977e0cd41ae83979ebf5440210cdf2a ryoku-shell-install +36b1e2c088ac9f4d8c46906a276cddce39a202b7eb132ca9bf4f0d78825d8f63 ryoku-shell-install diff --git a/ryoku/CHANGELOG.md b/ryoku/CHANGELOG.md index dbded6abbb..8b31322c93 100644 --- a/ryoku/CHANGELOG.md +++ b/ryoku/CHANGELOG.md @@ -3,6 +3,33 @@ ## Unreleased ### Fixed +- **Holding a mute, mic-mute or play key on niri toggles once.** niri repeats + binds while a key is held, so a slightly long press could mute and unmute + the microphone again or pause and resume playback; these toggles now ignore + repeat like they do on Hyprland and Wayfire (`wm/niri/config_binds.go`). +- **niri can always recover desktop shortcuts from an application's keyboard + grab.** Controls and the shortcut-inhibit toggle now bypass the inhibitor, + matching the recovery bindings on Hyprland and Wayfire + (`wm/niri/config_binds.go`). +- **Rashin history finds conversations in both chat lanes again.** It reads + Hermes's current workspace column when ACP omits that metadata, so saved + Agent and Chat sessions remain visible and reopenable (`rashin/backend/`). +- **Game Mode's live strip comes from the shipped Hyprland config.** Turning it on + loads the same module that config reloads keep applying while it stays on + (`wm/hyprland/act.go`). +- **Whole column widths and opacities no longer break niri settings.** The + provider writes these values as float literals so niri can load the generated + config (`wm/niri/config_layout.go`, `wm/niri/config_rules.go`). +- **Wallpaper previews tint the shell without changing application themes.** + Cancelling restores the previous colours, while applying runs the normal + theme update (`shell/ryogami/daemon/preview.go`, `shell/quickshell/`). +- **Desktop apps and folders work under every bar style.** Desktop placement + reads the editor's settings again, app choices resolve installed entries, + and a new folder opens for renaming after the menu closes. Shima's desktop + menu and empty-notifications message read naturally in Spanish + (`shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcuts.qml`, + `shell/quickshell/stage/services/AppSearch.qml`, `ui/Chips.qml`, + `shell/quickshell/shell/modules/desktop/DesktopContextMenu.qml`, `i18n/catalog/`). - **Bar Studio and RyoStore show every bar style again.** Subshells remain grouped on their own pages while also appearing in the complete bar-style galleries, with Python selecting its real shell instead of disappearing diff --git a/ryoku/apps/CHANGELOG.md b/ryoku/apps/CHANGELOG.md index 131d1c3fb1..3541ce8132 100644 --- a/ryoku/apps/CHANGELOG.md +++ b/ryoku/apps/CHANGELOG.md @@ -3,6 +3,13 @@ ## Unreleased ### Added +- `ryostore/`: **Apps: browse and install Flathub applications.** The Apps + category lists popular and installed applications, searches the wider Flathub + catalogue, and labels Flathub verification. New apps use the doctor's shared + system remote when available, or an existing canonical user remote; the + store checks remote identity and leaves unrelated runtimes alone + (`ryostore/backend/provider_flatpak.go`, `ryostore/quickshell/App.qml`, + `ryostore/quickshell/ProductDetail.qml`). - `ryostore/`: **Built-in bar styles carry a resource tier.** Orpheus and Kairos are `light`; QS Bar, Python, Nomarchy, Shima and Sumi are `heavy`, so the Hub can say which styles may cost more. A catalogue snapshot from an @@ -180,6 +187,29 @@ v0.56.0-beta.19") via `ryoku version --pretty` (`config.jsonc`). ### Fixed +- `fastfetch/`: **The terminal greeting shows the Ryoku logo image in Ghostty.** + The greeting kept the image only in kitty, so Ghostty, which speaks the same + graphics protocol, fell back to a text logo or none (`ryoku-fastfetch`). + Contributed by SourceAura. +- `ryovm/`, `ryostore/`: **Open apps survive a Ryoku update.** Packaged + Quickshell apps keep their loaded generation while materialize replaces their + QML tree, then pick up the new one on their next launch; source runs still + hot-reload (`quickshell/shell.qml`). +- `ryostore/`, `ryovm/`: **The Japanese gloss preference reaches app chrome.** + Decorative seals and Ryoku marks change to hidden or Latin forms with the + desktop setting (`*/quickshell/`). +- `ryostore/`: **A bundle install refreshes the store when it finishes.** The + launcher waited only for the terminal to exist, so the catalogue reloaded + milliseconds later and kept listing the bundle as not installed until a + manual refresh. It now waits for the terminal to close, the startup probe + keeps its live build as the launch snapshot so a part installed outside the + store is not frozen as absent, and an install-all queue installs one bundle + at a time instead of opening a terminal per item + (`ryostore/backend/provider_bundles.go`, `ryostore/backend/main.go`, + `ryostore/quickshell/Singletons/Store.qml`). +- `ryostore/`: **New bundles appear on the first Store visit.** When the + upstream catalogue has changed, the Store refreshes its cached view on open + instead of waiting for a manual refresh (`ryostore/quickshell/Singletons/Store.qml`). - `fish/`, `bash/`, `zsh/`, `terminal-shell/`: **Multiplexer panes started before the desktop can paste again.** A herdr, tmux or zellij server keeps the environment of the login that started it, so one started over SSH or from a diff --git a/ryoku/apps/fastfetch/ryoku-fastfetch b/ryoku/apps/fastfetch/ryoku-fastfetch index 15096363ab..c70289bffb 100755 --- a/ryoku/apps/fastfetch/ryoku-fastfetch +++ b/ryoku/apps/fastfetch/ryoku-fastfetch @@ -2,8 +2,8 @@ # Ryoku-branded fastfetch. The emblem is rendered natively by fastfetch's # kitty-direct logo (image left, dossier right). fastfetch auto-enables "pipe # mode" when it thinks stdout isn't a real TTY, which SILENTLY disables image -# logos and falls back to ASCII -- so in kitty we force `--pipe false` to keep -# the image. Outside kitty: chafa if present, else no logo. +# logos and falls back to ASCII -- so in kitty and ghostty we force +# `--pipe false` to keep the image. Outside those: chafa if present, else no logo. # # A Ryostore preset may hand drawing to its own renderer (for image formats # fastfetch cannot show, such as icat PNGs) with a comment in its config: @@ -62,7 +62,7 @@ fi if [[ ! -f "$config" ]]; then exec fastfetch "$@" -elif [[ -n "${KITTY_WINDOW_ID:-}" || "${TERM:-}" == "xterm-kitty" ]]; then +elif [[ -n "${KITTY_WINDOW_ID:-}" || "${TERM:-}" == "xterm-kitty" || "${TERM:-}" == "xterm-ghostty" || "${TERM_PROGRAM:-}" == "ghostty" ]]; then exec fastfetch --config "$effective" --pipe false "$@" elif command -v chafa >/dev/null 2>&1; then exec fastfetch --config "$effective" --logo-type chafa "$@" diff --git a/ryoku/apps/ryostore/backend/asset_cache.go b/ryoku/apps/ryostore/backend/asset_cache.go index ed84137e9a..ac1cbadd6d 100644 --- a/ryoku/apps/ryostore/backend/asset_cache.go +++ b/ryoku/apps/ryostore/backend/asset_cache.go @@ -44,7 +44,7 @@ func assetCacheDir() string { // delegates fetch only visible media, instead of a refresh downloading the // entire marketplace in the background. func shouldWarmAssets(item Item) bool { - return item.Category != "omarchy-plugins" + return item.Category != "omarchy-plugins" && item.Category != flatpakCategory } // rewriteCachedAssets swaps every already-cached remote asset URL for its local diff --git a/ryoku/apps/ryostore/backend/catalog.go b/ryoku/apps/ryostore/backend/catalog.go index 39d4ab154f..c82d13c2b4 100644 --- a/ryoku/apps/ryostore/backend/catalog.go +++ b/ryoku/apps/ryostore/backend/catalog.go @@ -37,6 +37,7 @@ func providers() []Provider { newVesktopThemesProvider(c), pluginProvider{cache: c}, newOmarchyPluginProvider(), + newFlatpakProvider(), bundleProvider{cache: c, status: defaultBundleStatus, launch: launchBundleInstall}, newDecorProvider(c), newLauncherImageProvider(c), diff --git a/ryoku/apps/ryostore/backend/catalog_test.go b/ryoku/apps/ryostore/backend/catalog_test.go index 93547b2b50..ed4d587f62 100644 --- a/ryoku/apps/ryostore/backend/catalog_test.go +++ b/ryoku/apps/ryostore/backend/catalog_test.go @@ -229,6 +229,33 @@ func TestRunCatalogSnapshotServesThenRefresh(t *testing.T) { } } +func TestRunCatalogRebuildsWhenProviderAdded(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + var originalLoads, addedLoads int32 + original := countingProvider{ + fakeProvider{category: Category{ID: "rices"}, items: []Item{{ID: "demo", Category: "rices"}}}, + &originalLoads, + } + if err := runCatalog(io.Discard, []Provider{original}, nil); err != nil { + t.Fatal(err) + } + added := countingProvider{ + fakeProvider{category: Category{ID: "flatpak"}, items: []Item{{ID: "org.example.App", Category: "flatpak"}}}, + &addedLoads, + } + var out bytes.Buffer + if err := runCatalog(&out, []Provider{original, added}, nil); err != nil { + t.Fatal(err) + } + var cat Catalog + if err := json.Unmarshal(out.Bytes(), &cat); err != nil { + t.Fatal(err) + } + if len(cat.Categories) != 2 || originalLoads != 2 || addedLoads != 1 { + t.Fatalf("new provider was hidden by old snapshot: categories=%d, loads=%d/%d", len(cat.Categories), originalLoads, addedLoads) + } +} + // TestRunCatalogDoesNotSnapshotEmpty proves a catalogue with no items is never // cached, so a launch after a failed fetch probes live again instead of pinning // an empty store. diff --git a/ryoku/apps/ryostore/backend/check_snapshot_test.go b/ryoku/apps/ryostore/backend/check_snapshot_test.go new file mode 100644 index 0000000000..9d479e398b --- /dev/null +++ b/ryoku/apps/ryostore/backend/check_snapshot_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "testing" +) + +// TestRunCheckRefreshesTheLaunchSnapshot proves the startup probe keeps its live +// build as the launch snapshot. Installed state is local and never part of the +// revision, so without this a part installed while the store was closed (from +// Settings or a terminal) stayed frozen as absent on the next open. +func TestRunCheckRefreshesTheLaunchSnapshot(t *testing.T) { + t.Setenv("RYOKU_EXTRAS_BASE", "") + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + + providers := func(installed bool) []Provider { + count := 0 + if installed { + count = 1 + } + return []Provider{fakeProvider{ + category: Category{ID: "bundles", Name: "Bundles", Count: 1}, + items: []Item{{ + ID: "demo", + Category: "bundles", + Name: "Demo", + Installed: installed, + InstalledCount: count, + TotalCount: 1, + }}, + }} + } + snapshot := func() Item { + data, err := os.ReadFile(filepath.Join(extrasCacheDir(), "catalog.json")) + if err != nil { + t.Fatalf("launch snapshot not written: %v", err) + } + var cat Catalog + if err := json.Unmarshal(data, &cat); err != nil { + t.Fatalf("decode snapshot: %v", err) + } + if len(cat.Items) != 1 { + t.Fatalf("snapshot items = %d, want 1", len(cat.Items)) + } + return cat.Items[0] + } + + if err := runCheck(io.Discard, providers(false)); err != nil { + t.Fatalf("runCheck before install: %v", err) + } + if got := snapshot(); got.Installed || got.InstalledCount != 0 { + t.Fatalf("snapshot before install = %+v, want absent", got) + } + + if err := runCheck(io.Discard, providers(true)); err != nil { + t.Fatalf("runCheck after install: %v", err) + } + if got := snapshot(); !got.Installed || got.InstalledCount != 1 { + t.Fatalf("snapshot after install = %+v, want installed", got) + } +} diff --git a/ryoku/apps/ryostore/backend/main.go b/ryoku/apps/ryostore/backend/main.go index 903b5ad7af..101c54ea0b 100644 --- a/ryoku/apps/ryostore/backend/main.go +++ b/ryoku/apps/ryostore/backend/main.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "strings" + "time" ) func main() { @@ -35,7 +36,7 @@ func main() { // the caller reports one useful line and exits nonzero. func dispatch(args []string) error { if len(args) == 0 { - return fmt.Errorf("no command; expected catalog, install, remove, check, open, settings, or internal") + return fmt.Errorf("no command; expected catalog, install, remove, search, check, open, settings, or internal") } switch args[0] { case "catalog": @@ -44,6 +45,8 @@ func dispatch(args []string) error { return runInstall(providers(), args[1:]) case "remove": return runRemove(providers(), args[1:]) + case "search": + return runSearch(os.Stdout, providers(), args[1:]) case "warm": return runWarm(args[1:]) case "check": @@ -55,10 +58,34 @@ func dispatch(args []string) error { case "internal": return runInternal(args[1:]) default: - return fmt.Errorf("unknown command %q; expected catalog, install, remove, check, warm, open, settings, or internal", args[0]) + return fmt.Errorf("unknown command %q; expected catalog, install, remove, search, check, warm, open, settings, or internal", args[0]) } } +// snapshotPath is the instant-launch catalogue snapshot. +func snapshotPath() string { + return filepath.Join(extrasCacheDir(), "catalog.json") +} + +// encodeSnapshot renders a catalogue for the store: previews already in the +// cache become local file:// paths (no downloads, so the catalogue stays +// instant); the detached `warm` command fills the rest, so the next open renders +// from disk with no network image loads to abort at close. +func encodeSnapshot(cat Catalog) ([]byte, error) { + rewriteCachedAssets(cat.Items) + data, err := json.Marshal(cat) + if err != nil { + return nil, err + } + return append(data, '\n'), nil +} + +// writeSnapshot stores an encoded catalogue as the launch snapshot. Best effort: +// a failed write only costs the next launch its instant render. +func writeSnapshot(data []byte) { + _ = atomicWrite(snapshotPath(), data, 0o644) +} + func runCatalog(w io.Writer, provs []Provider, args []string) error { refresh := false category := "" @@ -96,7 +123,7 @@ func runCatalog(w io.Writer, provs []Provider, args []string) error { } // The full catalogue is snapshotted so a launch is instant and works offline; // the store's refresh button (--refresh) rebuilds it live and rewrites it. - snapshot := filepath.Join(extrasCacheDir(), "catalog.json") + snapshot := snapshotPath() if !refresh { // A healthy snapshot answers instantly. One flagged offline came from a // transient source failure; serving it again pins the store to "offline" @@ -107,27 +134,21 @@ func runCatalog(w io.Writer, provs []Provider, args []string) error { // which products run here, and after a compositor switch that answer is // the old one. One whose built-in styles read differently predates this // binary. - if data, err := os.ReadFile(snapshot); err == nil && len(data) > 0 && !snapshotOffline(data) && !snapshotForeignWindowManager(data) && !snapshotStaleBuiltins(data) { + if data, err := os.ReadFile(snapshot); err == nil && len(data) > 0 && !snapshotOffline(data) && !snapshotForeignWindowManager(data) && !snapshotStaleBuiltins(data) && !snapshotProviderMismatch(data, provs) && !snapshotFlatpakRemoteMismatch(data, provs) { _, err := w.Write(data) return err } } cat := BuildCatalog(ctx, provs, refresh) cat.Revision = catalogRevision(cat) - // Rewrite items to any preview already in the cache (no downloads, so the - // catalogue stays instant); the detached `warm` command fills the rest, so - // the next open renders from disk with no network image loads to abort at - // close. - rewriteCachedAssets(cat.Items) - data, err := json.Marshal(cat) + data, err := encodeSnapshot(cat) if err != nil { return err } - data = append(data, '\n') // Only snapshot a catalogue that produced items, so a failed first fetch is // retried live next launch instead of caching an empty store. if len(cat.Items) > 0 { - _ = atomicWrite(snapshot, data, 0o644) + writeSnapshot(data) // A freshly built catalogue is the baseline the user is now looking at, // so acknowledge it: the refresh dot stays clear until upstream advances. writeSeenRevision(cat.Revision) @@ -136,6 +157,81 @@ func runCatalog(w io.Writer, provs []Provider, args []string) error { return err } +func snapshotProviderMismatch(data []byte, provs []Provider) bool { + var s struct { + Categories []struct { + ID string `json:"id"` + } `json:"categories"` + } + if json.Unmarshal(data, &s) != nil || len(s.Categories) != len(provs) { + return true + } + for i, p := range provs { + if s.Categories[i].ID != p.Category().ID { + return true + } + } + return false +} + +func snapshotFlatpakRemoteMismatch(data []byte, provs []Provider) bool { + var p flatpakProvider + found := false + for _, provider := range provs { + if flatpak, ok := provider.(flatpakProvider); ok { + p, found = flatpak, true + break + } + } + if !found { + return false + } + var snapshot struct { + Items []struct { + Category string `json:"category"` + Installed bool `json:"installed"` + UnavailableReason string `json:"unavailableReason"` + Metadata map[string]any `json:"metadata"` + } `json:"items"` + } + if json.Unmarshal(data, &snapshot) != nil { + return true + } + present := p.present() + systemReason := "Flatpak is not installed on this system." + checked := map[string]string{} + if present { + checked = p.remoteReasons(context.Background(), nil) + _, systemReason = newFlatpakInstallation(checked) + } + for _, item := range snapshot.Items { + if item.Category != flatpakCategory { + continue + } + if !item.Installed { + if item.UnavailableReason != systemReason { + return true + } + continue + } + installation, ok := item.Metadata["installation"].(string) + if !ok || !present { + return true + } + if reason, ok := checked[installation]; ok { + if reason != "" { + return true + } + } else { + checked[installation] = p.flathubRemoteReason(context.Background(), installation) + if checked[installation] != "" { + return true + } + } + } + return false +} + // snapshotOffline reports whether a cached catalogue was written while a source // was failing, so a stale offline snapshot is retried live instead of pinning // the store offline on every launch. @@ -203,7 +299,15 @@ func runWarm(_ []string) error { // (runCatalog) acknowledges it. func runCheck(w io.Writer, provs []Provider) error { cat := BuildCatalog(context.Background(), provs, true) - revision := catalogRevision(cat) + cat.Revision = catalogRevision(cat) + // The probe already built the catalogue live, so keep that build as the + // launch snapshot. Installed state is local and never part of the revision, + // so a part installed outside the store (Settings, a terminal) would + // otherwise stay frozen as absent in the snapshot served on the next open. + if data, err := encodeSnapshot(cat); err == nil && len(cat.Items) > 0 { + writeSnapshot(data) + } + revision := cat.Revision seen := readSeenRevision() out := struct { Revision string `json:"revision"` @@ -282,6 +386,40 @@ func runInstall(provs []Provider, args []string) error { return finish(p.Install(ctx, id)) } +type searcher interface { + Search(ctx context.Context, query string) ([]Item, error) +} + +const searchTimeout = 20 * time.Second + +func runSearch(w io.Writer, provs []Provider, args []string) error { + if len(args) != 2 { + return fmt.Errorf("search needs ") + } + p, ok := providerFor(provs, args[0]) + if !ok { + return fmt.Errorf("unknown category %q", args[0]) + } + s, ok := p.(searcher) + if !ok { + return fmt.Errorf("category %q does not support search", args[0]) + } + ctx, cancel := context.WithTimeout(context.Background(), searchTimeout) + defer cancel() + items, err := s.Search(ctx, args[1]) + if err != nil { + return err + } + if items == nil { + items = []Item{} + } + return json.NewEncoder(w).Encode(struct { + Category string `json:"category"` + Query string `json:"query"` + Items []Item `json:"items"` + }{args[0], args[1], items}) +} + // localInstaller is a provider that can install a product from a local // directory (the plugins provider, for `ryoku plugin add`). type localInstaller interface { diff --git a/ryoku/apps/ryostore/backend/provider_bundles.go b/ryoku/apps/ryostore/backend/provider_bundles.go index 95457f2c95..953439f328 100644 --- a/ryoku/apps/ryostore/backend/provider_bundles.go +++ b/ryoku/apps/ryostore/backend/provider_bundles.go @@ -261,8 +261,12 @@ func defaultBundleStatus(ctx context.Context, overlay string) bundleStatus { } // launchBundleInstall runs the actuator's bundle install in a floating kitty -// terminal, detached into its own session so it owns the sudo prompt and -// long-running output independently of this short-lived process. +// terminal, in its own session so it owns the sudo prompt and long-running +// output. It waits for that terminal to close: the store refreshes the +// catalogue when the install really ends, not the instant kitty is spawned, +// and an install-all queue runs one bundle at a time instead of a terminal +// per item. The separate session keeps the install alive if the store closes +// mid-run. func launchBundleInstall(id string, only []string) error { args := []string{"--class", "ryostore", "-e", "ryostore-install", "install", "bundle", id} if len(only) > 0 { @@ -270,8 +274,5 @@ func launchBundleInstall(id string, only []string) error { } cmd := exec.Command("kitty", args...) cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} - if err := cmd.Start(); err != nil { - return err - } - return cmd.Process.Release() + return cmd.Run() } diff --git a/ryoku/apps/ryostore/backend/provider_bundles_test.go b/ryoku/apps/ryostore/backend/provider_bundles_test.go index b14d8cd4e2..f6b3aa5176 100644 --- a/ryoku/apps/ryostore/backend/provider_bundles_test.go +++ b/ryoku/apps/ryostore/backend/provider_bundles_test.go @@ -193,6 +193,35 @@ func TestBundleProviderInstallComponents(t *testing.T) { } } +// TestLaunchBundleInstallWaitsForTheTerminal proves the launcher hands the +// install to the terminal and returns only when it closes, so the refresh +// after an install reads finished state instead of a racing snapshot, and an +// install-all queue cannot spawn a terminal per item. +func TestLaunchBundleInstallWaitsForTheTerminal(t *testing.T) { + dir := t.TempDir() + argsFile := filepath.Join(dir, "args") + script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + argsFile + "\nsleep 0.2\n" + if err := os.WriteFile(filepath.Join(dir, "kitty"), []byte(script), 0o755); err != nil { + t.Fatalf("write fake terminal: %v", err) + } + t.Setenv("PATH", dir+":"+os.Getenv("PATH")) + + start := time.Now() + if err := launchBundleInstall("demo", []string{"steam", "lutris"}); err != nil { + t.Fatalf("launchBundleInstall: %v", err) + } + if elapsed := time.Since(start); elapsed < 150*time.Millisecond { + t.Fatalf("launcher returned in %s, want it to wait for the terminal", elapsed) + } + got, err := os.ReadFile(argsFile) + if err != nil { + t.Fatalf("fake terminal never ran: %v", err) + } + if want := "--class ryostore -e ryostore-install install bundle demo --only steam,lutris\n"; string(got) != want { + t.Fatalf("terminal args = %q, want %q", got, want) + } +} + // TestBundleProviderBrowseFetchesRegistryOnly proves browsing is a single // registry request: no per-bundle definition and no installer is fetched to // render the catalogue. diff --git a/ryoku/apps/ryostore/backend/provider_flatpak.go b/ryoku/apps/ryostore/backend/provider_flatpak.go new file mode 100644 index 0000000000..f5ded1f20c --- /dev/null +++ b/ryoku/apps/ryostore/backend/provider_flatpak.go @@ -0,0 +1,643 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + "time" + "unicode/utf8" +) + +const ( + flatpakCategory = "flatpak" + flatpakRemote = "flathub" + flathubRepoURL = "https://dl.flathub.org/repo/" + flathubAPI = "https://flathub.org/api/v2" + flathubPageSize = 300 + flathubSearchHits = 30 + flathubQueryMin = 2 + flathubQueryMax = 100 + flathubCache = "flathub-popular.json" + flatpakTimeout = 30 * time.Minute + flatpakUpdateProbe = 20 * time.Second +) + +var flatpakIDPattern = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+){2,}$`) + +func validFlatpakID(id string) bool { return len(id) <= 255 && flatpakIDPattern.MatchString(id) } + +type flathubHit struct { + AppID string `json:"app_id"` + Name string `json:"name"` + Summary string `json:"summary"` + Description string `json:"description"` + Type string `json:"type"` + Icon string `json:"icon"` + License string `json:"project_license"` + Developer string `json:"developer_name"` + MainCategories any `json:"main_categories"` + SubCategories any `json:"sub_categories"` + Verified bool `json:"verification_verified"` + Arches []string `json:"arches"` + AddedAt int64 `json:"added_at"` + InstallsLastMonth int64 `json:"installs_last_month"` +} + +type flathubCollection struct { + Hits []flathubHit `json:"hits"` +} + +type installedFlatpak struct { + ID string + Name string + Version string + Installation string + Origin string +} + +type flatpakProvider struct { + api string + present func() bool + installed func(ctx context.Context) ([]installedFlatpak, error) + updates func(ctx context.Context, installations []string) map[string]bool + run func(ctx context.Context, args ...string) ([]byte, error) +} + +func newFlatpakProvider() flatpakProvider { + p := flatpakProvider{api: flathubAPI} + p.present = func() bool { _, err := exec.LookPath("flatpak"); return err == nil } + p.run = runFlatpak + p.installed = func(ctx context.Context) ([]installedFlatpak, error) { + out, err := runFlatpak(ctx, "list", "--app", "--columns=application:full,name:full,version:full,installation:full,origin:full") + if err != nil { + return nil, err + } + return parseFlatpakList(out), nil + } + p.updates = func(ctx context.Context, installations []string) map[string]bool { + return pendingFlatpakUpdates(ctx, p.run, installations) + } + return p +} + +func pendingFlatpakUpdates(ctx context.Context, run func(context.Context, ...string) ([]byte, error), installations []string) map[string]bool { + pending := map[string]bool{} + for _, installation := range installations { + ctx, cancel := context.WithTimeout(ctx, flatpakUpdateProbe) + out, err := run(ctx, "remote-ls", "--updates", "--app", "--columns=application:full,origin:full", installationFlag(installation)) + cancel() + if err != nil { + continue + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Split(line, "\t") + if len(fields) == 2 && strings.TrimSpace(fields[1]) == flatpakRemote { + if id := strings.TrimSpace(fields[0]); validFlatpakID(id) { + pending[installation+"\x00"+id] = true + } + } + } + } + return pending +} + +func installationFlag(installation string) string { + switch installation { + case "user": + return "--user" + case "", "system": + return "--system" + } + return "--installation=" + installation +} + +func (flatpakProvider) Category() Category { + return Category{ + ID: flatpakCategory, + Name: "Apps", + Group: "EXTEND", + Description: "Popular applications from Flathub, installed with Flatpak. Flathub is a third-party source: Ryostore does not review these.", + } +} + +func runFlatpak(ctx context.Context, args ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, "flatpak", args...) + cmd.Env = append(os.Environ(), "LC_ALL=C") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + err := cmd.Run() + return out.Bytes(), err +} + +func parseFlatpakList(out []byte) []installedFlatpak { + var apps []installedFlatpak + for _, line := range strings.Split(string(out), "\n") { + f := strings.Split(line, "\t") + if len(f) < 5 || !validFlatpakID(strings.TrimSpace(f[0])) { + continue + } + apps = append(apps, installedFlatpak{ + ID: strings.TrimSpace(f[0]), + Name: strings.TrimSpace(f[1]), + Version: strings.TrimSpace(f[2]), + Installation: strings.TrimSpace(f[3]), + Origin: strings.TrimSpace(f[4]), + }) + } + return apps +} + +func (p flatpakProvider) Load(ctx context.Context, refresh bool) ([]Item, SourceState, error) { + hits, state, err := p.popular(ctx, refresh) + var installed []installedFlatpak + if p.present() && p.installed != nil { + installed, _ = p.installed(ctx) + } + if err != nil && len(installed) == 0 { + return nil, state, err + } + + remoteReasons := p.remoteReasons(ctx, installed) + owned := ownedFlathub(installed, remoteReasons) + _, unavailable := newFlatpakInstallation(remoteReasons) + if !p.present() { + unavailable = "Flatpak is not installed on this system." + } + updatable := map[string]bool{} + if p.present() && p.updates != nil && len(owned) > 0 { + updatable = p.updates(ctx, installationsOf(owned)) + } + items := flatpakItems(hits, owned, unavailable, updatable) + seen := map[string]bool{} + for _, item := range items { + seen[item.ID] = true + } + ids := make([]string, 0, len(owned)) + for id := range owned { + if !seen[id] { + ids = append(ids, id) + } + } + sort.Strings(ids) + for _, id := range ids { + item := flatpakInstalledItem(owned[id]) + item.UpdateAvailable = updatable[owned[id].Installation+"\x00"+id] + items = append(items, item) + } + return items, state, nil +} + +// When an ID exists in both installations, keep the user's existing copy. +func ownedFlathub(installed []installedFlatpak, remoteReasons map[string]string) map[string]installedFlatpak { + owned := map[string]installedFlatpak{} + for _, app := range installed { + if app.Origin == flatpakRemote && remoteReasons[app.Installation] == "" && + (owned[app.ID].ID == "" || app.Installation == "user") { + owned[app.ID] = app + } + } + return owned +} + +func (p flatpakProvider) remoteReasons(ctx context.Context, installed []installedFlatpak) map[string]string { + reasons := map[string]string{} + if !p.present() { + return reasons + } + reasons["system"] = p.flathubRemoteReason(ctx, "system") + if reasons["system"] != "" { + reasons["user"] = p.flathubRemoteReason(ctx, "user") + } + for _, app := range installed { + if app.Origin == flatpakRemote { + if _, checked := reasons[app.Installation]; !checked { + reasons[app.Installation] = p.flathubRemoteReason(ctx, app.Installation) + } + } + } + return reasons +} + +func (p flatpakProvider) holdingFlathub(ctx context.Context, apps []installedFlatpak, id string) (installedFlatpak, string, bool) { + var selected installedFlatpak + for _, app := range apps { + if app.ID == id && app.Origin == flatpakRemote && (selected.ID == "" || app.Installation == "user") { + selected = app + } + } + if selected.ID == "" { + return installedFlatpak{}, "", false + } + reason := p.flathubRemoteReason(ctx, selected.Installation) + if reason == "" { + return selected, "", true + } + for _, app := range apps { + if app.ID == id && app.Origin == flatpakRemote && app.Installation != selected.Installation && + p.flathubRemoteReason(ctx, app.Installation) == "" { + return app, "", true + } + } + return installedFlatpak{}, reason, true +} + +func newFlatpakInstallation(reasons map[string]string) (string, string) { + if reasons["system"] == "" { + return "system", "" + } + if reason, checked := reasons["user"]; checked && reason == "" { + return "user", "" + } + return "", reasons["system"] +} + +func (p flatpakProvider) flathubRemoteReason(ctx context.Context, installation string) string { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + out, err := p.run(ctx, "remotes", "--columns=name,url", installationFlag(installation)) + if err != nil { + return "Could not check the " + installation + " Flathub remote." + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Split(line, "\t") + if len(fields) != 2 || strings.TrimSpace(fields[0]) != flatpakRemote { + continue + } + if remoteURL := strings.TrimSpace(fields[1]); remoteURL == flathubRepoURL || remoteURL == strings.TrimSuffix(flathubRepoURL, "/") { + return "" + } + return "The " + installation + " flathub remote does not point to the official Flathub repository." + } + if installation == "system" { + return "The system Flathub remote is missing. Run ryoku doctor to set it up." + } + return "The " + installation + " Flathub remote is missing." +} + +func flatpakItems(hits []flathubHit, owned map[string]installedFlatpak, unavailable string, updatable map[string]bool) []Item { + arch := flathubArch() + items := make([]Item, 0, len(hits)) + seen := map[string]bool{} + for _, hit := range hits { + if !validFlatpakID(hit.AppID) || seen[hit.AppID] || hit.Type != "desktop-application" || !hasArch(hit.Arches, arch) { + continue + } + seen[hit.AppID] = true + item := flatpakItem(hit, owned[hit.AppID], unavailable) + item.UpdateAvailable = item.Installed && updatable[owned[hit.AppID].Installation+"\x00"+hit.AppID] + items = append(items, item) + } + return items +} + +func installationsOf(owned map[string]installedFlatpak) []string { + set := map[string]bool{} + for _, app := range owned { + set[app.Installation] = true + } + names := make([]string, 0, len(set)) + for name := range set { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func flathubArch() string { + switch runtime.GOARCH { + case "amd64": + return "x86_64" + case "arm64": + return "aarch64" + } + return runtime.GOARCH +} + +func hasArch(arches []string, arch string) bool { + for _, a := range arches { + if a == arch { + return true + } + } + return false +} + +func (p flatpakProvider) popular(ctx context.Context, refresh bool) ([]flathubHit, SourceState, error) { + file := filepath.Join(extrasCacheDir(), flathubCache) + if !refresh { + if info, err := os.Stat(file); err == nil && time.Since(info.ModTime()) < 6*time.Hour { + if hits, err := decodeFlathub(mustRead(file)); err == nil { + return hits, SourceState{}, nil + } + } + } + raw, err := p.fetchPopular(ctx) + if err == nil { + if hits, derr := decodeFlathub(raw); derr == nil { + _ = atomicWrite(file, raw, 0o644) + return hits, SourceState{}, nil + } else { + err = derr + } + } + if archived := mustRead(file); len(archived) > 0 { + if hits, derr := decodeFlathub(archived); derr == nil { + state := SourceState{Offline: true} + if info, serr := os.Stat(file); serr == nil { + state.CachedAt = info.ModTime().UTC().Format(time.RFC3339) + } + return hits, state, nil + } + } + return nil, SourceState{}, fmt.Errorf("flathub: %w", err) +} + +func mustRead(path string) []byte { + f, err := os.Open(path) + if err != nil { + return nil + } + defer f.Close() + b, err := io.ReadAll(io.LimitReader(f, maxBody+1)) + if err != nil || len(b) > maxBody { + return nil + } + return b +} + +func decodeFlathub(raw []byte) ([]flathubHit, error) { + var c flathubCollection + if err := json.Unmarshal(raw, &c); err != nil { + return nil, err + } + if len(c.Hits) == 0 { + return nil, errors.New("empty collection") + } + return c.Hits, nil +} + +func (p flatpakProvider) fetchPopular(ctx context.Context) ([]byte, error) { + return p.flathub(ctx, http.MethodGet, fmt.Sprintf("/collection/popular?page=1&per_page=%d", flathubPageSize), nil) +} + +func (p flatpakProvider) flathub(ctx context.Context, method, path string, payload any) ([]byte, error) { + base, err := url.Parse(p.api) + if err != nil { + return nil, err + } + endpoint := strings.TrimRight(p.api, "/") + path + ctx, cancel := context.WithTimeout(ctx, cacheTimeout) + defer cancel() + var body io.Reader + if payload != nil { + encoded, err := json.Marshal(payload) + if err != nil { + return nil, err + } + body = bytes.NewReader(encoded) + } + req, err := http.NewRequestWithContext(ctx, method, endpoint, body) + if err != nil { + return nil, err + } + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + client := http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 || req.URL.Scheme != base.Scheme || req.URL.Host != base.Host { + return errors.New("Flathub redirected outside its API origin") + } + return nil + }} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %s", endpoint, resp.Status) + } + data, err := io.ReadAll(io.LimitReader(resp.Body, maxBody+1)) + if err != nil { + return nil, err + } + if len(data) > maxBody { + return nil, fmt.Errorf("%s: response over %d bytes", endpoint, maxBody) + } + return data, nil +} + +func flathubIcon(raw string) string { + u, err := url.Parse(raw) + if err != nil || u.Scheme != "https" || u.Host != "dl.flathub.org" || u.User != nil { + return "" + } + return raw +} + +func flatpakItem(hit flathubHit, owned installedFlatpak, unavailable string) Item { + verification := "unverified" + if hit.Verified { + verification = "verified" + } + item := Item{ + ID: hit.AppID, + Category: flatpakCategory, + Name: firstNonEmpty(hit.Name, owned.Name, hit.AppID), + Summary: strings.TrimSpace(hit.Summary), + Description: strings.Join(strings.Fields(hit.Description), " "), + Art: flathubIcon(hit.Icon), + Author: hit.Developer, + Upstream: "https://flathub.org/apps/" + hit.AppID, + License: hit.License, + Kind: "flatpak", + VerificationStatus: verification, + Tags: flathubTags(hit), + Installed: owned.ID != "", + InstalledVersion: owned.Version, + Metadata: map[string]any{ + "provider": "Flathub", + "installsLastMonth": hit.InstallsLastMonth, + }, + } + if hit.AddedAt > 0 { + item.ListedAt = time.Unix(hit.AddedAt, 0).UTC().Format(time.RFC3339) + } + if owned.Installation != "" { + item.Metadata["installation"] = owned.Installation + } + if owned.ID == "" { + markFlatpakUsable(&item, unavailable) + } + return item +} + +func flatpakInstalledItem(app installedFlatpak) Item { + item := Item{ + ID: app.ID, + Category: flatpakCategory, + Name: firstNonEmpty(app.Name, app.ID), + Upstream: "https://flathub.org/apps/" + app.ID, + Kind: "flatpak", + VerificationStatus: "unknown", + Installed: true, + InstalledVersion: app.Version, + Metadata: map[string]any{"provider": "Flathub", "installation": app.Installation}, + } + return item +} + +func markFlatpakUsable(item *Item, reason string) { + if reason == "" { + return + } + item.Unavailable = true + item.UnavailableEdition = true + item.UnavailableReason = reason +} + +func flathubTags(hit flathubHit) []string { + var tags []string + add := func(v any) { + switch t := v.(type) { + case string: + if t != "" { + tags = append(tags, strings.ToLower(t)) + } + case []any: + for _, e := range t { + if s, ok := e.(string); ok && s != "" { + tags = append(tags, strings.ToLower(s)) + } + } + } + } + add(hit.MainCategories) + add(hit.SubCategories) + return tags +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return strings.TrimSpace(v) + } + } + return "" +} + +func (p flatpakProvider) Install(ctx context.Context, id string) error { + if !validFlatpakID(id) { + return fmt.Errorf("invalid flatpak id %q", id) + } + if !p.present() { + return errors.New("flatpak is not installed") + } + ctx, cancel := context.WithTimeout(ctx, flatpakTimeout) + defer cancel() + reportProgressNow(ctx, "apply", 0, 0) + apps, err := p.installed(ctx) + if err != nil { + return flatpakError("list installed applications", nil, err) + } + if app, reason, found := p.holdingFlathub(ctx, apps, id); found { + if reason != "" { + return errors.New(reason) + } + if out, err := p.run(ctx, "update", "--noninteractive", "--assumeyes", installationFlag(app.Installation), id); err != nil { + return flatpakError("update "+id, out, err) + } + return nil + } + installation, reason := newFlatpakInstallation(p.remoteReasons(ctx, nil)) + if reason != "" { + return errors.New(reason) + } + if out, err := p.run(ctx, "install", installationFlag(installation), "--noninteractive", "--assumeyes", flatpakRemote, id); err != nil { + return flatpakError("install "+id, out, err) + } + return nil +} + +func (p flatpakProvider) Remove(ctx context.Context, id string) error { + if !validFlatpakID(id) { + return fmt.Errorf("invalid flatpak id %q", id) + } + if !p.present() { + return errors.New("flatpak is not installed") + } + ctx, cancel := context.WithTimeout(ctx, flatpakTimeout) + defer cancel() + apps, err := p.installed(ctx) + if err != nil { + return flatpakError("list installed applications", nil, err) + } + app, reason, found := p.holdingFlathub(ctx, apps, id) + if !found { + return fmt.Errorf("%s is not installed from Flathub", id) + } + if reason != "" { + return errors.New(reason) + } + if out, err := p.run(ctx, "uninstall", "--noninteractive", "--assumeyes", installationFlag(app.Installation), id); err != nil { + return flatpakError("remove "+id, out, err) + } + return nil +} + +func flatpakError(action string, out []byte, err error) error { + detail := lastLine(out) + if detail == "" { + return fmt.Errorf("could not %s: %w", action, err) + } + return fmt.Errorf("could not %s: %s", action, detail) +} + +func lastLine(out []byte) string { + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + return strings.TrimSpace(lines[len(lines)-1]) +} + +func (p flatpakProvider) Search(ctx context.Context, query string) ([]Item, error) { + query = strings.Join(strings.Fields(query), " ") + if r := []rune(query); len(r) > flathubQueryMax { + query = string(r[:flathubQueryMax]) + } + if utf8.RuneCountInString(query) < flathubQueryMin { + return []Item{}, nil + } + raw, err := p.flathub(ctx, http.MethodPost, "/search", map[string]any{ + "query": query, "page": 1, "hits_per_page": flathubSearchHits, + }) + if err != nil { + return nil, fmt.Errorf("flathub search: %w", err) + } + var found flathubCollection + if err := json.Unmarshal(raw, &found); err != nil { + return nil, fmt.Errorf("flathub search: %w", err) + } + unavailable := "" + var installed []installedFlatpak + if p.present() && p.installed != nil { + installed, _ = p.installed(ctx) + } + reasons := p.remoteReasons(ctx, installed) + _, unavailable = newFlatpakInstallation(reasons) + if !p.present() { + unavailable = "Flatpak is not installed on this system." + } + return flatpakItems(found.Hits, ownedFlathub(installed, reasons), unavailable, nil), nil +} diff --git a/ryoku/apps/ryostore/backend/provider_flatpak_test.go b/ryoku/apps/ryostore/backend/provider_flatpak_test.go new file mode 100644 index 0000000000..1137088007 --- /dev/null +++ b/ryoku/apps/ryostore/backend/provider_flatpak_test.go @@ -0,0 +1,823 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "path/filepath" + "reflect" + "strings" + "testing" +) + +type flatpakFixture struct { + provider flatpakProvider + calls [][]string + failing bool + body map[string]any + remotes map[string]string + found map[string]any + searched []map[string]any +} + +func hit(id, name, kind string, verified bool, arches ...string) map[string]any { + return map[string]any{ + "app_id": id, "name": name, "summary": name + " summary", "type": kind, + "description": "A long\n description of " + name, + "icon": "https://dl.flathub.org/media/" + id + ".png", + "project_license": "GPL-3.0+", "developer_name": "Dev", + "main_categories": "graphics", "sub_categories": []string{"Photography"}, + "verification_verified": verified, "arches": arches, + "added_at": 1493820137, "installs_last_month": 42, + } +} + +func newFlatpakFixture(t *testing.T, installed []installedFlatpak) *flatpakFixture { + t.Helper() + t.Setenv("HOME", t.TempDir()) + t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache")) + arch := flathubArch() + fx := &flatpakFixture{remotes: map[string]string{"system": flathubRepoURL, "user": flathubRepoURL, "extra": flathubRepoURL}} + fx.body = map[string]any{"hits": []any{ + hit("org.example.Alpha", "Alpha", "desktop-application", true, arch, "other"), + hit("org.example.Beta", "Beta", "desktop-application", false, arch), + hit("org.example.Alpha", "Alpha again", "desktop-application", true, arch), + hit("org.example.Cli", "Cli", "console-application", true, arch), + hit("org.example.Foreign", "Foreign", "desktop-application", true, "riscv64"), + hit("--evil", "Evil", "desktop-application", true, arch), + }} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if fx.failing { + http.Error(w, "down", http.StatusBadGateway) + return + } + switch { + case r.Method == http.MethodGet && r.URL.Path == "/collection/popular": + _ = json.NewEncoder(w).Encode(fx.body) + case r.Method == http.MethodPost && r.URL.Path == "/search": + var sent map[string]any + _ = json.NewDecoder(r.Body).Decode(&sent) + fx.searched = append(fx.searched, sent) + _ = json.NewEncoder(w).Encode(fx.found) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + fx.provider = flatpakProvider{ + api: server.URL, + present: func() bool { return true }, + installed: func(context.Context) ([]installedFlatpak, error) { + return installed, nil + }, + run: func(_ context.Context, args ...string) ([]byte, error) { + fx.calls = append(fx.calls, args) + if args[0] == "remotes" { + installation := strings.TrimPrefix(args[2], "--installation=") + if args[2] == "--user" { + installation = "user" + } else if args[2] == "--system" { + installation = "system" + } + if remote := fx.remotes[installation]; remote != "" { + return []byte("community\thttps://example.org/repo/\nflathub\t" + remote + "\n"), nil + } + } + return nil, nil + }, + } + return fx +} + +func TestParseFlatpakList(t *testing.T) { + out := []byte("org.example.Alpha\tAlpha\t1.2\tsystem\tflathub\n" + + "not-an-id\tBad\t1\tuser\tflathub\n" + + "org.example.Short\tShort\n" + + "org.example.Beta\tBeta\t\tuser\tflathub-beta\n") + got := parseFlatpakList(out) + want := []installedFlatpak{ + {ID: "org.example.Alpha", Name: "Alpha", Version: "1.2", Installation: "system", Origin: "flathub"}, + {ID: "org.example.Beta", Name: "Beta", Installation: "user", Origin: "flathub-beta"}, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("parseFlatpakList = %+v, want %+v", got, want) + } +} + +func TestFlatpakLoadFiltersAndMarksInstalled(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Name: "Alpha", Version: "1.2", Installation: "system", Origin: "flathub"}, + {ID: "org.example.Zeta", Name: "Zeta", Version: "9", Installation: "user", Origin: "flathub"}, + {ID: "org.example.Beta", Name: "Beta", Version: "3", Installation: "user", Origin: "flathub-beta"}, + }) + items, state, err := fx.provider.Load(context.Background(), true) + if err != nil || state.Offline { + t.Fatalf("Load = %v, offline %v", err, state.Offline) + } + var ids []string + for _, it := range items { + ids = append(ids, it.ID) + } + wantIDs := []string{"org.example.Alpha", "org.example.Beta", "org.example.Zeta"} + if !reflect.DeepEqual(ids, wantIDs) { + t.Fatalf("ids = %v, want %v (console apps, other arches, duplicates and invalid ids are dropped; installed outside the ranking is appended)", ids, wantIDs) + } + + alpha, beta, zeta := items[0], items[1], items[2] + if !alpha.Installed || alpha.InstalledVersion != "1.2" || alpha.Metadata["installation"] != "system" { + t.Errorf("alpha installed state wrong: %+v", alpha) + } + if beta.Installed { + t.Errorf("beta is installed only from another remote, got Installed") + } + if !zeta.Installed || zeta.Name != "Zeta" || zeta.Metadata["installation"] != "user" { + t.Errorf("zeta should be listed as an installed extra: %+v", zeta) + } + if alpha.VerificationStatus != "verified" || beta.VerificationStatus != "unverified" { + t.Errorf("verification = %q / %q", alpha.VerificationStatus, beta.VerificationStatus) + } + if zeta.VerificationStatus != "unknown" { + t.Errorf("installed-only verification must not be guessed: %q", zeta.VerificationStatus) + } + if alpha.Description != "A long description of Alpha" { + t.Errorf("description whitespace not collapsed: %q", alpha.Description) + } + if alpha.Upstream != "https://flathub.org/apps/org.example.Alpha" || alpha.Kind != "flatpak" || alpha.License != "GPL-3.0+" { + t.Errorf("provenance fields wrong: %+v", alpha) + } + if !reflect.DeepEqual(alpha.Tags, []string{"graphics", "photography"}) { + t.Errorf("tags = %v", alpha.Tags) + } + if alpha.Unavailable { + t.Errorf("alpha should be available") + } +} + +func TestFlatpakCategoryCountsInstalled(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Name: "Alpha", Installation: "user", Origin: "flathub"}, + }) + cat := BuildCatalog(context.Background(), []Provider{fx.provider}, true) + if len(cat.Categories) != 1 || cat.Categories[0].ID != flatpakCategory { + t.Fatalf("categories = %+v", cat.Categories) + } + if got := cat.Categories[0]; got.Count != 2 || got.InstalledCount != 1 || got.Error != "" { + t.Fatalf("category = %+v", got) + } +} + +func TestFlatpakOfflineServesArchive(t *testing.T) { + fx := newFlatpakFixture(t, nil) + if _, _, err := fx.provider.Load(context.Background(), true); err != nil { + t.Fatal(err) + } + fx.failing = true + items, state, err := fx.provider.Load(context.Background(), true) + if err != nil || !state.Offline || state.CachedAt == "" || len(items) != 2 { + t.Fatalf("archive fallback = %d items, state %+v, err %v", len(items), state, err) + } +} + +func TestFlatpakFirstFetchFailureIsIsolatedToTheCategory(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.failing = true + if _, _, err := fx.provider.Load(context.Background(), true); err == nil { + t.Fatal("expected an error with no network, no archive and nothing installed") + } + + fx2 := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Alpha", Name: "Alpha", Installation: "user", Origin: "flathub"}}) + fx2.failing = true + items, _, err := fx2.provider.Load(context.Background(), true) + if err != nil || len(items) != 1 || !items[0].Installed { + t.Fatalf("installed apps must stay listed (and removable) offline: %v %+v", err, items) + } +} + +func TestFlatpakAbsentGreysItemsOut(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.provider.present = func() bool { return false } + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil || len(items) == 0 { + t.Fatalf("Load = %d items, %v", len(items), err) + } + for _, it := range items { + if !it.Unavailable || !it.UnavailableEdition || !strings.Contains(it.UnavailableReason, "Flatpak") { + t.Fatalf("%s should be unavailable without flatpak: %+v", it.ID, it) + } + } + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err == nil { + t.Fatal("Install must refuse without flatpak") + } +} + +func TestFlatpakInstallUsesSystemInstallation(t *testing.T) { + fx := newFlatpakFixture(t, nil) + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", "--system"}, + {"install", "--system", "--noninteractive", "--assumeyes", "flathub", "org.example.Alpha"}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("calls = %v, want %v", fx.calls, want) + } +} + +func TestFlatpakFallsBackToExistingUserRemote(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}}) + delete(fx.remotes, "system") + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil { + t.Fatal(err) + } + if items[0].Unavailable || !items[2].Installed || items[2].Unavailable { + t.Fatalf("canonical user remote should keep apps available: %+v", items) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", "--system"}, + {"remotes", "--columns=name,url", "--user"}, + {"install", "--user", "--noninteractive", "--assumeyes", "flathub", "org.example.Alpha"}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("new app must use existing user remote, without adding one: %v", fx.calls) + } +} + +func TestFlatpakWithoutCanonicalRemoteExplainsDoctorSetup(t *testing.T) { + fx := newFlatpakFixture(t, nil) + delete(fx.remotes, "system") + delete(fx.remotes, "user") + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil || !items[0].Unavailable || !strings.Contains(items[0].UnavailableReason, "ryoku doctor") { + t.Fatalf("missing remotes must explain system setup: %+v, %v", items, err) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err == nil || !strings.Contains(err.Error(), "ryoku doctor") { + t.Fatalf("new installation without remote = %v", err) + } + if !reflect.DeepEqual(fx.calls, [][]string{{"remotes", "--columns=name,url", "--system"}, {"remotes", "--columns=name,url", "--user"}}) { + t.Fatalf("no remote-add or install is allowed: %v", fx.calls) + } +} + +func TestFlatpakUserInstallWorksWithoutSystemRemote(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}}) + delete(fx.remotes, "system") + if err := fx.provider.Install(context.Background(), "org.example.Zeta"); err != nil { + t.Fatal(err) + } + if err := fx.provider.Remove(context.Background(), "org.example.Zeta"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", "--user"}, + {"update", "--noninteractive", "--assumeyes", "--user", "org.example.Zeta"}, + {"remotes", "--columns=name,url", "--user"}, + {"uninstall", "--noninteractive", "--assumeyes", "--user", "org.example.Zeta"}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("existing user app actions = %v, want %v", fx.calls, want) + } +} + +func TestFlatpakRemoteIdentityRejectsImpostors(t *testing.T) { + for _, remote := range []string{"https://evil.example/repo/", "http://dl.flathub.org/repo/", "https://dl.flathub.org/repo/extra", "https://dl.flathub.org/repo//"} { + t.Run(remote, func(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.remotes["system"] = remote + delete(fx.remotes, "user") + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil { + t.Fatal(err) + } + if !items[0].Unavailable || !strings.Contains(items[0].UnavailableReason, "official Flathub") { + t.Fatalf("spoofed remote must be unavailable: %+v", items[0]) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err == nil || !strings.Contains(err.Error(), "official Flathub") { + t.Fatalf("spoofed installation accepted: %v", err) + } + if !reflect.DeepEqual(fx.calls, [][]string{{"remotes", "--columns=name,url", "--system"}, {"remotes", "--columns=name,url", "--user"}}) { + t.Fatalf("spoofed install ran other commands: %v", fx.calls) + } + }) + } +} + +func TestFlatpakUsesCanonicalUserRemoteWhenSystemIsImpostor(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.remotes["system"] = "https://example.org/repo/" + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil || items[0].Unavailable { + t.Fatalf("canonical user remote should still be usable: %+v, %v", items, err) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err != nil { + t.Fatal(err) + } + if got := fx.calls[len(fx.calls)-1]; !reflect.DeepEqual(got, []string{"install", "--user", "--noninteractive", "--assumeyes", flatpakRemote, "org.example.Alpha"}) { + t.Fatalf("installation should use canonical user remote: %v", fx.calls) + } +} + +func TestFlatpakAcceptsCanonicalRemoteWithoutTrailingSlash(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.remotes["system"] = strings.TrimSuffix(flathubRepoURL, "/") + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err != nil { + t.Fatal(err) + } +} + +func TestFlatpakUserRemoteImpostorIsNotAFlathubInstall(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Alpha", Installation: "user", Origin: "flathub"}}) + fx.remotes["user"] = "https://example.org/repo/" + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil { + t.Fatal(err) + } + if items[0].Installed || items[0].Metadata["installation"] != nil { + t.Fatalf("spoofed user install claimed as Flathub: %+v", items[0]) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err == nil || !strings.Contains(err.Error(), "official Flathub") { + t.Fatalf("spoofed update accepted: %v", err) + } + if err := fx.provider.Remove(context.Background(), "org.example.Alpha"); err == nil || !strings.Contains(err.Error(), "official Flathub") { + t.Fatalf("spoofed removal accepted: %v", err) + } + for _, call := range fx.calls { + if call[0] == "update" || call[0] == "uninstall" || call[0] == "install" { + t.Fatalf("spoofed app mutated: %v", fx.calls) + } + } + fx.found = searchResult(hit("org.example.Alpha", "Alpha", "desktop-application", true, flathubArch())) + results, err := fx.provider.Search(context.Background(), "alpha") + if err != nil || len(results) != 1 || results[0].Installed { + t.Fatalf("search claimed spoofed installation: %+v, %v", results, err) + } +} + +func TestFlatpakRemoteProbeFailureRefusesInstallation(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.provider.run = func(_ context.Context, args ...string) ([]byte, error) { + fx.calls = append(fx.calls, args) + return nil, errors.New("flatpak remotes failed") + } + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil || !items[0].Unavailable || !strings.Contains(items[0].UnavailableReason, "Could not check") { + t.Fatalf("remote probe failure must be unavailable: %+v, %v", items, err) + } + fx.calls = nil + if err := fx.provider.Install(context.Background(), "org.example.Alpha"); err == nil || !strings.Contains(err.Error(), "Could not check") { + t.Fatalf("remote probe failure accepted: %v", err) + } + if len(fx.calls) != 2 || fx.calls[0][0] != "remotes" || fx.calls[1][0] != "remotes" { + t.Fatalf("probe failure reached installation: %v", fx.calls) + } +} + +func TestFlatpakSnapshotRechecksRemoteIdentity(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}}) + fx.provider.updates = nil + var first strings.Builder + if err := runCatalog(&first, []Provider{fx.provider}, nil); err != nil { + t.Fatal(err) + } + fx.remotes["user"] = "https://example.org/repo/" + delete(fx.remotes, "system") + var second strings.Builder + if err := runCatalog(&second, []Provider{fx.provider}, nil); err != nil { + t.Fatal(err) + } + var cat Catalog + if err := json.Unmarshal([]byte(second.String()), &cat); err != nil { + t.Fatal(err) + } + if len(cat.Items) != 2 || cat.Items[0].UnavailableReason == "" { + t.Fatalf("snapshot retained a stale install or missed the missing system remote: %+v", cat.Items) + } + fx.remotes["system"] = flathubRepoURL + fx.remotes["user"] = flathubRepoURL + var third strings.Builder + if err := runCatalog(&third, []Provider{fx.provider}, nil); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(third.String()), &cat); err != nil { + t.Fatal(err) + } + if cat.Items[0].Unavailable || !cat.Items[2].Installed { + t.Fatalf("snapshot pinned bad remote after repair: %+v", cat.Items) + } +} + +func TestFlatpakRefusesIDsThatCouldBeFlags(t *testing.T) { + fx := newFlatpakFixture(t, nil) + for _, id := range []string{"--remove-all", "--evil.org.App", "-y", "a.b", "org.example.Alpha --user", "", "org/example/Alpha"} { + if err := fx.provider.Install(context.Background(), id); err == nil { + t.Errorf("Install(%q) should fail", id) + } + if err := fx.provider.Remove(context.Background(), id); err == nil { + t.Errorf("Remove(%q) should fail", id) + } + } + if len(fx.calls) != 0 { + t.Fatalf("flatpak must not run for a refused id: %v", fx.calls) + } +} + +func TestFlatpakInstallReportsLastOutputLine(t *testing.T) { + fx := newFlatpakFixture(t, nil) + run := fx.provider.run + fx.provider.run = func(ctx context.Context, args ...string) ([]byte, error) { + if args[0] == "install" { + return []byte("Looking for matches…\nerror: No remote refs found for 'org.example.Alpha'\n"), errors.New("exit status 1") + } + return run(ctx, args...) + } + err := fx.provider.Install(context.Background(), "org.example.Alpha") + if err == nil || !strings.Contains(err.Error(), "No remote refs found") { + t.Fatalf("error = %v", err) + } +} + +func TestFlatpakRemoveTargetsTheHoldingInstallation(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Installation: "system", Origin: "flathub"}, + {ID: "org.example.Beta", Installation: "user", Origin: "flathub"}, + }) + for id, scope := range map[string]string{"org.example.Alpha": "--system", "org.example.Beta": "--user"} { + fx.calls = nil + if err := fx.provider.Remove(context.Background(), id); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", scope}, + {"uninstall", "--noninteractive", "--assumeyes", scope, id}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("Remove(%s) calls = %v, want %v", id, fx.calls, want) + } + } + if err := fx.provider.Remove(context.Background(), "org.example.Missing"); err == nil { + t.Fatal("removing an app that is not installed should fail") + } +} + +func TestFlatpakIsRegistered(t *testing.T) { + if _, ok := providerFor(providers(), flatpakCategory); !ok { + t.Fatal("flatpak provider is not registered") + } + if !storeSection(flatpakCategory) { + t.Fatal("flatpak is not an openable store section") + } + if shouldWarmAssets(Item{Category: flatpakCategory}) { + t.Fatal("flatpak icons must load lazily, not warm for every listed app") + } +} + +func TestInstallationFlag(t *testing.T) { + for installation, want := range map[string]string{ + "user": "--user", "system": "--system", "": "--system", "extra": "--installation=extra", + } { + if got := installationFlag(installation); got != want { + t.Errorf("installationFlag(%q) = %q, want %q", installation, got, want) + } + } +} + +func TestPendingFlatpakUpdatesAsksEachInstallation(t *testing.T) { + var asked [][]string + run := func(_ context.Context, args ...string) ([]byte, error) { + asked = append(asked, args) + switch args[len(args)-1] { + case "--user": + return []byte("org.example.Alpha\tflathub\norg.example.Beta\tcommunity\nnot-an-id\tflathub\n"), nil + case "--system": + return nil, errors.New("exit status 1") + } + return []byte("org.example.Gamma\tflathub\n"), nil + } + got := pendingFlatpakUpdates(context.Background(), run, []string{"user", "system", "extra"}) + want := map[string]bool{"user\x00org.example.Alpha": true, "extra\x00org.example.Gamma": true} + if !reflect.DeepEqual(got, want) { + t.Fatalf("pending = %v, want %v (a failing installation is skipped, bad lines are ignored)", got, want) + } + wantAsked := [][]string{ + {"remote-ls", "--updates", "--app", "--columns=application:full,origin:full", "--user"}, + {"remote-ls", "--updates", "--app", "--columns=application:full,origin:full", "--system"}, + {"remote-ls", "--updates", "--app", "--columns=application:full,origin:full", "--installation=extra"}, + } + if !reflect.DeepEqual(asked, wantAsked) { + t.Fatalf("asked = %v, want %v", asked, wantAsked) + } +} + +func TestFlatpakLoadFlagsOnlyInstalledAppsWithAnUpdate(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Name: "Alpha", Installation: "system", Origin: "flathub"}, + {ID: "org.example.Zeta", Name: "Zeta", Installation: "user", Origin: "flathub"}, + }) + var installations []string + fx.provider.updates = func(_ context.Context, names []string) map[string]bool { + installations = names + return map[string]bool{"system\x00org.example.Alpha": true, "user\x00org.example.Zeta": true, "user\x00org.example.Beta": true} + } + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil { + t.Fatal(err) + } + got := map[string]bool{} + for _, it := range items { + got[it.ID] = it.UpdateAvailable + } + want := map[string]bool{"org.example.Alpha": true, "org.example.Beta": false, "org.example.Zeta": true} + if !reflect.DeepEqual(got, want) { + t.Fatalf("updateAvailable = %v, want %v (Beta is not installed, so an update is meaningless)", got, want) + } + if !reflect.DeepEqual(installations, []string{"system", "user"}) { + t.Fatalf("installations probed = %v", installations) + } +} + +func TestFlatpakUpdateBelongsToSelectedInstallation(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Installation: "user", Origin: "flathub"}, + {ID: "org.example.Alpha", Installation: "system", Origin: "flathub"}, + }) + fx.provider.updates = func(_ context.Context, _ []string) map[string]bool { + return map[string]bool{"system\x00org.example.Alpha": true} + } + items, _, err := fx.provider.Load(context.Background(), true) + if err != nil { + t.Fatal(err) + } + if items[0].UpdateAvailable || items[0].Metadata["installation"] != "user" { + t.Fatalf("system update presented for user installation: %+v", items[0]) + } +} + +func TestFlatpakLoadSkipsTheUpdateProbeWhenNothingIsInstalled(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.provider.updates = func(context.Context, []string) map[string]bool { + t.Fatal("no installed app, so no installation needs probing") + return nil + } + if _, _, err := fx.provider.Load(context.Background(), true); err != nil { + t.Fatal(err) + } +} + +func TestFlatpakInstallOnAnInstalledAppUpdatesIt(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Alpha", Installation: "system", Origin: "flathub"}, + {ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}, + }) + for id, scope := range map[string]string{"org.example.Alpha": "--system", "org.example.Zeta": "--user"} { + fx.calls = nil + if err := fx.provider.Install(context.Background(), id); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", scope}, + {"update", "--noninteractive", "--assumeyes", scope, id}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("Install(%s) calls = %v, want %v", id, fx.calls, want) + } + } +} + +func TestFlatpakUpdateFailureNamesTheCause(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Alpha", Installation: "user", Origin: "flathub"}}) + run := fx.provider.run + fx.provider.run = func(ctx context.Context, args ...string) ([]byte, error) { + if args[0] == "update" { + return []byte("error: Failed to update: no space left\n"), errors.New("exit status 1") + } + return run(ctx, args...) + } + err := fx.provider.Install(context.Background(), "org.example.Alpha") + if err == nil || !strings.Contains(err.Error(), "could not update org.example.Alpha") || !strings.Contains(err.Error(), "no space left") { + t.Fatalf("error = %v", err) + } +} + +func TestFlatpakRemoveInACustomInstallation(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Alpha", Installation: "extra", Origin: "flathub"}}) + if err := fx.provider.Remove(context.Background(), "org.example.Alpha"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", "--installation=extra"}, + {"uninstall", "--noninteractive", "--assumeyes", "--installation=extra", "org.example.Alpha"}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("calls = %v, want %v", fx.calls, want) + } +} + +func TestFlatpakRemoveDoesNotUninstallOtherRemotes(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Zeta", Installation: "user", Origin: "community"}}) + if err := fx.provider.Remove(context.Background(), "org.example.Zeta"); err == nil { + t.Fatal("a non-Flathub app must not be removed through the Flathub category") + } + if len(fx.calls) != 0 { + t.Fatalf("unexpected commands: %v", fx.calls) + } +} + +func TestFlatpakDuplicateInstallationsPreferUser(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}, + {ID: "org.example.Zeta", Installation: "system", Origin: "flathub"}, + }) + if err := fx.provider.Install(context.Background(), "org.example.Zeta"); err != nil { + t.Fatal(err) + } + if err := fx.provider.Remove(context.Background(), "org.example.Zeta"); err != nil { + t.Fatal(err) + } + want := [][]string{ + {"remotes", "--columns=name,url", "--user"}, + {"update", "--noninteractive", "--assumeyes", "--user", "org.example.Zeta"}, + {"remotes", "--columns=name,url", "--user"}, + {"uninstall", "--noninteractive", "--assumeyes", "--user", "org.example.Zeta"}, + } + if !reflect.DeepEqual(fx.calls, want) { + t.Fatalf("calls = %v, want %v", fx.calls, want) + } +} + +func TestFlatpakRemoveFailure(t *testing.T) { + fx := newFlatpakFixture(t, []installedFlatpak{{ID: "org.example.Zeta", Installation: "user", Origin: "flathub"}}) + run := fx.provider.run + fx.provider.run = func(ctx context.Context, args ...string) ([]byte, error) { + if args[0] == "uninstall" { + fx.calls = append(fx.calls, args) + return []byte("error: not removable\n"), errors.New("exit status 1") + } + return run(ctx, args...) + } + if err := fx.provider.Remove(context.Background(), "org.example.Zeta"); err == nil { + t.Fatal("expected the removal error") + } + if len(fx.calls) != 2 { + t.Fatalf("failed removal ran extra commands: %v", fx.calls) + } +} + +func searchResult(hits ...map[string]any) map[string]any { + list := make([]any, len(hits)) + for i, h := range hits { + list[i] = h + } + return map[string]any{"hits": list} +} + +func TestFlatpakSearchFindsAppsOutsideTheRanking(t *testing.T) { + arch := flathubArch() + fx := newFlatpakFixture(t, []installedFlatpak{ + {ID: "org.example.Rare", Name: "Rare", Version: "2", Installation: "user", Origin: "flathub"}, + }) + fx.found = searchResult( + hit("org.example.Rare", "Rare", "desktop-application", true, arch), + hit("org.example.Obscure", "Obscure", "desktop-application", false, arch), + hit("org.example.Cli", "Cli", "console-application", true, arch), + hit("org.example.Foreign", "Foreign", "desktop-application", true, "riscv64"), + hit("--evil", "Evil", "desktop-application", true, arch), + ) + items, err := fx.provider.Search(context.Background(), " rare thing ") + if err != nil { + t.Fatal(err) + } + var ids []string + for _, it := range items { + ids = append(ids, it.ID) + } + if want := []string{"org.example.Rare", "org.example.Obscure"}; !reflect.DeepEqual(ids, want) { + t.Fatalf("ids = %v, want %v", ids, want) + } + if !items[0].Installed || items[0].InstalledVersion != "2" || items[1].Installed { + t.Fatalf("installed state not joined: %+v / %+v", items[0], items[1]) + } + if len(fx.searched) != 1 || fx.searched[0]["query"] != "rare thing" || fx.searched[0]["hits_per_page"] != float64(flathubSearchHits) { + t.Fatalf("request = %v", fx.searched) + } +} + +func TestFlatpakSearchIgnoresTooShortQueriesWithoutAsking(t *testing.T) { + fx := newFlatpakFixture(t, nil) + for _, q := range []string{"", " ", "a", " b "} { + items, err := fx.provider.Search(context.Background(), q) + if err != nil || len(items) != 0 || items == nil { + t.Fatalf("Search(%q) = %v, %v; want an empty non-nil list", q, items, err) + } + } + if len(fx.searched) != 0 { + t.Fatalf("a short query must not reach Flathub: %v", fx.searched) + } +} + +func TestFlatpakSearchCapsTheQueryLength(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.found = searchResult() + if _, err := fx.provider.Search(context.Background(), strings.Repeat("é", 500)); err != nil { + t.Fatal(err) + } + if got := fx.searched[0]["query"].(string); len([]rune(got)) != flathubQueryMax { + t.Fatalf("query is %d runes, want %d", len([]rune(got)), flathubQueryMax) + } +} + +func TestFlatpakSearchReportsAnUnreachableFlathub(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.failing = true + if _, err := fx.provider.Search(context.Background(), "discord"); err == nil || !strings.Contains(err.Error(), "flathub search") { + t.Fatalf("error = %v", err) + } +} + +type plainProvider struct{} + +func (plainProvider) Category() Category { return Category{ID: "plain"} } +func (plainProvider) Load(context.Context, bool) ([]Item, SourceState, error) { + return nil, SourceState{}, nil +} +func (plainProvider) Install(context.Context, string) error { return nil } +func (plainProvider) Remove(context.Context, string) error { return nil } + +func TestRunSearchPrintsItemsAndRejectsBadInvocations(t *testing.T) { + fx := newFlatpakFixture(t, nil) + fx.found = searchResult(hit("org.example.Obscure", "Obscure", "desktop-application", false, flathubArch())) + provs := []Provider{fx.provider, plainProvider{}} + + var out strings.Builder + if err := runSearch(&out, provs, []string{"flatpak", "-obscure"}); err != nil { + t.Fatal(err) + } + var got struct { + Category string `json:"category"` + Query string `json:"query"` + Items []Item `json:"items"` + } + if err := json.Unmarshal([]byte(out.String()), &got); err != nil { + t.Fatal(err) + } + if got.Category != "flatpak" || got.Query != "-obscure" || len(got.Items) != 1 || got.Items[0].ID != "org.example.Obscure" { + t.Fatalf("output = %+v (a query that looks like a flag is still just the query)", got) + } + + for name, args := range map[string][]string{ + "missing query": {"flatpak"}, + "unknown category": {"nope", "x"}, + "no search support": {"plain", "xx"}, + "too many arguments": {"flatpak", "a", "b"}, + } { + if err := runSearch(&strings.Builder{}, provs, args); err == nil { + t.Errorf("%s: expected an error", name) + } + } +} + +func TestFlatpakIconRejectsUntrustedOrigins(t *testing.T) { + for _, icon := range []string{ + "http://dl.flathub.org/icon.png", + "https://dl.flathub.org.evil.test/icon.png", + "file:///etc/passwd", + "https://127.0.0.1/private", + } { + if got := flathubIcon(icon); got != "" { + t.Errorf("icon %q accepted as %q", icon, got) + } + } + const icon = "https://dl.flathub.org/media/icon.png" + if got := flathubIcon(icon); got != icon { + t.Errorf("Flathub icon rejected: %q", got) + } +} + +func TestFlathubRejectsCrossOriginRedirect(t *testing.T) { + fetched := false + target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + fetched = true + })) + defer target.Close() + source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target.URL+"/private", http.StatusFound) + })) + defer source.Close() + p := flatpakProvider{api: source.URL} + if _, err := p.fetchPopular(context.Background()); err == nil { + t.Fatal("cross-origin redirect accepted") + } + if fetched { + t.Fatal("cross-origin target received a request") + } +} diff --git a/ryoku/apps/ryostore/backend/routing.go b/ryoku/apps/ryostore/backend/routing.go index a0e3eaab03..f8010c0aaf 100644 --- a/ryoku/apps/ryostore/backend/routing.go +++ b/ryoku/apps/ryostore/backend/routing.go @@ -11,7 +11,7 @@ var storeSections = map[string]struct{}{ "subshells": {}, "barstyles": {}, "docks": {}, "launchers": {}, "osds": {}, "notifications": {}, "sidebars": {}, "fastfetch": {}, "plugins": {}, "bundles": {}, "decors": {}, "launcher-images": {}, "fastfetch-emblems": {}, "ryotunes-skins": {}, - "vesktop-themes": {}, + "vesktop-themes": {}, "flatpak": {}, } func storeSection(section string) bool { diff --git a/ryoku/apps/ryostore/quickshell/App.qml b/ryoku/apps/ryostore/quickshell/App.qml index b8ab3c2cd5..71d47ef9a9 100644 --- a/ryoku/apps/ryostore/quickshell/App.qml +++ b/ryoku/apps/ryostore/quickshell/App.qml @@ -48,11 +48,49 @@ Rectangle { property string pendingRoute: "" onNavigationCategoriesChanged: if (app.pendingRoute !== "" && app.validRoute(app.pendingRoute)) app.openRoute(app.pendingRoute) - readonly property var searchableItems: Store.items + readonly property bool remoteScope: searchOpen && searchContext !== null + && String(searchContext.categoryID || "") === "flatpak" + readonly property string remoteQuery: remoteScope ? query.trim() : "" + readonly property var remoteMatches: { + const matches = {}; + if (remoteScope && Store.remoteResultQuery === remoteQuery) + Store.remoteItems.forEach(item => matches[StoreLogic.itemKey(item)] = true); + return matches; + } + readonly property var remoteExtras: { + if (!remoteScope || Store.remoteResultQuery !== remoteQuery) + return []; + const known = {}; + Store.items.forEach(item => known[StoreLogic.itemKey(item)] = true); + return Store.remoteItems + .filter(item => !known[StoreLogic.itemKey(item)]) + .map(item => { + const copy = {}; + Object.keys(item).forEach(key => copy[key] = item[key]); + copy.remoteMatch = true; + return copy; + }); + } + onRemoteQueryChanged: { + Store.clearRemote(); + if (remoteQuery.length < 2) + remoteDebounce.stop(); + else + remoteDebounce.restart(); + } + Timer { + id: remoteDebounce + interval: 350 + onTriggered: if (app.remoteQuery.length >= 2) Store.searchRemote("flatpak", app.remoteQuery) + } + + readonly property var searchableItems: Store.items.concat(app.remoteExtras) .filter(item => String(item.category || "") !== "omarchy-plugins" || app.nomarchyActive) .map(item => { const copy = {}; Object.keys(item).forEach(key => copy[key] = item[key]); + if (app.remoteMatches[StoreLogic.itemKey(item)] === true) + copy.remoteMatch = true; const category = Store.category(item.category); copy.categoryName = StoreLogic.categoryName(item, category); copy.searchIndex = StoreLogic.searchText(copy); @@ -111,7 +149,9 @@ Rectangle { readonly property string pageGroup: searchOpen ? I18n.tr("SEARCH") : (view === "library" ? I18n.tr("YOUR STORE") : I18n.tr("BROWSE")) readonly property string pageDescription: searchOpen - ? I18n.tr("Results from every category, with installed state kept in view.") + ? (remoteScope + ? I18n.tr("Results from the whole of Flathub, with installed state kept in view.") + : I18n.tr("Results from every category, with installed state kept in view.")) : (view === "library" ? (updateCount > 0 ? I18n.tr("%1 installed pieces, %2 ready to update.").arg(libraryCount).arg(updateCount) @@ -491,7 +531,7 @@ Rectangle { spacing: Tokens.s1 Text { - text: "力 " + app.pageGroup + text: UiPreferences.brandMark("力") + " " + app.pageGroup color: Tokens.inkDim font.family: Tokens.mono font.pixelSize: Tokens.fMicro @@ -810,9 +850,13 @@ Rectangle { anchors.horizontalCenter: parent.horizontalCenter caption: app.view === "library" ? I18n.tr("Installed pieces appear here, with updates called out in place.") - : (app.query !== "" - ? I18n.tr("No products match this search.") - : I18n.tr("Nothing has been published to this collection yet.")) + : (app.remoteScope && Store.remoteSearching + ? I18n.tr("Searching Flathub.") + : (app.remoteScope && Store.remoteError !== "" + ? Store.remoteError + : (app.query !== "" + ? I18n.tr("No products match this search.") + : I18n.tr("Nothing has been published to this collection yet.")))) } Btn { diff --git a/ryoku/apps/ryostore/quickshell/ProductDetail.qml b/ryoku/apps/ryostore/quickshell/ProductDetail.qml index 6b2bc55d49..8bfa1a5dfa 100644 --- a/ryoku/apps/ryostore/quickshell/ProductDetail.qml +++ b/ryoku/apps/ryostore/quickshell/ProductDetail.qml @@ -71,10 +71,13 @@ FocusScope { readonly property bool isCommunityPlugin: String(actionItem.category || "") === "plugins" && !!(actionItem.metadata && actionItem.metadata.community === true) readonly property bool isOmarchyPlugin: String(actionItem.category || "") === "omarchy-plugins" + readonly property bool isFlathubApp: String(actionItem.category || "") === "flatpak" readonly property bool isUnsandboxedPlugin: isCommunityPlugin || isOmarchyPlugin - readonly property string trustLabel: isOmarchyPlugin - ? String(actionItem.verificationStatus || I18n.tr("UNVERIFIED")).toUpperCase() - : I18n.tr("COMMUNITY") + readonly property string trustLabel: isFlathubApp + ? I18n.tr("FLATHUB %1").arg(String(actionItem.verificationStatus || "unverified").toUpperCase()) + : (isOmarchyPlugin + ? String(actionItem.verificationStatus || I18n.tr("UNVERIFIED")).toUpperCase() + : I18n.tr("COMMUNITY")) readonly property var components: (actionItem.metadata && Array.isArray(actionItem.metadata.items)) ? actionItem.metadata.items : [] readonly property var coreComponents: components.filter(c => String(c.tier || "core") !== "optional") readonly property var optionalComponents: components.filter(c => String(c.tier || "core") === "optional") @@ -123,7 +126,7 @@ FocusScope { // Bundle component lists need the dossier body. Media stays in the adjacent // gallery plate; richer dossiers still pin copy and actions to opposite ends. readonly property bool hasBodyContent: isBundle - readonly property bool dossierAnchored: isBundle || screenshotCount > 0 || isUnsandboxedPlugin + readonly property bool dossierAnchored: isBundle || screenshotCount > 0 || isUnsandboxedPlugin || isFlathubApp readonly property real targetHeight: Math.max(1, height - Tokens.s6 * 2) readonly property rect effectiveOrigin: originRect.width > 0 && originRect.height > 0 ? originRect @@ -572,7 +575,7 @@ FocusScope { Rectangle { id: communityTag objectName: "ryostore-detail-community-tag" - visible: detail.isUnsandboxedPlugin + visible: detail.isUnsandboxedPlugin || detail.isFlathubApp width: communityTagLabel.implicitWidth + Tokens.s3 * 2 height: communityTagLabel.implicitHeight + Tokens.s2 radius: Tokens.radius @@ -688,7 +691,7 @@ FocusScope { Rectangle { objectName: "ryostore-detail-community-warning" width: parent.width - visible: detail.isUnsandboxedPlugin + visible: detail.isUnsandboxedPlugin || detail.isFlathubApp implicitHeight: communityWarningRow.implicitHeight + (dossier.compact ? Tokens.s2 : Tokens.s3) * 2 height: implicitHeight @@ -719,13 +722,15 @@ FocusScope { width: parent.width - (communityWarningMark.visible ? communityWarningMark.width + parent.spacing : 0) - text: detail.isOmarchyPlugin - ? (dossier.compact - ? I18n.tr("Runs unsandboxed with your shell permissions. Review the source before enabling.") - : I18n.tr("This plugin runs unsandboxed code inside your shell with your permissions. Review its source before enabling it.")) - : (dossier.compact - ? I18n.tr("Community plugin. Inspect its code before granting shell access.") - : I18n.tr("Community plugin. Ryoku does not review or maintain it: it runs inside your shell with your permissions, so inspect its code before you trust it.")) + text: detail.isFlathubApp + ? I18n.tr("Flathub application. Ryostore does not review it. Check its source and permissions. New apps install system-wide when available (authorization may be required), otherwise through an existing user remote.") + : (detail.isOmarchyPlugin + ? (dossier.compact + ? I18n.tr("Runs unsandboxed with your shell permissions. Review the source before enabling.") + : I18n.tr("This plugin runs unsandboxed code inside your shell with your permissions. Review its source before enabling it.")) + : (dossier.compact + ? I18n.tr("Community plugin. Inspect its code before granting shell access.") + : I18n.tr("Community plugin. Ryoku does not review or maintain it: it runs inside your shell with your permissions, so inspect its code before you trust it."))) color: Tokens.inkDim font.family: Tokens.ui font.pixelSize: Tokens.fSmall diff --git a/ryoku/apps/ryostore/quickshell/Singletons/Store.qml b/ryoku/apps/ryostore/quickshell/Singletons/Store.qml index ea4207b59e..e3685a4eb2 100644 --- a/ryoku/apps/ryostore/quickshell/Singletons/Store.qml +++ b/ryoku/apps/ryostore/quickshell/Singletons/Store.qml @@ -31,6 +31,17 @@ Singleton { property string revision: "" property bool _forced: false property string _checkOutput: "" + property bool _reloadAfterCheck: false + property var remoteItems: [] + property string remoteResultQuery: "" + property bool remoteSearching: false + property string remoteError: "" + property string _remoteWanted: "" + property string _remoteRunning: "" + property string _remoteOutput: "" + property string _remoteErrorText: "" + property string _checkRevision: "" + property bool _autoRefreshAttempted: false property string _op: "install" // which flow installProc is running property string _failedOp: "" @@ -97,6 +108,36 @@ Singleton { return null; } + function searchRemote(category, query) { + const wanted = String(category) + "\n" + String(query); + if (wanted === _remoteWanted) + return; + _remoteWanted = wanted; + remoteResultQuery = ""; + remoteItems = []; + remoteError = ""; + remoteSearching = true; + if (!remoteProc.running) + _startRemote(); + } + + function clearRemote() { + _remoteWanted = ""; + remoteResultQuery = ""; + remoteItems = []; + remoteError = ""; + remoteSearching = false; + } + + function _startRemote() { + const parts = _remoteWanted.split("\n"); + _remoteRunning = _remoteWanted; + _remoteOutput = ""; + _remoteErrorText = ""; + remoteProc.command = ["ryostore", "search", parts[0], parts.slice(1).join("\n")]; + remoteProc.running = true; + } + function refresh(force) { if (catalogProc.running) return; @@ -109,6 +150,15 @@ Singleton { catalogProc.running = true; } + function refreshIfStale() { + if (!root._autoRefreshAttempted && !catalogProc.running && !root.loading + && root.revision !== "" && root._checkRevision !== "" + && root.revision !== root._checkRevision) { + root._autoRefreshAttempted = true; + root.refresh(true); + } + } + // A lightweight upstream probe (registries only, no downloads): it lights the // refresh dot when ryostore has advanced past what the user last pulled, // without touching the shown catalogue. @@ -125,6 +175,8 @@ Singleton { catalogProc.running = false; installProc.running = false; checkProc.running = false; + clearRemote(); + remoteProc.running = false; } function install(item, dither, components) { @@ -264,6 +316,7 @@ Singleton { root.revision = String(next.revision || ""); if (root._forced) root.updateAvailable = false; + Qt.callLater(() => root.refreshIfStale()); // Fill the on-disk asset cache out of band so the next open (and // this session's later scrolling) renders previews from disk. Quickshell.execDetached(["ryostore", "warm"]); @@ -280,6 +333,10 @@ Singleton { } } root._pumpQueue(); + if (root._reloadAfterCheck) { + root._reloadAfterCheck = false; + root.refresh(false); + } } } @@ -311,6 +368,40 @@ Singleton { } } + Process { + id: remoteProc + stdout: StdioCollector { onStreamFinished: root._remoteOutput = text } + stderr: StdioCollector { onStreamFinished: root._remoteErrorText = text } + onExited: code => { + const answered = root._remoteRunning; + root._remoteRunning = ""; + if (root._remoteWanted === "") { + root.remoteSearching = false; + return; + } + if (root._remoteWanted !== answered) { + root._startRemote(); + return; + } + root.remoteSearching = false; + if (code !== 0) { + root.remoteItems = []; + root.remoteError = root._remoteErrorText.trim() || I18n.tr("Search failed"); + return; + } + try { + const res = JSON.parse(root._remoteOutput); + if (!Array.isArray(res.items)) + throw new Error("search items are missing"); + root.remoteResultQuery = answered.split("\n").slice(1).join("\n"); + root.remoteItems = res.items.slice(); + } catch (e) { + root.remoteItems = []; + root.remoteError = I18n.tr("Search failed"); + } + } + } + Process { id: checkProc command: ["ryostore", "check"] @@ -318,12 +409,24 @@ Singleton { onExited: code => { if (code !== 0) return; + const reloading = catalogProc.running; try { const res = JSON.parse(root._checkOutput); root.updateAvailable = res.updateAvailable === true; + root._checkRevision = res.offline === true ? "" : String(res.revision || ""); + root.refreshIfStale(); } catch (e) { // A malformed probe never asserts an update. } + // The probe rebuilt the catalogue live and refreshed the launch + // snapshot. Installed state is local and not part of the revision, + // so a part installed outside the store (Settings, a terminal) would + // otherwise stay missing until a manual refresh. A stale rebuild + // started just above is already live, so it needs no second read. + if (reloading) + root._reloadAfterCheck = true; + else if (!catalogProc.running) + root.refresh(false); } } } diff --git a/ryoku/apps/ryostore/quickshell/StoreHeader.qml b/ryoku/apps/ryostore/quickshell/StoreHeader.qml index 55dd2792d9..49bfcfd3f7 100644 --- a/ryoku/apps/ryostore/quickshell/StoreHeader.qml +++ b/ryoku/apps/ryostore/quickshell/StoreHeader.qml @@ -45,11 +45,12 @@ Item { "plugins": "部品", "omarchy-plugins": "部品", "bundles": "束", + "flatpak": "応", "decors": "飾", "launcher-images": "起動", "fastfetch-emblems": "徽" }; - return seals[String(id)] || "品"; + return UiPreferences.text(seals[String(id)] || "品", ""); } component RailItem: Rectangle { @@ -180,7 +181,7 @@ Item { Row { spacing: Tokens.s3 Text { - text: "力" + text: UiPreferences.brandMark("力") color: Tokens.sun font.family: Tokens.jp font.pixelSize: 24 @@ -397,7 +398,7 @@ Item { objectName: "ryostore-header-discover" label: I18n.tr("Discover") note: I18n.tr("A changing edit") - seal: "発見" + seal: UiPreferences.text("発見", "") current: header.view === "discover" && header.categoryID === "" && !header.searchActive onChose: header.activateDiscover() } @@ -433,7 +434,7 @@ Item { : (header.updateCount > 1 ? I18n.tr("%1 installed, %2 updates ready").arg(header.libraryCount).arg(header.updateCount) : I18n.tr("%1 installed").arg(header.libraryCount)) - seal: "蔵" + seal: UiPreferences.text("蔵", "") current: header.view === "library" && !header.searchActive flagged: header.updateCount > 0 onChose: header.activateLibrary() diff --git a/ryoku/apps/ryostore/quickshell/lib/store.js b/ryoku/apps/ryostore/quickshell/lib/store.js index 84b44735ce..fb94ac9173 100644 --- a/ryoku/apps/ryostore/quickshell/lib/store.js +++ b/ryoku/apps/ryostore/quickshell/lib/store.js @@ -97,6 +97,10 @@ function matchesQuery(item, query) { var terms = String(query || "").trim().toLowerCase().split(/\s+/).filter(Boolean); if (terms.length === 0) return true; + // A live search result was matched by its source (typos, keywords and + // translations included), so the literal text test would wrongly drop it. + if (item && item.remoteMatch === true) + return true; var haystack = searchText(item); return terms.every(function(term) { return haystack.indexOf(term) !== -1; }); } diff --git a/ryoku/apps/ryostore/quickshell/lib/store.test.mjs b/ryoku/apps/ryostore/quickshell/lib/store.test.mjs index 69cb255c87..8605b30002 100644 --- a/ryoku/apps/ryostore/quickshell/lib/store.test.mjs +++ b/ryoku/apps/ryostore/quickshell/lib/store.test.mjs @@ -241,3 +241,17 @@ eq(Store.categoryName(shellProducts[0], { name: "Bar styles" }), "Subshell", "subshell cards carry a small marker"); eq(Store.categoryName(shellProducts[4], { name: "Bar styles" }), "Bar styles", "ordinary barstyle cards keep their category"); + +eq( + Store.filter([ + { id: "local", category: "flatpak", name: "Local", summary: "no match here" }, + { id: "live", category: "flatpak", name: "Live", summary: "no match here", remoteMatch: true } + ], { query: "discord" }).map(i => i.id), + ["live"], + "a live search result is kept even when its text does not contain the query" +); +eq( + Store.filter([{ id: "live", category: "flatpak", name: "Live", remoteMatch: true }], { category: "plugins", query: "discord" }).length, + 0, + "a live search result still respects the category filter" +); diff --git a/ryoku/apps/ryostore/quickshell/shell.qml b/ryoku/apps/ryostore/quickshell/shell.qml index 8333265b8f..e5cc2e6b41 100644 --- a/ryoku/apps/ryostore/quickshell/shell.qml +++ b/ryoku/apps/ryostore/quickshell/shell.qml @@ -4,6 +4,9 @@ import Quickshell.Io import Ryoku.Ui.Singletons ShellRoot { + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") + FloatingWindow { id: win title: I18n.tr("Ryostore") diff --git a/ryoku/apps/ryovm/quickshell/AddConsole.qml b/ryoku/apps/ryovm/quickshell/AddConsole.qml index c4fcd7f5d1..23272635c7 100644 --- a/ryoku/apps/ryovm/quickshell/AddConsole.qml +++ b/ryoku/apps/ryovm/quickshell/AddConsole.qml @@ -116,7 +116,7 @@ Item { font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark anchors.verticalCenter: parent.verticalCenter } - Text { text: "接続"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } + Text { visible: UiPreferences.showJapaneseGloss; text: "接続"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } } Rectangle { width: parent.width; height: 1; color: Tokens.lineSoft } } diff --git a/ryoku/apps/ryovm/quickshell/AddRemote.qml b/ryoku/apps/ryovm/quickshell/AddRemote.qml index cf574dc79f..da4ceee727 100644 --- a/ryoku/apps/ryovm/quickshell/AddRemote.qml +++ b/ryoku/apps/ryovm/quickshell/AddRemote.qml @@ -125,7 +125,7 @@ Item { font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark anchors.verticalCenter: parent.verticalCenter } - Text { text: "接続"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } + Text { visible: UiPreferences.showJapaneseGloss; text: "接続"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } } Rectangle { width: parent.width; height: 1; color: Tokens.lineSoft } } diff --git a/ryoku/apps/ryovm/quickshell/ConsolesPage.qml b/ryoku/apps/ryovm/quickshell/ConsolesPage.qml index 342496b5be..178d73e8be 100644 --- a/ryoku/apps/ryovm/quickshell/ConsolesPage.qml +++ b/ryoku/apps/ryovm/quickshell/ConsolesPage.qml @@ -361,7 +361,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: proto.modelData.jp + text: UiPreferences.text(proto.modelData.jp, "") color: Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 11 anchors.verticalCenter: parent.verticalCenter diff --git a/ryoku/apps/ryovm/quickshell/Dashboard.qml b/ryoku/apps/ryovm/quickshell/Dashboard.qml index 5c8976f760..65605e4584 100644 --- a/ryoku/apps/ryovm/quickshell/Dashboard.qml +++ b/ryoku/apps/ryovm/quickshell/Dashboard.qml @@ -328,7 +328,7 @@ Item { width: parent.width text: resourceRow.modelData.title + (resourceRow.modelData.kind === "console" ? " ⌁" : "") - + (resourceRow.modelData.auth === true ? " 鍵" : "") + + (resourceRow.modelData.auth === true ? UiPreferences.text(" 鍵", "") : "") elide: Text.ElideRight color: sel ? Tokens.inkOnBone : Tokens.ink font.family: Tokens.ui diff --git a/ryoku/apps/ryovm/quickshell/HarbourLog.qml b/ryoku/apps/ryovm/quickshell/HarbourLog.qml index e0f3f90f26..6690e069da 100644 --- a/ryoku/apps/ryovm/quickshell/HarbourLog.qml +++ b/ryoku/apps/ryovm/quickshell/HarbourLog.qml @@ -59,7 +59,7 @@ Item { font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark anchors.verticalCenter: parent.verticalCenter } - Text { text: mark.glyph; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } + Text { text: UiPreferences.text(mark.glyph, ""); color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } } Rectangle { diff --git a/ryoku/apps/ryovm/quickshell/Mark.qml b/ryoku/apps/ryovm/quickshell/Mark.qml index 404815e8dd..40c0e2058a 100644 --- a/ryoku/apps/ryovm/quickshell/Mark.qml +++ b/ryoku/apps/ryovm/quickshell/Mark.qml @@ -8,7 +8,7 @@ import Ryoku.Ui.Singletons Text { property real size: 96 property color tint: Tokens.inkFaint - text: "\u529b" + text: UiPreferences.brandMark("\u529b") color: tint font.family: Tokens.jp font.pixelSize: size diff --git a/ryoku/apps/ryovm/quickshell/PageHead.qml b/ryoku/apps/ryovm/quickshell/PageHead.qml index b9210405ec..e92929139f 100644 --- a/ryoku/apps/ryovm/quickshell/PageHead.qml +++ b/ryoku/apps/ryovm/quickshell/PageHead.qml @@ -24,7 +24,7 @@ Item { visible: head.eyebrow.length > 0 spacing: Tokens.s2 Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } + Text { text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { text: head.eyebrow color: Tokens.inkMuted diff --git a/ryoku/apps/ryovm/quickshell/PassthroughPage.qml b/ryoku/apps/ryovm/quickshell/PassthroughPage.qml index da133bfd05..d0616f96e8 100644 --- a/ryoku/apps/ryovm/quickshell/PassthroughPage.qml +++ b/ryoku/apps/ryovm/quickshell/PassthroughPage.qml @@ -525,6 +525,7 @@ Item { font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark } Text { + visible: UiPreferences.showJapaneseGloss text: "客"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12 anchors.verticalCenter: parent.verticalCenter } diff --git a/ryoku/apps/ryovm/quickshell/Rail.qml b/ryoku/apps/ryovm/quickshell/Rail.qml index f73b5fdace..67de5f8e13 100644 --- a/ryoku/apps/ryovm/quickshell/Rail.qml +++ b/ryoku/apps/ryovm/quickshell/Rail.qml @@ -49,7 +49,7 @@ Item { Row { anchors { left: parent.left; verticalCenter: parent.verticalCenter; leftMargin: Tokens.s4 } spacing: Tokens.s3 - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 22 } + Text { text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 22 } Column { spacing: 1 anchors.verticalCenter: parent.verticalCenter @@ -65,7 +65,7 @@ Item { } Text { anchors { right: parent.right; top: parent.top; margins: Tokens.s2 } - text: "港"; color: Tokens.inkFaint + text: UiPreferences.text("港", ""); color: Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 13 } } @@ -198,7 +198,7 @@ Item { } Text { anchors { right: parent.right; rightMargin: Tokens.s3; verticalCenter: parent.verticalCenter } - text: rail.jpName[navItem.modelData.key] || "" + text: UiPreferences.text(rail.jpName[navItem.modelData.key] || "", "") color: navItem.sel ? Tokens.inkOnBoneDim : Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 12 Behavior on color { ColorAnimation { duration: Tokens.snap } } diff --git a/ryoku/apps/ryovm/quickshell/RemoteDetail.qml b/ryoku/apps/ryovm/quickshell/RemoteDetail.qml index eb518cc519..ea12383f2a 100644 --- a/ryoku/apps/ryovm/quickshell/RemoteDetail.qml +++ b/ryoku/apps/ryovm/quickshell/RemoteDetail.qml @@ -143,7 +143,7 @@ Item { spacing: Tokens.s2 Row { spacing: Tokens.s2 - Text { text: "鍵"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter } + Text { visible: UiPreferences.showJapaneseGloss; text: "鍵"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("REACHABLE · PROBE BLOCKED") color: Tokens.ink diff --git a/ryoku/apps/ryovm/quickshell/RemoteTile.qml b/ryoku/apps/ryovm/quickshell/RemoteTile.qml index a78139b038..88ca1a8236 100644 --- a/ryoku/apps/ryovm/quickshell/RemoteTile.qml +++ b/ryoku/apps/ryovm/quickshell/RemoteTile.qml @@ -40,7 +40,7 @@ Rectangle { Text { width: parent.width text: row.alias - + (row.needsAuth ? " 鍵" : "") + + (row.needsAuth ? UiPreferences.text(" 鍵", "") : "") elide: Text.ElideRight color: row.selected ? Tokens.inkOnBone : Tokens.ink font.family: Tokens.ui diff --git a/ryoku/apps/ryovm/quickshell/RemotesPage.qml b/ryoku/apps/ryovm/quickshell/RemotesPage.qml index e707ef3132..a696ff6265 100644 --- a/ryoku/apps/ryovm/quickshell/RemotesPage.qml +++ b/ryoku/apps/ryovm/quickshell/RemotesPage.qml @@ -202,6 +202,7 @@ Item { font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark } Text { + visible: UiPreferences.showJapaneseGloss text: "鍵"; color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12 anchors.verticalCenter: parent.verticalCenter } diff --git a/ryoku/apps/ryovm/quickshell/shell.qml b/ryoku/apps/ryovm/quickshell/shell.qml index a93b80e781..2361d93587 100644 --- a/ryoku/apps/ryovm/quickshell/shell.qml +++ b/ryoku/apps/ryovm/quickshell/shell.qml @@ -5,6 +5,9 @@ import Ryoku.Ui.Singletons // The runtime title stays compatible with ryoku-summon and both compositor // providers. Ryoport is the visible product identity inside the window. ShellRoot { + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") + FloatingWindow { id: win title: "ryovm" diff --git a/ryoku/assets/rices/default/rice.json b/ryoku/assets/rices/default/rice.json index 2fca780d95..568b7c306c 100644 --- a/ryoku/assets/rices/default/rice.json +++ b/ryoku/assets/rices/default/rice.json @@ -145,6 +145,7 @@ } }, "barStyle": "qsbar", + "showJapaneseGloss": true, "bars": { "bottom_bar": { "center_widgets": [], diff --git a/ryoku/assets/rices/noc/rice.json b/ryoku/assets/rices/noc/rice.json index 032afcc326..16703d9264 100644 --- a/ryoku/assets/rices/noc/rice.json +++ b/ryoku/assets/rices/noc/rice.json @@ -151,6 +151,7 @@ } }, "barStyle": "orpheus", + "showJapaneseGloss": true, "bars": { "bottom_bar": { "center_widgets": [], diff --git a/ryoku/cli/CHANGELOG.md b/ryoku/cli/CHANGELOG.md index b2af969e78..3f2d59bc68 100644 --- a/ryoku/cli/CHANGELOG.md +++ b/ryoku/cli/CHANGELOG.md @@ -1,6 +1,9 @@ # Ryoku CLI Changelog ## Unreleased +- **Phone installs use headers for the installed kernels, not driver modules.** + Pacman reads each kernel's pkgbase and no longer adds stock headers on systems + without the stock kernel (`internal/host/phone.go`, `internal/host/phone_test.go`). - **Doctor stops reporting user service activation after it is fixed.** The user service record is now saved even when nothing needs enabling (`internal/host/service_manifest.go`). diff --git a/ryoku/cli/internal/doctor/reconcile_lockscreen.go b/ryoku/cli/internal/doctor/reconcile_lockscreen.go index 8bb8fcc27c..f71fdf9bd0 100644 --- a/ryoku/cli/internal/doctor/reconcile_lockscreen.go +++ b/ryoku/cli/internal/doctor/reconcile_lockscreen.go @@ -4,9 +4,11 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" "os" "os/exec" "path/filepath" + "strconv" "strings" "syscall" @@ -77,41 +79,67 @@ func lockscreenStale(bundle string) bool { treeDiffers(filepath.Join(bundle, "themes", defaultLockSkin), filepath.Join(home, ".local", "share", "qylock", "themes", defaultLockSkin)) } -// greeterStale: the SDDM greeter is the shipped default skin (the preference -// names it, or was never written) and its installed copy drifted from the -// bundle. A picked skin is the user's; leave it. +func greeterClock24h() bool { + b, err := os.ReadFile(filepath.Join(sys.ConfigHome(), "ryoku", "shell.json")) + if err != nil { + return false + } + var settings struct { + General struct { + ClockFormat24H bool `json:"clock_format_24_h"` + } `json:"general"` + } + return json.Unmarshal(b, &settings) == nil && settings.General.ClockFormat24H +} + +func installedGreeterClock24h(path string) (bool, bool) { + for _, line := range strings.Split(readFileSafe(path), "\n") { + key, value, ok := strings.Cut(line, "=") + if ok && strings.TrimSpace(key) == "clock24h" { + clock24h, err := strconv.ParseBool(strings.TrimSpace(value)) + return clock24h, err == nil + } + } + return false, false +} + +// greeterStale checks the shared SDDM host for every skin. A selected optional +// skin remains untouched beneath theme/; only the host is compared. The shipped +// default is also compared with the bundle so package fixes reach it. func greeterStale(bundle string) bool { pref := strings.TrimSpace(readFileSafe(filepath.Join(sys.ConfigHome(), "qylock", "theme"))) - if pref != "" && pref != defaultLockSkin { + if !sys.Exists(filepath.Join(greeterThemeDir, "Main.qml")) { return false } - if !sys.Exists(filepath.Join(greeterThemeDir, "Main.qml")) { + host := filepath.Join(bundle, "sddm-host") + installedClock24h, hasClockFormat := installedGreeterClock24h(filepath.Join(greeterThemeDir, "theme.conf")) + if fileDiffers(filepath.Join(host, "Main.qml"), filepath.Join(greeterThemeDir, "Main.qml")) || + fileDiffers(filepath.Join(host, "metadata.desktop"), filepath.Join(greeterThemeDir, "metadata.desktop")) || + !hasClockFormat || installedClock24h != greeterClock24h() { + return true + } + if pref != "" && pref != defaultLockSkin { return false } - return treeDiffers(filepath.Join(bundle, "themes", defaultLockSkin), greeterThemeDir) + return treeDiffers(filepath.Join(bundle, "themes", defaultLockSkin), filepath.Join(greeterThemeDir, "theme")) } -// refreshGreeter re-lays the shipped default skin as the SDDM greeter, with the -// ownership and modes the sddm user needs (see reconcileGreeterTheme). +// refreshGreeter lays the selected skin beneath the shared SDDM host. An old +// optional skin installed directly at the greeter root is used as its own +// migration source; the default is refreshed from the shipped bundle. func refreshGreeter(bundle string) error { + pref := strings.TrimSpace(readFileSafe(filepath.Join(sys.ConfigHome(), "qylock", "theme"))) src := filepath.Join(bundle, "themes", defaultLockSkin) - tmp := greeterThemeDir + ".new" - if err := sys.Run("sudo", "rm", "-rf", tmp); err != nil { - return err - } - if err := sys.Run("sudo", "cp", "-a", src, tmp); err != nil { - return err - } - if err := sys.Run("sudo", "chown", "-R", "root:root", tmp); err != nil { - return err - } - if err := sys.Run("sudo", "chmod", "-R", "a+rX", tmp); err != nil { - return err - } - if err := sys.Run("sudo", "rm", "-rf", greeterThemeDir); err != nil { - return err + if pref != "" && pref != defaultLockSkin { + wrapped := filepath.Join(greeterThemeDir, "theme") + if sys.Exists(filepath.Join(wrapped, "Main.qml")) { + src = wrapped + } else { + src = greeterThemeDir + } } - return sys.Run("sudo", "mv", tmp, greeterThemeDir) + installer := filepath.Join(bundle, "sddm-host", "install-greeter-theme") + return sys.Run("sudo", installer, src, greeterThemeDir, strconv.FormatBool(greeterClock24h())) } // greeterScriptSource is the shipped greeter compositor script. On a package box diff --git a/ryoku/cli/internal/host/host.go b/ryoku/cli/internal/host/host.go index e1a79f8571..dcd4d21632 100644 --- a/ryoku/cli/internal/host/host.go +++ b/ryoku/cli/internal/host/host.go @@ -159,6 +159,7 @@ type Config struct { PAMModulePaths []string UIDSet bool UID int + KernelModulesDir string PhoneModulesLoadPath string PhoneModprobePath string PhoneStatePath string @@ -267,6 +268,9 @@ func New(cfg Config) *App { "/usr/lib64/security/pam_fprintd_grosshack.so", } } + if cfg.KernelModulesDir == "" { + cfg.KernelModulesDir = "/usr/lib/modules" + } if cfg.PhoneModulesLoadPath == "" { cfg.PhoneModulesLoadPath = "/etc/modules-load.d/phone-v4l2loopback.conf" } diff --git a/ryoku/cli/internal/host/phone.go b/ryoku/cli/internal/host/phone.go index 117d0d4b25..519e835a6a 100644 --- a/ryoku/cli/internal/host/phone.go +++ b/ryoku/cli/internal/host/phone.go @@ -18,7 +18,6 @@ var phoneBasePackages = []string{ "avahi", "python-pillow", "v4l2loopback-dkms", - "linux-headers", } const ( @@ -83,14 +82,21 @@ func (a *App) Phone(args []string) int { func (a *App) phonePackages(manager PackageManager) []string { packages := append([]string{}, phoneBasePackages...) if manager == Pacman { - result := a.query("pacman", "-Qqo", "/usr/lib/modules") - if result.Code == 0 { - for _, kernel := range strings.Fields(result.Output) { - if strings.HasPrefix(kernel, "linux-cachyos") && !strings.Contains(kernel, "headers") { + modules, err := os.ReadDir(a.cfg.KernelModulesDir) + if err == nil { + for _, module := range modules { + pkgbase, err := os.ReadFile(filepath.Join(a.cfg.KernelModulesDir, module.Name(), "pkgbase")) + if err != nil { + continue + } + kernel := strings.TrimSpace(string(pkgbase)) + if strings.HasPrefix(kernel, "linux") && !strings.ContainsAny(kernel, " \t\r\n/") { packages = append(packages, kernel+"-headers") } } } + } else { + packages = append(packages, "linux-headers") } seen := make(map[string]bool, len(packages)) out := make([]string, 0, len(packages)) diff --git a/ryoku/cli/internal/host/phone_test.go b/ryoku/cli/internal/host/phone_test.go index d154a2dec8..1a3d7054ee 100644 --- a/ryoku/cli/internal/host/phone_test.go +++ b/ryoku/cli/internal/host/phone_test.go @@ -43,6 +43,68 @@ func decodePhonePlan(t *testing.T, body string) phonePlan { return plan } +func phoneKernelModules(t *testing.T, kernels map[string]string) string { + t.Helper() + root := t.TempDir() + for version, pkgbase := range kernels { + dir := filepath.Join(root, version) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if pkgbase != "" { + if err := os.WriteFile(filepath.Join(dir, "pkgbase"), []byte(pkgbase+"\n"), 0o644); err != nil { + t.Fatal(err) + } + } + } + return root +} + +func TestPhonePacmanKernelHeaders(t *testing.T) { + cases := []struct { + name string + kernels map[string]string + headers []string + }{ + { + name: "cachyos with nvidia modules", + kernels: map[string]string{ + "7.3.0-4-cachyos": "linux-cachyos", + "nvidia-open-7.3.0-4": "", + "zfs-7.3.0-4": "", + }, + headers: []string{"linux-cachyos-headers"}, + }, + { + name: "stock kernel", + kernels: map[string]string{"7.2.11-arch1-1": "linux"}, + headers: []string{"linux-headers"}, + }, + { + name: "two kernels", + kernels: map[string]string{ + "6.12.1-1-lts": "linux-lts", + "7.3.0-4-zen": "linux-zen", + }, + headers: []string{"linux-lts-headers", "linux-zen-headers"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + app, stdout, stderr := testApp(&fakeRunner{}, map[string]string{"RYOKU_HOST_PKGMGR": "pacman"}) + app.cfg.KernelModulesDir = phoneKernelModules(t, tc.kernels) + if code := app.Phone([]string{"plan-enable"}); code != ExitOK { + t.Fatalf("exit %d: %s", code, stderr.String()) + } + want := append(append([]string{}, phoneBasePackages...), tc.headers...) + plan := decodePhonePlan(t, stdout.String()) + if !reflect.DeepEqual(plan.Packages, want) { + t.Fatalf("packages = %#v, want %#v", plan.Packages, want) + } + }) + } +} + func TestPhoneEnablePlansExactTransactionPerEdition(t *testing.T) { cases := []struct { name string @@ -53,7 +115,7 @@ func TestPhoneEnablePlansExactTransactionPerEdition(t *testing.T) { { name: "arch", manager: "pacman", paths: map[string]bool{}, want: [][]string{ - {"pacman", "-S", "--needed", "--noconfirm", "kdeconnect", "scrcpy", "android-tools", "python-gobject", "python-dbus", "avahi", "python-pillow", "v4l2loopback-dkms", "linux-headers", "linux-cachyos-headers"}, + {"pacman", "-S", "--needed", "--noconfirm", "kdeconnect", "scrcpy", "android-tools", "python-gobject", "python-dbus", "avahi", "python-pillow", "v4l2loopback-dkms", "linux-cachyos-headers"}, {"modprobe", "v4l2loopback"}, }, }, @@ -76,17 +138,16 @@ func TestPhoneEnablePlansExactTransactionPerEdition(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - runner := &fakeRunner{paths: tc.paths, answer: func(command Command) Result { - if argv(command) == "pacman -Qqo /usr/lib/modules" { - return Result{Output: "linux-cachyos\nlinux-cachyos-headers\n"} - } - return Result{} - }} + runner := &fakeRunner{paths: tc.paths} env := map[string]string{"RYOKU_HOST_PKGMGR": tc.manager} if tc.manager != "pacman" { env["RYOKU_HOST_PKG_TABLE"] = phonePackageTable(t, tc.name) } app, stdout, stderr := testApp(runner, env) + app.cfg.KernelModulesDir = phoneKernelModules(t, map[string]string{ + "7.3.0-4-cachyos": "linux-cachyos", + "nvidia-open": "", + }) app.cfg.PhoneModulesLoadPath = "ETC/modules-load.d/phone-v4l2loopback.conf" app.cfg.PhoneModprobePath = "ETC/modprobe.d/phone-v4l2loopback.conf" app.cfg.PhoneStatePath = filepath.Join(t.TempDir(), "missing-state") diff --git a/ryoku/hub/CHANGELOG.md b/ryoku/hub/CHANGELOG.md index 386052eacd..deda5235a3 100644 --- a/ryoku/hub/CHANGELOG.md +++ b/ryoku/hub/CHANGELOG.md @@ -3,6 +3,24 @@ ## Unreleased ### Fixed +- **Bluetooth can recover when rfkill hides the adapter.** Connections shows + the software-blocked state and keeps its power switch available so the shared + helper can bring the controller back (`quickshell/pages/ConnectionsPage.qml`). +- **Ryoku Settings stays loadable while an update lays its QML files.** The + packaged window waits for the updater's clean reopen instead of watching each + copied file; checkout runs still hot-reload (`quickshell/shell.qml`). +- **Japanese glosses can be switched off.** Global > Japanese glosses hides the + kana beside labels, the page watermarks and the 力 mark across Settings, and + Fastfetch drops it from its greeting without rewriting the user's config + (`quickshell/`, `backend/fastfetch.go`). Started by Harsh Singh in #619. +- **Phone installation shows the package manager's error.** The Phone card + displays the failure line instead of only reporting that installation failed + (`quickshell/pages/PhoneConnectionTab.qml`). +- **Updates and Add-ons show actions that work.** Update now remains available when + no Ryoku update is pending; Add-ons opens RyoStore, offers bundle installation, + and separates optional parts from required ones + (`quickshell/pages/UpdatesPage.qml`, `quickshell/pages/AddonsPage.qml`, + `quickshell/pages/AddonBundleCard.qml`, `quickshell/pages/AddonBundleSheet.qml`). - **Vial keyboards follow the desktop accent.** Boards identified by their Vial serial use the VialRGB raw HID interface, including the board's brightness range, instead of sending VIA lighting commands the firmware ignores @@ -28,6 +46,12 @@ `../shell/matugen/config.toml`). ### Added +- **Update now can include the system packages.** Updates has an INCLUDE SYSTEM + switch, off by default, saved as `update_system` in the Hub config. When it is + on, Update now runs `ryoku update --gui --system`, so the distribution + upgrade, kernel included, rides along with the Ryoku packages + (`backend/config.go`, `quickshell/pages/UpdatesPage.qml`, + `quickshell/schema/UpdatesPage.js`). - **General manages people on this computer.** The Users tab lists local accounts, sign-in state, roles, permissions and home storage, and lets an administrator add or remove people, reset passwords and set storage limits diff --git a/ryoku/hub/README.md b/ryoku/hub/README.md index 5f10d1362e..0f9fc05912 100644 --- a/ryoku/hub/README.md +++ b/ryoku/hub/README.md @@ -37,11 +37,12 @@ changing a page's look. prunes it: `prune` drops every unstarred entry with its files and prints the refreshed report. The daemon owns the entries, so the Hub never measures or deletes around it. - - `ryoku-hub lock list|set|apply-greeter ` manages installed qylock - themes: `list` is the local inventory, `set` writes the in-session - preference and applies the SDDM greeter, and `apply-greeter` is the - privileged half under `/usr/share/sddm/themes/ryoku`. RyoStore owns remote - discovery and installation; Settings owns activation only. + - `ryoku-hub lock list|set|apply-greeter` manages installed qylock themes: + `list` is the local inventory, `set` writes the in-session preference and + applies the SDDM greeter, and `apply-greeter` carries the global 12/24-hour + preference through the privileged install under + `/usr/share/sddm/themes/ryoku`. RyoStore owns remote discovery and + installation; Settings owns activation only. - `quickshell/` The UI, hand-written Quickshell (QML), deployed to `~/.config/quickshell/hub` and launched with `qs -c hub`: - `shell.qml` the `FloatingWindow`; `Hub.qml` the app (rail + content + the data diff --git a/ryoku/hub/backend/config.go b/ryoku/hub/backend/config.go index 8940bca685..f9f9a9f210 100644 --- a/ryoku/hub/backend/config.go +++ b/ryoku/hub/backend/config.go @@ -19,11 +19,12 @@ type Config struct { type UIConfig struct { Section string `toml:"section"` UpdateInterval string `toml:"update_interval"` + UpdateSystem string `toml:"update_system"` Advanced string `toml:"advanced"` } func defaultConfig() Config { - return Config{UI: UIConfig{Section: "displays", UpdateInterval: "daily"}} + return Config{UI: UIConfig{Section: "displays", UpdateInterval: "daily", UpdateSystem: "off"}} } func configPath() string { @@ -84,6 +85,8 @@ func configGet(key string) (string, bool) { return c.UI.Section, true case "update_interval": return c.UI.UpdateInterval, true + case "update_system": + return c.UI.UpdateSystem, true case "advanced": return c.UI.Advanced, true } @@ -97,6 +100,11 @@ func configSet(key, value string) error { c.UI.Section = normalizeSection(value) case "update_interval": c.UI.UpdateInterval = value + case "update_system": + if value != "on" && value != "off" { + return fmt.Errorf("update_system takes on or off, not %q", value) + } + c.UI.UpdateSystem = value case "advanced": c.UI.Advanced = value default: diff --git a/ryoku/hub/backend/config_test.go b/ryoku/hub/backend/config_test.go index 410cb053ec..ca77c393e9 100644 --- a/ryoku/hub/backend/config_test.go +++ b/ryoku/hub/backend/config_test.go @@ -48,6 +48,29 @@ func TestUpdateInterval(t *testing.T) { } } +func TestUpdateSystem(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + + // the distribution upgrade stays opt-in: no file reads off. + if v, ok := configGet("update_system"); !ok || v != "off" { + t.Fatalf("default update_system = %q ok=%v, want off", v, ok) + } + + if err := configSet("update_system", "on"); err != nil { + t.Fatal(err) + } + if v, ok := configGet("update_system"); !ok || v != "on" { + t.Fatalf("after set: got %q ok=%v, want on", v, ok) + } + + if err := configSet("update_system", "maybe"); err == nil { + t.Error("update_system should reject anything but on or off") + } + if v, _ := configGet("update_system"); v != "on" { + t.Errorf("a rejected value must not change the setting, got %q", v) + } +} + func TestAdvanced(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) diff --git a/ryoku/hub/backend/fastfetch.go b/ryoku/hub/backend/fastfetch.go index bc5f5f78b9..db8827b707 100644 --- a/ryoku/hub/backend/fastfetch.go +++ b/ryoku/hub/backend/fastfetch.go @@ -80,6 +80,31 @@ func fastfetchPalettePath() string { func fastfetchPaletteStatePath() string { return filepath.Join(filepath.Dir(fastfetchDir()), "ryoku", "fastfetch.json") } +func fastfetchJapaneseGlossEnabled() bool { + b, err := os.ReadFile(filepath.Join(configHome(), "ryoku", "shell.json")) + if err != nil { + return true + } + var settings struct { + ShowJapaneseGloss *bool `json:"showJapaneseGloss"` + } + if json.Unmarshal(b, &settings) != nil || settings.ShowJapaneseGloss == nil { + return true + } + return *settings.ShowJapaneseGloss +} + +func applyFastfetchJapaneseGloss(b []byte, enabled bool) []byte { + if enabled { + return b + } + replacer := strings.NewReplacer( + "RYOKU · 力 · ", "RYOKU · ", + `RYOKU \u00b7 \u529b \u00b7 `, `RYOKU \u00b7 `, + `RYOKU \u00B7 \u529B \u00B7 `, `RYOKU \u00B7 `, + ) + return []byte(replacer.Replace(string(b))) +} // stripJSONC removes // line and /* */ block comments from a JSONC document, // leaving comment-like text inside string literals (a $schema URL) intact so @@ -801,7 +826,7 @@ func buildFastfetchEffectiveWithRoles(m ffModel, recolor map[string]string) ([]b return append(b, '\n'), nil } -func fastfetchEffectiveFresh(path string, mode string) bool { +func fastfetchEffectiveFresh(path string, mode string, japaneseGloss bool) bool { out, err := os.Stat(path) if err != nil || out.IsDir() { return false @@ -821,8 +846,13 @@ func fastfetchEffectiveFresh(path string, mode string) bool { return false } } + shellStore := filepath.Join(configHome(), "ryoku", "shell.json") + if info, err := os.Stat(shellStore); err == nil && out.ModTime().Before(info.ModTime()) { + return false + } + signature := fmt.Sprintf("%s|japanese=%t", mode, japaneseGloss) stored, err := os.ReadFile(effectiveModeMarker(path)) - if err != nil || strings.TrimSpace(string(stored)) != mode { + if err != nil || strings.TrimSpace(string(stored)) != signature { return false } if executable, err := os.Executable(); err == nil { @@ -834,12 +864,11 @@ func fastfetchEffectiveFresh(path string, mode string) bool { return true } -// effectiveModeMarker is the sidecar path that records which palette mode -// produced the effective config at path. The palette mode cannot be detected by -// mtime alone: a state file that is *removed* (returning to the wallpaper -// default) has no timestamp to compare, so a stale fixed-mode cache would be -// served. Storing the resolved mode beside the cache makes every transition -// (set, change, unset) visible. +// effectiveModeMarker is the sidecar path that records which effective inputs +// produced the config at path. A state-file removal and the Japanese gloss +// preference cannot be detected reliably by comparing source mtimes alone, so +// the signature beside the cache makes every mode or preference transition +// rebuild it. func effectiveModeMarker(path string) string { return path + ".mode" } // writeEffectiveFastfetch renders the config fastfetch should actually read into @@ -849,7 +878,8 @@ func effectiveModeMarker(path string) string { return path + ".mode" } // retint never rewrites their layout or comments); wallpaper mode rebuilds it func writeEffectiveFastfetch(path string) error { mode := readFastfetchPaletteMode() - if fastfetchEffectiveFresh(path, mode) { + japaneseGloss := fastfetchJapaneseGlossEnabled() + if fastfetchEffectiveFresh(path, mode, japaneseGloss) { return nil } raw, err := os.ReadFile(fastfetchConfigPath()) @@ -858,7 +888,7 @@ func writeEffectiveFastfetch(path string) error { } var b []byte if mode != ffPaletteWallpaper { - b = raw + b = applyFastfetchJapaneseGloss(raw, japaneseGloss) } else { model, err := loadFastfetch() if err != nil { @@ -871,10 +901,12 @@ func writeEffectiveFastfetch(path string) error { } b = built } + b = applyFastfetchJapaneseGloss(b, japaneseGloss) if err := atomicWrite(path, b, 0o600); err != nil { return err } - return atomicWrite(effectiveModeMarker(path), []byte(mode+"\n"), 0o600) + signature := fmt.Sprintf("%s|japanese=%t\n", mode, japaneseGloss) + return atomicWrite(effectiveModeMarker(path), []byte(signature), 0o600) } // ffApplyAccentToDisplay writes the accent into display.color.keys so the key @@ -993,6 +1025,7 @@ func previewFastfetch(m ffModel) error { if err != nil { return err } + b = applyFastfetchJapaneseGloss(b, fastfetchJapaneseGlossEnabled()) tmp, err := os.CreateTemp("", "ryoku-ff-*.jsonc") if err != nil { return err diff --git a/ryoku/hub/backend/fastfetch_test.go b/ryoku/hub/backend/fastfetch_test.go index 873fddc6f7..429dd1ccca 100644 --- a/ryoku/hub/backend/fastfetch_test.go +++ b/ryoku/hub/backend/fastfetch_test.go @@ -75,6 +75,28 @@ func rowByKind(rows []ffRow, kind string) *ffRow { return nil } +func TestFastfetchJapaneseGlossPreference(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", dir) + if !fastfetchJapaneseGlossEnabled() { + t.Fatal("missing setting should preserve the current Japanese gloss") + } + if err := os.MkdirAll(filepath.Join(dir, "ryoku"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "ryoku", "shell.json"), []byte(`{"showJapaneseGloss":false}`), 0o644); err != nil { + t.Fatal(err) + } + if fastfetchJapaneseGlossEnabled() { + t.Fatal("disabled setting was ignored") + } + input := []byte(`{\"format\":\"RYOKU · 力 · a hand-built Linux desktop\"}`) + got := string(applyFastfetchJapaneseGloss(input, false)) + if strings.Contains(got, "力") || !strings.Contains(got, "RYOKU · a hand-built Linux desktop") { + t.Fatalf("Japanese gloss remained in effective config: %s", got) + } +} + func TestStripJSONC(t *testing.T) { in := `{ "$schema": "https://x/y.json", // trailing "a": 1, /* block */ "b": "//not a comment" }` diff --git a/ryoku/hub/backend/lock.go b/ryoku/hub/backend/lock.go index 25df18d938..a7db999b49 100644 --- a/ryoku/hub/backend/lock.go +++ b/ryoku/hub/backend/lock.go @@ -9,6 +9,7 @@ import ( "os/user" "path/filepath" "sort" + "strconv" "strings" ) @@ -16,9 +17,9 @@ import ( // one swaps the in-session lock preference plus the SDDM greeter theme. Ryostore // owns upstream browsing and install-only downloads; Hub owns activation. // -// ryoku-hub lock list installed skins + active one, as JSON -// ryoku-hub lock set activate an installed skin -// ryoku-hub lock apply-greeter install it as the SDDM greeter +// ryoku-hub lock list installed skins + active one, as JSON +// ryoku-hub lock set activate an installed skin +// ryoku-hub lock apply-greeter privileged greeter install // // a skin = any folder under the themes dir with a Main.qml. slug = its path // under that dir (e.g. "clockwork/orbital"), exactly what lock.sh resolves @@ -93,10 +94,14 @@ func runLock(args []string) error { } return setLockSkin(args[1]) case "apply-greeter": - if len(args) < 2 { - return fmt.Errorf("lock apply-greeter needs a slug") + if len(args) < 3 { + return fmt.Errorf("lock apply-greeter needs a slug and true or false") + } + clock24h, err := strconv.ParseBool(args[2]) + if err != nil { + return fmt.Errorf("invalid clock format %q", args[2]) } - return applyGreeter(args[1]) + return applyGreeter(args[1], clock24h) default: return fmt.Errorf("lock needs list|set|apply-greeter") } @@ -115,6 +120,19 @@ func readLockPref(path string) string { return strings.TrimSpace(string(b)) } +func clockFormat24h() bool { + b, err := os.ReadFile(filepath.Join(xdgHome("XDG_CONFIG_HOME", ".config"), "ryoku", "shell.json")) + if err != nil { + return false + } + var settings struct { + General struct { + ClockFormat24H bool `json:"clock_format_24_h"` + } `json:"general"` + } + return json.Unmarshal(b, &settings) == nil && settings.General.ClockFormat24H +} + // listLockSkinsIn scans dir for installed skins, unions in any store product the // folder scan missed but whose receipt in receiptsDir points at a Main.qml that // is present, marks whichever slug matches active, and reports a read failure so @@ -350,7 +368,7 @@ func setLockSkin(slug string) error { if err := setLockSkinIn(dir, qylockThemePref(), slug); err != nil { return err } - if err := escalateGreeter(slug); err != nil { + if err := escalateGreeter(slug, clockFormat24h()); err != nil { return fmt.Errorf("lock skin set for this session; the sign-in screen keeps its old one: %w", err) } return nil @@ -370,6 +388,13 @@ func setLockSkinIn(dir, pref, slug string) error { const greeterTheme = "ryoku" +func greeterThemeInstaller() string { + if v := os.Getenv("RYOKU_GREETER_THEME_INSTALLER"); v != "" { + return v + } + return "/usr/share/ryoku/lockscreen/qylock/sddm-host/install-greeter-theme" +} + func sddmThemesDir() string { if v := os.Getenv("RYOKU_SDDM_THEMES_DIR"); v != "" { return v @@ -396,12 +421,12 @@ func validSlug(slug string) error { // escalateGreeter re-execs this binary under pkexec to install the skin as the // SDDM greeter (it has to write /usr/share/sddm + /etc). pkexec pops the // graphical polkit prompt; cancel -> error. -func escalateGreeter(slug string) error { +func escalateGreeter(slug string, clock24h bool) error { self, err := os.Executable() if err != nil { return err } - cmd := exec.Command("pkexec", self, "lock", "apply-greeter", slug) + cmd := exec.Command("pkexec", self, "lock", "apply-greeter", slug, strconv.FormatBool(clock24h)) cmd.Stdin = os.Stdin cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr @@ -414,12 +439,12 @@ func escalateGreeter(slug string) error { // applyGreeter = the privileged half, run as root by pkexec. installs the // skin the invoking user picked as the greeter. source resolves from the // invoking user's home, never from a caller-supplied path. don't change that. -func applyGreeter(slug string) error { +func applyGreeter(slug string, clock24h bool) error { src := os.Getenv("RYOKU_QYLOCK_THEMES") if src == "" { src = invokingUserThemes() } - return installGreeter(src, sddmThemesDir(), sddmConfPath(), slug) + return installGreeter(src, sddmThemesDir(), sddmConfPath(), slug, clock24h) } // invokingUserThemes: qylock themes dir of whoever invoked us via pkexec @@ -437,10 +462,11 @@ func invokingUserThemes() string { return qylockThemesDir() } -// installGreeter copies srcThemes/slug into themesDir under a fixed name and -// points the greeter config at it, so the login screen wears the same skin as -// the in-session lock. privileged; broken out for tests. -func installGreeter(srcThemes, themesDir, confPath, slug string) error { +// installGreeter installs srcThemes/slug beneath the shared SDDM host and +// points the greeter config at it. The host owns cursor fallback, focus and the +// global clock preference, so optional skins receive the same login-screen +// behavior as the built-in skin. +func installGreeter(srcThemes, themesDir, confPath, slug string, clock24h bool) error { if err := validSlug(slug); err != nil { return err } @@ -452,22 +478,11 @@ func installGreeter(srcThemes, themesDir, confPath, slug string) error { if err := os.MkdirAll(themesDir, 0o755); err != nil { return err } - if err := os.RemoveAll(dst); err != nil { - return err - } - if out, err := exec.Command("cp", "-a", src, dst).CombinedOutput(); err != nil { + if out, err := exec.Command( + greeterThemeInstaller(), src, dst, strconv.FormatBool(clock24h), + ).CombinedOutput(); err != nil { return fmt.Errorf("install greeter theme: %v: %s", err, out) } - // The greeter runs as the unprivileged `sddm` user, so it must be able to - // read the theme no matter how the source was owned or masked. Catalog skins - // download into an os.MkdirTemp dir (always 0700) and `cp -a` preserves that, - // leaving the greeter dir unreadable to sddm -> SDDM silently falls back to - // its embedded theme on every boot. Normalize: root-owned (best effort, since - // this half runs as root under pkexec) and world-readable. - _ = exec.Command("chown", "-R", "root:root", dst).Run() - if out, err := exec.Command("chmod", "-R", "a+rX", dst).CombinedOutput(); err != nil { - return fmt.Errorf("make greeter theme readable: %v: %s", err, out) - } if err := os.MkdirAll(filepath.Dir(confPath), 0o755); err != nil { return err } diff --git a/ryoku/hub/backend/lock_test.go b/ryoku/hub/backend/lock_test.go index 7f5f369253..f0471b3163 100644 --- a/ryoku/hub/backend/lock_test.go +++ b/ryoku/hub/backend/lock_test.go @@ -24,6 +24,15 @@ func mkSkin(t *testing.T, dir, slug string, withPreview bool) { } } +func useGreeterThemeInstaller(t *testing.T) { + t.Helper() + path, err := filepath.Abs(filepath.Join("..", "..", "lockscreen", "qylock", "sddm-host", "install-greeter-theme")) + if err != nil { + t.Fatal(err) + } + t.Setenv("RYOKU_GREETER_THEME_INSTALLER", path) +} + func findSkin(skins []LockSkin, slug string) *LockSkin { for i := range skins { if skins[i].Slug == slug { @@ -118,17 +127,28 @@ func TestSetLockSkinIn(t *testing.T) { } func TestInstallGreeter(t *testing.T) { + useGreeterThemeInstaller(t) src := t.TempDir() mkSkin(t, src, "material-you", false) themes := t.TempDir() conf := filepath.Join(t.TempDir(), "sddm.conf.d", "99-ryoku.conf") - if err := installGreeter(src, themes, conf, "material-you"); err != nil { + if err := installGreeter(src, themes, conf, "material-you", false); err != nil { t.Fatalf("install greeter: %v", err) } if !fileExists(filepath.Join(themes, greeterTheme, "Main.qml")) { t.Fatalf("greeter theme %q not installed under %s", greeterTheme, themes) } + if !fileExists(filepath.Join(themes, greeterTheme, "theme", "Main.qml")) { + t.Fatalf("selected skin not installed beneath the greeter host") + } + themeConfig, err := os.ReadFile(filepath.Join(themes, greeterTheme, "theme.conf")) + if err != nil { + t.Fatalf("read greeter theme config: %v", err) + } + if !strings.Contains(string(themeConfig), "clock24h=false") { + t.Fatalf("greeter theme config did not receive 12-hour preference: %q", themeConfig) + } b, err := os.ReadFile(conf) if err != nil { t.Fatalf("read conf: %v", err) @@ -139,15 +159,22 @@ func TestInstallGreeter(t *testing.T) { // second skin overwrites the same fixed greeter dir; nothing orphans. mkSkin(t, src, "clockwork/orbital", false) - if err := installGreeter(src, themes, conf, "clockwork/orbital"); err != nil { + if err := installGreeter(src, themes, conf, "clockwork/orbital", true); err != nil { t.Fatalf("reinstall greeter: %v", err) } if !fileExists(filepath.Join(themes, greeterTheme, "Main.qml")) { t.Fatalf("greeter theme missing after switch") } + themeConfig, err = os.ReadFile(filepath.Join(themes, greeterTheme, "theme.conf")) + if err != nil { + t.Fatalf("read reinstalled greeter theme config: %v", err) + } + if !strings.Contains(string(themeConfig), "clock24h=true") { + t.Fatalf("greeter theme config did not receive 24-hour preference: %q", themeConfig) + } // unknown skin -> error before touching anything privileged. - if err := installGreeter(src, themes, conf, "ghost/none"); err == nil { + if err := installGreeter(src, themes, conf, "ghost/none", false); err == nil { t.Fatalf("installing an unknown skin should error") } } @@ -170,6 +197,7 @@ func TestValidSlug(t *testing.T) { // copy to world-readable, else SDDM can't read the theme and silently falls back // to its embedded one on every boot. func TestInstallGreeterMakesThemeWorldReadable(t *testing.T) { + useGreeterThemeInstaller(t) src := t.TempDir() mkSkin(t, src, "video/tape", false) // mimic a catalogue download: owner-only perms on the skin tree. @@ -183,7 +211,7 @@ func TestInstallGreeterMakesThemeWorldReadable(t *testing.T) { themes := t.TempDir() conf := filepath.Join(t.TempDir(), "sddm.conf.d", "99-ryoku.conf") - if err := installGreeter(src, themes, conf, "video/tape"); err != nil { + if err := installGreeter(src, themes, conf, "video/tape", false); err != nil { t.Fatalf("install greeter: %v", err) } diff --git a/ryoku/hub/quickshell/Hub.qml b/ryoku/hub/quickshell/Hub.qml index 7c782e0ad1..80ad6eff5a 100644 --- a/ryoku/hub/quickshell/Hub.qml +++ b/ryoku/hub/quickshell/Hub.qml @@ -113,6 +113,13 @@ Rectangle { property bool advanced: false onAdvancedChanged: Quickshell.execDetached(["ryoku-hub", "config", "set", "advanced", hub.advanced ? "1" : "0"]) + // The daemon owns this preference; the shared file reader covers startup + // before the first settings frame arrives. + readonly property bool showJapaneseGloss: Settings.ready + ? Settings.get("showJapaneseGloss") !== false + : UiPreferences.showJapaneseGloss + readonly property string brandMark: showJapaneseGloss ? "力" : "R" + // The full catalogue. `wired` marks the pages whose content and // persistence are ported; the rest render an honest porting plate rather // than a settings page that cannot save. `needs` is what the active window @@ -573,7 +580,7 @@ Rectangle { "weatherLocation": "", "weatherUnit": "auto", "formatLocale": "", "general.clock_format_24_h": false, "reloadCover": ReloadCoverModel.empty(), - "language": "Auto", "barStyle": "orpheus", "obi": {}, "nacre": NacreConfig.defaultConfig(), "qsbar": {}, "dock": {}, + "language": "Auto", "barStyle": "orpheus", "showJapaneseGloss": true, "obi": {}, "nacre": NacreConfig.defaultConfig(), "qsbar": {}, "dock": {}, "notifications.show_images": false, "deckFrostEnabled": true, "deckFrost": 0.45, "clipboard.widthPercent": 65, "clipboard.heightPercent": 42, "clipboard.bottomPercent": 0, @@ -1108,7 +1115,7 @@ Rectangle { Row { anchors { left: parent.left; verticalCenter: parent.verticalCenter; leftMargin: Tokens.s4 } spacing: Tokens.s3 - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 22 } + Text { text: hub.brandMark; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 22 } Column { spacing: 1 anchors.verticalCenter: parent.verticalCenter @@ -1321,7 +1328,7 @@ Rectangle { Text { id: navKana anchors { right: parent.right; rightMargin: Tokens.s3; verticalCenter: parent.verticalCenter } - text: hub.jpName[navItem.modelData.key] || "" + text: hub.showJapaneseGloss ? (hub.jpName[navItem.modelData.key] || "") : "" color: navItem.sel ? Tokens.inkOnBoneDim : Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 12 Behavior on color { ColorAnimation { duration: Tokens.snap } } @@ -1502,7 +1509,7 @@ Rectangle { Row { spacing: Tokens.s2 Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } + Text { text: hub.brandMark; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr(hub.groupFor(hub.section)); color: Tokens.inkMuted font.family: Tokens.ui; font.pixelSize: 9; font.weight: Font.Medium @@ -1788,7 +1795,7 @@ Rectangle { Text { id: kseal anchors { right: parent.right; rightMargin: Tokens.s3; verticalCenter: parent.verticalCenter } - text: hub.jpName[rr.modelData.section] || "" + text: hub.showJapaneseGloss ? (hub.jpName[rr.modelData.section] || "") : "" color: Tokens.inkFaint; font.family: Tokens.jp; font.pixelSize: 12 } MouseArea { diff --git a/ryoku/hub/quickshell/ReloadCoverControl.qml b/ryoku/hub/quickshell/ReloadCoverControl.qml index 3c6773eb82..d5beb0bfe1 100644 --- a/ryoku/hub/quickshell/ReloadCoverControl.qml +++ b/ryoku/hub/quickshell/ReloadCoverControl.qml @@ -70,7 +70,7 @@ Item { Text { anchors.centerIn: parent visible: renderer.status === Loader.Error - text: "力" + text: UiPreferences.brandMark("力") color: Tokens.inkFaint font.family: Tokens.jp font.pixelSize: Tokens.fHero diff --git a/ryoku/hub/quickshell/SchemaPage.qml b/ryoku/hub/quickshell/SchemaPage.qml index 23e54e054b..8bf2032511 100644 --- a/ryoku/hub/quickshell/SchemaPage.qml +++ b/ryoku/hub/quickshell/SchemaPage.qml @@ -102,7 +102,7 @@ Item { anchors.verticalCenter: parent.verticalCenter Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/AddonBundleCard.qml b/ryoku/hub/quickshell/pages/AddonBundleCard.qml index de06e7d8ad..2f6baa49bf 100644 --- a/ryoku/hub/quickshell/pages/AddonBundleCard.qml +++ b/ryoku/hub/quickshell/pages/AddonBundleCard.qml @@ -11,19 +11,17 @@ Rectangle { property bool busy: false signal openRequested() - signal repairRequested() + signal installRequested() signal removeRequested() + readonly property var components: ((card.bundle.metadata || {}).items || []) readonly property int installedCount: Number(card.bundle.installedCount || 0) - // Every part, including those this system cannot have: the card reports - // those as unavailable rather than leaving them out of the bundle. - readonly property int totalCount: ((card.bundle.metadata || {}).items || []).length - readonly property int missingCount: Math.max(0, totalCount - installedCount) - readonly property var unavailableComponents: ((card.bundle.metadata || {}).items || []) - .filter(item => item.available === false && item.installed !== true) - readonly property int unavailableCount: card.unavailableComponents.length - readonly property int repairableMissingCount: Math.max(0, - card.missingCount - card.unavailableCount) + readonly property int totalCount: card.components.length + readonly property var requiredComponents: card.components.filter(item => + item.tier !== "optional" && item.available !== false) + readonly property int installedRequiredCount: card.requiredComponents + .filter(item => item.installed === true).length + readonly property int missingCount: card.requiredComponents.length - card.installedRequiredCount readonly property string summary: card.bundle.summary || card.bundle.description || I18n.tr("Installed add-on bundle.") @@ -178,9 +176,7 @@ Rectangle { anchors.centerIn: parent text: card.missingCount === 0 ? I18n.tr("Complete") - : (card.unavailableCount === card.missingCount - ? I18n.tr("%1 unavailable").arg(card.unavailableCount) - : I18n.tr("%1 missing").arg(card.missingCount)) + : I18n.tr("%1 missing").arg(card.missingCount) color: card.missingCount === 0 ? Tokens.inkOnBone : Tokens.inkMuted font.family: Tokens.ui font.pixelSize: Tokens.fTiny @@ -194,11 +190,12 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter spacing: Tokens.s2 Btn { - visible: card.repairableMissingCount > 0 + visible: card.missingCount > 0 compact: true - text: card.busy ? I18n.tr("WORKING") : I18n.tr("REPAIR") + text: card.busy ? I18n.tr("WORKING") + : card.installedRequiredCount === 0 ? I18n.tr("INSTALL") : I18n.tr("INSTALL MISSING") armed: !card.busy - onAct: card.repairRequested() + onAct: card.installRequested() } Btn { visible: card.installedCount > 0 diff --git a/ryoku/hub/quickshell/pages/AddonBundleSheet.qml b/ryoku/hub/quickshell/pages/AddonBundleSheet.qml index c90cdf3982..643106fb6d 100644 --- a/ryoku/hub/quickshell/pages/AddonBundleSheet.qml +++ b/ryoku/hub/quickshell/pages/AddonBundleSheet.qml @@ -14,22 +14,24 @@ FocusScope { property bool busy: false signal dismissed() - signal repairRequested() + signal installRequested() signal removeRequested() readonly property var components: ((sheet.bundle.metadata || {}).items || []) readonly property var installedComponents: sheet.components.filter(item => item.installed === true) - readonly property var missingComponents: sheet.components.filter(item => item.installed !== true) + readonly property var missingComponents: sheet.components.filter(item => + item.installed !== true && item.available !== false && item.tier !== "optional") + readonly property var optionalComponents: sheet.components.filter(item => + item.installed !== true && item.available !== false && item.tier === "optional") + readonly property var unavailableComponents: sheet.components.filter(item => + item.installed !== true && item.available === false) + readonly property var requiredComponents: sheet.components.filter(item => + item.available !== false && item.tier !== "optional") + readonly property int installedRequiredCount: sheet.requiredComponents + .filter(item => item.installed === true).length readonly property int installedCount: Number(sheet.bundle.installedCount || 0) - // Every part, including those this system cannot have, which the sheet lists - // greyed with their reason. readonly property int totalCount: sheet.components.length - readonly property int missingCount: Math.max(0, sheet.totalCount - sheet.installedCount) - readonly property var unavailableComponents: sheet.missingComponents - .filter(item => item.available === false) - readonly property int unavailableCount: sheet.unavailableComponents.length - readonly property int repairableMissingCount: Math.max(0, - sheet.missingCount - sheet.unavailableCount) + readonly property int missingCount: sheet.missingComponents.length readonly property string summary: sheet.bundle.summary || sheet.bundle.description || I18n.tr("Installed add-on bundle.") readonly property string icon: String((sheet.bundle.metadata || {}).icon || "inventory_2") @@ -185,10 +187,10 @@ FocusScope { elide: Text.ElideRight } Text { - width: parent.width text: sheet.loading ? I18n.tr("Checking bundle components…") - : I18n.tr("%1 of %2 installed").arg(sheet.installedCount).arg(sheet.totalCount) + : I18n.tr("%1 of %2 required installed") + .arg(sheet.installedRequiredCount).arg(sheet.requiredComponents.length) color: sheet.loading ? Tokens.inkMuted : Tokens.inkDim font.family: Tokens.ui font.pixelSize: Tokens.fTiny @@ -246,6 +248,16 @@ FocusScope { title: I18n.tr("MISSING"), components: sheet.missingComponents, installed: false + }, + { + title: I18n.tr("OPTIONAL"), + components: sheet.optionalComponents, + installed: false + }, + { + title: I18n.tr("UNAVAILABLE"), + components: sheet.unavailableComponents, + installed: false } ].filter(group => group.components.length > 0) @@ -411,14 +423,10 @@ FocusScope { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter text: sheet.missingCount === 0 - ? I18n.tr("Bundle complete") - : (sheet.unavailableCount === sheet.missingCount - ? (sheet.unavailableCount === 1 - ? I18n.tr("%1 component unavailable") - : I18n.tr("%1 components unavailable")).arg(sheet.unavailableCount) - : (sheet.missingCount === 1 - ? I18n.tr("%1 component missing") - : I18n.tr("%1 components missing")).arg(sheet.missingCount)) + ? I18n.tr("Required components complete") + : (sheet.missingCount === 1 + ? I18n.tr("%1 required component missing") + : I18n.tr("%1 required components missing")).arg(sheet.missingCount) color: Tokens.inkMuted font.family: Tokens.ui font.pixelSize: Tokens.fTiny @@ -430,10 +438,11 @@ FocusScope { spacing: Tokens.s2 Btn { - visible: sheet.repairableMissingCount > 0 - text: sheet.busy ? I18n.tr("WORKING") : I18n.tr("REPAIR") + visible: sheet.missingCount > 0 + text: sheet.busy ? I18n.tr("WORKING") + : sheet.installedRequiredCount === 0 ? I18n.tr("INSTALL") : I18n.tr("INSTALL MISSING") armed: !sheet.busy && !sheet.loading - onAct: sheet.repairRequested() + onAct: sheet.installRequested() } Btn { visible: sheet.installedCount > 0 diff --git a/ryoku/hub/quickshell/pages/AddonsPage.qml b/ryoku/hub/quickshell/pages/AddonsPage.qml index bb51b61519..688cbd2f22 100644 --- a/ryoku/hub/quickshell/pages/AddonsPage.qml +++ b/ryoku/hub/quickshell/pages/AddonsPage.qml @@ -198,7 +198,7 @@ Item { case "bundles-read": pg.loadBundles(); break; case "bundle-status": pg.loadBundleStatus(pg.errorTarget); break; case "bundle-remove": pg.removeBundle(pg.errorTarget, pg.errorItem); break; - case "bundle-repair": pg.repairBundle(pg.errorTarget); break; + case "bundle-install": pg.installBundle(pg.errorTarget); break; case "plugin-install": pg.install(pg.errorTarget); break; case "plugin-remove": pg.busyId = pg.errorTarget; @@ -238,7 +238,10 @@ Item { omarchyListProc.running = true; } function browseStore() { - Spawn.runApp(["ryostore", "open"], { label: "ryostore" }); + Spawn.runApp(["ryostore", "open", "discover"], { label: "ryostore" }); + } + function browseBundles() { + Spawn.runApp(["ryostore", "open", "bundles"], { label: "ryostore" }); } function browseOmarchyMarket() { Spawn.runApp(["ryostore", "open", "omarchy-plugins"], { label: "ryostore" }); @@ -284,13 +287,13 @@ Item { "kitty", "--class", "ryostore", "-e", "ryostore-install", "remove"].concat(scope); bundleMutProc.running = true; } - function repairBundle(id) { + function installBundle(id) { if (!id || bundleMutProc.running) return; pg.bundleBusyId = id; pg.bundleMutationId = id; pg.bundleMutationItem = ""; - pg.bundleMutationAction = "repair"; + pg.bundleMutationAction = "install"; bundleMutProc.command = ["ryoku-host", "app", "--label", "ryostore", "--", "kitty", "--class", "ryostore", "-e", "ryostore-install", "install", "bundle", id]; bundleMutProc.running = true; @@ -739,13 +742,13 @@ Item { const id = pg.bundleMutationId; const item = pg.bundleMutationItem; const action = pg.bundleMutationAction; - const retry = action === "repair" ? "bundle-repair" : "bundle-remove"; + const retry = action === "install" ? "bundle-install" : "bundle-remove"; pg.bundleBusyId = ""; if (code !== 0) { pg.showError(retry, pg.compactError(bundleMutErr.text, - action === "repair" - ? I18n.tr("Couldn't repair bundle (exit %1).") + action === "install" + ? I18n.tr("Couldn't install bundle (exit %1).") : I18n.tr("Couldn't remove bundle content (exit %1)."), code), bundleMutProc.command, id, item); @@ -834,7 +837,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { @@ -863,7 +866,7 @@ Item { } Text { width: Math.min(parent.width, 720) - text: I18n.tr("Shell add-ons and bundles you've installed.") + text: I18n.tr("Shell add-ons and bundles with parts already on this computer.") color: Tokens.inkMuted font.family: Tokens.ui font.pixelSize: Tokens.fBody @@ -1076,10 +1079,12 @@ Item { AddonSectionHeader { width: parent.width - title: I18n.tr("BUNDLES") + title: I18n.tr("BUNDLES WITH INSTALLED PARTS") countText: (pg.bundles.length === 1 ? I18n.tr("%1 BUNDLE") : I18n.tr("%1 BUNDLES")) .arg(pg.bundles.length) + actionText: I18n.tr("BROWSE BUNDLES") + onActionRequested: pg.browseBundles() onRefreshRequested: pg.loadBundles() } @@ -1097,7 +1102,7 @@ Item { bundle: installedBundleCard.modelData busy: pg.bundleBusyId === installedBundleCard.modelData.id onOpenRequested: pg.openBundle(installedBundleCard.modelData) - onRepairRequested: pg.repairBundle(installedBundleCard.modelData.id) + onInstallRequested: pg.installBundle(installedBundleCard.modelData.id) onRemoveRequested: pg.removeBundle(installedBundleCard.modelData.id, "") } } @@ -2088,7 +2093,7 @@ Item { loading: pg.bundleSheetLoading busy: pg.bundleBusyId === pg.bundleSheetId onDismissed: pg.bundleSheetOpen = false - onRepairRequested: pg.repairBundle(pg.bundleSheetId) + onInstallRequested: pg.installBundle(pg.bundleSheetId) onRemoveRequested: pg.removeBundle(pg.bundleSheetId, "") } diff --git a/ryoku/hub/quickshell/pages/AnimationsPage.qml b/ryoku/hub/quickshell/pages/AnimationsPage.qml index b74c74eb5a..16d87711f7 100644 --- a/ryoku/hub/quickshell/pages/AnimationsPage.qml +++ b/ryoku/hub/quickshell/pages/AnimationsPage.qml @@ -608,7 +608,7 @@ Item { width: mv.winW; height: 36; radius: Tokens.radius color: Tokens.bone y: mv.trackY - height; x: 0 - Text { anchors.centerIn: parent; text: "\u529b"; color: Tokens.inkOnBone; font.family: Tokens.jp; font.pixelSize: Tokens.fSmall } + Text { anchors.centerIn: parent; text: UiPreferences.brandMark("\u529b"); color: Tokens.inkOnBone; font.family: Tokens.jp; font.pixelSize: Tokens.fSmall } } NumberAnimation { id: anim @@ -657,7 +657,7 @@ Item { height: Tokens.s5 spacing: Tokens.s2 Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } + Text { text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("DESKTOP"); color: Tokens.inkMuted; font.family: Tokens.ui font.pixelSize: Tokens.fTiny; font.weight: Font.Medium; font.letterSpacing: Tokens.trackMark diff --git a/ryoku/hub/quickshell/pages/AppOverridesPage.qml b/ryoku/hub/quickshell/pages/AppOverridesPage.qml index 9bfcd3dfa9..db77fe98e8 100644 --- a/ryoku/hub/quickshell/pages/AppOverridesPage.qml +++ b/ryoku/hub/quickshell/pages/AppOverridesPage.qml @@ -102,7 +102,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/BarStudioPage.qml b/ryoku/hub/quickshell/pages/BarStudioPage.qml index 204bb19c5c..add4a1db42 100644 --- a/ryoku/hub/quickshell/pages/BarStudioPage.qml +++ b/ryoku/hub/quickshell/pages/BarStudioPage.qml @@ -219,7 +219,7 @@ Item { anchors.verticalCenter: parent.verticalCenter Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/ConnectionsPage.qml b/ryoku/hub/quickshell/pages/ConnectionsPage.qml index 57feb56fba..c6008b18d6 100644 --- a/ryoku/hub/quickshell/pages/ConnectionsPage.qml +++ b/ryoku/hub/quickshell/pages/ConnectionsPage.qml @@ -1390,10 +1390,12 @@ Item { readonly property bool adapterOn: adapter ? adapter.enabled === true : false readonly property bool discovering: adapter ? adapter.discovering === true : false readonly property bool hasAdapter: adapter !== null - // rfkill (airplane mode) blocks the radio at the kernel; BlueZ then - // refuses Powered=true. surfaced so the toggle can unblock first. - readonly property bool blocked: (adapter && typeof BluetoothAdapterState !== "undefined") + readonly property bool adapterBlocked: (adapter && typeof BluetoothAdapterState !== "undefined") ? adapter.state === BluetoothAdapterState.Blocked : false + readonly property bool blocked: adapterBlocked || softBlocked || hardBlocked + property bool softBlocked: false + property bool hardBlocked: false + onAdapterChanged: rfkillState.running = true readonly property int connectedCount: { var n = 0; for (var i = 0; i < devices.length; i++) @@ -1468,9 +1470,8 @@ Item { } // Keep the fresh-install default on, but persist every explicit choice. + // The helper can recover a soft-blocked radio before BlueZ has an adapter. function setAdapterEnabled(v) { - if (!bt.adapter) - return; Quickshell.execDetached(["ryoku-bluetooth-power", v ? "on" : "off"]); } @@ -1503,6 +1504,20 @@ Item { onTriggered: bt.failedAddress = "" } + Process { + id: rfkillState + command: ["rfkill", "-n", "-o", "TYPE,SOFT,HARD", "list", "bluetooth"] + running: true + stdout: StdioCollector { + onStreamFinished: { + const rows = text.trim().split("\n").map(line => line.trim().split(/\s+/)) + .filter(fields => fields[0] === "bluetooth"); + bt.softBlocked = rows.some(fields => fields[1] === "blocked"); + bt.hardBlocked = rows.some(fields => fields[2] === "blocked"); + } + } + } + Process { id: pairProc stdout: StdioCollector {} @@ -1616,11 +1631,11 @@ Item { } } - // one primary toggle for the whole adapter, hidden when the - // service is gone: a switch that can't act shouldn't look live. + // A platform rfkill switch can remove the adapter from + // BlueZ. Keep the switch available for a recoverable soft block. Sw { anchors.verticalCenter: parent.verticalCenter - visible: bt.hasAdapter + visible: bt.hasAdapter || bt.softBlocked on: bt.adapterOn onToggled: (v) => bt.setAdapterEnabled(v) } @@ -1653,8 +1668,8 @@ Item { Text { anchors.horizontalCenter: parent.horizontalCenter - text: !bt.hasAdapter ? I18n.tr("Bluetooth isn't available.") - : bt.blocked ? I18n.tr("Bluetooth is blocked.") + text: bt.blocked ? I18n.tr("Bluetooth is blocked.") + : !bt.hasAdapter ? I18n.tr("Bluetooth isn't available.") : !bt.adapterOn ? I18n.tr("Bluetooth is off.") : (bt.discovering ? I18n.tr("Scanning\u2026") : I18n.tr("No devices yet.")) color: Tokens.inkDim @@ -1665,8 +1680,9 @@ Item { Text { anchors.horizontalCenter: parent.horizontalCenter visible: !bt.hasAdapter || !bt.adapterOn || (!bt.discovering && bt.devices.length === 0) - text: !bt.hasAdapter ? I18n.tr("The bluetooth service (bluez) isn't running.") - : bt.blocked ? I18n.tr("The radio is off at the hardware level (rfkill); the toggle unblocks it.") + text: bt.softBlocked ? I18n.tr("The radio is software blocked; the toggle unblocks it.") + : bt.hardBlocked ? I18n.tr("The radio is off at the hardware level.") + : !bt.hasAdapter ? I18n.tr("The bluetooth service (bluez) isn't running.") : !bt.adapterOn ? I18n.tr("Turn the adapter on to see nearby and paired devices.") : I18n.tr("Hit Scan to discover nearby devices.") color: Tokens.inkMuted @@ -1678,7 +1694,7 @@ Item { // service repair, shown only when org.bluez is missing entirely. Btn { anchors.horizontalCenter: parent.horizontalCenter - visible: !bt.hasAdapter + visible: !bt.hasAdapter && !bt.blocked text: svcProc.running ? I18n.tr("Starting\u2026") : I18n.tr("Start service") armed: !svcProc.running onAct: bt.startService() @@ -2150,7 +2166,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "\u529b"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("\u529b"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/ControlsPage.qml b/ryoku/hub/quickshell/pages/ControlsPage.qml index 1f96073258..a87d7fd6c3 100644 --- a/ryoku/hub/quickshell/pages/ControlsPage.qml +++ b/ryoku/hub/quickshell/pages/ControlsPage.qml @@ -161,7 +161,7 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: Tokens.s2 Rectangle { width: 16; height: Tokens.border; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } - Text { text: "力"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } + Text { text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("DESKTOP") color: Tokens.inkMuted diff --git a/ryoku/hub/quickshell/pages/CreditsPage.qml b/ryoku/hub/quickshell/pages/CreditsPage.qml index a4c26b5929..b669be8653 100644 --- a/ryoku/hub/quickshell/pages/CreditsPage.qml +++ b/ryoku/hub/quickshell/pages/CreditsPage.qml @@ -117,7 +117,7 @@ Item { Text { anchors.centerIn: parent rotation: -90 - text: "RYOKU \u00b7 \u611f\u8b1d KANSHA \u00b7 SHOT ON BLACK" + text: UiPreferences.text("RYOKU \u00b7 \u611f\u8b1d KANSHA \u00b7 SHOT ON BLACK", "RYOKU \u00b7 THANKS \u00b7 SHOT ON BLACK") color: Tokens.inkFaint font.family: Tokens.mono font.pixelSize: Tokens.fTiny @@ -161,7 +161,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力" + text: UiPreferences.brandMark("力") color: Tokens.ink font.family: Tokens.jp font.pixelSize: Tokens.fMicro @@ -190,7 +190,7 @@ Item { width: parent.width spacing: 0 Text { - text: "感謝" + text: UiPreferences.text("感謝", "THANKS") color: Tokens.ink font.family: Tokens.jp font.pixelSize: pg.fKanji diff --git a/ryoku/hub/quickshell/pages/DictationPage.qml b/ryoku/hub/quickshell/pages/DictationPage.qml index debb9536d9..a5c3109bcd 100644 --- a/ryoku/hub/quickshell/pages/DictationPage.qml +++ b/ryoku/hub/quickshell/pages/DictationPage.qml @@ -288,7 +288,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/DisplaysPage.qml b/ryoku/hub/quickshell/pages/DisplaysPage.qml index 9babde6e2f..a7bfff2bca 100644 --- a/ryoku/hub/quickshell/pages/DisplaysPage.qml +++ b/ryoku/hub/quickshell/pages/DisplaysPage.qml @@ -895,7 +895,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "\u529b"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("\u529b"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/FastfetchPage.qml b/ryoku/hub/quickshell/pages/FastfetchPage.qml index 82fedc1574..613345cb1b 100644 --- a/ryoku/hub/quickshell/pages/FastfetchPage.qml +++ b/ryoku/hub/quickshell/pages/FastfetchPage.qml @@ -698,7 +698,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: Tokens.fMicro; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/GpuPage.qml b/ryoku/hub/quickshell/pages/GpuPage.qml index 788b29fbb3..89adc36fba 100644 --- a/ryoku/hub/quickshell/pages/GpuPage.qml +++ b/ryoku/hub/quickshell/pages/GpuPage.qml @@ -570,7 +570,7 @@ done spacing: Tokens.s2 Rectangle { width: 16; height: 1; color: Tokens.ink; anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/ImportPage.qml b/ryoku/hub/quickshell/pages/ImportPage.qml index 18346e6b40..d8de84c164 100644 --- a/ryoku/hub/quickshell/pages/ImportPage.qml +++ b/ryoku/hub/quickshell/pages/ImportPage.qml @@ -463,7 +463,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { @@ -1145,7 +1145,7 @@ Item { Row { spacing: Tokens.s2 Text { - text: pg.undone ? "空" : "力" + text: pg.undone ? UiPreferences.text("空", "EMPTY") : UiPreferences.brandMark("力") color: pg.undone ? Tokens.inkDim : Tokens.sun font.family: Tokens.jp; font.pixelSize: Tokens.fHero anchors.verticalCenter: parent.verticalCenter diff --git a/ryoku/hub/quickshell/pages/InputPage.qml b/ryoku/hub/quickshell/pages/InputPage.qml index 4229ba406a..dd87ae150f 100644 --- a/ryoku/hub/quickshell/pages/InputPage.qml +++ b/ryoku/hub/quickshell/pages/InputPage.qml @@ -1008,7 +1008,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/KeybindsPage.qml b/ryoku/hub/quickshell/pages/KeybindsPage.qml index 529b6f7568..253856961f 100644 --- a/ryoku/hub/quickshell/pages/KeybindsPage.qml +++ b/ryoku/hub/quickshell/pages/KeybindsPage.qml @@ -844,7 +844,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/LauncherPage.qml b/ryoku/hub/quickshell/pages/LauncherPage.qml index 27449d53bf..9da0c2618f 100644 --- a/ryoku/hub/quickshell/pages/LauncherPage.qml +++ b/ryoku/hub/quickshell/pages/LauncherPage.qml @@ -322,7 +322,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "\u529b"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("\u529b"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/LayerRulesPage.qml b/ryoku/hub/quickshell/pages/LayerRulesPage.qml index 8f739c2ecc..7240bf779d 100644 --- a/ryoku/hub/quickshell/pages/LayerRulesPage.qml +++ b/ryoku/hub/quickshell/pages/LayerRulesPage.qml @@ -157,7 +157,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/LockscreenPage.qml b/ryoku/hub/quickshell/pages/LockscreenPage.qml index 54b30b95ba..f03fb0fc6b 100644 --- a/ryoku/hub/quickshell/pages/LockscreenPage.qml +++ b/ryoku/hub/quickshell/pages/LockscreenPage.qml @@ -979,7 +979,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/PerformancePage.qml b/ryoku/hub/quickshell/pages/PerformancePage.qml index 3a18693128..b13ee3a9a1 100644 --- a/ryoku/hub/quickshell/pages/PerformancePage.qml +++ b/ryoku/hub/quickshell/pages/PerformancePage.qml @@ -305,7 +305,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/PhoneConnectionTab.qml b/ryoku/hub/quickshell/pages/PhoneConnectionTab.qml index 43a1962cde..6af42eb607 100644 --- a/ryoku/hub/quickshell/pages/PhoneConnectionTab.qml +++ b/ryoku/hub/quickshell/pages/PhoneConnectionTab.qml @@ -207,9 +207,7 @@ Item { id: switchProc property bool turningOn: false environment: Spawn.env - stderr: StdioCollector { - onStreamFinished: if (this.text.trim()) pg.error = this.text.trim() - } + stderr: StdioCollector { id: switchError } onExited: function(code) { if (code === 0 && turningOn) { Settings.patch("phone.enabled", true, function(ok, why) { @@ -223,8 +221,18 @@ Item { } pg.busy = false; pg.busyLabel = ""; - if (code !== 0 && pg.error === "") - pg.error = turningOn ? I18n.tr("Phone tools could not be installed.") : I18n.tr("Phone tools could not be removed."); + if (code !== 0) { + var lines = switchError.text.trim().split("\n"); + var detail = ""; + for (var i = 0; i < lines.length; i++) { + if (lines[i].trim().indexOf("error:") === 0) { + detail = lines[i].trim(); + break; + } + } + pg.error = detail || switchError.text.trim() + || (turningOn ? I18n.tr("Phone tools could not be installed.") : I18n.tr("Phone tools could not be removed.")); + } pg.refresh(); } } diff --git a/ryoku/hub/quickshell/pages/ProfilePage.qml b/ryoku/hub/quickshell/pages/ProfilePage.qml index 3d11f2cab3..d9667affca 100644 --- a/ryoku/hub/quickshell/pages/ProfilePage.qml +++ b/ryoku/hub/quickshell/pages/ProfilePage.qml @@ -384,7 +384,7 @@ Item { // A monumental face-kanji breathing behind the void (the Profile's own 横顔). Text { - visible: pg.blockVisible("watermark") + visible: UiPreferences.showJapaneseGloss && pg.blockVisible("watermark") opacity: pg.blockOpacity("watermark") anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter @@ -503,7 +503,9 @@ Item { y: 400 spacing: Tokens.s3 Repeater { - model: pg.f("text.verse", ["力", "美", "滅", "虚"]) + model: UiPreferences.showJapaneseGloss + ? pg.f("text.verse", ["力", "美", "滅", "虚"]) + : ["POWER", "BEAUTY", "DECAY", "VOID"] delegate: Text { required property var modelData text: modelData @@ -677,7 +679,7 @@ Item { spacing: Tokens.s2 Text { anchors.verticalCenter: parent.verticalCenter - text: "力" + text: UiPreferences.brandMark("力") color: Tokens.inkDim font.family: Tokens.jp font.pixelSize: Tokens.fMicro @@ -883,7 +885,7 @@ Item { { x: cx, sx: leadFrom, cy: firstY + gap, fx: 0.30, fy: 0.40, k: "gpu", name: "GPU", big: true }, { x: cx, sx: leadFrom, cy: firstY + 2 * gap, fx: 0.28, fy: 0.54, k: "mem", name: I18n.tr("MEMORY"), big: true }, { x: cx, sx: leadFrom, cy: firstY + 3 * gap, fx: 0.40, fy: 0.70, k: "net", name: I18n.tr("NETWORK"), big: true }, - { x: 372, sx: 372, cy: firstY + Math.round(gap * 0.94), fx: 0.47, fy: 0.44, k: "frac", name: "亀裂 · " + I18n.tr("FRACTURE"), big: false } + { x: 372, sx: 372, cy: firstY + Math.round(gap * 0.94), fx: 0.47, fy: 0.44, k: "frac", name: UiPreferences.text("亀裂 · ", "") + I18n.tr("FRACTURE"), big: false } ] // Eyebrow for the live column, with the never-still pulse. @@ -906,7 +908,7 @@ Item { } Text { anchors.verticalCenter: parent.verticalCenter - text: "脈 · " + I18n.tr("LIVE TELEMETRY") + text: UiPreferences.text("脈 · ", "") + I18n.tr("LIVE TELEMETRY") color: Tokens.inkMuted font.family: Tokens.ui font.pixelSize: Tokens.fTiny diff --git a/ryoku/hub/quickshell/pages/RashinPage.qml b/ryoku/hub/quickshell/pages/RashinPage.qml index fcebf8223d..73789bc4dc 100644 --- a/ryoku/hub/quickshell/pages/RashinPage.qml +++ b/ryoku/hub/quickshell/pages/RashinPage.qml @@ -291,7 +291,7 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: Tokens.s2 Text { - text: hd.kanji; color: Tokens.alert; font.family: Tokens.jp + text: UiPreferences.text(hd.kanji, ""); color: Tokens.alert; font.family: Tokens.jp font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter } Text { @@ -333,7 +333,7 @@ Item { font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } Text { - text: fc.kanji; color: Tokens.inkDim; font.family: Tokens.jp + text: UiPreferences.text(fc.kanji, ""); color: Tokens.inkDim; font.family: Tokens.jp font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } } @@ -552,6 +552,7 @@ Item { spacing: Tokens.s2 Text { text: "羅針"; color: Tokens.alert; font.family: Tokens.jp; font.pixelSize: 13 + visible: UiPreferences.showJapaneseGloss anchors.verticalCenter: parent.verticalCenter } Text { @@ -599,7 +600,7 @@ Item { spacing: 6 Row { spacing: Tokens.s2 - Text { text: "羅針"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 15; anchors.verticalCenter: parent.verticalCenter } + Text { visible: UiPreferences.showJapaneseGloss; text: "羅針"; color: Tokens.ink; font.family: Tokens.jp; font.pixelSize: 15; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("RASHIN"); color: Tokens.ink; font.family: Tokens.display font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter @@ -641,7 +642,7 @@ Item { spacing: 6 Row { spacing: Tokens.s2 - Text { text: "徘徊"; color: Tokens.alert; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } + Text { visible: UiPreferences.showJapaneseGloss; text: "徘徊"; color: Tokens.alert; font.family: Tokens.jp; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter } Text { text: I18n.tr("PROWL GATEWAY"); color: Tokens.ink; font.family: Tokens.display font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter diff --git a/ryoku/hub/quickshell/pages/RecordingPage.qml b/ryoku/hub/quickshell/pages/RecordingPage.qml index f747e07e62..7346241788 100644 --- a/ryoku/hub/quickshell/pages/RecordingPage.qml +++ b/ryoku/hub/quickshell/pages/RecordingPage.qml @@ -560,7 +560,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/SessionPage.qml b/ryoku/hub/quickshell/pages/SessionPage.qml index 87e2f38418..e2104594ea 100644 --- a/ryoku/hub/quickshell/pages/SessionPage.qml +++ b/ryoku/hub/quickshell/pages/SessionPage.qml @@ -191,7 +191,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/pages/UpdatesPage.qml b/ryoku/hub/quickshell/pages/UpdatesPage.qml index 176103fa95..73c2e95f70 100644 --- a/ryoku/hub/quickshell/pages/UpdatesPage.qml +++ b/ryoku/hub/quickshell/pages/UpdatesPage.qml @@ -83,6 +83,47 @@ Item { Process { id: saveInterval } + property bool systemFull: false + property bool systemTouched: false + property bool systemSavePending: false + + function setSystemFull(on) { + if (pg.systemFull === on) + return; + pg.systemTouched = true; + pg.systemFull = on; + pg.persistSystemFull(); + } + function persistSystemFull() { + if (saveSystem.running) { + pg.systemSavePending = true; + return; + } + saveSystem.command = ["ryoku-hub", "config", "set", "update_system", pg.systemFull ? "on" : "off"]; + saveSystem.running = true; + } + + Process { + command: ["ryoku-hub", "config", "get", "update_system"] + running: true + stdout: StdioCollector { + onStreamFinished: if (!pg.systemTouched) + pg.systemFull = (this.text.trim() === "on") + } + } + + Process { + id: saveSystem + onExited: code => { + if (code !== 0) + pg.actionError = I18n.tr("Couldn't save the system update choice."); + if (pg.systemSavePending) { + pg.systemSavePending = false; + Qt.callLater(() => pg.persistSystemFull()); + } + } + } + // re-check on the configured cadence. readonly property int intervalMs: { switch (pg.interval) { @@ -422,7 +463,7 @@ Item { Process { id: guiProc - command: ["ryoku", "update", "--gui"] + command: pg.systemFull ? ["ryoku", "update", "--gui", "--system"] : ["ryoku", "update", "--gui"] environment: Spawn.env stderr: StdioCollector { id: guiErr } onExited: (code) => { @@ -544,7 +585,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { @@ -1186,8 +1227,24 @@ Item { text: I18n.tr("CHECK AGAIN") onAct: Updates.check() } + Row { + anchors.verticalCenter: parent.verticalCenter + visible: pg.phase === "idle" && !pg.starting + spacing: Tokens.s2 + Text { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("INCLUDE SYSTEM") + color: Tokens.inkDim; font.family: Tokens.ui; font.pixelSize: Tokens.fMicro + font.letterSpacing: Tokens.trackMark + } + Sw { + anchors.verticalCenter: parent.verticalCenter + on: pg.systemFull + onToggled: (v) => pg.setSystemFull(v) + } + } Item { - visible: pg.phase === "idle" && !pg.starting && Updates.available + visible: pg.phase === "idle" && !pg.starting width: updateBtn.implicitWidth height: updateBtn.implicitHeight diff --git a/ryoku/hub/quickshell/pages/WindowRulesPage.qml b/ryoku/hub/quickshell/pages/WindowRulesPage.qml index 5cc23c39e7..a69a998178 100644 --- a/ryoku/hub/quickshell/pages/WindowRulesPage.qml +++ b/ryoku/hub/quickshell/pages/WindowRulesPage.qml @@ -171,7 +171,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "力"; color: Tokens.ink; font.family: Tokens.jp + text: UiPreferences.brandMark("力"); color: Tokens.ink; font.family: Tokens.jp font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter } Text { diff --git a/ryoku/hub/quickshell/schema/BluetoothTab.js b/ryoku/hub/quickshell/schema/BluetoothTab.js index 0cd2d400af..ecbb8c253c 100644 --- a/ryoku/hub/quickshell/schema/BluetoothTab.js +++ b/ryoku/hub/quickshell/schema/BluetoothTab.js @@ -12,6 +12,6 @@ var rows = [ "label": "Bluetooth", "desc": "Powers the adapter and remembers the choice across restarts", "ctl": "sw", - "src": "ryoku-bluetooth-power on|off writes the user's power choice, updates BlueZ and applies an rfkill soft block while off." + "src": "ryoku-bluetooth-power on|off writes the user's power choice, updates BlueZ, and clears legacy rfkill soft blocks while turning on." } ]; diff --git a/ryoku/hub/quickshell/schema/GlobalPage.js b/ryoku/hub/quickshell/schema/GlobalPage.js index 4c5dc275fd..9beb9ceba8 100644 --- a/ryoku/hub/quickshell/schema/GlobalPage.js +++ b/ryoku/hub/quickshell/schema/GlobalPage.js @@ -62,6 +62,14 @@ var rows = [ "ctl": "seg", "src": "shell", "opts": ["auto", "celsius", "fahrenheit"] + }, { + "tab": "Region & fonts", + "group": "INTERFACE", + "key": "showJapaneseGloss", + "label": "Japanese glosses", + "desc": "Show Ryoku's Japanese glosses, decorative text and mark across the desktop. Off keeps English and Latin marks; CJK fonts, language support and your content are unaffected.", + "ctl": "sw", + "src": "shell" }, { "tab": "Region & fonts", "group": "FONT", diff --git a/ryoku/hub/quickshell/schema/UpdatesPage.js b/ryoku/hub/quickshell/schema/UpdatesPage.js index a90dd2b178..b7022fa076 100644 --- a/ryoku/hub/quickshell/schema/UpdatesPage.js +++ b/ryoku/hub/quickshell/schema/UpdatesPage.js @@ -19,5 +19,14 @@ var rows = [ "daily", "weekly" ] + }, + { + "tab": "", + "group": "UPDATE NOW", + "key": "update_system", + "label": "Include system packages", + "desc": "Update now also takes the distribution's packages, kernel included (ryoku update --system)", + "ctl": "sw", + "src": "hub.toml), TOML table [ui], field update_system" } ]; diff --git a/ryoku/hub/quickshell/shell.qml b/ryoku/hub/quickshell/shell.qml index 92f6c4fc33..8bd31d3c20 100644 --- a/ryoku/hub/quickshell/shell.qml +++ b/ryoku/hub/quickshell/shell.qml @@ -7,6 +7,9 @@ import Ryoku.Ui.Singletons // surface); the Hyprland window rule floats and centres it. `qs -c hub` loads // this (the config dir and binary keep the internal "hub" name). ShellRoot { + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") + FloatingWindow { id: win title: I18n.tr("Ryoku Settings") diff --git a/ryoku/hyprland/CHANGELOG.md b/ryoku/hyprland/CHANGELOG.md index 0834547d6a..c3e76ab6b3 100644 --- a/ryoku/hyprland/CHANGELOG.md +++ b/ryoku/hyprland/CHANGELOG.md @@ -25,6 +25,10 @@ (`modules/binds.lua`, `modules/ryoshot.lua`). ### Fixed +- **Game Mode keeps compositor blur, shadows and animations off across reloads.** + The live action and config both load one strip; tiled windows keep their faint + tint without blurring the moving desktop, while floating glass still blurs + (`hyprland.lua`, `modules/game_mode.lua`, `modules/decoration.lua`). - **Super+Escape always opens Controls while an app holds the keyboard.** The Hyprland bind bypasses keyboard-shortcut inhibition from apps such as VM viewers, leaving Lock, Log out, Restart and Power off reachable without diff --git a/ryoku/hyprland/hyprland.lua b/ryoku/hyprland/hyprland.lua index 6f589575a3..bc10f8cc90 100644 --- a/ryoku/hyprland/hyprland.lua +++ b/ryoku/hyprland/hyprland.lua @@ -65,3 +65,12 @@ optional("ghosttype") -- last word: ~/.config/hypr/user.lua. seeded once with a header explaining the -- load order, then yours; never touched by updates. optional("user") + +-- Game Mode's persisted strip is the one override user.lua cannot undo. +local home = os.getenv("HOME") or "" +local state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state") +local game_mode = io.open(state_home .. "/ryoku/game-mode.enabled", "r") +if game_mode then + game_mode:close() + require("modules.game_mode") +end diff --git a/ryoku/hyprland/modules/decoration.lua b/ryoku/hyprland/modules/decoration.lua index 51046b9d2c..ccec864c96 100644 --- a/ryoku/hyprland/modules/decoration.lua +++ b/ryoku/hyprland/modules/decoration.lua @@ -83,6 +83,14 @@ hl.config({ }, }) +-- Inactive tiled windows keep their 94% tint, but do not blur the whole +-- animating desktop beneath them. Floating glass and layer cards still blur. +hl.window_rule({ + name = "opaque-tile-no-blur", + match = { float = false }, + no_blur = true, +}) + -- Sidebar panels stay mapped at a 1 px floor, like ryoku-frame-edge. Their QML -- resize and exclusive-zone motion never remaps the layer, so no_anim would only -- affect startup. The clear dismiss layer is not matched by any blur rule. diff --git a/ryoku/hyprland/modules/game_mode.lua b/ryoku/hyprland/modules/game_mode.lua new file mode 100644 index 0000000000..188ffff2cb --- /dev/null +++ b/ryoku/hyprland/modules/game_mode.lua @@ -0,0 +1,18 @@ +-- Game Mode's strip, shared by the live WM action and every config reload while +-- the request is on. Immediate tearing reaches only the sole fullscreen game and +-- VRR only fullscreen windows; render_unfocused keeps a game on another +-- workspace drawing, so its input and multiplayer never stall. +hl.config({ + general = { allow_tearing = true }, + decoration = { + rounding = 0, + active_opacity = 1.0, + inactive_opacity = 1.0, + blur = { enabled = false }, + shadow = { enabled = false }, + }, + animations = { enabled = false }, + misc = { vrr = 2 }, +}) +hl.window_rule({ name = "game-mode-immediate", match = { class = ".*" }, immediate = true }) +hl.window_rule({ name = "game-mode-render-unfocused", match = { fullscreen = true }, render_unfocused = true }) diff --git a/ryoku/i18n/catalog/ar.json b/ryoku/i18n/catalog/ar.json index 959d94f24c..02e4832ac8 100644 --- a/ryoku/i18n/catalog/ar.json +++ b/ryoku/i18n/catalog/ar.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "تحرير", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "تحرير شيما", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "تحرير سطح المكتب", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "تم تطبيق نصف قطر تمويه غاوسي على النسخة. الأعلى هو ليونة.", "General": "عام", + "General settings": "الإعدادات العامة", "General limit: %1": "الحد الأقصى: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "لا تأخذ التركيز أبدا", "New": "جديد", "New chat": "دردشة جديدة", - "New folder": "New folder", + "New folder": "مجلد جديد", "New keyring password": "كلمة مرور حلقة مفاتيح جديدة", "New machine defaults": "افتراضيات الجهاز الجديد", "New machine from ISO": "جهاز جديد من ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "إجراءات سريعة", "Quick Settings": "الإعدادات السريعة", - "Quick controls": "Quick controls", + "Quick controls": "عناصر التحكم السريعة", "Quick place": "Quick place", "Quick settings": "إعدادات سريعة", "Quicklink": "رابط سريع", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "إعادة تحميل", "Reload cover": "إعادة تحميل الغطاء", + "Reload desktop": "إعادة تحميل سطح المكتب", "Reload shell": "إعادة تحميل قذيفة", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "جاري إعادة تحميل سطح المكتب", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "يحرك الظل جانبيًا، كما لو كان مضاءً من جانب واحد", "Shifts the shadow up or down; positive is below": "يحرك الظل لأعلى أو لأسفل، الموجب يكون للأسفل", "Shima": "Shima", + "Shima Studio": "استوديو شيما", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "الفن الذي تم شحنه", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "ابقَ ظاهرًا في مساحات العمل الفارغة", "Steam API key": "مفتاح واجهة برمجة تطبيقات Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "متصفح ستيم ووركشوب", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "استوديو", "Style": "النمط", "Style name": "Style name", "Subject": "الموضوع", diff --git a/ryoku/i18n/catalog/bn.json b/ryoku/i18n/catalog/bn.json index 283390236b..837c9c61f3 100644 --- a/ryoku/i18n/catalog/bn.json +++ b/ryoku/i18n/catalog/bn.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "সম্পাদনা করুন", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "শিমা সম্পাদনা", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "ডেস্কটপ সম্পাদনা", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "গাউসিয়ান ব্লার ব্যাসার্ধ অনুলিপিতে প্রয়োগ করা হয়েছে। উচ্চতর নরম।", "General": "সাধারণ", + "General settings": "সাধারণ সেটিংস", "General limit: %1": "সাধারণ সীমা: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "কখনই ফোকাস করবেন না", "New": "নতুন", "New chat": "নতুন আড্ডা", - "New folder": "New folder", + "New folder": "নতুন ফোল্ডার", "New keyring password": "নতুন কীরিং পাসওয়ার্ড", "New machine defaults": "নতুন মেশিন ডিফল্ট", "New machine from ISO": "ISO থেকে নতুন মেশিন", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "দ্রুত অ্যাকশন", "Quick Settings": "দ্রুত সেটিংস", - "Quick controls": "Quick controls", + "Quick controls": "দ্রুত নিয়ন্ত্রণ", "Quick place": "Quick place", "Quick settings": "দ্রুত সেটিংস", "Quicklink": "দ্রুত লিংক", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "পুনরায় লোড করুন", "Reload cover": "কভার পুনরায় লোড করুন", + "Reload desktop": "ডেস্কটপ পুনরায় লোড করুন", "Reload shell": "শেল পুনরায় লোড করুন", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "ডেস্কটপ পুনরায় লোড করছি", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "ছায়াকে পাশে সরায়, যেন এক দিক থেকে আলো পড়ছে", "Shifts the shadow up or down; positive is below": "ছায়াকে উপরে বা নিচে সরায়; ধনাত্মক নিচে থাকে", "Shima": "Shima", + "Shima Studio": "শিমা স্টুডিও", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "প্রেরিত শিল্প", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "খালি কর্মক্ষেত্রে দৃশ্যমান থাকুন", "Steam API key": "স্টিম এপিআই কী", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "স্টিম ওয়ার্কশপ ব্রাউজার", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "স্টুডিও", "Style": "শৈলী", "Style name": "Style name", "Subject": "বিষয়", diff --git a/ryoku/i18n/catalog/cs.json b/ryoku/i18n/catalog/cs.json index eb041b2a62..26fc2b21d1 100644 --- a/ryoku/i18n/catalog/cs.json +++ b/ryoku/i18n/catalog/cs.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Upravit", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Upravit Shimu", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Upravit plochu", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Gaussův poloměr rozostření použitý na kopii. Vyšší je měkčí.", "General": "Generál", + "General settings": "Obecná nastavení", "General limit: %1": "Obecný limit: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nikdy se nesoustřeďte", "New": "Nový", "New chat": "Nový chat", - "New folder": "New folder", + "New folder": "Nová složka", "New keyring password": "Nové heslo pro klíčenku", "New machine defaults": "Výchozí nastavení nového stroje", "New machine from ISO": "Nový stroj od ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Rychlé akce", "Quick Settings": "Rychlé nastavení", - "Quick controls": "Quick controls", + "Quick controls": "Rychlé ovládání", "Quick place": "Quick place", "Quick settings": "Rychlé nastavení", "Quicklink": "Rychlý odkaz", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Znovu načíst", "Reload cover": "Znovu načtěte kryt", + "Reload desktop": "Znovu načíst plochu", "Reload shell": "Znovu načtěte shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Znovu načítání plochy", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Posouvá stín do strany, jako by byl osvětlen z jedné strany", "Shifts the shadow up or down; positive is below": "Posouvá stín nahoru nebo dolů; kladné je dole", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Odeslané umění", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Zůstat viditelný na prázdných pracovních plochách", "Steam API key": "Klíč API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Prohlížeč Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Ateliér", "Style": "styl", "Style name": "Style name", "Subject": "Předmět", diff --git a/ryoku/i18n/catalog/de.json b/ryoku/i18n/catalog/de.json index 4e6f4c6445..3abd72e5b4 100644 --- a/ryoku/i18n/catalog/de.json +++ b/ryoku/i18n/catalog/de.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Bearbeiten", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shima bearbeiten", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Desktop bearbeiten", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Auf die Kopie angewendeter Gaußscher Unschärferadius. Höher ist weicher.", "General": "Allgemein", + "General settings": "Allgemeine Einstellungen", "General limit: %1": "Allgemeines Limit: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Konzentrieren Sie sich niemals", "New": "Neu", "New chat": "Neuer Chat", - "New folder": "New folder", + "New folder": "Neuer Ordner", "New keyring password": "Neues Schlüsselbund-Passwort", "New machine defaults": "Neue Maschinenstandards", "New machine from ISO": "Neue Maschine von ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Schnelle Aktionen", "Quick Settings": "Schnelleinstellungen", - "Quick controls": "Quick controls", + "Quick controls": "Schnellsteuerung", "Quick place": "Quick place", "Quick settings": "Schnelleinstellungen", "Quicklink": "Quicklink", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Neu laden", "Reload cover": "Deckung neu laden", + "Reload desktop": "Desktop neu laden", "Reload shell": "Shell neu laden", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Lade Desktop neu", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Verschiebt den Schatten seitlich, als ob er von einer Seite beleuchtet wäre", "Shifts the shadow up or down; positive is below": "Verschiebt den Schatten nach oben oder unten; positiv ist unten", "Shima": "Shima", + "Shima Studio": "Shima-Studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Kunst verschickt", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Auf leeren Arbeitsflächen sichtbar bleiben", "Steam API key": "Steam-API-Schlüssel", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop-Browser", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Gestaltungsstudio", "Style": "Stil", "Style name": "Style name", "Subject": "Betreff", diff --git a/ryoku/i18n/catalog/el.json b/ryoku/i18n/catalog/el.json index 6ee932cae1..5fca510b2b 100644 --- a/ryoku/i18n/catalog/el.json +++ b/ryoku/i18n/catalog/el.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Επεξεργασία", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Επεξεργασία Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Επεξεργασία επιφάνειας εργασίας", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Ακτίνα Gaussian blur στο αντίγραφο. Υψηλότερη είναι μαλακότερη.", "General": "Γενικά", + "General settings": "Γενικές ρυθμίσεις", "General limit: %1": "Γενικό όριο: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Ποτέ εστίαση", "New": "Νέο", "New chat": "Νέα συνομιλία", - "New folder": "New folder", + "New folder": "Νέος φάκελος", "New keyring password": "Νέος κωδικός κλειδοθήκης", "New machine defaults": "Προεπιλογές νέας μηχανής", "New machine from ISO": "Νέα μηχανή από ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Γρήγορες Ενέργειες", "Quick Settings": "Γρήγορες Ρυθμίσεις", - "Quick controls": "Quick controls", + "Quick controls": "Γρήγορες ρυθμίσεις", "Quick place": "Quick place", "Quick settings": "Γρήγορες ρυθμίσεις", "Quicklink": "Γρήγορος σύνδεσμος", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Επαναφόρτωση", "Reload cover": "Επαναφόρτωση εξωφύλλου", + "Reload desktop": "Επαναφόρτωση επιφάνειας εργασίας", "Reload shell": "Επαναφόρτωση shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Επαναφόρτωση της επιφάνειας εργασίας", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Μετατοπίζει τη σκιά πλάγια, σαν να φωτίζεται από μία πλευρά", "Shifts the shadow up or down; positive is below": "Μετατοπίζει τη σκιά πάνω ή κάτω, θετικό είναι κάτω", "Shima": "Shima", + "Shima Studio": "Στούντιο Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Προεγκατεστημένη τέχνη", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Να παραμένει ορατό σε άδειους χώρους εργασίας", "Steam API key": "Κλειδί Steam API", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Περιηγητής Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Στούντιο", "Style": "Στυλ", "Style name": "Style name", "Subject": "Θέμα", diff --git a/ryoku/i18n/catalog/en.json b/ryoku/i18n/catalog/en.json index 7487843811..926a0e4c05 100644 --- a/ryoku/i18n/catalog/en.json +++ b/ryoku/i18n/catalog/en.json @@ -84,11 +84,7 @@ "%1 commits": "%1 commits", "%1 commits behind · checked %2": "%1 commits behind · checked %2", "%1 component": "%1 component", - "%1 component missing": "%1 component missing", - "%1 component unavailable": "%1 component unavailable", "%1 components": "%1 components", - "%1 components missing": "%1 components missing", - "%1 components unavailable": "%1 components unavailable", "%1 conflicting shortcut": "%1 conflicting shortcut", "%1 conflicting shortcuts": "%1 conflicting shortcuts", "%1 connected": "%1 connected", @@ -118,7 +114,6 @@ "%1 finished": "%1 finished", "%1 focus sessions today": "%1 focus sessions today", "%1 for today": "%1 for today", - "%1 found. Rescan after plugging something in.": "%1 found. Rescan after plugging something in.", "%1 free · %2": "%1 free · %2", "%1 glasses": "%1 glasses", "%1 grains/frame · %2": "%1 grains/frame · %2", @@ -159,7 +154,6 @@ "%1 known": "%1 known", "%1 laps": "%1 laps", "%1 late": "%1 late", - "%1 layers ready": "%1 layers ready", "%1 left": "%1 left", "%1 left · %2 done today": "%1 left · %2 done today", "%1 machines · %2 remotes": "%1 machines · %2 remotes", @@ -180,7 +174,7 @@ "%1 notifications": "%1 notifications", "%1 of %2": "%1 of %2", "%1 of %2 glasses · %3 ml": "%1 of %2 glasses · %3 ml", - "%1 of %2 installed": "%1 of %2 installed", + "%1 of %2 required installed": "%1 of %2 required installed", "%1 on %2": "%1 on %2", "%1 on the default": "%1 on the default", "%1 only": "%1 only", @@ -207,6 +201,8 @@ "%1 reminders": "%1 reminders", "%1 renders here": "%1 renders here", "%1 request failed (HTTP %2).": "%1 request failed (HTTP %2).", + "%1 required component missing": "%1 required component missing", + "%1 required components missing": "%1 required components missing", "%1 results": "%1 results", "%1 running": "%1 running", "%1 s": "%1 s", @@ -229,7 +225,6 @@ "%1 tokens": "%1 tokens", "%1 took longer than %2 minutes and was stopped. Try a lighter model.": "%1 took longer than %2 minutes and was stopped. Try a lighter model.", "%1 tools": "%1 tools", - "%1 unavailable": "%1 unavailable", "%1 unbinds added so your shortcuts win": "%1 unbinds added so your shortcuts win", "%1 usage unavailable": "%1 usage unavailable", "%1 volume": "%1 volume", @@ -446,7 +441,6 @@ "(uninstall)": "(uninstall)", "(unnamed)": "(unnamed)", "(user)": "(user)", - "(user, clearable)": "(user, clearable)", "); write only under stateDir (R8)": "); write only under stateDir (R8)", "+ %d more": "+ %d more", "+ ADD": "+ ADD", @@ -492,7 +486,6 @@ "1 hour": "1 hour", "1 item": "1 item", "1 item on the dock besides apps": "1 item on the dock besides apps", - "1 layer ready": "1 layer ready", "1 min": "1 min", "1 minute": "1 minute", "1 notification": "1 notification", @@ -645,15 +638,12 @@ "": "", " (dynamic; fallback \\\"DISPLAY\\\")": " (dynamic; fallback \\\"DISPLAY\\\")", - "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` follows unstable builds again.": "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` follows unstable builds again.", "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` switches to the source checkout.": "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` switches to the source checkout.", "==> Already tracking %s; moving the Ryoku set to what it serves": "==> Already tracking %s; moving the Ryoku set to what it serves", "==> Moving the Ryoku set to release %s": "==> Moving the Ryoku set to release %s", "==> Moving the Ryoku set to unstable build %s": "==> Moving the Ryoku set to unstable build %s", "==> Now tracking stable packages: named releases as they are published.": "==> Now tracking stable packages: named releases as they are published.", - "==> Now tracking unstable packages: rebuilt on every push. `ryoku track stable` returns to stable releases.": "==> Now tracking unstable packages: rebuilt on every push. `ryoku track stable` returns to stable releases.", "==> Pinned to release %s. `ryoku update` keeps this release; `ryoku track stable` follows releases again.": "==> Pinned to release %s. `ryoku update` keeps this release; `ryoku track stable` follows releases again.", - "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` follows unstable builds again.": "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` follows unstable builds again.", "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` switches to the source checkout.": "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` switches to the source checkout.", "==> Retired the source checkout as the update source; the ~/ryoku-arch clone stays on disk but no longer drives updates.": "==> Retired the source checkout as the update source; the ~/ryoku-arch clone stays on disk but no longer drives updates.", "==> Unstable is now the source checkout; `ryoku track unstable` switches this box to it. `ryoku track stable` returns to stable packages.": "==> Unstable is now the source checkout; `ryoku track unstable` switches this box to it. `ryoku track stable` returns to stable packages.", @@ -675,7 +665,6 @@ "A Material Symbol by name, or any picture in assets/icons; black-and-white ones take the theme's colours": "A Material Symbol by name, or any picture in assets/icons; black-and-white ones take the theme's colours", "A NAME=value exported into your session at login.": "A NAME=value exported into your session at login.", "A Pixel Watch-inspired concentric dial clock with modular complications and interactive customization.": "A Pixel Watch-inspired concentric dial clock with modular complications and interactive customization.", - "A Ryoku effect is drawn live, so there is nothing to store in the device. One of its own effects can be saved, if it offers that.": "A Ryoku effect is drawn live, so there is nothing to store in the device. One of its own effects can be saved, if it offers that.", "A Ryoku tool's own JSON/TOML state under ~/.config/ryoku, each with one\nwriter. Change them through their GUI or command, never by hand:": "A Ryoku tool's own JSON/TOML state under ~/.config/ryoku, each with one\nwriter. Change them through their GUI or command, never by hand:", "A Ryoku tool's store. Change it through its Ryoku Hub page or the owning command, not by hand.": "A Ryoku tool's store. Change it through its Ryoku Hub page or the owning command, not by hand.", "A Scallop clock face with outer 5-minute step numbers, inner 1-12 hour numbers, and a center date badge.": "A Scallop clock face with outer 5-minute step numbers, inner 1-12 hour numbers, and a center date badge.", @@ -700,7 +689,6 @@ "A clear list.": "A clear list.", "A clear list. Room for what matters next.": "A clear list. Room for what matters next.", "A clock island that grows with you.": "A clock island that grows with you.", - "A colour of your own, kept through wallpaper changes.": "A colour of your own, kept through wallpaper changes.", "A coloured halo cast behind every window": "A coloured halo cast behind every window", "A command is required.": "A command is required.", "A concentric circle clock featuring outer minute step numbers, an intermediate circle with a smooth minute pointer tab, and a central two-digit hour circle.": "A concentric circle clock featuring outer minute step numbers, an intermediate circle with a smooth minute pointer tab, and a central two-digit hour circle.", @@ -708,7 +696,6 @@ "A count of unread notifications; its card lists them.": "A count of unread notifications; its card lists them.", "A count on an app with unread notifications": "A count on an app with unread notifications", "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out %1/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.": "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out %1/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.", - "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out ~/Pictures/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.": "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out ~/Pictures/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.", "A download is running. Cancel it, or quit again to abandon it": "A download is running. Cancel it, or quit again to abandon it", "A drawing is on screen": "A drawing is on screen", "A fainter divider colour.": "A fainter divider colour.", @@ -869,7 +856,6 @@ "APPLY": "APPLY", "APPLY INSTALLED STYLE": "APPLY INSTALLED STYLE", "APPLYING": "APPLYING", - "APPROVE IN CHAT": "APPROVE IN CHAT", "APPS": "APPS", "APPS & KEYS": "APPS & KEYS", "ARRANGEMENT": "ARRANGEMENT", @@ -1111,6 +1097,7 @@ "Align now": "Align now", "Align right": "Align right", "Align top": "Align top", + "Aligning %d newer package(s) from [ryoku]": "Aligning %d newer package(s) from [ryoku]", "Alignment": "Alignment", "All": "All", "All Day": "All Day", @@ -1335,8 +1322,6 @@ "Ask routes": "Ask routes", "Ask something": "Ask something", "Ask the needle anything. ENTER sends; \\resume recalls recent asks.": "Ask the needle anything. ENTER sends; \\resume recalls recent asks.", - "Asking the lighting providers what is connected. The first scan can take a few seconds.": "Asking the lighting providers what is connected. The first scan can take a few seconds.", - "Asks": "Asks", "Asks geoclue instead of guessing from your connection. Off unless you want it.": "Asks geoclue instead of guessing from your connection. Off unless you want it.", "Assistant Icon": "Assistant Icon", "At": "At", @@ -1458,6 +1443,7 @@ "BRING": "BRING", "BRING ICONS HERE": "BRING ICONS HERE", "BROWSE": "BROWSE", + "BROWSE BUNDLES": "BROWSE BUNDLES", "BROWSE RYOSTORE": "BROWSE RYOSTORE", "BROWSE STORE": "BROWSE STORE", "BUILD AND ADD": "BUILD AND ADD", @@ -1465,7 +1451,7 @@ "BUILDING": "BUILDING", "BUILDING ·": "BUILDING ·", "BUILDING…": "BUILDING…", - "BUNDLES": "BUNDLES", + "BUNDLES WITH INSTALLED PARTS": "BUNDLES WITH INSTALLED PARTS", "BURN": "BURN", "BURNING": "BURNING", "Back": "Back", @@ -1605,7 +1591,6 @@ "Best downloads the 4K file and plays a copy sized to your screen; the HD files are heavily compressed. Light takes HD: a fifth of the download, visibly softer.": "Best downloads the 4K file and plays a copy sized to your screen; the HD files are heavily compressed. Light takes HD: a fifth of the download, visibly softer.", "Bias the field toward a part of the mix": "Bias the field toward a part of the mix", "Bibata cursor and the selected AUR app components": "Bibata cursor and the selected AUR app components", - "Bibata cursor, LocalSend and Voxtype": "Bibata cursor, LocalSend and Voxtype", "Binary": "Binary", "Binary data": "Binary data", "Binary splits, master and stack, or a scrolling strip": "Binary splits, master and stack, or a scrolling strip", @@ -1803,15 +1788,12 @@ "Build rules from condition blocks and drag to set priority; the first match wins. Legacy day and night settings become two rules on first open.": "Build rules from condition blocks and drag to set priority; the first match wins. Legacy day and night settings become two rules on first open.", "Build the depth map": "Build the depth map", "Building %1": "Building %1", - "Building layers · %1%": "Building layers · %1%", "Building the AUR extras": "Building the AUR extras", "Building the Ryoku desktop from source": "Building the Ryoku desktop from source", - "Building the scene layers": "Building the scene layers", "Built-in": "Built-in", "Built-in display": "Built-in display", "Built: %1": "Built: %1", "Bun": "Bun", - "Bundle complete": "Bundle complete", "Bundle version, environment, and recent logs into one file to attach to an issue.": "Bundle version, environment, and recent logs into one file to attach to an issue.", "Bundled word source; no network requests.": "Bundled word source; no network requests.", "Bundles": "Bundles", @@ -1860,7 +1842,6 @@ "CHANGE ON MAP": "CHANGE ON MAP", "CHAT": "CHAT", "CHAT BACKEND": "CHAT BACKEND", - "CHEATSHEET": "CHEATSHEET", "CHECK AGAIN": "CHECK AGAIN", "CHOOSE": "CHOOSE", "CHOOSE .TXT": "CHOOSE .TXT", @@ -1940,7 +1921,6 @@ "CUSTOM": "CUSTOM", "CUSTOM ASSET": "CUSTOM ASSET", "CUSTOM OFF": "CUSTOM OFF", - "CUT QUALITY": "CUT QUALITY", "CUT-OUT MODEL": "CUT-OUT MODEL", "CUT-OUTS": "CUT-OUTS", "CW %1": "CW %1", @@ -2098,7 +2078,6 @@ "Changes the active profile now.": "Changes the active profile now.", "Changing models switches to a separate index. Search becomes available when the background index task finishes.": "Changing models switches to a separate index. Search becomes available when the background index task finishes.", "Channel history diverged; reconciling %s onto its checked target": "Channel history diverged; reconciling %s onto its checked target", - "Channel history diverged; reconciling %s onto origin/%s": "Channel history diverged; reconciling %s onto origin/%s", "Character": "Character", "Character columns the art spans": "Character columns the art spans", "Character sets corners, fills and contrast at once; the rows below fine-tune it.": "Character sets corners, fills and contrast at once; the rows below fine-tune it.", @@ -2119,10 +2098,7 @@ "Charging at": "Charging at", "Chassis": "Chassis", "Chat": "Chat", - "Chat models": "Chat models", - "Chat models load with the agent": "Chat models load with the agent", "ChatGPT / Codex": "ChatGPT / Codex", - "Chats": "Chats", "Cheatsheet": "Cheatsheet", "Check again": "Check again", "Check again after installing an app or changing its configuration.": "Check again after installing an app or changing its configuration.", @@ -2141,12 +2117,12 @@ "Checking Gmail setup...": "Checking Gmail setup...", "Checking account support…": "Checking account support…", "Checking bundle components…": "Checking bundle components…", + "Checking for connected phones": "Checking for connected phones", "Checking installed components": "Checking installed components", "Checking lighting support…": "Checking lighting support…", "Checking phone support...": "Checking phone support...", "Checking saved and connected lighting devices…": "Checking saved and connected lighting devices…", "Checking the cut-out model catalogue": "Checking the cut-out model catalogue", - "Checking the model catalogue": "Checking the model catalogue", "Checking the spatial model": "Checking the spatial model", "Checking...": "Checking...", "Checking…": "Checking…", @@ -2155,7 +2131,6 @@ "Checklists": "Checklists", "Checks for unstable-dev at %s are still running (%s)": "Checks for unstable-dev at %s are still running (%s)", "Choose": "Choose", - "Choose Depth or Parallax, or add a PNG, to build the layer stack.": "Choose Depth or Parallax, or add a PNG, to build the layer stack.", "Choose Image": "Choose Image", "Choose Set as backdrop on a wallpaper, or enter an image, video, or Wallpaper Engine project path. Leave empty to use the last wallpaper.": "Choose Set as backdrop on a wallpaper, or enter an image, video, or Wallpaper Engine project path. Leave empty to use the last wallpaper.", "Choose a built-in palette, one you saved in the designer, or Custom seeds to generate one from your own colours.": "Choose a built-in palette, one you saved in the designer, or Custom seeds to generate one from your own colours.", @@ -2163,7 +2138,6 @@ "Choose a command first": "Choose a command first", "Choose a condition, or add a nested group when this rule needs mixed all/any logic.": "Choose a condition, or add a nested group when this rule needs mixed all/any logic.", "Choose a config folder": "Choose a config folder", - "Choose a cut quality, then build the scene.": "Choose a cut quality, then build the scene.", "Choose a device": "Choose a device", "Choose a disk strategy first (press esc).": "Choose a disk strategy first (press esc).", "Choose a display, or use whichever monitor is focused when the picker opens. Close and reopen the picker after changing this.": "Choose a display, or use whichever monitor is focused when the picker opens. Close and reopen the picker after changing this.", @@ -2178,10 +2152,10 @@ "Choose a name for this stack": "Choose a name for this stack", "Choose a network": "Choose a network", "Choose a network below.": "Choose a network below.", + "Choose a phone": "Choose a phone", "Choose a phone first": "Choose a phone first", "Choose a picture": "Choose a picture", "Choose a picture or live wall. Ryoku follows its colours automatically.": "Choose a picture or live wall. Ryoku follows its colours automatically.", - "Choose a picture, then confirm the cut": "Choose a picture, then confirm the cut", "Choose a playlist from the index": "Choose a playlist from the index", "Choose a semantic-pack.json or .skwdmodel archive. Ryogami validates the pack, installs it, and selects it.": "Choose a semantic-pack.json or .skwdmodel archive. Ryogami validates the pack, installs it, and selects it.", "Choose a size to add a ready-to-use preset. Expand it to change orientation, rename it, or set a custom range.": "Choose a size to add a ready-to-use preset. Expand it to change orientation, rename it, or set a custom range.", @@ -2242,6 +2216,7 @@ "Choose where this playlist runs. Selecting every display creates one shared assignment.": "Choose where this playlist runs. Selecting every display creates one shared assignment.", "Choose whether a window pause condition pauses its display or every display.": "Choose whether a window pause condition pauses its display or every display.", "Choose whether downloads use the running Steam client or steamcmd. Restart the wallpaper service after changing this.": "Choose whether downloads use the running Steam client or steamcmd. Restart the wallpaper service after changing this.", + "Choose whether one click selects or opens an icon": "Choose whether one click selects or opens an icon", "Choose whether the whole field stays flat or bends into an arc, wave, S shape, or cylinder.": "Choose whether the whole field stays flat or bends into an arc, wave, S shape, or cylinder.", "Choose which apps follow your colours. Turning an app off restores its previous theme setup.": "Choose which apps follow your colours. Turning an app off restores its previous theme setup.", "Choose which dominant wallpaper colour leads.": "Choose which dominant wallpaper colour leads.", @@ -2365,7 +2340,6 @@ "Close all windows": "Close all windows", "Close equalizer": "Close equalizer", "Close mixer": "Close mixer", - "Close models": "Close models", "Close on selection": "Close on selection", "Close panel": "Close panel", "Close settings": "Close settings", @@ -2519,13 +2493,11 @@ "Confirm or change the suggested profile.": "Confirm or change the suggested profile.", "Confirm passphrase:": "Confirm passphrase:", "Confirm password": "Confirm password", - "Confirm password:": "Confirm password:", "Confirm root password": "Confirm root password", "Connect": "Connect", "Connect %1 in Tasks settings": "Connect %1 in Tasks settings", "Connect Ryogami to the desktop and the overview.": "Connect Ryogami to the desktop and the overview.", "Connect a USB cable": "Connect a USB cable", - "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills, then routes that agent's models through the Prowl gateway.": "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills, then routes that agent's models through the Prowl gateway.", "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills. It never changes the agent's own model; making Prowl auto the default is a separate switch.": "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills. It never changes the agent's own model; making Prowl auto the default is a separate switch.", "Connect automatically": "Connect automatically", "Connect one in Settings": "Connect one in Settings", @@ -2540,7 +2512,6 @@ "Connected Devices Battery List (1x1)": "Connected Devices Battery List (1x1)", "Connected Devices Battery List (2x1)": "Connected Devices Battery List (2x1)", "Connected and saved devices, including devices that are currently unplugged.": "Connected and saved devices, including devices that are currently unplugged.", - "Connected devices": "Connected devices", "Connected display count is": "Connected display count is", "Connected to %1": "Connected to %1", "Connected to %1 · %2%": "Connected to %1 · %2%", @@ -2676,6 +2647,7 @@ "Could not copy the SSH command": "Could not copy the SSH command", "Could not copy to clipboard": "Could not copy to clipboard", "Could not create the phone microphone source": "Could not create the phone microphone source", + "Could not create the temporary image": "Could not create the temporary image", "Could not enable wireless ADB": "Could not enable wireless ADB", "Could not fetch the OS catalogue": "Could not fetch the OS catalogue", "Could not find %1 again": "Could not find %1 again", @@ -2685,6 +2657,8 @@ "Could not load today's games": "Could not load today's games", "Could not load user accounts.": "Could not load user accounts.", "Could not open %1 - ADB unreachable": "Could not open %1 - ADB unreachable", + "Could not prepare private temporary storage": "Could not prepare private temporary storage", + "Could not protect the temporary image": "Could not protect the temporary image", "Could not read phone support on this system.": "Could not read phone support on this system.", "Could not read that region": "Could not read that region", "Could not read the OS catalogue": "Could not read the OS catalogue", @@ -2704,6 +2678,7 @@ "Could not save the drawing": "Could not save the drawing", "Could not save this phone setting.": "Could not save this phone setting.", "Could not send %1 to %2": "Could not send %1 to %2", + "Could not send screenshot": "Could not send screenshot", "Could not start scrcpy on the phone — reconnect it and try again": "Could not start scrcpy on the phone — reconnect it and try again", "Could not start the bluetooth service.": "Could not start the bluetooth service.", "Could not synchronize calendar.": "Could not synchronize calendar.", @@ -2721,6 +2696,7 @@ "Couldn't cut that layer": "Couldn't cut that layer", "Couldn't finish": "Couldn't finish", "Couldn't import this asset. Choose a supported local file under 64 MiB.": "Couldn't import this asset. Choose a supported local file under 64 MiB.", + "Couldn't install bundle (exit %1).": "Couldn't install bundle (exit %1).", "Couldn't measure this picture": "Couldn't measure this picture", "Couldn't preview this asset; shell reload will use the Ryoku wordmark.": "Couldn't preview this asset; shell reload will use the Ryoku wordmark.", "Couldn't prune the clipboard history.": "Couldn't prune the clipboard history.", @@ -2756,11 +2732,11 @@ "Couldn't remove the emblem.": "Couldn't remove the emblem.", "Couldn't remove the fingerprint": "Couldn't remove the fingerprint", "Couldn't remove the imported emblem.": "Couldn't remove the imported emblem.", - "Couldn't repair bundle (exit %1).": "Couldn't repair bundle (exit %1).", "Couldn't save \\\"%1\\\"": "Couldn't save \\\"%1\\\"", "Couldn't save add-on setting (exit %1).": "Couldn't save add-on setting (exit %1).", "Couldn't save placement (exit %1).": "Couldn't save placement (exit %1).", "Couldn't save the Fastfetch settings.": "Couldn't save the Fastfetch settings.", + "Couldn't save the system update choice.": "Couldn't save the system update choice.", "Couldn't save window-manager settings.": "Couldn't save window-manager settings.", "Couldn't switch skin: %1": "Couldn't switch skin: %1", "Couldn't translate. Check the connection or the translator setup.": "Couldn't translate. Check the connection or the translator setup.", @@ -2847,24 +2823,19 @@ "Customize…": "Customize…", "Custom…": "Custom…", "Cut": "Cut", - "Cut a layer from a picture": "Cut a layer from a picture", "Cut a layer from another picture": "Cut a layer from another picture", "Cut angle": "Cut angle", - "Cut blocked · %1": "Cut blocked · %1", "Cut blue light with a warmer screen tint.": "Cut blue light with a warmer screen tint.", "Cut direction": "Cut direction", "Cut from another picture": "Cut from another picture", "Cut from this wallpaper": "Cut from this wallpaper", "Cut out the subject": "Cut out the subject", - "Cut the wallpaper first, then choose which desktop pieces rise above its foreground.": "Cut the wallpaper first, then choose which desktop pieces rise above its foreground.", "Cut the wallpaper into layers and lift widgets between them": "Cut the wallpaper into layers and lift widgets between them", - "Cut the wallpaper's subject out so widgets can sit behind it, and choose how the layers look and move.": "Cut the wallpaper's subject out so widgets can sit behind it, and choose how the layers look and move.", "Cut this area": "Cut this area", "Cut this picture into a new layer?": "Cut this picture into a new layer?", "Cut variance": "Cut variance", "Cutting a new layer": "Cutting a new layer", "Cutting the layer": "Cutting the layer", - "Cutting the wallpaper": "Cutting the wallpaper", "Cyanotype ink and plates": "Cyanotype ink and plates", "Cyber": "Cyber", "Cyberpunk": "Cyberpunk", @@ -2901,7 +2872,6 @@ "DETAILS shows everything the update printed.": "DETAILS shows everything the update printed.", "DETECT": "DETECT", "DEVICE": "DEVICE", - "DEVICE LIGHTING": "DEVICE LIGHTING", "DEVICES": "DEVICES", "DEVICES & SYSTEM": "DEVICES & SYSTEM", "DHCP": "DHCP", @@ -2945,9 +2915,9 @@ "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku materialize": "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku materialize", "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)": "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)", "DRYRUN: %s %s": "DRYRUN: %s %s", + "DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version": "DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version", "DRYRUN: append %s to %s": "DRYRUN: append %s to %s", "DRYRUN: arch-chroot /mnt pacman -Sy --noprogressbar": "DRYRUN: arch-chroot /mnt pacman -Sy --noprogressbar", - "DRYRUN: arch-chroot /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)": "DRYRUN: arch-chroot /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)", "DRYRUN: arch-chroot /mnt systemctl enable limine-snapper-sync.service": "DRYRUN: arch-chroot /mnt systemctl enable limine-snapper-sync.service", "DRYRUN: back up %s": "DRYRUN: back up %s", "DRYRUN: bootstrap yay, install boot-critical (%s) first under per-package timeouts, then the rest of %s best-effort under a global timeout, as %s": "DRYRUN: bootstrap yay, install boot-critical (%s) first under per-package timeouts, then the rest of %s best-effort under a global timeout, as %s", @@ -2977,7 +2947,6 @@ "DRYRUN: seed %s -> ~/%s": "DRYRUN: seed %s -> ~/%s", "DRYRUN: seed XBPS key plists from %s into %s": "DRYRUN: seed XBPS key plists from %s into %s", "DRYRUN: seed keyboard layout %q variant %q options %q from %s into %s": "DRYRUN: seed keyboard layout %q variant %q options %q from %s into %s", - "DRYRUN: select CPU-compatible CachyOS repos, enable [multilib], populate the cachyos keyring in the target": "DRYRUN: select CPU-compatible CachyOS repos, enable [multilib], populate the cachyos keyring in the target", "DRYRUN: set %s as the default browser for http, https and html": "DRYRUN: set %s as the default browser for http, https and html", "DRYRUN: set %s as the default file manager (inode/directory + desktop.apps.files) for %s if installed": "DRYRUN: set %s as the default file manager (inode/directory + desktop.apps.files) for %s if installed", "DRYRUN: set %s as the default web browser (xdg + desktop.apps.browser) for %s if installed": "DRYRUN: set %s as the default web browser (xdg + desktop.apps.browser) for %s if installed", @@ -3124,9 +3093,7 @@ "Depth Anything V2 Small": "Depth Anything V2 Small", "Depth Parallax": "Depth Parallax", "Depth falloff": "Depth falloff", - "Depth keeps the cut-outs still. Switch modes to tune scene motion.": "Depth keeps the cut-outs still. Switch modes to tune scene motion.", "Depth of the field into the desktop": "Depth of the field into the desktop", - "Depth scene": "Depth scene", "Depth shadow": "Depth shadow", "Derive from colour": "Derive from colour", "Describe": "Describe", @@ -3169,7 +3136,6 @@ "Detecting outputs": "Detecting outputs", "Detecting…": "Detecting…", "Dev": "Dev", - "Developer toolchains": "Developer toolchains", "Device": "Device", "Device default": "Device default", "Device effects": "Device effects", @@ -3216,7 +3182,6 @@ "Dimming": "Dimming", "Dims a screen that turns white on you.": "Dims a screen that turns white on you.", "Dims a window that suddenly turns white": "Dims a window that suddenly turns white", - "Dims the effect Ryoku draws.": "Dims the effect Ryoku draws.", "Dims the screen when a mostly white window suddenly fills it.": "Dims the screen when a mostly white window suddenly fills it.", "Direct": "Direct", "Direct-GPU virtual machines": "Direct-GPU virtual machines", @@ -3335,6 +3300,7 @@ "Dot": "Dot", "Dot Ring": "Dot Ring", "Dots": "Dots", + "Double click": "Double click", "Down": "Down", "Download": "Download", "Download %1 (%2)": "Download %1 (%2)", @@ -3348,12 +3314,10 @@ "Download cancelled.": "Download cancelled.", "Download engine": "Download engine", "Download failed": "Download failed", - "Download failed (voxtype exited %1).": "Download failed (voxtype exited %1).", "Download mode": "Download mode", "Download selected model": "Download selected model", "Download the cobalt image": "Download the cobalt image", "Download the model once to build this wallpaper's depth map": "Download the model once to build this wallpaper's depth map", - "Downloaded": "Downloaded", "Downloading": "Downloading", "Downloading %1": "Downloading %1", "Downloading %1 · %2 queued…": "Downloading %1 · %2 queued…", @@ -3363,7 +3327,6 @@ "Downloading…": "Downloading…", "Downloading… %1%": "Downloading… %1%", "Downloads": "Downloads", - "Draft": "Draft", "Drag a band, use the wheel or arrow keys, or double-click it to return that band to zero.": "Drag a band, use the wheel or arrow keys, or double-click it to return that band to zero.", "Drag a box around one thing. Whatever stands out inside it is cut out.": "Drag a box around one thing. Whatever stands out inside it is cut out.", "Drag a dial to set a duration": "Drag a dial to set a duration", @@ -3512,7 +3475,6 @@ "Each pass fills the ring — keep touching until it closes": "Each pass fills the ring — keep touching until it closes", "Each provider automatically shows the quota windows it publishes. Antigravity model pools are separate stops; OpenRouter always shows remaining credits regardless of the percentage setting.": "Each provider automatically shows the quota windows it publishes. Antigravity model pools are separate stops; OpenRouter always shows remaining credits regardless of the percentage setting.", "Each screen's picture and how it sits. While this is open, drag the desktop to move the wallpaper and use the wheel or a pinch to zoom.": "Each screen's picture and how it sits. While this is open, drag the desktop to move the wallpaper and use the wheel or a pinch to zoom.", - "Each tier remembers its model and edge treatment": "Each tier remembers its model and edge treatment", "Each widget keeps its Material design": "Each widget keeps its Material design", "Each widget's distance is set from that widget's own page.": "Each widget's distance is set from that widget's own page.", "Each workspace": "Each workspace", @@ -3798,7 +3760,6 @@ "Esc or click to close": "Esc or click to close", "Esc saves the recording": "Esc saves the recording", "Esc to cancel": "Esc to cancel", - "Esc to close": "Esc to close", "Escalonado paper plates": "Escalonado paper plates", "Escape": "Escape", "Essentials": "Essentials", @@ -3827,7 +3788,6 @@ "Every %1 years": "Every %1 years", "Every Linux mouse remains bindable even when it is not in the catalogue.": "Every Linux mouse remains bindable even when it is not in the catalogue.", "Every area": "Every area", - "Every bar style shares one dock. Choose its design, behaviour and pinned apps in Stage Editor.": "Every bar style shares one dock. Choose its design, behaviour and pinned apps in Stage Editor.", "Every boot re-provisions the ryoku account, factory-fresh.": "Every boot re-provisions the ryoku account, factory-fresh.", "Every built-in is already on the bar.": "Every built-in is already on the bar.", "Every change": "Every change", @@ -3944,7 +3904,7 @@ "FIRST RUN": "FIRST RUN", "FIT": "FIT", "FIX WITH AI": "FIX WITH AI", - "FIXED": "FIXED", + "FLATHUB %1": "FLATHUB %1", "FLEET": "FLEET", "FOCUS": "FOCUS", "FOCUS FLASH": "FOCUS FLASH", @@ -4000,7 +3960,6 @@ "Fast": "Fast", "Fast and compact for most wallpapers": "Fast and compact for most wallpapers", "Fast is used for immediate feedback, Standard for normal movement, and Slow for larger layout changes.": "Fast is used for immediate feedback, Standard for normal movement, and Slow for larger layout changes.", - "Fast lane": "Fast lane", "Fastfetch": "Fastfetch", "Favorites": "Favorites", "Favorites · %1 sites": "Favorites · %1 sites", @@ -4141,6 +4100,7 @@ "Flat frame-off panel with hairline cell separators": "Flat frame-off panel with hairline cell separators", "Flat modules with hairline accent underlines": "Flat modules with hairline accent underlines", "Flat ties travel to the hand; Adaptive speeds quick moves.": "Flat ties travel to the hand; Adaptive speeds quick moves.", + "Flathub application. Ryostore does not review it. Check its source and permissions. New apps install system-wide when available (authorization may be required), otherwise through an existing user remote.": "Flathub application. Ryostore does not review it. Check its source and permissions. New apps install system-wide when available (authorization may be required), otherwise through an existing user remote.", "Flatten": "Flatten", "Fleet": "Fleet", "Fleet overview": "Fleet overview", @@ -4214,7 +4174,6 @@ "Follow wallpaper & theme": "Follow wallpaper & theme", "Follow wallpaper recolours from each applied wallpaper. Fixed palette uses one chosen palette. Keep current colours leaves the theme alone.": "Follow wallpaper recolours from each applied wallpaper. Fixed palette uses one chosen palette. Keep current colours leaves the theme alone.", "Follows every new wallpaper": "Follows every new wallpaper", - "Follows the desktop accent, so the device retints with the wallpaper. Now: %1": "Follows the desktop accent, so the device retints with the wallpaper. Now: %1", "Font": "Font", "Font Size": "Font Size", "Font roundness": "Font roundness", @@ -4416,6 +4375,7 @@ "Gemini": "Gemini", "General": "General", "General limit: %1": "General limit: %1", + "General settings": "General settings", "General wallpaper cut-outs": "General wallpaper cut-outs", "Generate": "Generate", "Generate bug report": "Generate bug report", @@ -4551,7 +4511,6 @@ "Guys": "Guys", "H.264 plays anywhere; HEVC and AV1 are smaller but need a newer GPU.": "H.264 plays anywhere; HEVC and AV1 are smaller but need a newer GPU.", "H:%1 L:%2": "H:%1 L:%2", - "HAND BACK": "HAND BACK", "HARBOUR LOG": "HARBOUR LOG", "HAWK": "HAWK", "HD ×2 (AI upscale)": "HD ×2 (AI upscale)", @@ -4582,7 +4541,6 @@ "Hallucination": "Hallucination", "Hand back": "Hand back", "Hand back every device": "Hand back every device", - "Handing a device to Ryoku lets its provider talk to it. Hardware that keeps profiles in its own memory can be left showing Ryoku's look after you hand it back; Ryoku restores the effect it found, but only the device's own software can rebuild a profile you overwrote with Save to device.": "Handing a device to Ryoku lets its provider talk to it. Hardware that keeps profiles in its own memory can be left showing Ryoku's look after you hand it back; Ryoku restores the effect it found, but only the device's own software can rebuild a profile you overwrote with Save to device.", "Handing it back restores %1.": "Handing it back restores %1.", "Handle": "Handle", "Hanging strips": "Hanging strips", @@ -4800,8 +4758,6 @@ "How far the window hops, in pixels; Slide style only": "How far the window hops, in pixels; Slide style only", "How far the window shrinks; Bounce style only": "How far the window shrinks; Bounce style only", "How far this layer travels in Parallax": "How far this layer travels in Parallax", - "How fast Ryoku moves the effect.": "How fast Ryoku moves the effect.", - "How fast the device runs the effect on its own.": "How fast the device runs the effect on its own.", "How fast the gradient circles the window": "How fast the gradient circles the window", "How fast the shell's own panels, menus and transitions move. Speed scales every Ryoku animation live; Reduce motion snaps them into place for comfort.": "How fast the shell's own panels, menus and transitions move. Speed scales every Ryoku animation live; Reduce motion snaps them into place for comfort.", "How high subtitles sit; 100 is the bottom edge.": "How high subtitles sit; 100 is the bottom edge.", @@ -4882,7 +4838,6 @@ "How wide grains spiral around their ribbon.": "How wide grains spiral around their ribbon.", "How wide the open Island is. The activity page keeps its proportion.": "How wide the open Island is. The activity page keeps its proportion.", "How wide the shoulders are where the Island turns into its edge.": "How wide the shoulders are where the Island turns into its edge.", - "How windows look and behave here. Changes apply at your next login.": "How windows look and behave here. Changes apply at your next login.", "How windows look and behave here. Save applies changes now.": "How windows look and behave here. Save applies changes now.", "How windows take focus as the pointer moves.": "How windows take focus as the pointer moves.", "How your folders line up in the gallery.": "How your folders line up in the gallery.", @@ -4915,6 +4870,7 @@ "IMPORT": "IMPORT", "IMPORT ANOTHER": "IMPORT ANOTHER", "IMPORT ISO": "IMPORT ISO", + "INCLUDE SYSTEM": "INCLUDE SYSTEM", "INCOMING COMMITS": "INCOMING COMMITS", "INFO": "INFO", "INHERIT": "INHERIT", @@ -4925,10 +4881,12 @@ "INSTALL ALL": "INSTALL ALL", "INSTALL ALL /": "INSTALL ALL /", "INSTALL ENGINE": "INSTALL ENGINE", + "INSTALL MISSING": "INSTALL MISSING", "INSTALL SELECTED": "INSTALL SELECTED", "INSTALLED": "INSTALLED", "INSTALLED THROUGH STASH": "INSTALLED THROUGH STASH", "INSTALLING": "INSTALLING", + "INTERFACE": "INTERFACE", "INTERFACE SCALE": "INTERFACE SCALE", "INTERNET ISOLATED": "INTERNET ISOLATED", "INTERNET KILL SWITCH": "INTERNET KILL SWITCH", @@ -5040,6 +4998,7 @@ "Inbox": "Inbox", "Include Wallpaper Engine": "Include Wallpaper Engine", "Include images": "Include images", + "Include system packages": "Include system packages", "Include this category when opening Wallhaven. If none are selected, General is used.": "Include this category when opening Wallhaven. If none are selected, General is used.", "Include this content when opening the source. Wallhaven uses SFW if no content levels are selected.": "Include this content when opening the source. Wallhaven uses SFW if no content levels are selected.", "Include this group": "Include this group", @@ -5104,7 +5063,6 @@ "Install the VirtIO drivers from the attached CD so disk and network work.": "Install the VirtIO drivers from the attached CD so disk and network work.", "Install the Zen palette integration using the recipe directory. The app and its dependencies must already be installed. Zen needs its signed palette extension installed separately and a one-time restart.": "Install the Zen palette integration using the recipe directory. The app and its dependencies must already be installed. Zen needs its signed palette extension installed separately and a one-time restart.", "Install the client package": "Install the client package", - "Install the palette bridge for the supported apps that are present.": "Install the palette bridge for the supported apps that are present.", "Install yt-dlp": "Install yt-dlp", "Installation canceled. Your selected model is unchanged.": "Installation canceled. Your selected model is unchanged.", "Installation failed": "Installation failed", @@ -5132,7 +5090,6 @@ "Installing…": "Installing…", "Installs Steam and CachyOS's gaming packages:": "Installs Steam and CachyOS's gaming packages:", "Installs Steam with 32-bit graphics support.": "Installs Steam with 32-bit graphics support.", - "Installs the -luks host variant.": "Installs the -luks host variant.", "Installs the Hyprland Ryoku desktop variant.": "Installs the Hyprland Ryoku desktop variant.", "Installs the experimental Wayfire Ryoku desktop variant.": "Installs the experimental Wayfire Ryoku desktop variant.", "Installs the niri Ryoku desktop variant.": "Installs the niri Ryoku desktop variant.", @@ -5193,6 +5150,7 @@ "JWT decoder": "JWT decoder", "Japan": "Japan", "Japanese Typography": "Japanese Typography", + "Japanese glosses": "Japanese glosses", "Japanese with subtitles, or dubbed where a dub exists. ani-cli only brings English subtitles and dubs.": "Japanese with subtitles, or dubbed where a dub exists. ani-cli only brings English subtitles and dubs.", "JaviBOT96": "JaviBOT96", "Jazz": "Jazz", @@ -5211,6 +5169,8 @@ "KATANA": "KATANA", "KB/s": "KB/s", "KDE Bluetooth": "KDE Bluetooth", + "KDE Connect could not reach %1": "KDE Connect could not reach %1", + "KDE Connect is not installed": "KDE Connect is not installed", "KDE Connect is not installed — phone integration requires it": "KDE Connect is not installed — phone integration requires it", "KDE Connect is not running": "KDE Connect is not running", "KDE Connect is not running on this computer. Try turning phone support on again.": "KDE Connect is not running on this computer. Try turning phone support on again.", @@ -5308,10 +5268,8 @@ "Keyboard layout and locks": "Keyboard layout and locks", "Keyboard layout · %1": "Keyboard layout · %1", "Keyboard mode": "Keyboard mode", - "Keyboard shortcuts": "Keyboard shortcuts", "Keyboard, pointer and touchpad, and key repeat.": "Keyboard, pointer and touchpad, and key repeat.", "Keyboard, while drawing": "Keyboard, while drawing", - "Keyboards, mice and other RGB hardware, through OpenRGB and native laptop providers. While this is off Ryoku never looks for a device and never writes to one, so hardware driven by its own software or a switch on the board is left alone. Turn it on and hand over the devices you want Ryoku to light, one at a time.": "Keyboards, mice and other RGB hardware, through OpenRGB and native laptop providers. While this is off Ryoku never looks for a device and never writes to one, so hardware driven by its own software or a switch on the board is left alone. Turn it on and hand over the devices you want Ryoku to light, one at a time.", "Keycap style": "Keycap style", "Keyframe interval": "Keyframe interval", "Keyring": "Keyring", @@ -5382,7 +5340,6 @@ "Language": "Language", "Language & formats. Sets i18n.defaultLocale.": "Language & formats. Sets i18n.defaultLocale.", "Language pack unavailable; using English.": "Language pack unavailable; using English.", - "Language, regional formats, location and the system font.": "Language, regional formats, location and the system font.", "Language, regional formats, people, fonts, icons, window blur and sounds.": "Language, regional formats, people, fonts, icons, window blur and sounds.", "Lap": "Lap", "Lap %1": "Lap %1", @@ -5422,7 +5379,6 @@ "Launcher mode": "Launcher mode", "Launcher search": "Launcher search", "Launcher, Kit, lock, clipboard, screenshot, wallpaper and more": "Launcher, Kit, lock, clipboard, screenshot, wallpaper and more", - "Launcher, cheatsheet, lock, clipboard, screenshot, wallpaper and more": "Launcher, cheatsheet, lock, clipboard, screenshot, wallpaper and more", "Launching %1…": "Launching %1…", "Launching scrcpy…": "Launching scrcpy…", "Layer %1": "Layer %1", @@ -5466,7 +5422,6 @@ "Left": "Left", "Left Complication": "Left Complication", "Left and right": "Left and right", - "Left off, Ryoku never writes to it: its own software, onboard profile or hardware switch stays in charge.": "Left off, Ryoku never writes to it: its own software, onboard profile or hardware switch stays in charge.", "Left, right and wheel only; nothing here to rebind.": "Left, right and wheel only; nothing here to rebind.", "Left-handed": "Left-handed", "Left-handed buttons": "Left-handed buttons", @@ -5854,12 +5809,10 @@ "Maximum resolution": "Maximum resolution", "Maximum results": "Maximum results", "Maximum size": "Maximum size", - "May need more CPU, GPU and memory.": "May need more CPU, GPU and memory.", "Measuring the wallpaper…": "Measuring the wallpaper…", "Measuring what the history holds…": "Measuring what the history holds…", "Mechanical": "Mechanical", "Media": "Media", - "Media & tools": "Media & tools", "Media Controls": "Media Controls", "Media Settings": "Media Settings", "Media Widget Options": "Media Widget Options", @@ -5983,8 +5936,6 @@ "Mode": "Mode", "Model": "Model", "Model %1": "Model %1", - "Model downloaded. If dictation doesn't respond, a reboot may be needed to apply it.": "Model downloaded. If dictation doesn't respond, a reboot may be needed to apply it.", - "Model for %1": "Model for %1", "Model installed.": "Model installed.", "Model manifest": "Model manifest", "Model ready": "Model ready", @@ -6038,7 +5989,6 @@ "Motion for open, close, move, workspaces; off snaps": "Motion for open, close, move, workspaces; off snaps", "Motion in the gaps between widgets": "Motion in the gaps between widgets", "Motion is off": "Motion is off", - "Motion needs Parallax": "Motion needs Parallax", "Motion speed": "Motion speed", "Motion weights": "Motion weights", "Motion while audio is quiet": "Motion while audio is quiet", @@ -6255,6 +6205,7 @@ "New VPN…": "New VPN…", "New WireGuard…": "New WireGuard…", "New alarm": "New alarm", + "New application": "New application", "New category": "New category", "New chat": "New chat", "New cuts wait until it finishes or is stopped.": "New cuts wait until it finishes or is stopped.", @@ -6440,7 +6391,6 @@ "No image": "No image", "No images match": "No images match", "No images or folders here": "No images or folders here", - "No in-front layers yet": "No in-front layers yet", "No input device": "No input device", "No installed font matches": "No installed font matches", "No internet connection": "No internet connection", @@ -6448,7 +6398,6 @@ "No keybind conflicts. Your binds layer in cleanly.": "No keybind conflicts. Your binds layer in cleanly.", "No laps recorded yet": "No laps recorded yet", "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets plus SMBus RAM and motherboard lighting; Ryoku already loads i2c-dev. Supported ASUS laptop keyboards use asusd, and QMK/VIA boards use qmk_hid. Reconnect the device, then choose Detect devices in RGB lighting.": "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets plus SMBus RAM and motherboard lighting; Ryoku already loads i2c-dev. Supported ASUS laptop keyboards use asusd, and QMK/VIA boards use qmk_hid. Reconnect the device, then choose Detect devices in RGB lighting.", - "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets; supported ASUS laptops use asusd for their built-in Aura controller. Motherboard and memory lighting still needs the i2c-dev module loaded, which Ryoku leaves to you.": "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets; supported ASUS laptops use asusd for their built-in Aura controller. Motherboard and memory lighting still needs the i2c-dev module loaded, which Ryoku leaves to you.", "No limit": "No limit", "No lock skins installed yet. Get some from Ryoku Store.": "No lock skins installed yet. Get some from Ryoku Store.", "No locked widgets on this display": "No locked widgets on this display", @@ -6542,7 +6491,6 @@ "No supported apps detected. Use Find apps after installing one.": "No supported apps detected. Use Find apps after installing one.", "No supported browser profile was found.": "No supported browser profile was found.", "No supported images were dropped": "No supported images were dropped", - "No supported lighting provider is installed, so no device can be reached.": "No supported lighting provider is installed, so no device can be reached.", "No supported media or folders here": "No supported media or folders here", "No synced lyrics": "No synced lyrics", "No synced lyrics for this track": "No synced lyrics for this track", @@ -6615,7 +6563,6 @@ "Not generated yet": "Not generated yet", "Not important": "Not important", "Not installed": "Not installed", - "Not installed · install ryoku-rashin": "Not installed · install ryoku-rashin", "Not installed · switch on to install it, with its Ask shortcut": "Not installed · switch on to install it, with its Ask shortcut", "Not mounted": "Not mounted", "Not on the desktop": "Not on the desktop", @@ -6898,7 +6845,6 @@ "One click adopts the device, picks a colour-capable mode and starts following the palette. Turning it off hands the device back.": "One click adopts the device, picks a colour-capable mode and starts following the palette. Turning it off hands the device back.", "One continuous plate": "One continuous plate", "One finger clicks left, two right, three middle.": "One finger clicks left, two right, three middle.", - "One font for the shell, apps and terminal.": "One font for the shell, apps and terminal.", "One look on every widget. Shima draws its own faces, Material keeps each widget's own style, iNstrument reads like a set of gauges and Readout gives figures room.": "One look on every widget. Shima draws its own faces, Material keeps each widget's own style, iNstrument reads like a set of gauges and Readout gives figures room.", "One picture runs from screen to screen": "One picture runs from screen to screen", "One shape for all": "One shape for all", @@ -6961,6 +6907,7 @@ "Open containing folder": "Open containing folder", "Open controls and session actions.": "Open controls and session actions.", "Open corners": "Open corners", + "Open desktop icons": "Open desktop icons", "Open dock settings in Stage Editor": "Open dock settings in Stage Editor", "Open edge": "Open edge", "Open equalizer": "Open equalizer", @@ -7179,7 +7126,6 @@ "PM": "PM", "PM10": "PM10", "PM2.5": "PM2.5", - "PNG images (*.png)": "PNG images (*.png)", "POINT ANY AGENT": "POINT ANY AGENT", "POINTER": "POINTER", "POINTER THEME": "POINTER THEME", @@ -7229,7 +7175,6 @@ "Page titles and card heads.": "Page titles and card heads.", "Page width": "Page width", "Pages": "Pages", - "Painted by Ryoku on this device's per-key mode, so they work whatever its firmware supports. They need Ryoku running.": "Painted by Ryoku on this device's per-key mode, so they work whatever its firmware supports. They need Ryoku running.", "Pair": "Pair", "Paired": "Paired", "Paired Devices": "Paired Devices", @@ -7251,7 +7196,6 @@ "Palette name": "Palette name", "Palette saved and applied.": "Palette saved and applied.", "Palette saved.": "Palette saved.", - "Palette source": "Palette source", "Palette used when auto-recolouring new wallpapers.": "Palette used when auto-recolouring new wallpapers.", "Palette used when auto-theming the backdrop.": "Palette used when auto-theming the backdrop.", "Pane rounding": "Pane rounding", @@ -7269,7 +7213,6 @@ "Panels hug their sections and grow up to this.": "Panels hug their sections and grow up to this.", "Paper islands": "Paper islands", "Parallax": "Parallax", - "Parallax scene": "Parallax scene", "Parks widgets while every monitor is covered": "Parks widgets while every monitor is covered", "Parks widgets while every monitor is covered.": "Parks widgets while every monitor is covered.", "Parse error:": "Parse error:", @@ -7399,6 +7342,7 @@ "Phone tools could not be removed.": "Phone tools could not be removed.", "Phone tools installed, but Phone could not be enabled.": "Phone tools installed, but Phone could not be enabled.", "Phone tools were removed": "Phone tools were removed", + "Phone transfer is unavailable": "Phone transfer is unavailable", "Phone's next alarm %1": "Phone's next alarm %1", "Photo": "Photo", "Photo 1x1": "Photo 1x1", @@ -7462,7 +7406,6 @@ "Pickup reminder": "Pickup reminder", "Picture": "Picture", "Pictures": "Pictures", - "Pictures (*.png *.jpg *.jpeg *.webp *.bmp)": "Pictures (*.png *.jpg *.jpeg *.webp *.bmp)", "Pieces": "Pieces", "Pieces you carry and the cards they grow": "Pieces you carry and the cards they grow", "Pill": "Pill", @@ -7503,7 +7446,6 @@ "Place a widget or visualizer, then return here to choose its depth.": "Place a widget or visualizer, then return here to choose its depth.", "Place apps on the desktop or in a folder": "Place apps on the desktop or in a folder", "Place the cut-out behind or over the desktop": "Place the cut-out behind or over the desktop", - "Place the widget around the wallpaper subject": "Place the widget around the wallpaper subject", "Placed apart from the desktop; drag it on the Lockscreen tab": "Placed apart from the desktop; drag it on the Lockscreen tab", "Placed apps can be removed from any location below": "Placed apps can be removed from any location below", "Placed desktop item": "Placed desktop item", @@ -7512,8 +7454,6 @@ "Plain": "Plain", "Plain header": "Plain header", "Plain rows sit on the Island's black like the rest of the page; Grouped puts the forecast and vitals strips on quiet plates.": "Plain rows sit on the Island's black like the rest of the page; Grouped puts the forecast and vitals strips on quiet plates.", - "Plain stays flat; Depth cuts layers; Parallax adds motion": "Plain stays flat; Depth cuts layers; Parallax adds motion", - "Plain wallpaper": "Plain wallpaper", "Plan Usage reads local client credentials only in memory and caches quota snapshots without tokens. Enabled cloud providers use their read-only usage endpoints; Antigravity reuses its local server.": "Plan Usage reads local client credentials only in memory and caches quota snapshots without tokens. Enabled cloud providers use their read-only usage endpoints; Antigravity reuses its local server.", "Plane": "Plane", "Plate": "Plate", @@ -7569,7 +7509,6 @@ "Plugged in and on battery.": "Plugged in and on battery.", "Plugin": "Plugin", "Plugin tile keyed by %1": "Plugin tile keyed by %1", - "Plugin tile · keyed by %1": "Plugin tile · keyed by %1", "Plugin widgets": "Plugin widgets", "Plugins": "Plugins", "Plugins cannot be built on this machine. Missing build tools: %1.": "Plugins cannot be built on this machine. Missing build tools: %1.", @@ -7626,7 +7565,6 @@ "Power state unavailable.": "Power state unavailable.", "Powerline": "Powerline", "Powers the adapter and remembers the choice across restarts": "Powers the adapter and remembers the choice across restarts", - "Powers the adapter, unblocking it if rfkill holds it": "Powers the adapter, unblocking it if rfkill holds it", "Precip": "Precip", "Prefixes: ; clipboard, = calculator, / actions.": "Prefixes: ; clipboard, = calculator, / actions.", "Prepare the archived homes folder: %v": "Prepare the archived homes folder: %v", @@ -7700,7 +7638,6 @@ "Primary ink for this instance": "Primary ink for this instance", "Primary monitor": "Primary monitor", "Primary only": "Primary only", - "Primary user": "Primary user", "Printed-page surfaces": "Printed-page surfaces", "Printer": "Printer", "Prints": "Prints", @@ -7824,7 +7761,6 @@ "RASHIN // ASK": "RASHIN // ASK", "RASHIN // TOOLS": "RASHIN // TOOLS", "RASHIN // WEB": "RASHIN // WEB", - "RASHIN SERVICE": "RASHIN SERVICE", "RASHIN · RECENT ASKS": "RASHIN · RECENT ASKS", "RATIO / SIZE": "RATIO / SIZE", "RAW": "RAW", @@ -7871,7 +7807,6 @@ "REPLY": "REPLY", "REQUEST SENT": "REQUEST SENT", "REQUIRED": "REQUIRED", - "RESCAN DEVICES": "RESCAN DEVICES", "RESET": "RESET", "RESET APPS": "RESET APPS", "RESET LAYOUT": "RESET LAYOUT", @@ -7960,7 +7895,6 @@ "Range": "Range", "Rank %1, %2": "Rank %1, %2", "Rashin": "Rashin", - "Rashin could not change its service state.": "Rashin could not change its service state.", "Rashin could not change its state.": "Rashin could not change its state.", "Rashin dashboard": "Rashin dashboard", "Rashin is Ryoku's AI agent: Ask on Alt+Space,": "Rashin is Ryoku's AI agent: Ask on Alt+Space,", @@ -7970,7 +7904,6 @@ "Raw condition": "Raw condition", "Raw xkb options, comma separated, for power users.": "Raw xkb options, comma separated, for power users.", "Re-apply the last wallpaper when the daemon starts.": "Re-apply the last wallpaper when the daemon starts.", - "Re-cut": "Re-cut", "Re-derive per-wallpaper colour bucket and saturation from existing thumbnails.": "Re-derive per-wallpaper colour bucket and saturation from existing thumbnails.", "Re-detect": "Re-detect", "Re-encode each video once at the size and frame rate below, then play the lighter copy.": "Re-encode each video once at the size and frame rate below, then play the lighter copy.", @@ -8125,6 +8058,7 @@ "Reload": "Reload", "Reload cover": "Reload cover", "Reload custom widgets": "Reload custom widgets", + "Reload desktop": "Reload desktop", "Reload shell": "Reload shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Reloading the desktop", @@ -8186,7 +8120,6 @@ "Remove pin": "Remove pin", "Remove pins or move them into place": "Remove pins or move them into place", "Remove selected items": "Remove selected items", - "Remove the palette bridge from every app.": "Remove the palette bridge from every app.", "Remove this command.": "Remove this command.", "Remove this reference from the model list. The model files and built index are kept.": "Remove this reference from the model list. The model files and built index are kept.", "Remove this resolution preset.": "Remove this resolution preset.", @@ -8236,6 +8169,7 @@ "Require the authenticator PIN when the key supports it": "Require the authenticator PIN when the key supports it", "Require the key's built-in user verification when available": "Require the key's built-in user verification when available", "Require touching the key during authentication": "Require touching the key during authentication", + "Required components complete": "Required components complete", "Required for USB connection, quick actions (screenshot, power button) and opening apps from notifications.": "Required for USB connection, quick actions (screenshot, power button) and opening apps from notifications.", "Required rows back a desktop feature and cannot be removed.": "Required rows back a desktop feature and cannot be removed.", "Rescan devices": "Rescan devices", @@ -8253,7 +8187,6 @@ "Reset island settings": "Reset island settings", "Reset layout": "Reset layout", "Reset look": "Reset look", - "Reset look and motion": "Reset look and motion", "Reset motion": "Reset motion", "Reset position, zoom and orientation": "Reset position, zoom and orientation", "Reset size": "Reset size", @@ -8293,17 +8226,14 @@ "Rest on the Island to peek, click to keep it, scroll for volume.": "Rest on the Island to peek, click to keep it, scroll for volume.", "Rest when paused": "Rest when paused", "Restart": "Restart", - "Restart apps": "Restart apps", "Restart bridge service": "Restart bridge service", "Restart shell": "Restart shell", - "Restart the bridged apps so they pick up the current colours.": "Restart the bridged apps so they pick up the current colours.", "Restart the palette service. Does not restart Spotify, Zen, or Vesktop.": "Restart the palette service. Does not restart Spotify, Zen, or Vesktop.", "Resting": "Resting", "Resting Island": "Resting Island", "Restore": "Restore", "Restore camera motion and widget lighting defaults": "Restore camera motion and widget lighting defaults", "Restore defaults": "Restore defaults", - "Restore edge, shadow and Parallax motion defaults": "Restore edge, shadow and Parallax motion defaults", "Restore edge, shadow and light direction defaults": "Restore edge, shadow and light direction defaults", "Restore every Parallax motion setting": "Restore every Parallax motion setting", "Restore every binding to its built-in key or click.": "Restore every binding to its built-in key or click.", @@ -8325,6 +8255,7 @@ "Results / page %1": "Results / page %1", "Results for “%1”": "Results for “%1”", "Results from every category, with installed state kept in view.": "Results from every category, with installed state kept in view.", + "Results from the whole of Flathub, with installed state kept in view.": "Results from the whole of Flathub, with installed state kept in view.", "Resume": "Resume", "Resume %1": "Resume %1", "Resume %1 episode %2": "Resume %1 episode %2", @@ -8446,7 +8377,6 @@ "Rules stay saved, but none of them will run.": "Rules stay saved, but none of them will run.", "Run": "Run", "Run by the device itself, so they keep going when Ryoku closes.": "Run by the device itself, so they keep going when Ryoku closes.", - "Run by the device itself, so they keep going with Ryoku closed. An effect that does nothing is its firmware; a Ryoku effect above will work instead.": "Run by the device itself, so they keep going with Ryoku closed. An effect that does nothing is its firmware; a Ryoku effect above will work instead.", "Run command": "Run command", "Run custom output": "Run custom output", "Run every time": "Run every time", @@ -8463,7 +8393,6 @@ "Running indicator": "Running indicator", "Running ryoku doctor": "Running ryoku doctor", "Running · %1": "Running · %1", - "Running · 127.0.0.1:%1": "Running · 127.0.0.1:%1", "Running · 127.0.0.1:%1 · off removes it and its Ask shortcut": "Running · 127.0.0.1:%1 · off removes it and its Ask shortcut", "Runs %1 after the step above; the rest of the list waits with it": "Runs %1 after the step above; the rest of the list waits with it", "Runs %1this routine again. Pick another target.": "Runs %1this routine again. Pick another target.", @@ -8501,12 +8430,10 @@ "Ryoku does not manage this path.": "Ryoku does not manage this path.", "Ryoku does not publish packages for Fedora %s yet. Supported releases could not be read from %s: %v": "Ryoku does not publish packages for Fedora %s yet. Supported releases could not be read from %s: %v", "Ryoku does not publish packages for Fedora %s yet. Supported: %s.": "Ryoku does not publish packages for Fedora %s yet. Supported: %s.", - "Ryoku effects": "Ryoku effects", "Ryoku greeter theme selected past KDE's kde_settings.conf drop-in": "Ryoku greeter theme selected past KDE's kde_settings.conf drop-in", "Ryoku has not published Fedora %s yet; Ryoku packages stay at their current version until it does": "Ryoku has not published Fedora %s yet; Ryoku packages stay at their current version until it does", "Ryoku in a single pass. Nothing is written until you": "Ryoku in a single pass. Nothing is written until you", "Ryoku in-shell": "Ryoku in-shell", - "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s": "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s", "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s files=%s rashin=%s hidden=%s cachyos-layer=%s": "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s files=%s rashin=%s hidden=%s cachyos-layer=%s", "Ryoku installed": "Ryoku installed", "Ryoku installer": "Ryoku installer", @@ -8529,12 +8456,10 @@ "Ryoku shell": "Ryoku shell", "Ryoku shell installer": "Ryoku shell installer", "Ryoku ships %d browsers on this edition; pick the one you want, or none.": "Ryoku ships %d browsers on this edition; pick the one you want, or none.", - "Ryoku ships %d browsers on this edition; pick the one you want.": "Ryoku ships %d browsers on this edition; pick the one you want.", "Ryoku ships this and overwrites it on every update. Fork it into the overlay at %s to keep a change; `ryoku reset %s` undoes the fork.": "Ryoku ships this and overwrites it on every update. Fork it into the overlay at %s to keep a change; `ryoku reset %s` undoes the fork.", "Ryoku ships this and overwrites it on every update. Put your change in %s instead; Ryoku reads it but never overwrites it.": "Ryoku ships this and overwrites it on every update. Put your change in %s instead; Ryoku reads it but never overwrites it.", "Ryoku ships this and overwrites it on every update. The shell tree cannot be forked (a fork breaks the shell); change it through ryoku-shell, Ryoku Hub, or a plugin (`ryoku plugin`).": "Ryoku ships this and overwrites it on every update. The shell tree cannot be forked (a fork breaks the shell); change it through ryoku-shell, Ryoku Hub, or a plugin (`ryoku plugin`).", "Ryoku ships three browsers; pick the one you want, or none.": "Ryoku ships three browsers; pick the one you want, or none.", - "Ryoku ships three browsers; pick the one you want.": "Ryoku ships three browsers; pick the one you want.", "Ryoku ships x86_64 builds only": "Ryoku ships x86_64 builds only", "Ryoku ships your desktop as packages, and every `ryoku update` re-lays the": "Ryoku ships your desktop as packages, and every `ryoku update` re-lays the", "Ryoku style wraps this widget; it keeps its own colours": "Ryoku style wraps this widget; it keeps its own colours", @@ -8559,7 +8484,6 @@ "SAVE AS TEMPLATE": "SAVE AS TEMPLATE", "SAVE LIMIT": "SAVE LIMIT", "SAVE PASSWORD": "SAVE PASSWORD", - "SAVE TO DEVICE": "SAVE TO DEVICE", "SAVE USER": "SAVE USER", "SAVED BERTHS": "SAVED BERTHS", "SAVED LOOKS": "SAVED LOOKS", @@ -8570,8 +8494,6 @@ "SCALE": "SCALE", "SCAN %1": "SCAN %1", "SCAN DENIED ✦ TYPE YOUR KEY": "SCAN DENIED ✦ TYPE YOUR KEY", - "SCANNING": "SCANNING", - "SCENE": "SCENE", "SCREEN MIRRORING": "SCREEN MIRRORING", "SCREEN PICKER": "SCREEN PICKER", "SCREENSHOT": "SCREENSHOT", @@ -8781,7 +8703,6 @@ "Scale the entire picker interface. The range is 0.5 to 2.0.": "Scale the entire picker interface. The range is 0.5 to 2.0.", "Scale this widget without changing its anchor": "Scale this widget without changing its anchor", "Scale videos to the display": "Scale videos to the display", - "Scaled into the steps this device offers, not a number it has to fake.": "Scaled into the steps this device offers, not a number it has to fake.", "Scales the clock island, resting and expanded.": "Scales the clock island, resting and expanded.", "Scales the shell surfaces and popouts together.": "Scales the shell surfaces and popouts together.", "Scallop Dot Clock": "Scallop Dot Clock", @@ -8903,7 +8824,6 @@ "Search by tag, or describe it": "Search by tag, or describe it", "Search colours": "Search colours", "Search every area: blur, corners, dock…": "Search every area: blur, corners, dock…", - "Search every shortcut": "Search every shortcut", "Search failed": "Search failed", "Search failed. Check your connection.": "Search failed. Check your connection.", "Search in %1, or type a path": "Search in %1, or type a path", @@ -8936,6 +8856,7 @@ "Search, network and data": "Search, network and data", "Searching": "Searching", "Searching %1…": "Searching %1…", + "Searching Flathub.": "Searching Flathub.", "Searching for nearby networks…": "Searching for nearby networks…", "Searching networks…": "Searching networks…", "Searching…": "Searching…", @@ -8986,7 +8907,6 @@ "Security key / Passkey": "Security key / Passkey", "Security key behavior": "Security key behavior", "Security key or password": "Security key or password", - "See every shortcut.": "See every shortcut.", "See the targets that pause wallpapers and add a running process.": "See the targets that pause wallpapers and add a running process.", "See what each display is showing, change its placement, or protect it from updates.": "See what each display is showing, change its placement, or protect it from updates.", "See where your day goes": "See where your day goes", @@ -9015,7 +8935,6 @@ "Selection frame": "Selection frame", "Selection, focus, and the main action colour.": "Selection, focus, and the main action colour.", "Selection, keyboard focus, and the main action.": "Selection, keyboard focus, and the main action.", - "Selects Main, Hero, or OkShell for Super+Space": "Selects Main, Hero, or OkShell for Super+Space", "Semantic models": "Semantic models", "Semantic search model": "Semantic search model", "Semi Bold": "Semi Bold", @@ -9036,9 +8955,12 @@ "Send text as keyboard input": "Send text as keyboard input", "Send the first reminder to plug in.": "Send the first reminder to plug in.", "Send the urgent reminder to plug in.": "Send the urgent reminder to plug in.", + "Send to %1": "Send to %1", + "Send to phone": "Send to phone", "Send traffic through %1": "Send traffic through %1", "Sending file...": "Sending file...", "Sending notification…": "Sending notification…", + "Sending screenshot": "Sending screenshot", "Sending…": "Sending…", "Sensitivity": "Sensitivity", "Sensor disconnected": "Sensor disconnected", @@ -9047,6 +8969,8 @@ "Sent": "Sent", "Sent %1 files": "Sent %1 files", "Sent 1 file": "Sent 1 file", + "Sent to %1": "Sent to %1", + "Sent via KDE Connect": "Sent via KDE Connect", "Separate /home": "Separate /home", "Separate filters with spaces. Every filter must match. Names and paths cannot contain spaces.": "Separate filters with spaces. Every filter must match. Names and paths cannot contain spaces.", "Separate folder for video wallpapers. Defaults to the wallpaper directory.": "Separate folder for video wallpapers. Defaults to the wallpaper directory.", @@ -9073,8 +8997,6 @@ "Set %1 exactly": "Set %1 exactly", "Set %1 minutes": "Set %1 minutes", "Set a LUKS passphrase:": "Set a LUKS passphrase:", - "Set a password for your account.": "Set a password for your account.", - "Set a password:": "Set a password:", "Set a separate root password": "Set a separate root password", "Set a time": "Set a time", "Set as Live Preview": "Set as Live Preview", @@ -9183,7 +9105,7 @@ "Shelf · 1 item · drag off the dock to use it": "Shelf · 1 item · drag off the dock to use it", "Shelf · drop files here": "Shelf · drop files here", "Shell": "Shell", - "Shell add-ons and bundles you've installed.": "Shell add-ons and bundles you've installed.", + "Shell add-ons and bundles with parts already on this computer.": "Shell add-ons and bundles with parts already on this computer.", "Shell and bar": "Shell and bar", "Shell command": "Shell command", "Shell corner radius": "Shell corner radius", @@ -9204,6 +9126,7 @@ "Shima": "Shima", "Shima Equalizer": "Shima Equalizer", "Shima Settings": "Shima Settings", + "Shima Studio": "Shima Studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shima design": "Shima design", "Shima is a modified version of iNiR's island design, rebuilt by Ryoku. It runs through Ryoku's window-manager layer, so it works on every compositor Ryoku supports.": "Shima is a modified version of iNiR's island design, rebuilt by Ryoku. It runs through Ryoku's window-manager layer, so it works on every compositor Ryoku supports.", @@ -9226,7 +9149,6 @@ "Show 'TIME' Top Header": "Show 'TIME' Top Header", "Show 24h Progress Arc": "Show 24h Progress Arc", "Show AM/PM Chip (12-Hour Mode)": "Show AM/PM Chip (12-Hour Mode)", - "Show Apply button": "Show Apply button", "Show Audio Device Pill": "Show Audio Device Pill", "Show Battery Percentage": "Show Battery Percentage", "Show Battery Percentages": "Show Battery Percentages", @@ -9258,6 +9180,7 @@ "Show Outer Numbers (00-58)": "Show Outer Numbers (00-58)", "Show Pexels in the download source picker.": "Show Pexels in the download source picker.", "Show Previous Button": "Show Previous Button", + "Show Ryoku's Japanese glosses, decorative text and mark across the desktop. Off keeps English and Latin marks; CJK fonts, language support and your content are unaffected.": "Show Ryoku's Japanese glosses, decorative text and mark across the desktop. Off keeps English and Latin marks; CJK fonts, language support and your content are unaffected.", "Show Second Hand": "Show Second Hand", "Show Sunset Gauge": "Show Sunset Gauge", "Show Sunset Sub-Dial (Top)": "Show Sunset Sub-Dial (Top)", @@ -9270,7 +9193,6 @@ "Show active workspace indicator": "Show active workspace indicator", "Show add event button": "Show add event button", "Show addresses": "Show addresses", - "Show an Apply button for this source. Change several filters, then press Apply or Enter to send one search request. When off, each filter change searches immediately.": "Show an Apply button for this source. Change several filters, then press Apply or Enter to send one search request. When off, each filter change searches immediately.", "Show answer": "Show answer", "Show app dividers": "Show app dividers", "Show app drawer button": "Show app drawer button", @@ -9422,6 +9344,7 @@ "Since": "Since", "Since %1": "Since %1", "Sine": "Sine", + "Single click": "Single click", "Single file": "Single file", "Six": "Six", "Sixteen": "Sixteen", @@ -9502,7 +9425,6 @@ "Snapshot name": "Snapshot name", "Snapshots": "Snapshots", "Snapshots & rollback": "Snapshots & rollback", - "Snapshots are not available on Fedora; recover the system by hand.": "Snapshots are not available on Fedora; recover the system by hand.", "Snippet": "Snippet", "Snooze %1 min": "Snooze %1 min", "Snooze length": "Snooze length", @@ -9621,7 +9543,6 @@ "Stacks gather loose files automatically.": "Stacks gather loose files automatically.", "Stacks gather loose folders automatically.": "Stacks gather loose folders automatically.", "Stage Editor": "Stage Editor", - "Stage Editor (GPL-3.0)": "Stage Editor (GPL-3.0)", "Stage Editor, ii dock, Kit, Phone (GPL-3.0)": "Stage Editor, ii dock, Kit, Phone (GPL-3.0)", "Stage and dock settings use their current stores": "Stage and dock settings use their current stores", "Stage captured — lift and touch again": "Stage captured — lift and touch again", @@ -9750,7 +9671,6 @@ "Storage limits must be whole bytes, or 0 for no limit.": "Storage limits must be whole bytes, or 0 for no limit.", "Storage path": "Storage path", "Store aggregate scores locally, never typed text.": "Store aggregate scores locally, never typed text.", - "Store this look in the device so it holds with Ryoku closed. It replaces what is in the device's active profile slot.": "Store this look in the device so it holds with Ryoku closed. It replaces what is in the device's active profile slot.", "Stormy": "Stormy", "Story": "Story", "Straight": "Straight", @@ -9892,7 +9812,6 @@ "Syncing...": "Syncing...", "Syncing…": "Syncing…", "System": "System", - "System (required)": "System (required)", "System / Uptime": "System / Uptime", "System Monitor": "System Monitor", "System Uptime": "System Uptime", @@ -9909,7 +9828,6 @@ "System font": "System font", "System locale": "System locale", "System power": "System power", - "System services": "System services", "System setting": "System setting", "System sound": "System sound", "System tray icons.": "System tray icons.", @@ -9976,7 +9894,6 @@ "Take screenshot": "Take screenshot", "Take this widget off the desktop": "Take this widget off the desktop", "Taking a snapshot": "Taking a snapshot", - "Talk to Hermes in the dashboard or a terminal.": "Talk to Hermes in the dashboard or a terminal.", "Talk to your agent in the dashboard or a terminal.": "Talk to your agent in the dashboard or a terminal.", "Tall": "Tall", "Tall canvas": "Tall canvas", @@ -10179,7 +10096,6 @@ "The colour theme's own": "The colour theme's own", "The colours come from one picture. Choosing another screen makes its picture the shared one; nothing on screen changes.": "The colours come from one picture. Choosing another screen makes its picture the shared one; nothing on screen changes.", "The colours come from this screen's picture": "The colours come from this screen's picture", - "The colours file Zen, Spotify and Vesktop read their theme from. Leave empty to use Ryoku's palette.": "The colours file Zen, Spotify and Vesktop read their theme from. Leave empty to use Ryoku's palette.", "The colours follow the video wallpaper. Give it a picture to take them from another screen.": "The colours follow the video wallpaper. Give it a picture to take them from another screen.", "The command could not be saved.": "The command could not be saved.", "The command file could not be read (%1)": "The command file could not be read (%1)", @@ -10289,7 +10205,6 @@ "The now-playing bubble that grows beside the island.": "The now-playing bubble that grows beside the island.", "The official localsend-cli binary was not found. Install it from Settings → Devices & Phone → LocalSend.": "The official localsend-cli binary was not found. Install it from Settings → Devices & Phone → LocalSend.", "The on-screen keyboard.": "The on-screen keyboard.", - "The optional local agent OS. A resident Hermes agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.": "The optional local agent OS. A resident Hermes agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.", "The optional local agent OS. Your agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.": "The optional local agent OS. Your agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.", "The output and input list.": "The output and input list.", "The output level; its glyph mutes.": "The output level; its glyph mutes.", @@ -10325,7 +10240,8 @@ "The previous config is still in use": "The previous config is still in use", "The profile of every bubble, bar and satellite. Squircles and squares read like app tiles.": "The profile of every bubble, bar and satellite. Squircles and squares read like app tiles.", "The proportional face used by the shell and apps.": "The proportional face used by the shell and apps.", - "The radio is off at the hardware level (rfkill); the toggle unblocks it.": "The radio is off at the hardware level (rfkill); the toggle unblocks it.", + "The radio is off at the hardware level.": "The radio is off at the hardware level.", + "The radio is software blocked; the toggle unblocks it.": "The radio is software blocked; the toggle unblocks it.", "The readout's lines: reorder, rename, remove": "The readout's lines: reorder, rename, remove", "The remote engine (ryossh) is not installed. Build it from ryoku/apps/ryovm/remote.": "The remote engine (ryossh) is not installed. Build it from ryoku/apps/ryovm/remote.", "The request failed.": "The request failed.", @@ -10338,7 +10254,6 @@ "The same edge where it does not face the light. With Blur it also smooths the stepped curves.": "The same edge where it does not face the light. With Blur it also smooths the stepped curves.", "The same motion when opening and closing": "The same motion when opening and closing", "The same, every switch a disc with its name under it.": "The same, every switch a disc with its name under it.", - "The scene could not be cut": "The scene could not be cut", "The scene's properties could not be read.": "The scene's properties could not be read.", "The schedule could not be loaded.": "The schedule could not be loaded.", "The schedule could not be saved.": "The schedule could not be saved.", @@ -10348,9 +10263,9 @@ "The seed colour the scheme derives from.": "The seed colour the scheme derives from.", "The selected model is available on this computer": "The selected model is available on this computer", "The selected path is not a readable folder or file.": "The selected path is not a readable folder or file.", + "The selected phone is no longer available": "The selected phone is no longer available", "The selected variant follows the current wallpaper.": "The selected variant follows the current wallpaper.", "The sensor gave up — try again": "The sensor gave up — try again", - "The service manager could not save this change.": "The service manager could not save this change.", "The shape behind the magnifier": "The shape behind the magnifier", "The shared dock is configured in Stage Editor, regardless of which shell or bar is active.": "The shared dock is configured in Stage Editor, regardless of which shell or bar is active.", "The shared picker UI pace. Change the Fast, Standard, and Slow weights in Motion settings.": "The shared picker UI pace. Change the Fast, Standard, and Slow weights in Motion settings.", @@ -10392,7 +10307,6 @@ "The update stopped before it finished (%v).": "The update stopped before it finished (%v).", "The update stopped before it finished.": "The update stopped before it finished.", "The update was no longer running.": "The update was no longer running.", - "The wallpaper is drawn flat behind the desktop.": "The wallpaper is drawn flat behind the desktop.", "The wallpaper stays; every generated and added layer is removed": "The wallpaper stays; every generated and added layer is removed", "The wheel over the dock turns between saved layouts": "The wheel over the dock turns between saved layouts", "The whole bar collapsed into one floating island": "The whole bar collapsed into one floating island", @@ -10455,10 +10369,6 @@ "This device": "This device", "This device cannot store the selected look in onboard memory. Ryoku reapplies it after login instead.": "This device cannot store the selected look in onboard memory. Ryoku reapplies it after login instead.", "This device did not provide a usable Bluetooth address.": "This device did not provide a usable Bluetooth address.", - "This device does not offer storing a look in its own memory, so Ryoku puts it back at every login instead.": "This device does not offer storing a look in its own memory, so Ryoku puts it back at every login instead.", - "This device lights in parts; each one takes its own colour.": "This device lights in parts; each one takes its own colour.", - "This device's effects": "This device's effects", - "This device's memory": "This device's memory", "This display no longer sets the colours.": "This display no longer sets the colours.", "This drawing's file is missing.": "This drawing's file is missing.", "This edition cannot remove the Voxtype package from the Hub.": "This edition cannot remove the Voxtype package from the Hub.", @@ -10486,7 +10396,6 @@ "This network type needs Wi-Fi settings to connect": "This network type needs Wi-Fi settings to connect", "This ordered rule stack is active.": "This ordered rule stack is active.", "This output's workspaces; tap for Overview or a workspace card.": "This output's workspaces; tap for Overview or a workspace card.", - "This package is not available on this edition.": "This package is not available on this edition.", "This page is being rebuilt into the monochrome instrument. Its settings and surfaces are wired page by page; the Shell page is the proven pattern.": "This page is being rebuilt into the monochrome instrument. Its settings and surfaces are wired page by page; the Shell page is the proven pattern.", "This palette could not be saved.": "This palette could not be saved.", "This palette is applied to the preview only": "This palette is applied to the preview only", @@ -10657,7 +10566,6 @@ "Tools": "Tools", "Tools baked in on first boot, clip is always on.": "Tools baked in on first boot, clip is always on.", "Toolset": "Toolset", - "Toolsets Hermes wields: search, files, the web.": "Toolsets Hermes wields: search, files, the web.", "Toolsets your agent wields: search, files, the web.": "Toolsets your agent wields: search, files, the web.", "Top": "Top", "Top and bottom": "Top and bottom", @@ -10767,7 +10675,6 @@ "Turn the adapter on to see nearby and paired devices.": "Turn the adapter on to see nearby and paired devices.", "Turn the dock on to choose its utility tiles and folders.": "Turn the dock on to choose its utility tiles and folders.", "Turn the touchpad off, the way the FN touchpad key does.": "Turn the touchpad off, the way the FN touchpad key does.", - "Turning this off puts the device back to %1, the effect it was on before Ryoku touched it.": "Turning this off puts the device back to %1, the effect it was on before Ryoku touched it.", "Turns every effect switch here on at once": "Turns every effect switch here on at once", "Turns every effect switch here on at once.": "Turns every effect switch here on at once.", "Turns glass, blur and heavy wallpaper effects off everywhere.": "Turns glass, blur and heavy wallpaper effects off everywhere.", @@ -10795,7 +10702,6 @@ "Type the digits, or pick a preset": "Type the digits, or pick a preset", "Type to begin": "Type to begin", "Type to filter · j/k or ↑↓ to move.": "Type to filter · j/k or ↑↓ to move.", - "Type to search": "Type to search", "Type to search or use the mode controls": "Type to search or use the mode controls", "Type to translate": "Type to translate", "Type your PIN in the window that opened — Android blanks the PIN screen, so it stays black": "Type your PIN in the window that opened — Android blanks the PIN screen, so it stays black", @@ -10810,6 +10716,7 @@ "UI scale": "UI scale", "UK / Ireland": "UK / Ireland", "UNAPPLIED LAYOUT CHANGES": "UNAPPLIED LAYOUT CHANGES", + "UNAVAILABLE": "UNAVAILABLE", "UNAVAILABLE ON %1": "UNAVAILABLE ON %1", "UNDER THE HOOD": "UNDER THE HOOD", "UNDO": "UNDO", @@ -10970,6 +10877,7 @@ "Update failed while %1": "Update failed while %1", "Update failed while %s": "Update failed while %s", "Update now": "Update now", + "Update now also takes the distribution's packages, kernel included (ryoku update --system)": "Update now also takes the distribution's packages, kernel included (ryoku update --system)", "Update preserves placement; Remove deletes the add-on and its settings": "Update preserves placement; Remove deletes the add-on and its settings", "Update running": "Update running", "Update stopped": "Update stopped", @@ -11004,7 +10912,6 @@ "Urgent notification": "Urgent notification", "Urgent reminder to plug in. Must be below the warning percentage.": "Urgent reminder to plug in. Must be below the warning percentage.", "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update --wait wait for a channel build or source check to finish\n update --force update despite an in-progress build or source check\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help": "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update --wait wait for a channel build or source check to finish\n update --force update despite an in-progress build or source check\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help", - "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help": "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help", "Usage: ryoku debug # print a shareable diagnostic bundle for bug reports": "Usage: ryoku debug # print a shareable diagnostic bundle for bug reports", "Usage: ryoku doctor [--check] [--verbose] [--report [file]] [--explain]\n\n (no args) check, and apply the safe automatic fixes\n --check, -n report what is wrong without changing anything\n --verbose, -v also list the checks that passed and advisory notes\n --report [file] write a shareable diagnostic report for the maintainers\n (default: %s)\n --explain ask your cloud model (Groq/OpenRouter) to reason over the report\n --json emit findings as JSON (read-only; powers the Hub System Check)": "Usage: ryoku doctor [--check] [--verbose] [--report [file]] [--explain]\n\n (no args) check, and apply the safe automatic fixes\n --check, -n report what is wrong without changing anything\n --verbose, -v also list the checks that passed and advisory notes\n --report [file] write a shareable diagnostic report for the maintainers\n (default: %s)\n --explain ask your cloud model (Groq/OpenRouter) to reason over the report\n --json emit findings as JSON (read-only; powers the Hub System Check)", "Usage: ryoku import [flags]\n\n a config folder, a dropped ~/.config, or a dotfiles tree\n --url clone a dotfiles repo instead of reading a local path\n --keep mine|ryoku how to settle every keybind clash (default: mine)\n --undo [] roll back the last import, or the one stamped \n\nHeadless: scan, resolve all conflicts to one side, apply, print the change set.\nThe Hub's Import page is the interactive path for reviewing clash by clash.": "Usage: ryoku import [flags]\n\n a config folder, a dropped ~/.config, or a dotfiles tree\n --url clone a dotfiles repo instead of reading a local path\n --keep mine|ryoku how to settle every keybind clash (default: mine)\n --undo [] roll back the last import, or the one stamped \n\nHeadless: scan, resolve all conflicts to one side, apply, print the change set.\nThe Hub's Import page is the interactive path for reviewing clash by clash.", @@ -11025,7 +10932,6 @@ "Use Add security key to enroll another key, or remove an old one below.": "Use Add security key to enroll another key, or remove an old one below.", "Use Android debugging over the local network.": "Use Android debugging over the local network.", "Use Black Background (WearOS style)": "Use Black Background (WearOS style)", - "Use Depth": "Use Depth", "Use Expressive Colors": "Use Expressive Colors", "Use GPS when there is one": "Use GPS when there is one", "Use ISOLATE from the physical machine; every remote connection is cut immediately.": "Use ISOLATE from the physical machine; every remote connection is cut immediately.", @@ -11036,7 +10942,6 @@ "Use Material Symbol for outer icon": "Use Material Symbol for outer icon", "Use Matugen for wallpaper colours. Turning this off uses built-in Iris.": "Use Matugen for wallpaper colours. Turning this off uses built-in Iris.", "Use Organic visualizer": "Use Organic visualizer", - "Use Parallax": "Use Parallax", "Use To-Do context": "Use To-Do context", "Use a Material Symbol name or an SVG/distro name from the icons folder.": "Use a Material Symbol name or an SVG/distro name from the icons folder.", "Use a Material Symbol name, distro, or an SVG name from the icons folder.": "Use a Material Symbol name, distro, or an SVG name from the icons folder.", @@ -11048,7 +10953,6 @@ "Use adaptive alignment": "Use adaptive alignment", "Use add --full-name --role admin|standard [--perm id]... [--limit bytes].": "Use add --full-name --role admin|standard [--perm id]... [--limit bytes].", "Use album colors": "Use album colors", - "Use an existing transparent cut-out": "Use an existing transparent cut-out", "Use an image, video, or Wallpaper Engine scene behind the overview. Blur and dim it independently of the desktop wallpaper.": "Use an image, video, or Wallpaper Engine scene behind the overview. Blur and dim it independently of the desktop wallpaper.", "Use as default": "Use as default", "Use as exit": "Use as exit", @@ -11109,7 +11013,6 @@ "User Card": "User Card", "User accounts are not available on this system.": "User accounts are not available on this system.", "User card": "User card", - "User password": "User password", "User script: %1": "User script: %1", "User verification": "User verification", "Username": "Username", @@ -11320,7 +11223,6 @@ "Waiting for Claude Code…": "Waiting for Claude Code…", "Waiting for a pen — a mouse always draws even lines": "Waiting for a pen — a mouse always draws even lines", "Waiting for ani-cli": "Waiting for ani-cli", - "Waiting for approval: %1": "Waiting for approval: %1", "Waiting for browser...": "Waiting for browser...", "Waiting for samples": "Waiting for samples", "Waiting for the %s channel build of %s to finish (%s)": "Waiting for the %s channel build of %s to finish (%s)", @@ -11499,7 +11401,6 @@ "Well done 💧": "Well done 💧", "WezTerm": "WezTerm", "What ? and Search the web use in Spotlight.": "What ? and Search the web use in Spotlight.", - "What Hermes remembers, carried across every session.": "What Hermes remembers, carried across every session.", "What Shima steps back from while you play": "What Shima steps back from while you play", "What a switch lights up with. Colourful gives each one its own hue; Mono lights them white.": "What a switch lights up with. Colourful gives each one its own hue; Mono lights them white.", "What an open card or panel is lit by: its own colour — the sky for weather, the highlight for timers — or the wallpaper's. It pours in from where the body grew.": "What an open card or panel is lit by: its own colour — the sky for weather, the highlight for timers — or the wallpaper's. It pours in from where the body grew.", @@ -11560,7 +11461,6 @@ "Whether you are on a VPN, as a shield that fills when you are. Its card lists your NetworkManager profiles and Tailscale, and turns any of them on or off.": "Whether you are on a VPN, as a shield that fills when you are. Its card lists your NetworkManager profiles and Tailscale, and turns any of them on or off.", "Which GPU renders the desktop; next login": "Which GPU renders the desktop; next login", "Which agent answers the Alt+Space bar's Agent mode. Hermes is recommended; others need their own ACP adapter installed.": "Which agent answers the Alt+Space bar's Agent mode. Hermes is recommended; others need their own ACP adapter installed.", - "Which agent answers the Super+S chat. Hermes is recommended; others need their own ACP adapter installed.": "Which agent answers the Super+S chat. Hermes is recommended; others need their own ACP adapter installed.", "Which app each launcher key opens, and the key itself": "Which app each launcher key opens, and the key itself", "Which bar the desktop draws, and how it looks.": "Which bar the desktop draws, and how it looks.", "Which coding-agent meters the AI pill shows.": "Which coding-agent meters the AI pill shows.", @@ -11604,7 +11504,6 @@ "Wi-Fi radio": "Wi-Fi radio", "Wi-Fi regulatory domain: %s": "Wi-Fi regulatory domain: %s", "Wi-Fi regulatory domain: %s not applied yet (ryoku-wifi-regdom is not in this target;": "Wi-Fi regulatory domain: %s not applied yet (ryoku-wifi-regdom is not in this target;", - "Wi-Fi, Bluetooth and this machine's hotspot, applied live.": "Wi-Fi, Bluetooth and this machine's hotspot, applied live.", "Wi-Fi, Bluetooth, hotspot and phone connections, applied live.": "Wi-Fi, Bluetooth, hotspot and phone connections, applied live.", "Wide": "Wide", "Wide banner behind the palette; drag to reframe it": "Wide banner behind the palette; drag to reframe it", @@ -11791,7 +11690,6 @@ "Your fork tree lives at `%s`. A regular file here wins over Ryoku's copy at\nthe mirrored ~/.config path on every update.": "Your fork tree lives at `%s`. A regular file here wins over Ryoku's copy at\nthe mirrored ~/.config path on every update.", "Your graphics driver exposes no tunable knobs on this session. Everything here is read-only on this hardware.": "Your graphics driver exposes no tunable knobs on this session. Everything here is read-only on this hardware.", "Your keybinds are in the Keybinds page and conflict-checked from now on; the rest of your config is layered in and live. Reload the desktop to see it, or undo the whole import.": "Your keybinds are in the Keybinds page and conflict-checked from now on; the rest of your config is layered in and live. Reload the desktop to see it, or undo the whole import.", - "Your login account name. Lowercase.": "Your login account name. Lowercase.", "Your modules": "Your modules", "Your music appears here": "Your music appears here", "Your name": "Your name", @@ -11800,7 +11698,6 @@ "Your own override file. Ryoku reads it but never ships or overwrites it, so this is where a tweak belongs.": "Your own override file. Ryoku reads it but never ships or overwrites it, so this is where a tweak belongs.", "Your paired phone's screen, a click away": "Your paired phone's screen, a click away", "Your password": "Your password", - "Your picks are kept in ~/.config/ryoku/lighting.json: an update, a shell restart or a reboot leaves them exactly as they are.": "Your picks are kept in ~/.config/ryoku/lighting.json: an update, a shell restart or a reboot leaves them exactly as they are.", "Your place is saved as you watch": "Your place is saved as you watch", "Your quote": "Your quote", "Your screen runs on %1. Switch to Hybrid GPU mode in the BIOS (look for GPU Mode, MUX, or Hybrid/Optimus) and reboot, so the built-in GPU drives the display and the discrete GPU is free.": "Your screen runs on %1. Switch to Hybrid GPU mode in the BIOS (look for GPU Mode, MUX, or Hybrid/Optimus) and reboot, so the built-in GPU drives the display and the discrete GPU is free.", @@ -11910,6 +11807,7 @@ "advice: %s": "advice: %s", "after": "after", "air %1 px": "air %1 px", + "aligning newer [ryoku] package overrides: %s": "aligning newer [ryoku] package overrides: %s", "all": "all", "all / favourites": "all / favourites", "all checks passed": "all checks passed", @@ -11939,8 +11837,6 @@ "alongside region: sectors %s-%s (%sGiB); boot %s-%s, root %s-%s": "alongside region: sectors %s-%s (%sGiB); boot %s-%s, root %s-%s", "alongside · btrfs %dsv": "alongside · btrfs %dsv", "already on %s; nothing to move, `ryoku update` keeps it current": "already on %s; nothing to move, `ryoku update` keeps it current", - "already on unstable (the channel `ryoku track unstable-dev` used to select); nothing to move, `ryoku update` keeps it current": "already on unstable (the channel `ryoku track unstable-dev` used to select); nothing to move, `ryoku update` keeps it current", - "already pinned to unstable build %s; nothing to move, `ryoku track unstable` follows unstable builds again": "already pinned to unstable build %s; nothing to move, `ryoku track unstable` follows unstable builds again", "already pinned to unstable build %s; nothing to move, `ryoku track unstable` switches to the source checkout": "already pinned to unstable build %s; nothing to move, `ryoku track unstable` switches to the source checkout", "already provided by installed packages: %s": "already provided by installed packages: %s", "an older one": "an older one", @@ -11964,7 +11860,6 @@ "ask": "ask", "ask the Rashin agent (one terse answer)": "ask the Rashin agent (one terse answer)", "asusctl was removed by hand; leaving the Aura keyboard unmanaged": "asusctl was removed by hand; leaving the Aura keyboard unmanaged", - "asusd is installed but not running, so the Aura keyboard is absent from Appearance": "asusd is installed but not running, so the Aura keyboard is absent from Appearance", "asusd is installed but not running, so the Aura keyboard is absent from Ryogami lighting settings": "asusd is installed but not running, so the Aura keyboard is absent from Ryogami lighting settings", "at least": "at least", "at most": "at most", @@ -12102,17 +11997,10 @@ "by %1": "by %1", "by %1 ↗": "by %1 ↗", "cachyos: applying CachyOS system identity": "cachyos: applying CachyOS system identity", - "cachyos: configuring the CachyOS repositories in the target": "cachyos: configuring the CachyOS repositories in the target", "cachyos: keyring populate failed (cachyos-keyring absent?); a later -Syu can 'pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com'": "cachyos: keyring populate failed (cachyos-keyring absent?); a later -Syu can 'pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com'", - "cachyos: no %s in the target yet; skipping repo config": "cachyos: no %s in the target yet; skipping repo config", - "cachyos: no [core] anchor in %s; appending the CachyOS repos": "cachyos: no [core] anchor in %s; appending the CachyOS repos", - "cachyos: repositories already present": "cachyos: repositories already present", "cachyos: suppressing CachyOS Welcome autostart": "cachyos: suppressing CachyOS Welcome autostart", "cachyos: the target pacman does not resolve Architecture = auto to %s; naming it explicitly": "cachyos: the target pacman does not resolve Architecture = auto to %s; naming it explicitly", "cachyos: trusted the CachyOS signing key via the installed keyring": "cachyos: trusted the CachyOS signing key via the installed keyring", - "cachyos: warning, could not add the CachyOS repo to %s": "cachyos: warning, could not add the CachyOS repo to %s", - "cachyos: warning, could not add the CachyOS repos to %s": "cachyos: warning, could not add the CachyOS repos to %s", - "cachyos: warning, could not enable [multilib] (lib32 updates will be unavailable)": "cachyos: warning, could not enable [multilib] (lib32 updates will be unavailable)", "caelestia": "caelestia", "calculator": "calculator", "camera.png": "camera.png", @@ -12143,7 +12031,6 @@ "cannot switch to %s yet: %s is unavailable on this host": "cannot switch to %s yet: %s is unavailable on this host", "cannot switch to %s: %s": "cannot switch to %s: %s", "cannot verify installed kernel metadata for %s; its module tree is incomplete": "cannot verify installed kernel metadata for %s; its module tree is incomplete", - "cannot verify installed kernel metadata: incomplete module tree(s) under %s: %s": "cannot verify installed kernel metadata: incomplete module tree(s) under %s: %s", "caps:escape": "caps:escape", "caps:none": "caps:none", "caps:swapescape": "caps:swapescape", @@ -12188,7 +12075,6 @@ "check `btrfs filesystem usage /`; may need `btrfs balance` or more free space": "check `btrfs filesystem usage /`; may need `btrfs balance` or more free space", "check sudo access, then re-run ryoku doctor": "check sudo access, then re-run ryoku doctor", "check the ESP and rerun ryoku doctor": "check the ESP and rerun ryoku doctor", - "check the ESP and rerun sudo ryoku doctor": "check the ESP and rerun sudo ryoku doctor", "check_box_outline_blank": "check_box_outline_blank", "checked: tip %s failed its checks (%s)": "checked: tip %s failed its checks (%s)", "checked: tip %s is being checked (%s)": "checked: tip %s is being checked (%s)", @@ -12210,7 +12096,6 @@ "clear Limine remembered entry: %w": "clear Limine remembered entry: %w", "clear Limine remembered-entry attributes: %w": "clear Limine remembered-entry attributes: %w", "cleared %d failed unit(s) with nothing left wrong (stale app launch units, TPM NvPCR setup)": "cleared %d failed unit(s) with nothing left wrong (stale app launch units, TPM NvPCR setup)", - "cleared %d failed unit(s) with nothing left wrong (stale app scopes, TPM NvPCR setup)": "cleared %d failed unit(s) with nothing left wrong (stale app scopes, TPM NvPCR setup)", "cleared a crashed update's run-state": "cleared a crashed update's run-state", "cleared the GPU render pin on %s; the discrete GPU can runtime-suspend after the next Hyprland login (switch back with `ryoku-gpu mode performance`)": "cleared the GPU render pin on %s; the discrete GPU can runtime-suspend after the next Hyprland login (switch back with `ryoku-gpu mode performance`)", "cleared the broken brand image (%s); the mark falls back to the text seal": "cleared the broken brand image (%s); the mark falls back to the text seal", @@ -12244,6 +12129,7 @@ "colours follow the wallpaper, or a palette is locked by choice": "colours follow the wallpaper, or a palette is locked by choice", "column": "column", "columnwidth": "columnwidth", + "com.github.wwmm.easyeffects": "com.github.wwmm.easyeffects", "comma-separated optional components to leave out": "comma-separated optional components to leave out", "command": "command", "command to run": "command to run", @@ -12419,7 +12305,6 @@ "could not remove %s, which the target release does not serve; the downgrade may fail on its exact pins (%v)": "could not remove %s, which the target release does not serve; the downgrade may fail on its exact pins (%v)", "could not remove %s: %v": "could not remove %s: %v", "could not remove home-deployed artifact(s) or reload repaired user units: %s": "could not remove home-deployed artifact(s) or reload repaired user units: %s", - "could not remove home-deployed artifact(s) still shadowing the packaged install: %s": "could not remove home-deployed artifact(s) still shadowing the packaged install: %s", "could not remove incompatible %s: %v": "could not remove incompatible %s: %v", "could not remove nested session bus wrappers (exit %d)": "could not remove nested session bus wrappers (exit %d)", "could not remove orphaned theme.lua: %v": "could not remove orphaned theme.lua: %v", @@ -12538,7 +12423,6 @@ "default file manager: warning, could not set %s for %s (continuing)": "default file manager: warning, could not set %s for %s (continuing)", "default sink %s not listed": "default sink %s not listed", "defaulted the bar to Orpheus / reduced any Sumi frame to left-only": "defaulted the bar to Orpheus / reduced any Sumi frame to left-only", - "defaulted the bar to qsbar / reduced any Sumi frame to left-only": "defaulted the bar to qsbar / reduced any Sumi frame to left-only", "deferring the initramfs build to the bootloader step: masking %s": "deferring the initramfs build to the bootloader step: masking %s", "deg": "deg", "degraded": "degraded", @@ -12600,7 +12484,6 @@ "dns: would verify name resolution and write fallback resolvers (1.1.1.1, 9.9.9.9, 8.8.8.8) if it fails": "dns: would verify name resolution and write fallback resolvers (1.1.1.1, 9.9.9.9, 8.8.8.8) if it fails", "do not match": "do not match", "dock config store": "dock config store", - "dock settings use the universal dock store": "dock settings use the universal dock store", "does not carry": "does not carry", "does not match": "does not match", "done": "done", @@ -12646,6 +12529,7 @@ "e2fsck found errors it could not fix on %s (rc=%s); refusing to shrink.": "e2fsck found errors it could not fix on %s (rc=%s); refusing to shrink.", "earlier Fedora boot-menu leftovers": "earlier Fedora boot-menu leftovers", "earlier backups sit alongside; the oldest holds your pre-Ryoku configs": "earlier backups sit alongside; the oldest holds your pre-Ryoku configs", + "easyeffects": "easyeffects", "edit": "edit", "edit %s by hand and delete: %s": "edit %s by hand and delete: %s", "edit: %s": "edit: %s", @@ -12668,7 +12552,6 @@ "enabled plugin(s) built for another compositor build and this box cannot rebuild them (missing %s): %s": "enabled plugin(s) built for another compositor build and this box cannot rebuild them (missing %s): %s", "enabled plugin(s) built for another compositor build: %s": "enabled plugin(s) built for another compositor build: %s", "enabled rashin at boot (the Alt+Space bar and AI dashboard); `ryoku-rashin disable` turns it off": "enabled rashin at boot (the Alt+Space bar and AI dashboard); `ryoku-rashin disable` turns it off", - "enabled rashin at boot (the Super+S needle and AI dashboard); `ryoku-rashin disable` turns it off": "enabled rashin at boot (the Super+S needle and AI dashboard); `ryoku-rashin disable` turns it off", "enabled rashin for this and future sessions; `ryoku-rashin disable` turns it off": "enabled rashin for this and future sessions; `ryoku-rashin disable` turns it off", "enabled snapper-cleanup.timer": "enabled snapper-cleanup.timer", "enabled the AI usage collector service": "enabled the AI usage collector service", @@ -12739,11 +12622,6 @@ "fastfetch's OS line names the release": "fastfetch's OS line names the release", "fastfetch's OS line now names the release (ryoku version --pretty)": "fastfetch's OS line now names the release (ryoku version --pretty)", "fastfetch's OS line predates named releases (ryoku version -> ryoku version --pretty)": "fastfetch's OS line predates named releases (ryoku version -> ryoku version --pretty)", - "feature: LAN file sharing (the pill's file stash speaks it; AUR)": "feature: LAN file sharing (the pill's file stash speaks it; AUR)", - "feature: Recognize Music in the launcher": "feature: Recognize Music in the launcher", - "feature: keyboard/mouse lighting follows the wallpaper palette": "feature: keyboard/mouse lighting follows the wallpaper palette", - "feature: ryoshot Beautify HD and ryowalls Enhance": "feature: ryoshot Beautify HD and ryowalls Enhance", - "feature: voice dictation on Super+` (AUR)": "feature: voice dictation on Super+` (AUR)", "federated apps, open windows, web and quick math": "federated apps, open windows, web and quick math", "feels": "feels", "fetch recovery script from %s: %w": "fetch recovery script from %s: %w", @@ -12779,6 +12657,7 @@ "flushing the target to disk": "flushing the target to disk", "focus": "focus", "focused": "focused", + "folder": "folder", "folder name or path": "folder name or path", "folder:nature/forest": "folder:nature/forest", "follow-mouse default": "follow-mouse default", @@ -12788,7 +12667,6 @@ "follow-wallpaper default": "follow-wallpaper default", "follow-wallpaper default already reconciled": "follow-wallpaper default already reconciled", "for": "for", - "for every keyboard shortcut": "for every keyboard shortcut", "for settings": "for settings", "for the sake of power & beauty": "for the sake of power & beauty", "for the sidebar": "for the sidebar", @@ -12885,7 +12763,6 @@ "hybrid GPU (%s) with only a firmware backlight (%s); the panel may not dim": "hybrid GPU (%s) with only a firmware backlight (%s); the panel may not dim", "hybrid GPU (%s): the kernel reports the dGPU has no working backlight, and the firmware fallback (%s) does not dim the panel": "hybrid GPU (%s): the kernel reports the dGPU has no working backlight, and the firmware fallback (%s) does not dim the panel", "hypr.json does not parse, so it cannot be migrated: %v": "hypr.json does not parse, so it cannot be migrated: %v", - "hypr/keyboard.lua already exists, keeping it": "hypr/keyboard.lua already exists, keeping it", "hyprmod": "hyprmod", "hyprpm": "hyprpm", "iGPU + dGPU for VM": "iGPU + dGPU for VM", @@ -12915,7 +12792,6 @@ "in": "in", "in Ryostore.": "in Ryostore.", "in the agent ·": "in the agent ·", - "in-game FPS and frametime overlay": "in-game FPS and frametime overlay", "in-session lock (next daemon start)": "in-session lock (next daemon start)", "in-session lockscreen": "in-session lockscreen", "in-session lockscreen installed": "in-session lockscreen installed", @@ -12945,7 +12821,6 @@ "install failed: %s": "install failed: %s", "install into free space (new ESP)": "install into free space (new ESP)", "install limine-mkinitcpio-hook, then ryoku doctor": "install limine-mkinitcpio-hook, then ryoku doctor", - "install limine-mkinitcpio-hook, then sudo ryoku doctor": "install limine-mkinitcpio-hook, then sudo ryoku doctor", "install log:": "install log:", "install steps": "install steps", "install that desktop's package, then run `ryoku doctor` again": "install that desktop's package, then run `ryoku doctor` again", @@ -12957,13 +12832,11 @@ "installed %s but could not lay its config down (%v); run `ryoku materialize` before logging out": "installed %s but could not lay its config down (%v); run `ryoku materialize` before logging out", "installed %s; %s did not land": "installed %s; %s did not land", "installed Shelly's Flatpak backend; its Flatpak searches and installs now work": "installed Shelly's Flatpak backend; its Flatpak searches and installs now work", - "installed asusctl and started asusd; the ASUS Aura keyboard is available in Appearance": "installed asusctl and started asusd; the ASUS Aura keyboard is available in Appearance", "installed asusctl and started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings": "installed asusctl and started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings", "installed browser animations for Zen profile: %s": "installed browser animations for Zen profile: %s", "installed limine-mkinitcpio-hook": "installed limine-mkinitcpio-hook", "installed plugin %q: %w": "installed plugin %q: %w", - "installed qmk-hid; the QMK/VIA keyboard is available in Appearance": "installed qmk-hid; the QMK/VIA keyboard is available in Appearance", - "installed qmk_hid; the QMK/VIA keyboard is available in Ryogami lighting settings": "installed qmk_hid; the QMK/VIA keyboard is available in Ryogami lighting settings", + "installed qmk-hid; the QMK/VIA keyboard is available in Ryogami lighting settings": "installed qmk-hid; the QMK/VIA keyboard is available in Ryogami lighting settings", "installed the %s desktop variant; log into it before the retired compositor is removed": "installed the %s desktop variant; log into it before the retired compositor is removed", "installed the Material Symbols font, but fontconfig cannot see it": "installed the Material Symbols font, but fontconfig cannot see it", "installed the Material Symbols icon font; `ryoku reload` picks it up": "installed the Material Symbols icon font; `ryoku reload` picks it up", @@ -13001,7 +12874,6 @@ "issue": "issue", "issues": "issues", "items on list": "items on list", - "its own effect": "its own effect", "iw is not installed, so the wireless regulatory domain cannot be read": "iw is not installed, so the wireless regulatory domain cannot be read", "iwd": "iwd", "just now": "just now", @@ -13067,7 +12939,6 @@ "laid down the %s config and started the desktop; log out and back in if a surface is still missing": "laid down the %s config and started the desktop; log out and back in if a surface is still missing", "laid down the %s config but could not safely start the desktop: %v": "laid down the %s config but could not safely start the desktop: %v", "laid down the compositor config for %s": "laid down the compositor config for %s", - "language/version manager": "language/version manager", "laocoon.png": "laocoon.png", "last": "last", "last output": "last output", @@ -13120,7 +12991,6 @@ "limine snapshot sync is not installed": "limine snapshot sync is not installed", "limine-entry-tool owns the boot menu (UKI tree with kernel sub-entries)": "limine-entry-tool owns the boot menu (UKI tree with kernel sub-entries)", "limine-mkinitcpio-hook adopted the placeholder as the boot tree: repointing the default": "limine-mkinitcpio-hook adopted the placeholder as the boot tree: repointing the default", - "limine-mkinitcpio-hook owns the boot menu (UKI tree with kernel sub-entries)": "limine-mkinitcpio-hook owns the boot menu (UKI tree with kernel sub-entries)", "limine-mkinitcpio-hook owns the menu: dropping the flat placeholder entry": "limine-mkinitcpio-hook owns the menu: dropping the flat placeholder entry", "limine-snapper-sync is not installed, so snapshots are missing from the boot": "limine-snapper-sync is not installed, so snapshots are missing from the boot", "limine-snapper-sync is not installed, so snapshots are not in the Limine boot menu (%s)": "limine-snapper-sync is not installed, so snapshots are not in the Limine boot menu (%s)", @@ -13145,6 +13015,7 @@ "lockscreen install failed: %v (%s)": "lockscreen install failed: %v (%s)", "lockscreen installer ran but %s did not appear": "lockscreen installer ran but %s did not appear", "lockscreen refresh failed: %v (%s)": "lockscreen refresh failed: %v (%s)", + "lockscreen-preview": "lockscreen-preview", "lockscreen: unavailable": "lockscreen: unavailable", "log": "log", "log in as %s · your configs live in ~/.config": "log in as %s · your configs live in ~/.config", @@ -13158,7 +13029,6 @@ "login screen cursor": "login screen cursor", "login shell": "login shell", "login shell %s is missing from the target": "login shell %s is missing from the target", - "login shell %s is missing from the target; refusing to create %s with an unusable shell": "login shell %s is missing from the target; refusing to create %s with an unusable shell", "login shell is %q on both the account and the session override": "login shell is %q on both the account and the session override", "login shell source": "login shell source", "login: %s": "login: %s", @@ -13166,9 +13036,7 @@ "logs in as ryoku / ryoku": "logs in as ryoku / ryoku", "logs only, no passwords or keys. Open an issue: %s": "logs only, no passwords or keys. Open an issue: %s", "looks like a hardcoded secret or key (R10)": "looks like a hardcoded secret or key (R10)", - "lowercase; start with a letter or _": "lowercase; start with a letter or _", "lowercase; start with a letter or _; 32 characters at most": "lowercase; start with a letter or _; 32 characters at most", - "ls/cat replacements · Ctrl-R + fuzzy cd in the shell (each degrades cleanly if removed)": "ls/cat replacements · Ctrl-R + fuzzy cd in the shell (each degrades cleanly if removed)", "m ago": "m ago", "machine": "machine", "making a mountpoint for the btrfs top level: %v": "making a mountpoint for the btrfs top level: %v", @@ -13200,13 +13068,13 @@ "media": "media", "medium": "medium", "melts into it": "melts into it", + "memory pressure": "memory pressure", "menu. Install it first:": "menu. Install it first:", "merge": "merge", "merge %s into %s and remove it (it shadows the generated boot entries: kernels, snapshots)": "merge %s into %s and remove it (it shadows the generated boot entries: kernels, snapshots)", "merge each .new- file into its base config, then remove the reviewed files with `sudo rm -f %s`": "merge each .new- file into its base config, then remove the reviewed files with `sudo rm -f %s`", "merge each .rpmnew or .rpmsave file into its base config, then remove the reviewed files": "merge each .rpmnew or .rpmsave file into its base config, then remove the reviewed files", "merged boot menu into %s": "merged boot menu into %s", - "merged retired dock settings into the universal dock store": "merged retired dock settings into the universal dock store", "mic.png": "mic.png", "middle": "middle", "migrated Atoll settings to frame bars": "migrated Atoll settings to frame bars", @@ -13248,7 +13116,6 @@ "missioncenter": "missioncenter", "mkinitcpio HOOKS drop-in (/etc/mkinitcpio.conf.d/ryoku.conf)": "mkinitcpio HOOKS drop-in (/etc/mkinitcpio.conf.d/ryoku.conf)", "mkinitcpio does not manage the initramfs on this host": "mkinitcpio does not manage the initramfs on this host", - "mkinitcpio not installed": "mkinitcpio not installed", "mkv": "mkv", "mode: %s": "mode: %s", "mode: %s (%s)": "mode: %s (%s)", @@ -13306,6 +13173,7 @@ "niri config not checked (niri is not on PATH)": "niri config not checked (niri is not on PATH)", "niri config still does not load: %s": "niri config still does not load: %s", "nixos-configuration": "nixos-configuration", + "nm-connection-editor": "nm-connection-editor", "nmtui in Foot": "nmtui in Foot", "nmtui in Kitty": "nmtui in Kitty", "no": "no", @@ -13702,7 +13570,6 @@ "pointed default_entry at the kernel entry path and enabled remember_last_entry, so the countdown autoboots the last kernel used": "pointed default_entry at the kernel entry path and enabled remember_last_entry, so the countdown autoboots the last kernel used", "pointed the \\\"default\\\" cursor theme at %s; the login screen has a pointer now": "pointed the \\\"default\\\" cursor theme at %s; the login screen has a pointer now", "pointed the default keyring at login": "pointed the default keyring at login", - "pointing the SDDM login screen at the %s layout via xorg.conf.d": "pointing the SDDM login screen at the %s layout via xorg.conf.d", "pointing the next boot at the restored root: %w": "pointing the next boot at the restored root: %w", "points the SDDM login screen at the Ryoku qylock greeter": "points the SDDM login screen at the Ryoku qylock greeter", "poke around first": "poke around first", @@ -13755,7 +13622,6 @@ "prowl comes from a package; refreshed with the system packages": "prowl comes from a package; refreshed with the system packages", "prowl for rashin": "prowl for rashin", "prowl is managed by %s; refreshed with the system packages": "prowl is managed by %s; refreshed with the system packages", - "prowl is managed by pacman; refreshed with the system packages": "prowl is managed by pacman; refreshed with the system packages", "prowl is not installed; gateway health is not applicable": "prowl is not installed; gateway health is not applicable", "prowl is present for the rashin agent index": "prowl is present for the rashin agent index", "pruned the stray boot entry for %s (kernel not installed)": "pruned the stray boot entry for %s (kernel not installed)", @@ -13796,7 +13662,6 @@ "radio_button_unchecked": "radio_button_unchecked", "random": "random", "rashin (the Alt+Space bar and AI dashboard) is off; Ryoku turns it on by default": "rashin (the Alt+Space bar and AI dashboard) is off; Ryoku turns it on by default", - "rashin (the Super+S needle and AI dashboard) is off; Ryoku turns it on by default": "rashin (the Super+S needle and AI dashboard) is off; Ryoku turns it on by default", "rashin agent daemon": "rashin agent daemon", "rashin daemon enabled and supervised in the runit session roster; the ryoku skill is wired": "rashin daemon enabled and supervised in the runit session roster; the ryoku skill is wired", "rashin daemon enabled with boot-start; the ryoku skill is wired": "rashin daemon enabled with boot-start; the ryoku skill is wired", @@ -13822,7 +13687,6 @@ "re-asserted a display that came up below its available resolution": "re-asserted a display that came up below its available resolution", "re-authored the niri config; it loads cleanly again": "re-authored the niri config; it loads cleanly again", "re-enabled [multilib] in %s so the %d 32-bit package(s) can update": "re-enabled [multilib] in %s so the %d 32-bit package(s) can update", - "re-enabled [multilib], but syncing it failed: %v": "re-enabled [multilib], but syncing it failed: %v", "re-enabled [multilib], but syncing package databases failed: %v": "re-enabled [multilib], but syncing package databases failed: %v", "re-enabled session units under %s: %s": "re-enabled session units under %s: %s", "re-registered the missing Ryoku UEFI boot entry": "re-registered the missing Ryoku UEFI boot entry", @@ -13864,9 +13728,7 @@ "recolor templates + animation presets": "recolor templates + animation presets", "recommended": "recommended", "reconcile %s to its checked target failed: %w": "reconcile %s to its checked target failed: %w", - "reconcile to origin/%s failed: %w": "reconcile to origin/%s failed: %w", "reconciled user and system-wide services from the activation manifest": "reconciled user and system-wide services from the activation manifest", - "reconciled user services from the activation manifest": "reconciled user services from the activation manifest", "record": "record", "record existing user service choices: %v": "record existing user service choices: %v", "record keyring mode: %w": "record keyring mode: %w", @@ -13961,7 +13823,6 @@ "requested region %s-%s is not inside a free area of %s (did the disk change since it was probed?); refusing to partition.": "requested region %s-%s is not inside a free area of %s (did the disk change since it was probed?); refusing to partition.", "required": "required", "required for Zen; also builds the selected AUR app components and the Bibata cursor": "required for Zen; also builds the selected AUR app components and the Bibata cursor", - "required for Zen; also installs Bibata cursor, LocalSend and Voxtype": "required for Zen; also installs Bibata cursor, LocalSend and Voxtype", "required: %s": "required: %s", "requires a network connection and x86-64-v3; installs the CachyOS kernel, tuning, sched-ext, Steam and Proton-CachyOS; the Arch kernel stays as a boot fallback": "requires a network connection and x86-64-v3; installs the CachyOS kernel, tuning, sched-ext, Steam and Proton-CachyOS; the Arch kernel stays as a boot fallback", "res:>=1920x1080": "res:>=1920x1080", @@ -14012,7 +13873,6 @@ "retired compositor artifacts need migration to %s": "retired compositor artifacts need migration to %s", "retired compositor package migration is not applicable on this host": "retired compositor package migration is not applicable on this host", "retired cursor keys": "retired cursor keys", - "retired dock settings still exist outside the universal dock store": "retired dock settings still exist outside the universal dock store", "retired filter bar keys": "retired filter bar keys", "retired panel keybind": "retired panel keybind", "retired shell menus": "retired shell menus", @@ -14074,7 +13934,6 @@ "run `ryoku update` once to record the baseline": "run `ryoku update` once to record the baseline", "run `ryoku update` to deliver what is missing": "run `ryoku update` to deliver what is missing", "run `ryoku-shell daemon` in a terminal to see why it exits": "run `ryoku-shell daemon` in a terminal to see why it exits", - "run `sudo ryoku doctor`; use `ryoku-host pkg advice ` for a missing snapshot package": "run `sudo ryoku doctor`; use `ryoku-host pkg advice ` for a missing snapshot package", "run as your normal user, not root (sudo is used when needed)": "run as your normal user, not root (sudo is used when needed)", "run as your normal user, not root; sudo is used where needed": "run as your normal user, not root; sudo is used where needed", "run non-interactively with the default plan": "run non-interactively with the default plan", @@ -14104,9 +13963,7 @@ "ryoku boot-menu runs as root (sudo ryoku boot-menu %s)": "ryoku boot-menu runs as root (sudo ryoku boot-menu %s)", "ryoku channel pin": "ryoku channel pin", "ryoku channel: pinned to release %s (packages); `ryoku track stable` follows releases again": "ryoku channel: pinned to release %s (packages); `ryoku track stable` follows releases again", - "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` follows unstable builds again": "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` follows unstable builds again", "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` switches to the unstable-dev source checkout": "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` switches to the unstable-dev source checkout", - "ryoku channel: stable packages (named releases); `ryoku track unstable` follows the unstable channel": "ryoku channel: stable packages (named releases); `ryoku track unstable` follows the unstable channel", "ryoku channel: stable packages (named releases); `ryoku track unstable` follows unstable-dev from source": "ryoku channel: stable packages (named releases); `ryoku track unstable` follows unstable-dev from source", "ryoku channel: unstable packages (rebuilt on every push); `ryoku track stable` returns to stable releases": "ryoku channel: unstable packages (rebuilt on every push); `ryoku track stable` returns to stable releases", "ryoku doctor": "ryoku doctor", @@ -14138,7 +13995,6 @@ "ryoku doctor changes it to the 2 px card-local frost default": "ryoku doctor changes it to the 2 px card-local frost default", "ryoku doctor clears it back to the text seal": "ryoku doctor clears it back to the text seal", "ryoku doctor converges it to the Orpheus default in place": "ryoku doctor converges it to the Orpheus default in place", - "ryoku doctor converges it to the qsbar default in place": "ryoku doctor converges it to the qsbar default in place", "ryoku doctor creates the limine snapshot-sync defaults": "ryoku doctor creates the limine snapshot-sync defaults", "ryoku doctor drops it": "ryoku doctor drops it", "ryoku doctor drops the pin so the network roams again": "ryoku doctor drops the pin so the network roams again", @@ -14155,11 +14011,9 @@ "ryoku doctor installs pam-fprint-grosshack (AUR)": "ryoku doctor installs pam-fprint-grosshack (AUR)", "ryoku doctor installs pam-fprint-grosshack from the configured Fedora repositories": "ryoku doctor installs pam-fprint-grosshack from the configured Fedora repositories", "ryoku doctor installs qmk-hid so the keyboard follows the theme": "ryoku doctor installs qmk-hid so the keyboard follows the theme", - "ryoku doctor installs qmk_hid so the keyboard follows the theme": "ryoku doctor installs qmk_hid so the keyboard follows the theme", "ryoku doctor installs shelly-flatpak-backend": "ryoku doctor installs shelly-flatpak-backend", "ryoku doctor installs the repository build, which is rebuilt with Qt": "ryoku doctor installs the repository build, which is rebuilt with Qt", "ryoku doctor matches the Nomarchy command surface to the selected bar style": "ryoku doctor matches the Nomarchy command surface to the selected bar style", - "ryoku doctor merges them into shell.json and removes the retired keys": "ryoku doctor merges them into shell.json and removes the retired keys", "ryoku doctor migrates default_entry to the kernel entry path and preserves remember_last_entry": "ryoku doctor migrates default_entry to the kernel entry path and preserves remember_last_entry", "ryoku doctor migrates it in place": "ryoku doctor migrates it in place", "ryoku doctor migrates it to desktop.json": "ryoku doctor migrates it to desktop.json", @@ -14238,7 +14092,6 @@ "ryoku-gpu check-pin answered %q, which this ryoku version does not understand; update ryoku or run `ryoku-gpu status`": "ryoku-gpu check-pin answered %q, which this ryoku version does not understand; update ryoku or run `ryoku-gpu status`", "ryoku-gpu is not available to audit the render pin (%v)": "ryoku-gpu is not available to audit the render pin (%v)", "ryoku-host svc --user enable %s": "ryoku-host svc --user enable %s", - "ryoku-host svc reconcile --user": "ryoku-host svc reconcile --user", "ryoku-hub, the settings backend": "ryoku-hub, the settings backend", "ryoku-keyring already installed, key trust in place": "ryoku-keyring already installed, key trust in place", "ryoku-keyring and the Ryoku release key %s are missing, so the Ryoku RPM repository cannot be read": "ryoku-keyring and the Ryoku release key %s are missing, so the Ryoku RPM repository cannot be read", @@ -14249,9 +14102,9 @@ "ryoku-keyring is not installed, so a new Ryoku release key cannot reach this machine": "ryoku-keyring is not installed, so a new Ryoku release key cannot reach this machine", "ryoku-keyring is not installed, so there are no revoked Ryoku keys to check": "ryoku-keyring is not installed, so there are no revoked Ryoku keys to check", "ryoku-keyring package installed": "ryoku-keyring package installed", + "ryoku-kit": "ryoku-kit", "ryoku-monitor not installed": "ryoku-monitor not installed", "ryoku-pkg-aur-add limine-mkinitcpio-hook, then ryoku doctor": "ryoku-pkg-aur-add limine-mkinitcpio-hook, then ryoku doctor", - "ryoku-pkg-aur-add limine-mkinitcpio-hook, then sudo ryoku doctor": "ryoku-pkg-aur-add limine-mkinitcpio-hook, then sudo ryoku doctor", "ryoku-pkg-aur-add pam-fprint-grosshack, then ryoku doctor": "ryoku-pkg-aur-add pam-fprint-grosshack, then ryoku doctor", "ryoku-rashin not installed": "ryoku-rashin not installed", "ryoku-rashin, the agent daemon": "ryoku-rashin, the agent daemon", @@ -14264,6 +14117,7 @@ "ryopin could not draw a pin": "ryopin could not draw a pin", "ryoshot could not draw its overlay": "ryoshot could not draw its overlay", "ryostage cache": "ryostage cache", + "ryostore": "ryostore", "ryostore cache is on the current path": "ryostore cache is on the current path", "ryostore cache location": "ryostore cache location", "ryotunes": "ryotunes", @@ -14330,12 +14184,10 @@ "seeded once by Ryoku, then yours": "seeded once by Ryoku, then yours", "seeded ~/%s": "seeded ~/%s", "seeding brand assets, wallpapers, decor art, and ~/.npmrc into %s": "seeding brand assets, wallpapers, decor art, and ~/.npmrc into %s", - "seeding keyboard layout %q variant %q options %q (from %s) into hypr/keyboard.lua": "seeding keyboard layout %q variant %q options %q (from %s) into hypr/keyboard.lua", "seeding the ryoku-console-keys initramfs hook (the desktop set did not install it)": "seeding the ryoku-console-keys initramfs hook (the desktop set did not install it)", "seeding the ryoku-gpu-trim initramfs hook (the desktop set did not install it)": "seeding the ryoku-gpu-trim initramfs hook (the desktop set did not install it)", "select": "select", "selected": "selected", - "send a chat message to fetch them": "send a chat message to fetch them", "sensor": "sensor", "separate password": "separate password", "service health could not be checked: %v": "service health could not be checked: %v", @@ -14361,7 +14213,6 @@ "sets layout be for Hyprland, the console (KEYMAP=be-latin1), and the SDDM login screen; turns the French toggle off": "sets layout be for Hyprland, the console (KEYMAP=be-latin1), and the SDDM login screen; turns the French toggle off", "sets layout fr for Hyprland, the console (KEYMAP=fr), and the SDDM login screen; turns the Belgian toggle off": "sets layout fr for Hyprland, the console (KEYMAP=fr), and the SDDM login screen; turns the Belgian toggle off", "setting Plymouth default theme 'ryoku'": "setting Plymouth default theme 'ryoku'", - "setting the console keymap to %s in /etc/vconsole.conf": "setting the console keymap to %s in /etc/vconsole.conf", "setting the current root aside: %w; nothing was changed": "setting the current root aside: %w; nothing was changed", "setting up /.snapshots: %v": "setting up /.snapshots: %v", "settings": "settings", @@ -14451,7 +14302,6 @@ "snapshot read access": "snapshot read access", "snapshots and rollback from the Limine boot menu": "snapshots and rollback from the Limine boot menu", "snapshots are readable without sudo": "snapshots are readable without sudo", - "snapshots are unavailable on Fedora; recover with DNF directly": "snapshots are unavailable on Fedora; recover with DNF directly", "snapshots in the boot menu": "snapshots in the boot menu", "snapshots were declined at install (/etc/ryoku/snapshots-disabled); delete the marker and run `ryoku doctor` to enable them": "snapshots were declined at install (/etc/ryoku/snapshots-disabled); delete the marker and run `ryoku doctor` to enable them", "snapshots: %d": "snapshots: %d", @@ -14493,7 +14343,6 @@ "start": "start", "start it in a terminal to see why: `ryoku-shell daemon`": "start it in a terminal to see why: `ryoku-shell daemon`", "start it with `ryoku-shell daemon` (Hyprland autostarts it at login)": "start it with `ryoku-shell daemon` (Hyprland autostarts it at login)", - "started asusd; the ASUS Aura keyboard is available in Appearance": "started asusd; the ASUS Aura keyboard is available in Appearance", "started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings": "started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings", "started fresh: blank %q keyring, old one backed up": "started fresh: blank %q keyring, old one backed up", "started ryoku-shell daemon but it did not come up": "started ryoku-shell daemon but it did not come up", @@ -14540,7 +14389,6 @@ "sudo limine-mkinitcpio (or: sudo mkinitcpio -P)": "sudo limine-mkinitcpio (or: sudo mkinitcpio -P)", "sudo limine-mkinitcpio || sudo mkinitcpio -P": "sudo limine-mkinitcpio || sudo mkinitcpio -P", "sudo limine-update, then ryoku doctor": "sudo limine-update, then ryoku doctor", - "sudo limine-update, then sudo ryoku doctor": "sudo limine-update, then sudo ryoku doctor", "sudo may ask for your password again below": "sudo may ask for your password again below", "sudo pacman -S --needed base-devel cmake git, then Settings > Plugins > Rebuild": "sudo pacman -S --needed base-devel cmake git, then Settings > Plugins > Rebuild", "sudo pacman -S quickshell takes the repository build, which is rebuilt with Qt": "sudo pacman -S quickshell takes the repository build, which is rebuilt with Qt", @@ -14549,7 +14397,6 @@ "sudo pacman -Syu brings Qt up to date, then the shell loads again": "sudo pacman -Syu brings Qt up to date, then the shell loads again", "sudo pacman -Syu quickshell": "sudo pacman -Syu quickshell", "sudo rm -f %s/*%s.conf": "sudo rm -f %s/*%s.conf", - "sudo ryoku doctor (or the next ryoku update)": "sudo ryoku doctor (or the next ryoku update)", "sudo ryoku-host svc --system enable %s": "sudo ryoku-host svc --system enable %s", "sudo ryoku-host svc --system enable --now %s": "sudo ryoku-host svc --system enable --now %s", "sudo ryoku-host svc --system enable ryoku-boot-guard.service && sudo install -d -m 0755 /var/lib/ryoku && sudo install -d -m 1777 /var/lib/ryoku/boot": "sudo ryoku-host svc --system enable ryoku-boot-guard.service && sudo install -d -m 0755 /var/lib/ryoku && sudo install -d -m 1777 /var/lib/ryoku/boot", @@ -14571,7 +14418,6 @@ "supported: %s": "supported: %s", "suppressevent": "suppressevent", "suspended, drawing no power": "suspended, drawing no power", - "svc reconcile --system must run as root": "svc reconcile --system must run as root", "svc reconcile --system must run as root unless --check is used": "svc reconcile --system must run as root unless --check is used", "swap is out of snapshots": "swap is out of snapshots", "swap kept out of snapshots": "swap kept out of snapshots", @@ -14589,7 +14435,6 @@ "symlinks are not allowed in a plugin (R2)": "symlinks are not allowed in a plugin (R2)", "synced the Snapshots submenu": "synced the Snapshots submenu", "system service health could not be checked without passwordless sudo; no failed user services": "system service health could not be checked without passwordless sudo; no failed user services", - "system summary card (launcher, RyoStore covers)": "system summary card (launcher, RyoStore covers)", "system: %d package(s) waiting (sudo %s upgrade -y --refresh)": "system: %d package(s) waiting (sudo %s upgrade -y --refresh)", "system: %d package(s) waiting (sudo pacman -Syu)": "system: %d package(s) waiting (sudo pacman -Syu)", "system: %d package(s) waiting (sudo xbps-install -Syu)": "system: %d package(s) waiting (sudo xbps-install -Syu)", @@ -14610,7 +14455,6 @@ "task left": "task left", "tasks left": "tasks left", "terminal": "terminal", - "terminal file manager": "terminal file manager", "text": "text", "that account name is already in use": "that account name is already in use", "that name is reserved by the system": "that name is reserved by the system", @@ -14623,8 +14467,6 @@ "the %s package": "the %s package", "the AI usage collector service is off, so the bar AI pill goes stale": "the AI usage collector service is off, so the bar AI pill goes stale", "the AI usage collector timer is off, so the bar AI pill goes stale": "the AI usage collector timer is off, so the bar AI pill goes stale", - "the CPU, GPU, and battery panels' monitor button opens btop in the terminal": "the CPU, GPU, and battery panels' monitor button opens btop in the terminal", - "the Cobalt engine the file stash downloads through": "the Cobalt engine the file stash downloads through", "the GPU render pin is a deliberate RYOKU_GPU_FORCE override; kept": "the GPU render pin is a deliberate RYOKU_GPU_FORCE override; kept", "the Hyprland GPU render pin matches ryoku-gpu policy": "the Hyprland GPU render pin matches ryoku-gpu policy", "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, rebuild the initramfs (`sudo mkinitcpio -P`), and reboot.": "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, rebuild the initramfs (`sudo mkinitcpio -P`), and reboot.", @@ -14651,8 +14493,6 @@ "the Ryoku XBPS repository points at %s, which Ryoku does not publish; releases will not arrive from it": "the Ryoku XBPS repository points at %s, which Ryoku does not publish; releases will not arrive from it", "the Ryoku desktop is installed; reboot to use it": "the Ryoku desktop is installed; reboot to use it", "the Ryoku desktop variant did not land for %s": "the Ryoku desktop variant did not land for %s", - "the Ryoku editor role: the launcher's editor opens 'kitty -e nvim'": "the Ryoku editor role: the launcher's editor opens 'kitty -e nvim'", - "the Ryoku file-manager role: the launcher's Files, the stash, and the wallpaper picker open it": "the Ryoku file-manager role: the launcher's Files, the stash, and the wallpaper picker open it", "the Ryoku package repository points at %s, a mirror Ryoku does not publish; edit its package-manager configuration by hand": "the Ryoku package repository points at %s, a mirror Ryoku does not publish; edit its package-manager configuration by hand", "the Ryoku package upgrade failed; %s: %w": "the Ryoku package upgrade failed; %s: %w", "the Ryoku pacman channel check": "the Ryoku pacman channel check", @@ -14661,9 +14501,6 @@ "the Ryoku release key %s is missing, so the Ryoku RPM repository cannot be read": "the Ryoku release key %s is missing, so the Ryoku RPM repository cannot be read", "the Ryoku release key %s no longer matches the one ryoku-keyring installed": "the Ryoku release key %s no longer matches the one ryoku-keyring installed", "the Ryoku set (its packages and config) to a published release, live;": "the Ryoku set (its packages and config) to a published release, live;", - "the Ryoku shell prompt": "the Ryoku shell prompt", - "the Ryoku stash actions in the Files right-click menu (ships with Files)": "the Ryoku stash actions in the Files right-click menu (ships with Files)", - "the Ryoku terminal role: the launcher's terminal, the Hub, and 'open in terminal' all run it": "the Ryoku terminal role: the launcher's terminal, the Hub, and 'open in terminal' all run it", "the Ryostore copy of the Ryoku Discord theme gave way to the one Ryoku ships": "the Ryostore copy of the Ryoku Discord theme gave way to the one Ryoku ships", "the Ryostore copy of the Ryoku Discord theme would give way to the one Ryoku ships": "the Ryostore copy of the Ryoku Discord theme would give way to the one Ryoku ships", "the SDDM PAM stack %s pam_gnome_keyring but mode is %s": "the SDDM PAM stack %s pam_gnome_keyring but mode is %s", @@ -14717,7 +14554,6 @@ "the effect Ryoku found": "the effect Ryoku found", "the flathub remote is configured": "the flathub remote is configured", "the flathub remote is missing": "the flathub remote is missing", - "the gh CLI and the lazygit terminal UI": "the gh CLI and the lazygit terminal UI", "the in-session lockscreen is missing and no bundle is available to install it; the lock button and lock-on-sleep do nothing": "the in-session lockscreen is missing and no bundle is available to install it; the lock button and lock-on-sleep do nothing", "the in-session lockscreen is missing; the lock button and lock-on-sleep do nothing": "the in-session lockscreen is missing; the lock button and lock-on-sleep do nothing", "the initramfs GPU trim hook is not installed yet": "the initramfs GPU trim hook is not installed yet", @@ -14734,6 +14570,7 @@ "the internal panel is driven by the integrated GPU and the discrete GPU can runtime-suspend": "the internal panel is driven by the integrated GPU and the discrete GPU can runtime-suspend", "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and is drawing %.1f W, running hot at idle instead of powering down": "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and is drawing %.1f W, running hot at idle instead of powering down", "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and stays awake burning power and running hot at idle instead of powering down": "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and stays awake burning power and running hot at idle instead of powering down", + "the kernel killed %d process(es) for memory this boot": "the kernel killed %d process(es) for memory this boot", "the keyboard layout %q and style %q are saved as names, not the xkb codes %q and %q the compositor needs": "the keyboard layout %q and style %q are saved as names, not the xkb codes %q and %q the compositor needs", "the keyboard layout %q is not an xkb code and no name in the rules base matches it, so it was left unchanged": "the keyboard layout %q is not an xkb code and no name in the rules base matches it, so it was left unchanged", "the keyboard layout and style were saved as names %q and %q; set them to the xkb codes %q and %q": "the keyboard layout and style were saved as names %q and %q; set them to the xkb codes %q and %q", @@ -14746,8 +14583,6 @@ "the live Arch image now, and this installer turns it into": "the live Arch image now, and this installer turns it into", "the live Void image now, and this installer turns it into": "the live Void image now, and this installer turns it into", "the loaded and installed NVIDIA driver versions match (%s)": "the loaded and installed NVIDIA driver versions match (%s)", - "the mixer the bar's audio button opens": "the mixer the bar's audio button opens", - "the nested gaming micro-compositor": "the nested gaming micro-compositor", "the offline install could not lay the base system from the ISO": "the offline install could not lay the base system from the ISO", "the offline install laid the base system but could not install the Ryoku desktop set (%s) from the ISO": "the offline install laid the base system but could not install the Ryoku desktop set (%s) from the ISO", "the offline install stopped on %s, which fails its recorded checksum. Every package lives on the disc, so the copy that reached the installer is bad: the ISO download or the USB write, most likely. Check the ISO checksum against the release page, rewrite the install medium, and run the installer again. If the checksum already matches, report it with /var/log/ryoku-install.log.": "the offline install stopped on %s, which fails its recorded checksum. Every package lives on the disc, so the copy that reached the installer is bad: the ISO download or the USB write, most likely. Check the ISO checksum against the release page, rewrite the install medium, and run the installer again. If the checksum already matches, report it with /var/log/ryoku-install.log.", @@ -14758,10 +14593,8 @@ "the pacman database lock check": "the pacman database lock check", "the pacman keyring is still empty after init + populate; package signatures cannot be verified. The live image's archlinux-keyring is broken; re-download or rewrite the ISO.": "the pacman keyring is still empty after init + populate; package signatures cannot be verified. The live image's archlinux-keyring is broken; re-download or rewrite the ISO.", "the password was not accepted three times": "the password was not accepted three times", - "the performance governor Steam invokes": "the performance governor Steam invokes", "the plugin tree is %d MB; keep it under 20 MB": "the plugin tree is %d MB; keep it under 20 MB", "the port did not answer": "the port did not answer", - "the portable app channel (the stash installs .flatpak bundles)": "the portable app channel (the stash installs .flatpak bundles)", "the portal frontend predates this login session; screen share can silently share nothing": "the portal frontend predates this login session; screen share can silently share nothing", "the portal frontend predates this login session; screen share opens no source picker and silently shares nothing": "the portal frontend predates this login session; screen share opens no source picker and silently shares nothing", "the proprietary NVIDIA driver is loaded; Ryoku leaves its setup on Fedora to you": "the proprietary NVIDIA driver is loaded; Ryoku leaves its setup on Fedora to you", @@ -14790,6 +14623,7 @@ "the session exported %q but the account login shell is %q; pointed the override at %q and refreshed the running session": "the session exported %q but the account login shell is %q; pointed the override at %q and refreshed the running session", "the shared ESP %s has filesystem errors fsck.fat could not repair (rc=%s); Ryoku refuses to write into it. Boot the OS that owns the ESP and repair the volume, or install with RYOKU_ESP_MODE=dedicated so Ryoku keeps to its own ESP.": "the shared ESP %s has filesystem errors fsck.fat could not repair (rc=%s); Ryoku refuses to write into it. Boot the OS that owns the ESP and repair the volume, or install with RYOKU_ESP_MODE=dedicated so Ryoku keeps to its own ESP.", "the shared ESP has %s KiB free; shared mode needs >= 8 MiB. Use dedicated mode or free space on the ESP.": "the shared ESP has %s KiB free; shared mode needs >= 8 MiB. Use dedicated mode or free space on the ESP.", + "the shell service recorded %d memory kill(s)": "the shell service recorded %d memory kill(s)", "the shipped limine snapshot-sync defaults are incomplete": "the shipped limine snapshot-sync defaults are incomplete", "the sign-in screen": "the sign-in screen", "the signed Ryoku RPM repository is missing and could not be restored: %v": "the signed Ryoku RPM repository is missing and could not be restored: %v", @@ -14814,7 +14648,6 @@ "the window width cycle held only %q, which the stepping key cannot use; set it to %s": "the window width cycle held only %q, which the stepping key cannot use; set it to %s", "the window width cycle holds only %q, so the key that steps a window through it has nowhere to go": "the window width cycle holds only %q, so the key that steps a window through it has nowhere to go", "the window width cycle steps through %d widths": "the window width cycle steps through %d widths", - "the windowed VM behind Ryoku Settings > GPU > Machine": "the windowed VM behind Ryoku Settings > GPU > Machine", "the wireless regulatory domain is set to %s (%s)": "the wireless regulatory domain is set to %s (%s)", "the wireless regulatory domain is unset (00) and ryoku-wifi-regdom is not installed to set it from the locale country %s": "the wireless regulatory domain is unset (00) and ryoku-wifi-regdom is not installed to set it from the locale country %s", "the wireless regulatory domain is unset (00), so the kernel keeps 5 GHz channels disabled, and no country could be inferred from the system locale to set one": "the wireless regulatory domain is unset (00), so the kernel keeps 5 GHz channels disabled, and no country could be inferred from the system locale to set one", @@ -14961,24 +14794,19 @@ "usage: ryoku security-key remove ": "usage: ryoku security-key remove ", "usage: ryoku security-key set |mode |touch-required |pin-verification |user-verification ": "usage: ryoku security-key set |mode |touch-required |pin-verification |user-verification ", "usage: ryoku security-key status [--json]": "usage: ryoku security-key status [--json]", - "usage: ryoku track >": "usage: ryoku track >", "usage: ryoku track unstable": "usage: ryoku track unstable", "usage: ryoku-install probe alongside ": "usage: ryoku-install probe alongside ", "usage: ryoku-install probe resize ": "usage: ryoku-install probe resize ", "use": "use", "use a local ryoku-arch checkout as the payload": "use a local ryoku-arch checkout as the payload", "used": "used", - "used at login; change it anytime · esc back": "used at login; change it anytime · esc back", - "user": "user", "user and system-wide services match the activation manifest": "user and system-wide services match the activation manifest", + "user apps are still inside the shell service: %s": "user apps are still inside the shell service: %s", "user edits overlay": "user edits overlay", "user or system-wide service activation needs reconciliation": "user or system-wide service activation needs reconciliation", "user service activation": "user service activation", - "user service activation needs reconciliation": "user service activation needs reconciliation", - "user services match the activation manifest": "user services match the activation manifest", "user verification required": "user verification required", "user verify: %s": "user verify: %s", - "user: %s (wheel,video,input; shell %s)": "user: %s (wheel,video,input; shell %s)", "user@pam!id=secret": "user@pam!id=secret", "users caps does not accept arguments.": "users caps does not accept arguments.", "users list does not accept arguments.": "users list does not accept arguments.", @@ -15041,7 +14869,6 @@ "warning: could not set %s as the default browser (continuing)": "warning: could not set %s as the default browser (continuing)", "warning: could not set %s as the default file manager (continuing)": "warning: could not set %s as the default file manager (continuing)", "warning: could not stop %s; it is disabled for the next boot": "warning: could not stop %s; it is disabled for the next boot", - "warning: could not update browser role in %s (invalid JSON)": "warning: could not update browser role in %s (invalid JSON)", "warning: could not update browser role in %s: %s": "warning: could not update browser role in %s: %s", "warning: could not update file manager role in %s: %s": "warning: could not update file manager role in %s: %s", "warning: couldn't record security-key policy: %v": "warning: couldn't record security-key policy: %v", @@ -15092,7 +14919,6 @@ "wire PAM: %w": "wire PAM: %w", "wired the ryoku agent skill": "wired the ryoku agent skill", "wired to the vault": "wired to the vault", - "wireless Xbox pads + controller access rules": "wireless Xbox pads + controller access rules", "wireless regulatory domain": "wireless regulatory domain", "with Wine and umu-launcher.": "with Wine and umu-launcher.", "wm": "wm", @@ -15111,7 +14937,6 @@ "writes outside the plugin state dir (R8)": "writes outside the plugin state dir (R8)", "writes under /etc; write only under stateDir (R8)": "writes under /etc; write only under stateDir (R8)", "writing %s: %v": "writing %s: %v", - "writing /etc/conf.d/snapper: %v": "writing /etc/conf.d/snapper: %v", "writing /etc/fstab": "writing /etc/fstab", "writing /etc/snapper/configs/root: %v": "writing /etc/snapper/configs/root: %v", "writing report: %w": "writing report: %w", @@ -15156,7 +14981,6 @@ "· building the speech runtime…": "· building the speech runtime…", "· downloaded": "· downloaded", "· downloading the model…": "· downloading the model…", - "· downloading…": "· downloading…", "· fetching the source…": "· fetching the source…", "· installing…": "· installing…", "· not downloaded": "· not downloaded", diff --git a/ryoku/i18n/catalog/es.json b/ryoku/i18n/catalog/es.json index d84af088e7..ffc4bedf3c 100644 --- a/ryoku/i18n/catalog/es.json +++ b/ryoku/i18n/catalog/es.json @@ -9,45 +9,116 @@ "#1e1e2e, #89b4fa, #f5c2e7": "#1e1e2e, #89b4fa, #f5c2e7", "#RRGGBB": "#RRGGBB", "#rrggbb": "#rrggbb", + "% GPU": "% GPU", + "%1 · set aside for your music": "%1 · set aside for your music", + "%1 · tuning in - a few quiet seconds is normal": "%1 · tuning in - a few quiet seconds is normal", + "%1 (+WG)": "%1 (+WG)", "%1 (driver not loaded)": "%1 (controlador no cargado)", "%1 (restart to use it)": "%1 (reiniciar para usarlo)", "%1 (this one)": "%1 (este)", "%1 / %2 GB": "%1 / %2 ES", + "%1 / %2 GiB": "%1 / %2 GiB", + "%1 / %2 SELECTED": "%1 / %2 SELECTED", + "%1 / %2 resolved": "%1 / %2 resolved", + "%1 / %2 wired": "%1 / %2 wired", + "%1 ADD-ON": "%1 ADD-ON", + "%1 ADD-ONS": "%1 ADD-ONS", + "%1 APP": "%1 APP", + "%1 APPEARANCE": "%1 APPEARANCE", + "%1 APPS": "%1 APPS", + "%1 ATTENTION": "%1 ATTENTION", + "%1 BIND": "%1 BIND", + "%1 BINDS": "%1 BINDS", + "%1 BUNDLE": "%1 BUNDLE", + "%1 BUNDLES": "%1 BUNDLES", + "%1 CHANGE · PREVIEWING · NOT SAVED": "%1 CHANGE · PREVIEWING · NOT SAVED", + "%1 CHANGES · PREVIEWING · NOT SAVED": "%1 CHANGES · PREVIEWING · NOT SAVED", + "%1 DISPLAYS DETECTED": "%1 DISPLAYS DETECTED", + "%1 ENTRIES": "%1 ENTRIES", + "%1 ENTRY": "%1 ENTRY", + "%1 EXPLICIT · %2 AUR · %3 TOTAL": "%1 EXPLICIT · %2 AUR · %3 TOTAL", + "%1 EXPLICIT · %2 TOTAL": "%1 EXPLICIT · %2 TOTAL", + "%1 GB/s": "%1 GB/s", + "%1 GHz": "%1 GHz", + "%1 GiB free": "%1 GiB free", "%1 Hz band": "Banda %1Hz", + "%1 KB": "%1 KB", + "%1 KB/s": "%1 KB/s", + "%1 MB": "%1 MB", + "%1 MB/s": "%1 MB/s", + "%1 PLUGIN RUNNING": "%1 PLUGIN RUNNING", + "%1 PLUGINS RUNNING": "%1 PLUGINS RUNNING", + "%1 PREVIEW": "%1 PREVIEW", + "%1 Preserved as native config: %2": "%1 Preserved as native config: %2", + "%1 RESULT": "%1 RESULT", + "%1 RESULTS": "%1 RESULTS", + "%1 RULE": "%1 RULE", + "%1 RULES": "%1 RULES", "%1 TESTS": "%1 PRUEBAS", + "%1 TiB free": "%1 TiB free", + "%1 UPDATES READY": "%1 UPDATES READY", "%1 USB devices connected": "%1 Dispositivos USB conectados", "%1 USB devices disconnected": "%1 Dispositivos USB desconectados", + "%1 active assignment": "%1 active assignment", + "%1 active assignments": "%1 active assignments", "%1 active on this display": "%1 activo en esta pantalla", + "%1 add-on": "%1 add-on", + "%1 add-ons": "%1 add-ons", + "%1 adjustable, %2 changed": "%1 adjustable, %2 changed", "%1 alert": "%1 alerta", + "%1 and %2 both use %3. The first one wins; rebind one of them.": "%1 and %2 both use %3. The first one wins; rebind one of them.", + "%1 app": "%1 app", + "%1 apps": "%1 apps", "%1 apps - favourites first": "%1 aplicaciones: favoritos primero", "%1 background apps": "%1 aplicaciones en segundo plano", "%1 blocks": "%1 bloques", "%1 blocks, in your order": "%1 bloques, en su orden", + "%1 bundle": "%1 bundle", + "%1 bundles": "%1 bundles", "%1 choices": "%1 opciones", "%1 cities": "%1 ciudades", "%1 colors": "%1 colores", "%1 columns": "%1 columnas", + "%1 commit": "%1 commit", + "%1 commit behind · checked %2": "%1 commit behind · checked %2", + "%1 commits": "%1 commits", + "%1 commits behind · checked %2": "%1 commits behind · checked %2", + "%1 component": "%1 component", + "%1 components": "%1 components", + "%1 conflicting shortcut": "%1 conflicting shortcut", + "%1 conflicting shortcuts": "%1 conflicting shortcuts", + "%1 connected": "%1 connected", + "%1 connected · %2 known": "%1 connected · %2 known", "%1 contacts - favourites first": "%1 contactos - favoritos primero", "%1 could not be mirrored": "%1 no se pudo reflejar", "%1 d ago": "Hace %1 d", "%1 dB": "%1dB", "%1 days": "%1 días", + "%1 decors": "%1 decors", + "%1 devices connected": "%1 devices connected", "%1 devices connected · %2 following the palette": "%1 dispositivos conectados · %2 siguiendo la paleta", "%1 didn't answer. It may be down; try again in a while.": "%1 no respondió. Puede que esté caído; Inténtalo de nuevo dentro de un rato.", "%1 didn't open": "%1 no abrió", "%1 disconnected": "%1 desconectado", "%1 done today": "%1 hecho hoy", "%1 early": "%1 temprano", + "%1 events": "%1 events", "%1 failed its checks. Update stays on the last checked commit.": "%1 no pasó sus controles. La actualización permanece en la última confirmación marcada.", + "%1 failed services": "%1 failed services", "%1 file(s) failed": "%1 archivo(s) falló", + "%1 files": "%1 files", "%1 files backed up.": "%1 archivos respaldados.", "%1 files waiting · pick a device": "%1 archivos en espera · elige un dispositivo", + "%1 finger enrolled": "%1 finger enrolled", + "%1 fingers enrolled": "%1 fingers enrolled", "%1 finished": "%1 terminado", "%1 focus sessions today": "%1 sesiones de enfoque hoy", "%1 for today": "%1 por hoy", "%1 free · %2": "%1 gratis · %2", "%1 glasses": "%1 vasos", + "%1 grains/frame · %2": "%1 grains/frame · %2", "%1 h %2 min": "%1 horas %2 minutos", + "%1 has printed nothing and used no CPU. It may be waiting on something that will not come.": "%1 has printed nothing and used no CPU. It may be waiting on something that will not come.", "%1 hidden": "%1 oculto", "%1 hr ago": "Hace %1 horas", "%1 icons": "%1 iconos", @@ -55,100 +126,294 @@ "%1 images converted": "%1 imágenes convertidas", "%1 in": "%1 en", "%1 in history": "%1 en la historia", + "%1 in this family. (template — dynamic count of shaders in the selected family)": "%1 in this family. (template - dynamic count of shaders in the selected family)", + "%1 installed": "%1 installed", + "%1 installed pieces, %2 ready to update.": "%1 installed pieces, %2 ready to update.", + "%1 installed pieces, all current.": "%1 installed pieces, all current.", + "%1 installed, %2 updates ready": "%1 installed, %2 updates ready", + "%1 installed, 1 update ready": "%1 installed, 1 update ready", "%1 is back to how Shima comes": "%1 vuelve a como viene Shima", "%1 is empty": "%1 está vacío", + "%1 is ready": "%1 is ready", + "%1 is set aside": "%1 is set aside", "%1 is still being checked. Update deploys the last checked commit.": "%1 todavía se está comprobando. La actualización implementa la última confirmación marcada.", "%1 is temporarily unavailable (HTTP %2).": "%1 no está disponible temporalmente (HTTP %2).", + "%1 is the default microphone": "%1 is the default microphone", + "%1 is the default output": "%1 is the default output", + "%1 is tuning in": "%1 is tuning in", "%1 items": "%1 artículos", "%1 items on the dock besides apps": "%1 elementos en el Dock además de aplicaciones", "%1 items selected": "%1 elementos seleccionados", + "%1 items · text %2 · images %3": "%1 items · text %2 · images %3", "%1 keeps your place; what you watch shows up here": "%1 conserva tu lugar; lo que ves aparece aquí", + "%1 key enrolled": "%1 key enrolled", + "%1 keybinds carry over unchanged (compositor-neutral).": "%1 keybinds carry over unchanged (compositor-neutral).", + "%1 keybinds ingested into the GUI": "%1 keybinds ingested into the GUI", + "%1 keybinds, %2 rules, %3 unbinds": "%1 keybinds, %2 rules, %3 unbinds", + "%1 keys enrolled": "%1 keys enrolled", + "%1 known": "%1 known", "%1 laps": "%1 vueltas", "%1 late": "%1 tarde", "%1 left": "%1 izquierda", "%1 left · %2 done today": "%1 izquierda · %2 hecho hoy", + "%1 machines · %2 remotes": "%1 machines · %2 remotes", + "%1 match": "%1 match", + "%1 matches": "%1 matches", "%1 min": "%1 minutos", "%1 min ago": "Hace %1 minutos", "%1 min before": "%1 minutos antes", "%1 minute timer": "Temporizador de %1 minutos", + "%1 minutes ago": "%1 minutes ago", + "%1 missing": "%1 missing", "%1 ml": "%1ml", "%1 more — keep typing to narrow the results": "%1 más: sigue escribiendo para limitar los resultados", + "%1 need a password": "%1 need a password", "%1 new": "%1 nuevo", "%1 not connected": "%1 no conectado", + "%1 notification": "%1 notification", + "%1 notifications": "%1 notifications", "%1 of %2": "%1 de %2", "%1 of %2 glasses · %3 ml": "%1 de %2 vasos · %3 ml", + "%1 of %2 required installed": "%1 of %2 required installed", + "%1 on %2": "%1 on %2", + "%1 on the default": "%1 on the default", + "%1 only": "%1 only", "%1 open": "%1 abierto", "%1 open tasks": "%1 tareas abiertas", + "%1 output(s) · %2 with audio · %3 playing": "%1 output(s) · %2 with audio · %3 playing", + "%1 package": "%1 package", + "%1 packages": "%1 packages", "%1 packages waiting": "%1 paquetes esperando", "%1 past bedtime": "%1 pasada la hora de acostarse", "%1 past your bedtime.": "%1 pasada la hora de acostarse.", "%1 picks what to resume": "%1 elige qué reanudar", + "%1 pieces": "%1 pieces", "%1 pieces built in %2": "%1 piezas construidas en %2", "%1 pieces on screen. Tap one to add it or take it away; carry it on screen to place it.": "%1 piezas en pantalla. Toca uno para agregarlo o quitarlo; llévalo a la pantalla para colocarlo.", "%1 pinned": "%1 fijado", + "%1 placed": "%1 placed", "%1 playing": "%1 jugando", + "%1 playing · %2 with audio": "%1 playing · %2 with audio", "%1 px · %2 pages, %3 shortcuts": "%1 px · %2 páginas, %3 atajos", + "%1 queued…": "%1 queued…", "%1 quotas": "%1 cuotas", "%1 remaining": "%1 restante", "%1 reminders": "%1 recordatorios", + "%1 renders here": "%1 renders here", "%1 request failed (HTTP %2).": "%1 solicitud fallida (HTTP %2).", + "%1 required component missing": "%1 required component missing", + "%1 required components missing": "%1 required components missing", + "%1 results": "%1 results", "%1 running": "%1 corriendo", "%1 s": "%1 s", + "%1 saved · none active": "%1 saved · none active", + "%1 seconds ago": "%1 seconds ago", + "%1 selected": "%1 selected", "%1 services ready": "%1 servicios listos", + "%1 settings layer on top and win": "%1 settings layer on top and win", "%1 sites": "%1 sitios", "%1 slots": "%1 ranuras", "%1 slots wide": "%1 ranuras de ancho", "%1 starred": "%1 destacado", "%1 starred · %2 in history": "%1 protagonizado · %2 en la historia", + "%1 tags chosen": "%1 tags chosen", "%1 tasks left": "%1 tareas restantes", "%1 timer": "%1 temporizador", "%1 timers": "%1 temporizadores", + "%1 times": "%1 times", "%1 to go": "%1 ir", + "%1 tokens": "%1 tokens", "%1 took longer than %2 minutes and was stopped. Try a lighter model.": "%1 tardó más de %2 minutos y se detuvo. Prueba con un modelo más ligero.", "%1 tools": "%1 herramientas", + "%1 unbinds added so your shortcuts win": "%1 unbinds added so your shortcuts win", "%1 usage unavailable": "%1 uso no disponible", + "%1 volume": "%1 volume", + "%1 wallpaper": "%1 wallpaper", + "%1 wallpapers": "%1 wallpapers", + "%1 wallpapers ready": "%1 wallpapers ready", "%1 wants to connect": "%1 quiere conectarse", "%1 was sent to %2": "%1 fue enviado a %2", "%1 widgets on this screen": "%1 widgets en esta pantalla", "%1 widgets, turned with the wheel": "%1 widgets, girados con la rueda", + "%1 window rules ingested": "%1 window rules ingested", + "%1 windows": "%1 windows", "%1 words": "%1 palabras", "%1 workspaces · %2 windows": "%1 espacios de trabajo · %2 ventanas", "%1 | Right-click to configure": "%1 | Haga clic derecho para configurar", + "%1 °C": "%1 °C", + "%1 · %2 GHz · %3%": "%1 · %2 GHz · %3%", "%1 · %2 in progress": "%1 · %2 en curso", + "%1 · %2 left": "%1 · %2 left", + "%1 · %2 to full": "%1 · %2 to full", "%1 · %2 today": "%1 · %2 hoy", "%1 · %2 used of %3 · %4 free": "%1 · %2 usado de %3 · %4 gratis", + "%1 · %2 °C": "%1 · %2 °C", + "%1 · EMPTY": "%1 · EMPTY", + "%1 · active on %2": "%1 · active on %2", "%1 · click to add": "%1 · haga clic para agregar", "%1 · newest: %2": "%1 · más nuevo: %2", + "%1 · on": "%1 · on", "%1 · on, click to remove": "%1 · activado, haga clic para eliminar", + "%1 · ready for display assignment": "%1 · ready for display assignment", "%1 · shaping this output": "%1 · dando forma a esta producción", "%1 · starting the audio graph": "%1 · iniciar el gráfico de audio", "%1 · week %2": "%1 · semana %2", "%1 — click again for other shapes · Shift snaps": "%1 - haga clic nuevamente para ver otras formas · Ajustes de mayúsculas", + "%1% ahead of pace": "%1% ahead of pace", + "%1% battery": "%1% battery", + "%1% behind pace": "%1% behind pace", + "%1% disk used": "%1% disk used", "%1% done": "%1% hecho", + "%1% illuminated": "%1% illuminated", + "%1% memory used": "%1% memory used", "%1% of the %2": "%1% del %2", + "%1% signal": "%1% signal", + "%1% used": "%1% used", + "%1% · %2 °C": "%1% · %2 °C", "%1, non-commercial licence": "%1, licencia no comercial", + "%1-second delay": "%1-second delay", + "%1d %2h": "%1d %2h", + "%1d ago": "%1d ago", + "%1h %2m": "%1h %2m", "%1h %2m left": "%1h %2me quedo", + "%1h ago": "%1h ago", "%1m %2s left": "%1m %2s restantes", + "%1m ago": "%1m ago", + "%1s delay before the shot": "%1s delay before the shot", "%1s left": "Quedan %1s", + "%1° limit": "%1° limit", + "%d 32-bit package(s) are installed but [multilib] is disabled, so `pacman -Syu` fails": "%d 32-bit package(s) are installed but [multilib] is disabled, so `pacman -Syu` fails", + "%d 32-bit package(s) update from [multilib]": "%d 32-bit package(s) update from [multilib]", + "%d EFI partitions found; Ryoku shares the %s ESP": "%d EFI partitions found; Ryoku shares the %s ESP", + "%d GiB free": "%d GiB free", + "%d GiB is already free": "%d GiB is already free", + "%d Ryoku package(s) are already newer than the [ryoku] repository serves; leaving them as they are": "%d Ryoku package(s) are already newer than the [ryoku] repository serves; leaving them as they are", + "%d Wi-Fi profile(s) are pinned to one access point and cannot roam: %s": "%d Wi-Fi profile(s) are pinned to one access point and cannot roam: %s", + "%d blocking audit finding(s); fix them or re-run with --allow-findings": "%d blocking audit finding(s); fix them or re-run with --allow-findings", + "%d blocking finding(s); fix them or re-run with --allow": "%d blocking finding(s); fix them or re-run with --allow", + "%d blocking, %d warnings": "%d blocking, %d warnings", + "%d check(s) failed": "%d check(s) failed", + "%d check(s) failed: %s": "%d check(s) failed: %s", + "%d decor art file(s) missing from %s": "%d decor art file(s) missing from %s", + "%d duplicate desktop instance(s) are running beside the live one (pid %d)": "%d duplicate desktop instance(s) are running beside the live one (pid %d)", + "%d enabled plugin(s) built for the installed compositor": "%d enabled plugin(s) built for the installed compositor", + "%d existing partition(s)": "%d existing partition(s)", + "%d leaked timeline snapshot(s) that number cleanup never reclaims": "%d leaked timeline snapshot(s) that number cleanup never reclaims", + "%d leftover partition(s) (%dG) from a failed prior install": "%d leftover partition(s) (%dG) from a failed prior install", + "%d of %d apps removed": "%d of %d apps removed", + "%d of %d packages already satisfied by installed providers": "%d of %d packages already satisfied by installed providers", + "%d orphaned package(s)": "%d orphaned package(s)", + "%d pending .pacnew are safe to resolve (identical to the live config, only the [ryoku] repo addition, a re-commented locale.gen, or a config never edited on this box)": "%d pending .pacnew are safe to resolve (identical to the live config, only the [ryoku] repo addition, a re-commented locale.gen, or a config never edited on this box)", + "%d pending RPM config update(s) need review: %s": "%d pending RPM config update(s) need review: %s", + "%d pending XBPS config update(s) need review: %s": "%d pending XBPS config update(s) need review: %s", + "%d pending config update(s) (.pacnew) need review": "%d pending config update(s) (.pacnew) need review", + "%d pending config update(s) (.pacnew) need review (%d %s)": "%d pending config update(s) (.pacnew) need review (%d %s)", "%d pieces built": "%d piezas construidas", "%d pieces · usually %s": "%d piezas · normalmente %s", + "%d recording(s) from another recorder sit in %s, outside %s": "%d recording(s) from another recorder sit in %s, outside %s", + "%d snapshots": "%d snapshots", + "%d step(s) already finished last time; keeps that run's backup dir and skips them (toggle off to redo everything)": "%d step(s) already finished last time; keeps that run's backup dir and skips them (toggle off to redo everything)", + "%d stream(s) playing off the default sink %s": "%d stream(s) playing off the default sink %s", + "%d system package(s) waiting from your distribution (kernel included): take them with `%s`": "%d system package(s) waiting from your distribution (kernel included): take them with `%s`", + "%d timeline snapshot(s) could not be deleted (retry: sudo snapper -c root cleanup number)": "%d timeline snapshot(s) could not be deleted (retry: sudo snapper -c root cleanup number)", + "%d unowned Ryoku file(s) block `pacman -Syu` (deploy-seeded): %s": "%d unowned Ryoku file(s) block `pacman -Syu` (deploy-seeded): %s", + "%d untagged Ryoku snapshot(s) that number cleanup never reclaims": "%d untagged Ryoku snapshot(s) that number cleanup never reclaims", + "%q is a reserved built-in widget id": "%q is a reserved built-in widget id", + "%q is a reserved built-in widget id; choose another": "%q is a reserved built-in widget id; choose another", + "%q keyring converted to blank": "%q keyring converted to blank", + "%q keyring is already blank": "%q keyring is already blank", + "%s %q does not exist in the plugin": "%s %q does not exist in the plugin", + "%s %q must be a relative path with no ..": "%s %q must be a relative path with no ..", + "%s %s at %s": "%s %s at %s", + "%s %s the shipped desktop": "%s %s the shipped desktop", + "%s %s · root %dG · swap %s": "%s %s · root %dG · swap %s", + "%s (%s) shrinks · the freed space becomes Ryoku": "%s (%s) shrinks · the freed space becomes Ryoku", + "%s (experimental)": "%s (experimental)", + "%s (from %s)": "%s (from %s)", + "%s (snapshots and new kernels appear in the boot menu from the next boot)": "%s (snapshots and new kernels appear in the boot menu from the next boot)", + "%s + %s (existing) entry in the boot menu": "%s + %s (existing) entry in the boot menu", + "%s / %s / %s": "%s / %s / %s", + "%s [y/N]": "%s [y/N]", + "%s already exists and is not empty": "%s already exists and is not empty", + "%s and install %s?": "%s and install %s?", + "%s asking %s to diagnose %s": "%s asking %s to diagnose %s", + "%s came back read-only after fsck.fat: the volume has errors the repair could not clear, back it up and recreate it": "%s came back read-only after fsck.fat: the volume has errors the repair could not clear, back it up and recreate it", + "%s choice %q is unavailable on %s: %s": "%s choice %q is unavailable on %s: %s", + "%s choice '%s' is unavailable on %s: %s": "%s choice '%s' is unavailable on %s: %s", + "%s config materialized in ~/.config/%s": "%s config materialized in ~/.config/%s", + "%s could not be installed": "%s could not be installed", + "%s diagnostic report written to %s": "%s diagnostic report written to %s", "%s does not list the root config (timers and hooks will skip it)": "%s no enumera la configuración raíz (los temporizadores y los ganchos la omitirán)", + "%s edition package table is missing: %s": "%s edition package table is missing: %s", "%s exceeds the supported byte count": "%s excede el recuento de bytes admitidos", "%s failed its checks (%s); staying on the last checked commit %s": "%s no pasó sus controles (%s); permanecer en el último compromiso marcado %s", + "%s has no %s anchor to wire pam_gnome_keyring after": "%s has no %s anchor to wire pam_gnome_keyring after", + "%s has no [multilib] stanza to enable; add it by hand": "%s has no [multilib] stanza to enable; add it by hand", + "%s has no [options] section to hold the progress-bar default": "%s has no [options] section to hold the progress-bar default", "%s has the Ryoku Discord theme in its own folder": "%s tiene el tema Ryoku Discord en su propia carpeta", + "%s in ~/.local/bin shadows the packaged app": "%s in ~/.local/bin shadows the packaged app", + "%s install %s? [y/N]": "%s install %s? [y/N]", + "%s is %s GiB; need at least 32 GiB": "%s is %s GiB; need at least 32 GiB", + "%s is already the compositor.": "%s is already the compositor.", + "%s is already your login shell": "%s is already your login shell", "%s is missing from the installed desktop package": "Falta %s en el paquete de escritorio instalado", + "%s is missing, so limine-snapper-sync cannot attach snapshots under %q": "%s is missing, so limine-snapper-sync cannot attach snapshots under %q", + "%s is not an export (run `ryoku plugin export %s` first)": "%s is not an export (run `ryoku plugin export %s` first)", + "%s is not applicable on this host": "%s is not applicable on this host", + "%s is not installed": "%s is not installed", "%s is not mounted from its own top-level btrfs subvolume; snapshots are not bootable (ryoku doctor moves it there)": "%s no se monta desde su propio subvolumen btrfs de nivel superior; las instantáneas no son de arranque (ryoku doctor las mueve allí)", + "%s is not writable (run via pkexec/root)": "%s is not writable (run via pkexec/root)", + "%s is on %q, not the tracked channel %q, and has uncommitted changes, so its branch was left as-is": "%s is on %q, not the tracked channel %q, and has uncommitted changes, so its branch was left as-is", + "%s is read-only and busy, so it could not be unmounted for a repair; close what holds it (or reboot) and run `sudo fsck.fat -a %s`, then `sudo mount %s`": "%s is read-only and busy, so it could not be unmounted for a repair; close what holds it (or reboot) and run `sudo fsck.fat -a %s`, then `sudo mount %s`", + "%s is read-only and not a FAT volume; check it by hand": "%s is read-only and not a FAT volume; check it by hand", + "%s is ready. Log out and pick %s at the greeter.": "%s is ready. Log out and pick %s at the greeter.", + "%s is retired; remove it": "%s is retired; remove it", + "%s is unavailable: %s": "%s is unavailable: %s", + "%s mounted but its state could not be re-read": "%s mounted but its state could not be re-read", + "%s mounted read-only; a repair would remount it read-write": "%s mounted read-only; a repair would remount it read-write", "%s must be 0 or 1": "%s debe ser 0 o 1", "%s must be a non-negative byte count": "%s debe ser un recuento de bytes no negativo", "%s names no root subvolume this can replace": "%s nombra ningún subvolumen raíz que pueda reemplazar", "%s needs a value.": "%s necesita un valor.", + "%s not present; nothing to wire": "%s not present; nothing to wire", "%s now uses the Ryoku Discord theme": "%s ahora usa el tema Ryoku Discord", + "%s of yours %s the shipped desktop": "%s of yours %s the shipped desktop", "%s or %s is missing from the btrfs top level; nothing was changed": "Falta %s o %s en el nivel superior de btrfs; nada fue cambiado", + "%s predates %s, so the disk passphrase prompt still uses the keymap baked in when it was built": "%s predates %s, so the disk passphrase prompt still uses the keymap baked in when it was built", + "%s reasons over the report with a cloud model.": "%s reasons over the report with a cloud model.", + "%s retired by %s stay installed (delete them by hand to reclaim)": "%s retired by %s stay installed (delete them by hand to reclaim)", + "%s routes portals away from the %s backend; apps stall ~25s at launch and screenshare breaks": "%s routes portals away from the %s backend; apps stall ~25s at launch and screenshare breaks", + "%s says this is a %q keyboard but the desktop is still on us": "%s says this is a %q keyboard but the desktop is still on us", + "%s stay removed (you deleted them; Ryoku does not put them back)": "%s stay removed (you deleted them; Ryoku does not put them back)", + "%s stays installed and selectable at login": "%s stays installed and selectable at login", + "%s stays installed; pick it from the session menu at the login screen anytime": "%s stays installed; pick it from the session menu at the login screen anytime", "%s stops layering the palette bridge's Midnight under the Ryoku Discord theme": "%s deja de superponer la medianoche del puente de paleta bajo el tema Ryoku Discord", "%s switches to the Ryoku Discord theme when it next starts": "%s cambia al tema de Ryoku Discord la próxima vez que se inicie", + "%s unavailable: %s": "%s unavailable: %s", + "%s was unmounted for a repair and would not mount again; run `sudo fsck.fat -a %s` and `sudo mount %s`": "%s was unmounted for a repair and would not mount again; run `sudo fsck.fat -a %s` and `sudo mount %s`", + "%s wayland session registered": "%s wayland session registered", "%s would get the Ryoku Discord theme in its own folder": "%s obtendría el tema de Ryoku Discord en su propia carpeta", "%s would stop layering the palette bridge's Midnight under the Ryoku Discord theme": "%s dejaría de superponer la medianoche del puente de paleta bajo el tema Ryoku Discord", "%s would switch to the Ryoku Discord theme": "%s cambiaría al tema Ryoku Discord", + "%s · %s · %s": "%s · %s · %s", + "%s · quiet %s": "%s · quiet %s", + "%s, so a failed update is not reverted automatically": "%s, so a failed update is not reverted automatically", + "%s, then Settings > Plugins > Rebuild": "%s, then Settings > Plugins > Rebuild", + "%s, then restart the shell": "%s, then restart the shell", + "%s, then ryoku doctor": "%s, then ryoku doctor", + "%s: missing header": "%s: missing header", + "%s:%d: %w": "%s:%d: %w", + "%s:%d: duplicate COPR project %q": "%s:%d: duplicate COPR project %q", + "%s:%d: duplicate service id %s": "%s:%d: duplicate service id %s", + "%s:%d: expected copr, includepkgs, why header": "%s:%d: expected copr, includepkgs, why header", + "%s:%d: expected three tab-separated columns": "%s:%d: expected three tab-separated columns", + "%s:%d: includepkgs and why are required": "%s:%d: includepkgs and why are required", + "%s:%d: invalid COPR project %q": "%s:%d: invalid COPR project %q", + "%s:%d: invalid package name %q": "%s:%d: invalid package name %q", + "%s:%d: want 6 tab-separated fields": "%s:%d: want 6 tab-separated fields", + "%s; run `ryoku update` to advance": "%s; run `ryoku update` to advance", + "%w\ngreeter and console are set; rerun `sudo mkinitcpio -P` to finish": "%w\ngreeter and console are set; rerun `sudo mkinitcpio -P` to finish", + "%w (is gnome-keyring-daemon running?)": "%w (is gnome-keyring-daemon running?)", "%w; putting the replaced root back failed as well (%v): it is %s at the btrfs top level": "%w; También falló al restablecer la raíz reemplazada (%v): está %s en el nivel superior de btrfs", "%w; the boot menu still lists \\\"Ryoku: restored system\\\" (%v), which now boots the current root until the next snapshot removes it": "%w; el menú de inicio todavía muestra \\\"Ryoku: sistema restaurado\\\" (%v), que ahora inicia la raíz actual hasta que la siguiente instantánea lo elimine", "%w; the current root is back": "%w; la raíz actual ha vuelto", @@ -176,7 +441,6 @@ "(uninstall)": "(desinstalar)", "(unnamed)": "(sin nombre)", "(user)": "(usuario)", - "(user, clearable)": "(usuario, borrable)", "); write only under stateDir (R8)": "); escribir solo bajo stateDir (R8)", "+ %d more": "+ %d más", "+ ADD": "+ AÑADIR", @@ -194,7 +458,6 @@ ", listed in the GRUB boot menu": ", listado en el menú de inicio de GRUB", ", listed in the Limine boot menu": ", listado en el menú de arranque Limine", ", then %s appears": ", luego aparece %s", - ", with the card open in %s": ", con la tarjeta abierta en %s", ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Notas para Zerochan:\n- Debes ingresar un color\n- Configure su nombre de usuario zerochan en la opción de configuración `sidebar.booru.zerochan.username`. Usted [podría ser baneado por no hacer entonces](https://www.zerochan.net/api#:~:text=La%20solicitud%20puede%20aún%20estar%20completada%20con éxito%2 0sin%20este%20encabezado%20personalizado%2C%20,pero%20su%20proyecto%2puede%2ser%20prohibido%20por%2ser%20anónimo.)!", "0 (Primary)": "0 (Principal)", "0.5x2 Android Pixel search bar with Google logo, inner search pill, and customizable filled action buttons.": "Barra de búsqueda de Android Pixel de 0,5x2 con el logotipo de Google, píldora de búsqueda interna y botones de acción rellenos personalizables.", @@ -223,7 +486,6 @@ "1 hour": "1 hour", "1 item": "1 artículo", "1 item on the dock besides apps": "1 elemento en el muelle además de las aplicaciones", - "1 layer ready": "1 layer ready", "1 min": "1 minuto", "1 minute": "1 minuto", "1 notification": "1 notificación", @@ -376,15 +638,12 @@ "": "", " (dynamic; fallback \\\"DISPLAY\\\")": " (dinámico; fallback \\\"DISPLAY\\\")", - "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` follows unstable builds again.": "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` follows unstable builds again.", "==> Already pinned to unstable build %s; moving the Ryoku set to what it serves. `ryoku track unstable` switches to the source checkout.": "==> Ya fijado a la compilación inestable %s; moviendo el conjunto Ryoku a lo que sirve. `ryoku track unstable` cambia al pago de la fuente.", "==> Already tracking %s; moving the Ryoku set to what it serves": "==> Ya se sigue %s; moviendo el conjunto Ryoku a lo que sirve", "==> Moving the Ryoku set to release %s": "==> Moviendo el conjunto Ryoku a la versión %s", "==> Moving the Ryoku set to unstable build %s": "==> Moving the Ryoku set to unstable build %s", "==> Now tracking stable packages: named releases as they are published.": "==> Ahora se siguen paquetes estables: versiones nombradas según se publican.", - "==> Now tracking unstable packages: rebuilt on every push. `ryoku track stable` returns to stable releases.": "==> Now tracking unstable packages: rebuilt on every push. `ryoku track stable` returns to stable releases.", "==> Pinned to release %s. `ryoku update` keeps this release; `ryoku track stable` follows releases again.": "==> Pinned to release %s. `ryoku update` keeps this release; `ryoku track stable` follows releases again.", - "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` follows unstable builds again.": "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` follows unstable builds again.", "==> Pinned to unstable build %s. `ryoku update` keeps this build; `ryoku track unstable` switches to the source checkout.": "==> Fijado a la compilación inestable %s. `ryoku update` mantiene esta compilación; `ryoku track unstable` cambia al pago de la fuente.", "==> Retired the source checkout as the update source; the ~/ryoku-arch clone stays on disk but no longer drives updates.": "==> Se retiró el checkout de fuente como origen de actualización; el clon ~/ryoku-arch permanece en disco pero ya no impulsa actualizaciones.", "==> Unstable is now the source checkout; `ryoku track unstable` switches this box to it. `ryoku track stable` returns to stable packages.": "==> Unstable es ahora el pago de origen; `ryoku track unstable` cambia este cuadro a él. `ryoku track stable` regresa a paquetes estables.", @@ -406,7 +665,6 @@ "A Material Symbol by name, or any picture in assets/icons; black-and-white ones take the theme's colours": "Un símbolo material por nombre o cualquier imagen en activos/iconos; los de blanco y negro toman los colores del tema", "A NAME=value exported into your session at login.": "Un NAME=value exportado a tu sesión al iniciar.", "A Pixel Watch-inspired concentric dial clock with modular complications and interactive customization.": "Un reloj con esfera concéntrica inspirado en Pixel Watch con complicaciones modulares y personalización interactiva.", - "A Ryoku effect is drawn live, so there is nothing to store in the device. One of its own effects can be saved, if it offers that.": "Un efecto Ryoku se dibuja en vivo, por lo que no hay nada que almacenar en el dispositivo. Uno de sus propios efectos puede guardarse, si lo ofrece.", "A Ryoku tool's own JSON/TOML state under ~/.config/ryoku, each with one\nwriter. Change them through their GUI or command, never by hand:": "A Ryoku tool's own JSON/TOML state under ~/.config/ryoku, each with one\nwriter. Change them through their GUI or command, never by hand:", "A Ryoku tool's store. Change it through its Ryoku Hub page or the owning command, not by hand.": "A Ryoku tool's store. Change it through its Ryoku Hub page or the owning command, not by hand.", "A Scallop clock face with outer 5-minute step numbers, inner 1-12 hour numbers, and a center date badge.": "Esfera de reloj festoneada con números de pasos exteriores de 5 minutos, números interiores de 1 a 12 horas y una insignia de fecha central.", @@ -431,7 +689,6 @@ "A clear list.": "Una lista clara.", "A clear list. Room for what matters next.": "Una lista clara. Espacio para lo que importa a continuación.", "A clock island that grows with you.": "A clock island that grows with you.", - "A colour of your own, kept through wallpaper changes.": "Un color propio, mantenido a través de cambios de fondo.", "A coloured halo cast behind every window": "Un halo de color proyectado detrás de cada ventana", "A command is required.": "Se requiere un comando.", "A concentric circle clock featuring outer minute step numbers, an intermediate circle with a smooth minute pointer tab, and a central two-digit hour circle.": "Un reloj de círculo concéntrico con números de pasos de minutos exteriores, un círculo intermedio con una pestaña de puntero de minutos suave y un círculo de horas central de dos dígitos.", @@ -439,7 +696,6 @@ "A count of unread notifications; its card lists them.": "Un recuento de notificaciones no leídas; su tarjeta los enumera.", "A count on an app with unread notifications": "Un recuento de una aplicación con notificaciones no leídas", "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out %1/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.": "Una imagen personalizada se difumina en vivo. Para incorporar tu propio arte en esta galería:\n ryodither --fuera %1/ryodecors\n(necesita almohada; agregue --invertir para un sujeto oscuro sobre un fondo pálido). Ajuste manualmente cualquier campo en ~/.config/ryoku/profile.json. Guía completa: docs/profile.md.", - "A custom image is dithered live. To bake your own art into this gallery:\n ryodither --out ~/Pictures/ryodecors\n(needs Pillow; add --invert for a dark subject on a pale ground). Hand-tune any field in ~/.config/ryoku/profile.json. Full guide: docs/profile.md.": "Una imagen personalizada se difumina en vivo. Para incorporar su propio arte a esta galería:\n ryodither --out ~/Pictures/ryodecors\n(necesita Pillow; añada --invert para un sujeto oscuro sobre un fondo pálido). Ajuste manualmente cualquier campo en ~/.config/ryoku/profile.json. Guía completa: docs/profile.md.", "A download is running. Cancel it, or quit again to abandon it": "Una descarga está en curso. Cancélala o sal de nuevo para abandonarla", "A drawing is on screen": "Hay un dibujo en la pantalla.", "A fainter divider colour.": "A fainter divider colour.", @@ -454,6 +710,7 @@ "A key is saved. Enter a new one to replace it.": "Se ha guardado una clave. Introduzca una nueva para reemplazarla.", "A laptop or render-only dGPU has no display outputs, so the guest also needs a virtual-display driver for Looking Glass to have a frame to capture.": "Una portátil o dGPU de solo renderizado no tiene salidas de pantalla, por lo que el huésped necesita un controlador de pantalla virtual para que Looking Glass capture un cuadro.", "A limit is enforced only when this home supports filesystem quotas.": "Se aplica un límite solo cuando esta casa admite cuotas del sistema de archivos.", + "A liquid bar that moves with your music.": "A liquid bar that moves with your music.", "A little room to breathe": "Un pequeño espacio para respirar", "A live specimen - cracked, shot on black.": "Un espécimen vivo, agrietado, fotografiado en negro.", "A local Lens model pack available to Describe search.": "A local Lens model pack available to Describe search.", @@ -599,7 +856,6 @@ "APPLY": "APLICAR", "APPLY INSTALLED STYLE": "APLICAR ESTILO INSTALADO", "APPLYING": "APLICANDO", - "APPROVE IN CHAT": "APPROVE IN CHAT", "APPS": "APLICACIONES", "APPS & KEYS": "APLICACIONES Y TECLAS", "ARRANGEMENT": "ARREGLO", @@ -841,6 +1097,7 @@ "Align now": "Alinear ahora", "Align right": "Alinear a la derecha", "Align top": "Alinear arriba", + "Aligning %d newer package(s) from [ryoku]": "Aligning %d newer package(s) from [ryoku]", "Alignment": "Alignment", "All": "Todo", "All Day": "Todo el dia", @@ -849,7 +1106,7 @@ "All animates this shader on every updated monitor. Primary animates it once and changes the other monitors immediately.": "All animates this shader on every updated monitor. Primary animates it once and changes the other monitors immediately.", "All apps": "Todas las apps", "All bar settings": "Todas las configuraciones de la barra", - "All caught up": "todos atrapados", + "All caught up": "No hay notificaciones pendientes", "All clear": "Todo claro", "All day": "todo el dia", "All displays": "All displays", @@ -1065,8 +1322,6 @@ "Ask routes": "Preguntar rutas", "Ask something": "preguntar algo", "Ask the needle anything. ENTER sends; \\resume recalls recent asks.": "Preguntale lo que sea a la aguja. ENTER envia; \\resume recupera preguntas recientes.", - "Asking the lighting providers what is connected. The first scan can take a few seconds.": "Consultando dispositivos de iluminación. El primer escaneo puede tardar segundos.", - "Asks": "Asks", "Asks geoclue instead of guessing from your connection. Off unless you want it.": "Pregunta geoclue en lugar de adivinar tu conexión. Apagado a menos que lo desees.", "Assistant Icon": "Icono de asistente", "At": "en", @@ -1188,6 +1443,7 @@ "BRING": "TRAER", "BRING ICONS HERE": "TRAE ICONOS AQUÍ", "BROWSE": "EXAMINAR", + "BROWSE BUNDLES": "BROWSE BUNDLES", "BROWSE RYOSTORE": "EXPLORAR RYOSTORE", "BROWSE STORE": "EXPLORAR TIENDA", "BUILD AND ADD": "CONSTRUIR Y AÑADIR", @@ -1195,7 +1451,7 @@ "BUILDING": "CONSTRUYENDO", "BUILDING ·": "CONSTRUYENDO ·", "BUILDING…": "CONSTRUYENDO…", - "BUNDLES": "BUNDLES", + "BUNDLES WITH INSTALLED PARTS": "BUNDLES WITH INSTALLED PARTS", "BURN": "GRABAR", "BURNING": "GRABANDO", "Back": "Atras", @@ -1329,12 +1585,12 @@ "Behind": "Behind", "Behind full-screen reminder alerts": "Detrás de las alertas recordatorias en pantalla completa", "Bend strength": "Fuerza de flexión", + "Best": "Best", "Best (4K)": "Best (4K)", "Best downloads 4K when MotionBGs offers it and falls back to HD. Light always uses HD for smaller files.": "Best downloads 4K when MotionBGs offers it and falls back to HD. Light always uses HD for smaller files.", "Best downloads the 4K file and plays a copy sized to your screen; the HD files are heavily compressed. Light takes HD: a fifth of the download, visibly softer.": "Lo mejor es descargar el archivo 4K y reproducir una copia del tamaño de su pantalla; Los archivos HD están muy comprimidos. La luz toma HD: una quinta parte de la descarga, visiblemente más suave.", "Bias the field toward a part of the mix": "Bias the field toward a part of the mix", "Bibata cursor and the selected AUR app components": "Cursor Bibata y los componentes seleccionados de la aplicación AUR", - "Bibata cursor, LocalSend and Voxtype": "Bibata cursor, LocalSend and Voxtype", "Binary": "binario", "Binary data": "datos binarios", "Binary splits, master and stack, or a scrolling strip": "Divisiones binarias, maestro y pila, o una tira desplazable", @@ -1532,15 +1788,12 @@ "Build rules from condition blocks and drag to set priority; the first match wins. Legacy day and night settings become two rules on first open.": "Build rules from condition blocks and drag to set priority; the first match wins. Legacy day and night settings become two rules on first open.", "Build the depth map": "Construye el mapa de profundidad", "Building %1": "Compilando %1", - "Building layers · %1%": "Building layers · %1%", "Building the AUR extras": "Compilando extras AUR", "Building the Ryoku desktop from source": "Compilando Ryoku desde código fuente", - "Building the scene layers": "Building the scene layers", "Built-in": "Integrado", "Built-in display": "Built-in display", "Built: %1": "Compilado: %1", "Bun": "bollo", - "Bundle complete": "Bundle complete", "Bundle version, environment, and recent logs into one file to attach to an issue.": "Bundle version, environment, and recent logs into one file to attach to an issue.", "Bundled word source; no network requests.": "Fuente de palabras agrupada; sin solicitudes de red.", "Bundles": "Paquetes", @@ -1589,7 +1842,6 @@ "CHANGE ON MAP": "CAMBIAR EN EL MAPA", "CHAT": "CHAT", "CHAT BACKEND": "BACKEND DE CHAT", - "CHEATSHEET": "ATAJOS", "CHECK AGAIN": "COMPROBAR DE NUEVO", "CHOOSE": "ELEGIR", "CHOOSE .TXT": "ELEGIR .TXT", @@ -1669,7 +1921,6 @@ "CUSTOM": "PERSONALIZADO", "CUSTOM ASSET": "ACTIVO PERSONALIZADO", "CUSTOM OFF": "PERSONALIZADO APAGADO", - "CUT QUALITY": "CUT QUALITY", "CUT-OUT MODEL": "MODELO RECORTABLE", "CUT-OUTS": "CUT-OUTS", "CW %1": "DC %1", @@ -1741,6 +1992,7 @@ "Cancel rename": "Cancelar cambio de nombre", "Cancel timer": "Cancelar temporizador", "Canceling…": "Cancelando…", + "Cancelled": "Cancelled", "Cap picker animation while discharging. The normal Max FPS still applies if lower; 0 disables this cap.": "Cap picker animation while discharging. The normal Max FPS still applies if lower; 0 disables this cap.", "Cap the downloaded resolution. Lower means smaller files and lighter playback.": "Cap the downloaded resolution. Lower means smaller files and lighter playback.", "Capabilities: %s": "Capacidades: %s", @@ -1826,7 +2078,6 @@ "Changes the active profile now.": "Cambia el perfil activo ahora.", "Changing models switches to a separate index. Search becomes available when the background index task finishes.": "Changing models switches to a separate index. Search becomes available when the background index task finishes.", "Channel history diverged; reconciling %s onto its checked target": "La historia del canal divergió; reconciliando %s con su objetivo marcado", - "Channel history diverged; reconciling %s onto origin/%s": "Historial de canal divergido; reconciliando %s en origin/%s", "Character": "Personaje", "Character columns the art spans": "Columnas de caracteres que abarca el arte", "Character sets corners, fills and contrast at once; the rows below fine-tune it.": "El personaje establece esquinas, rellenos y contrastes a la vez; las filas siguientes lo afinan.", @@ -1847,10 +2098,7 @@ "Charging at": "Cargando a", "Chassis": "Chasis", "Chat": "Chat", - "Chat models": "Chat models", - "Chat models load with the agent": "Chat models load with the agent", "ChatGPT / Codex": "ChatGPT/Códice", - "Chats": "Chats", "Cheatsheet": "Cheatsheet", "Check again": "comprobar de nuevo", "Check again after installing an app or changing its configuration.": "Check again after installing an app or changing its configuration.", @@ -1869,12 +2117,12 @@ "Checking Gmail setup...": "Comprobando la configuración de Gmail...", "Checking account support…": "Soporte para cuentas corrientes...", "Checking bundle components…": "Checking bundle components…", + "Checking for connected phones": "Checking for connected phones", "Checking installed components": "Checking installed components", "Checking lighting support…": "Comprobando el soporte de iluminación...", "Checking phone support...": "Comprobando soporte telefónico...", "Checking saved and connected lighting devices…": "Comprobando los dispositivos de iluminación guardados y conectados...", "Checking the cut-out model catalogue": "Consultando el catálogo de modelos recortados", - "Checking the model catalogue": "Checking the model catalogue", "Checking the spatial model": "Comprobando el modelo espacial", "Checking...": "Comprobando...", "Checking…": "Comprobando…", @@ -1883,7 +2131,6 @@ "Checklists": "Listas de verificación", "Checks for unstable-dev at %s are still running (%s)": "Las comprobaciones de desarrollo inestable en %s todavía se están ejecutando (%s)", "Choose": "Elegir", - "Choose Depth or Parallax, or add a PNG, to build the layer stack.": "Choose Depth or Parallax, or add a PNG, to build the layer stack.", "Choose Image": "Elige Imagen", "Choose Set as backdrop on a wallpaper, or enter an image, video, or Wallpaper Engine project path. Leave empty to use the last wallpaper.": "Choose Set as backdrop on a wallpaper, or enter an image, video, or Wallpaper Engine project path. Leave empty to use the last wallpaper.", "Choose a built-in palette, one you saved in the designer, or Custom seeds to generate one from your own colours.": "Choose a built-in palette, one you saved in the designer, or Custom seeds to generate one from your own colours.", @@ -1891,7 +2138,6 @@ "Choose a command first": "Elija un comando primero", "Choose a condition, or add a nested group when this rule needs mixed all/any logic.": "Choose a condition, or add a nested group when this rule needs mixed all/any logic.", "Choose a config folder": "Elegir carpeta de configuración", - "Choose a cut quality, then build the scene.": "Choose a cut quality, then build the scene.", "Choose a device": "Elige un dispositivo", "Choose a disk strategy first (press esc).": "Elige una estrategia de disco primero (presiona esc).", "Choose a display, or use whichever monitor is focused when the picker opens. Close and reopen the picker after changing this.": "Choose a display, or use whichever monitor is focused when the picker opens. Close and reopen the picker after changing this.", @@ -1906,10 +2152,10 @@ "Choose a name for this stack": "Elija un nombre para esta pila", "Choose a network": "Elige una red", "Choose a network below.": "Choose a network below.", + "Choose a phone": "Choose a phone", "Choose a phone first": "Elige un teléfono primero", "Choose a picture": "Elige una imagen", "Choose a picture or live wall. Ryoku follows its colours automatically.": "Choose a picture or live wall. Ryoku follows its colours automatically.", - "Choose a picture, then confirm the cut": "Choose a picture, then confirm the cut", "Choose a playlist from the index": "Choose a playlist from the index", "Choose a semantic-pack.json or .skwdmodel archive. Ryogami validates the pack, installs it, and selects it.": "Choose a semantic-pack.json or .skwdmodel archive. Ryogami validates the pack, installs it, and selects it.", "Choose a size to add a ready-to-use preset. Expand it to change orientation, rename it, or set a custom range.": "Choose a size to add a ready-to-use preset. Expand it to change orientation, rename it, or set a custom range.", @@ -1970,6 +2216,7 @@ "Choose where this playlist runs. Selecting every display creates one shared assignment.": "Choose where this playlist runs. Selecting every display creates one shared assignment.", "Choose whether a window pause condition pauses its display or every display.": "Choose whether a window pause condition pauses its display or every display.", "Choose whether downloads use the running Steam client or steamcmd. Restart the wallpaper service after changing this.": "Choose whether downloads use the running Steam client or steamcmd. Restart the wallpaper service after changing this.", + "Choose whether one click selects or opens an icon": "Choose whether one click selects or opens an icon", "Choose whether the whole field stays flat or bends into an arc, wave, S shape, or cylinder.": "Choose whether the whole field stays flat or bends into an arc, wave, S shape, or cylinder.", "Choose which apps follow your colours. Turning an app off restores its previous theme setup.": "Choose which apps follow your colours. Turning an app off restores its previous theme setup.", "Choose which dominant wallpaper colour leads.": "Elija qué color de papel tapiz dominante lleva.", @@ -2093,7 +2340,6 @@ "Close all windows": "cerrar todas las ventanas", "Close equalizer": "Cerrar el ecualizador", "Close mixer": "Close mixer", - "Close models": "Close models", "Close on selection": "Close on selection", "Close panel": "Cerrar panel", "Close settings": "Cerrar configuración", @@ -2247,13 +2493,11 @@ "Confirm or change the suggested profile.": "Confirmar o cambiar el perfil sugerido.", "Confirm passphrase:": "Confirmar frase de contraseña:", "Confirm password": "Confirmar contraseña", - "Confirm password:": "Confirmar contraseña:", "Confirm root password": "Confirmar contraseña de root", "Connect": "Conectar", "Connect %1 in Tasks settings": "Conecte %1 en la configuración de Tareas", "Connect Ryogami to the desktop and the overview.": "Connect Ryogami to the desktop and the overview.", "Connect a USB cable": "Conecte un cable USB", - "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills, then routes that agent's models through the Prowl gateway.": "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills, then routes that agent's models through the Prowl gateway.", "Connect any coding agent to the same living map of this machine. One click adds the Ryoku and Prowl skills. It never changes the agent's own model; making Prowl auto the default is a separate switch.": "Conecte cualquier agente de codificación al mismo mapa viviente de esta máquina. Un clic agrega las habilidades Ryoku y Prowl. Nunca cambia el modelo del propio agente; hacer que Prowl auto sea el predeterminado es un interruptor separado.", "Connect automatically": "Conectarse automáticamente", "Connect one in Settings": "Conecte uno en Configuración", @@ -2268,7 +2512,6 @@ "Connected Devices Battery List (1x1)": "Lista de baterías de dispositivos conectados (1x1)", "Connected Devices Battery List (2x1)": "Lista de baterías de dispositivos conectados (2x1)", "Connected and saved devices, including devices that are currently unplugged.": "Dispositivos conectados y guardados, incluidos los dispositivos que están actualmente desconectados.", - "Connected devices": "Dispositivos conectados", "Connected display count is": "Connected display count is", "Connected to %1": "Conectado a %1", "Connected to %1 · %2%": "Conectado a %1 · %2%", @@ -2404,6 +2647,7 @@ "Could not copy the SSH command": "No se pudo copiar el comando SSH", "Could not copy to clipboard": "No se pudo copiar al portapapeles", "Could not create the phone microphone source": "No se pudo crear la fuente del micrófono del teléfono", + "Could not create the temporary image": "Could not create the temporary image", "Could not enable wireless ADB": "No se pudo habilitar el ADB inalámbrico", "Could not fetch the OS catalogue": "No se pudo obtener el catálogo del SO", "Could not find %1 again": "No se pudo encontrar %1 nuevamente", @@ -2413,6 +2657,8 @@ "Could not load today's games": "No se pudieron cargar los juegos de hoy.", "Could not load user accounts.": "No se pudieron cargar cuentas de usuario.", "Could not open %1 - ADB unreachable": "No se pudo abrir %1 - ADB inalcanzable", + "Could not prepare private temporary storage": "Could not prepare private temporary storage", + "Could not protect the temporary image": "Could not protect the temporary image", "Could not read phone support on this system.": "No se pudo leer la asistencia telefónica en este sistema.", "Could not read that region": "No se pudo leer esa región", "Could not read the OS catalogue": "No se pudo leer el catálogo del SO", @@ -2432,6 +2678,7 @@ "Could not save the drawing": "No se pudo guardar el dibujo.", "Could not save this phone setting.": "No se pudo guardar esta configuración del teléfono.", "Could not send %1 to %2": "No se pudo enviar %1 a %2", + "Could not send screenshot": "Could not send screenshot", "Could not start scrcpy on the phone — reconnect it and try again": "No se pudo iniciar scrcpy en el teléfono; vuelva a conectarlo e inténtelo de nuevo", "Could not start the bluetooth service.": "No se pudo iniciar el servicio Bluetooth.", "Could not synchronize calendar.": "No se pudo sincronizar el calendario.", @@ -2449,6 +2696,7 @@ "Couldn't cut that layer": "No se pudo cortar esa capa", "Couldn't finish": "no pude terminar", "Couldn't import this asset. Choose a supported local file under 64 MiB.": "No se pudo importar este activo. Elija un archivo local compatible de menos de 64 MiB.", + "Couldn't install bundle (exit %1).": "Couldn't install bundle (exit %1).", "Couldn't measure this picture": "No se pudo medir esta imagen", "Couldn't preview this asset; shell reload will use the Ryoku wordmark.": "No se pudo previsualizar este activo; la recarga del shell usará la marca Ryoku.", "Couldn't prune the clipboard history.": "No se pudo podar el historial del portapapeles.", @@ -2456,6 +2704,7 @@ "Couldn't read Omarchy add-ons.": "Couldn't read Omarchy add-ons.", "Couldn't read \\\"%1\\\"": "No se pudo leer \\\"%1\\\"", "Couldn't read bundle state.": "Couldn't read bundle state.", + "Couldn't read formats": "Couldn't read formats", "Couldn't read installed add-ons (exit %1).": "Couldn't read installed add-ons (exit %1).", "Couldn't read installed add-ons.": "Couldn't read installed add-ons.", "Couldn't read installed apps": "No se pudieron leer las aplicaciones instaladas", @@ -2483,11 +2732,11 @@ "Couldn't remove the emblem.": "No se pudo quitar el emblema.", "Couldn't remove the fingerprint": "No se pudo eliminar la huella", "Couldn't remove the imported emblem.": "No se pudo quitar el emblema importado.", - "Couldn't repair bundle (exit %1).": "Couldn't repair bundle (exit %1).", "Couldn't save \\\"%1\\\"": "No se pudo guardar \\\"%1\\\"", "Couldn't save add-on setting (exit %1).": "Couldn't save add-on setting (exit %1).", "Couldn't save placement (exit %1).": "Couldn't save placement (exit %1).", "Couldn't save the Fastfetch settings.": "Couldn't save the Fastfetch settings.", + "Couldn't save the system update choice.": "Couldn't save the system update choice.", "Couldn't save window-manager settings.": "Couldn't save window-manager settings.", "Couldn't switch skin: %1": "No se pudo cambiar tema: %1", "Couldn't translate. Check the connection or the translator setup.": "No se pudo traducir. Verifique la conexión o la configuración del traductor.", @@ -2574,24 +2823,19 @@ "Customize…": "Customize…", "Custom…": "Personalizado…", "Cut": "Cortar", - "Cut a layer from a picture": "Cortar una capa de una imagen", "Cut a layer from another picture": "Cortar una capa de otra imagen.", "Cut angle": "Cut angle", - "Cut blocked · %1": "Cut blocked · %1", "Cut blue light with a warmer screen tint.": "Reduce la luz azul con un tinte de pantalla más cálido.", "Cut direction": "Cut direction", "Cut from another picture": "Recortado de otra imagen", "Cut from this wallpaper": "Cortar de este fondo de pantalla", "Cut out the subject": "Corta el tema", - "Cut the wallpaper first, then choose which desktop pieces rise above its foreground.": "Cut the wallpaper first, then choose which desktop pieces rise above its foreground.", "Cut the wallpaper into layers and lift widgets between them": "Cut the wallpaper into layers and lift widgets between them", - "Cut the wallpaper's subject out so widgets can sit behind it, and choose how the layers look and move.": "Cut the wallpaper's subject out so widgets can sit behind it, and choose how the layers look and move.", "Cut this area": "Corta esta área", "Cut this picture into a new layer?": "Cut this picture into a new layer?", "Cut variance": "Cut variance", "Cutting a new layer": "Cortar una nueva capa", "Cutting the layer": "Cortar la capa", - "Cutting the wallpaper": "Cutting the wallpaper", "Cyanotype ink and plates": "Tinta y planchas de cianotipo.", "Cyber": "cibernético", "Cyberpunk": "Ciberpunk", @@ -2628,7 +2872,6 @@ "DETAILS shows everything the update printed.": "DETAILS shows everything the update printed.", "DETECT": "DETECT", "DEVICE": "DISPOSITIVO", - "DEVICE LIGHTING": "ILUMINACIÓN DEL DISPOSITIVO", "DEVICES": "DISPOSITIVOS", "DEVICES & SYSTEM": "DISPOSITIVOS Y SISTEMA", "DHCP": "DHCP", @@ -2672,9 +2915,9 @@ "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku materialize": "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku materialize", "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)": "DRYRUN: $RYOKU_TARGET_CHROOT /mnt runuser -u $u -- env HOME=/home/$u modo ryoku-gpu $mapped (escribe /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)", "DRYRUN: %s %s": "FUNCIONAMIENTO EN SECO: %s %s", + "DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version": "DRYRUN: after installation, align installed packages that [ryoku] serves at a newer version", "DRYRUN: append %s to %s": "DRYRUN: append %s to %s", "DRYRUN: arch-chroot /mnt pacman -Sy --noprogressbar": "PRUEBA: arch-chroot /mnt pacman -Sy --noprogressbar", - "DRYRUN: arch-chroot /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)": "DRYRUN: arch-chroot /mnt runuser -u $u -- env HOME=/home/$u ryoku-gpu mode $mapped (writes /home/$u/.config/$RYOKU_COMPOSITOR_CONFIG_DIR/$RYOKU_COMPOSITOR_GPU_PIN)", "DRYRUN: arch-chroot /mnt systemctl enable limine-snapper-sync.service": "PRUEBA: arch-chroot /mnt systemctl enable limine-snapper-sync.service", "DRYRUN: back up %s": "SIMULACIÓN: copiar %s", "DRYRUN: bootstrap yay, install boot-critical (%s) first under per-package timeouts, then the rest of %s best-effort under a global timeout, as %s": "DRYRUN: bootstrap yay, instalar crítica de arranque (%s) primero con timeouts por paquete, luego el resto de %s best-effort con timeout global, como %s", @@ -2704,7 +2947,6 @@ "DRYRUN: seed %s -> ~/%s": "SIMULACIÓN: sembrar %s -> ~/%s", "DRYRUN: seed XBPS key plists from %s into %s": "DRYRUN: seed XBPS key plists from %s into %s", "DRYRUN: seed keyboard layout %q variant %q options %q from %s into %s": "DRYRUN: distribución del teclado inicial %q variante %q opciones %q de %s a %s", - "DRYRUN: select CPU-compatible CachyOS repos, enable [multilib], populate the cachyos keyring in the target": "PRUEBA: seleccionar repositorios CachyOS compatibles con CPU, habilitar [multilib], rellenar el llavero cachyos en el objetivo", "DRYRUN: set %s as the default browser for http, https and html": "DRYRUN: set %s as the default browser for http, https and html", "DRYRUN: set %s as the default file manager (inode/directory + desktop.apps.files) for %s if installed": "DRYRUN: establezca %s como administrador de archivos predeterminado (inodo/directorio + escritorio.apps.files) para %s si está instalado", "DRYRUN: set %s as the default web browser (xdg + desktop.apps.browser) for %s if installed": "DRYRUN: set %s as the default web browser (xdg + desktop.apps.browser) for %s if installed", @@ -2851,9 +3093,7 @@ "Depth Anything V2 Small": "Profundidad Cualquier cosa V2 Pequeña", "Depth Parallax": "Depth Parallax", "Depth falloff": "Depth falloff", - "Depth keeps the cut-outs still. Switch modes to tune scene motion.": "Depth keeps the cut-outs still. Switch modes to tune scene motion.", "Depth of the field into the desktop": "Depth of the field into the desktop", - "Depth scene": "Depth scene", "Depth shadow": "Sombra de profundidad", "Derive from colour": "Derive from colour", "Describe": "Describe", @@ -2896,7 +3136,6 @@ "Detecting outputs": "Detecting outputs", "Detecting…": "Detectando…", "Dev": "Desarrollo", - "Developer toolchains": "Developer toolchains", "Device": "Dispositivo", "Device default": "Dispositivo predeterminado", "Device effects": "Efectos del dispositivo", @@ -2943,7 +3182,6 @@ "Dimming": "Atenuación", "Dims a screen that turns white on you.": "Atenúa una pantalla que se vuelve blanca ante ti.", "Dims a window that suddenly turns white": "Atenúa una ventana que de repente se vuelve blanca.", - "Dims the effect Ryoku draws.": "Atenúa el efecto que dibuja Ryoku.", "Dims the screen when a mostly white window suddenly fills it.": "Atenúa la pantalla cuando de repente una ventana mayoritariamente blanca la llena.", "Direct": "directo", "Direct-GPU virtual machines": "Direct-GPU virtual machines", @@ -3062,6 +3300,7 @@ "Dot": "Dot", "Dot Ring": "Anillo de puntos", "Dots": "Puntos", + "Double click": "Double click", "Down": "Down", "Download": "Descargar", "Download %1 (%2)": "Descargar %1 (%2)", @@ -3069,17 +3308,16 @@ "Download Rate": "Tasa de Descarga", "Download URL": "Download URL", "Download a model below before you can switch this on.": "Descarga un modelo a continuación antes de poder activarlo.", + "Download best": "Download best", "Download button": "Download button", "Download cancelled": "Descarga cancelada", "Download cancelled.": "Download cancelled.", "Download engine": "Download engine", "Download failed": "Descarga fallida", - "Download failed (voxtype exited %1).": "Descarga fallida (voxtype salió %1).", "Download mode": "Download mode", "Download selected model": "Descargar modelo seleccionado", "Download the cobalt image": "Descargar imagen cobalt", "Download the model once to build this wallpaper's depth map": "Descargue el modelo una vez para construir el mapa de profundidad de este fondo de pantalla.", - "Downloaded": "Downloaded", "Downloading": "Descargando", "Downloading %1": "Descargando %1", "Downloading %1 · %2 queued…": "Downloading %1 · %2 queued…", @@ -3089,7 +3327,6 @@ "Downloading…": "Downloading…", "Downloading… %1%": "Descargando… %1%", "Downloads": "Downloads", - "Draft": "Draft", "Drag a band, use the wheel or arrow keys, or double-click it to return that band to zero.": "Arrastre una banda, use la rueda o las teclas de flecha, o haga doble clic en ella para devolver esa banda a cero.", "Drag a box around one thing. Whatever stands out inside it is cut out.": "Arrastra un cuadro alrededor de una cosa. Todo lo que destaca en su interior se recorta.", "Drag a dial to set a duration": "Arrastra un dial para establecer una duración", @@ -3238,7 +3475,6 @@ "Each pass fills the ring — keep touching until it closes": "Cada pasada llena el anillo - sigue tocando hasta que se cierre", "Each provider automatically shows the quota windows it publishes. Antigravity model pools are separate stops; OpenRouter always shows remaining credits regardless of the percentage setting.": "Cada proveedor muestra automáticamente las ventanas de cuota que publica. Los grupos de modelos antigravedad son paradas separadas; OpenRouter siempre muestra los créditos restantes independientemente de la configuración del porcentaje.", "Each screen's picture and how it sits. While this is open, drag the desktop to move the wallpaper and use the wheel or a pinch to zoom.": "La imagen de cada pantalla y cómo queda. Mientras esté abierto, arrastre el escritorio para mover el fondo de pantalla y use la rueda o un pellizco para hacer zoom.", - "Each tier remembers its model and edge treatment": "Each tier remembers its model and edge treatment", "Each widget keeps its Material design": "Cada widget mantiene su diseño de Material.", "Each widget's distance is set from that widget's own page.": "La distancia de cada widget se establece desde la propia página de ese widget.", "Each workspace": "Cada espacio de trabajo", @@ -3280,11 +3516,11 @@ "Edit ›": "Edit ›", "Edit %1": "Editar %1", "Edit Dynamic Island": "Editar isla dinámica", - "Edit Shima": "Edit Shima", + "Edit Shima": "Editar Shima", "Edit alarm": "Editar alarma", "Edit category": "Editar categoría", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Editar escritorio", "Edit desktop widgets": "Editar widgets de escritorio", "Edit dock": "Editar muelle", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", @@ -3524,7 +3760,6 @@ "Esc or click to close": "Esc o clic para cerrar", "Esc saves the recording": "Esc saves the recording", "Esc to cancel": "Esc to cancel", - "Esc to close": "Esc para cerrar", "Escalonado paper plates": "Platos de papel escalonado", "Escape": "Escape", "Essentials": "Essentials", @@ -3553,7 +3788,6 @@ "Every %1 years": "Cada %1 años", "Every Linux mouse remains bindable even when it is not in the catalogue.": "Every Linux mouse remains bindable even when it is not in the catalogue.", "Every area": "Cada área", - "Every bar style shares one dock. Choose its design, behaviour and pinned apps in Stage Editor.": "Every bar style shares one dock. Choose its design, behaviour and pinned apps in Stage Editor.", "Every boot re-provisions the ryoku account, factory-fresh.": "Cada arranque reaprovisiona la cuenta ryoku, como de fábrica.", "Every built-in is already on the bar.": "Cada componente ya está en la barra.", "Every change": "Cada cambio", @@ -3670,7 +3904,7 @@ "FIRST RUN": "FIRST RUN", "FIT": "FIT", "FIX WITH AI": "FIX WITH AI", - "FIXED": "FIJO", + "FLATHUB %1": "FLATHUB %1", "FLEET": "FLOTA", "FOCUS": "ENFOQUE", "FOCUS FLASH": "DESTELLO DE FOCO", @@ -3726,7 +3960,6 @@ "Fast": "Rápido", "Fast and compact for most wallpapers": "Rápido y compacto para la mayoría de los fondos de pantalla", "Fast is used for immediate feedback, Standard for normal movement, and Slow for larger layout changes.": "Fast is used for immediate feedback, Standard for normal movement, and Slow for larger layout changes.", - "Fast lane": "Fast lane", "Fastfetch": "Fastfetch", "Favorites": "Favoritos", "Favorites · %1 sites": "Favoritos · %1 sitios", @@ -3867,6 +4100,7 @@ "Flat frame-off panel with hairline cell separators": "Panel plano sin marco con separadores finos", "Flat modules with hairline accent underlines": "Módulos planos con subrayados finos", "Flat ties travel to the hand; Adaptive speeds quick moves.": "Plano vincula el viaje a la mano; Adaptativo acelera los movimientos rápidos.", + "Flathub application. Ryostore does not review it. Check its source and permissions. New apps install system-wide when available (authorization may be required), otherwise through an existing user remote.": "Flathub application. Ryostore does not review it. Check its source and permissions. New apps install system-wide when available (authorization may be required), otherwise through an existing user remote.", "Flatten": "Aplanar", "Fleet": "Fleet", "Fleet overview": "Fleet overview", @@ -3940,7 +4174,6 @@ "Follow wallpaper & theme": "Seguir fondo de pantalla y tema", "Follow wallpaper recolours from each applied wallpaper. Fixed palette uses one chosen palette. Keep current colours leaves the theme alone.": "Follow wallpaper recolours from each applied wallpaper. Fixed palette uses one chosen palette. Keep current colours leaves the theme alone.", "Follows every new wallpaper": "Sigue cada nuevo fondo de pantalla", - "Follows the desktop accent, so the device retints with the wallpaper. Now: %1": "Sigue el acento del escritorio, para que el dispositivo se tiña con el fondo de pantalla. Ahora: %1", "Font": "Font", "Font Size": "Tamaño de fuente", "Font roundness": "Redondez de la fuente", @@ -4142,6 +4375,7 @@ "Gemini": "Géminis", "General": "General", "General limit: %1": "Límite general: %1", + "General settings": "Ajustes generales", "General wallpaper cut-outs": "Recortes generales de papel tapiz", "Generate": "generar", "Generate bug report": "Generate bug report", @@ -4277,7 +4511,6 @@ "Guys": "Chicos", "H.264 plays anywhere; HEVC and AV1 are smaller but need a newer GPU.": "H.264 plays anywhere; HEVC and AV1 are smaller but need a newer GPU.", "H:%1 L:%2": "Alto:%1 L:%2", - "HAND BACK": "DEVOLVER", "HARBOUR LOG": "HARBOUR LOG", "HAWK": "HALCÓN", "HD ×2 (AI upscale)": "HD ×2 (escalado por IA)", @@ -4308,7 +4541,6 @@ "Hallucination": "Alucinación", "Hand back": "Devolver la mano", "Hand back every device": "Devolver todos los dispositivos", - "Handing a device to Ryoku lets its provider talk to it. Hardware that keeps profiles in its own memory can be left showing Ryoku's look after you hand it back; Ryoku restores the effect it found, but only the device's own software can rebuild a profile you overwrote with Save to device.": "Entregar un dispositivo a Ryoku permite que su proveedor se comunique. El hardware que guarda perfiles puede mostrar el aspecto de Ryoku; Ryoku restaura el efecto original, pero solo su software puede recrear un perfil sobrescrito con Guardar en dispositivo.", "Handing it back restores %1.": "Al devolverlo se restaura %1.", "Handle": "Manejar", "Hanging strips": "Tiras colgantes", @@ -4526,8 +4758,6 @@ "How far the window hops, in pixels; Slide style only": "Cuán lejos salta la ventana, en píxeles; solo estilo Deslizamiento", "How far the window shrinks; Bounce style only": "Cuán lejos se encoge la ventana; Solo estilo Rebote", "How far this layer travels in Parallax": "How far this layer travels in Parallax", - "How fast Ryoku moves the effect.": "Qué tan rápido Ryoku mueve el efecto.", - "How fast the device runs the effect on its own.": "Qué tan rápido el dispositivo ejecuta el efecto por sí mismo.", "How fast the gradient circles the window": "Cuán rápido el gradiente rodea la ventana", "How fast the shell's own panels, menus and transitions move. Speed scales every Ryoku animation live; Reduce motion snaps them into place for comfort.": "Qué tan rápido se mueven los paneles, menús y transiciones del propio shell. La velocidad escala cada animación de Ryoku en vivo; Reducir movimiento los encaja en su lugar para mayor comodidad.", "How high subtitles sit; 100 is the bottom edge.": "Qué tan altos se ubican los subtítulos; 100 es el borde inferior.", @@ -4608,7 +4838,6 @@ "How wide grains spiral around their ribbon.": "How wide grains spiral around their ribbon.", "How wide the open Island is. The activity page keeps its proportion.": "Qué tan ancha es la isla abierta. La página de actividad mantiene su proporción.", "How wide the shoulders are where the Island turns into its edge.": "Qué anchos son los hombros donde la isla se convierte en su borde.", - "How windows look and behave here. Changes apply at your next login.": "Cómo se ven y se comportan las ventanas aquí. Los cambios se aplicarán en tu próximo inicio de sesión.", "How windows look and behave here. Save applies changes now.": "Cómo se ven y se comportan las ventanas aquí. Guardar aplica los cambios ahora.", "How windows take focus as the pointer moves.": "Cómo las ventanas toman el foco a medida que se mueve el puntero.", "How your folders line up in the gallery.": "Cómo se alinean tus carpetas en la galería.", @@ -4641,6 +4870,7 @@ "IMPORT": "IMPORTAR", "IMPORT ANOTHER": "IMPORTAR OTRO", "IMPORT ISO": "IMPORT ISO", + "INCLUDE SYSTEM": "INCLUDE SYSTEM", "INCOMING COMMITS": "COMMITS ENTRANTES", "INFO": "INFORMACIÓN", "INHERIT": "HEREDAR", @@ -4651,10 +4881,12 @@ "INSTALL ALL": "INSTALAR TODO", "INSTALL ALL /": "INSTALAR TODO /", "INSTALL ENGINE": "INSTALAR MOTOR", + "INSTALL MISSING": "INSTALL MISSING", "INSTALL SELECTED": "INSTALAR SELECCIÓN", "INSTALLED": "INSTALADO", "INSTALLED THROUGH STASH": "INSTALADO A TRAVÉS DE STASH", "INSTALLING": "INSTALANDO", + "INTERFACE": "INTERFACE", "INTERFACE SCALE": "ESCALA DE INTERFAZ", "INTERNET ISOLATED": "INTERNET AISLADO", "INTERNET KILL SWITCH": "INTERRUPTOR DE RED", @@ -4766,6 +4998,7 @@ "Inbox": "Bandeja de entrada", "Include Wallpaper Engine": "Incluir Wallpaper Engine", "Include images": "Incluir imágenes", + "Include system packages": "Include system packages", "Include this category when opening Wallhaven. If none are selected, General is used.": "Include this category when opening Wallhaven. If none are selected, General is used.", "Include this content when opening the source. Wallhaven uses SFW if no content levels are selected.": "Include this content when opening the source. Wallhaven uses SFW if no content levels are selected.", "Include this group": "Include this group", @@ -4830,7 +5063,6 @@ "Install the VirtIO drivers from the attached CD so disk and network work.": "Instala los controladores VirtIO desde el CD adjunto para que el disco y la red funcionen.", "Install the Zen palette integration using the recipe directory. The app and its dependencies must already be installed. Zen needs its signed palette extension installed separately and a one-time restart.": "Instale la integración de la paleta Zen usando el directorio de recetas. La aplicación y sus dependencias ya deben estar instaladas. Zen necesita su extensión de paleta firmada instalada por separado y un reinicio único.", "Install the client package": "Install the client package", - "Install the palette bridge for the supported apps that are present.": "Install the palette bridge for the supported apps that are present.", "Install yt-dlp": "Install yt-dlp", "Installation canceled. Your selected model is unchanged.": "Instalación cancelada. El modelo seleccionado no ha cambiado.", "Installation failed": "Instalación fallida", @@ -4858,7 +5090,6 @@ "Installing…": "Instalando…", "Installs Steam and CachyOS's gaming packages:": "Instala los paquetes de juegos de Steam y CachyOS:", "Installs Steam with 32-bit graphics support.": "Instala Steam con soporte para gráficos de 32 bits.", - "Installs the -luks host variant.": "Instala variante host -luks.", "Installs the Hyprland Ryoku desktop variant.": "Installs the Hyprland Ryoku desktop variant.", "Installs the experimental Wayfire Ryoku desktop variant.": "Installs the experimental Wayfire Ryoku desktop variant.", "Installs the niri Ryoku desktop variant.": "Installs the niri Ryoku desktop variant.", @@ -4919,6 +5150,7 @@ "JWT decoder": "decodificador JWT", "Japan": "Japón", "Japanese Typography": "Tipografía japonesa", + "Japanese glosses": "Japanese glosses", "Japanese with subtitles, or dubbed where a dub exists. ani-cli only brings English subtitles and dubs.": "Japonés con subtítulos o doblado cuando exista doblaje. ani-cli solo trae subtítulos y doblajes en inglés.", "JaviBOT96": "JaviBOT96", "Jazz": "Jazz", @@ -4937,6 +5169,8 @@ "KATANA": "KATANA", "KB/s": "KB/s", "KDE Bluetooth": "Bluetooth KDE", + "KDE Connect could not reach %1": "KDE Connect could not reach %1", + "KDE Connect is not installed": "KDE Connect is not installed", "KDE Connect is not installed — phone integration requires it": "KDE Connect no está instalado: la integración del teléfono lo requiere", "KDE Connect is not running": "KDE Connect no se está ejecutando", "KDE Connect is not running on this computer. Try turning phone support on again.": "KDE Connect no se está ejecutando en esta computadora. Intente activar la asistencia telefónica nuevamente.", @@ -5034,10 +5268,8 @@ "Keyboard layout and locks": "Distribución del teclado y bloqueos", "Keyboard layout · %1": "Distribución de teclado · %1", "Keyboard mode": "Modo teclado", - "Keyboard shortcuts": "Atajos de teclado", "Keyboard, pointer and touchpad, and key repeat.": "Teclado, puntero y touchpad, y repetición de tecla.", "Keyboard, while drawing": "Teclado, mientras dibujas", - "Keyboards, mice and other RGB hardware, through OpenRGB and native laptop providers. While this is off Ryoku never looks for a device and never writes to one, so hardware driven by its own software or a switch on the board is left alone. Turn it on and hand over the devices you want Ryoku to light, one at a time.": "Teclados, ratones y hardware RGB via OpenRGB y proveedores nativos de portátiles. Apagado, Ryoku no busca ni escribe a ningún dispositivo, dejando el hardware controlado por su propio software o interruptor. Enciéndelo y entrega los dispositivos que quieras que Ryoku ilumine, uno a la vez.", "Keycap style": "Estilo de tecla", "Keyframe interval": "Keyframe interval", "Keyring": "Llavero", @@ -5108,7 +5340,6 @@ "Language": "Idioma", "Language & formats. Sets i18n.defaultLocale.": "Idioma y formatos. Establece i18n.defaultLocale.", "Language pack unavailable; using English.": "Paquete de idioma no disponible; usando el inglés.", - "Language, regional formats, location and the system font.": "Idioma, formatos regionales, ubicación y fuente del sistema.", "Language, regional formats, people, fonts, icons, window blur and sounds.": "Idioma, formatos regionales, personas, fuentes, íconos, desenfoque de ventanas y sonidos.", "Lap": "vuelta", "Lap %1": "Vuelta %1", @@ -5148,7 +5379,6 @@ "Launcher mode": "Modo lanzador", "Launcher search": "Búsqueda del lanzador", "Launcher, Kit, lock, clipboard, screenshot, wallpaper and more": "Lanzador, Kit, bloqueo, portapapeles, captura de pantalla, fondo de pantalla y más", - "Launcher, cheatsheet, lock, clipboard, screenshot, wallpaper and more": "Lanzador, atajos, bloqueo, portapapeles, captura, fondo de pantalla y más", "Launching %1…": "Lanzando %1…", "Launching scrcpy…": "Lanzando scrcpy…", "Layer %1": "Capa %1", @@ -5192,7 +5422,6 @@ "Left": "Izquierda", "Left Complication": "Complicación izquierda", "Left and right": "Izquierda y derecha", - "Left off, Ryoku never writes to it: its own software, onboard profile or hardware switch stays in charge.": "Apagado, Ryoku nunca escribe en él: su propio software, perfil interno o interruptor de hardware sigue a cargo.", "Left, right and wheel only; nothing here to rebind.": "Left, right and wheel only; nothing here to rebind.", "Left-handed": "Left-handed", "Left-handed buttons": "Botones para zurdos", @@ -5584,7 +5813,6 @@ "Measuring what the history holds…": "Midiendo lo que la historia contiene…", "Mechanical": "Mecánico", "Media": "Multimedia", - "Media & tools": "Media & tools", "Media Controls": "Controles de medios", "Media Settings": "Configuración de medios", "Media Widget Options": "Opciones de widget multimedia", @@ -5642,6 +5870,7 @@ "Microsoft sign-in has expired.": "El inicio de sesión de Microsoft ha caducado.", "Microsoft sign-in returned incomplete instructions.": "El inicio de sesión de Microsoft devolvió instrucciones incompletas.", "Microsoft token refresh failed.": "Error al actualizar el token de Microsoft.", + "Mid": "Mid", "Mid-flight pinch of the streams; lower = tighter throat.": "Mid-flight pinch of the streams; lower = tighter throat.", "Middle": "Middle", "Middle colour in the field ramp": "Middle colour in the field ramp", @@ -5707,8 +5936,6 @@ "Mode": "Modo", "Model": "Modelo", "Model %1": "Model %1", - "Model downloaded. If dictation doesn't respond, a reboot may be needed to apply it.": "Modelo descargado. Si dictation no responde, reinicia para aplicarlo.", - "Model for %1": "Model for %1", "Model installed.": "Modelo instalado.", "Model manifest": "Model manifest", "Model ready": "Modelo listo", @@ -5762,7 +5989,6 @@ "Motion for open, close, move, workspaces; off snaps": "Movimiento para abrir, cerrar, mover, espacios de trabajo; off ajusta", "Motion in the gaps between widgets": "Movimiento en los huecos entre widgets", "Motion is off": "El movimiento está apagado", - "Motion needs Parallax": "Motion needs Parallax", "Motion speed": "Motion speed", "Motion weights": "Motion weights", "Motion while audio is quiet": "Motion while audio is quiet", @@ -5979,12 +6205,13 @@ "New VPN…": "Nueva VPN...", "New WireGuard…": "Nuevo WireGuard...", "New alarm": "Nueva alarma", + "New application": "Nueva aplicación", "New category": "Nueva categoría", "New chat": "Nuevo chat", "New cuts wait until it finishes or is stopped.": "Los nuevos cortes esperan hasta que finalice o se detenga.", "New desktop apps join the Apps stack": "Nuevas aplicaciones de escritorio se unen a la pila de aplicaciones", "New event": "Nuevo evento", - "New folder": "New folder", + "New folder": "Nueva carpeta", "New keyring password": "Contraseña nueva del llavero", "New machine defaults": "Valores por defecto de máquina nueva", "New machine from ISO": "Máquina nueva desde ISO", @@ -6085,6 +6312,7 @@ "No audio": "No audio", "No audio output": "No audio output", "No audio playing": "No audio playing", + "No audio tracks": "No audio tracks", "No autostart commands yet. Add one to get started.": "Aún no hay comandos de autoarranque. Añade uno para empezar.", "No background apps": "Sin aplicaciones en segundo plano", "No bar plugins installed yet.": "No hay plugins de barra instalados aún.", @@ -6154,6 +6382,7 @@ "No folders in here.": "No folders in here.", "No forecast yet": "Aún no hay pronóstico", "No forks yet.": "No forks yet.", + "No formats for this": "No formats for this", "No free space, and no partition here can be shrunk safely (see reasons below). Press esc to pick another disk or 'Erase whole disk'.": "No hay espacio libre, y ninguna partición aquí se puede reducir de forma segura (ver razones a continuación). Presiona esc para elegir otro disco o 'Borrar disco completo'.", "No grid": "Sin cuadrícula", "No holidays or events": "Sin festivos ni eventos", @@ -6162,7 +6391,6 @@ "No image": "Sin imagen", "No images match": "Ninguna imagen coincide", "No images or folders here": "No hay imágenes o carpetas aquí", - "No in-front layers yet": "No in-front layers yet", "No input device": "Sin dispositivo de entrada", "No installed font matches": "No hay coincidencias de fuentes instaladas", "No internet connection": "Sin conexión a internet", @@ -6170,7 +6398,6 @@ "No keybind conflicts. Your binds layer in cleanly.": "No hay conflictos de atajos. Tus enlaces se aplican limpiamente.", "No laps recorded yet": "Aún no se han registrado vueltas", "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets plus SMBus RAM and motherboard lighting; Ryoku already loads i2c-dev. Supported ASUS laptop keyboards use asusd, and QMK/VIA boards use qmk_hid. Reconnect the device, then choose Detect devices in RGB lighting.": "Ningún dispositivo de iluminación respondió. OpenRGB llega a teclados, ratones y auriculares USB además de RAM SMBus e iluminación de placa base; Ryoku ya carga i2c-dev. Los teclados de portátiles ASUS compatibles utilizan asusd y las placas QMK/VIA utilizan qmk_hid. Vuelva a conectar el dispositivo, luego elija Detectar dispositivos en iluminación RGB.", - "No lighting device answered. OpenRGB reaches USB keyboards, mice and headsets; supported ASUS laptops use asusd for their built-in Aura controller. Motherboard and memory lighting still needs the i2c-dev module loaded, which Ryoku leaves to you.": "Ningún dispositivo de iluminación respondió. OpenRGB alcanza teclados, ratones y auriculares USB; portátiles ASUS soportados usan asusd para su controlador Aura. La iluminación de placa base y memoria aún necesita el módulo i2c-dev cargado, lo cual Ryoku te deja a ti.", "No limit": "Sin límite", "No lock skins installed yet. Get some from Ryoku Store.": "No hay temas de bloqueo instalados. Consigue algunos en la Tienda Ryoku.", "No locked widgets on this display": "No hay widgets bloqueados en esta pantalla", @@ -6264,7 +6491,6 @@ "No supported apps detected. Use Find apps after installing one.": "No supported apps detected. Use Find apps after installing one.", "No supported browser profile was found.": "No se encontró ningún perfil de navegador compatible.", "No supported images were dropped": "No se eliminaron imágenes compatibles", - "No supported lighting provider is installed, so no device can be reached.": "No hay proveedor de iluminación compatible instalado; ningún dispositivo es accesible.", "No supported media or folders here": "No hay medios o carpetas compatibles aquí", "No synced lyrics": "Sin letras sincronizadas", "No synced lyrics for this track": "No hay letras sincronizadas para esta pista.", @@ -6337,7 +6563,6 @@ "Not generated yet": "Aún no generado", "Not important": "No es importante", "Not installed": "Not installed", - "Not installed · install ryoku-rashin": "No instalado · instalar ryoku-rashin", "Not installed · switch on to install it, with its Ask shortcut": "No instalado · actívelo para instalarlo, con su acceso directo Preguntar", "Not mounted": "No montado", "Not on the desktop": "No en el escritorio", @@ -6620,7 +6845,6 @@ "One click adopts the device, picks a colour-capable mode and starts following the palette. Turning it off hands the device back.": "Un clic adopta el dispositivo, elige un modo compatible con colores y comienza a seguir la paleta. Al apagarlo, se devuelve el dispositivo.", "One continuous plate": "Una placa continua", "One finger clicks left, two right, three middle.": "Un dedo clic izquierdo, dos derecho, tres medio.", - "One font for the shell, apps and terminal.": "Una fuente para el shell, apps y terminal.", "One look on every widget. Shima draws its own faces, Material keeps each widget's own style, iNstrument reads like a set of gauges and Readout gives figures room.": "Una mirada a cada widget. Shima dibuja sus propias caras, Material mantiene el estilo propio de cada widget, iNstrument se lee como un conjunto de indicadores y Readout da espacio a las figuras.", "One picture runs from screen to screen": "Una imagen pasa de pantalla en pantalla.", "One shape for all": "Una forma para todos", @@ -6683,10 +6907,11 @@ "Open containing folder": "Abrir carpeta contenedora", "Open controls and session actions.": "Open controls and session actions.", "Open corners": "Esquinas abiertas", + "Open desktop icons": "Open desktop icons", "Open dock settings in Stage Editor": "Abrir la configuración del muelle en Stage Editor", "Open edge": "Borde abierto", "Open equalizer": "Ecualizador abierto", - "Open folder": "Open folder", + "Open folder": "Abrir carpeta", "Open full history": "Abrir historial completo", "Open full widget settings": "Abrir la configuración completa del widget", "Open in File Manager": "Abrir en el Administrador de archivos", @@ -6798,6 +7023,7 @@ "Organic edge settings": "Configuraciones de borde orgánico", "Orientation": "Orientation", "Original": "Originales", + "Orpheus": "Orpheus", "Other": "Otro", "Other Hands": "Otras manos", "Other VPN…": "Otra VPN...", @@ -6900,7 +7126,6 @@ "PM": "PM", "PM10": "PM10", "PM2.5": "PM2,5", - "PNG images (*.png)": "PNG images (*.png)", "POINT ANY AGENT": "APUNTAR CUALQUIER AGENTE", "POINTER": "PUNTERO", "POINTER THEME": "TEMA DEL PUNTERO", @@ -6950,7 +7175,6 @@ "Page titles and card heads.": "Títulos de páginas y encabezados de tarjetas.", "Page width": "Ancho de página", "Pages": "paginas", - "Painted by Ryoku on this device's per-key mode, so they work whatever its firmware supports. They need Ryoku running.": "Pintado por Ryoku en el modo por tecla de este dispositivo, para que funcionen con cualquier firmware que admita. Necesitan que Ryoku esté funcionando.", "Pair": "Emparejar", "Paired": "Emparejado", "Paired Devices": "Dispositivos emparejados", @@ -6972,7 +7196,6 @@ "Palette name": "Palette name", "Palette saved and applied.": "Palette saved and applied.", "Palette saved.": "Palette saved.", - "Palette source": "Palette source", "Palette used when auto-recolouring new wallpapers.": "Paleta usada al recolorizar fondos automáticamente.", "Palette used when auto-theming the backdrop.": "Palette used when auto-theming the backdrop.", "Pane rounding": "Redondeo del panel", @@ -6990,7 +7213,6 @@ "Panels hug their sections and grow up to this.": "Los paneles abrazan sus secciones y crecen hasta llegar a esto.", "Paper islands": "Islas de papel", "Parallax": "Paralaje", - "Parallax scene": "Parallax scene", "Parks widgets while every monitor is covered": "Estaciona widgets cuando todos los monitores están cubiertos", "Parks widgets while every monitor is covered.": "Estaciona widgets cuando todos los monitores están cubiertos.", "Parse error:": "Error de análisis:", @@ -7120,6 +7342,7 @@ "Phone tools could not be removed.": "No se pudieron eliminar las herramientas del teléfono.", "Phone tools installed, but Phone could not be enabled.": "Herramientas del teléfono instaladas, pero no se pudo habilitar el teléfono.", "Phone tools were removed": "Se eliminaron las herramientas del teléfono.", + "Phone transfer is unavailable": "Phone transfer is unavailable", "Phone's next alarm %1": "Próxima alarma del teléfono %1", "Photo": "Foto", "Photo 1x1": "Foto 1x1", @@ -7183,7 +7406,6 @@ "Pickup reminder": "Recordatorio de recogida", "Picture": "Picture", "Pictures": "Fotos", - "Pictures (*.png *.jpg *.jpeg *.webp *.bmp)": "Pictures (*.png *.jpg *.jpeg *.webp *.bmp)", "Pieces": "Piezas", "Pieces you carry and the cards they grow": "Piezas que llevas y las cartas que crecen", "Pill": "Pill", @@ -7224,7 +7446,6 @@ "Place a widget or visualizer, then return here to choose its depth.": "Place a widget or visualizer, then return here to choose its depth.", "Place apps on the desktop or in a folder": "Coloque aplicaciones en el escritorio o en una carpeta", "Place the cut-out behind or over the desktop": "Place the cut-out behind or over the desktop", - "Place the widget around the wallpaper subject": "Place the widget around the wallpaper subject", "Placed apart from the desktop; drag it on the Lockscreen tab": "Colocado aparte del escritorio; arrástralo a la pestaña Lockscreen", "Placed apps can be removed from any location below": "Las aplicaciones colocadas se pueden eliminar desde cualquier ubicación a continuación", "Placed desktop item": "Placed desktop item", @@ -7233,8 +7454,6 @@ "Plain": "Plain", "Plain header": "encabezado simple", "Plain rows sit on the Island's black like the rest of the page; Grouped puts the forecast and vitals strips on quiet plates.": "Las filas simples se asientan sobre el negro de la isla como el resto de la página; Agrupado pone el pronóstico y las tiras vitales en platos tranquilos.", - "Plain stays flat; Depth cuts layers; Parallax adds motion": "Plain stays flat; Depth cuts layers; Parallax adds motion", - "Plain wallpaper": "Plain wallpaper", "Plan Usage reads local client credentials only in memory and caches quota snapshots without tokens. Enabled cloud providers use their read-only usage endpoints; Antigravity reuses its local server.": "Plan Usage lee las credenciales del cliente local solo en la memoria y almacena en caché instantáneas de cuota sin tokens. Los proveedores de nube habilitados utilizan sus puntos finales de uso de solo lectura; Antigravity reutiliza su servidor local.", "Plane": "Plano", "Plate": "plato", @@ -7290,7 +7509,6 @@ "Plugged in and on battery.": "Conectado y con batería.", "Plugin": "Plugin", "Plugin tile keyed by %1": "Mosaico de complemento con clave %1", - "Plugin tile · keyed by %1": "Plugin tile · keyed by %1", "Plugin widgets": "Plugin widgets", "Plugins": "Plugins", "Plugins cannot be built on this machine. Missing build tools: %1.": "Plugins cannot be built on this machine. Missing build tools: %1.", @@ -7347,7 +7565,6 @@ "Power state unavailable.": "Power state unavailable.", "Powerline": "Powerline", "Powers the adapter and remembers the choice across restarts": "Enciende el adaptador y recuerda la elección entre reinicios", - "Powers the adapter, unblocking it if rfkill holds it": "Enciende el adaptador, desbloqueándolo si rfkill lo retiene", "Precip": "Precip", "Prefixes: ; clipboard, = calculator, / actions.": "Prefijos: ; portapapeles, = calculadora, / acciones.", "Prepare the archived homes folder: %v": "Prepare la carpeta de casas archivadas: %v", @@ -7421,7 +7638,6 @@ "Primary ink for this instance": "Primary ink for this instance", "Primary monitor": "Primary monitor", "Primary only": "Primary only", - "Primary user": "Usuario primario", "Printed-page surfaces": "Superficies de página impresa", "Printer": "Impresora", "Prints": "Huellas", @@ -7500,7 +7716,7 @@ "Quick Presets": "Preajustes rápidos", "Quick Settings": "Ajustes rápidos", "Quick ask and chat": "Pregunta rápida y chatea", - "Quick controls": "Quick controls", + "Quick controls": "Ajustes rápidos", "Quick place": "Quick place", "Quick restart": "Reinicio rápido", "Quick settings": "Ajustes rápidos", @@ -7545,7 +7761,6 @@ "RASHIN // ASK": "RASHIN // ASK", "RASHIN // TOOLS": "RASHIN // HERRAMIENTAS", "RASHIN // WEB": "RASHIN // WEB", - "RASHIN SERVICE": "SERVICIO RASHIN", "RASHIN · RECENT ASKS": "RASHIN · CONSULTAS RECIENTES", "RATIO / SIZE": "PROPORCIÓN / TAMAÑO", "RAW": "CRUDO", @@ -7592,7 +7807,6 @@ "REPLY": "RESPONDER", "REQUEST SENT": "SOLICITUD ENVIADA", "REQUIRED": "REQUIRED", - "RESCAN DEVICES": "VOLVER A ESCANEAR DISPOSITIVOS", "RESET": "RESTABLECER", "RESET APPS": "RESTABLECER APPS", "RESET LAYOUT": "REINICIAR DISEÑO", @@ -7681,7 +7895,6 @@ "Range": "Rango", "Rank %1, %2": "Rango %1, %2", "Rashin": "Rashin", - "Rashin could not change its service state.": "Rashin could not change its service state.", "Rashin could not change its state.": "Rashin no pudo cambiar su estado.", "Rashin dashboard": "Rashin dashboard", "Rashin is Ryoku's AI agent: Ask on Alt+Space,": "Rashin es el agente de IA de Ryoku: pregunta en Alt+Espacio,", @@ -7691,7 +7904,6 @@ "Raw condition": "Raw condition", "Raw xkb options, comma separated, for power users.": "Opciones xkb puras, separadas por comas, para usuarios avanzados.", "Re-apply the last wallpaper when the daemon starts.": "Reaplicar el último fondo al iniciar el demonio.", - "Re-cut": "Recortar", "Re-derive per-wallpaper colour bucket and saturation from existing thumbnails.": "Re-derive per-wallpaper colour bucket and saturation from existing thumbnails.", "Re-detect": "Re-detect", "Re-encode each video once at the size and frame rate below, then play the lighter copy.": "Re-encode each video once at the size and frame rate below, then play the lighter copy.", @@ -7713,6 +7925,7 @@ "Readiness checks": "Comprobaciones de preparación", "Reading displays…": "Leyendo pantallas...", "Reading drive information…": "Leyendo información de la unidad…", + "Reading formats": "Reading formats", "Reading installed apps...": "Leyendo aplicaciones instaladas...", "Reading installed apps…": "Leyendo aplicaciones instaladas…", "Reading the scene's properties…": "Reading the scene's properties…", @@ -7845,6 +8058,7 @@ "Reload": "Recargar", "Reload cover": "Portada de recarga", "Reload custom widgets": "Recargar widgets personalizados", + "Reload desktop": "Recargar escritorio", "Reload shell": "Recargar shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Recargando el escritorio", @@ -7906,7 +8120,6 @@ "Remove pin": "Quitar pasador", "Remove pins or move them into place": "Retire los pasadores o colóquelos en su lugar", "Remove selected items": "Eliminar elementos seleccionados", - "Remove the palette bridge from every app.": "Remove the palette bridge from every app.", "Remove this command.": "Remove this command.", "Remove this reference from the model list. The model files and built index are kept.": "Remove this reference from the model list. The model files and built index are kept.", "Remove this resolution preset.": "Remove this resolution preset.", @@ -7956,6 +8169,7 @@ "Require the authenticator PIN when the key supports it": "Requerir el PIN del autenticador si la clave lo soporta", "Require the key's built-in user verification when available": "Requerir la verificación de usuario integrada si está disponible", "Require touching the key during authentication": "Requerir tocar la clave durante la autenticación", + "Required components complete": "Required components complete", "Required for USB connection, quick actions (screenshot, power button) and opening apps from notifications.": "Requerido para conexión USB, acciones rápidas (captura de pantalla, botón de encendido) y abrir aplicaciones desde notificaciones.", "Required rows back a desktop feature and cannot be removed.": "Required rows back a desktop feature and cannot be removed.", "Rescan devices": "Volver a escanear dispositivos", @@ -7973,7 +8187,6 @@ "Reset island settings": "Restablecer ajustes de isla", "Reset layout": "Reestablecer diseño", "Reset look": "Restablecer apariencia", - "Reset look and motion": "Reset look and motion", "Reset motion": "Restablecer movimiento", "Reset position, zoom and orientation": "Restablecer posición, zoom y orientación.", "Reset size": "Reset size", @@ -8012,18 +8225,15 @@ "Responsive multi-device cards widget scaling horizontally per connected Bluetooth device with liquid battery fill.": "Widget responsivo de tarjetas multidispositivo que se escala horizontalmente por dispositivo Bluetooth conectado con carga de batería líquida.", "Rest on the Island to peek, click to keep it, scroll for volume.": "Descanse en la isla para echar un vistazo, haga clic para conservarla y desplácese para ver el volumen.", "Rest when paused": "Descanse cuando esté en pausa", - "Restart": "Restart", - "Restart apps": "Restart apps", + "Restart": "Reiniciar", "Restart bridge service": "Reiniciar el servicio puente", "Restart shell": "Reiniciar shell", - "Restart the bridged apps so they pick up the current colours.": "Restart the bridged apps so they pick up the current colours.", "Restart the palette service. Does not restart Spotify, Zen, or Vesktop.": "Reinicie el servicio de paleta. No reinicia Spotify, Zen o Vesktop.", "Resting": "descansando", "Resting Island": "Isla de descanso", "Restore": "Restore", "Restore camera motion and widget lighting defaults": "Restaurar los valores predeterminados de iluminación de widgets y movimiento de la cámara", "Restore defaults": "Restore defaults", - "Restore edge, shadow and Parallax motion defaults": "Restore edge, shadow and Parallax motion defaults", "Restore edge, shadow and light direction defaults": "Restaurar los valores predeterminados de dirección de luz, sombra y borde", "Restore every Parallax motion setting": "Restaura cada configuración de movimiento Parallax", "Restore every binding to its built-in key or click.": "Restore every binding to its built-in key or click.", @@ -8045,6 +8255,7 @@ "Results / page %1": "Results / page %1", "Results for “%1”": "Resultados para “%1”", "Results from every category, with installed state kept in view.": "Results from every category, with installed state kept in view.", + "Results from the whole of Flathub, with installed state kept in view.": "Results from the whole of Flathub, with installed state kept in view.", "Resume": "Reanudar", "Resume %1": "Reanudar %1", "Resume %1 episode %2": "Reanudar %1 episodio %2", @@ -8166,7 +8377,6 @@ "Rules stay saved, but none of them will run.": "Rules stay saved, but none of them will run.", "Run": "Ejecutar", "Run by the device itself, so they keep going when Ryoku closes.": "Se ejecutan por el propio dispositivo, por lo que continúan cuando Ryoku cierra.", - "Run by the device itself, so they keep going with Ryoku closed. An effect that does nothing is its firmware; a Ryoku effect above will work instead.": "Ejecutados por el dispositivo, siguen con Ryoku cerrado. Un efecto que no hace nada es su firmware; un efecto Ryoku superior funcionará en su lugar.", "Run command": "Ejecutar comando", "Run custom output": "Run custom output", "Run every time": "Run every time", @@ -8183,7 +8393,6 @@ "Running indicator": "Indicador de funcionamiento", "Running ryoku doctor": "Running ryoku doctor", "Running · %1": "Running · %1", - "Running · 127.0.0.1:%1": "Ejecutando · 127.0.0.1:%1", "Running · 127.0.0.1:%1 · off removes it and its Ask shortcut": "Ejecutar · 127.0.0.1:%1 · desactivado lo elimina y su acceso directo Preguntar", "Runs %1 after the step above; the rest of the list waits with it": "Ejecuta %1 después del paso anterior; el resto de la lista espera con él", "Runs %1this routine again. Pick another target.": "Ejecuta %1esta rutina nuevamente. Elige otro objetivo.", @@ -8221,12 +8430,10 @@ "Ryoku does not manage this path.": "Ryoku does not manage this path.", "Ryoku does not publish packages for Fedora %s yet. Supported releases could not be read from %s: %v": "Ryoku does not publish packages for Fedora %s yet. Supported releases could not be read from %s: %v", "Ryoku does not publish packages for Fedora %s yet. Supported: %s.": "Ryoku does not publish packages for Fedora %s yet. Supported: %s.", - "Ryoku effects": "Efectos Ryoku", "Ryoku greeter theme selected past KDE's kde_settings.conf drop-in": "Tema greeter de Ryoku ignoró drop-in kde_settings.conf de KDE", "Ryoku has not published Fedora %s yet; Ryoku packages stay at their current version until it does": "Ryoku has not published Fedora %s yet; Ryoku packages stay at their current version until it does", "Ryoku in a single pass. Nothing is written until you": "Ryoku en un solo paso. Nada se escribe hasta que", "Ryoku in-shell": "Ryoku in-shell", - "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s": "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s", "Ryoku install: disk=%s host=%s user=%s profile=%s wm=%s browser=%s shell=%s files=%s rashin=%s hidden=%s cachyos-layer=%s": "Instalación de Ryoku: disco=%s host=%s usuario=%s perfil=%s wm=%s navegador=%s shell=%s archivos=%s Rashin=%s oculto=%s cachyos-layer=%s", "Ryoku installed": "Ryoku instalado", "Ryoku installer": "Instalador Ryoku", @@ -8249,12 +8456,10 @@ "Ryoku shell": "Ryoku shell", "Ryoku shell installer": "Instalador shell Ryoku", "Ryoku ships %d browsers on this edition; pick the one you want, or none.": "Ryoku incluye %d navegadores en esta edición; elige el que quieras o ninguno.", - "Ryoku ships %d browsers on this edition; pick the one you want.": "Ryoku ships %d browsers on this edition; pick the one you want.", "Ryoku ships this and overwrites it on every update. Fork it into the overlay at %s to keep a change; `ryoku reset %s` undoes the fork.": "Ryoku ships this and overwrites it on every update. Fork it into the overlay at %s to keep a change; `ryoku reset %s` undoes the fork.", "Ryoku ships this and overwrites it on every update. Put your change in %s instead; Ryoku reads it but never overwrites it.": "Ryoku ships this and overwrites it on every update. Put your change in %s instead; Ryoku reads it but never overwrites it.", "Ryoku ships this and overwrites it on every update. The shell tree cannot be forked (a fork breaks the shell); change it through ryoku-shell, Ryoku Hub, or a plugin (`ryoku plugin`).": "Ryoku ships this and overwrites it on every update. The shell tree cannot be forked (a fork breaks the shell); change it through ryoku-shell, Ryoku Hub, or a plugin (`ryoku plugin`).", "Ryoku ships three browsers; pick the one you want, or none.": "Ryoku incluye tres navegadores; elige el que quieras o ninguno.", - "Ryoku ships three browsers; pick the one you want.": "Ryoku ships three browsers; pick the one you want.", "Ryoku ships x86_64 builds only": "Ryoku solo distribuye compilaciones x86_64", "Ryoku ships your desktop as packages, and every `ryoku update` re-lays the": "Ryoku ships your desktop as packages, and every `ryoku update` re-lays the", "Ryoku style wraps this widget; it keeps its own colours": "Ryoku style wraps this widget; it keeps its own colours", @@ -8279,7 +8484,6 @@ "SAVE AS TEMPLATE": "GUARDAR COMO PLANTILLA", "SAVE LIMIT": "GUARDAR LÍMITE", "SAVE PASSWORD": "SAVE PASSWORD", - "SAVE TO DEVICE": "GUARDAR EN DISPOSITIVO", "SAVE USER": "SAVE USER", "SAVED BERTHS": "SAVED BERTHS", "SAVED LOOKS": "MIRADAS GUARDADAS", @@ -8290,8 +8494,6 @@ "SCALE": "ESCALA", "SCAN %1": "SCAN %1", "SCAN DENIED ✦ TYPE YOUR KEY": "ESCANEO DENEGADO ✦ ESCRIBE TU CLAVE", - "SCANNING": "ESCANEANDO", - "SCENE": "SCENE", "SCREEN MIRRORING": "ESPEJO DE PANTALLA", "SCREEN PICKER": "SELECTOR DE PANTALLA", "SCREENSHOT": "CAPTURA", @@ -8501,7 +8703,6 @@ "Scale the entire picker interface. The range is 0.5 to 2.0.": "Scale the entire picker interface. The range is 0.5 to 2.0.", "Scale this widget without changing its anchor": "Scale this widget without changing its anchor", "Scale videos to the display": "Scale videos to the display", - "Scaled into the steps this device offers, not a number it has to fake.": "Escalado a los pasos que ofrece este dispositivo, no un número que tenga que simular.", "Scales the clock island, resting and expanded.": "Escala la isla del reloj, en reposo y expandida.", "Scales the shell surfaces and popouts together.": "Scales the shell surfaces and popouts together.", "Scallop Dot Clock": "Reloj de punto festoneado", @@ -8623,7 +8824,6 @@ "Search by tag, or describe it": "Search by tag, or describe it", "Search colours": "Search colours", "Search every area: blur, corners, dock…": "Busca en todas las áreas: desenfoque, esquinas, muelle…", - "Search every shortcut": "Buscar atajo", "Search failed": "Search failed", "Search failed. Check your connection.": "La búsqueda falló. Comprueba tu conexión.", "Search in %1, or type a path": "Busca en %1 o escribe una ruta", @@ -8656,6 +8856,7 @@ "Search, network and data": "Búsqueda, red y datos", "Searching": "Searching", "Searching %1…": "Searching %1…", + "Searching Flathub.": "Searching Flathub.", "Searching for nearby networks…": "Searching for nearby networks…", "Searching networks…": "Buscando redes…", "Searching…": "Buscando…", @@ -8706,7 +8907,6 @@ "Security key / Passkey": "Clave de seguridad / Passkey", "Security key behavior": "Comportamiento de clave de seguridad", "Security key or password": "Clave de seguridad o contraseña", - "See every shortcut.": "See every shortcut.", "See the targets that pause wallpapers and add a running process.": "See the targets that pause wallpapers and add a running process.", "See what each display is showing, change its placement, or protect it from updates.": "See what each display is showing, change its placement, or protect it from updates.", "See where your day goes": "Mira a dónde va tu día", @@ -8735,7 +8935,6 @@ "Selection frame": "Selection frame", "Selection, focus, and the main action colour.": "Selection, focus, and the main action colour.", "Selection, keyboard focus, and the main action.": "Selection, keyboard focus, and the main action.", - "Selects Main, Hero, or OkShell for Super+Space": "Selecciona Main, Hero u OkShell para Super+Espacio", "Semantic models": "Semantic models", "Semantic search model": "Semantic search model", "Semi Bold": "Semi negrita", @@ -8756,9 +8955,12 @@ "Send text as keyboard input": "Enviar texto como entrada de teclado", "Send the first reminder to plug in.": "Send the first reminder to plug in.", "Send the urgent reminder to plug in.": "Send the urgent reminder to plug in.", + "Send to %1": "Send to %1", + "Send to phone": "Send to phone", "Send traffic through %1": "Enviar tráfico a través de %1", "Sending file...": "Enviando archivo...", "Sending notification…": "Sending notification…", + "Sending screenshot": "Sending screenshot", "Sending…": "Enviando…", "Sensitivity": "Sensibilidad", "Sensor disconnected": "Sensor desconectado", @@ -8767,6 +8969,8 @@ "Sent": "Enviado", "Sent %1 files": "Enviados %1 archivos", "Sent 1 file": "Enviado 1 archivo", + "Sent to %1": "Sent to %1", + "Sent via KDE Connect": "Sent via KDE Connect", "Separate /home": "Carpeta /home separada", "Separate filters with spaces. Every filter must match. Names and paths cannot contain spaces.": "Separate filters with spaces. Every filter must match. Names and paths cannot contain spaces.", "Separate folder for video wallpapers. Defaults to the wallpaper directory.": "Carpeta separada para fondos de video. Por defecto, la carpeta de fondos.", @@ -8793,8 +8997,6 @@ "Set %1 exactly": "Set %1 exactly", "Set %1 minutes": "Establecer %1 minutos", "Set a LUKS passphrase:": "Establecer contraseña LUKS:", - "Set a password for your account.": "Establecer contraseña de la cuenta.", - "Set a password:": "Establecer contraseña:", "Set a separate root password": "Establecer una contraseña de root separada", "Set a time": "Establecer una hora", "Set as Live Preview": "Establecer como vista previa en vivo", @@ -8903,7 +9105,7 @@ "Shelf · 1 item · drag off the dock to use it": "Estante · 1 artículo · arrástrelo fuera del muelle para usarlo", "Shelf · drop files here": "Estante · soltar archivos aquí", "Shell": "Shell", - "Shell add-ons and bundles you've installed.": "Shell add-ons and bundles you've installed.", + "Shell add-ons and bundles with parts already on this computer.": "Shell add-ons and bundles with parts already on this computer.", "Shell and bar": "Shell and bar", "Shell command": "Shell command", "Shell corner radius": "Radio de la esquina de la carcasa", @@ -8924,6 +9126,7 @@ "Shima": "Shima", "Shima Equalizer": "Ecualizador Shima", "Shima Settings": "Configuración de Shima", + "Shima Studio": "Estudio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shima design": "diseño shima", "Shima is a modified version of iNiR's island design, rebuilt by Ryoku. It runs through Ryoku's window-manager layer, so it works on every compositor Ryoku supports.": "Shima es una versión modificada del diseño de la isla de iNiR, reconstruida por Ryoku. Se ejecuta a través de la capa del administrador de ventanas de Ryoku, por lo que funciona en todos los compositores que Ryoku admite.", @@ -8946,7 +9149,6 @@ "Show 'TIME' Top Header": "Mostrar encabezado superior 'TIEMPO'", "Show 24h Progress Arc": "Mostrar arco de progreso de 24 horas", "Show AM/PM Chip (12-Hour Mode)": "Mostrar chip AM/PM (modo de 12 horas)", - "Show Apply button": "Show Apply button", "Show Audio Device Pill": "Mostrar píldora del dispositivo de audio", "Show Battery Percentage": "Mostrar porcentaje de batería", "Show Battery Percentages": "Mostrar porcentajes de batería", @@ -8978,6 +9180,7 @@ "Show Outer Numbers (00-58)": "Mostrar números exteriores (00-58)", "Show Pexels in the download source picker.": "Show Pexels in the download source picker.", "Show Previous Button": "Mostrar botón anterior", + "Show Ryoku's Japanese glosses, decorative text and mark across the desktop. Off keeps English and Latin marks; CJK fonts, language support and your content are unaffected.": "Show Ryoku's Japanese glosses, decorative text and mark across the desktop. Off keeps English and Latin marks; CJK fonts, language support and your content are unaffected.", "Show Second Hand": "Mostrar Segunda Mano", "Show Sunset Gauge": "Mostrar indicador de puesta de sol", "Show Sunset Sub-Dial (Top)": "Mostrar subesfera Sunset (arriba)", @@ -8990,7 +9193,6 @@ "Show active workspace indicator": "Mostrar indicador de espacio de trabajo activo", "Show add event button": "Mostrar botón para agregar evento", "Show addresses": "Mostrar direcciones", - "Show an Apply button for this source. Change several filters, then press Apply or Enter to send one search request. When off, each filter change searches immediately.": "Show an Apply button for this source. Change several filters, then press Apply or Enter to send one search request. When off, each filter change searches immediately.", "Show answer": "Mostrar respuesta", "Show app dividers": "Mostrar divisores de aplicaciones", "Show app drawer button": "Mostrar botón del cajón de aplicaciones", @@ -9020,6 +9222,7 @@ "Show home-screen page counter": "Mostrar el contador de páginas de la pantalla de inicio", "Show hourly weather": "Mostrar el clima por hora", "Show icons": "Mostrar iconos", + "Show images in notifications": "Show images in notifications", "Show in calendar": "Mostrar en calendario", "Show in folder": "Mostrar en carpeta", "Show key presses": "Mostrar pulsaciones de tecla", @@ -9095,6 +9298,7 @@ "Shown in place": "Mostrado en su lugar", "Shown while the desktop shell restarts": "Mostrado mientras el shell de escritorio se reinicia", "Shows a copy of another display here": "Muestra una copia de otra pantalla aquí", + "Shows a sent image, or the app icon when no image is attached": "Shows a sent image, or the app icon when no image is attached", "Shows authoritative component state; terminal removal skips items that need manual uninstall": "Shows authoritative component state; terminal removal skips items that need manual uninstall", "Shows only remaining account credits, or the remaining cap of a limited key": "Muestra solo los créditos restantes de la cuenta o el límite restante de una clave limitada", "Shows the next anime episode on the Island. Its card lists what is coming and follows the shows you tap.": "Muestra el próximo episodio del anime en la Isla. Su tarjeta enumera lo que está por venir y sigue los programas que tocas.", @@ -9105,7 +9309,7 @@ "Shuffle wallpapers": "Fondos de pantalla aleatorios", "Shut Down": "Apagar", "Shut down": "Apagar", - "Shutdown": "Apagado", + "Shutdown": "Apagar", "Side": "Side", "Side Panels": "Paneles laterales", "Side panels": "Paneles laterales", @@ -9140,6 +9344,7 @@ "Since": "desde", "Since %1": "Desde %1", "Sine": "seno", + "Single click": "Single click", "Single file": "Single file", "Six": "seis", "Sixteen": "dieciséis", @@ -9168,7 +9373,7 @@ "Slant the card edges. Zero keeps them upright.": "Slant the card edges. Zero keeps them upright.", "Slant the top and bottom edges. Positive values raise the right edge; negative values raise the left edge.": "Slant the top and bottom edges. Positive values raise the right edge; negative values raise the left edge.", "Slanted": "inclinado", - "Sleep": "Sleep", + "Sleep": "Suspender", "Sleepy rounded surfaces": "Superficies redondeadas y somnolientas", "Sleet": "aguanieve", "Sleet showers": "Duchas de aguanieve", @@ -9220,7 +9425,6 @@ "Snapshot name": "Snapshot name", "Snapshots": "Snapshots", "Snapshots & rollback": "Instantáneas y reversión", - "Snapshots are not available on Fedora; recover the system by hand.": "Snapshots are not available on Fedora; recover the system by hand.", "Snippet": "Fragmento", "Snooze %1 min": "Posponer %1 min", "Snooze length": "Duración de la repetición", @@ -9339,7 +9543,6 @@ "Stacks gather loose files automatically.": "Las pilas reúnen archivos sueltos automáticamente.", "Stacks gather loose folders automatically.": "Las pilas reúnen carpetas sueltas automáticamente.", "Stage Editor": "Stage Editor", - "Stage Editor (GPL-3.0)": "Stage Editor (GPL-3.0)", "Stage Editor, ii dock, Kit, Phone (GPL-3.0)": "Editor de escenario, base ii, kit, teléfono (GPL-3.0)", "Stage and dock settings use their current stores": "Las configuraciones del escenario y el muelle utilizan sus tiendas actuales", "Stage captured — lift and touch again": "Etapa capturada - levanta y toca de nuevo", @@ -9468,7 +9671,6 @@ "Storage limits must be whole bytes, or 0 for no limit.": "Los límites de almacenamiento deben ser bytes completos o 0 para que no haya límite.", "Storage path": "Storage path", "Store aggregate scores locally, never typed text.": "Almacene puntuaciones agregadas localmente, nunca texto escrito.", - "Store this look in the device so it holds with Ryoku closed. It replaces what is in the device's active profile slot.": "Guardar este aspecto en el dispositivo para que se mantenga con Ryoku cerrado. Reemplaza lo que está en la ranura de perfil activa del dispositivo.", "Stormy": "Stormy", "Story": "Historia", "Straight": "Straight", @@ -9482,7 +9684,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Estudio", "Style": "Estilo", "Style & Appearance": "Estilo y apariencia", "Style name": "Style name", @@ -9610,7 +9812,6 @@ "Syncing...": "Sincronizando...", "Syncing…": "Sincronizando…", "System": "Sistema", - "System (required)": "System (required)", "System / Uptime": "Sistema/tiempo de actividad", "System Monitor": "Monitor del sistema", "System Uptime": "Tiempo de actividad del sistema", @@ -9627,7 +9828,6 @@ "System font": "Fuente del sistema", "System locale": "Locale del sistema", "System power": "System power", - "System services": "System services", "System setting": "Configuración del sistema", "System sound": "sonido del sistema", "System tray icons.": "Iconos de la tray del sistema.", @@ -9694,7 +9894,6 @@ "Take screenshot": "Tomar captura de pantalla", "Take this widget off the desktop": "Take this widget off the desktop", "Taking a snapshot": "Tomando snapshot", - "Talk to Hermes in the dashboard or a terminal.": "Habla con Hermes en el panel o en una terminal.", "Talk to your agent in the dashboard or a terminal.": "Habla con tu agente en el tablero o en una terminal.", "Tall": "Tall", "Tall canvas": "Tall canvas", @@ -9897,7 +10096,6 @@ "The colour theme's own": "El tema del color es propio.", "The colours come from one picture. Choosing another screen makes its picture the shared one; nothing on screen changes.": "Los colores provienen de una imagen. Elegir otra pantalla hace que su imagen sea compartida; nada en la pantalla cambia.", "The colours come from this screen's picture": "Los colores provienen de la imagen de esta pantalla.", - "The colours file Zen, Spotify and Vesktop read their theme from. Leave empty to use Ryoku's palette.": "The colours file Zen, Spotify and Vesktop read their theme from. Leave empty to use Ryoku's palette.", "The colours follow the video wallpaper. Give it a picture to take them from another screen.": "Los colores siguen el fondo de pantalla del vídeo. Dale una foto para tomarlas desde otra pantalla.", "The command could not be saved.": "No se pudo guardar el comando.", "The command file could not be read (%1)": "No se pudo leer el archivo de comando (%1)", @@ -10007,7 +10205,6 @@ "The now-playing bubble that grows beside the island.": "La burbuja de reproducción que crece junto a la isla.", "The official localsend-cli binary was not found. Install it from Settings → Devices & Phone → LocalSend.": "No se encontró el binario oficial localsend-cli. Instálelo desde Configuración → Dispositivos y teléfono → LocalSend.", "The on-screen keyboard.": "El teclado en pantalla.", - "The optional local agent OS. A resident Hermes agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.": "El SO opcional de agente local. Un agente Hermes residente mantiene un mapa vivo de esta máquina (hardware, paquetes, cada config junto al binario que la posee), así tus agentes de código leen el terreno sin redescubrirlo. Nada sale de la caja.", "The optional local agent OS. Your agent keeps a living map of this machine - hardware, packages, every config beside the binary that owns it - so your coding agents read the terrain instead of rediscovering it. Nothing ever leaves the box.": "El sistema operativo del agente local opcional. Su agente mantiene un mapa vivo de esta máquina (hardware, paquetes, cada configuración además del binario que la posee) para que sus agentes codificadores lean el terreno en lugar de redescubrirlo. Nada sale nunca de la caja.", "The output and input list.": "La lista de salida y entrada.", "The output level; its glyph mutes.": "El nivel de salida; su glifo se silencia.", @@ -10043,7 +10240,8 @@ "The previous config is still in use": "La configuración anterior todavía está en uso.", "The profile of every bubble, bar and satellite. Squircles and squares read like app tiles.": "El perfil de cada burbuja, barra y satélite. Las ardillas y los cuadrados se leen como mosaicos de aplicaciones.", "The proportional face used by the shell and apps.": "La cara proporcional utilizada por el shell y las aplicaciones.", - "The radio is off at the hardware level (rfkill); the toggle unblocks it.": "La radio está apagada a nivel de hardware (rfkill); el interruptor la desbloquea.", + "The radio is off at the hardware level.": "The radio is off at the hardware level.", + "The radio is software blocked; the toggle unblocks it.": "The radio is software blocked; the toggle unblocks it.", "The readout's lines: reorder, rename, remove": "Las líneas de la lectura: reordenar, renombrar, eliminar", "The remote engine (ryossh) is not installed. Build it from ryoku/apps/ryovm/remote.": "El motor remoto (ryossh) no esta instalado. Compilalo desde ryoku/apps/ryovm/remote.", "The request failed.": "The request failed.", @@ -10056,7 +10254,6 @@ "The same edge where it does not face the light. With Blur it also smooths the stepped curves.": "El mismo borde donde no da a la luz. Con Blur también suaviza las curvas escalonadas.", "The same motion when opening and closing": "El mismo movimiento al abrir y cerrar.", "The same, every switch a disc with its name under it.": "Lo mismo, cada cambio un disco con su nombre debajo.", - "The scene could not be cut": "The scene could not be cut", "The scene's properties could not be read.": "The scene's properties could not be read.", "The schedule could not be loaded.": "The schedule could not be loaded.", "The schedule could not be saved.": "The schedule could not be saved.", @@ -10066,9 +10263,9 @@ "The seed colour the scheme derives from.": "The seed colour the scheme derives from.", "The selected model is available on this computer": "El modelo seleccionado está disponible en este ordenador.", "The selected path is not a readable folder or file.": "La ruta seleccionada no es una carpeta o un archivo legible.", + "The selected phone is no longer available": "The selected phone is no longer available", "The selected variant follows the current wallpaper.": "La variante seleccionada sigue el fondo de pantalla actual.", "The sensor gave up — try again": "El sensor se rindió - intenta de nuevo", - "The service manager could not save this change.": "The service manager could not save this change.", "The shape behind the magnifier": "La forma detrás de la lupa.", "The shared dock is configured in Stage Editor, regardless of which shell or bar is active.": "El Dock compartido se configura en Stage Editor, independientemente de qué shell o barra esté activa.", "The shared picker UI pace. Change the Fast, Standard, and Slow weights in Motion settings.": "The shared picker UI pace. Change the Fast, Standard, and Slow weights in Motion settings.", @@ -10110,7 +10307,6 @@ "The update stopped before it finished (%v).": "The update stopped before it finished (%v).", "The update stopped before it finished.": "The update stopped before it finished.", "The update was no longer running.": "The update was no longer running.", - "The wallpaper is drawn flat behind the desktop.": "The wallpaper is drawn flat behind the desktop.", "The wallpaper stays; every generated and added layer is removed": "The wallpaper stays; every generated and added layer is removed", "The wheel over the dock turns between saved layouts": "La rueda sobre el muelle gira entre diseños guardados", "The whole bar collapsed into one floating island": "La barra entera se convirtió en una isla flotante", @@ -10173,10 +10369,6 @@ "This device": "este dispositivo", "This device cannot store the selected look in onboard memory. Ryoku reapplies it after login instead.": "Este dispositivo no puede almacenar el aspecto seleccionado en la memoria integrada. Ryoku lo vuelve a aplicar después de iniciar sesión.", "This device did not provide a usable Bluetooth address.": "This device did not provide a usable Bluetooth address.", - "This device does not offer storing a look in its own memory, so Ryoku puts it back at every login instead.": "Este dispositivo no ofrece guardar un aspecto en su propia memoria, así que Ryoku lo restablece en cada inicio de sesión.", - "This device lights in parts; each one takes its own colour.": "Este dispositivo se ilumina por partes; cada una toma su propio color.", - "This device's effects": "Efectos de este dispositivo", - "This device's memory": "Memoria de este dispositivo", "This display no longer sets the colours.": "This display no longer sets the colours.", "This drawing's file is missing.": "Falta el archivo de este dibujo.", "This edition cannot remove the Voxtype package from the Hub.": "This edition cannot remove the Voxtype package from the Hub.", @@ -10204,7 +10396,6 @@ "This network type needs Wi-Fi settings to connect": "Este tipo de red necesita configuración Wi-Fi para conectar", "This ordered rule stack is active.": "This ordered rule stack is active.", "This output's workspaces; tap for Overview or a workspace card.": "Los espacios de trabajo de esta salida; toque para ver Descripción general o una tarjeta de espacio de trabajo.", - "This package is not available on this edition.": "This package is not available on this edition.", "This page is being rebuilt into the monochrome instrument. Its settings and surfaces are wired page by page; the Shell page is the proven pattern.": "Esta página se está reconstruyendo en el instrumento monocromático. Sus configuraciones se conectan página por página; la página Shell es el patrón probado.", "This palette could not be saved.": "This palette could not be saved.", "This palette is applied to the preview only": "This palette is applied to the preview only", @@ -10375,7 +10566,6 @@ "Tools": "Tools", "Tools baked in on first boot, clip is always on.": "Herramientas integradas al primer arranque, clip siempre activo.", "Toolset": "Conjunto de herramientas", - "Toolsets Hermes wields: search, files, the web.": "Conjuntos de herramientas que Hermes maneja: búsqueda, archivos, la web.", "Toolsets your agent wields: search, files, the web.": "Conjuntos de herramientas que utiliza su agente: búsqueda, archivos, la web.", "Top": "Superior", "Top and bottom": "Arriba y abajo", @@ -10485,7 +10675,6 @@ "Turn the adapter on to see nearby and paired devices.": "Enciende el adaptador para ver dispositivos cercanos y emparejados.", "Turn the dock on to choose its utility tiles and folders.": "Encienda el Dock para elegir sus carpetas y mosaicos de utilidades.", "Turn the touchpad off, the way the FN touchpad key does.": "Apagar el touchpad, como la tecla FN.", - "Turning this off puts the device back to %1, the effect it was on before Ryoku touched it.": "Desactivar esto devuelve el dispositivo a %1, el efecto que tenía antes de que Ryoku lo tocara.", "Turns every effect switch here on at once": "Activa todos los efectos a la vez", "Turns every effect switch here on at once.": "Activa todos los efectos a la vez.", "Turns glass, blur and heavy wallpaper effects off everywhere.": "Desactiva los efectos de vidrio, desenfoque y papel tapiz pesado en todas partes.", @@ -10513,7 +10702,6 @@ "Type the digits, or pick a preset": "Escribe los dígitos o elige un ajuste preestablecido", "Type to begin": "Escribe para comenzar", "Type to filter · j/k or ↑↓ to move.": "Escribe para filtrar · j/k o ↑↓ para mover.", - "Type to search": "Escriba para buscar", "Type to search or use the mode controls": "Escribe para buscar o usa los controles de modo", "Type to translate": "Escribe para traducir", "Type your PIN in the window that opened — Android blanks the PIN screen, so it stays black": "Escriba su PIN en la ventana que se abrió: Android deja la pantalla del PIN en blanco, por lo que permanece negra", @@ -10528,6 +10716,7 @@ "UI scale": "Escala de UI", "UK / Ireland": "Reino Unido / Irlanda", "UNAPPLIED LAYOUT CHANGES": "CAMBIOS DE DISEÑO SIN APLICAR", + "UNAVAILABLE": "UNAVAILABLE", "UNAVAILABLE ON %1": "NO DISPONIBLE EN %1", "UNDER THE HOOD": "BAJO EL CAPÓ", "UNDO": "DESHACER", @@ -10688,6 +10877,7 @@ "Update failed while %1": "Actualización falló mientras %1", "Update failed while %s": "Update failed while %s", "Update now": "Actualizar ahora", + "Update now also takes the distribution's packages, kernel included (ryoku update --system)": "Update now also takes the distribution's packages, kernel included (ryoku update --system)", "Update preserves placement; Remove deletes the add-on and its settings": "Update preserves placement; Remove deletes the add-on and its settings", "Update running": "Actualización en curso", "Update stopped": "Update stopped", @@ -10722,7 +10912,6 @@ "Urgent notification": "Notificación urgente", "Urgent reminder to plug in. Must be below the warning percentage.": "Urgent reminder to plug in. Must be below the warning percentage.", "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update --wait wait for a channel build or source check to finish\n update --force update despite an in-progress build or source check\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help": "Uso: ryoku \n\n actualizar actualizar los paquetes de Ryoku (o confirmaciones de canales), volver a implementar, recargar\n actualizar: el sistema es el mismo, más la actualización del paquete de su distribución\n actualización: espera, espera a que finalice la compilación del canal o la verificación de la fuente.\n actualizar: forzar la actualización a pesar de una compilación en progreso o una verificación de fuente\n actualizar -v lo mismo, transmitiendo la salida sin procesar completa en lugar del resumen\n actualización: gui ejecuta la actualización en segundo plano, impulsada desde Ryoku Hub\n rastrear paquetes : estable|inestable|v; agregue --source para compilar desde un pago\n versiones e instantáneas de la lista de reversión; --to regresa a una versión; [id] guía una restauración de instantánea\n estado del canal, versión instalada/última, cambios pendientes, recuento de instantáneas\n versión []; --pretty -> · , --branch -> canal\n propietario ... [--json] | --map: clasificar la propiedad de la configuración\n materializar (interno) componer base inmutable + user_edits en ~/.config\n doctor converge drift (--verificar informes solamente; --explicar muestra todos los conciliadores)\n wm list|status|caps|config|act ... (costura del administrador de ventanas respaldada por el proveedor)\n abstracción interna del host utilizada por los scripts y el Hub\n conjunto de enlace|reset ; lista [--json]\n complemento nuevo|validar|añadir|eliminar|compartir...\n agregar barra|eliminar|mover|mostrar|ocultar|lista|configuración|posición|formulario...\n ayuda mostrar esta ayuda", - "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help": "Usage: ryoku \n\n update update the Ryoku packages (or channel commits), redeploy, reload\n update --system the same, plus your distribution's package upgrade\n update -v the same, streaming the full raw output instead of the summary\n update --gui run the update in the background, driven from Ryoku Hub\n track packages: stable|unstable|v; add --source to build from a checkout\n rollback list releases and snapshots; --to moves back to a release; [id] guides a snapshot restore\n status show channel, installed/latest version, pending changes, snapshot count\n version []; --pretty -> · , --branch -> channel\n owner ... [--json] | --map: classify config ownership\n materialize (internal) compose immutable base + user_edits into ~/.config\n doctor converge drift (--check reports only; --explain shows every reconciler)\n wm list|status|caps|config|act ... (provider-backed window-manager seam)\n host internal host abstraction used by scripts and the Hub\n bind set|reset ; list [--json]\n plugin new|validate|add|remove|share ...\n bar add|remove|move|show|hide|list|settings|position|form ...\n help show this help", "Usage: ryoku debug # print a shareable diagnostic bundle for bug reports": "Uso: ryoku debug # imprime un paquete diagnóstico compartible para reportes de bugs", "Usage: ryoku doctor [--check] [--verbose] [--report [file]] [--explain]\n\n (no args) check, and apply the safe automatic fixes\n --check, -n report what is wrong without changing anything\n --verbose, -v also list the checks that passed and advisory notes\n --report [file] write a shareable diagnostic report for the maintainers\n (default: %s)\n --explain ask your cloud model (Groq/OpenRouter) to reason over the report\n --json emit findings as JSON (read-only; powers the Hub System Check)": "Uso: ryoku doctor [--check] [--verbose] [--report [archivo]] [--explain]\n\n (sin args) comprueba y aplica los arreglos automáticos seguros\n --check, -n reporta qué está mal sin cambiar nada\n --verbose, -v también lista las comprobaciones aprobadas y notas informativas\n --report [archivo] escribe un reporte diagnóstico compartible para mantenedores\n (predeterminado: %s)\n --explain pide a tu modelo en la nube (Groq/OpenRouter) razonar sobre el reporte\n --json emite hallazgos como JSON (solo lectura; alimenta el System Check del Hub)", "Usage: ryoku import [flags]\n\n a config folder, a dropped ~/.config, or a dotfiles tree\n --url clone a dotfiles repo instead of reading a local path\n --keep mine|ryoku how to settle every keybind clash (default: mine)\n --undo [] roll back the last import, or the one stamped \n\nHeadless: scan, resolve all conflicts to one side, apply, print the change set.\nThe Hub's Import page is the interactive path for reviewing clash by clash.": "Uso: ryoku import [flags]\n\n carpeta de config, ~/.config copiado, o árbol de dotfiles\n --url clona un repo de dotfiles en lugar de leer una ruta local\n --keep mine|ryoku cómo resolver cada conflicto de keybind (predeterminado: mine)\n --undo [] revierte la última importación o la marcada \n\nSin interfaz: escanea, resuelve todos los conflictos a un lado, aplica, imprime los cambios.\nLa página Import del Hub es la ruta interactiva para revisar conflicto por conflicto.", @@ -10743,7 +10932,6 @@ "Use Add security key to enroll another key, or remove an old one below.": "Use Añadir clave de seguridad para registrar otra, o elimine una antigua abajo.", "Use Android debugging over the local network.": "Utilice la depuración de Android a través de la red local.", "Use Black Background (WearOS style)": "Usar fondo negro (estilo WearOS)", - "Use Depth": "Use Depth", "Use Expressive Colors": "Utilice colores expresivos", "Use GPS when there is one": "Usar GPS cuando haya uno", "Use ISOLATE from the physical machine; every remote connection is cut immediately.": "Usa AISLAR desde la máquina física; cada conexión remota se corta inmediatamente.", @@ -10754,7 +10942,6 @@ "Use Material Symbol for outer icon": "Utilice el símbolo de material para el icono exterior", "Use Matugen for wallpaper colours. Turning this off uses built-in Iris.": "Use Matugen for wallpaper colours. Turning this off uses built-in Iris.", "Use Organic visualizer": "Utilice visualizador orgánico", - "Use Parallax": "Use Parallax", "Use To-Do context": "Usar el contexto de tareas pendientes", "Use a Material Symbol name or an SVG/distro name from the icons folder.": "Utilice un nombre de símbolo de material o un nombre SVG/distribución de la carpeta de iconos.", "Use a Material Symbol name, distro, or an SVG name from the icons folder.": "Utilice un nombre de símbolo de material, una distribución o un nombre SVG de la carpeta de iconos.", @@ -10766,7 +10953,6 @@ "Use adaptive alignment": "Utilice alineación adaptativa", "Use add --full-name --role admin|standard [--perm id]... [--limit bytes].": "Utilice agregar --full-name --role admin|estándar [--perm id]... [--limit bytes].", "Use album colors": "Usar colores del álbum", - "Use an existing transparent cut-out": "Use an existing transparent cut-out", "Use an image, video, or Wallpaper Engine scene behind the overview. Blur and dim it independently of the desktop wallpaper.": "Use an image, video, or Wallpaper Engine scene behind the overview. Blur and dim it independently of the desktop wallpaper.", "Use as default": "Usar como predeterminado", "Use as exit": "Usar como salida", @@ -10827,7 +11013,6 @@ "User Card": "Tarjeta de usuario", "User accounts are not available on this system.": "Las cuentas de usuario no están disponibles en este sistema.", "User card": "tarjeta de usuario", - "User password": "Contraseña de usuario", "User script: %1": "Guión de usuario: %1", "User verification": "Verificación de usuario", "Username": "Nombre de usuario", @@ -11038,7 +11223,6 @@ "Waiting for Claude Code…": "Esperando el Código Claude...", "Waiting for a pen — a mouse always draws even lines": "Esperando un bolígrafo: el ratón siempre dibuja líneas uniformes", "Waiting for ani-cli": "Esperando ani-cli", - "Waiting for approval: %1": "Waiting for approval: %1", "Waiting for browser...": "Esperando al navegador...", "Waiting for samples": "Waiting for samples", "Waiting for the %s channel build of %s to finish (%s)": "Esperando a que finalice la construcción del canal %s de %s (%s)", @@ -11217,7 +11401,6 @@ "Well done 💧": "Bien hecho 💧", "WezTerm": "WezTérmino", "What ? and Search the web use in Spotlight.": "Qué ? y Buscar el uso web en Spotlight.", - "What Hermes remembers, carried across every session.": "Lo que Hermes recuerda, llevado a cada sesión.", "What Shima steps back from while you play": "De qué se aleja Shima mientras juegas", "What a switch lights up with. Colourful gives each one its own hue; Mono lights them white.": "Con qué interruptor se ilumina. El colorido le da a cada uno su propia tonalidad; Mono los ilumina en blanco.", "What an open card or panel is lit by: its own colour — the sky for weather, the highlight for timers — or the wallpaper's. It pours in from where the body grew.": "Lo que ilumina una tarjeta o panel abierto: su propio color (el cielo para el clima, lo más destacado para los temporizadores) o el del fondo de pantalla. Se derrama desde donde creció el cuerpo.", @@ -11278,7 +11461,6 @@ "Whether you are on a VPN, as a shield that fills when you are. Its card lists your NetworkManager profiles and Tailscale, and turns any of them on or off.": "Ya sea que estés en una VPN, como un escudo que se llena cuando lo estás. Su tarjeta enumera sus perfiles de NetworkManager y Tailscale, y activa o desactiva cualquiera de ellos.", "Which GPU renders the desktop; next login": "Qué GPU renderiza el escritorio; próximo inicio de sesión", "Which agent answers the Alt+Space bar's Agent mode. Hermes is recommended; others need their own ACP adapter installed.": "Qué agente responde al modo Agente de la barra Alt+Espacio. Se recomienda Hermes; otros necesitan tener instalado su propio adaptador ACP.", - "Which agent answers the Super+S chat. Hermes is recommended; others need their own ACP adapter installed.": "Qué agente responde al chat Super+S. Se recomienda Hermes; otros necesitan su propio adaptador ACP instalado.", "Which app each launcher key opens, and the key itself": "Qué app abre cada tecla del lanzador, y la tecla en sí", "Which bar the desktop draws, and how it looks.": "Qué barra dibuja el escritorio y cómo se ve.", "Which coding-agent meters the AI pill shows.": "Qué medidor del agente de código muestra la píldora IA.", @@ -11322,7 +11504,6 @@ "Wi-Fi radio": "Wi-Fi radio", "Wi-Fi regulatory domain: %s": "Dominio regulatorio Wi-Fi: %s", "Wi-Fi regulatory domain: %s not applied yet (ryoku-wifi-regdom is not in this target;": "Dominio regulatorio Wi-Fi: %s no aplicado aún (ryoku-wifi-regdom no está en este destino;", - "Wi-Fi, Bluetooth and this machine's hotspot, applied live.": "Wi-Fi, Bluetooth y el punto de acceso de esta máquina, aplicado en vivo.", "Wi-Fi, Bluetooth, hotspot and phone connections, applied live.": "Conexiones Wi-Fi, Bluetooth, hotspot y teléfono, aplicadas en vivo.", "Wide": "Ancho", "Wide banner behind the palette; drag to reframe it": "Banner ancho detrás de la paleta; arrastrar para reencuadrarlo", @@ -11490,8 +11671,8 @@ "You have forked this. Edit the overlay copy at %s; `ryoku reset %s` drops the fork and takes Ryoku's version again.": "You have forked this. Edit the overlay copy at %s; `ryoku reset %s` drops the fork and takes Ryoku's version again.", "You planned to stop at %1.": "Planeabas detenerte en %1.", "You type its passphrase at every boot.": "Escribe su frase de contraseña en cada arranque.", - "You're all caught up": "Estáis todos atrapados", - "You're all caught up.": "Estáis todos atrapados.", + "You're all caught up": "No hay notificaciones disponibles", + "You're all caught up.": "No hay notificaciones disponibles.", "You're up to date. Recent changes will appear here once loaded.": "Estás al día. Los cambios recientes aparecerán aquí una vez cargados.", "YouTube": "YouTube", "YouTube loops are often hours long. Download only this many minutes; each video can override this before download. 0 downloads everything.": "YouTube loops are often hours long. Download only this many minutes; each video can override this before download. 0 downloads everything.", @@ -11509,7 +11690,6 @@ "Your fork tree lives at `%s`. A regular file here wins over Ryoku's copy at\nthe mirrored ~/.config path on every update.": "Your fork tree lives at `%s`. A regular file here wins over Ryoku's copy at\nthe mirrored ~/.config path on every update.", "Your graphics driver exposes no tunable knobs on this session. Everything here is read-only on this hardware.": "Tu controlador gráfico no expone controles ajustables en esta sesión. Todo aquí es de solo lectura en este hardware.", "Your keybinds are in the Keybinds page and conflict-checked from now on; the rest of your config is layered in and live. Reload the desktop to see it, or undo the whole import.": "Tus atajos están en la página de Atajos y se comprueban los conflictos de ahora en adelante; el resto de tu configuración se aplica y está activa. Recarga el escritorio para verlo, o deshaz toda la importación.", - "Your login account name. Lowercase.": "Tu nombre de cuenta. Minúsculas.", "Your modules": "Tus módulos", "Your music appears here": "Tu música aparece aquí.", "Your name": "tu nombre", @@ -11518,7 +11698,6 @@ "Your own override file. Ryoku reads it but never ships or overwrites it, so this is where a tweak belongs.": "Your own override file. Ryoku reads it but never ships or overwrites it, so this is where a tweak belongs.", "Your paired phone's screen, a click away": "La pantalla de tu teléfono vinculado, a un clic de distancia", "Your password": "Your password", - "Your picks are kept in ~/.config/ryoku/lighting.json: an update, a shell restart or a reboot leaves them exactly as they are.": "Tus selecciones se guardan en ~/.config/ryoku/lighting.json: una actualización, reinicio de shell o reinicio los deja exactamente como están.", "Your place is saved as you watch": "Tu lugar se guarda mientras miras", "Your quote": "Tu cotización", "Your screen runs on %1. Switch to Hybrid GPU mode in the BIOS (look for GPU Mode, MUX, or Hybrid/Optimus) and reboot, so the built-in GPU drives the display and the discrete GPU is free.": "Tu pantalla funciona con %1. Cambia al modo GPU Híbrido en la BIOS (busca Modo GPU, MUX o Híbrido/Optimus) y reinicia, para que la GPU integrada controle la pantalla y la GPU discreta esté libre.", @@ -11628,6 +11807,7 @@ "advice: %s": "advice: %s", "after": "after", "air %1 px": "aire %1 px", + "aligning newer [ryoku] package overrides: %s": "aligning newer [ryoku] package overrides: %s", "all": "todo", "all / favourites": "all / favourites", "all checks passed": "todas las verificaciones pasaron", @@ -11657,15 +11837,12 @@ "alongside region: sectors %s-%s (%sGiB); boot %s-%s, root %s-%s": "Región junto a sistema: sectores %s-%s (%sGiB); arranque %s-%s, raíz %s-%s", "alongside · btrfs %dsv": "junto · btrfs %dsv", "already on %s; nothing to move, `ryoku update` keeps it current": "already on %s; nothing to move, `ryoku update` keeps it current", - "already on unstable (the channel `ryoku track unstable-dev` used to select); nothing to move, `ryoku update` keeps it current": "already on unstable (the channel `ryoku track unstable-dev` used to select); nothing to move, `ryoku update` keeps it current", - "already pinned to unstable build %s; nothing to move, `ryoku track unstable` follows unstable builds again": "already pinned to unstable build %s; nothing to move, `ryoku track unstable` follows unstable builds again", "already pinned to unstable build %s; nothing to move, `ryoku track unstable` switches to the source checkout": "ya fijado a la compilación inestable %s; nada que moverse, `ryoku track unstable` cambia al pago de origen", "already provided by installed packages: %s": "ya proporcionado por los paquetes instalados: %s", "an older one": "an older one", "an update cutover is running; its sleep guard is live": "an update cutover is running; its sleep guard is live", "an update is running; run-state is live": "una actualización se está ejecutando; run-state está activo", "and": "and", - "and is drawing %.1f W": "y está consumiendo %.1f W", "android-tools (adb)": "herramientas de Android (adb)", "ani-cli stopped": "ani-cli se detuvo", "animations": "animaciones", @@ -11683,7 +11860,6 @@ "ask": "preguntar", "ask the Rashin agent (one terse answer)": "preguntar al agente Rashin (una respuesta breve)", "asusctl was removed by hand; leaving the Aura keyboard unmanaged": "asusctl fue removido manualmente; teclado Aura sin gestionar", - "asusd is installed but not running, so the Aura keyboard is absent from Appearance": "asusd instalado pero no ejecutándose, teclado Aura ausente de Apariencia", "asusd is installed but not running, so the Aura keyboard is absent from Ryogami lighting settings": "asusd está instalado pero no se está ejecutando, por lo que el teclado Aura no está en la configuración de iluminación de Ryogami", "at least": "at least", "at most": "at most", @@ -11821,17 +11997,10 @@ "by %1": "por %1", "by %1 ↗": "por %1 ↗", "cachyos: applying CachyOS system identity": "cachyos: aplicación de la identidad del sistema CachyOS", - "cachyos: configuring the CachyOS repositories in the target": "cachyos: configurando repositorios CachyOS en el destino", "cachyos: keyring populate failed (cachyos-keyring absent?); a later -Syu can 'pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com'": "cachyos: falló poblado de llavero (¿cachyos-keyring ausente?); un -Syu posterior puede ejecutar 'pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com'", - "cachyos: no %s in the target yet; skipping repo config": "cachyos: sin %s en destino aún; omitiendo config de repo", - "cachyos: no [core] anchor in %s; appending the CachyOS repos": "cachyos: sin ancla [core] en %s; añadiendo repos CachyOS", - "cachyos: repositories already present": "cachyos: repositorios ya presentes", "cachyos: suppressing CachyOS Welcome autostart": "cachyos: suprimiendo el inicio automático de bienvenida de CachyOS", "cachyos: the target pacman does not resolve Architecture = auto to %s; naming it explicitly": "cachyos: el pacman objetivo no resuelve Arquitectura = auto a %s; nombrándolo explícitamente", "cachyos: trusted the CachyOS signing key via the installed keyring": "cachyos: clave de firma CachyOS confiada vía llavero instalado", - "cachyos: warning, could not add the CachyOS repo to %s": "cachyos: advertencia, no se pudo añadir el repo de CachyOS a %s", - "cachyos: warning, could not add the CachyOS repos to %s": "cachyos: aviso, no se pudieron añadir repos CachyOS a %s", - "cachyos: warning, could not enable [multilib] (lib32 updates will be unavailable)": "cachyos: aviso, no se pudo activar [multilib] (actualizaciones lib32 no disponibles)", "caelestia": "caelestia", "calculator": "calculadora", "camera.png": "camera.png", @@ -11862,7 +12031,6 @@ "cannot switch to %s yet: %s is unavailable on this host": "cannot switch to %s yet: %s is unavailable on this host", "cannot switch to %s: %s": "cannot switch to %s: %s", "cannot verify installed kernel metadata for %s; its module tree is incomplete": "no se pueden verificar los metadatos del kernel instalado para %s; su árbol de módulos está incompleto", - "cannot verify installed kernel metadata: incomplete module tree(s) under %s: %s": "cannot verify installed kernel metadata: incomplete module tree(s) under %s: %s", "caps:escape": "caps:escape", "caps:none": "caps:none", "caps:swapescape": "caps:swapescape", @@ -11907,7 +12075,6 @@ "check `btrfs filesystem usage /`; may need `btrfs balance` or more free space": "verificar `btrfs filesystem usage /`; puede necesitar `btrfs balance` o más espacio", "check sudo access, then re-run ryoku doctor": "verificar acceso sudo, luego ejecutar ryoku doctor de nuevo", "check the ESP and rerun ryoku doctor": "verifique el ESP y vuelva a ejecutar ryoku doctor", - "check the ESP and rerun sudo ryoku doctor": "verificar ESP y ejecutar sudo ryoku doctor de nuevo", "check_box_outline_blank": "check_box_outline_blank", "checked: tip %s failed its checks (%s)": "comprobado: consejo %s falló sus comprobaciones (%s)", "checked: tip %s is being checked (%s)": "comprobado: el consejo %s está siendo verificado (%s)", @@ -11929,7 +12096,6 @@ "clear Limine remembered entry: %w": "clear Limine remembered entry: %w", "clear Limine remembered-entry attributes: %w": "clear Limine remembered-entry attributes: %w", "cleared %d failed unit(s) with nothing left wrong (stale app launch units, TPM NvPCR setup)": "se borraron %d unidades fallidas sin ningún error (unidades de inicio de aplicaciones obsoletas, configuración de TPM NvPCR)", - "cleared %d failed unit(s) with nothing left wrong (stale app scopes, TPM NvPCR setup)": "cleared %d failed unit(s) with nothing left wrong (stale app scopes, TPM NvPCR setup)", "cleared a crashed update's run-state": "estado de ejecución de actualización fallida borrado", "cleared the GPU render pin on %s; the discrete GPU can runtime-suspend after the next Hyprland login (switch back with `ryoku-gpu mode performance`)": "Se borró el pin de renderizado de la GPU en %s; la GPU discreta puede suspenderse en tiempo de ejecución tras el próximo inicio de sesión en Hyprland (vuelva con `ryoku-gpu mode performance`)", "cleared the broken brand image (%s); the mark falls back to the text seal": "se borró la imagen de marca dañada (%s); la marca recurre al sello de texto", @@ -11963,6 +12129,7 @@ "colours follow the wallpaper, or a palette is locked by choice": "colours follow the wallpaper, or a palette is locked by choice", "column": "columna", "columnwidth": "ancho de columna", + "com.github.wwmm.easyeffects": "com.github.wwmm.easyeffects", "comma-separated optional components to leave out": "componentes opcionales separados por comas para omitir", "command": "comando", "command to run": "comando a ejecutar", @@ -12138,7 +12305,6 @@ "could not remove %s, which the target release does not serve; the downgrade may fail on its exact pins (%v)": "could not remove %s, which the target release does not serve; the downgrade may fail on its exact pins (%v)", "could not remove %s: %v": "no se pudo quitar %s: %v", "could not remove home-deployed artifact(s) or reload repaired user units: %s": "no se pudieron eliminar los artefactos implementados en el hogar ni recargar las unidades de usuario reparadas: %s", - "could not remove home-deployed artifact(s) still shadowing the packaged install: %s": "no se pudo quitar artefacto(s) en home que sombra la instalación empaquetada: %s", "could not remove incompatible %s: %v": "could not remove incompatible %s: %v", "could not remove nested session bus wrappers (exit %d)": "could not remove nested session bus wrappers (exit %d)", "could not remove orphaned theme.lua: %v": "no se pudo quitar theme.lua huérfano: %v", @@ -12257,7 +12423,6 @@ "default file manager: warning, could not set %s for %s (continuing)": "administrador de archivos predeterminado: advertencia, no se pudo configurar %s para %s (continuación)", "default sink %s not listed": "salida por defecto %s no listada", "defaulted the bar to Orpheus / reduced any Sumi frame to left-only": "puso la barra por defecto en Orfeo / redujo cualquier cuadro de Sumi a solo la izquierda", - "defaulted the bar to qsbar / reduced any Sumi frame to left-only": "barra predeterminada a qsbar, marco Sumi solo a la izquierda", "deferring the initramfs build to the bootloader step: masking %s": "aplazando compilación initramfs al paso cargador: enmascarando %s", "deg": "deg", "degraded": "degraded", @@ -12319,7 +12484,6 @@ "dns: would verify name resolution and write fallback resolvers (1.1.1.1, 9.9.9.9, 8.8.8.8) if it fails": "dns: verificaría resolución nombres y escribiría resolvers respaldo (1.1.1.1, 9.9.9.9, 8.8.8.8) si falla", "do not match": "no coinciden", "dock config store": "almacén de config del dock", - "dock settings use the universal dock store": "dock settings use the universal dock store", "does not carry": "no contiene", "does not match": "no coincide", "done": "hecho", @@ -12365,6 +12529,7 @@ "e2fsck found errors it could not fix on %s (rc=%s); refusing to shrink.": "e2fsck encontró errores que no pudo corregir en %s (rc=%s); rechazando reducción.", "earlier Fedora boot-menu leftovers": "Restos anteriores del menú de inicio de Fedora", "earlier backups sit alongside; the oldest holds your pre-Ryoku configs": "copias anteriores están junto a esta; la más antigua contiene tus configs pre-Ryoku", + "easyeffects": "easyeffects", "edit": "editar", "edit %s by hand and delete: %s": "editar %s manualmente y eliminar: %s", "edit: %s": "edit: %s", @@ -12387,7 +12552,6 @@ "enabled plugin(s) built for another compositor build and this box cannot rebuild them (missing %s): %s": "plugins habilitados construidos para otra compilación de compositor y esta caja no puede reconstruirlos (falta %s): %s", "enabled plugin(s) built for another compositor build: %s": "plugins habilitados construidos para otra compilación de compositor: %s", "enabled rashin at boot (the Alt+Space bar and AI dashboard); `ryoku-rashin disable` turns it off": "Rashin habilitado en el arranque (la barra Alt+Espacio y el panel de IA); `ryoku-rashin deshabilitar` lo apaga", - "enabled rashin at boot (the Super+S needle and AI dashboard); `ryoku-rashin disable` turns it off": "rashin habilitado al arrancar (la aguja Super+S y el panel de IA); `ryoku-rashin disable` lo desactiva", "enabled rashin for this and future sessions; `ryoku-rashin disable` turns it off": "enabled rashin for this and future sessions; `ryoku-rashin disable` turns it off", "enabled snapper-cleanup.timer": "snapper-cleanup.timer activado", "enabled the AI usage collector service": "enabled the AI usage collector service", @@ -12458,11 +12622,6 @@ "fastfetch's OS line names the release": "la línea SO de fastfetch nombra el lanzamiento", "fastfetch's OS line now names the release (ryoku version --pretty)": "la línea SO de fastfetch ahora nombra el lanzamiento (ryoku version --pretty)", "fastfetch's OS line predates named releases (ryoku version -> ryoku version --pretty)": "la línea SO de fastfetch es anterior a los lanzamientos nombrados (ryoku version -> ryoku version --pretty)", - "feature: LAN file sharing (the pill's file stash speaks it; AUR)": "feature: LAN file sharing (the pill's file stash speaks it; AUR)", - "feature: Recognize Music in the launcher": "feature: Recognize Music in the launcher", - "feature: keyboard/mouse lighting follows the wallpaper palette": "feature: keyboard/mouse lighting follows the wallpaper palette", - "feature: ryoshot Beautify HD and ryowalls Enhance": "feature: ryoshot Beautify HD and ryowalls Enhance", - "feature: voice dictation on Super+` (AUR)": "feature: voice dictation on Super+` (AUR)", "federated apps, open windows, web and quick math": "aplicaciones federadas, ventanas abiertas, web y cálculo rápido", "feels": "se siente", "fetch recovery script from %s: %w": "obtener script de recuperación de %s: %w", @@ -12498,6 +12657,7 @@ "flushing the target to disk": "sincronizando destino a disco", "focus": "foco", "focused": "enfocado", + "folder": "folder", "folder name or path": "folder name or path", "folder:nature/forest": "folder:nature/forest", "follow-mouse default": "predeterminado seguir ratón", @@ -12507,7 +12667,6 @@ "follow-wallpaper default": "follow-wallpaper default", "follow-wallpaper default already reconciled": "follow-wallpaper default already reconciled", "for": "for", - "for every keyboard shortcut": "para cada atajo de teclado", "for settings": "para ajustes", "for the sake of power & beauty": "por el bien del poder y la belleza", "for the sidebar": "para la barra lateral", @@ -12604,7 +12763,6 @@ "hybrid GPU (%s) with only a firmware backlight (%s); the panel may not dim": "GPU híbrida (%s) solo con retroiluminación de firmware (%s); el panel podría no atenuarse", "hybrid GPU (%s): the kernel reports the dGPU has no working backlight, and the firmware fallback (%s) does not dim the panel": "GPU híbrida (%s): el kernel reporta que la dGPU no tiene retroiluminación funcional y el respaldo de firmware (%s) no atenúa el panel", "hypr.json does not parse, so it cannot be migrated: %v": "hypr.json no se analiza, por lo que no se puede migrar: %v", - "hypr/keyboard.lua already exists, keeping it": "hypr/keyboard.lua ya existe, manteniéndolo", "hyprmod": "hyprmod", "hyprpm": "hyprpm", "iGPU + dGPU for VM": "iGPU + dGPU para MV", @@ -12634,7 +12792,6 @@ "in": "en", "in Ryostore.": "en Ryostore.", "in the agent ·": "en el agente ·", - "in-game FPS and frametime overlay": "in-game FPS and frametime overlay", "in-session lock (next daemon start)": "bloqueo en sesión (próximo inicio del demonio)", "in-session lockscreen": "pantalla de bloqueo en sesión", "in-session lockscreen installed": "pantalla de bloqueo en sesión instalada", @@ -12664,7 +12821,6 @@ "install failed: %s": "instalación fallida: %s", "install into free space (new ESP)": "instalar en espacio libre (nuevo ESP)", "install limine-mkinitcpio-hook, then ryoku doctor": "instale limine-mkinitcpio-hook, luego ryoku doctor", - "install limine-mkinitcpio-hook, then sudo ryoku doctor": "instala limine-mkinitcpio-hook, luego sudo ryoku doctor", "install log:": "registro de instalación:", "install steps": "pasos de instalación", "install that desktop's package, then run `ryoku doctor` again": "instala el paquete de ese escritorio, luego ejecuta `ryoku doctor` de nuevo", @@ -12676,13 +12832,11 @@ "installed %s but could not lay its config down (%v); run `ryoku materialize` before logging out": "%s instalado pero no se pudo aplicar su configuración (%v); ejecuta `ryoku materialize` antes de cerrar sesión", "installed %s; %s did not land": "instalado %s; %s no se ejecutó", "installed Shelly's Flatpak backend; its Flatpak searches and installs now work": "instalé el backend Flatpak de Shelly; Sus búsquedas e instalaciones de Flatpak ahora funcionan.", - "installed asusctl and started asusd; the ASUS Aura keyboard is available in Appearance": "asusctl instalado e iniciado asusd; el teclado ASUS Aura está disponible en Apariencia", "installed asusctl and started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings": "instalé asusctl e inicié asusd; el teclado ASUS Aura está disponible en la configuración de iluminación de Ryogami", "installed browser animations for Zen profile: %s": "animaciones de navegador instaladas para perfil Zen: %s", "installed limine-mkinitcpio-hook": "limine-mkinitcpio-hook instalado", "installed plugin %q: %w": "complemento instalado %q: %w", - "installed qmk-hid; the QMK/VIA keyboard is available in Appearance": "qmk-hid instalado; teclado QMK/VIA disponible en Apariencia", - "installed qmk_hid; the QMK/VIA keyboard is available in Ryogami lighting settings": "instalado qmk_hid; El teclado QMK/VIA está disponible en la configuración de iluminación de Ryogami.", + "installed qmk-hid; the QMK/VIA keyboard is available in Ryogami lighting settings": "installed qmk-hid; the QMK/VIA keyboard is available in Ryogami lighting settings", "installed the %s desktop variant; log into it before the retired compositor is removed": "installed the %s desktop variant; log into it before the retired compositor is removed", "installed the Material Symbols font, but fontconfig cannot see it": "installed the Material Symbols font, but fontconfig cannot see it", "installed the Material Symbols icon font; `ryoku reload` picks it up": "fuente Material Symbols instalada; `ryoku reload` la carga", @@ -12720,7 +12874,6 @@ "issue": "problema", "issues": "problemas", "items on list": "elementos en la lista", - "its own effect": "su propio efecto", "iw is not installed, so the wireless regulatory domain cannot be read": "iw no instalado; dominio regulatorio inalámbrico no disponible", "iwd": "iwd", "just now": "hace un momento", @@ -12786,7 +12939,6 @@ "laid down the %s config and started the desktop; log out and back in if a surface is still missing": "aplicada la configuración de %s y iniciado el escritorio; cierra y vuelve a iniciar sesión si una superficie sigue faltando", "laid down the %s config but could not safely start the desktop: %v": "se estableció la configuración %s pero no se pudo iniciar el escritorio de forma segura: %v", "laid down the compositor config for %s": "aplicada la configuración del compositor para %s", - "language/version manager": "language/version manager", "laocoon.png": "laocoon.png", "last": "último", "last output": "last output", @@ -12839,7 +12991,6 @@ "limine snapshot sync is not installed": "limine snapshot sync is not installed", "limine-entry-tool owns the boot menu (UKI tree with kernel sub-entries)": "limine-entry-tool posee el menú de inicio (árbol UKI con subentradas del kernel)", "limine-mkinitcpio-hook adopted the placeholder as the boot tree: repointing the default": "limine-mkinitcpio-hook adoptó el marcador de posición como árbol de arranque: reapuntando el predeterminado", - "limine-mkinitcpio-hook owns the boot menu (UKI tree with kernel sub-entries)": "limine-mkinitcpio-hook posee el menú de arranque (árbol UKI con subentradas de kernel)", "limine-mkinitcpio-hook owns the menu: dropping the flat placeholder entry": "limine-mkinitcpio-hook posee el menú: eliminando entrada de marcador de posición plana", "limine-snapper-sync is not installed, so snapshots are missing from the boot": "limine-snapper-sync no está instalado, así que faltan instantáneas en el arranque", "limine-snapper-sync is not installed, so snapshots are not in the Limine boot menu (%s)": "limine-snapper-sync is not installed, so snapshots are not in the Limine boot menu (%s)", @@ -12864,6 +13015,7 @@ "lockscreen install failed: %v (%s)": "instalación de pantalla de bloqueo fallida: %v (%s)", "lockscreen installer ran but %s did not appear": "instalador de pantalla de bloqueo ejecutado pero %s no apareció", "lockscreen refresh failed: %v (%s)": "actualización de pantalla de bloqueo fallida: %v (%s)", + "lockscreen-preview": "lockscreen-preview", "lockscreen: unavailable": "pantalla de bloqueo: no disponible", "log": "registro", "log in as %s · your configs live in ~/.config": "inicia sesión como %s · tus configuraciones están en ~/.config", @@ -12877,7 +13029,6 @@ "login screen cursor": "cursor de pantalla de inicio", "login shell": "login shell", "login shell %s is missing from the target": "El shell de inicio de sesión %s no se encuentra en el destino.", - "login shell %s is missing from the target; refusing to create %s with an unusable shell": "login shell %s is missing from the target; refusing to create %s with an unusable shell", "login shell is %q on both the account and the session override": "el shell de inicio de sesión es %q tanto en la cuenta como en la anulación de sesión", "login shell source": "origen del shell de inicio de sesión", "login: %s": "inicio: %s", @@ -12885,9 +13036,7 @@ "logs in as ryoku / ryoku": "inicia sesión como ryoku / ryoku", "logs only, no passwords or keys. Open an issue: %s": "solo registros, sin contraseñas o claves. Abre un problema: %s", "looks like a hardcoded secret or key (R10)": "parece un secreto o clave codificado (R10)", - "lowercase; start with a letter or _": "minúsculas; comienza con una letra o _", "lowercase; start with a letter or _; 32 characters at most": "minúscula; comience con una letra o _; 32 caracteres como máximo", - "ls/cat replacements · Ctrl-R + fuzzy cd in the shell (each degrades cleanly if removed)": "ls/cat replacements · Ctrl-R + fuzzy cd in the shell (each degrades cleanly if removed)", "m ago": "hace m", "machine": "máquina", "making a mountpoint for the btrfs top level: %v": "haciendo un punto de montaje para el nivel superior de btrfs: %v", @@ -12919,13 +13068,13 @@ "media": "multimedia", "medium": "medio", "melts into it": "se derrite en él", + "memory pressure": "memory pressure", "menu. Install it first:": "menú. Instálalo primero:", "merge": "fusionar", "merge %s into %s and remove it (it shadows the generated boot entries: kernels, snapshots)": "fusionar %s en %s y eliminarlo (oculta las entradas de arranque generadas: kernels, snapshots)", "merge each .new- file into its base config, then remove the reviewed files with `sudo rm -f %s`": "merge each .new- file into its base config, then remove the reviewed files with `sudo rm -f %s`", "merge each .rpmnew or .rpmsave file into its base config, then remove the reviewed files": "merge each .rpmnew or .rpmsave file into its base config, then remove the reviewed files", "merged boot menu into %s": "menú de arranque fusionado en %s", - "merged retired dock settings into the universal dock store": "merged retired dock settings into the universal dock store", "mic.png": "mic.png", "middle": "medio", "migrated Atoll settings to frame bars": "ajustes Atoll migrados a barras de marco", @@ -12967,7 +13116,6 @@ "missioncenter": "centro de misión", "mkinitcpio HOOKS drop-in (/etc/mkinitcpio.conf.d/ryoku.conf)": "drop-in HOOKS de mkinitcpio (/etc/mkinitcpio.conf.d/ryoku.conf)", "mkinitcpio does not manage the initramfs on this host": "mkinitcpio does not manage the initramfs on this host", - "mkinitcpio not installed": "mkinitcpio no instalado", "mkv": "mkv", "mode: %s": "modo: %s", "mode: %s (%s)": "modo: %s (%s)", @@ -13025,6 +13173,7 @@ "niri config not checked (niri is not on PATH)": "configuración de Niri no comprobada (niri no está en PATH)", "niri config still does not load: %s": "la configuración de Niri aún no carga: %s", "nixos-configuration": "nixos-configuration", + "nm-connection-editor": "nm-connection-editor", "nmtui in Foot": "nmtui En pie", "nmtui in Kitty": "nmtui En gatito", "no": "no", @@ -13327,6 +13476,7 @@ "orphaned packages": "paquetes huérfanos", "orphaned theme.lua": "theme.lua huérfano", "orphaned theme.lua from the retired Themes feature: %s": "theme.lua huérfano de la función Themes retirada: %s", + "orpheus.reaction became orpheus.gain": "orpheus.reaction became orpheus.gain", "outputs apply and persist a neutral output layout (JSON)": "outputs apply and persist a neutral output layout (JSON)", "overlay %s: %w": "superposición %s: %w", "overlay is set up": "overlay está configurado", @@ -13420,7 +13570,6 @@ "pointed default_entry at the kernel entry path and enabled remember_last_entry, so the countdown autoboots the last kernel used": "apuntó default_entry a ruta entrada kernel y habilitó remember_last_entry, así la cuenta atrás autoarranca último kernel usado", "pointed the \\\"default\\\" cursor theme at %s; the login screen has a pointer now": "apuntó tema cursor \"default\" a %s; pantalla inicio tiene puntero ahora", "pointed the default keyring at login": "apuntó llavero predeterminado al inicio", - "pointing the SDDM login screen at the %s layout via xorg.conf.d": "apuntando pantalla inicio SDDM a diseño %s vía xorg.conf.d", "pointing the next boot at the restored root: %w": "apuntando el siguiente inicio a la raíz restaurada: %w", "points the SDDM login screen at the Ryoku qylock greeter": "apunta pantalla inicio SDDM a saludador qylock Ryoku", "poke around first": "primero investiga", @@ -13473,7 +13622,6 @@ "prowl comes from a package; refreshed with the system packages": "el acecho proviene de un paquete; actualizado con los paquetes del sistema", "prowl for rashin": "prowl para rashin", "prowl is managed by %s; refreshed with the system packages": "el acecho es gestionado por %s; actualizado con los paquetes del sistema", - "prowl is managed by pacman; refreshed with the system packages": "prowl is managed by pacman; refreshed with the system packages", "prowl is not installed; gateway health is not applicable": "prowl is not installed; gateway health is not applicable", "prowl is present for the rashin agent index": "prowl está presente para el índice del agente rashin", "pruned the stray boot entry for %s (kernel not installed)": "eliminó entrada de arranque huérfana para %s (núcleo no instalado)", @@ -13514,7 +13662,6 @@ "radio_button_unchecked": "radio_button_unchecked", "random": "random", "rashin (the Alt+Space bar and AI dashboard) is off; Ryoku turns it on by default": "Rashin (la barra Alt+Espacio y el panel de IA) está desactivado; Ryoku lo activa de forma predeterminada", - "rashin (the Super+S needle and AI dashboard) is off; Ryoku turns it on by default": "rashin (la aguja Super+S y el panel de IA) está apagado; Ryoku lo enciende por defecto", "rashin agent daemon": "demonio agente rashin", "rashin daemon enabled and supervised in the runit session roster; the ryoku skill is wired": "rashin daemon enabled and supervised in the runit session roster; the ryoku skill is wired", "rashin daemon enabled with boot-start; the ryoku skill is wired": "demonio rashin habilitado con arranque automático; habilidad ryoku conectada", @@ -13540,7 +13687,6 @@ "re-asserted a display that came up below its available resolution": "reafirmó pantalla que iniciaba bajo su resolución disponible", "re-authored the niri config; it loads cleanly again": "se volvió a autorizar la config de niri; se carga limpiamente de nuevo", "re-enabled [multilib] in %s so the %d 32-bit package(s) can update": "[multilib] re-habilitado en %s para que el/los %d paquete(s) de 32 bits puedan actualizarse", - "re-enabled [multilib], but syncing it failed: %v": "[multilib] re-habilitado, pero la sincronización falló: %v", "re-enabled [multilib], but syncing package databases failed: %v": "se volvió a habilitar [multilib], pero falló la sincronización de las bases de datos del paquete: %v", "re-enabled session units under %s: %s": "unidades de sesión re-habilitadas bajo %s: %s", "re-registered the missing Ryoku UEFI boot entry": "re-registró entrada arranque UEFI Ryoku faltante", @@ -13582,9 +13728,7 @@ "recolor templates + animation presets": "recolorear plantillas + preajustes de animación", "recommended": "recomendado", "reconcile %s to its checked target failed: %w": "Error al reconciliar %s con su objetivo marcado: %w", - "reconcile to origin/%s failed: %w": "reconciliar con origin/%s falló: %w", "reconciled user and system-wide services from the activation manifest": "Servicios conciliados de usuario y de todo el sistema desde el manifiesto de activación.", - "reconciled user services from the activation manifest": "reconciled user services from the activation manifest", "record": "grabar", "record existing user service choices: %v": "record existing user service choices: %v", "record keyring mode: %w": "registrar modo de llavero: %w", @@ -13679,7 +13823,6 @@ "requested region %s-%s is not inside a free area of %s (did the disk change since it was probed?); refusing to partition.": "región solicitada %s-%s no está dentro de área libre de %s (¿cambió el disco desde el sondeo?); rechazando particionar.", "required": "requerido", "required for Zen; also builds the selected AUR app components and the Bibata cursor": "requerido para el Zen; también construye los componentes de la aplicación AUR seleccionados y el cursor Bibata", - "required for Zen; also installs Bibata cursor, LocalSend and Voxtype": "required for Zen; also installs Bibata cursor, LocalSend and Voxtype", "required: %s": "required: %s", "requires a network connection and x86-64-v3; installs the CachyOS kernel, tuning, sched-ext, Steam and Proton-CachyOS; the Arch kernel stays as a boot fallback": "requiere una conexión de red y x86-64-v3; instala el kernel de CachyOS, tuning, sched-ext, Steam y Proton-CachyOS; el kernel de Arch permanece como respaldo de arranque", "res:>=1920x1080": "res:>=1920x1080", @@ -13730,7 +13873,6 @@ "retired compositor artifacts need migration to %s": "retired compositor artifacts need migration to %s", "retired compositor package migration is not applicable on this host": "retired compositor package migration is not applicable on this host", "retired cursor keys": "teclas de cursor retiradas", - "retired dock settings still exist outside the universal dock store": "retired dock settings still exist outside the universal dock store", "retired filter bar keys": "retired filter bar keys", "retired panel keybind": "retired panel keybind", "retired shell menus": "menús de shell retirados", @@ -13792,7 +13934,6 @@ "run `ryoku update` once to record the baseline": "ejecuta `ryoku update` una vez para registrar la línea base", "run `ryoku update` to deliver what is missing": "ejecuta `ryoku update` para traer lo que falta", "run `ryoku-shell daemon` in a terminal to see why it exits": "ejecutar `ryoku-shell daemon` en terminal para ver por qué sale", - "run `sudo ryoku doctor`; use `ryoku-host pkg advice ` for a missing snapshot package": "run `sudo ryoku doctor`; use `ryoku-host pkg advice ` for a missing snapshot package", "run as your normal user, not root (sudo is used when needed)": "ejecuta como usuario normal, no como root (sudo se usa cuando es necesario)", "run as your normal user, not root; sudo is used where needed": "ejecutar como usuario normal, no root; sudo se usa donde es necesario", "run non-interactively with the default plan": "ejecutar no interactivamente con plan por defecto", @@ -13822,9 +13963,7 @@ "ryoku boot-menu runs as root (sudo ryoku boot-menu %s)": "El menú de inicio de ryoku se ejecuta como root (sudo ryoku boot-menu %s)", "ryoku channel pin": "ryoku channel pin", "ryoku channel: pinned to release %s (packages); `ryoku track stable` follows releases again": "ryoku channel: pinned to release %s (packages); `ryoku track stable` follows releases again", - "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` follows unstable builds again": "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` follows unstable builds again", "ryoku channel: pinned to unstable build %s (packages); `ryoku track unstable` switches to the unstable-dev source checkout": "canal ryoku: anclado a la compilación inestable %s (paquetes); `ryoku track unstable` cambia al control de fuente de desarrollo inestable", - "ryoku channel: stable packages (named releases); `ryoku track unstable` follows the unstable channel": "ryoku channel: stable packages (named releases); `ryoku track unstable` follows the unstable channel", "ryoku channel: stable packages (named releases); `ryoku track unstable` follows unstable-dev from source": "canal ryoku: paquetes estables (versiones con nombre); `ryoku track unstable` sigue a unstable-dev desde la fuente", "ryoku channel: unstable packages (rebuilt on every push); `ryoku track stable` returns to stable releases": "ryoku channel: unstable packages (rebuilt on every push); `ryoku track stable` returns to stable releases", "ryoku doctor": "ryoku doctor", @@ -13856,7 +13995,6 @@ "ryoku doctor changes it to the 2 px card-local frost default": "ryoku doctor lo cambia al predeterminado frost local 2 px", "ryoku doctor clears it back to the text seal": "ryoku doctor lo limpia al sello de texto", "ryoku doctor converges it to the Orpheus default in place": "El médico de Ryoku lo converge con el valor predeterminado de Orfeo en su lugar.", - "ryoku doctor converges it to the qsbar default in place": "ryoku doctor lo converge al predeterminado qsbar in situ", "ryoku doctor creates the limine snapshot-sync defaults": "ryoku doctor creates the limine snapshot-sync defaults", "ryoku doctor drops it": "ryoku doctor lo descarta", "ryoku doctor drops the pin so the network roams again": "ryoku doctor drops the pin so the network roams again", @@ -13873,11 +14011,9 @@ "ryoku doctor installs pam-fprint-grosshack (AUR)": "ryoku doctor instala pam-fprint-grosshack (AUR)", "ryoku doctor installs pam-fprint-grosshack from the configured Fedora repositories": "ryoku doctor installs pam-fprint-grosshack from the configured Fedora repositories", "ryoku doctor installs qmk-hid so the keyboard follows the theme": "ryoku doctor instala qmk-hid para que el teclado siga el tema", - "ryoku doctor installs qmk_hid so the keyboard follows the theme": "ryoku doctor instala qmk_hid para que el teclado siga el tema", "ryoku doctor installs shelly-flatpak-backend": "Ryoku Doctor instala Shelly-Flatpak-backend", "ryoku doctor installs the repository build, which is rebuilt with Qt": "ryoku doctor instala la compilación del repositorio, reconstruida con Qt", "ryoku doctor matches the Nomarchy command surface to the selected bar style": "ryoku doctor matches the Nomarchy command surface to the selected bar style", - "ryoku doctor merges them into shell.json and removes the retired keys": "ryoku doctor merges them into shell.json and removes the retired keys", "ryoku doctor migrates default_entry to the kernel entry path and preserves remember_last_entry": "ryoku doctor migra default_entry a la ruta de entrada del kernel y conserva Remember_last_entry", "ryoku doctor migrates it in place": "ryoku doctor lo migra in situ", "ryoku doctor migrates it to desktop.json": "Ryoku doctor lo migra a desktop.json", @@ -13956,7 +14092,6 @@ "ryoku-gpu check-pin answered %q, which this ryoku version does not understand; update ryoku or run `ryoku-gpu status`": "ryoku-gpu check-pin respondió %q, que esta versión no entiende; actualiza ryoku o ejecuta `ryoku-gpu status`", "ryoku-gpu is not available to audit the render pin (%v)": "ryoku-gpu no disponible para auditar el pin de render (%v)", "ryoku-host svc --user enable %s": "ryoku-host svc --user enable %s", - "ryoku-host svc reconcile --user": "ryoku-host svc reconcile --user", "ryoku-hub, the settings backend": "ryoku-hub, el backend de configuración", "ryoku-keyring already installed, key trust in place": "ryoku-keyring ya instalado, confianza de clave establecida", "ryoku-keyring and the Ryoku release key %s are missing, so the Ryoku RPM repository cannot be read": "Faltan ryoku-keyring y la clave de lanzamiento de Ryoku %s, por lo que no se puede leer el repositorio RPM de Ryoku", @@ -13967,9 +14102,9 @@ "ryoku-keyring is not installed, so a new Ryoku release key cannot reach this machine": "Ryoku-keyring no está instalado, por lo que una nueva clave de liberación de Ryoku no puede llegar a esta máquina", "ryoku-keyring is not installed, so there are no revoked Ryoku keys to check": "ryoku-keyring no está instalado, por lo que no hay claves Ryoku revocadas para verificar", "ryoku-keyring package installed": "paquete ryoku-keyring instalado", + "ryoku-kit": "ryoku-kit", "ryoku-monitor not installed": "ryoku-monitor no instalado", "ryoku-pkg-aur-add limine-mkinitcpio-hook, then ryoku doctor": "ryoku-pkg-aur-add limine-mkinitcpio-hook, luego ryoku doctor", - "ryoku-pkg-aur-add limine-mkinitcpio-hook, then sudo ryoku doctor": "ryoku-pkg-aur-add limine-mkinitcpio-hook, luego sudo ryoku doctor", "ryoku-pkg-aur-add pam-fprint-grosshack, then ryoku doctor": "ryoku-pkg-aur-add pam-fprint-grosshack, luego ryoku doctor", "ryoku-rashin not installed": "ryoku-rashin no instalado", "ryoku-rashin, the agent daemon": "ryoku-rashin, el demonio agente", @@ -13982,6 +14117,7 @@ "ryopin could not draw a pin": "ryopin no pudo dibujar un pin", "ryoshot could not draw its overlay": "ryoshot no pudo dibujar su overlay", "ryostage cache": "caché ryostage", + "ryostore": "ryostore", "ryostore cache is on the current path": "caché ryostore está en la ruta actual", "ryostore cache location": "ubicación de caché ryostore", "ryotunes": "Ryotunes", @@ -14048,12 +14184,10 @@ "seeded once by Ryoku, then yours": "seeded once by Ryoku, then yours", "seeded ~/%s": "sembrados ~/%s", "seeding brand assets, wallpapers, decor art, and ~/.npmrc into %s": "sembrando assets de marca, fondos, arte decorativo y ~/.npmrc en %s", - "seeding keyboard layout %q variant %q options %q (from %s) into hypr/keyboard.lua": "sembrando diseño %q variante %q opciones %q (desde %s) en hypr/keyboard.lua", "seeding the ryoku-console-keys initramfs hook (the desktop set did not install it)": "seeding the ryoku-console-keys initramfs hook (the desktop set did not install it)", "seeding the ryoku-gpu-trim initramfs hook (the desktop set did not install it)": "sembrando el hook initramfs de ryoku-gpu-trim (el conjunto de escritorio no lo instaló)", "select": "seleccionar", "selected": "selected", - "send a chat message to fetch them": "send a chat message to fetch them", "sensor": "sensor", "separate password": "contraseña separada", "service health could not be checked: %v": "service health could not be checked: %v", @@ -14079,7 +14213,6 @@ "sets layout be for Hyprland, the console (KEYMAP=be-latin1), and the SDDM login screen; turns the French toggle off": "establece diseño be para Hyprland, consola y SDDM; desactiva toggle francés", "sets layout fr for Hyprland, the console (KEYMAP=fr), and the SDDM login screen; turns the Belgian toggle off": "establece diseño fr para Hyprland, consola y SDDM; desactiva toggle belga", "setting Plymouth default theme 'ryoku'": "estableciendo tema Plymouth por defecto 'ryoku'", - "setting the console keymap to %s in /etc/vconsole.conf": "estableciendo mapa de consola a %s en /etc/vconsole.conf", "setting the current root aside: %w; nothing was changed": "dejando a un lado la raíz actual: %w; nada fue cambiado", "setting up /.snapshots: %v": "configurar /.snapshots: %v", "settings": "ajustes", @@ -14169,7 +14302,6 @@ "snapshot read access": "acceso de lectura a instantáneas", "snapshots and rollback from the Limine boot menu": "snapshots y reversión desde el menú de inicio Limine", "snapshots are readable without sudo": "instantáneas legibles sin sudo", - "snapshots are unavailable on Fedora; recover with DNF directly": "snapshots are unavailable on Fedora; recover with DNF directly", "snapshots in the boot menu": "snapshots en el menú de inicio", "snapshots were declined at install (/etc/ryoku/snapshots-disabled); delete the marker and run `ryoku doctor` to enable them": "snapshots fueron rechazados en la instalación (/etc/ryoku/snapshots-disabled); elimina el marcador y ejecuta `ryoku doctor` para activarlos", "snapshots: %d": "instantáneas: %d", @@ -14211,7 +14343,6 @@ "start": "inicio", "start it in a terminal to see why: `ryoku-shell daemon`": "inícialo en una terminal para ver por qué: `ryoku-shell daemon`", "start it with `ryoku-shell daemon` (Hyprland autostarts it at login)": "inícialo con `ryoku-shell daemon` (Hyprland lo inicia automáticamente al iniciar sesión)", - "started asusd; the ASUS Aura keyboard is available in Appearance": "asusd iniciado; el teclado ASUS Aura está disponible en Apariencia", "started asusd; the ASUS Aura keyboard is available in Ryogami lighting settings": "comenzó asusd; el teclado ASUS Aura está disponible en la configuración de iluminación de Ryogami", "started fresh: blank %q keyring, old one backed up": "comenzado limpio: llavero %q en blanco, el antiguo se respalda", "started ryoku-shell daemon but it did not come up": "daemon ryoku-shell iniciado pero no se activó", @@ -14258,7 +14389,6 @@ "sudo limine-mkinitcpio (or: sudo mkinitcpio -P)": "sudo limine-mkinitcpio (o: sudo mkinitcpio -P)", "sudo limine-mkinitcpio || sudo mkinitcpio -P": "sudo limine-mkinitcpio || sudo mkinitcpio -P", "sudo limine-update, then ryoku doctor": "sudo limine-update, luego ryoku doctor", - "sudo limine-update, then sudo ryoku doctor": "sudo limine-update, luego sudo ryoku doctor", "sudo may ask for your password again below": "sudo may ask for your password again below", "sudo pacman -S --needed base-devel cmake git, then Settings > Plugins > Rebuild": "sudo pacman -S --needed base-devel cmake git, luego Configuración > Complementos > Reconstruir", "sudo pacman -S quickshell takes the repository build, which is rebuilt with Qt": "sudo pacman -S quickshell toma la compilación del repositorio, que se reconstruye con Qt", @@ -14267,7 +14397,6 @@ "sudo pacman -Syu brings Qt up to date, then the shell loads again": "sudo pacman -Syu brings Qt up to date, then the shell loads again", "sudo pacman -Syu quickshell": "sudo pacman -Syu quickshell", "sudo rm -f %s/*%s.conf": "sudo rm -f %s/*%s.conf", - "sudo ryoku doctor (or the next ryoku update)": "sudo ryoku doctor (o la próxima actualización de ryoku)", "sudo ryoku-host svc --system enable %s": "sudo ryoku-host svc --system enable %s", "sudo ryoku-host svc --system enable --now %s": "sudo ryoku-host svc --system enable --now %s", "sudo ryoku-host svc --system enable ryoku-boot-guard.service && sudo install -d -m 0755 /var/lib/ryoku && sudo install -d -m 1777 /var/lib/ryoku/boot": "sudo ryoku-host svc --system enable ryoku-boot-guard.service && sudo install -d -m 0755 /var/lib/ryoku && sudo install -d -m 1777 /var/lib/ryoku/boot", @@ -14289,7 +14418,6 @@ "supported: %s": "compatible: %s", "suppressevent": "suprimir evento", "suspended, drawing no power": "suspendido, sin consumo", - "svc reconcile --system must run as root": "svc reconcile --system must run as root", "svc reconcile --system must run as root unless --check is used": "svc reconcile --el sistema debe ejecutarse como root a menos que se utilice --check", "swap is out of snapshots": "swap sin instantáneas", "swap kept out of snapshots": "swap mantenido fuera de instantáneas", @@ -14307,7 +14435,6 @@ "symlinks are not allowed in a plugin (R2)": "enlaces simbólicos no permitidos en complemento (R2)", "synced the Snapshots submenu": "submenú Instantáneas sincronizado", "system service health could not be checked without passwordless sudo; no failed user services": "system service health could not be checked without passwordless sudo; no failed user services", - "system summary card (launcher, RyoStore covers)": "system summary card (launcher, RyoStore covers)", "system: %d package(s) waiting (sudo %s upgrade -y --refresh)": "system: %d package(s) waiting (sudo %s upgrade -y --refresh)", "system: %d package(s) waiting (sudo pacman -Syu)": "sistema: %d paquete(s) esperando (sudo pacman -Syu)", "system: %d package(s) waiting (sudo xbps-install -Syu)": "system: %d package(s) waiting (sudo xbps-install -Syu)", @@ -14328,7 +14455,6 @@ "task left": "tarea restante", "tasks left": "tareas pendientes", "terminal": "terminal", - "terminal file manager": "terminal file manager", "text": "texto", "that account name is already in use": "ese nombre de cuenta ya está en uso", "that name is reserved by the system": "ese nombre está reservado por el sistema", @@ -14341,16 +14467,17 @@ "the %s package": "the %s package", "the AI usage collector service is off, so the bar AI pill goes stale": "the AI usage collector service is off, so the bar AI pill goes stale", "the AI usage collector timer is off, so the bar AI pill goes stale": "el temporizador del recolector de uso de IA está apagado, por lo que la pastilla de IA de la barra caduca", - "the CPU, GPU, and battery panels' monitor button opens btop in the terminal": "the CPU, GPU, and battery panels' monitor button opens btop in the terminal", - "the Cobalt engine the file stash downloads through": "the Cobalt engine the file stash downloads through", "the GPU render pin is a deliberate RYOKU_GPU_FORCE override; kept": "el pin de renderizado GPU es un override RYOKU_GPU_FORCE deliberado; conservado", "the Hyprland GPU render pin matches ryoku-gpu policy": "el pin de renderizado GPU de Hyprland coincide con la política ryoku-gpu", "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, rebuild the initramfs (`sudo mkinitcpio -P`), and reboot.": "el driver NVIDIA reporta D3 desactivado para esta tarjeta, nunca se apaga. Activa control granular con `NVreg_DynamicPowerManagement=0x02` en /etc/modprobe.d/nvidia.conf, reconstruye initramfs (`sudo mkinitcpio -P`) y reinicia.", "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, rebuild the initramfs with `sudo dracut --regenerate-all --force`, and reboot.": "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, rebuild the initramfs with `sudo dracut --regenerate-all --force`, and reboot.", "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, then rebuild the dracut initramfs with XBPS (`sudo xbps-reconfigure -f `) or directly (`sudo dracut --regenerate-all --force`), and reboot.": "the NVIDIA driver reports runtime D3 power management off for this card, so it can never power down. Enable fine-grained control with `NVreg_DynamicPowerManagement=0x02` in a /etc/modprobe.d/nvidia.conf `options nvidia` line, then rebuild the dracut initramfs with XBPS (`sudo xbps-reconfigure -f `) or directly (`sudo dracut --regenerate-all --force`), and reboot.", + "the NVIDIA elogind sleep hook is installed": "the NVIDIA elogind sleep hook is installed", "the NVIDIA elogind sleep hook is missing, so VRAM may not survive suspend": "the NVIDIA elogind sleep hook is missing, so VRAM may not survive suspend", "the NVIDIA post-install kernel hook is missing, stale, or not executable; a failed DKMS rebuild could leave nouveau blacklisted for the new kernel": "the NVIDIA post-install kernel hook is missing, stale, or not executable; a failed DKMS rebuild could leave nouveau blacklisted for the new kernel", "the NVIDIA sleep units (%s) are disabled, so VRAM is not saved across suspend the way the installer sets up; a box with the kernel suspend notifier off wakes to a corrupted session": "las unidades de suspensión NVIDIA (%s) están deshabilitadas, por lo que la VRAM no se guarda al suspender como lo configura el instalador; un equipo con el notificador de suspensión del kernel desactivado se despierta a una sesión corrupta", + "the NVIDIA sleep units are enabled": "the NVIDIA sleep units are enabled", + "the NVIDIA sleep units are not installed on this machine": "the NVIDIA sleep units are not installed on this machine", "the NVIDIA update guard pacman hook is missing or stale; a failed DKMS rebuild on a kernel update could strand the box at the SDDM login (the login loop)": "falta el hook pacman de protección NVIDIA o está obsoleto; un fallo en DKMS al actualizar kernel podría quedar en SDDM (bucle de login)", "the QML module %s was built against another Qt and stops the desktop loading": "el módulo QML %s se compiló contra otro Qt y bloquea la carga del escritorio", "the Rashin window and the bar's AI usage.": "la ventana de Rashin y el uso de la IA de la barra.", @@ -14366,8 +14493,6 @@ "the Ryoku XBPS repository points at %s, which Ryoku does not publish; releases will not arrive from it": "the Ryoku XBPS repository points at %s, which Ryoku does not publish; releases will not arrive from it", "the Ryoku desktop is installed; reboot to use it": "Ryoku está instalado; reinicia para usarlo", "the Ryoku desktop variant did not land for %s": "la variante de escritorio Ryoku no aterrizó durante %s", - "the Ryoku editor role: the launcher's editor opens 'kitty -e nvim'": "the Ryoku editor role: the launcher's editor opens 'kitty -e nvim'", - "the Ryoku file-manager role: the launcher's Files, the stash, and the wallpaper picker open it": "the Ryoku file-manager role: the launcher's Files, the stash, and the wallpaper picker open it", "the Ryoku package repository points at %s, a mirror Ryoku does not publish; edit its package-manager configuration by hand": "the Ryoku package repository points at %s, a mirror Ryoku does not publish; edit its package-manager configuration by hand", "the Ryoku package upgrade failed; %s: %w": "falló la actualización del paquete Ryoku; %s: %w", "the Ryoku pacman channel check": "the Ryoku pacman channel check", @@ -14376,9 +14501,6 @@ "the Ryoku release key %s is missing, so the Ryoku RPM repository cannot be read": "falta la clave de versión de Ryoku %s, por lo que no se puede leer el repositorio RPM de Ryoku", "the Ryoku release key %s no longer matches the one ryoku-keyring installed": "la clave de liberación de Ryoku %s ya no coincide con el llavero ryoku instalado", "the Ryoku set (its packages and config) to a published release, live;": "el conjunto Ryoku (paquetes y config) a una versión publicada, en directo;", - "the Ryoku shell prompt": "the Ryoku shell prompt", - "the Ryoku stash actions in the Files right-click menu (ships with Files)": "the Ryoku stash actions in the Files right-click menu (ships with Files)", - "the Ryoku terminal role: the launcher's terminal, the Hub, and 'open in terminal' all run it": "the Ryoku terminal role: the launcher's terminal, the Hub, and 'open in terminal' all run it", "the Ryostore copy of the Ryoku Discord theme gave way to the one Ryoku ships": "la copia de Ryostore del tema Ryoku Discord dio paso a la que Ryoku envía", "the Ryostore copy of the Ryoku Discord theme would give way to the one Ryoku ships": "la copia de Ryostore del tema Ryoku Discord daría paso a la que Ryoku envía", "the SDDM PAM stack %s pam_gnome_keyring but mode is %s": "la pila SDDM PAM %s pam_gnome_keyring pero el modo es %s", @@ -14418,8 +14540,12 @@ "the desktop has not reported a finished load yet; `ryoku doctor` says why": "el escritorio aún no ha informado de una carga finalizada; El \"doctor ryoku\" dice por qué", "the desktop is loaded": "el escritorio está cargado", "the discrete GPU": "la GPU discreta", - "the discrete GPU does not drive the panel yet has never runtime-suspended%s%s": "la GPU discreta no maneja el panel pero nunca se ha suspendido%s%s", - "the discrete GPU does not drive the panel yet has never runtime-suspended%s: the driver reports runtime D3 %q": "la GPU discreta no maneja el panel pero nunca se ha suspendido%s: el driver reporta D3 %q", + "the discrete GPU does not drive the panel yet has never runtime-suspended": "the discrete GPU does not drive the panel yet has never runtime-suspended", + "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W": "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W", + "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W, with the card open in %s": "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W, with the card open in %s", + "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W: the driver reports runtime D3 %q": "the discrete GPU does not drive the panel yet has never runtime-suspended and is drawing %.1f W: the driver reports runtime D3 %q", + "the discrete GPU does not drive the panel yet has never runtime-suspended, with the card open in %s": "the discrete GPU does not drive the panel yet has never runtime-suspended, with the card open in %s", + "the discrete GPU does not drive the panel yet has never runtime-suspended: the driver reports runtime D3 %q": "the discrete GPU does not drive the panel yet has never runtime-suspended: the driver reports runtime D3 %q", "the discrete GPU drives the panel but can still runtime-suspend, so it is not pinned awake": "la GPU discreta maneja el panel pero puede suspenderse, no está fija despierta", "the discrete GPU is driving %s and drawing %.1f W; it cannot runtime-suspend while that display is attached": "la GPU discreta genera %s y consume %.1f W; no se puede suspender el tiempo de ejecución mientras esa pantalla está adjunta", "the discrete GPU is driving %s and drawing %.1f W; it cannot runtime-suspend while those displays are attached": "la GPU discreta genera %s y consume %.1f W; no se puede suspender el tiempo de ejecución mientras esas pantallas están conectadas", @@ -14428,7 +14554,6 @@ "the effect Ryoku found": "el efecto que encontró Ryoku", "the flathub remote is configured": "el remoto flathub está configurado", "the flathub remote is missing": "falta el remoto flathub", - "the gh CLI and the lazygit terminal UI": "the gh CLI and the lazygit terminal UI", "the in-session lockscreen is missing and no bundle is available to install it; the lock button and lock-on-sleep do nothing": "falta la pantalla de bloqueo y no hay paquete para instalarla; botón y bloqueo al dormir no hacen nada", "the in-session lockscreen is missing; the lock button and lock-on-sleep do nothing": "falta la pantalla de bloqueo; botón y bloqueo al dormir no hacen nada", "the initramfs GPU trim hook is not installed yet": "el hook de recorte de GPU de initramfs no está instalado aún", @@ -14438,12 +14563,14 @@ "the initramfs unlock prompt can fall back to a US layout with Num Lock off, so a passphrase set in the installer is rejected at boot": "the initramfs unlock prompt can fall back to a US layout with Num Lock off, so a passphrase set in the installer is rejected at boot", "the initramfs unlock prompt now reads keys in the user's console layout with Num Lock on; rebuilt the kernel images": "the initramfs unlock prompt now reads keys in the user's console layout with Num Lock on; rebuilt the kernel images", "the initramfs unlock prompt reads keys in the user's console layout with Num Lock on": "the initramfs unlock prompt reads keys in the user's console layout with Num Lock on", + "the installed NVIDIA branch uses the kernel suspend path and does not ship an elogind sleep hook": "the installed NVIDIA branch uses the kernel suspend path and does not ship an elogind sleep hook", "the installed NVIDIA driver version could not be compared with the loaded module": "la versión del controlador NVIDIA instalado no se pudo comparar con el módulo cargado", "the installed lockscreen predates the shipped one and no installer is available": "la pantalla instalada es anterior a la enviada y no hay instalador", "the installed lockscreen predates the shipped one; lock fixes have not reached this box": "la pantalla instalada es anterior a la enviada; las correcciones no han llegado", "the internal panel is driven by the integrated GPU and the discrete GPU can runtime-suspend": "el panel interno lo maneja la GPU integrada y la discreta puede suspenderse", "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and is drawing %.1f W, running hot at idle instead of powering down": "el panel está conectado a la GPU discreta%s, nunca se suspende y consume %.1f W, se calienta en reposo en lugar de apagarse", "the internal panel is wired to the discrete GPU%s, so it can never runtime-suspend and stays awake burning power and running hot at idle instead of powering down": "el panel está conectado a la GPU discreta%s, nunca se suspende, quema energía y se calienta en reposo en lugar de apagarse", + "the kernel killed %d process(es) for memory this boot": "the kernel killed %d process(es) for memory this boot", "the keyboard layout %q and style %q are saved as names, not the xkb codes %q and %q the compositor needs": "la disposición del teclado %q y el estilo %q se guardan como nombres, no los códigos xkb %q y %q que necesita el compositor", "the keyboard layout %q is not an xkb code and no name in the rules base matches it, so it was left unchanged": "la disposición del teclado %q no es un código xkb y ningún nombre en la base de reglas coincide, por lo que se dejó sin cambios", "the keyboard layout and style were saved as names %q and %q; set them to the xkb codes %q and %q": "la disposición del teclado y el estilo se guardaron como nombres %q y %q; configúrelos a los códigos xkb %q y %q", @@ -14456,8 +14583,6 @@ "the live Arch image now, and this installer turns it into": "la imagen Arch en directo ahora, e este instalador la convierte en", "the live Void image now, and this installer turns it into": "the live Void image now, and this installer turns it into", "the loaded and installed NVIDIA driver versions match (%s)": "las versiones del controlador NVIDIA cargadas e instaladas coinciden (%s)", - "the mixer the bar's audio button opens": "the mixer the bar's audio button opens", - "the nested gaming micro-compositor": "the nested gaming micro-compositor", "the offline install could not lay the base system from the ISO": "la instalación sin conexión no pudo instalar sistema base desde ISO", "the offline install laid the base system but could not install the Ryoku desktop set (%s) from the ISO": "instalación sin conexión completó base pero no pudo instalar Ryoku (%s) desde ISO", "the offline install stopped on %s, which fails its recorded checksum. Every package lives on the disc, so the copy that reached the installer is bad: the ISO download or the USB write, most likely. Check the ISO checksum against the release page, rewrite the install medium, and run the installer again. If the checksum already matches, report it with /var/log/ryoku-install.log.": "la instalación sin conexión se detuvo en %s, fallando su checksum grabado. Cada paquete vive en el disco, por lo que la copia que llegó al instalador es mala, probablemente la descarga ISO o la escritura USB. Compruebe el checksum ISO contra la página de lanzamiento, reescriba el medio de instalación y ejecute el instalador de nuevo. Si el checksum ya coincide, repórtelo con /var/log/ryoku-install.log.", @@ -14468,10 +14593,8 @@ "the pacman database lock check": "the pacman database lock check", "the pacman keyring is still empty after init + populate; package signatures cannot be verified. The live image's archlinux-keyring is broken; re-download or rewrite the ISO.": "llavero pacman vacío tras init + populate; firmas no verificables. archlinux-keyring de imagen rota; descarga o reescribe ISO.", "the password was not accepted three times": "the password was not accepted three times", - "the performance governor Steam invokes": "the performance governor Steam invokes", "the plugin tree is %d MB; keep it under 20 MB": "el árbol de plugins es %d MB; mantenlo bajo 20 MB", "the port did not answer": "the port did not answer", - "the portable app channel (the stash installs .flatpak bundles)": "the portable app channel (the stash installs .flatpak bundles)", "the portal frontend predates this login session; screen share can silently share nothing": "el frontend del portal es anterior a esta sesión; compartir pantalla puede no compartir nada en silencio", "the portal frontend predates this login session; screen share opens no source picker and silently shares nothing": "el frontend del portal es anterior a esta sesión de inicio de sesión; compartir pantalla no abre ningún selector de fuente y no comparte nada en silencio", "the proprietary NVIDIA driver is loaded; Ryoku leaves its setup on Fedora to you": "se carga el controlador propietario de NVIDIA; Ryoku te deja a ti la configuración de Fedora", @@ -14500,6 +14623,7 @@ "the session exported %q but the account login shell is %q; pointed the override at %q and refreshed the running session": "la sesión exportó %q, pero el shell de inicio de sesión de la cuenta es %q; apuntó la anulación a %q y actualizó la sesión en ejecución", "the shared ESP %s has filesystem errors fsck.fat could not repair (rc=%s); Ryoku refuses to write into it. Boot the OS that owns the ESP and repair the volume, or install with RYOKU_ESP_MODE=dedicated so Ryoku keeps to its own ESP.": "the shared ESP %s has filesystem errors fsck.fat could not repair (rc=%s); Ryoku refuses to write into it. Boot the OS that owns the ESP and repair the volume, or install with RYOKU_ESP_MODE=dedicated so Ryoku keeps to its own ESP.", "the shared ESP has %s KiB free; shared mode needs >= 8 MiB. Use dedicated mode or free space on the ESP.": "ESP compartida: %s KiB libres; modo compartido requiere >= 8 MiB. Usa modo dedicado o libera espacio.", + "the shell service recorded %d memory kill(s)": "the shell service recorded %d memory kill(s)", "the shipped limine snapshot-sync defaults are incomplete": "the shipped limine snapshot-sync defaults are incomplete", "the sign-in screen": "la pantalla de inicio de sesión", "the signed Ryoku RPM repository is missing and could not be restored: %v": "the signed Ryoku RPM repository is missing and could not be restored: %v", @@ -14524,7 +14648,6 @@ "the window width cycle held only %q, which the stepping key cannot use; set it to %s": "el ciclo de ancho de ventana solo contenía %q, que la tecla de paso no puede usar; configúrelo a %s", "the window width cycle holds only %q, so the key that steps a window through it has nowhere to go": "el ciclo de ancho de ventana solo contiene %q, por lo que la tecla que la recorre no tiene adonde ir", "the window width cycle steps through %d widths": "el ciclo de ancho de ventana pasa por %d anchos", - "the windowed VM behind Ryoku Settings > GPU > Machine": "the windowed VM behind Ryoku Settings > GPU > Machine", "the wireless regulatory domain is set to %s (%s)": "el dominio regulador inalámbrico está configurado en %s (%s)", "the wireless regulatory domain is unset (00) and ryoku-wifi-regdom is not installed to set it from the locale country %s": "el dominio regulador inalámbrico no está configurado (00) y ryoku-wifi-regdom no está instalado para establecerlo desde el país %s", "the wireless regulatory domain is unset (00), so the kernel keeps 5 GHz channels disabled, and no country could be inferred from the system locale to set one": "el dominio regulador inalámbrico no está configurado (00), así que el kernel mantiene deshabilitados los canales de 5 GHz, y no se pudo inferir país de la configuración regional del sistema", @@ -14671,24 +14794,19 @@ "usage: ryoku security-key remove ": "uso: ryoku security-key remove ", "usage: ryoku security-key set |mode |touch-required |pin-verification |user-verification ": "uso: ryoku security-key set |mode |touch-required |pin-verification |user-verification ", "usage: ryoku security-key status [--json]": "uso: ryoku security-key status [--json]", - "usage: ryoku track >": "usage: ryoku track >", "usage: ryoku track unstable": "uso: pista ryoku inestable", "usage: ryoku-install probe alongside ": "uso: ryoku-install probe alongside ", "usage: ryoku-install probe resize ": "uso: ryoku-install probe resize ", "use": "usar", "use a local ryoku-arch checkout as the payload": "usar un checkout local de ryoku-arch como carga útil", "used": "usado", - "used at login; change it anytime · esc back": "usado en inicio de sesión; cambialo en cualquier momento · esc atrás", - "user": "usuario", "user and system-wide services match the activation manifest": "Los servicios de usuario y de todo el sistema coinciden con el manifiesto de activación.", + "user apps are still inside the shell service: %s": "user apps are still inside the shell service: %s", "user edits overlay": "usuario edita superposición", "user or system-wide service activation needs reconciliation": "La activación del usuario o del servicio en todo el sistema necesita conciliación.", "user service activation": "user service activation", - "user service activation needs reconciliation": "user service activation needs reconciliation", - "user services match the activation manifest": "user services match the activation manifest", "user verification required": "verificación de usuario requerida", "user verify: %s": "verificación de usuario: %s", - "user: %s (wheel,video,input; shell %s)": "user: %s (wheel,video,input; shell %s)", "user@pam!id=secret": "user@pam!id=secret", "users caps does not accept arguments.": "Los usuarios caps no aceptan argumentos.", "users list does not accept arguments.": "La lista de usuarios no acepta argumentos.", @@ -14751,7 +14869,6 @@ "warning: could not set %s as the default browser (continuing)": "warning: could not set %s as the default browser (continuing)", "warning: could not set %s as the default file manager (continuing)": "Advertencia: no se pudo configurar %s como administrador de archivos predeterminado (continuación)", "warning: could not stop %s; it is disabled for the next boot": "warning: could not stop %s; it is disabled for the next boot", - "warning: could not update browser role in %s (invalid JSON)": "warning: could not update browser role in %s (invalid JSON)", "warning: could not update browser role in %s: %s": "Advertencia: no se pudo actualizar la función del navegador en %s: %s", "warning: could not update file manager role in %s: %s": "Advertencia: no se pudo actualizar la función del administrador de archivos en %s: %s", "warning: couldn't record security-key policy: %v": "advertencia: no se pudo registrar política de clave de seguridad: %v", @@ -14802,7 +14919,6 @@ "wire PAM: %w": "conectar PAM: %w", "wired the ryoku agent skill": "conexión de habilidad del agente ryoku", "wired to the vault": "conectado a la bóveda", - "wireless Xbox pads + controller access rules": "wireless Xbox pads + controller access rules", "wireless regulatory domain": "dominio regulatorio inalámbrico", "with Wine and umu-launcher.": "con Wine y umu-launcher.", "wm": "wm", @@ -14821,7 +14937,6 @@ "writes outside the plugin state dir (R8)": "escribe fuera del dir de estado del plugin (R8)", "writes under /etc; write only under stateDir (R8)": "escribe bajo /etc; escribe solo bajo stateDir (R8)", "writing %s: %v": "escribiendo %s: %v", - "writing /etc/conf.d/snapper: %v": "escribiendo /etc/conf.d/snapper: %v", "writing /etc/fstab": "escribiendo /etc/fstab", "writing /etc/snapper/configs/root: %v": "escribiendo /etc/snapper/configs/root: %v", "writing report: %w": "escribiendo informe: %w", @@ -14853,6 +14968,8 @@ "zen browser animations": "animaciones del navegador zen", "zen browser policy": "política del navegador zen", "| Tree | Files | Change goes to |\n|---|---|---|": "| Tree | Files | Change goes to |\n|---|---|---|", + "~%1 GB": "~%1 GB", + "~%1 MB": "~%1 MB", "~/.config/kitty/colors.toml": "~/.config/kitty/colors.toml", "~/.ssh/id_ed25519 (optional)": "~/.ssh/id_ed25519 (opcional)", "~/Pictures/Wallpapers": "~/Pictures/Wallpapers", @@ -14864,7 +14981,6 @@ "· building the speech runtime…": "· construir el tiempo de ejecución del discurso...", "· downloaded": "· descargado", "· downloading the model…": "· descargando el modelo…", - "· downloading…": "· descargando…", "· fetching the source…": "· buscando la fuente…", "· installing…": "· instalando…", "· not downloaded": "· no descargado", diff --git a/ryoku/i18n/catalog/fa.json b/ryoku/i18n/catalog/fa.json index 7fbad94d88..ffd080d2b1 100644 --- a/ryoku/i18n/catalog/fa.json +++ b/ryoku/i18n/catalog/fa.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "ویرایش", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "ویرایش شیما", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "ویرایش میزکار", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "شعاع بلور گاوسی اعمال‌شده بر روی کپی. بیشتر = نرم‌تر.", "General": "عمومی", + "General settings": "تنظیمات عمومی", "General limit: %1": "محدودیت عمومی: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "هرگز تمرکز نگیر", "New": "جدید", "New chat": "گفتگوی جدید", - "New folder": "New folder", + "New folder": "پوشهٔ جدید", "New keyring password": "رمز keyring جدید", "New machine defaults": "پیش‌فرض دستگاه جدید", "New machine from ISO": "دستگاه جدید از ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "اقدامات سریع", "Quick Settings": "تنظیمات سریع", - "Quick controls": "Quick controls", + "Quick controls": "کنترل‌های سریع", "Quick place": "Quick place", "Quick settings": "تنظیمات سریع", "Quicklink": "پیوند سریع", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "بارگذاری مجدد", "Reload cover": "بارگذاری مجدد روپوش", + "Reload desktop": "بارگذاری دوبارهٔ میزکار", "Reload shell": "بارگذاری مجدد Shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "بارگذاری مجدد دسکتاپ", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "سایه را به پهلو جابجا می‌کند، گویی از یک طرف روشن شده است", "Shifts the shadow up or down; positive is below": "سایه را بالا یا پایین می‌برد؛ مثبت یعنی پایین‌تر", "Shima": "Shima", + "Shima Studio": "استودیوی شیما", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "هنر شامل شده", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "در فضاهای کاری خالی نمایان بماند", "Steam API key": "کلید API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "مرورگر Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "استودیو", "Style": "سبک", "Style name": "Style name", "Subject": "موضوع", diff --git a/ryoku/i18n/catalog/fi.json b/ryoku/i18n/catalog/fi.json index df6f14c23f..833d4a5097 100644 --- a/ryoku/i18n/catalog/fi.json +++ b/ryoku/i18n/catalog/fi.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Muokkaa", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Muokkaa Shimaa", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Muokkaa työpöytää", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Kopiolle käytetty Gaussin sumennussäde. Korkeampi on pehmeämpi.", "General": "Yleiset", + "General settings": "Yleiset asetukset", "General limit: %1": "Yleinen raja: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Älä saa fokusta koskaan", "New": "Uusi", "New chat": "Uusi keskustelu", - "New folder": "New folder", + "New folder": "Uusi kansio", "New keyring password": "Uusi avainring-salasana", "New machine defaults": "Uuden koneen oletukset", "New machine from ISO": "Uusi kone ISO:sta", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Pikatoiminnot", "Quick Settings": "Pikaset", - "Quick controls": "Quick controls", + "Quick controls": "Pikasäätimet", "Quick place": "Quick place", "Quick settings": "Pikaset", "Quicklink": "Pikalinkkι", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Lataa uudelleen", "Reload cover": "Lataa kansi uudelleen", + "Reload desktop": "Lataa työpöytä uudelleen", "Reload shell": "Lataa shell uudelleen", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Ladataan työpöytää uudelleen", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Siirtää varjoa sivuttain, ikään kuin valaistu toiselta puolelta", "Shifts the shadow up or down; positive is below": "Siirtää varjoa ylös tai alas; positiivinen on alapuolella", "Shima": "Shima", + "Shima Studio": "Shiman studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Laivatasoitettu taide", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Pysy näkyvissä tyhjillä työtiloilla", "Steam API key": "Steam API -avain", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop -selain", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Työstudio", "Style": "Tyyli", "Style name": "Style name", "Subject": "Aihe", diff --git a/ryoku/i18n/catalog/fr.json b/ryoku/i18n/catalog/fr.json index c93b89c916..7e9b4e5c90 100644 --- a/ryoku/i18n/catalog/fr.json +++ b/ryoku/i18n/catalog/fr.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Modifier", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Modifier Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Modifier le bureau", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Rayon de flou gaussien appliqué à la copie. Plus élevé est plus doux.", "General": "Général", + "General settings": "Paramètres généraux", "General limit: %1": "Limite générale: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Ne jamais prendre le focus", "New": "Nouveau", "New chat": "Nouvelle discussion", - "New folder": "New folder", + "New folder": "Nouveau dossier", "New keyring password": "Nouveau mot de passe trousseau", "New machine defaults": "Valeurs par défaut des nouvelles machines", "New machine from ISO": "Nouvelle machine depuis ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Actions rapides", "Quick Settings": "Paramètres rapides", - "Quick controls": "Quick controls", + "Quick controls": "Commandes rapides", "Quick place": "Quick place", "Quick settings": "Paramètres rapides", "Quicklink": "Lien rapide", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Recharger", "Reload cover": "Couverture de rechargement", + "Reload desktop": "Recharger le bureau", "Reload shell": "Recharger le shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Rechargement du bureau", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Déplace l'ombre latéralement, comme si éclairée d'un côté", "Shifts the shadow up or down; positive is below": "Déplace l'ombre vers le haut ou le bas ; positif signifie en dessous", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Art fourni", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Rester visible sur les espaces de travail vides", "Steam API key": "Clé API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Navigateur Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Atelier", "Style": "Style", "Style name": "Style name", "Subject": "Sujet", diff --git a/ryoku/i18n/catalog/he.json b/ryoku/i18n/catalog/he.json index 6b56736fef..4da6a039cc 100644 --- a/ryoku/i18n/catalog/he.json +++ b/ryoku/i18n/catalog/he.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "ערוך", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "עריכת שימה", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "עריכת שולחן העבודה", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "רדיוס טישטוש גאוסי החל על העותק. גבוה יותר רך יותר.", "General": "כללי", + "General settings": "הגדרות כלליות", "General limit: %1": "הגבלה כללית: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "אל תקח מיקוד", "New": "חדש", "New chat": "צ'אט חדש", - "New folder": "New folder", + "New folder": "תיקייה חדשה", "New keyring password": "סיסמת keyring חדשה", "New machine defaults": "ברירות מכונה חדשה", "New machine from ISO": "מכונה חדשה מ-ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "פעולות מהירות", "Quick Settings": "הגדרות מהירות", - "Quick controls": "Quick controls", + "Quick controls": "בקרה מהירה", "Quick place": "Quick place", "Quick settings": "הגדרות מהירות", "Quicklink": "קישור מהיר", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "טען מחדש", "Reload cover": "טען כיסוי מחדש", + "Reload desktop": "טעינה מחדש של שולחן העבודה", "Reload shell": "טען shell מחדש", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "טעינת שולחן העבודה מחדש", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "מזיז את הצל לצדדים, כאילו מואר מצד אחד", "Shifts the shadow up or down; positive is below": "מזיז את הצל למעלה או למטה, חיובי הוא למטה", "Shima": "Shima", + "Shima Studio": "סטודיו שימה", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "אומנות משולחת", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "להישאר גלוי במרחבי עבודה ריקים", "Steam API key": "מפתח Steam API", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "דפדפן Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "סטודיו", "Style": "סגנון", "Style name": "Style name", "Subject": "נושא", diff --git a/ryoku/i18n/catalog/hi.json b/ryoku/i18n/catalog/hi.json index 38e4108ccc..2174ff0f65 100644 --- a/ryoku/i18n/catalog/hi.json +++ b/ryoku/i18n/catalog/hi.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "संपादित करें", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "शिमा संपादित करें", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "डेस्कटॉप संपादित करें", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "प्रतिलिपि पर लागू Gaussian blur त्रिज्या। अधिक मुलायम है।", "General": "सामान्य", + "General settings": "सामान्य सेटिंग्स", "General limit: %1": "सामान्य सीमा: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "फोकस कभी न लें", "New": "नया", "New chat": "नई chat", - "New folder": "New folder", + "New folder": "नया फ़ोल्डर", "New keyring password": "नया keyring पासवर्ड", "New machine defaults": "नई मशीन का डिफ़ॉल्ट", "New machine from ISO": "ISO से नई मशीन", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "त्वरित क्रियाएँ", "Quick Settings": "त्वरित सेटिंग्स", - "Quick controls": "Quick controls", + "Quick controls": "त्वरित नियंत्रण", "Quick place": "Quick place", "Quick settings": "त्वरित सेटिंग्स", "Quicklink": "क्विकलिंक", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "फिर से लोड करें", "Reload cover": "कवर फिर से लोड करें", + "Reload desktop": "डेस्कटॉप फिर से लोड करें", "Reload shell": "Shell फिर से लोड करें", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "डेस्कटॉप फिर से लोड किया जा रहा है", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "परछाई को एक तरफ से सरकाता है, जैसे कि एक तरफ से रोशनी आ रही हो", "Shifts the shadow up or down; positive is below": "परछाई को ऊपर या नीचे सरकाता है; धनात्मक नीचे है", "Shima": "Shima", + "Shima Studio": "शिमा स्टूडियो", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "भेजी गई कला", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "खाली कार्यस्थानों पर दिखाई देते रहें", "Steam API key": "स्टीम एपीआई कुंजी", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop ब्राउज़र", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "स्टूडियो", "Style": "शैली", "Style name": "Style name", "Subject": "विषय", diff --git a/ryoku/i18n/catalog/hu.json b/ryoku/i18n/catalog/hu.json index bcdc69cf05..3e4167bb6b 100644 --- a/ryoku/i18n/catalog/hu.json +++ b/ryoku/i18n/catalog/hu.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Szerkesztés", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shima szerkesztése", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Asztal szerkesztése", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Gauss-féle elmosási sugár. Magasabb = puhább.", "General": "Általános", + "General settings": "Általános beállítások", "General limit: %1": "Általános korlát: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Soha ne vegyen fokuszt", "New": "Új", "New chat": "Új csevegés", - "New folder": "New folder", + "New folder": "Új mappa", "New keyring password": "Új kulcsgyűrű jelszó", "New machine defaults": "Új gép alapértelmezései", "New machine from ISO": "Új gép az ISO-ból", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Gyors műveletek", "Quick Settings": "Gyors beállítások", - "Quick controls": "Quick controls", + "Quick controls": "Gyorsvezérlők", "Quick place": "Quick place", "Quick settings": "Gyors beállítások", "Quicklink": "Gyorslink", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Újrabetöltés", "Reload cover": "Borító újrabetöltése", + "Reload desktop": "Asztal újratöltése", "Reload shell": "Shell újrabetöltése", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Asztal újrabetöltése", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Oldalra tolja az árnyékot, mintha az egyik oldalról lenne megvilágítva", "Shifts the shadow up or down; positive is below": "Felfelé vagy lefelé tolja az árnyékot; a pozitív lefelé mutat", "Shima": "Shima", + "Shima Studio": "Shima Stúdió", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Szállított művészet", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Maradjon látható az üres munkaterületeken", "Steam API key": "Steam API kulcs", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop böngésző", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Műhely", "Style": "Stílus", "Style name": "Style name", "Subject": "Tárgy", diff --git a/ryoku/i18n/catalog/id.json b/ryoku/i18n/catalog/id.json index b9a784f527..b4e749ba84 100644 --- a/ryoku/i18n/catalog/id.json +++ b/ryoku/i18n/catalog/id.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Edit", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Sunting Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Sunting desktop", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Radius blur Gaussian diterapkan pada salinan. Lebih tinggi lebih lembut.", "General": "Umum", + "General settings": "Pengaturan umum", "General limit: %1": "Batas umum: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Jangan ambil fokus", "New": "Baru", "New chat": "Chat baru", - "New folder": "New folder", + "New folder": "Folder baru", "New keyring password": "Sandi keyring baru", "New machine defaults": "Default mesin baru", "New machine from ISO": "Mesin baru dari ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Tindakan Cepat", "Quick Settings": "Pengaturan Cepat", - "Quick controls": "Quick controls", + "Quick controls": "Kontrol cepat", "Quick place": "Quick place", "Quick settings": "Pengaturan cepat", "Quicklink": "Tautan cepat", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Muat ulang", "Reload cover": "Muat ulang cover", + "Reload desktop": "Muat ulang desktop", "Reload shell": "Muat ulang shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Memuat ulang desktop", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Geser bayangan ke samping, seolah disinari dari satu sisi", "Shifts the shadow up or down; positive is below": "Geser bayangan ke atas atau bawah; positif di bawah", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Seni pengiriman", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Tetap terlihat di ruang kerja kosong", "Steam API key": "Kunci API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Peramban Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Studio desain", "Style": "Gaya", "Style name": "Style name", "Subject": "Subjek", diff --git a/ryoku/i18n/catalog/it.json b/ryoku/i18n/catalog/it.json index 5970067868..4c365ba9ca 100644 --- a/ryoku/i18n/catalog/it.json +++ b/ryoku/i18n/catalog/it.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Modifica", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Modifica Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Modifica desktop", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Raggio blur Gaussiano applicato alla copia. Più alto è più morbido.", "General": "Generale", + "General settings": "Impostazioni generali", "General limit: %1": "Limite generale: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Non prendere mai il focus", "New": "Nuovo", "New chat": "Nuova chat", - "New folder": "New folder", + "New folder": "Nuova cartella", "New keyring password": "Nuova password keyring", "New machine defaults": "Predefiniti nuova macchina", "New machine from ISO": "Nuova macchina da ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Azioni rapide", "Quick Settings": "Impostazioni rapide", - "Quick controls": "Quick controls", + "Quick controls": "Controlli rapidi", "Quick place": "Quick place", "Quick settings": "Impostazioni rapide", "Quicklink": "Link rapido", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Ricarica", "Reload cover": "Ricarica copertina", + "Reload desktop": "Ricarica desktop", "Reload shell": "Ricarica shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Ricarica desktop", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Sposta l'ombra lateralmente, come se fosse illuminata da un lato", "Shifts the shadow up or down; positive is below": "Sposta l'ombra su o giù; positivo è sotto", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Art spedito", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Resta visibile negli spazi di lavoro vuoti", "Steam API key": "Chiave API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Browser Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Laboratorio", "Style": "Stile", "Style name": "Style name", "Subject": "Oggetto", diff --git a/ryoku/i18n/catalog/ja.json b/ryoku/i18n/catalog/ja.json index bb2a3c5e21..1624f83584 100644 --- a/ryoku/i18n/catalog/ja.json +++ b/ryoku/i18n/catalog/ja.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "編集", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shimaを編集", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "デスクトップを編集", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "コピーに適用するガウスぼかし半径。高いほど柔らかい。", "General": "一般", + "General settings": "一般設定", "General limit: %1": "全般制限: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "フォーカスを取らない", "New": "新規", "New chat": "新規チャット", - "New folder": "New folder", + "New folder": "新しいフォルダー", "New keyring password": "新しいキーリング パスワード", "New machine defaults": "新規マシン デフォルト", "New machine from ISO": "ISOから新規マシン", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "クイックアクション", "Quick Settings": "クイック設定", - "Quick controls": "Quick controls", + "Quick controls": "クイック操作", "Quick place": "Quick place", "Quick settings": "クイック設定", "Quicklink": "クイックリンク", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "再読み込み", "Reload cover": "カバーを再読み込み", + "Reload desktop": "デスクトップを再読み込み", "Reload shell": "シェルを再読み込み", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "デスクトップを再読み込み中", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "影を横にずらす、片側から照らされたように", "Shifts the shadow up or down; positive is below": "影を上下にずらす、正の値は下", "Shima": "Shima", + "Shima Studio": "Shimaスタジオ", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "同梱アート", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "空のワークスペースでも表示する", "Steam API key": "Steam API キー", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam ワークショップブラウザ", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "スタジオ", "Style": "スタイル", "Style name": "Style name", "Subject": "被写体", diff --git a/ryoku/i18n/catalog/ko.json b/ryoku/i18n/catalog/ko.json index a19165570f..9a92d6e764 100644 --- a/ryoku/i18n/catalog/ko.json +++ b/ryoku/i18n/catalog/ko.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "편집", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shima 편집", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "바탕 화면 편집", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "사본에 적용된 가우시안 흐림 반경, 클수록 부드러움", "General": "일반", + "General settings": "일반 설정", "General limit: %1": "일반 제한: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "절대 포커스 받지 않음", "New": "새로 만들기", "New chat": "새 채팅", - "New folder": "New folder", + "New folder": "새 폴더", "New keyring password": "새 키링 비밀번호", "New machine defaults": "새 머신 기본값", "New machine from ISO": "ISO에서 새 머신", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "빠른 작업", "Quick Settings": "빠른 설정", - "Quick controls": "Quick controls", + "Quick controls": "빠른 제어", "Quick place": "Quick place", "Quick settings": "빠른 설정", "Quicklink": "빠른 링크", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "다시 로드", "Reload cover": "커버 다시 로드", + "Reload desktop": "바탕 화면 새로고침", "Reload shell": "셸 다시 로드", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "데스크톱 다시 로드 중", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "한쪽에서 비추는 것처럼 그림자를 옆으로 이동", "Shifts the shadow up or down; positive is below": "그림자를 위아래로 이동 (양수는 아래)", "Shima": "Shima", + "Shima Studio": "Shima 스튜디오", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "배송된 아트", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "빈 작업 공간에서도 계속 표시", "Steam API key": "Steam API 키", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop 브라우저", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "스튜디오", "Style": "스타일", "Style name": "Style name", "Subject": "주제", diff --git a/ryoku/i18n/catalog/ml.json b/ryoku/i18n/catalog/ml.json index 71930ff6d5..5376c210bf 100644 --- a/ryoku/i18n/catalog/ml.json +++ b/ryoku/i18n/catalog/ml.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "എഡിറ്റ് ചെയ്യുക", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "ഷിമ തിരുത്തുക", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "ഡെസ്ക്ടോപ്പ് തിരുത്തുക", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "കോപ്പിയിൽ പ്രയോഗിച്ച ഗാസിയൻ ബ്ലർ ആരം. കൂടുതൽ മൃദു.", "General": "സാധാരണ", + "General settings": "പൊതു ക്രമീകരണങ്ങൾ", "General limit: %1": "സാധാരണ പരിധി: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "ഒരിക്കലും ഫോക്കസ് എടുക്കരുത്", "New": "പുതിയ", "New chat": "പുതിയ ചാറ്റ്", - "New folder": "New folder", + "New folder": "പുതിയ ഫോൾഡർ", "New keyring password": "പുതിയ keyring പാസ്വർഡ്", "New machine defaults": "പുതിയ മെഷീൻ ഡിഫോൾട്ടുകൾ", "New machine from ISO": "ISO ൽ നിന്ന് പുതിയ മെഷീൻ", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "ദ്രുത പ്രവർത്തനങ്ങൾ", "Quick Settings": "ദ്രുത സെറ്റിംഗുകൾ", - "Quick controls": "Quick controls", + "Quick controls": "ദ്രുത നിയന്ത്രണങ്ങൾ", "Quick place": "Quick place", "Quick settings": "ദ്രുത സെറ്റിംഗുകൾ", "Quicklink": "ദ്രുത ലിങ്ക്", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "വീണ്ടും ലോഡ് ചെയ്യുക", "Reload cover": "കവർ വീണ്ടും ലോഡ് ചെയ്യുക", + "Reload desktop": "ഡെസ്ക്ടോപ്പ് വീണ്ടും ലോഡുചെയ്യുക", "Reload shell": "Shell വീണ്ടും ലോഡ് ചെയ്യുക", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "ഡെസ്ക്ടോപ്പ് വീണ്ടും ലോഡ് ചെയ്യുന്നു", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "നിഴലിനെ ഒരു വശത്ത് നിന്ന് പ്രകാശിക്കുന്നതുപോലെ വശങ്ങളിലേക്ക് മാറ്റുന്നു", "Shifts the shadow up or down; positive is below": "ഷാഡോ മുകളിലേക്കോ താഴേക്കോ മാറ്റുക; പോസിറ്റീവ് താഴെയാണ്", "Shima": "Shima", + "Shima Studio": "ഷിമ സ്റ്റുഡിയോ", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "നിരുപിച്ച കലാകര്മ്മം", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "ശൂന്യമായ വർക്ക്‌സ്‌പേസുകളിൽ ദൃശ്യമാകുക", "Steam API key": "Steam API കീ", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop ബ്രൗസർ", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "സ്റ്റുഡിയോ", "Style": "ശൈലി", "Style name": "Style name", "Subject": "വിഷയം", diff --git a/ryoku/i18n/catalog/mr.json b/ryoku/i18n/catalog/mr.json index 1973ed88ed..275a521614 100644 --- a/ryoku/i18n/catalog/mr.json +++ b/ryoku/i18n/catalog/mr.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "संपादित करा", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "शिमा संपादित करा", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "डेस्कटॉप संपादित करा", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "प्रतीकृतीवर लागू केलेली गॉसियन ब्लर त्रिज्या. जास्त हे मृदु आहे.", "General": "सामान्य", + "General settings": "सामान्य सेटिंग्स", "General limit: %1": "सामान्य मर्यादा: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "कधीही फोकस घेऊ नका", "New": "नवीन", "New chat": "नवीन चॅट", - "New folder": "New folder", + "New folder": "नवीन फोल्डर", "New keyring password": "नवीन कीरिंग संकेतशब्द", "New machine defaults": "नवीन मशीन डिफॉल्ट", "New machine from ISO": "ISO पासून नवीन मशीन", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "द्रुत क्रिया", "Quick Settings": "द्रुत सेटिंग्ज", - "Quick controls": "Quick controls", + "Quick controls": "झटपट नियंत्रणे", "Quick place": "Quick place", "Quick settings": "द्रुत सेटिंग्ज", "Quicklink": "द्रुत दुवा", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "पुनः लोड करा", "Reload cover": "Cover पुनः लोड करा", + "Reload desktop": "डेस्कटॉप पुन्हा लोड करा", "Reload shell": "Shell पुनः लोड करा", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "डेस्कटॉप पुनः लोड करत आहे", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "छाया बाजूला सरकवते, जसे की एका बाजूने प्रकाशित केले आहे", "Shifts the shadow up or down; positive is below": "छाया वर किंवा खाली सरकवते; सकारात्मक खाली आहे", "Shima": "Shima", + "Shima Studio": "शिमा स्टुडिओ", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "शिप केलेली कला", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "रिकाम्या कार्यक्षेत्रांवर दृश्यमान राहा", "Steam API key": "Steam API की", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop ब्राउজर", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "स्टुडिओ", "Style": "शैली", "Style name": "Style name", "Subject": "विषय", diff --git a/ryoku/i18n/catalog/nb.json b/ryoku/i18n/catalog/nb.json index 41e02bffd7..7732ee9fab 100644 --- a/ryoku/i18n/catalog/nb.json +++ b/ryoku/i18n/catalog/nb.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Rediger", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Rediger Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Rediger skrivebordet", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Gaussisk uskarphetsradius brukt på kopien. Høyere er mykeere.", "General": "Generelt", + "General settings": "Generelle innstillinger", "General limit: %1": "Generell grense: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Aldri ta fokus", "New": "Ny", "New chat": "Ny chat", - "New folder": "New folder", + "New folder": "Ny mappe", "New keyring password": "Nytt nøkkelringer-passord", "New machine defaults": "Standard for ny maskin", "New machine from ISO": "Ny maskin fra ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Hurtighandlinger", "Quick Settings": "Hurtiginnstillinger", - "Quick controls": "Quick controls", + "Quick controls": "Hurtigkontroller", "Quick place": "Quick place", "Quick settings": "Hurtiginnstillinger", "Quicklink": "Hurtiglenke", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Last på nytt", "Reload cover": "Last omslag på nytt", + "Reload desktop": "Last skrivebordet på nytt", "Reload shell": "Last shell på nytt", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Laster skrivebord på nytt", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Skyver skyggen sidelengs, som om belyst fra én side", "Shifts the shadow up or down; positive is below": "Skyver skyggen opp eller ned; positiv er under", "Shima": "Shima", + "Shima Studio": "Shima-studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Sendt kunst", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Forbli synlig på tomme arbeidsområder", "Steam API key": "Steam API-nøkkel", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop-nettleser", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Atelier", "Style": "Stil", "Style name": "Style name", "Subject": "Emne", diff --git a/ryoku/i18n/catalog/nl.json b/ryoku/i18n/catalog/nl.json index cbfffb215f..c641572e93 100644 --- a/ryoku/i18n/catalog/nl.json +++ b/ryoku/i18n/catalog/nl.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Bewerk", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shima bewerken", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Bureaublad bewerken", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Gaussiaanse wazig straal op kopie. Hoger is zachter.", "General": "Algemeen", + "General settings": "Algemene instellingen", "General limit: %1": "Algemene limiet: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nooit focus nemen", "New": "Nieuw", "New chat": "Nieuw chat", - "New folder": "New folder", + "New folder": "Nieuwe map", "New keyring password": "Nieuw sleutelring-wachtwoord", "New machine defaults": "Standaards nieuwe machine", "New machine from ISO": "Nieuwe machine van ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Snelle acties", "Quick Settings": "Snelle instellingen", - "Quick controls": "Quick controls", + "Quick controls": "Snelle bediening", "Quick place": "Quick place", "Quick settings": "Snelle instellingen", "Quicklink": "Snelkoppeling", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Opnieuw laden", "Reload cover": "Cover opnieuw laden", + "Reload desktop": "Bureaublad opnieuw laden", "Reload shell": "Shell opnieuw laden", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Bureaublad opnieuw laden", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Verschuift de schaduw zijwaarts, alsof van één kant belicht", "Shifts the shadow up or down; positive is below": "Verschuift de schaduw omhoog of omlaag; positief is lager", "Shima": "Shima", + "Shima Studio": "Shima-studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Verzonden art", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Zichtbaar blijven op lege werkbladen", "Steam API key": "Steam API-sleutel", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop-browser", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Ontwerpstudio", "Style": "Stijl", "Style name": "Style name", "Subject": "Onderwerp", diff --git a/ryoku/i18n/catalog/overrides/es.json b/ryoku/i18n/catalog/overrides/es.json index d4dfd6fcf9..6be1aed053 100644 --- a/ryoku/i18n/catalog/overrides/es.json +++ b/ryoku/i18n/catalog/overrides/es.json @@ -105,11 +105,13 @@ "MT/s": "MT/s", "MUX": "MUX", "MiB": "MiB", + "New application": "Nueva aplicación", "No.": "No.", "Normal": "Normal", "O": "O", "OkShell": "OkShell", "Omarchy": "Omarchy", + "Open folder": "Abrir carpeta", "OpenAI Codex": "OpenAI Codex", "OpenCode": "OpenCode", "P1": "P1", @@ -149,6 +151,7 @@ "SSID": "SSID", "Save": "Guardar", "Shell": "Shell", + "Sleep": "Suspender", "Space Grotesk 14": "Space Grotesk 14", "SpaceMono 12": "SpaceMono 12", "TEMP": "TEMP", diff --git a/ryoku/i18n/catalog/pl.json b/ryoku/i18n/catalog/pl.json index 328fa2b6e9..31cd2edf69 100644 --- a/ryoku/i18n/catalog/pl.json +++ b/ryoku/i18n/catalog/pl.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Edytuj", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Edytuj Shimę", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Edytuj pulpit", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Promień rozmycia gaussowskiego zastosowany do kopii. Wyższa jest bardziej miękka.", "General": "Generał", + "General settings": "Ustawienia ogólne", "General limit: %1": "Limit ogólny: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nigdy nie skupiaj się", "New": "Nowe", "New chat": "Nowy czat", - "New folder": "New folder", + "New folder": "Nowy folder", "New keyring password": "Nowe hasło do pęku kluczy", "New machine defaults": "Nowe ustawienia domyślne maszyny", "New machine from ISO": "Nowa maszyna od ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Szybkie działania", "Quick Settings": "Szybkie ustawienia", - "Quick controls": "Quick controls", + "Quick controls": "Szybkie sterowanie", "Quick place": "Quick place", "Quick settings": "Szybkie ustawienia", "Quicklink": "Szybki link", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Załaduj ponownie", "Reload cover": "Załaduj ponownie okładkę", + "Reload desktop": "Wczytaj ponownie pulpit", "Reload shell": "Załaduj ponownie powłokę", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Przeładowywanie pulpitu", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Przesuwa cień na boki, jakby był oświetlony z jednej strony", "Shifts the shadow up or down; positive is below": "Przesuwa cień w górę lub w dół; dodatnie to poniżej", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Wysłana sztuka", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Pozostań widoczny na pustych obszarach roboczych", "Steam API key": "Klucz API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Przeglądarka Warsztatu Steam", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Pracownia", "Style": "Styl", "Style name": "Style name", "Subject": "Temat", diff --git a/ryoku/i18n/catalog/pt.json b/ryoku/i18n/catalog/pt.json index 27abe99a4c..8b5fd52f90 100644 --- a/ryoku/i18n/catalog/pt.json +++ b/ryoku/i18n/catalog/pt.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Editar", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Editar Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Editar ambiente de trabalho", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Raio de desfoque gaussiano aplicado à cópia. Maior é mais suave.", "General": "Geral", + "General settings": "Definições gerais", "General limit: %1": "Limite geral: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nunca focar", "New": "Novo", "New chat": "Nova conversa", - "New folder": "New folder", + "New folder": "Nova pasta", "New keyring password": "Nova senha do chaveiro", "New machine defaults": "Predefinicoes de nova maquina", "New machine from ISO": "Nova maquina a partir de ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Ações Rápidas", "Quick Settings": "Configurações Rápidas", - "Quick controls": "Quick controls", + "Quick controls": "Controlos rápidos", "Quick place": "Quick place", "Quick settings": "Configurações rápidas", "Quicklink": "Link Rápido", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Recarregar", "Reload cover": "Capa de recarregamento", + "Reload desktop": "Recarregar ambiente de trabalho", "Reload shell": "Recarregar shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Recarregando a área de trabalho", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Desloca a sombra lateralmente, como se iluminado de um lado", "Shifts the shadow up or down; positive is below": "Desloca a sombra para cima ou para baixo; positivo é abaixo", "Shima": "Shima", + "Shima Studio": "Estúdio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Arte incluída", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Manter visível em áreas de trabalho vazias", "Steam API key": "Chave API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Navegador Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Estúdio", "Style": "Estilo", "Style name": "Style name", "Subject": "Assunto", diff --git a/ryoku/i18n/catalog/pt_BR.json b/ryoku/i18n/catalog/pt_BR.json index 061805f308..34823ce1f9 100644 --- a/ryoku/i18n/catalog/pt_BR.json +++ b/ryoku/i18n/catalog/pt_BR.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Editar", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Editar Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Editar área de trabalho", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Raio de desfoque gaussiano aplicado à cópia. Maior = mais suave.", "General": "Geral", + "General settings": "Configurações gerais", "General limit: %1": "Limite geral: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nunca focar", "New": "Novo", "New chat": "Nova conversa", - "New folder": "New folder", + "New folder": "Nova pasta", "New keyring password": "Nova senha do chaveiro", "New machine defaults": "Padroes de nova maquina", "New machine from ISO": "Nova maquina a partir de ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Ações Rápidas", "Quick Settings": "Configurações Rápidas", - "Quick controls": "Quick controls", + "Quick controls": "Controles rápidos", "Quick place": "Quick place", "Quick settings": "Configurações rápidas", "Quicklink": "Link Rápido", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Recarregar", "Reload cover": "Capa de recarga", + "Reload desktop": "Recarregar área de trabalho", "Reload shell": "Recarregar shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Recarregando a tela", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Desloca a sombra para os lados, como se iluminado de um lado", "Shifts the shadow up or down; positive is below": "Desloca a sombra para cima ou para baixo; positivo é abaixo", "Shima": "Shima", + "Shima Studio": "Estúdio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Arte inclusa", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Permanecer visível em áreas de trabalho vazias", "Steam API key": "Chave da API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Navegador Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Estúdio", "Style": "Estilo", "Style name": "Style name", "Subject": "Assunto", diff --git a/ryoku/i18n/catalog/ro.json b/ryoku/i18n/catalog/ro.json index ce4fba093b..f47c08c1e8 100644 --- a/ryoku/i18n/catalog/ro.json +++ b/ryoku/i18n/catalog/ro.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Editează", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Editează Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Editează desktopul", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Rază estompare Gauss aplicată copiei. Mai înalt este mai moale.", "General": "General", + "General settings": "Setări generale", "General limit: %1": "Limită generală: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Nu lua focusul niciodată", "New": "Nou", "New chat": "Chat nou", - "New folder": "New folder", + "New folder": "Dosar nou", "New keyring password": "Parolă keyring nouă", "New machine defaults": "Implicite mașină nouă", "New machine from ISO": "Mașină nouă din ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Acțiuni rapide", "Quick Settings": "Setări rapide", - "Quick controls": "Quick controls", + "Quick controls": "Comenzi rapide", "Quick place": "Quick place", "Quick settings": "Setări rapide", "Quicklink": "Legătură rapidă", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Reîncărcare", "Reload cover": "Reîncarcă copertă", + "Reload desktop": "Reîncarcă desktopul", "Reload shell": "Reîncarcă shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Se reîncarcă desktop", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Deplasează umbra lateral, ca și cum ar fi iluminată dintr-o parte", "Shifts the shadow up or down; positive is below": "Deplasează umbra în sus sau în jos; pozitiv este în jos", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Grafică expediere", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Rămâi vizibil pe spațiile de lucru goale", "Steam API key": "Cheie API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Browser Workshop Steam", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Atelier", "Style": "Stil", "Style name": "Style name", "Subject": "Subiect", diff --git a/ryoku/i18n/catalog/ru.json b/ryoku/i18n/catalog/ru.json index 572e535878..4d679ebfb0 100644 --- a/ryoku/i18n/catalog/ru.json +++ b/ryoku/i18n/catalog/ru.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Редактировать", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Изменить Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Изменить рабочий стол", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Радиус размытия, применяемый к копии. Больше - мягче.", "General": "Основное", + "General settings": "Общие настройки", "General limit: %1": "Основной лимит: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Не захватывать фокус", "New": "Новое", "New chat": "Новый чат", - "New folder": "New folder", + "New folder": "Новая папка", "New keyring password": "Новый пароль связки ключей", "New machine defaults": "Параметры новой машины", "New machine from ISO": "Новая машина из ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Быстрые действия", "Quick Settings": "Быстрые параметры", - "Quick controls": "Quick controls", + "Quick controls": "Быстрое управление", "Quick place": "Quick place", "Quick settings": "Быстрые параметры", "Quicklink": "Быстрая ссылка", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Перезагрузить", "Reload cover": "Перезагрузить обложку", + "Reload desktop": "Перезагрузить рабочий стол", "Reload shell": "Перезагрузить shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Перезагрузка рабочего стола", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Сдвигает тень вбок, как будто освещена с одной стороны", "Shifts the shadow up or down; positive is below": "Сдвигает тень вверх или вниз; положительное - вниз", "Shima": "Shima", + "Shima Studio": "Студия Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Встроенное оформление", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Оставаться видимым на пустых рабочих столах", "Steam API key": "Ключ API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Браузер Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Студия", "Style": "Стиль", "Style name": "Style name", "Subject": "Тема", diff --git a/ryoku/i18n/catalog/sv.json b/ryoku/i18n/catalog/sv.json index 1ae1a63ee4..7cf8d5eafe 100644 --- a/ryoku/i18n/catalog/sv.json +++ b/ryoku/i18n/catalog/sv.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Redigera", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Redigera Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Redigera skrivbordet", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Gaussisk suddningsradie på kopian. Högre är mjukare.", "General": "Allmänt", + "General settings": "Allmänna inställningar", "General limit: %1": "Allmän gräns: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Ta aldrig fokus", "New": "Ny", "New chat": "Ny chatt", - "New folder": "New folder", + "New folder": "Ny mapp", "New keyring password": "Nytt nyckelringslösenord", "New machine defaults": "Nya maskinstandard", "New machine from ISO": "Ny maskin från ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Snabbåtgärder", "Quick Settings": "Snabbinställningar", - "Quick controls": "Quick controls", + "Quick controls": "Snabbkontroller", "Quick place": "Quick place", "Quick settings": "Snabbinställningar", "Quicklink": "Snabbkälla", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Ladda om", "Reload cover": "Ladda om omslag", + "Reload desktop": "Ladda om skrivbordet", "Reload shell": "Ladda om shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Laddar om skrivbordet", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Flyttar skuggan i sidled, som om belyst från en sida", "Shifts the shadow up or down; positive is below": "Flyttar skuggan upp eller ner; positiv är nedan", "Shima": "Shima", + "Shima Studio": "Shima-studio", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Levererad grafik", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Förbli synlig på tomma arbetsytor", "Steam API key": "Steam API-nyckel", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop-webbläsare", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Ateljé", "Style": "Stil", "Style name": "Style name", "Subject": "Ämne", diff --git a/ryoku/i18n/catalog/ta.json b/ryoku/i18n/catalog/ta.json index 2e809eea21..661b966ff3 100644 --- a/ryoku/i18n/catalog/ta.json +++ b/ryoku/i18n/catalog/ta.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "திருத்து", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "ஷிமாவைத் திருத்து", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "பணிமேடையைத் திருத்து", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "நகலுக்குப் பயன்படுத்தப்படும் காஸியன் மங்கலான ஆரம். அதிகமாக மென்மையாக உள்ளது.", "General": "பொதுவு", + "General settings": "பொது அமைப்புகள்", "General limit: %1": "பொதுவு வரம்பு: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "ஒருபோதும் கவனத்தை பெற வேண்டாம்", "New": "புதிய", "New chat": "புதிய சட்டை", - "New folder": "New folder", + "New folder": "புதிய கோப்புறை", "New keyring password": "புதிய keyring கடவுச்சொல்", "New machine defaults": "புதிய கணினி இயல்புகள்", "New machine from ISO": "ISO இலிருந்து புதிய கணினி", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "விரைந்த செயல்கள்", "Quick Settings": "விரைந்த அமைப்புகள்", - "Quick controls": "Quick controls", + "Quick controls": "விரைவு கட்டுப்பாடுகள்", "Quick place": "Quick place", "Quick settings": "விரைந்த அமைப்புகள்", "Quicklink": "விரைந்த இணைப்பு", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "மீண்டும் ஏற்றவும்", "Reload cover": "மூடி மீண்டும் ஏற்றவும்", + "Reload desktop": "பணிமேடையை மீண்டும் ஏற்று", "Reload shell": "Shell மீண்டும் ஏற்றவும்", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "மேசைப்பணி மீண்டும் ஏற்றுகிறது", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "நிழலை பக்கவாட்டாக நகர்த்துகிறது, ஒரு பக்கத்திலிருந்து ஒளிரப்பட்டது போல", "Shifts the shadow up or down; positive is below": "நிழலை மேலும் கீழும் நகர்த்துகிறது; நேர்மறை என்றால் கீழே", "Shima": "Shima", + "Shima Studio": "ஷிமா ஸ்டுடியோ", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "கப்பல் கலை", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "காலியான பணியிடங்களில் தொடர்ந்து காண்பி", "Steam API key": "Steam API விசை", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop உலாவி", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "ஸ்டுடியோ", "Style": "பாணி", "Style name": "Style name", "Subject": "பொருள்", diff --git a/ryoku/i18n/catalog/te.json b/ryoku/i18n/catalog/te.json index 9d4e2075f3..a0981f7ff4 100644 --- a/ryoku/i18n/catalog/te.json +++ b/ryoku/i18n/catalog/te.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "సవరించు", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "షిమాను సవరించు", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "డెస్క్‌టాప్‌ను సవరించు", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "కాపీకి వర్తించిన గాస్సియన్ బ్లర్ వ్యాసార్థం. ఎక్కువ మృదువైనది.", "General": "సాధారణ", + "General settings": "సాధారణ సెట్టింగ్‌లు", "General limit: %1": "సాధారణ సీమ: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "ఎప్పుడూ ఫోకస్ తీసుకోవద్దు", "New": "కొత్త", "New chat": "కొత్త చాట్", - "New folder": "New folder", + "New folder": "కొత్త ఫోల్డర్", "New keyring password": "కీరింగ్ కొత్త పాస్‌వర్డ్", "New machine defaults": "కొత్త యంత్ర డిఫాల్ట్‌లు", "New machine from ISO": "ISO నుండి కొత్త యంత్రం", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "త్వరిత చర్యలు", "Quick Settings": "త్వరిత సెట్టింగ్‌లు", - "Quick controls": "Quick controls", + "Quick controls": "శీఘ్ర నియంత్రణలు", "Quick place": "Quick place", "Quick settings": "త్వరిత సెట్టింగ్‌లు", "Quicklink": "త్వరిత లింక్", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "పునరాలోడ్ చేయండి", "Reload cover": "కవర్‌ను పునరాలోడ్ చేయండి", + "Reload desktop": "డెస్క్‌టాప్‌ను మళ్లీ లోడ్ చేయి", "Reload shell": "Shell ను పునరాలోడ్ చేయండి", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "డెస్క్‌టాప్‌ను పునరాలోడ్ చేస్తున్న", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "నీడను పక్కకు మారుస్తుంది, ఒక వైపు నుండి వెలుగుతున్నట్లుగా", "Shifts the shadow up or down; positive is below": "నీడను పైకి లేదా క్రిందికి మారుస్తుంది; సానుకూలం క్రింద ఉంది.", "Shima": "Shima", + "Shima Studio": "షిమా స్టూడియో", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "అందించిన కళ", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "ఖాళీ పని ప్రదేశాల్లో కనిపించేలా ఉంచు", "Steam API key": "Steam API కీ", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop ఐప్రేక్ష", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "స్టూడియో", "Style": "శైలి", "Style name": "Style name", "Subject": "విషయం", diff --git a/ryoku/i18n/catalog/th.json b/ryoku/i18n/catalog/th.json index 2fe4b0eff5..6ce3c4def8 100644 --- a/ryoku/i18n/catalog/th.json +++ b/ryoku/i18n/catalog/th.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "แก้ไข", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "แก้ไข Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "แก้ไขเดสก์ท็อป", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "รัศมีเบลอเกาส์เซียนที่ใช้กับสำเนา ยิ่งสูงยิ่งนุ่มนวล", "General": "ทั่วไป", + "General settings": "การตั้งค่าทั่วไป", "General limit: %1": "ขีดจำกัดทั่วไป: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "ไม่ใช้โฟกัสเลย", "New": "ใหม่", "New chat": "แชทใหม่", - "New folder": "New folder", + "New folder": "โฟลเดอร์ใหม่", "New keyring password": "รหัสผ่าน Keyring ใหม่", "New machine defaults": "ค่าเริ่มต้นเครื่องใหม่", "New machine from ISO": "เครื่องใหม่จาก ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "การกระทำด่วน", "Quick Settings": "การตั้งค่าด่วน", - "Quick controls": "Quick controls", + "Quick controls": "การควบคุมด่วน", "Quick place": "Quick place", "Quick settings": "การตั้งค่าด่วน", "Quicklink": "ลิงก์ด่วน", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "โหลดใหม่", "Reload cover": "โหลดปกใหม่", + "Reload desktop": "โหลดเดสก์ท็อปใหม่", "Reload shell": "โหลด shell ใหม่", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "กำลังโหลดเดสก์ท็อปใหม่", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "เลื่อนเงาไปด้านข้าง เหมือนมีแสงส่องจากด้านเดียว", "Shifts the shadow up or down; positive is below": "เลื่อนเงาขึ้นหรือลง ค่าบวกคือด้านล่าง", "Shima": "Shima", + "Shima Studio": "สตูดิโอ Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "ศิลปะที่ส่งแล้ว", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "แสดงต่อไปบนพื้นที่ทำงานที่ว่างเปล่า", "Steam API key": "คีย์ Steam API", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "ตัวท่องแบบ Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "สตูดิโอ", "Style": "สไตล์", "Style name": "Style name", "Subject": "หัวเรื่อง", diff --git a/ryoku/i18n/catalog/tr.json b/ryoku/i18n/catalog/tr.json index 1247c4bbc0..a1c366f5f6 100644 --- a/ryoku/i18n/catalog/tr.json +++ b/ryoku/i18n/catalog/tr.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Düzenle", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Shima’yı düzenle", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Masaüstünü düzenle", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Kopyaya uygulanan Gauss bulanıklığı yarıçapı. Yüksek değer daha yumuşak.", "General": "Genel", + "General settings": "Genel ayarlar", "General limit: %1": "Genel sınır: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Asla odağı alma", "New": "Yeni", "New chat": "Yeni sohbet", - "New folder": "New folder", + "New folder": "Yeni klasör", "New keyring password": "Yeni anahtar depo şifresi", "New machine defaults": "Yeni makine varsayılanları", "New machine from ISO": "ISO'dan yeni makine", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Hızlı İşlemler", "Quick Settings": "Hızlı Ayarlar", - "Quick controls": "Quick controls", + "Quick controls": "Hızlı denetimler", "Quick place": "Quick place", "Quick settings": "Hızlı ayarlar", "Quicklink": "Hızlı Bağlantı", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Yeniden yükle", "Reload cover": "Kapağı yeniden yükle", + "Reload desktop": "Masaüstünü yeniden yükle", "Reload shell": "Shell'i yeniden yükle", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Masaüstü yeniden yükleniyor", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Gölgeyi tek taraftan aydınlatılmış gibi yana kaydırır", "Shifts the shadow up or down; positive is below": "Gölgeyi yukarı veya aşağı kaydırır; pozitif aşağıdadır", "Shima": "Shima", + "Shima Studio": "Shima Stüdyosu", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Gönderilen sanat", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Boş çalışma alanlarında görünür kal", "Steam API key": "Steam API anahtarı", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam Workshop tarayıcı", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Stüdyo", "Style": "Stil", "Style name": "Style name", "Subject": "Konu", diff --git a/ryoku/i18n/catalog/uk.json b/ryoku/i18n/catalog/uk.json index 7bdfb3ec76..ba66941815 100644 --- a/ryoku/i18n/catalog/uk.json +++ b/ryoku/i18n/catalog/uk.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Редагувати", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Редагувати Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Редагувати стільницю", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Радіус розмиття Гауса для копії. Більше = м'якше.", "General": "Загальні", + "General settings": "Загальні налаштування", "General limit: %1": "Загальне обмеження: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Ніколи не брати фокус", "New": "Нова", "New chat": "Новий чат", - "New folder": "New folder", + "New folder": "Нова тека", "New keyring password": "Новий пароль сховища", "New machine defaults": "Нові налаштування машини", "New machine from ISO": "Нова машина з ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Швидкі дії", "Quick Settings": "Швидкі налаштування", - "Quick controls": "Quick controls", + "Quick controls": "Швидке керування", "Quick place": "Quick place", "Quick settings": "Швидкі налаштування", "Quicklink": "Швидке посилання", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Перезавантажити", "Reload cover": "Перезавантажити обкладинку", + "Reload desktop": "Перезавантажити стільницю", "Reload shell": "Перезавантажити shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Перезавантаження робочого стола", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Зміщує тінь убік, ніби освітлену з одного боку", "Shifts the shadow up or down; positive is below": "Зміщує тінь вгору або вниз; позитивне значення - вниз", "Shima": "Shima", + "Shima Studio": "Студія Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Включене мистецтво", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Залишатися видимим на порожніх робочих просторах", "Steam API key": "Ключ API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Переглядач Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Студія", "Style": "Стиль", "Style name": "Style name", "Subject": "Тема", diff --git a/ryoku/i18n/catalog/vi.json b/ryoku/i18n/catalog/vi.json index 99954bf4c7..841c3bf793 100644 --- a/ryoku/i18n/catalog/vi.json +++ b/ryoku/i18n/catalog/vi.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "Chỉnh sửa", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "Chỉnh sửa Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "Chỉnh sửa màn hình nền", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "Bán kính mờ Gaussian áp dụng cho bản sao. Cao hơn mềm hơn.", "General": "Chung", + "General settings": "Cài đặt chung", "General limit: %1": "Giới hạn chung: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "Không bao giờ lấy tiêu điểm", "New": "Mới", "New chat": "Chat mới", - "New folder": "New folder", + "New folder": "Thư mục mới", "New keyring password": "Mật khẩu keyring mới", "New machine defaults": "Giá trị mặc định máy mới", "New machine from ISO": "Máy mới từ ISO", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "Hành động nhanh", "Quick Settings": "Cài đặt nhanh", - "Quick controls": "Quick controls", + "Quick controls": "Điều khiển nhanh", "Quick place": "Quick place", "Quick settings": "Cài đặt nhanh", "Quicklink": "Liên kết nhanh", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "Tải lại", "Reload cover": "Tải lại bìa", + "Reload desktop": "Tải lại màn hình nền", "Reload shell": "Tải lại shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "Đang tải lại desktop", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "Dịch chuyển bóng sang một bên, như thể được chiếu sáng từ một phía", "Shifts the shadow up or down; positive is below": "Dịch chuyển bóng lên hoặc xuống; giá trị dương là bên dưới", "Shima": "Shima", + "Shima Studio": "Studio Shima", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "Nghệ thuật gắn sẵn", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "Luôn hiển thị trên không gian làm việc trống", "Steam API key": "Khóa API Steam", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Trình duyệt Steam Workshop", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "Xưởng thiết kế", "Style": "Kiểu dáng", "Style name": "Style name", "Subject": "Chủ đề", diff --git a/ryoku/i18n/catalog/zh_CN.json b/ryoku/i18n/catalog/zh_CN.json index e496a11f1f..d657e26fe6 100644 --- a/ryoku/i18n/catalog/zh_CN.json +++ b/ryoku/i18n/catalog/zh_CN.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "编辑", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "编辑 Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "编辑桌面", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "应用于副本的高斯模糊半径。越高越柔和。", "General": "常规", + "General settings": "常规设置", "General limit: %1": "常规限制: %1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "从不取得焦点", "New": "新建", "New chat": "新建聊天", - "New folder": "New folder", + "New folder": "新建文件夹", "New keyring password": "新钥匙圈密码", "New machine defaults": "新机器默认值", "New machine from ISO": "从 ISO 创建新机器", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "快速操作", "Quick Settings": "快速设置", - "Quick controls": "Quick controls", + "Quick controls": "快捷控制", "Quick place": "Quick place", "Quick settings": "快速设置", "Quicklink": "快速链接", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "重新加载", "Reload cover": "重新加载封面", + "Reload desktop": "重新加载桌面", "Reload shell": "重新加载 shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "正在重新加载桌面", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "横向移动阴影,如同侧面打光", "Shifts the shadow up or down; positive is below": "向上或向下移动阴影;正值表示下方", "Shima": "Shima", + "Shima Studio": "Shima 工作室", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "已发送艺术", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "在空工作区保持显示", "Steam API key": "Steam API 密钥", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam 创意工坊浏览器", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "工作室", "Style": "风格", "Style name": "Style name", "Subject": "主题", diff --git a/ryoku/i18n/catalog/zh_TW.json b/ryoku/i18n/catalog/zh_TW.json index 6570fb7620..1e32510546 100644 --- a/ryoku/i18n/catalog/zh_TW.json +++ b/ryoku/i18n/catalog/zh_TW.json @@ -1694,9 +1694,9 @@ "Edges": "Edges", "Edit": "編輯", "Edit ›": "Edit ›", - "Edit Shima": "Edit Shima", + "Edit Shima": "編輯 Shima", "Edit conditions": "Edit conditions", - "Edit desktop": "Edit desktop", + "Edit desktop": "編輯桌面", "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.": "Edit each interface colour on a colour wheel and check the result in a live preview. Saved palettes join the Theme list above.", "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.": "Edit it in place. Ryoku seeds it once on a fresh install and never overwrites it again.", "Edit the full colour scheme, preview the Wall interface, and save both variants.": "Edit the full colour scheme, preview the Wall interface, and save both variants.", @@ -2134,6 +2134,7 @@ "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.": "Gap between skewed slices at their vertical midpoint. 0 makes edges touch; negative values overlap.", "Gaussian blur radius applied to the copy. Higher is softer.": "套用到副本的高斯模糊半徑。數值越高越柔和。", "General": "一般", + "General settings": "一般設定", "General limit: %1": "一般限制:%1", "Generate bug report": "Generate bug report", "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.": "Generate this output and run its reload command when colours change. Keep this off after switching to managed theming.", @@ -3126,7 +3127,7 @@ "Never take focus": "不搶焦點", "New": "新增", "New chat": "新聊天", - "New folder": "New folder", + "New folder": "新增資料夾", "New keyring password": "新鑰匙圈密碼", "New machine defaults": "新機器預設值", "New machine from ISO": "從 ISO 新建機器", @@ -3933,7 +3934,7 @@ "Queued…": "Queued…", "Quick Actions": "快速操作", "Quick Settings": "快速設定", - "Quick controls": "Quick controls", + "Quick controls": "快速控制", "Quick place": "Quick place", "Quick settings": "快速設定", "Quicklink": "快捷連結", @@ -4155,6 +4156,7 @@ "Relief": "Relief", "Reload": "重新載入", "Reload cover": "重新載入封面", + "Reload desktop": "重新載入桌面", "Reload shell": "重新載入 Shell", "Reload the selected source with the current blur, dimming, and theme settings.": "Reload the selected source with the current blur, dimming, and theme settings.", "Reloading the desktop": "重新載入桌面", @@ -4805,6 +4807,7 @@ "Shifts the shadow sideways, as if lit from one side": "橫向移動陰影,如同從一側打光", "Shifts the shadow up or down; positive is below": "上下移動陰影;正值表示在下方", "Shima": "Shima", + "Shima Studio": "Shima 工作室", "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.": "Shima arranges its island, bubbles and look in its settings overlay. Dock settings stay in Stage Editor.", "Shipped art": "已載入美術資源", "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.": "Shipped by the package and overwritten on every update. Never edit these; put\nthe change where the last column says.", @@ -5012,6 +5015,7 @@ "Status changes, connections, tunnels, and remote actions recorded in this session.": "Status changes, connections, tunnels, and remote actions recorded in this session.", "Status is %1": "Status is %1", "Stay Awake": "Stay Awake", + "Stay visible on empty workspaces": "在空白工作區保持顯示", "Steam API key": "Steam API 金鑰", "Steam Workshop": "Steam Workshop", "Steam Workshop browser": "Steam 工坊瀏覽器", @@ -5058,7 +5062,7 @@ "Strip": "Strip", "Strong": "Strong", "Structure of the flowing body": "Structure of the flowing body", - "Studio": "Studio", + "Studio": "工作室", "Style": "風格", "Style name": "Style name", "Subject": "主題", diff --git a/ryoku/lockscreen/CHANGELOG.md b/ryoku/lockscreen/CHANGELOG.md index 8bad0c2ef7..b4c85f4255 100644 --- a/ryoku/lockscreen/CHANGELOG.md +++ b/ryoku/lockscreen/CHANGELOG.md @@ -20,6 +20,33 @@ - The built-in in-session clockwork lockscreen follows the global 12/24-hour preference, including AM/PM. ### Fixed +- **A saved Shima clock choice reaches the lock screen after updating.** Shima + now retires its duplicate `inir.time.format` value into the global clock + preference once, then every bar style and the lock read that one setting + (`../shell/quickshell/inir/modules/common/Config.qml`, + `tests/lock-time-format.qml`). +- **The login clock follows the global 12/24-hour preference.** The shared SDDM + host passes the preference from its installed theme config to every skin that + exposes a clock-format property, while keeping the greeter locale for + translated day and month names. The sign-in screen picks up a new choice the + next time its skin is applied or Ryoku updates + (`qylock/quickshell-lockscreen/lock_shell.qml`, `qylock/sddm-host`, + `../hub/backend/lock.go`). +- **Every selected sign-in skin has a visible pointer on hybrid graphics.** + SDDM now loads the chosen skin beneath a shared greeter host that draws the + pointer as normal scene content, avoiding Weston's missing cursor-shape + protocol and unreliable legacy cursor surface without changing the session + cursor (`qylock/sddm-host`, `install-qylock`, `../hub/backend/lock.go`, + `../cli/internal/doctor/reconcile_lockscreen.go`). +- **Every in-session lock opens with its password field ready to type.** The + lock host now focuses each mapped session-lock surface through the theme's + focus scope, so optional skins no longer need a delayed per-theme timer or a + pointer click before the first key reaches their password field + (`qylock/quickshell-lockscreen/lock_shell.qml`). +- **Orbital's rings cross the password pill again.** The moving marks and + numbers stay in front of the pill as in 1.0.3 without losing smooth motion + or alignment (`qylock/themes/clockwork/orbital/Main.qml`, + `tests/lock-date-rollover.qml`). - **Orbital follows every clock-format control and keeps its pointer aligned.** Shima, Python and the clock app now update the same preference as the Hub, while the highlighted marker follows the rendered rings instead of jumping diff --git a/ryoku/lockscreen/README.md b/ryoku/lockscreen/README.md index a9c49fb5e2..646a98620d 100644 --- a/ryoku/lockscreen/README.md +++ b/ryoku/lockscreen/README.md @@ -57,9 +57,11 @@ The skin is chosen in Ryoku Settings (**Lockscreen**), which browses the full qy catalogue live from upstream with looping previews. Selecting a skin rewrites `~/.config/qylock/theme` (the in-session lock) and reinstalls it as the SDDM greeter under `/usr/share/sddm/themes/ryoku`. The greeter half lives on a system path, so the -Hub escalates it with pkexec (`ryoku-hub lock apply-greeter`); skins not yet present -download into `~/.local/share/qylock/themes` first. Only the theme changes; the -login/auth flow is untouched. +Hub escalates it with pkexec (`ryoku-hub lock apply-greeter`) and records the global +12/24-hour preference in the installed theme config. The sign-in screen picks up a +new clock choice the next time its skin is applied or Ryoku updates. Skins not yet +present download into `~/.local/share/qylock/themes` first. The login/auth flow is +untouched. ## What owns the lock, the lid and sleep diff --git a/ryoku/lockscreen/install-qylock b/ryoku/lockscreen/install-qylock index d7cab4eaec..92015fbbb8 100755 --- a/ryoku/lockscreen/install-qylock +++ b/ryoku/lockscreen/install-qylock @@ -358,10 +358,17 @@ echo "Installing qylock clockwork/orbital greeter for $TARGET_USER" install_greeter() { # --- 1. SDDM greeter theme (system). The fixed "ryoku" name is the one the Hub's # lock picker overwrites (ryoku-hub lock apply-greeter) when the user changes skins. + # The host keeps the pointer independent of the selected skin and Weston's + # hardware-cursor path. echo " greeter theme -> /usr/share/sddm/themes/ryoku" - priv mkdir -p /usr/share/sddm/themes - priv rm -rf /usr/share/sddm/themes/ryoku - priv cp -a "$BUNDLE/themes/clockwork/orbital" /usr/share/sddm/themes/ryoku + local greeter_clock24h=false + if [[ -r $TARGET_HOME/.config/ryoku/shell.json ]] && + grep -Eq '"clock_format_24_h"[[:space:]]*:[[:space:]]*true' \ + "$TARGET_HOME/.config/ryoku/shell.json"; then + greeter_clock24h=true + fi + priv "$BUNDLE/sddm-host/install-greeter-theme" \ + "$BUNDLE/themes/clockwork/orbital" /usr/share/sddm/themes/ryoku "$greeter_clock24h" # --- 2. point SDDM at it (system) -------------------------------------------- echo " greeter selection -> /etc/sddm.conf.d/99-ryoku.conf" diff --git a/ryoku/lockscreen/qylock/quickshell-lockscreen/lock_shell.qml b/ryoku/lockscreen/qylock/quickshell-lockscreen/lock_shell.qml index d8fbf1660c..a4bd0f5bd6 100644 --- a/ryoku/lockscreen/qylock/quickshell-lockscreen/lock_shell.qml +++ b/ryoku/lockscreen/qylock/quickshell-lockscreen/lock_shell.qml @@ -34,7 +34,14 @@ ShellRoot { // Themes bind to sddm.login(), sddm.loginSucceeded, sddm.loginFailed, // sddm.hostName, sddm.fingerprintHint, etc. readonly property var sddm: sddmShim.sddm - readonly property var config: sddmShim.config + readonly property var config: { + const merged = {} + const themeConfig = sddmShim.config || {} + for (const key in themeConfig) + merged[key] = themeConfig[key] + merged.clock24h = TimeFormat.is24h ? "true" : "false" + return merged + } readonly property var userModel: sddmShim.userModel readonly property var sessionModel: sddmShim.sessionModel readonly property var keyboard: sddmShim.keyboard @@ -235,13 +242,14 @@ ShellRoot { id: themeComponent Loader { anchors.fill: parent + focus: true source: "file://" + shellRoot.themePath + "/Main.qml" onLoaded: { if (item.clock24h !== undefined) item.clock24h = Qt.binding(() => TimeFormat.is24h); if (item.clockLocale !== undefined) item.clockLocale = Qt.binding(() => TimeFormat.locale); - item.forceActiveFocus(); + Qt.callLater(() => forceActiveFocus(Qt.OtherFocusReason)); } onStatusChanged: { if (status === Loader.Error) { @@ -344,8 +352,21 @@ ShellRoot { surface: Component { WlSessionLockSurface { + id: lockSurface color: "black" + function focusTheme() { + Qt.callLater(function() { + if (lockSurface.visible && themeSurface.status === Loader.Ready) + themeSurface.forceActiveFocus(Qt.OtherFocusReason); + }); + } + + onVisibleChanged: { + if (visible) + focusTheme(); + } + // Absorb unhandled gestures (scroll, pinch) so they // don't leak through to the desktop underneath. PinchHandler { target: null } @@ -358,8 +379,11 @@ ShellRoot { } Loader { + id: themeSurface anchors.fill: parent + focus: true sourceComponent: themeComponent + onLoaded: lockSurface.focusTheme() } Loader { anchors.fill: parent diff --git a/ryoku/lockscreen/qylock/sddm-host/Main.qml b/ryoku/lockscreen/qylock/sddm-host/Main.qml new file mode 100644 index 0000000000..f6486caf3b --- /dev/null +++ b/ryoku/lockscreen/qylock/sddm-host/Main.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Window + +Item { + id: host + width: Screen.width + height: Screen.height + + readonly property bool clock24h: { + const value = typeof config !== "undefined" ? config.clock24h : undefined + if (value === undefined || value === null || String(value).length === 0) + return true + return value === true || String(value).toLowerCase() === "true" + } + + Loader { + id: theme + anchors.fill: parent + focus: true + source: "theme/Main.qml" + onLoaded: { + if (item.clock24h !== undefined) + item.clock24h = host.clock24h + if (item.clockLocale !== undefined) + item.clockLocale = Qt.locale() + Qt.callLater(() => forceActiveFocus(Qt.OtherFocusReason)) + } + } + + // Weston does not advertise the cursor-shape protocol used by Qt on the + // desktop. Keep the login pointer in the scene so every selected skin has + // one even when the legacy cursor surface cannot be scanned out. + HoverHandler { + id: pointer + cursorShape: Qt.BlankCursor + } + + Canvas { + width: 26 * scaleFactor + height: 26 * scaleFactor + z: 2000000 + visible: pointer.hovered + x: pointer.point.position.x + y: pointer.point.position.y + antialiasing: true + + readonly property real scaleFactor: Math.max(1, Screen.height / 768) + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.scale(scaleFactor, scaleFactor) + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, 16) + ctx.lineTo(4, 12.5) + ctx.lineTo(6.5, 19) + ctx.lineTo(9, 18) + ctx.lineTo(6.5, 11.5) + ctx.lineTo(11, 11.5) + ctx.closePath() + ctx.fillStyle = "#ffffff" + ctx.fill() + ctx.lineWidth = 1.2 + ctx.strokeStyle = "#000000" + ctx.stroke() + } + } +} diff --git a/ryoku/lockscreen/qylock/sddm-host/install-greeter-theme b/ryoku/lockscreen/qylock/sddm-host/install-greeter-theme new file mode 100755 index 0000000000..1f1bde5bee --- /dev/null +++ b/ryoku/lockscreen/qylock/sddm-host/install-greeter-theme @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +clock24h=${3:-} +if [[ $# -ne 3 || ($clock24h != true && $clock24h != false) ]]; then + echo "usage: install-greeter-theme " >&2 + exit 2 +fi + +src=${1%/} +dst=${2%/} +host=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +for file in "$src/Main.qml" "$host/Main.qml" "$host/metadata.desktop"; do + [[ -f $file && ! -L $file ]] || { + echo "install-greeter-theme: missing regular file: $file" >&2 + exit 1 + } +done + +parent=$(dirname "$dst") +tmp="$dst.new.$$" +cleanup() { rm -rf "$tmp"; } +trap cleanup EXIT + +mkdir -p "$parent" +rm -rf "$tmp" +mkdir -p "$tmp" +cp -a "$src" "$tmp/theme" +install -m644 "$host/Main.qml" "$tmp/Main.qml" +install -m644 "$host/metadata.desktop" "$tmp/metadata.desktop" +if [[ -f $src/theme.conf && ! -L $src/theme.conf ]]; then + cp "$src/theme.conf" "$tmp/theme.conf.source" +else + : >"$tmp/theme.conf.source" +fi +awk -v value="$clock24h" ' + BEGIN { in_general = 0; saw_general = 0; wrote_clock = 0 } + function write_clock() { + if (in_general && !wrote_clock) { + print "clock24h=" value + wrote_clock = 1 + } + } + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + write_clock() + in_general = ($0 ~ /^[[:space:]]*\[General\][[:space:]]*$/) + if (in_general) saw_general = 1 + print + next + } + in_general && /^[[:space:]]*clock24h[[:space:]]*=/ { + if (!wrote_clock) { + print "clock24h=" value + wrote_clock = 1 + } + next + } + { print } + END { + write_clock() + if (!saw_general) { + if (NR > 0) print "" + print "[General]" + print "clock24h=" value + } + } +' "$tmp/theme.conf.source" >"$tmp/theme.conf" +rm "$tmp/theme.conf.source" +chmod -R a+rX "$tmp" +if (( EUID == 0 )); then + chown -R root:root "$tmp" +fi +rm -rf "$dst" +mv "$tmp" "$dst" +trap - EXIT diff --git a/ryoku/lockscreen/qylock/sddm-host/metadata.desktop b/ryoku/lockscreen/qylock/sddm-host/metadata.desktop new file mode 100644 index 0000000000..3294001a5f --- /dev/null +++ b/ryoku/lockscreen/qylock/sddm-host/metadata.desktop @@ -0,0 +1,8 @@ +[SddmGreeterTheme] +Name=Ryoku lockscreen host +Description=Ryoku lockscreen theme host +Author=Ryoku contributors +Type=sddm-theme +MainScript=Main.qml +ConfigFile=theme.conf +QtVersion=6 diff --git a/ryoku/lockscreen/qylock/themes/clockwork/orbital/Main.qml b/ryoku/lockscreen/qylock/themes/clockwork/orbital/Main.qml index 33e02003ab..0b42f2d9fe 100644 --- a/ryoku/lockscreen/qylock/themes/clockwork/orbital/Main.qml +++ b/ryoku/lockscreen/qylock/themes/clockwork/orbital/Main.qml @@ -20,43 +20,6 @@ Rectangle { property bool isQuickshell: typeof sddm === "undefined" || sddm.hostName === undefined readonly property bool isSessionLock: (typeof sddm !== "undefined") && sddm.fingerprintHint === true - readonly property bool softwareCursorEnabled: !isQuickshell && !isSessionLock - - // --- Software cursor for the SDDM login screen -------------------------- - // hostName exists in the lock shim too, so fingerprintHint distinguishes it from SDDM. - HoverHandler { - id: swPointer - enabled: root.softwareCursorEnabled - } - Canvas { - id: swCursor - width: 26 * root.s - height: 26 * root.s - z: 2000000 - visible: root.softwareCursorEnabled && swPointer.hovered - x: swPointer.point.position.x - y: swPointer.point.position.y - antialiasing: true - onPaint: { - var ctx = getContext("2d"); - ctx.reset(); - ctx.scale(root.s, root.s); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(0, 16); - ctx.lineTo(4, 12.5); - ctx.lineTo(6.5, 19); - ctx.lineTo(9, 18); - ctx.lineTo(6.5, 11.5); - ctx.lineTo(11, 11.5); - ctx.closePath(); - ctx.fillStyle = "#ffffff"; - ctx.fill(); - ctx.lineWidth = 1.2; - ctx.strokeStyle = "#000000"; - ctx.stroke(); - } - } // Ring transforms interpolate between one-second wall-clock samples. readonly property bool clockAwake: root.isSessionLock || Window.active @@ -239,7 +202,7 @@ Rectangle { readonly property real secR: 480 * s Rectangle { - id: indicatorPill; z: 1; x: clockContainer.cx + 230 * s; anchors.verticalCenter: parent.verticalCenter + id: indicatorPill; objectName: "indicatorPill"; z: 1; x: clockContainer.cx + 230 * s; anchors.verticalCenter: parent.verticalCenter width: 330 * s; height: 90 * s; radius: 45 * s; color: root.pillColor; border.color: root.pillBorder; border.width: 1 * s Rectangle { x: 170 * s; anchors.verticalCenter: parent.verticalCenter; width: 1 * s; height: 35 * s; color: root.pillInnerLine } } @@ -258,6 +221,7 @@ Rectangle { Item { id: minuteRing + objectName: "minuteRing"; z: 10 x: clockContainer.cx - clockContainer.minR y: clockContainer.cy - clockContainer.minR width: clockContainer.minR * 2 @@ -306,6 +270,7 @@ Rectangle { Item { id: secondRing + objectName: "secondRing"; z: 10 x: clockContainer.cx - clockContainer.secR y: clockContainer.cy - clockContainer.secR width: clockContainer.secR * 2 diff --git a/ryoku/rashin/backend/acp.go b/ryoku/rashin/backend/acp.go index f6cc47c332..28140fe959 100644 --- a/ryoku/rashin/backend/acp.go +++ b/ryoku/rashin/backend/acp.go @@ -706,28 +706,63 @@ func (c *acpConn) LoadSession(id string) error { return err } -// ListSessions fetches stored session metadata over ACP. +// ListSessions fetches stored session metadata over ACP. Hermes's state +// database is also merged in because its workspace column is authoritative: +// some valid rows expose that cwd to the CLI but not through session/list. func (c *acpConn) ListSessions() []SessionMeta { - res, err := c.request("session/list", map[string]any{}) - if err != nil { - return nil + var list []SessionMeta + if res, err := c.request("session/list", map[string]any{}); err == nil { + var out struct { + Sessions []struct { + SessionID string `json:"sessionId"` + Title string `json:"title"` + Cwd string `json:"cwd"` + UpdatedAt string `json:"updatedAt"` + } `json:"sessions"` + } + if json.Unmarshal(res, &out) == nil { + list = make([]SessionMeta, 0, len(out.Sessions)) + for _, s := range out.Sessions { + list = append(list, SessionMeta{ID: s.SessionID, Title: cleanTitle(s.Title), Cwd: s.Cwd, UpdatedAt: s.UpdatedAt}) + } + } } - var out struct { - Sessions []struct { - SessionID string `json:"sessionId"` - Title string `json:"title"` - Cwd string `json:"cwd"` - UpdatedAt string `json:"updatedAt"` - } `json:"sessions"` + if c.backend() == "hermes" { + list = mergeHermesSessions(list, hermesHistorySessions(1000)) } - if json.Unmarshal(res, &out) != nil { - return nil + return list +} + +func mergeHermesSessions(agent, stored []SessionMeta) []SessionMeta { + if len(stored) == 0 { + return agent } - list := make([]SessionMeta, 0, len(out.Sessions)) - for _, s := range out.Sessions { - list = append(list, SessionMeta{ID: s.SessionID, Title: cleanTitle(s.Title), Cwd: s.Cwd, UpdatedAt: s.UpdatedAt}) + byID := make(map[string]SessionMeta, len(agent)) + for _, s := range agent { + byID[s.ID] = s } - return list + out := make([]SessionMeta, 0, len(stored)+len(agent)) + for _, s := range stored { + if live, ok := byID[s.ID]; ok { + if s.Title == "" { + s.Title = live.Title + } + if s.Cwd == "" { + s.Cwd = live.Cwd + } + if s.UpdatedAt == "" { + s.UpdatedAt = live.UpdatedAt + } + delete(byID, s.ID) + } + out = append(out, s) + } + for _, s := range agent { + if _, ok := byID[s.ID]; ok { + out = append(out, s) + } + } + return out } // cleanTitle drops notes that only the agent saw from a generated session diff --git a/ryoku/rashin/backend/acp_test.go b/ryoku/rashin/backend/acp_test.go index 25a5717e46..afd1668590 100644 --- a/ryoku/rashin/backend/acp_test.go +++ b/ryoku/rashin/backend/acp_test.go @@ -7,6 +7,7 @@ import ( "errors" "io" "reflect" + "sync" "testing" "time" ) @@ -272,6 +273,33 @@ func TestACPDeadChildEmitsDeadState(t *testing.T) { } } +// A prompt still running when the agent exits must not crash the daemon (#595). +func TestACPEmitAfterAgentExitDoesNotPanic(t *testing.T) { + for range 100 { + cr, cw := io.Pipe() + ar, aw := io.Pipe() + go func() { _, _ = io.Copy(io.Discard, cr) }() + conn := newACPConn(cw, ar, cw) + var wg sync.WaitGroup + for range 4 { + wg.Add(1) + go func() { + defer wg.Done() + for range 100 { + conn.emit(AcpEvent{Type: "turn_end"}) + } + }() + } + _ = aw.Close() + go func() { + for range conn.Events() { + } + }() + wg.Wait() + conn.Close() + } +} + func TestACPV2ImagesModelsSessions(t *testing.T) { conn, fa := newTestPair(t) defer conn.Close() @@ -385,6 +413,27 @@ func TestACPV2ImagesModelsSessions(t *testing.T) { expectEvent(t, conn.Events(), "replay_end") } +func TestMergeHermesSessionsUsesStoredWorkspace(t *testing.T) { + agent := []SessionMeta{ + {ID: "agent", Title: "Agent", Cwd: ".", UpdatedAt: "old"}, + {ID: "live", Title: "Live", Cwd: "/tmp/live"}, + } + stored := []SessionMeta{ + {ID: "agent", Title: "Agent", Cwd: "/tmp/rashin", UpdatedAt: "new"}, + {ID: "chat", Title: "Chat", Cwd: "/tmp/rashin-chat"}, + } + got := mergeHermesSessions(agent, stored) + if len(got) != 3 { + t.Fatalf("merged sessions = %+v", got) + } + if got[0].ID != "agent" || got[0].Cwd != "/tmp/rashin" || got[0].UpdatedAt != "new" { + t.Fatalf("stored workspace did not replace ACP placeholder: %+v", got[0]) + } + if got[1].ID != "chat" || got[2].ID != "live" { + t.Fatalf("stored and live-only sessions were not retained: %+v", got) + } +} + func stubChatProwlProfileSets(t *testing.T, lookup func(context.Context) (string, []string, error)) { t.Helper() previous := chatProwlProfileSets diff --git a/ryoku/rashin/backend/introspect.go b/ryoku/rashin/backend/introspect.go index de8cc73c33..567af19308 100644 --- a/ryoku/rashin/backend/introspect.go +++ b/ryoku/rashin/backend/introspect.go @@ -514,15 +514,49 @@ func buildHeatmap(sessions []SessionEntry) []HeatmapDay { return out } -// hermesSessions reads session metadata from ~/.hermes/state.db without -// disturbing the live daemon's WAL handle. -func hermesSessions(limit int) []SessionEntry { - entries, _ := hermesSessionReport(limit) - return entries +// hermesSessionHome mirrors Hermes's profile selection for the session store. +// A concrete profile HERMES_HOME wins; a root follows its sticky active profile. +func hermesSessionHome() string { + root := strings.TrimSpace(os.Getenv("HERMES_HOME")) + if root == "" { + root = filepath.Join(home(), ".hermes") + } else if strings.HasPrefix(root, "~/") { + root = filepath.Join(home(), root[2:]) + } + root = filepath.Clean(root) + if filepath.Base(filepath.Dir(root)) == "profiles" { + return root + } + active, err := os.ReadFile(filepath.Join(root, "active_profile")) + if err != nil { + return root + } + name := strings.TrimSpace(string(active)) + if name == "" || name == "default" || !validHermesProfileName(name) { + return root + } + profile := filepath.Join(root, "profiles", name) + if dirExists(profile) { + return profile + } + return root } -func hermesSessionReport(limit int) ([]SessionEntry, string) { - db := filepath.Join(home(), ".hermes", "state.db") +func validHermesProfileName(name string) bool { + if len(name) == 0 || len(name) > 64 { + return false + } + for i, r := range name { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || (i > 0 && (r == '_' || r == '-')) { + continue + } + return false + } + return true +} + +func hermesStateQuery(query string) ([]byte, string) { + db := filepath.Join(hermesSessionHome(), "state.db") if !fileExists(db) { return nil, "" } @@ -532,11 +566,61 @@ func hermesSessionReport(limit int) ([]SessionEntry, string) { } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - q := "SELECT id, COALESCE(title,'') AS title, COALESCE(model,'') AS model, message_count, started_at, source FROM sessions WHERE archived IS NOT 1 ORDER BY started_at DESC LIMIT " + itoa(limit) - out, err := exec.CommandContext(ctx, bin, "-readonly", "-json", "file:"+db+"?mode=ro", q).Output() + out, err := exec.CommandContext(ctx, bin, "-readonly", "-json", "file:"+db+"?mode=ro", query).Output() if err != nil || len(out) == 0 { return nil, "" } + return out, "" +} + +// hermesHistorySessions reads the same ACP rows that can be reopened, using +// the dedicated cwd column preferred by current Hermes and the legacy +// model_config value as a fallback. +func hermesHistorySessions(limit int) []SessionMeta { + if limit <= 0 { + return nil + } + q := "SELECT id, COALESCE(title,'') AS title, " + + "COALESCE(NULLIF(cwd,''), CASE WHEN json_valid(model_config) THEN json_extract(model_config,'$.cwd') END, '') AS cwd, " + + "COALESCE(last_activity_at, ended_at, started_at) AS updated_at " + + "FROM sessions WHERE source = 'acp' AND archived IS NOT 1 AND message_count > 0 " + + "ORDER BY updated_at DESC LIMIT " + itoa(limit) + out, _ := hermesStateQuery(q) + var rows []struct { + ID string `json:"id"` + Title string `json:"title"` + Cwd string `json:"cwd"` + UpdatedAt float64 `json:"updated_at"` + } + if json.Unmarshal(out, &rows) != nil { + return nil + } + sessions := make([]SessionMeta, 0, len(rows)) + for _, r := range rows { + updated := "" + if r.UpdatedAt > 0 { + updated = time.Unix(int64(r.UpdatedAt), 0).Format(time.RFC3339) + } + sessions = append(sessions, SessionMeta{ + ID: r.ID, Title: cleanTitle(r.Title), Cwd: r.Cwd, UpdatedAt: updated, + }) + } + return sessions +} + +// hermesSessions reads session metadata from Hermes's active state.db without +// disturbing its WAL handle. +func hermesSessions(limit int) []SessionEntry { + entries, _ := hermesSessionReport(limit) + return entries +} + +func hermesSessionReport(limit int) ([]SessionEntry, string) { + q := "SELECT id, COALESCE(title,'') AS title, COALESCE(model,'') AS model, message_count, started_at, source FROM sessions WHERE archived IS NOT 1 ORDER BY started_at DESC LIMIT " + itoa(limit) + out, note := hermesStateQuery(q) + if note != "" || len(out) == 0 { + return nil, note + } var rows []struct { ID string `json:"id"` Title string `json:"title"` diff --git a/ryoku/shell/CHANGELOG.md b/ryoku/shell/CHANGELOG.md index 2e30c3a293..4f0ed571ae 100644 --- a/ryoku/shell/CHANGELOG.md +++ b/ryoku/shell/CHANGELOG.md @@ -1,6 +1,118 @@ # Changelog: ryoku/shell/ ## Unreleased +- **Shima and Python dock icons stay visible and clickable with the Shima bar.** + Their icon surfaces now stay above Shima's full-output chassis, and Python + reads each pinned app's bound entry before launching through the shared service + (`quickshell/{inir/modules/iris/dock,shell/modules/bar/barstyles/python/dock}/`). +- **The system monitor stops warning every second on machines without a fan + sensor or GPU power reading.** It opened the missing sensor paths anyway on + each slow sample (`plugin/systemmonitor.cpp`). Contributed by SourceAura. +- **Voice typing no longer leaves status followers inside the shell daemon.** + Ending dictation now kills and reaps its Voxtype status stream, consecutive + sessions cannot overlap, and holding the niri shortcut does not repeat the + toggle (`ipc/{daemon,voicewatch}.go`, `wm/niri/config_binds.go`). +- **The wallpaper picker no longer misses Super+W behind a busy event queue.** + Picker controls displace an older progress event when a slow subscriber has + filled its buffer (`ryogami/daemon/topics.go`, `daemon.go`). +- **Login opens straight into a ready desktop.** The boot cover now maps closed + behind the greeter curtain instead of replaying a shell-reload close over the + wallpaper while the shell starts (`quickshell/reload-cover/shell.qml`). +- **Super+Escape controls reopen without rebuilding the whole sidebar.** The + hidden panel stays warm between ordinary toggles while its probes and surfaces + remain inactive (`quickshell/shell/shell.qml`). +- **The ii dock paints immediately and auto-hide works above open windows.** + Island backgrounds no longer wait for an app-list refresh after shell startup, + and the edge reveal strip stays reachable over tiled, floating and maximised + windows (`quickshell/shell/modules/dock/ii/`). +- **The launcher's Audio Visualizer stays behind apps and the dock.** The Media + action now uses the ordinary desktop mode; the separately named overlay + shortcut still raises it over windows on demand + (`quickshell/shell/modules/launcher/shared/providers/actions/catalog.js`). +- **Music visualizers follow routed playback and react at normal volume.** The + shared feeds now capture the routed sink's monitor, adapt to quiet playback, + and apply gain consistently to every look (`plugin/cavafeed.cpp`, + `quickshell/shell/{services/AudioBars.qml,modules/visualizer/}`). +- **Added visualizers keep their own look and selection.** Every instance now + has a stable saved identity, so adding or removing another no longer reuses + its number, retargets the editor, or hides it behind the wrong view + (`quickshell/shell/modules/{desktop,stage,visualizer}/`). +- **Ryoshot sends annotated captures to a chosen KDE Connect device.** The + transfer uses private temporary files, reports failures, and cleans each file + after KDE Connect has accepted it. Contributed by Ronin-CK + (`quickshell/ryoshot/`). +- **Updates no longer make open Ryoku surfaces reload a half-laid QML tree.** + Packaged shell tools stay on their loaded generation until their normal restart, + while checkout configs still hot-reload source edits (`quickshell/*/shell.qml`). +- **Desktop application shortcuts can be added from the right-click menu.** + The existing installed-app picker places them on the clicked display, icon + settings choose single- or double-click opening, and Stage menus follow the + shared Ryoku language (`quickshell/{shell/modules/desktop,stage/}`). +- **Creating desktop folders and application shortcuts returns input to the + desktop.** A selected icon now holds keyboard focus only while the pointer + remains over a shortcut and releases it before another app is clicked; folder + naming keeps its field focused, and the application search releases focus + when its picker closes + (`quickshell/{shell/modules/desktop,stage/modules/ii/background/shortcuts/}`). +- **Japanese glosses can be hidden across the desktop.** The shared setting now + updates shell cards, menus, bar and launcher marks, wallpaper typography and + Ryogami live, while the default remains the existing Japanese treatment + (`quickshell/`, `ryogami/picker/qml/`, `../ui/`). +- **Python bar notification and visualizer controls now share Ryoku settings.** + Do Not Disturb and notification sounds follow the shell, including custom + clips, volume and critical cues; visualizers follow the active colour and gain + (`quickshell/shell/modules/bar/barstyles/python/`). +- **Bluetooth toggles can recover a radio that an older build left soft-blocked,** + even while BlueZ reports no adapter. +- **Desktop widgets can be added on a second display from the Stage Editor.** + Switching displays keeps the editor connected to that display's widgets, + without changing the first display (`quickshell/stage/GlobalStates.qml`, + `quickshell/shell/modules/stage/StageEditorHost.qml`). +- **Dock window and music previews stay open while the pointer crosses from the + dock into their controls.** The first trip gets a longer grace period; after + the card is reached, leaving still closes it promptly + (`quickshell/shell/modules/bar/popouts/`). +- **Desktop session confirmation matches Ryoku's paper-and-ink menu.** Power off, + restart and log out now show a theme-aware action card with keyboard cancellation + and confirmation on every bar style (`quickshell/shell/modules/confirm/RyokuConfirmationDialog.qml`). +- **Desktop frost blurs the wallpaper and widgets with a visualizer on.** + The plain visualizer stays sharp above the glass in the same desktop surface + (`quickshell/shell/modules/desktop/Desktop.qml`). +- **Game Mode reports a failed compositor strip instead of showing On.** It + persists the request before the strip, and a failed restore no longer skips + the power, tune or Wi-Fi cleanup (`scripts/ryoku-cmd-game-mode`). +- **The Ryoku dock can stay visible on empty workspaces.** Set this in Stage + Editor to keep an auto-hiding dock up on its own empty workspace, even when + another display has a focused window (`quickshell/shell/modules/dock/DockSurface.qml`, + `quickshell/stage/modules/ii/editMode/EditDockAppearancePage.qml`). +- **The bar shows ethernet on machines with two network ports.** The daemon + read only the first device of each type, so an unplugged port listed ahead + of the connected one reported the link as down; a second Wi-Fi adapter had + the same problem. It now reports the connected device, and with nothing up + reaches a usable adapter first (`ipc/network.go`). +- **Microphone level popups no longer follow app gain changes.** Shell controls + and explicit requests still show them; microphone mute and speaker keys and + knobs still show (`quickshell/shell/services/OsdFeed.qml`, + `quickshell/{shell/modules,inir/}`). +- **Controls can scroll to the bottom on shorter displays.** The overview stays + inside its panel while the header remains in place (`quickshell/shell/modules/sidebar/ControlsBoard.qml`). +- **Desktop visualizers reappear when added after removal.** The plain look + paints with the wallpaper instead of relying on a separate layer, while + the audio feed survives its surface being rebuilt + (`quickshell/shell/{shell.qml,modules/desktop/Desktop.qml,modules/visualizer/}`). +- **Shima's desktop visualizer stays inside its card.** Its bars now follow + either the Shima plate or the Ryoku host's curved edges, even at small sizes + and loud playback (`quickshell/inir/modules/background/widgets/visualizer/VisualizerWidget.qml`, + `quickshell/shell/modules/desktop/iris/IrisCanvasProvider.qml`). +- **Pinned dock apps open again and the dock sits behind normal windows.** Running + marks update as soon as a window opens, and Stage still shows the dock while editing + (`quickshell/shell/modules/dock/`, `quickshell/shell/services/{AppLaunch,Dock}.qml`). +- **Music-reactive bars and visualizers hear music played through Easy Effects.** They + listen to the output the music actually plays into instead of only the default one + (`quickshell/shell/services/Audio.qml`). +- **Music-reactive bars and visualizers always run in Performance mode.** They no + longer pause when Ryoku misses the playing app's audio stream + (`quickshell/shell/services/Perf.qml`). - **Settings save again when notifications sit bottom right.** That position was refused, which blocked every other change; notifications now follow the chosen position (`ipc/settings.go`, `modules/notifications/NotificationPopups.qml`). @@ -13,6 +125,10 @@ - Clipboard history and Starred can be navigated and acted on from the keyboard. Tab switches panes; arrows, Page Up/Down, Home/End and Ctrl+J/K or Ctrl+N/P move the selection; Enter copies, Ctrl/Alt+S toggles a star, and Delete removes an item. ### Fixed +- **Image notifications keep their full body, actions and bottom edge visible.** + A thumbnail that finishes loading can no longer leave the popup surface at + the card's shorter pre-image height + (`quickshell/shell/modules/notifications/{NotificationCard,NotificationPopups}.qml`). - **Every spectrum visualizer reacts at ordinary playback levels.** The shared native feed's silence state now wakes desktop looks instead of an unreachable average-energy threshold (`quickshell/shell/modules/visualizer/{Motion,AuraMotion}.qml`). diff --git a/ryoku/shell/ipc/actions.go b/ryoku/shell/ipc/actions.go index e053eae291..d279b933b0 100644 --- a/ryoku/shell/ipc/actions.go +++ b/ryoku/shell/ipc/actions.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "sync" @@ -250,8 +251,15 @@ func qylockProcessPIDs(session, token string) []int { if session == "" { return nil } + pattern := lockClientPattern + if token == "" { + pattern = lockClientOrWrapperPattern + if script := os.Getenv("RYOKU_QYLOCK_LOCK_SCRIPT"); script != "" { + pattern += `|(^| )` + regexp.QuoteMeta(script) + `( |$)` + } + } var matches []int - for _, value := range currentUserProcessIDs(lockClientPattern) { + for _, value := range currentUserProcessIDs(pattern) { env, err := os.ReadFile(filepath.Join("/proc", value, "environ")) if err != nil || processEnvironmentValue(env, "XDG_SESSION_ID") != session { continue @@ -315,6 +323,7 @@ var ( // validation also checks the selected session and generation token through // /proc. const lockClientPattern = "quickshell.*(quickshell-lockscreen|qylock-next/lockscreen).*/lock_shell[.]qml" +const lockClientOrWrapperPattern = lockClientPattern + `|(^| )([^ ]*/)?(quickshell-lockscreen|qylock-next/lockscreen)/lock[.]sh( |$)|(^| )([^ ]*/)?ryoku-qylock-lock( |$)` // lockSession locks the screen with qylock, the in-session lock Ryoku ships. // The shell has no lock surface of its own. diff --git a/ryoku/shell/ipc/bar.go b/ryoku/shell/ipc/bar.go index 6b29576341..71c8276e65 100644 --- a/ryoku/shell/ipc/bar.go +++ b/ryoku/shell/ipc/bar.go @@ -1211,7 +1211,7 @@ func (d *daemon) barSettings(args []string) string { func defaultDock() map[string]any { return map[string]any{ "enabled": false, "edge": "auto", "edgeGap": dockEdgeGapDefault, - "autohide": false, "pinned": []string{}, + "autohide": false, "revealOnEmpty": true, "pinned": []string{}, "magnify": true, "frost": true, "shadow": true, "labels": true, "media": false, } } diff --git a/ryoku/shell/ipc/bar_test.go b/ryoku/shell/ipc/bar_test.go index 28ea97a78f..9fcbe3e8ad 100644 --- a/ryoku/shell/ipc/bar_test.go +++ b/ryoku/shell/ipc/bar_test.go @@ -471,7 +471,7 @@ func TestDockVerbsThroughStore(t *testing.T) { // The shipped fields the CLI never sets survive the write. dock, _ := d.frame(t)["dock"].(map[string]any) - if dock["magnify"] != true || dock["frost"] != true { + if dock["magnify"] != true || dock["frost"] != true || dock["revealOnEmpty"] != true { t.Fatalf("dock write dropped untouched fields: %+v", dock) } diff --git a/ryoku/shell/ipc/daemon.go b/ryoku/shell/ipc/daemon.go index 7de9da5734..e1444fdafc 100644 --- a/ryoku/shell/ipc/daemon.go +++ b/ryoku/shell/ipc/daemon.go @@ -131,6 +131,7 @@ type daemon struct { failMu sync.Mutex lastFail map[string]string // component -> last line it died with voiceMu sync.Mutex // serializes voice (Super+`) toggles + voiceWatchMu sync.Mutex // allows only one live voxtype follow stream voiceOn bool // dictation active; guarded by voiceMu voiceStop chan struct{} // reaps the live voxtype state stream; nil when none prompter *prompter // GNOME keyring system prompter (nil when unavailable) @@ -1758,12 +1759,19 @@ func (d *daemon) voice() string { defer d.voiceMu.Unlock() if !dictationReady() { d.voiceOn = false + if d.voiceStop != nil { + close(d.voiceStop) + d.voiceStop = nil + } d.ensure("shell") return shellIpc("openSurface", d.activeMonitor(), "voice-off") } d.voiceOn = !d.voiceOn if d.voiceOn { d.ensure("shell") + if d.voiceStop != nil { + close(d.voiceStop) + } stop := make(chan struct{}) d.voiceStop = stop go d.watchVoice(stop) diff --git a/ryoku/shell/ipc/lock_test.go b/ryoku/shell/ipc/lock_test.go index 2a557bd9bc..e2dbeed1e5 100644 --- a/ryoku/shell/ipc/lock_test.go +++ b/ryoku/shell/ipc/lock_test.go @@ -82,6 +82,32 @@ func TestLockerProcessLookupIgnoresAnotherUsersLock(t *testing.T) { t.Fatalf("current user's qylock matches = %v, want [102]", got) } } +func TestLockerProcessLookupIncludesSessionWrapper(t *testing.T) { + root := t.TempDir() + for pid, command := range map[string]string{ + "101": "bash\x00/home/user/.local/share/quickshell-lockscreen/lock.sh\x00", + "102": "bash\x00/home/user/.local/share/ryoku/qylock-next/lockscreen/lock.sh\x00", + "103": "bash\x00/usr/bin/ryoku-qylock-lock\x00", + } { + dir := filepath.Join(root, pid) + if err := os.Mkdir(dir, 0o755); err != nil { + t.Fatal(err) + } + status := "Uid:\t0\t" + strconv.Itoa(os.Getuid()) + "\t0\t0\n" + if err := os.WriteFile(filepath.Join(dir, "status"), []byte(status), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "cmdline"), []byte(command), 0o600); err != nil { + t.Fatal(err) + } + } + if got := userProcessIDsUnder(root, os.Getuid(), lockClientOrWrapperPattern); len(got) != 3 { + t.Fatalf("session wrappers found = %v, want all three", got) + } + if got := userProcessIDsUnder(root, os.Getuid(), lockClientPattern); len(got) != 0 { + t.Fatalf("wrapper was accepted as secure client: %v", got) + } +} func TestLockerProcessLookupFindsRetainedStageClient(t *testing.T) { cmd := exec.Command( @@ -402,6 +428,23 @@ func TestLockSessionClearsStaleMarkerAndReportsTimeout(t *testing.T) { waitFor(t, spawned) } +func TestLockRetryDoesNotStartAnotherLiveClient(t *testing.T) { + t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) + starts := 0 + setLockSeams(t, func() bool { return starts > 0 }, func(int) error { + starts++ + return nil + }) + for range 2 { + if err := ensureSessionLocked(time.Now().Add(20 * time.Millisecond)); err == nil { + t.Fatal("unconfirmed lock was accepted") + } + } + if starts != 1 { + t.Fatalf("live unconfirmed client started %d times, want one", starts) + } +} + // waitFor polls briefly for a file the fake locker writes asynchronously. func waitFor(t *testing.T, path string) { t.Helper() diff --git a/ryoku/shell/ipc/network.go b/ryoku/shell/ipc/network.go index be9fc0cb88..a70df061c4 100644 --- a/ryoku/shell/ipc/network.go +++ b/ryoku/shell/ipc/network.go @@ -418,19 +418,55 @@ func (n *networkState) snapshotState() *networkSnapshot { } } -// deviceOfType returns the path of the first device of the given NMDeviceType, -// or "" if none. Service order (GetAllDevices) is preserved; no sort. +// deviceOfType returns the preferred device of the given NMDeviceType, or "" +// if none. Taking the first in service order let an unplugged port or idle +// adapter listed ahead of the live one hide it. func (n *networkState) deviceOfType(devType uint32) dbus.ObjectPath { var paths []dbus.ObjectPath if n.obj(nmPath).Call(nmIface+".GetAllDevices", 0).Store(&paths) != nil { return "" } + var devs []nmDevice for _, p := range paths { if n.uintProp(n.obj(p), nmDeviceIface+".DeviceType") == devType { - return p + devs = append(devs, nmDevice{path: p, state: n.uintProp(n.obj(p), nmDeviceIface+".State")}) } } - return "" + return preferredDevice(devs) +} + +// nmDevice is one NetworkManager device and its NMDeviceState. +type nmDevice struct { + path dbus.ObjectPath + state uint32 +} + +// preferredDevice picks the highest-ranked device, the first in order on a +// tie, or "" for an empty list. +func preferredDevice(devs []nmDevice) dbus.ObjectPath { + best, bestRank := dbus.ObjectPath(""), -1 + for _, d := range devs { + if r := deviceRank(d.state); r > bestRank { + best, bestRank = d.path, r + } + } + return best +} + +// deviceRank orders NMDeviceStates: Connected, then Connecting, then +// disconnected but usable (30), so a Wi-Fi scan or connect with nothing up +// reaches a working adapter rather than an unavailable, unmanaged or failed one. +func deviceRank(state uint32) int { + switch { + case connectivityFromState(state) == "Connected": + return 3 + case connectivityFromState(state) == "Connecting": + return 2 + case state == 30: + return 1 + default: + return 0 + } } // apPaths lists the access points the wifi device currently sees, in service diff --git a/ryoku/shell/ipc/network_test.go b/ryoku/shell/ipc/network_test.go index 4c52ae689b..a3f4bf9f71 100644 --- a/ryoku/shell/ipc/network_test.go +++ b/ryoku/shell/ipc/network_test.go @@ -63,6 +63,33 @@ func TestConnectivityFromState(t *testing.T) { } } +// preferredDevice reports the live link on a multi-device machine: an +// unplugged port or idle adapter listed first must not mask the connected +// one, a usable adapter beats an unusable one, and ties keep order. +func TestPreferredDevice(t *testing.T) { + cases := []struct { + name string + devs []nmDevice + want dbus.ObjectPath + }{ + {"none", nil, ""}, + {"single unavailable", []nmDevice{{"/d/2", 20}}, "/d/2"}, + {"unplugged listed first", []nmDevice{{"/d/2", 20}, {"/d/3", 100}}, "/d/3"}, + {"unmanaged listed first", []nmDevice{{"/d/2", 10}, {"/d/3", 100}}, "/d/3"}, + {"connecting beats disconnected", []nmDevice{{"/d/2", 30}, {"/d/3", 70}}, "/d/3"}, + {"connected beats connecting", []nmDevice{{"/d/2", 70}, {"/d/3", 100}}, "/d/3"}, + {"tie keeps service order", []nmDevice{{"/d/2", 100}, {"/d/3", 100}}, "/d/2"}, + {"usable beats unavailable", []nmDevice{{"/d/2", 20}, {"/d/3", 30}}, "/d/3"}, + {"usable beats failed", []nmDevice{{"/d/2", 120}, {"/d/3", 30}}, "/d/3"}, + {"all unusable keeps first", []nmDevice{{"/d/2", 20}, {"/d/3", 10}}, "/d/2"}, + } + for _, c := range cases { + if got := preferredDevice(c.devs); got != c.want { + t.Errorf("%s: preferredDevice = %q, want %q", c.name, got, c.want) + } + } +} + // wifiConnectSettings omits the security block for an open network, derives // key-mgmt from the resolved AP's security (sae for WPA3, wpa-psk otherwise), // pins the band only when the caller supplied a BSSID and the AP is known, and diff --git a/ryoku/shell/ipc/settings.go b/ryoku/shell/ipc/settings.go index c2ec43b0b7..93fe856e2f 100644 --- a/ryoku/shell/ipc/settings.go +++ b/ryoku/shell/ipc/settings.go @@ -42,19 +42,20 @@ import ( // A patch whose first path segment is one of these is validated; anything else is // passthrough. var contractKeys = map[string]bool{ - "deckFrostEnabled": true, - "deckFrost": true, - "general": true, - "theme": true, - "bars": true, - "menus": true, - "notifications": true, - "wallpaper": true, - "ask": true, - "phone": true, - "controls": true, - "batteryAlerts": true, - "sounds": true, + "deckFrostEnabled": true, + "deckFrost": true, + "showJapaneseGloss": true, + "general": true, + "theme": true, + "bars": true, + "menus": true, + "notifications": true, + "wallpaper": true, + "ask": true, + "phone": true, + "controls": true, + "batteryAlerts": true, + "sounds": true, } const ( @@ -123,19 +124,20 @@ var ( // icon-theme group, custom-CSS file, and app-launcher menu that Ryoku does not // consume. Ask and Controls are Ryoku's typed shell state. type settings struct { - DeckFrostEnabled bool `json:"deckFrostEnabled"` - DeckFrost float64 `json:"deckFrost"` - General generalSettings `json:"general"` - Theme themeSettings `json:"theme"` - Bars barsSettings `json:"bars"` - Menus menusSettings `json:"menus"` - Notifications notificationsSettings `json:"notifications"` - Wallpaper wallpaperSettings `json:"wallpaper"` - Ask askSettings `json:"ask"` - Phone phoneSettings `json:"phone"` - Controls controlsSettings `json:"controls"` - BatteryAlerts batteryAlertsSettings `json:"batteryAlerts"` - Sounds soundsSettings `json:"sounds"` + DeckFrostEnabled bool `json:"deckFrostEnabled"` + DeckFrost float64 `json:"deckFrost"` + ShowJapaneseGloss bool `json:"showJapaneseGloss"` + General generalSettings `json:"general"` + Theme themeSettings `json:"theme"` + Bars barsSettings `json:"bars"` + Menus menusSettings `json:"menus"` + Notifications notificationsSettings `json:"notifications"` + Wallpaper wallpaperSettings `json:"wallpaper"` + Ask askSettings `json:"ask"` + Phone phoneSettings `json:"phone"` + Controls controlsSettings `json:"controls"` + BatteryAlerts batteryAlertsSettings `json:"batteryAlerts"` + Sounds soundsSettings `json:"sounds"` } type generalSettings struct { @@ -398,8 +400,9 @@ func defaultSettings() *settings { {ID: "bottomControls", Visible: true}, } return &settings{ - DeckFrostEnabled: true, - DeckFrost: 0.45, + DeckFrostEnabled: true, + DeckFrost: 0.45, + ShowJapaneseGloss: true, General: generalSettings{ ClockFormat24H: false, WeatherLocationQuery: locationQuery{Type: "Coordinates", Lat: 0, Lon: 0, Name: "", Country: ""}, @@ -975,19 +978,20 @@ func resolveThemePalette(full map[string]any, themeName string) { func buildSettings(raw map[string]any, strict bool) (*settings, error) { s := defaultSettings() dst := map[string]any{ - "deckFrostEnabled": &s.DeckFrostEnabled, - "deckFrost": &s.DeckFrost, - "general": &s.General, - "theme": &s.Theme, - "bars": &s.Bars, - "menus": &s.Menus, - "notifications": &s.Notifications, - "wallpaper": &s.Wallpaper, - "ask": &s.Ask, - "phone": &s.Phone, - "controls": &s.Controls, - "batteryAlerts": &s.BatteryAlerts, - "sounds": &s.Sounds, + "deckFrostEnabled": &s.DeckFrostEnabled, + "deckFrost": &s.DeckFrost, + "showJapaneseGloss": &s.ShowJapaneseGloss, + "general": &s.General, + "theme": &s.Theme, + "bars": &s.Bars, + "menus": &s.Menus, + "notifications": &s.Notifications, + "wallpaper": &s.Wallpaper, + "ask": &s.Ask, + "phone": &s.Phone, + "controls": &s.Controls, + "batteryAlerts": &s.BatteryAlerts, + "sounds": &s.Sounds, } for k, ptr := range dst { raw, ok := raw[k] diff --git a/ryoku/shell/ipc/settings_test.go b/ryoku/shell/ipc/settings_test.go index 65949d4dc9..fdef71f259 100644 --- a/ryoku/shell/ipc/settings_test.go +++ b/ryoku/shell/ipc/settings_test.go @@ -51,6 +51,13 @@ func TestNotificationDefaultsMatchContract(t *testing.T) { } } +func TestJapaneseGlossDefaultsOn(t *testing.T) { + store := newTestStore(t) + if got := frameGet(t, store.frameLocked(), "showJapaneseGloss"); got != true { + t.Fatalf("showJapaneseGloss = %v, want true", got) + } +} + func TestPhoneDefaultsMatchContract(t *testing.T) { store := newTestStore(t) var want any @@ -498,8 +505,8 @@ func TestMalformedFileFirstLoad(t *testing.T) { t.Fatalf("first-load fallback carried %d top-level keys, want %d (schema only)", len(s.raw), len(contractKeys)) } for _, k := range []string{ - "deckFrostEnabled", "deckFrost", "general", "theme", "bars", "menus", "notifications", - "wallpaper", "ask", "controls", "batteryAlerts", + "deckFrostEnabled", "deckFrost", "showJapaneseGloss", "general", "theme", "bars", "menus", + "notifications", "wallpaper", "ask", "controls", "batteryAlerts", } { if _, ok := s.raw[k]; !ok { t.Fatalf("first-load fallback missing schema root %q", k) diff --git a/ryoku/shell/ipc/sleepwake.go b/ryoku/shell/ipc/sleepwake.go index 8b5177e43b..b62bcdeeb8 100644 --- a/ryoku/shell/ipc/sleepwake.go +++ b/ryoku/shell/ipc/sleepwake.go @@ -42,10 +42,11 @@ var ( // The released block fd is observed by logind asynchronously (it polls // the fd); Suspend must not overtake that observation or login1 denies // the daemon's own transaction. Poll its inhibitor list to confirm. - blockReleaseWait = 2 * time.Second - blockReleasePoll = 25 * time.Millisecond - errLogin1OwnerRestart = errors.New("login1 D-Bus owner changed") - errLogin1SignalLost = errors.New("login1 signal stream ended") + blockReleaseWait = 2 * time.Second + blockReleasePoll = 25 * time.Millisecond + errLogin1OwnerRestart = errors.New("login1 D-Bus owner changed") + errLogin1SignalLost = errors.New("login1 signal stream ended") + errLogin1SessionRemoved = errors.New("login1 session removed") ) type sleepInhibitor interface { @@ -180,6 +181,7 @@ type sleepCycle struct { guardKick func() resumeKick func() disconnected <-chan struct{} + sessionID string sessionPath dbus.ObjectPath activeCheck func(context.Context) (bool, error) blockCleared func(context.Context) (bool, error) @@ -801,6 +803,12 @@ func runSleepSignals( cycle.block.release() return errLogin1OwnerRestart } + if sig != nil && + sig.Name == login1Interface+".SessionRemoved" && + sig.Path == login1Path && + len(sig.Body) >= 1 && sig.Body[0] == cycle.sessionID { + return errLogin1SessionRemoved + } if active, changed := sessionActiveChange(sig, cycle.sessionPath); changed { cycle.setSessionActive(active) if cycle.protect() { @@ -882,6 +890,15 @@ func (d *daemon) connectSleepGuard() (_ *connectedSleepGuard, err error) { ); err != nil { return nil, fmt.Errorf("install sleep signal match: %w", err) } + if err := conn.AddMatchSignal( + dbus.WithMatchSender(login1Bus), + dbus.WithMatchObjectPath(login1Path), + dbus.WithMatchInterface(login1Interface), + dbus.WithMatchMember("SessionRemoved"), + dbus.WithMatchArg(0, sessionID), + ); err != nil { + return nil, fmt.Errorf("install session removal match: %w", err) + } if err := conn.AddMatchSignal( dbus.WithMatchSender(login1Bus), dbus.WithMatchObjectPath(sessionPath), @@ -925,6 +942,7 @@ func (d *daemon) connectSleepGuard() (_ *connectedSleepGuard, err error) { retryProtect: make(chan struct{}, 1), disconnected: conn.Context().Done(), sessionPath: sessionPath, + sessionID: sessionID, activeCheck: func(ctx context.Context) (bool, error) { value, err := login1SessionProperty(ctx, conn, sessionPath, "Active") if err != nil { @@ -1061,6 +1079,10 @@ func (d *daemon) superviseSleepWake() { guard.release() return } + if errors.Is(err, errLogin1SessionRemoved) { + guard.release() + return + } log.Printf("ryoku-shell: sleep guard reconnecting: %v", err) if errors.Is(err, errLogin1OwnerRestart) { // The old FDs died with logind, but its phase and suspend-clock diff --git a/ryoku/shell/ipc/sleepwake_test.go b/ryoku/shell/ipc/sleepwake_test.go index 15e576041b..c77983addf 100644 --- a/ryoku/shell/ipc/sleepwake_test.go +++ b/ryoku/shell/ipc/sleepwake_test.go @@ -1190,6 +1190,32 @@ func TestLogin1OwnerLossInvalidatesReadiness(t *testing.T) { } } +func TestRemovedLogin1SessionStopsSleepLockRetries(t *testing.T) { + block := &fakeSleepInhibitor{heldState: true} + delay := &fakeSleepInhibitor{heldState: true} + cycle := &sleepCycle{ + sessionID: "old-session", + block: block, + delay: delay, + retryProtect: make(chan struct{}, 1), + logf: func(string, ...any) {}, + } + sigs := make(chan *dbus.Signal, 2) + sigs <- &dbus.Signal{ + Name: login1Interface + ".SessionRemoved", + Path: login1Path, + Body: []any{"other-session", dbus.ObjectPath("/org/freedesktop/login1/session/other")}, + } + sigs <- &dbus.Signal{ + Name: login1Interface + ".SessionRemoved", + Path: login1Path, + Body: []any{"old-session", dbus.ObjectPath("/org/freedesktop/login1/session/old")}, + } + if err := runSleepSignals(sigs, make(chan struct{}), cycle, true, time.Second); !errors.Is(err, errLogin1SessionRemoved) { + t.Fatalf("removed session returned %v", err) + } +} + func TestClosedLogin1SignalStreamRetainsHardBlockForReconnect(t *testing.T) { block := &fakeSleepInhibitor{heldState: true} delay := &fakeSleepInhibitor{heldState: true} diff --git a/ryoku/shell/ipc/voicewatch.go b/ryoku/shell/ipc/voicewatch.go index aba341997b..3deaea3c2d 100644 --- a/ryoku/shell/ipc/voicewatch.go +++ b/ryoku/shell/ipc/voicewatch.go @@ -68,11 +68,20 @@ func voiceStopOnIdle(sawActive bool, class string) (closeNow, nextSawActive bool } // watchVoice streams Voxtype's state for one dictation session, started from -// voice() on the ON edge. It exits on its own once dictation ends (Voxtype -// returns to idle) or the session is stopped, so an idle desktop spawns nothing. -// A stop channel lets a manual OFF tap reap the stream; the watcher only -// performs the surface close itself when Voxtype ended without that tap. +// voice() on the ON edge. Only one stream may own the Voxtype follow connection +// at a time; canceled sessions leave before spawning and the next live session +// waits until its predecessor has killed and reaped the child. func (d *daemon) watchVoice(stop <-chan struct{}) { + d.voiceWatchMu.Lock() + defer d.voiceWatchMu.Unlock() + + select { + case <-d.quit: + return + case <-stop: + return + default: + } if _, err := exec.LookPath("voxtype"); err != nil { return } @@ -88,17 +97,10 @@ func (d *daemon) watchVoice(stop <-chan struct{}) { return } - kill := make(chan struct{}) - go func() { - select { - case <-d.quit: - _ = cmd.Process.Kill() - case <-kill: - } - }() - classes := make(chan string, 8) + scanDone := make(chan struct{}) go func() { + defer close(scanDone) sc := bufio.NewScanner(stdout) for sc.Scan() { if class, ok := parseVoxtypeClass(sc.Text()); ok { @@ -111,17 +113,21 @@ func (d *daemon) watchVoice(stop <-chan struct{}) { close(classes) }() + killAndWait := func() { + _ = cmd.Process.Kill() + _ = cmd.Wait() + <-scanDone + } + sawActive := false for { var closeNow bool select { case <-d.quit: - close(kill) - _ = cmd.Wait() + killAndWait() return case <-stop: - close(kill) - _ = cmd.Wait() + killAndWait() return case class, ok := <-classes: if !ok { @@ -132,9 +138,8 @@ func (d *daemon) watchVoice(stop <-chan struct{}) { } } if closeNow { - close(kill) - _ = cmd.Wait() - d.voiceEnded() + killAndWait() + d.voiceEnded(stop) return } } @@ -144,9 +149,9 @@ func (d *daemon) watchVoice(stop <-chan struct{}) { // it closes the card and clears the toggle so the next tap starts fresh. It is a // no-op if a manual tap already closed the surface (voiceOn is false), so the two // close paths never fight over the state. -func (d *daemon) voiceEnded() { +func (d *daemon) voiceEnded(stop <-chan struct{}) { d.voiceMu.Lock() - if !d.voiceOn { + if !d.voiceOn || d.voiceStop != stop { d.voiceMu.Unlock() return } diff --git a/ryoku/shell/ipc/voicewatch_test.go b/ryoku/shell/ipc/voicewatch_test.go index 2934808d42..d74be880ae 100644 --- a/ryoku/shell/ipc/voicewatch_test.go +++ b/ryoku/shell/ipc/voicewatch_test.go @@ -1,6 +1,14 @@ package main -import "testing" +import ( + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" + "time" +) // #244: the dictation surface stayed open after Voxtype returned to idle, // because the only close path was a second tap. The watcher must close on the @@ -45,3 +53,149 @@ func TestParseVoxtypeClass(t *testing.T) { } } } + +const fakeVoxtype = `#!/bin/sh +set -eu +printf '%s\n' "$$" >> "$VOXTYPE_PID_FILE" +printf '{"class":"recording"}\n' +if [ "${VOXTYPE_MODE:-forever}" = idle ]; then + printf '{"class":"idle"}\n' +fi +exec sleep 300 +` + +func installFakeVoxtype(t *testing.T, mode string) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "voxtype") + if err := os.WriteFile(path, []byte(fakeVoxtype), 0755); err != nil { + t.Fatal(err) + } + pidFile := filepath.Join(dir, "pids") + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("VOXTYPE_PID_FILE", pidFile) + t.Setenv("VOXTYPE_MODE", mode) + return pidFile +} + +func startVoiceWatcher(d *daemon, stop <-chan struct{}) <-chan struct{} { + done := make(chan struct{}) + go func() { + d.watchVoice(stop) + close(done) + }() + return done +} + +func waitVoxtypePID(t *testing.T, pidFile string, count int) int { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + data, err := os.ReadFile(pidFile) + if err == nil { + fields := strings.Fields(string(data)) + if len(fields) >= count { + pid, err := strconv.Atoi(fields[count-1]) + if err != nil { + t.Fatalf("bad fake voxtype pid %q: %v", fields[count-1], err) + } + return pid + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("fake voxtype did not start process %d", count) + return 0 +} + +func processAlive(pid int) bool { + err := syscall.Kill(pid, 0) + return err == nil || err == syscall.EPERM +} + +func waitWatcherDone(done <-chan struct{}) bool { + select { + case <-done: + return true + case <-time.After(2 * time.Second): + return false + } +} + +func killTestProcess(pid int, done <-chan struct{}) { + _ = syscall.Kill(pid, syscall.SIGKILL) + select { + case <-done: + case <-time.After(time.Second): + } +} + +func TestWatchVoiceReapsFollowProcess(t *testing.T) { + t.Run("manual stop", func(t *testing.T) { + pidFile := installFakeVoxtype(t, "forever") + d := &daemon{quit: make(chan struct{})} + stop := make(chan struct{}) + done := startVoiceWatcher(d, stop) + pid := waitVoxtypePID(t, pidFile, 1) + + close(stop) + if !waitWatcherDone(done) { + killTestProcess(pid, done) + t.Fatal("watchVoice did not kill and reap voxtype after stop") + } + if processAlive(pid) { + killTestProcess(pid, done) + t.Fatalf("voxtype process %d remains after stop", pid) + } + }) + + t.Run("idle close", func(t *testing.T) { + pidFile := installFakeVoxtype(t, "idle") + d := &daemon{quit: make(chan struct{})} + stop := make(chan struct{}) + done := startVoiceWatcher(d, stop) + pid := waitVoxtypePID(t, pidFile, 1) + + if !waitWatcherDone(done) { + killTestProcess(pid, done) + t.Fatal("watchVoice did not kill and reap voxtype after idle") + } + if processAlive(pid) { + killTestProcess(pid, done) + t.Fatalf("voxtype process %d remains after idle", pid) + } + }) +} + +func TestWatchVoiceConsecutiveSessionsDoNotOverlap(t *testing.T) { + pidFile := installFakeVoxtype(t, "forever") + d := &daemon{quit: make(chan struct{})} + + stopFirst := make(chan struct{}) + firstDone := startVoiceWatcher(d, stopFirst) + firstPID := waitVoxtypePID(t, pidFile, 1) + close(stopFirst) + + stopSecond := make(chan struct{}) + secondDone := startVoiceWatcher(d, stopSecond) + secondPID := waitVoxtypePID(t, pidFile, 2) + if processAlive(firstPID) { + killTestProcess(firstPID, firstDone) + close(stopSecond) + killTestProcess(secondPID, secondDone) + t.Fatalf("first voxtype process %d still alive when session two started", firstPID) + } + + close(stopSecond) + if !waitWatcherDone(firstDone) || !waitWatcherDone(secondDone) { + killTestProcess(firstPID, firstDone) + killTestProcess(secondPID, secondDone) + t.Fatal("consecutive voice watchers did not exit") + } + if processAlive(secondPID) { + killTestProcess(secondPID, secondDone) + t.Fatalf("second voxtype process %d remains after stop", secondPID) + } +} diff --git a/ryoku/shell/plugin/cavafeed.cpp b/ryoku/shell/plugin/cavafeed.cpp index 28afe85563..473d2302b2 100644 --- a/ryoku/shell/plugin/cavafeed.cpp +++ b/ryoku/shell/plugin/cavafeed.cpp @@ -16,8 +16,8 @@ constexpr int StopGraceMs = 500; constexpr int SilenceDelayMs = 1000; constexpr qreal PeakTarget = 0.6; constexpr qreal PeakReleaseMs = 8000.0; -constexpr qreal QuietBoostMax = 4.0; -constexpr qreal PeakNoiseFloor = 0.02; +constexpr qreal QuietBoostMax = 8.0; +constexpr qreal LoudCutMin = 0.5; } CavaFeed::CavaFeed(QObject* parent) @@ -239,6 +239,8 @@ void CavaFeed::startProcess() { m_intentionalStop = false; m_acceptOutput = false; m_frameBytes = 0; + m_peak = 0.0; + m_lastFrameMs = -1; m_process.setProcessChannelMode(QProcess::SeparateChannels); m_process.start(executable, { QStringLiteral("-p"), m_configPath }, QIODevice::ReadOnly); } @@ -334,10 +336,10 @@ void CavaFeed::processFrame() { if (m_peakFollow) { m_peak = std::max(frameMax, m_peak * std::exp(-static_cast(elapsed) / PeakReleaseMs)); if (m_peak > 0.0) - gain = std::clamp(PeakTarget / m_peak, qreal(1.0), QuietBoostMax); + gain = std::clamp(PeakTarget / m_peak, LoudCutMin, QuietBoostMax); } - const qreal floor = m_peakFollow ? std::max(m_noiseFloor, PeakNoiseFloor) : m_noiseFloor; + const qreal floor = m_noiseFloor; qreal sum = 0.0; bool levelsDiffer = false; for (int i = 0; i < m_bars; ++i) { diff --git a/ryoku/shell/plugin/systemmonitor.cpp b/ryoku/shell/plugin/systemmonitor.cpp index cb73f2a621..a993f59716 100644 --- a/ryoku/shell/plugin/systemmonitor.cpp +++ b/ryoku/shell/plugin/systemmonitor.cpp @@ -743,7 +743,7 @@ class GpuSampler { reading.memoryTotalGiB = static_cast(total) / BytesPerGiB; reading.memoryAvailable = true; } - if (includePower) { + if (includePower && !m_powerPath.isEmpty()) { quint64 microwatts = 0; if (readUnsignedFile(m_powerPath, microwatts)) reading.powerWatts = static_cast(microwatts) / 1000000.0; @@ -1376,8 +1376,10 @@ void SystemMonitor::startWorker() { slow.battery = readBattery(batterySensor); slow.uptimeSeconds = readUptimeSeconds(); slow.storage = readRootStorage(); - quint64 fanRpm = 0; - slow.fanRpm = readUnsignedFile(fanPath, fanRpm) ? static_cast(fanRpm) : 0.0; + if (!fanPath.isEmpty()) { + quint64 fanRpm = 0; + slow.fanRpm = readUnsignedFile(fanPath, fanRpm) ? static_cast(fanRpm) : 0.0; + } } ++tick; result.loadAverage = slow.load.average; diff --git a/ryoku/shell/plugin/wavecapture.cpp b/ryoku/shell/plugin/wavecapture.cpp index 8dc1c62ec2..b05c6e4a16 100644 --- a/ryoku/shell/plugin/wavecapture.cpp +++ b/ryoku/shell/plugin/wavecapture.cpp @@ -85,6 +85,21 @@ void WaveCapture::setActive(bool active) { stopCapture(); emit activeChanged(); } +void WaveCapture::setSource(const QString& source) { + QString bounded = source.trimmed(); + bounded.replace('\n', ' '); + bounded.replace('\r', ' '); + if (bounded == QStringLiteral("auto")) + bounded.clear(); + if (m_source == bounded) + return; + m_source = bounded; + emit sourceChanged(); + if (m_active) { + stopCapture(); + scheduleRestart(); + } +} void WaveCapture::startResolution() { if (!m_active || m_resolver.state() != QProcess::NotRunning @@ -92,10 +107,20 @@ void WaveCapture::startResolution() { return; m_restart.stop(); - m_defaultSink.clear(); - m_resolveStage = ResolveStage::DefaultSink; - m_resolver.setProgram(QStringLiteral("pactl")); - m_resolver.setArguments({QStringLiteral("get-default-sink")}); + if (m_source.isEmpty()) { + m_defaultSink.clear(); + m_explicitResolution = false; + m_resolveStage = ResolveStage::DefaultSink; + m_resolver.setProgram(QStringLiteral("pactl")); + m_resolver.setArguments({QStringLiteral("get-default-sink")}); + } else { + m_defaultSink = m_source; + m_explicitResolution = true; + m_resolveStage = ResolveStage::Sources; + m_resolver.setProgram(QStringLiteral("pactl")); + m_resolver.setArguments({QStringLiteral("list"), QStringLiteral("short"), + QStringLiteral("sources")}); + } m_resolver.start(); } @@ -142,6 +167,15 @@ void WaveCapture::handleResolverExit(int exitCode, QProcess::ExitStatus status) break; } } + if (target.isEmpty() && m_explicitResolution) { + m_explicitResolution = false; + m_defaultSink.clear(); + m_resolveStage = ResolveStage::DefaultSink; + m_resolver.setProgram(QStringLiteral("pactl")); + m_resolver.setArguments({QStringLiteral("get-default-sink")}); + m_resolver.start(); + return; + } if (target.isEmpty()) target = fallback; m_resolveStage = ResolveStage::Idle; diff --git a/ryoku/shell/plugin/wavecapture.hpp b/ryoku/shell/plugin/wavecapture.hpp index ff9f00ba1b..bdde65b6ae 100644 --- a/ryoku/shell/plugin/wavecapture.hpp +++ b/ryoku/shell/plugin/wavecapture.hpp @@ -12,6 +12,7 @@ class WaveCapture : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QList samples READ samples NOTIFY samplesChanged) Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(bool silent READ silent NOTIFY silentChanged) @@ -22,12 +23,14 @@ class WaveCapture : public QObject { bool active() const { return m_active; } void setActive(bool active); - + const QString& source() const { return m_source; } + void setSource(const QString& source); const QList& samples() const { return m_samples; } bool running() const { return m_running; } bool silent() const { return m_silent; } signals: + void sourceChanged(); void activeChanged(); void samplesChanged(); void runningChanged(); @@ -64,7 +67,9 @@ class WaveCapture : public QObject { qsizetype m_pcmOffset = 0; QList m_samples; QString m_defaultSink; + QString m_source; ResolveStage m_resolveStage = ResolveStage::Idle; + bool m_explicitResolution = false; bool m_active = false; bool m_running = false; bool m_silent = true; diff --git a/ryoku/shell/quickshell/inir/modules/background/widgets/japaneseTypography/JapaneseTypographyWidget.qml b/ryoku/shell/quickshell/inir/modules/background/widgets/japaneseTypography/JapaneseTypographyWidget.qml index 7be2edd6f1..4d63dea107 100644 --- a/ryoku/shell/quickshell/inir/modules/background/widgets/japaneseTypography/JapaneseTypographyWidget.qml +++ b/ryoku/shell/quickshell/inir/modules/background/widgets/japaneseTypography/JapaneseTypographyWidget.qml @@ -8,6 +8,7 @@ import inir.modules.common import inir.modules.common.functions import inir.modules.common.widgets import inir.modules.background.widgets +import Ryoku.Ui.Singletons import "JapaneseTypographyPresets.js" as JapanesePresets AbstractBackgroundWidget { @@ -192,6 +193,9 @@ AbstractBackgroundWidget { readonly property string sealText: Config.getNestedValue("background.widgets.japaneseTypography.sealText", "特別展") readonly property string footerText: Config.getNestedValue("background.widgets.japaneseTypography.footerText", "PACIFIC DRIVE-IN") readonly property string dateText: Config.getNestedValue("background.widgets.japaneseTypography.dateText", "7.12 — 8.31") + readonly property string displayPrimaryText: UiPreferences.shippedText(root.primaryText, "夏の記憶", "SUMMER MEMORIES") + readonly property string displaySecondaryText: UiPreferences.shippedText(root.secondaryText, "潮風と、あの子と、終わらない夏", "SEA BREEZE AND ENDLESS SUMMER") + readonly property string displaySealText: UiPreferences.shippedText(root.sealText, "特別展", "SPECIAL") readonly property bool showSecondary: Config.getNestedValue("background.widgets.japaneseTypography.showSecondary", true) readonly property bool showSeal: Config.getNestedValue("background.widgets.japaneseTypography.showSeal", true) readonly property bool showFooter: Config.getNestedValue("background.widgets.japaneseTypography.showFooter", true) @@ -370,7 +374,7 @@ AbstractBackgroundWidget { bottom: parent.bottom } width: root.leadWidth - text: root.primaryText + text: root.displayPrimaryText fontFamily: root.fontFamily fontPixelSize: root.leadPixelSize fontWeight: root.primaryWeight @@ -395,7 +399,7 @@ AbstractBackgroundWidget { bottom: parent.bottom } width: root.noteWidth - text: root.secondaryText + text: root.displaySecondaryText fontFamily: root.secondaryFontFamily fontPixelSize: root.notePixelSize fontWeight: root.secondaryWeight @@ -431,8 +435,8 @@ AbstractBackgroundWidget { height: Math.max(1, Math.min( parent.height - sealTypography.innerPadding * 2, Math.max(sealTypography.cellAdvance, - Array.from(root.sealText).length * sealTypography.cellAdvance))) - text: root.sealText + Array.from(root.displaySealText).length * sealTypography.cellAdvance))) + text: root.displaySealText fontFamily: root.fontFamily fontPixelSize: Math.max(10 * root.scaleFactor, root.notePixelSize * 0.86) fontWeight: Font.DemiBold diff --git a/ryoku/shell/quickshell/inir/modules/background/widgets/visualizer/VisualizerWidget.qml b/ryoku/shell/quickshell/inir/modules/background/widgets/visualizer/VisualizerWidget.qml index 5fa69cec4b..8229d9b904 100644 --- a/ryoku/shell/quickshell/inir/modules/background/widgets/visualizer/VisualizerWidget.qml +++ b/ryoku/shell/quickshell/inir/modules/background/widgets/visualizer/VisualizerWidget.qml @@ -45,6 +45,7 @@ AbstractBackgroundWidget { // Ryoku hosting draws the slot card, so drop this widget's own card in Ryoku // style — the bars/organic art sit on the single shared surface. readonly property bool _ryokuBare: root.ryokuHosted && root.ryokuStyle + property real ryokuSlotRadius: -1 readonly property string vizType: Config.getNestedValue("background.widgets.visualizer.vizType", "bars") readonly property int waveOpacity: Config.getNestedValue("background.widgets.visualizer.waveOpacity", -1) @@ -401,6 +402,11 @@ AbstractBackgroundWidget { animate: root.motionActive && vizDrawn.drawing visualizerType: root.vizType normalizationCeiling: cavaProcess.normalizationCeiling + topLeftRadius: Math.max(0, (root._ryokuBare && root.ryokuSlotRadius >= 0 + ? root.ryokuSlotRadius : cardBg.radius) - anchors.margins) + topRightRadius: topLeftRadius + bottomLeftRadius: topLeftRadius + bottomRightRadius: topLeftRadius spectrumColors: root.spectrumPalette spectrumColor: root.widgetAccentVisible spectrumOpacity: (root.vizType === "wave" diff --git a/ryoku/shell/quickshell/inir/modules/common/Config.qml b/ryoku/shell/quickshell/inir/modules/common/Config.qml index e1cc12d3ba..bd13cc8e73 100644 --- a/ryoku/shell/quickshell/inir/modules/common/Config.qml +++ b/ryoku/shell/quickshell/inir/modules/common/Config.qml @@ -101,8 +101,15 @@ Singleton { function setNestedValues(patches) { let optionsDirty = false for (const path of Object.keys(patches)) { - if (path === "time.format") - root.pending["general.clock_format_24_h"] = RyokuUi.TimeFormat.is24HourPattern(patches[path]) + if (path === "time.format") { + const format = String(patches[path]) + root.options = Object.assign({}, root.options, { + time: Object.assign({}, root.options?.time ?? ({}), { format: format }) + }) + root.pending["general.clock_format_24_h"] = RyokuUi.TimeFormat.is24HourPattern(format) + optionsDirty = true + continue + } if (root.sharedKeys.includes(path)) { root.pending[path] = patches[path] continue @@ -151,8 +158,11 @@ Singleton { // daemon's single-writer lock on first boot. function _seed(frame) { const stored = frame?.inir ?? ({}); + const legacyTimeFormat = typeof stored?.time?.format === "string" + ? stored.time.format : "" const defs = IrisDefaults.config(); const merged = Object.assign({}, stored); + const needsSeed = Object.keys(defs).some(k => stored[k] === undefined) for (const k of Object.keys(defs)) { if (k === "enabledPanels") { const have = Array.isArray(merged.enabledPanels) ? merged.enabledPanels : null; @@ -165,10 +175,16 @@ Singleton { } } merged.panelFamily = "iris"; + if (merged.time !== null && typeof merged.time === "object") { + merged.time = Object.assign({}, merged.time) + delete merged.time.format + } root.options = Object.assign({}, merged, { time: Object.assign({}, merged.time, { format: RyokuUi.TimeFormat.pattern() }) }); - if (!root._seeded && Object.keys(defs).some(k => stored[k] === undefined)) + if (!root._seeded && legacyTimeFormat !== "") + root.pending["general.clock_format_24_h"] = RyokuUi.TimeFormat.is24HourPattern(legacyTimeFormat) + if (!root._seeded && (needsSeed || legacyTimeFormat !== "")) root.pending["inir"] = merged; root._seeded = true; if (Object.keys(root.pending).length > 0) diff --git a/ryoku/shell/quickshell/inir/modules/common/models/quickToggles/BluetoothToggle.qml b/ryoku/shell/quickshell/inir/modules/common/models/quickToggles/BluetoothToggle.qml index 5039b6211b..0dc0c579ae 100644 --- a/ryoku/shell/quickshell/inir/modules/common/models/quickToggles/BluetoothToggle.qml +++ b/ryoku/shell/quickshell/inir/modules/common/models/quickToggles/BluetoothToggle.qml @@ -8,9 +8,11 @@ import inir.modules.common.widgets QuickToggleModel { name: Translation.tr("Bluetooth") - statusText: BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Not connected") + statusText: BluetoothStatus.blocked ? Translation.tr("Blocked") + : BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Not connected") tooltipText: Translation.tr("%1 | Right-click to configure").arg( - BluetoothStatus.activeDeviceSummary(true) || Translation.tr("Bluetooth") + BluetoothStatus.blocked ? Translation.tr("Bluetooth is blocked") + : BluetoothStatus.activeDeviceSummary(true) || Translation.tr("Bluetooth") ) icon: BluetoothStatus.activeIcon diff --git a/ryoku/shell/quickshell/inir/modules/iris/bar/IrisIsland.qml b/ryoku/shell/quickshell/inir/modules/iris/bar/IrisIsland.qml index f489277e04..3118d171f8 100644 --- a/ryoku/shell/quickshell/inir/modules/iris/bar/IrisIsland.qml +++ b/ryoku/shell/quickshell/inir/modules/iris/bar/IrisIsland.qml @@ -353,13 +353,14 @@ Item { readonly property string levelStyle: String(root.osdPrefs?.style ?? "capsule") readonly property bool levelFigure: root.osdPrefs?.figure ?? true readonly property bool osdKeyboard: (root.osdPrefs?.keyboard ?? true) && root.osdGameRule === "show" - function showFeedback(kind: string, requested = false): void { + function showFeedback(kind: string, requested = false, micVolumeChange = false, muteChange = false): void { if (Date.now() < GlobalStates.irisLevelQuietUntil) return if (!(Config.options?.iris?.modules?.osd ?? true) || root.expanded || root.fullscreenCovered || root.osdGameRule === "hide" || root.targetScreen?.name !== GlobalStates.focusedScreen?.name) return + if (kind === "mic" && micVolumeChange && !Ryoku.OsdFeed.acceptsMicVolume(Audio.micVolume)) return const mode = String(root.osdPrefs?.levels ?? "yours") const touched = kind === "brightness" ? Brightness.lastUserChange : Audio.lastUserChange - if (!requested && (mode === "off" || (mode === "yours" && Date.now() - touched > 1200))) return + if (!requested && (mode === "off" || (kind !== "volume" && !muteChange && mode === "yours" && Date.now() - touched > 1200))) return root.feedbackKind = kind feedbackTimer.restart() } @@ -1069,12 +1070,12 @@ Item { Connections { target: Audio.sink?.audio ?? null function onVolumeChanged(): void { root.showFeedback("volume") } - function onMutedChanged(): void { root.showFeedback("volume") } + function onMutedChanged(): void { root.showFeedback("volume", false, false, true) } } Connections { target: Audio - function onMicVolumeChanged(): void { root.showFeedback("mic") } - function onMicMutedChanged(): void { root.showFeedback("mic") } + function onMicVolumeChanged(): void { root.showFeedback("mic", false, true) } + function onMicMutedChanged(): void { root.showFeedback("mic", false, false, true) } } Connections { target: Brightness diff --git a/ryoku/shell/quickshell/inir/modules/iris/dock/IrisDockSurface.qml b/ryoku/shell/quickshell/inir/modules/iris/dock/IrisDockSurface.qml index d3f4380863..b04d7af026 100644 --- a/ryoku/shell/quickshell/inir/modules/iris/dock/IrisDockSurface.qml +++ b/ryoku/shell/quickshell/inir/modules/iris/dock/IrisDockSurface.qml @@ -24,7 +24,10 @@ Scope { anchors { left: true; right: true; top: true; bottom: true } exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "ryoku-dock-shima" - WlrLayershell.layer: root.stageEditing ? WlrLayer.Overlay : WlrLayer.Top + // The Shima chassis paints this dock's fused body in its Top-layer + // field. Keep the icon surface above that field regardless of which + // one finishes loading first or whether the shell was just restarted. + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: dock.menuOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None // The dock body is visually reparented through the Stage layer. Mapping // it to `rest` collapses to (0, 0), so build the compositor mask from diff --git a/ryoku/shell/quickshell/inir/modules/iris/onScreenDisplay/IrisOSD.qml b/ryoku/shell/quickshell/inir/modules/iris/onScreenDisplay/IrisOSD.qml index eaee17e390..76796ebc44 100644 --- a/ryoku/shell/quickshell/inir/modules/iris/onScreenDisplay/IrisOSD.qml +++ b/ryoku/shell/quickshell/inir/modules/iris/onScreenDisplay/IrisOSD.qml @@ -15,6 +15,7 @@ import inir.modules.iris.style import inir.modules.iris.field as Field import inir.modules.iris.components import inir.modules.iris.bar.island +import shell.services as Ryoku Scope { id: root @@ -55,10 +56,11 @@ Scope { if (root.gameRule === "quiet") return nextKind !== "media" && nextKind !== "keyboard" && nextKind !== "connection" return nextKind !== "keyboard" || (root.prefs?.keyboard ?? true) } - function showLevel(nextKind: string): void { + function showLevel(nextKind: string, volumeChange = false, muteChange = false): void { if (root.levelsMode === "off") return + if (nextKind === "mic" && volumeChange && !Ryoku.OsdFeed.acceptsMicVolume(Audio.micVolume)) return const touched = nextKind === "brightness" ? Brightness.lastUserChange : Audio.lastUserChange - if (root.levelsMode === "yours" && Date.now() - touched > 1200) return + if (nextKind !== "volume" && !muteChange && root.levelsMode === "yours" && Date.now() - touched > 1200) return root.show(nextKind, root.holdMs) } @@ -148,12 +150,12 @@ Scope { Connections { target: Audio.sink?.audio ?? null function onVolumeChanged(): void { root.showLevel("volume") } - function onMutedChanged(): void { root.showLevel("volume") } + function onMutedChanged(): void { root.showLevel("volume", false, true) } } Connections { target: Audio - function onMicVolumeChanged(): void { root.showLevel("mic") } - function onMicMutedChanged(): void { root.showLevel("mic") } + function onMicVolumeChanged(): void { root.showLevel("mic", true) } + function onMicMutedChanged(): void { root.showLevel("mic", false, true) } function onSinkProtectionTriggered(reason: string): void { root.warningText = reason root.show("warning", root.holdMs + 1100) diff --git a/ryoku/shell/quickshell/inir/modules/iris/settings/IrisSettings.qml b/ryoku/shell/quickshell/inir/modules/iris/settings/IrisSettings.qml index 2312dc3077..cf651f68f6 100644 --- a/ryoku/shell/quickshell/inir/modules/iris/settings/IrisSettings.qml +++ b/ryoku/shell/quickshell/inir/modules/iris/settings/IrisSettings.qml @@ -663,7 +663,7 @@ Item { Behavior on scale { NumberAnimation { duration: IrisStyle.feedbackDuration } } IrisText { anchors.centerIn: parent - text: frontendSwitch.ryoku ? "力" : "i" + text: frontendSwitch.ryoku ? UiPreferences.brandMark("力") : "i" color: frontendSwitch.ryoku ? IrisStyle.accent : IrisStyle.muted font.family: frontendSwitch.ryoku ? Tokens.jp : IrisStyle.fontMain font.pixelSize: 11 * IrisStyle.typeScale @@ -1135,7 +1135,7 @@ Item { IrisText { id: jpSeal visible: !sectionRow.compact && IrisStyle.ryokuFrontend - && String(root.jpName[sectionRow.modelData.id] ?? "").length > 0 + && UiPreferences.showJapaneseGloss && String(root.jpName[sectionRow.modelData.id] ?? "").length > 0 anchors.right: parent.right anchors.rightMargin: 10 * root.d anchors.verticalCenter: parent.verticalCenter diff --git a/ryoku/shell/quickshell/inir/services/Audio.qml b/ryoku/shell/quickshell/inir/services/Audio.qml index a66b8676a4..579d7a75cb 100644 --- a/ryoku/shell/quickshell/inir/services/Audio.qml +++ b/ryoku/shell/quickshell/inir/services/Audio.qml @@ -6,6 +6,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import inir.services.deferred +import shell.services as Ryoku /** * A nice wrapper for default Pipewire audio sink and source. @@ -47,6 +48,9 @@ Singleton { function onVolumeChanged(): void { root._micVolume = root.source?.audio?.volume ?? 0 } + function onMutedChanged(): void { + root._micMuted = root.source?.audio?.muted ?? false + } } onSourceChanged: { @@ -126,6 +130,7 @@ Singleton { function setSourceVolume(target: real): void { root.lastUserChange = Date.now() const clamped = Math.max(0, Math.min(root.hardMaxValue, target)) + Ryoku.OsdFeed.expectMicVolume(clamped) root._micVolume = clamped root._queueSourceVolume(clamped) } @@ -507,5 +512,8 @@ Singleton { function micMute(): void { root.toggleMicMute(); } + function micVolume(value: real): void { + root.setSourceVolume(value); + } } } diff --git a/ryoku/shell/quickshell/inir/services/BluetoothStatus.qml b/ryoku/shell/quickshell/inir/services/BluetoothStatus.qml index 767f5c7093..d8b4abbf62 100644 --- a/ryoku/shell/quickshell/inir/services/BluetoothStatus.qml +++ b/ryoku/shell/quickshell/inir/services/BluetoothStatus.qml @@ -16,9 +16,14 @@ Singleton { // adapter) until `clear` or a restart, so every Bluetooth surface can be seen without the hardware. property string simulated: "" readonly property int _simulatedCount: /^\d+$/.test(root.simulated) ? parseInt(root.simulated) : 0 - readonly property bool available: root.simulated.length > 0 ? root.simulated !== "none" : Bluetooth.adapters.values.length > 0 + readonly property bool available: root.simulated.length > 0 ? root.simulated !== "none" + : Bluetooth.adapters.values.length > 0 || root.blocked readonly property bool enabled: root.simulated.length > 0 ? (root.simulated === "on" || /^\d+$/.test(root.simulated)) : Bluetooth.defaultAdapter?.enabled ?? false + readonly property bool blocked: root.simulated.length === 0 && (root.softBlocked || root.hardBlocked + || Bluetooth.defaultAdapter?.state === BluetoothAdapterState.Blocked) + property bool softBlocked: false + property bool hardBlocked: false readonly property BluetoothDevice firstActiveDevice: root.simulated.length > 0 ? null : Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null readonly property int activeDeviceCount: root.simulated.length > 0 ? root._simulatedCount @@ -29,7 +34,7 @@ Singleton { target: "bluetooth" function status(): string { - const state = !root.available ? "no adapter" : !root.enabled ? "off" + const state = !root.available ? "no adapter" : root.blocked ? "blocked" : !root.enabled ? "off" : root.activeDeviceCount > 0 ? `${root.activeDeviceCount} connected` : "on" return state + (root.simulated.length > 0 ? " (simulated)" : "") } @@ -41,9 +46,28 @@ Singleton { } } + Connections { + target: Bluetooth + function onDefaultAdapterChanged(): void { + rfkillState.running = true; + } + } + + Process { + id: rfkillState + command: ["rfkill", "-n", "-o", "TYPE,SOFT,HARD", "list", "bluetooth"] + running: true + stdout: StdioCollector { + onStreamFinished: { + const rows = text.trim().split("\n").map(line => line.trim().split(/\s+/)) + .filter(fields => fields[0] === "bluetooth"); + root.softBlocked = rows.some(fields => fields[1] === "blocked"); + root.hardBlocked = rows.some(fields => fields[2] === "blocked"); + } + } + } + function setEnabled(on: bool): void { - if (!Bluetooth.defaultAdapter) - return; Quickshell.execDetached(["ryoku-bluetooth-power", on ? "on" : "off"]); } diff --git a/ryoku/shell/quickshell/inir/services/MaterialThemeLoader.qml b/ryoku/shell/quickshell/inir/services/MaterialThemeLoader.qml index e37c350092..2a74ca230b 100644 --- a/ryoku/shell/quickshell/inir/services/MaterialThemeLoader.qml +++ b/ryoku/shell/quickshell/inir/services/MaterialThemeLoader.qml @@ -16,6 +16,11 @@ Singleton { id: root property string filePath: Directories.generatedMaterialThemePath property bool ready: false + property string committedContent: "" + property string previewContent: "" + function refresh(): void { + root.applyColors(root.previewContent || root.committedContent) + } function _log(...args): void { if (Quickshell.env("QS_DEBUG") === "1") console.log(...args); @@ -74,10 +79,30 @@ Singleton { id: themeFileView path: Qt.resolvedUrl(root.filePath) watchChanges: true - onLoadedChanged: { - root.applyColors(themeFileView.text()) + onFileChanged: reload() + onLoaded: { + root.committedContent = text() + root.refresh() root.ready = true } onLoadFailed: root._log("[MaterialThemeLoader] palette file not present yet") } + FileView { + id: previewFileView + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + const content = text() + try { + const parsed = JSON.parse(content || "{}") + root.previewContent = parsed && parsed.background ? content : "" + } catch (e) { + root.previewContent = "" + } + root.refresh() + } + } } diff --git a/ryoku/shell/quickshell/keys-hint/shell.qml b/ryoku/shell/quickshell/keys-hint/shell.qml index f26f06bf18..844f1adad7 100644 --- a/ryoku/shell/quickshell/keys-hint/shell.qml +++ b/ryoku/shell/quickshell/keys-hint/shell.qml @@ -10,6 +10,8 @@ import Ryoku.Ui.Singletons // out; dismissing it quits the process and releases the launch flock. ShellRoot { id: app + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") property bool closing: false function dismiss() { @@ -129,7 +131,7 @@ ShellRoot { } Text { anchors.verticalCenter: parent.verticalCenter - text: "\u529b"; color: Tokens.ink + text: UiPreferences.brandMark("\u529b"); color: Tokens.ink font.family: Tokens.jp; font.pixelSize: Tokens.fBody } Text { diff --git a/ryoku/shell/quickshell/keys/Kit.qml b/ryoku/shell/quickshell/keys/Kit.qml index adc4b68d4a..e82c0e384d 100644 --- a/ryoku/shell/quickshell/keys/Kit.qml +++ b/ryoku/shell/quickshell/keys/Kit.qml @@ -361,7 +361,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - text: "\u529b" + text: UiPreferences.brandMark("\u529b") color: Tokens.ink font.family: Tokens.jp font.pixelSize: Tokens.fMicro diff --git a/ryoku/shell/quickshell/keys/commands/catalog/ryoku-cli.json b/ryoku/shell/quickshell/keys/commands/catalog/ryoku-cli.json index 1a22d0faeb..fdc6650d69 100644 --- a/ryoku/shell/quickshell/keys/commands/catalog/ryoku-cli.json +++ b/ryoku/shell/quickshell/keys/commands/catalog/ryoku-cli.json @@ -5196,8 +5196,8 @@ "id": "ryoku.ryoku-hub.lock.apply-greeter", "group": "ryoku-internal", "title": "Apply the greeter lock theme", - "command": "ryoku-hub lock apply-greeter ", - "description": "Runs the privileged greeter-theme installation step.", + "command": "ryoku-hub lock apply-greeter ", + "description": "Runs the privileged greeter-theme installation step with the global clock format.", "tool": "ryoku-hub", "level": "internal", "privileged": true diff --git a/ryoku/shell/quickshell/keys/shell.qml b/ryoku/shell/quickshell/keys/shell.qml index 838d5d210f..cc81cdef4d 100644 --- a/ryoku/shell/quickshell/keys/shell.qml +++ b/ryoku/shell/quickshell/keys/shell.qml @@ -12,6 +12,8 @@ import Ryoku.Ui.Singletons // and leaves no idle process. ShellRoot { id: app + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") property var categories: [] property bool loaded: false diff --git a/ryoku/shell/quickshell/plugins/kit/SearchField.qml b/ryoku/shell/quickshell/plugins/kit/SearchField.qml index 8c94578d13..49662991ed 100644 --- a/ryoku/shell/quickshell/plugins/kit/SearchField.qml +++ b/ryoku/shell/quickshell/plugins/kit/SearchField.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import "Singletons" +import Ryoku.Ui.Singletons as Ui Item { id: root @@ -24,7 +25,7 @@ Item { id: glyph anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - text: root.kanji + text: Ui.UiPreferences.text(root.kanji, "") color: Theme.brand font.family: Theme.fontJp font.weight: Font.Medium diff --git a/ryoku/shell/quickshell/plugins/kit/Singletons/Scheme.qml b/ryoku/shell/quickshell/plugins/kit/Singletons/Scheme.qml index 7828737af3..07c12f7a46 100644 --- a/ryoku/shell/quickshell/plugins/kit/Singletons/Scheme.qml +++ b/ryoku/shell/quickshell/plugins/kit/Singletons/Scheme.qml @@ -25,6 +25,7 @@ Singleton { property bool matchWallpaper: true property var namedScheme: null property var wall: ({}) + property var preview: ({}) // A palette value is only usable when it is a non-empty hex string; a null, // missing or half-written role falls through to the next layer, so a partial @@ -34,6 +35,8 @@ Singleton { // Resolve one role through the layer chain: a selected named scheme wins, // then the live wallpaper palette while Match wallpaper is on, then the base. function role(key, base) { + if (usable(preview[key])) + return preview[key]; if (namedScheme && usable(namedScheme[key])) return namedScheme[key]; if (matchWallpaper && usable(wall[key])) @@ -104,6 +107,17 @@ Singleton { onFileChanged: reload() onLoaded: root.refreshWall() } + FileView { + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } FileView { id: shellFile path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/shell.json" diff --git a/ryoku/shell/quickshell/reload-cover/shell.qml b/ryoku/shell/quickshell/reload-cover/shell.qml index 3b62f3c4d0..817755d20c 100644 --- a/ryoku/shell/quickshell/reload-cover/shell.qml +++ b/ryoku/shell/quickshell/reload-cover/shell.qml @@ -6,10 +6,14 @@ import Quickshell.Io ShellRoot { id: root + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") readonly property string token: Quickshell.env("RYOKU_RELOAD_COVER_TOKEN") readonly property string mode: Quickshell.env("RYOKU_RELOAD_COVER_MODE") - property string phase: "closing" + // Login arrives behind the greeter's closed curtain. Map boot covers closed + // too; replaying the reload close would expose the half-built desktop. + property string phase: mode === "boot" ? "hold" : "closing" property bool startClose: false property bool finishQueued: false property int mappedCount: 0 @@ -33,7 +37,7 @@ ShellRoot { mappedOutputs[name] = true; mappedCount += 1; } - if (mappedCount >= Quickshell.screens.length) + if (mappedCount >= Quickshell.screens.length && phase === "closing") startClose = true; } function finish(value: string): bool { diff --git a/ryoku/shell/quickshell/ryopin/shell.qml b/ryoku/shell/quickshell/ryopin/shell.qml index f3d884a9f1..0afb41c370 100644 --- a/ryoku/shell/quickshell/ryopin/shell.qml +++ b/ryoku/shell/quickshell/ryopin/shell.qml @@ -7,7 +7,6 @@ import Ryoku.Ui.Singletons ShellRoot { id: root - property var pins: [] property bool framePainted: false @@ -74,7 +73,11 @@ ShellRoot { onFileChanged: reload() } - Component.onCompleted: root.relist() + Component.onCompleted: { + Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/"); + root.relist(); + } Variants { model: root.pins diff --git a/ryoku/shell/quickshell/ryoshot/Beautify.qml b/ryoku/shell/quickshell/ryoshot/Beautify.qml index f1cb1b3197..7c778faf69 100644 --- a/ryoku/shell/quickshell/ryoshot/Beautify.qml +++ b/ryoku/shell/quickshell/ryoshot/Beautify.qml @@ -61,7 +61,7 @@ Item { // (Ryoku Settings -> Shell -> Global). the stamped watermark and the editor // seals follow it; the user@host handle beside the watermark stays as-is. // empty/absent markText falls back to the 力 default, so this never seeds. - readonly property string mark: brandAdapter.markText.length > 0 ? brandAdapter.markText : "\u529b" + readonly property string mark: UiPreferences.brandMark(brandAdapter.markText) property bool composeOnly: false property string composeMode: "" property bool hd: false diff --git a/ryoku/shell/quickshell/ryoshot/Icon.qml b/ryoku/shell/quickshell/ryoshot/Icon.qml index bf0c990db2..713ff57fa7 100644 --- a/ryoku/shell/quickshell/ryoshot/Icon.qml +++ b/ryoku/shell/quickshell/ryoshot/Icon.qml @@ -47,7 +47,8 @@ Item { "mic": "M9 5a3 3 0 0 1 6 0v7a3 3 0 0 1 -6 0z M6 12a6 6 0 0 0 12 0 M12 18v3 M9 21h6", "keyboard": "M3 6h18v12H3z M6 9h1 M10 9h1 M14 9h1 M18 9h1 M6 12h1 M10 12h1 M14 12h1 M18 12h1 M7 15h10", "webcam": "M5 7h14v10H5z M15 12a3 3 0 1 1 -6 0a3 3 0 0 1 6 0z M12 17v3 M9 20h6", - "folder": "M3 6h7l2 2h9v11H3z" + "folder": "M3 6h7l2 2h9v11H3z", + "phone": "M6 2h12a2 2 0 0 1 2 2v16a2 2 0 0 1 -2 2H6a2 2 0 0 1 -2 -2V4a2 2 0 0 1 2 -2z M11 5h2 M12 18h.01" }) readonly property string d: defs[name] !== undefined ? defs[name] : "" diff --git a/ryoku/shell/quickshell/ryoshot/IconButton.qml b/ryoku/shell/quickshell/ryoshot/IconButton.qml index 247b16a413..4ec0552e59 100644 --- a/ryoku/shell/quickshell/ryoshot/IconButton.qml +++ b/ryoku/shell/quickshell/ryoshot/IconButton.qml @@ -29,8 +29,7 @@ Rectangle { id: ma anchors.fill: parent hoverEnabled: true - enabled: !btn.dim - onClicked: btn.clicked() + onClicked: if (!btn.dim) btn.clicked() } Timer { @@ -40,7 +39,7 @@ Rectangle { } onHoveredChanged: { - if (hovered && tooltip.length > 0 && !dim) + if (hovered && tooltip.length > 0) tipDelay.restart() else { tipDelay.stop() diff --git a/ryoku/shell/quickshell/ryoshot/PhonePopover.qml b/ryoku/shell/quickshell/ryoshot/PhonePopover.qml new file mode 100644 index 0000000000..40dcf4efb9 --- /dev/null +++ b/ryoku/shell/quickshell/ryoshot/PhonePopover.qml @@ -0,0 +1,101 @@ +import QtQuick +import QtQuick.Layouts +import "Singletons" +import Ryoku.Ui.Singletons + +Item { + id: pop + + property var devices: [] + + signal picked(string deviceId, string deviceName) + signal closeRequested() + + focus: visible + Keys.onEscapePressed: pop.closeRequested() + + readonly property int arrow: 7 + implicitWidth: card.implicitWidth + implicitHeight: card.implicitHeight + arrow + + Rectangle { + id: card + width: parent.width + height: parent.height - pop.arrow + radius: Theme.radius + color: Theme.panel + border.color: Theme.hair + border.width: 1 + implicitWidth: 230 + implicitHeight: body.implicitHeight + 24 + + ColumnLayout { + id: body + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 12 + spacing: 6 + + Text { + text: I18n.tr("Send to phone") + color: Theme.ink + font.family: Theme.ui + font.pixelSize: 13 + font.weight: Font.DemiBold + } + + Repeater { + model: pop.devices + + Rectangle { + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: 36 + radius: 6 + color: deviceMouse.containsMouse ? Theme.hover : "transparent" + + Text { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 + anchors.rightMargin: 8 + text: modelData.name + elide: Text.ElideRight + color: Theme.ink + font.family: Theme.ui + font.pixelSize: 12 + } + + MouseArea { + id: deviceMouse + anchors.fill: parent + hoverEnabled: true + onClicked: pop.picked(String(modelData.deviceId), String(modelData.name)) + } + } + } + } + } + + Canvas { + anchors.top: card.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: 14 + height: pop.arrow + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(width, 0); + ctx.lineTo(width / 2, height); + ctx.closePath(); + ctx.fillStyle = Theme.panel; + ctx.fill(); + } + } +} diff --git a/ryoku/shell/quickshell/ryoshot/ShortcutSheet.qml b/ryoku/shell/quickshell/ryoshot/ShortcutSheet.qml index 68ae21a516..708ab4524d 100644 --- a/ryoku/shell/quickshell/ryoshot/ShortcutSheet.qml +++ b/ryoku/shell/quickshell/ryoshot/ShortcutSheet.qml @@ -15,6 +15,7 @@ Item { { key: "Ctrl+S", label: I18n.tr("Save") }, { key: "Ctrl+P", label: I18n.tr("Pin") }, { key: "Ctrl+U", label: I18n.tr("Upload") }, + { key: "Ctrl+K", label: I18n.tr("Send to phone") }, { key: "Ctrl+B", label: I18n.tr("Beautify") }, { key: "Enter", label: I18n.tr("Copy and save") }, { key: "Ctrl+Z", label: I18n.tr("Undo") }, diff --git a/ryoku/shell/quickshell/ryoshot/Toolbar.qml b/ryoku/shell/quickshell/ryoshot/Toolbar.qml index 57d418e27a..9ac70987de 100644 --- a/ryoku/shell/quickshell/ryoshot/Toolbar.qml +++ b/ryoku/shell/quickshell/ryoshot/Toolbar.qml @@ -21,9 +21,14 @@ Item { property bool canUndo: false property bool canRedo: false property bool settingsOpen: false + property bool phoneAvailable: false + property var phoneDevices: [] + property bool phoneReady: false + property bool phoneBusy: false readonly property real colorCenterX: colorBtn.x + row.x + colorBtn.width / 2 readonly property real widthCenterX: widthBtn.x + row.x + widthBtn.width / 2 + readonly property real phoneCenterX: phoneBtn.x + row.x + phoneBtn.width / 2 readonly property real gearCenterX: gear.x + row.x + gear.width / 2 signal toolPicked(string tool) @@ -36,6 +41,7 @@ Item { signal copyRequested() signal saveRequested() signal uploadRequested() + signal phoneRequested() signal pinRequested() signal helpRequested() signal settingsRequested() @@ -45,7 +51,7 @@ Item { // the desktop brand seal, user-overridable via ~/.config/ryoku/brand.json // (Ryoku Settings -> Shell -> Global). the beautify button follows markText; // empty/absent falls back to the 力 default, so this never seeds. - readonly property string mark: brandAdapter.markText.length > 0 ? brandAdapter.markText : "\u529b" + readonly property string mark: UiPreferences.brandMark(brandAdapter.markText) FileView { id: brandFile path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/brand.json" @@ -204,6 +210,19 @@ Item { IconButton { icon: "save"; tooltip: I18n.tr("Save") + " (ctrl+s)"; onClicked: tb.saveRequested() } IconButton { icon: "pin"; tooltip: I18n.tr("Pin to desktop") + " (ctrl+p)"; onClicked: tb.pinRequested() } IconButton { icon: "upload"; tooltip: I18n.tr("Upload") + " (ctrl+u)"; onClicked: tb.uploadRequested() } + IconButton { + id: phoneBtn + visible: tb.phoneAvailable + icon: "phone" + dim: tb.phoneBusy || !tb.phoneReady || tb.phoneDevices.length === 0 + tooltip: tb.phoneBusy ? I18n.tr("Sending screenshot") + : !tb.phoneReady ? I18n.tr("Phone transfer is unavailable") + : tb.phoneDevices.length === 0 ? I18n.tr("No connected phone") + : tb.phoneDevices.length === 1 + ? I18n.tr("Send to %1").arg(tb.phoneDevices[0].name) + " (ctrl+k)" + : I18n.tr("Choose a phone") + " (ctrl+k)" + onClicked: tb.phoneRequested() + } Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 20; color: Theme.hair; Layout.leftMargin: 3; Layout.rightMargin: 3 } diff --git a/ryoku/shell/quickshell/ryoshot/shell.qml b/ryoku/shell/quickshell/ryoshot/shell.qml index 21e635b81a..c94d625af1 100644 --- a/ryoku/shell/quickshell/ryoshot/shell.qml +++ b/ryoku/shell/quickshell/ryoshot/shell.qml @@ -14,7 +14,6 @@ import "lib/constrain.js" as Constrain ShellRoot { id: root - property var globalSel: null property var pressPoint: null property bool capturing: false @@ -38,6 +37,23 @@ ShellRoot { property bool textEditing: false property string openPopover: "" property bool eyedropArmed: false + property bool phoneAvailable: false + property var phoneDevices: [] + property bool phoneBusy: false + property bool phoneProbeStarted: false + property bool phoneProbeFinished: false + property bool phoneTempReady: false + property string phoneFile: "" + property string phoneTargetName: "" + property string phoneTargetId: "" + property string phoneFailureTitle: "" + property string phoneFailureBody: "" + property int phoneSequence: 0 + + onPhaseChanged: { + if (phase === "editing" && !phoneProbeStarted) + root.refreshPhoneDevices(); + } property var selectedIndex: null property var moveOffset: null @@ -129,6 +145,8 @@ ShellRoot { readonly property string homeDir: Quickshell.env("HOME") readonly property string shotsDir: (Quickshell.env("XDG_PICTURES_DIR") || (homeDir + "/Pictures")) + "/Screenshots" readonly property string pinDir: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/ryoku/pins" + readonly property string runtimeDir: Quickshell.env("XDG_RUNTIME_DIR") || "" + readonly property string phoneTempDir: runtimeDir.length > 0 ? runtimeDir + "/ryoshot" : "" /** * Canonical tool list. The toolbar renders it, the key handler derives its @@ -249,7 +267,12 @@ ShellRoot { beautifySrc = path; phase = "beautify"; } - Component.onCompleted: if (openPath.length > 0) openForBeautify(openPath); + Component.onCompleted: { + Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/"); + if (openPath.length > 0) + openForBeautify(openPath); + } function beginSelection(gx, gy) { frontPressed = true; @@ -1034,6 +1057,94 @@ ShellRoot { }); } + function phoneDevicesFrom(text) { + var devices = []; + var lines = String(text).split("\n"); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + var separator = line.indexOf(" "); + if (separator < 1) continue; + var deviceId = line.substring(0, separator); + if (!/^[A-Za-z0-9_-]+$/.test(deviceId)) continue; + var deviceName = line.substring(separator + 1).trim(); + devices.push({ deviceId: deviceId, name: deviceName.length > 0 ? deviceName : deviceId }); + } + return devices; + } + + function refreshPhoneDevices() { + if (root.phoneProbeStarted) return; + root.phoneProbeStarted = true; + kdeStateProc.running = true; + } + + function doSendPhone() { + if (!root.phoneProbeFinished) { + root.notifySend(I18n.tr("Checking for connected phones"), ""); + return; + } + if (!root.phoneAvailable) { + root.notifySend(I18n.tr("KDE Connect is not installed"), ""); + return; + } + if (!root.phoneTempReady) { + root.notifySend(I18n.tr("Phone transfer is unavailable"), + I18n.tr("Could not prepare private temporary storage")); + return; + } + if (root.phoneDevices.length === 0) { + root.notifySend(I18n.tr("No connected phone"), ""); + return; + } + if (root.phoneDevices.length > 1) { + root.openPopover = root.openPopover === "phone" ? "" : "phone"; + return; + } + root.sendPhoneTo(root.phoneDevices[0].deviceId, root.phoneDevices[0].name); + } + + function sendPhoneTo(deviceId, deviceName) { + if (root.phoneBusy || root.exported) return; + root.openPopover = ""; + root.phoneBusy = true; + root.exported = true; + root.phoneTargetName = deviceName; + root.phoneTargetId = deviceId; + root.phoneSequence += 1; + root.phoneFile = root.phoneTempDir + "/capture-" + Date.now() + "-" + + root.phoneSequence + "-" + Math.floor(Math.random() * 0x100000000).toString(16) + ".png"; + grabTo(root.phoneFile, function (ok) { + if (!ok) { + root.failPhoneTransfer(I18n.tr("Could not send screenshot"), + I18n.tr("Could not create the temporary image")); + return; + } + root.shutter(); + phoneFileModeProc.run(root.phoneFile); + }); + } + + function failPhoneTransfer(title, body) { + root.phoneFailureTitle = title; + root.phoneFailureBody = body; + phoneCleanupProc.run(root.phoneFile); + } + + function deferPhoneCleanup(path, delaySeconds) { + if (path.length === 0 && root.phoneTempDir.length === 0) return; + Quickshell.execDetached(["python3", "-c", + "import os,sys,time\n" + + "time.sleep(float(sys.argv[2])); p=sys.argv[1]; d=sys.argv[3]\n" + + "if p:\n" + + " try: os.unlink(p)\n" + + " except FileNotFoundError: pass\n" + + "try: os.rmdir(d)\n" + + "except OSError: pass", + path, String(delaySeconds), root.phoneTempDir]); + } + + Component.onDestruction: root.deferPhoneCleanup(root.phoneFile, 0) + /** * Pins the finished shot: the PNG lands in the pin directory, the poke file * wakes an already running host, and the flock spawn starts one if there is @@ -1261,6 +1372,116 @@ ShellRoot { } } + Process { + id: kdeStateProc + stdout: StdioCollector { id: kdeStateOut } + stderr: StdioCollector {} + command: ["/usr/bin/env", "kdeconnect-cli", "-a", "--id-name-only"] + onExited: (code) => { + root.phoneProbeFinished = true; + root.phoneAvailable = code === 0; + root.phoneDevices = code === 0 ? root.phoneDevicesFrom(kdeStateOut.text) : []; + if (root.phoneAvailable && root.phoneTempDir.length > 0) + phoneDirCreateProc.run(); + } + } + + Process { + id: phoneDirCreateProc + stderr: StdioCollector {} + function run() { + command = ["mkdir", "-p", "-m", "700", "--", root.phoneTempDir]; + running = true; + } + onExited: (code) => { + if (code === 0) phoneDirModeProc.run(); + else root.phoneTempReady = false; + } + } + + Process { + id: phoneDirModeProc + stderr: StdioCollector {} + function run() { + command = ["chmod", "700", "--", root.phoneTempDir]; + running = true; + } + onExited: (code) => root.phoneTempReady = code === 0 + } + + Process { + id: phoneFileModeProc + stderr: StdioCollector {} + function run(file) { + command = ["chmod", "600", "--", file]; + running = true; + } + onExited: (code) => { + if (code === 0) + kdePhoneProc.run(root.phoneFile, root.phoneTargetId); + else + root.failPhoneTransfer(I18n.tr("Could not send screenshot"), + I18n.tr("Could not protect the temporary image")); + } + } + + Process { + id: kdePhoneProc + stdout: StdioCollector {} + stderr: StdioCollector {} + function run(file, targetId) { + if (targetId.length === 0) { + root.failPhoneTransfer(I18n.tr("Could not send screenshot"), + I18n.tr("The selected phone is no longer available")); + return; + } + command = ["/usr/bin/env", "kdeconnect-cli", "-d", targetId, "--share", file]; + running = true; + } + onExited: (code) => { + if (code === 0) { + var sentFile = root.phoneFile; + root.deferPhoneCleanup(sentFile, 15); + root.phoneFile = ""; + root.phoneBusy = false; + root.notifySend(I18n.tr("Sent via KDE Connect"), + I18n.tr("Sent to %1").arg(root.phoneTargetName)); + root.quitSoon(); + } else { + root.failPhoneTransfer(I18n.tr("Could not send screenshot"), + I18n.tr("KDE Connect could not reach %1").arg(root.phoneTargetName)); + } + } + } + + Process { + id: phoneCleanupProc + property string doomedFile: "" + stderr: StdioCollector {} + function run(file) { + doomedFile = file; + if (file.length === 0) { + finish(); + return; + } + command = ["rm", "-f", "--", file]; + running = true; + } + function finish() { + root.phoneFile = ""; + root.phoneBusy = false; + root.exported = false; + root.notifySend(root.phoneFailureTitle, root.phoneFailureBody); + root.phoneFailureTitle = ""; + root.phoneFailureBody = ""; + doomedFile = ""; + } + onExited: (code) => { + if (code !== 0) root.deferPhoneCleanup(doomedFile, 0); + finish(); + } + } + // Upload the crop, then open a reverse-image search on the hosted URL. Bing is // the default engine iRiS settled on (Google and Yandex paste endpoints broke). Process { @@ -1345,6 +1566,7 @@ ShellRoot { if (e.key === Qt.Key_S) { root.exportSaveOrStyled(); return true; } if (e.key === Qt.Key_P) { root.doPin(); return true; } if (e.key === Qt.Key_U) { root.doUpload(); return true; } + if (e.key === Qt.Key_K) { root.doSendPhone(); return true; } if (e.key === Qt.Key_B) { root.openBeautify(); return true; } if (e.key === Qt.Key_Z) { if (shift) root.redo(); else root.undo(); return true; } if (e.key === Qt.Key_Y) { root.redo(); return true; } @@ -1523,6 +1745,10 @@ ShellRoot { canUndo: { root.annRevision; return root.model ? root.model.canUndo() : false; } canRedo: { root.annRevision; return root.model ? root.model.canRedo() : false; } settingsOpen: root.settingsOpen + phoneAvailable: root.phoneAvailable + phoneDevices: root.phoneDevices + phoneReady: root.phoneTempReady + phoneBusy: root.phoneBusy x: { if (!win.selLocal) return 0; @@ -1546,6 +1772,7 @@ ShellRoot { onCopyRequested: root.exportCopyOrStyled() onSaveRequested: root.exportSaveOrStyled() onUploadRequested: root.doUpload() + onPhoneRequested: root.doSendPhone() onPinRequested: root.doPin() onHelpRequested: root.shortcutsOpen = !root.shortcutsOpen onSettingsRequested: root.settingsOpen = !root.settingsOpen @@ -1575,6 +1802,16 @@ ShellRoot { onCloseRequested: root.openPopover = "" } + PhonePopover { + visible: toolbar.visible && root.openPopover === "phone" + devices: root.phoneDevices + x: Math.max(8, Math.min(toolbar.x + toolbar.phoneCenterX - width / 2, + win.width - width - 8)) + y: toolbar.y - height - 6 + onPicked: (deviceId, deviceName) => root.sendPhoneTo(deviceId, deviceName) + onCloseRequested: root.openPopover = "" + } + SettingsPanel { id: hotkeyPopover visible: toolbar.visible && root.settingsOpen diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/BluetoothPage.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/BluetoothPage.qml index a0878c67e3..38db47a6e3 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/BluetoothPage.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/BluetoothPage.qml @@ -131,7 +131,7 @@ Column { } K.QsSwitch { anchors { right: parent.right; verticalCenter: parent.verticalCenter } - visible: page.adapter !== null + visible: BluetoothPower.available value: page.adapter ? page.adapter.enabled : false onToggled: v => BluetoothPower.setEnabled(v) } @@ -149,7 +149,7 @@ Column { Text { visible: !page.btOn - text: I18n.tr("Bluetooth is off") + text: BluetoothPower.blocked ? I18n.tr("Bluetooth is blocked") : I18n.tr("Bluetooth is off") color: page.dim(0.55) font.family: Theme.fontPrimary font.pixelSize: 13 diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/HomePage.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/HomePage.qml index 1e7cf72b51..e383d40a46 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/HomePage.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/kairos/quicksettings/pages/HomePage.qml @@ -30,6 +30,7 @@ Column { return I18n.tr("Not connected"); } readonly property string btSubtitle: { + if (BluetoothPower.blocked) return I18n.tr("Blocked"); if (!page.adapter) return I18n.tr("Not available"); if (!page.adapter.enabled) return I18n.tr("Off"); var devs = Bluetooth.devices ? Bluetooth.devices.values : []; diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/Host.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/Host.qml index b4f96e00cc..57b1647b75 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/Host.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/Host.qml @@ -42,8 +42,9 @@ Item { readonly property var osdSourceAudio: osdSource && osdSource.audio ? osdSource.audio : null property bool osdBridgeReady: false - function showAudioOsd(audio, microphone) { + function showAudioOsd(audio, microphone, volumeChange) { if (!osdBridgeReady || !audio) return + if (microphone && volumeChange && !Ryoku.OsdFeed.acceptsMicVolume(audio.volume)) return var percent = Math.round(Math.max(0, Math.min(1, audio.volume)) * 100) shell.summon("omarchy.osd", JSON.stringify({ icon: microphone ? (audio.muted ? "microphone-muted" : "microphone") @@ -60,16 +61,17 @@ Item { Timer { interval: 700; running: true; onTriggered: shell.osdBridgeReady = true } Connections { target: shell.osdSinkAudio - function onVolumesChanged() { shell.showAudioOsd(shell.osdSinkAudio, false) } - function onMutedChanged() { shell.showAudioOsd(shell.osdSinkAudio, false) } + function onVolumesChanged() { shell.showAudioOsd(shell.osdSinkAudio, false, true) } + function onMutedChanged() { shell.showAudioOsd(shell.osdSinkAudio, false, false) } } Connections { target: shell.osdSourceAudio - function onVolumesChanged() { shell.showAudioOsd(shell.osdSourceAudio, true) } - function onMutedChanged() { shell.showAudioOsd(shell.osdSourceAudio, true) } + function onVolumesChanged() { shell.showAudioOsd(shell.osdSourceAudio, true, true) } + function onMutedChanged() { shell.showAudioOsd(shell.osdSourceAudio, true, false) } } Connections { target: Ryoku.OsdFeed + function onMicRequested() { shell.showAudioOsd(shell.osdSourceAudio, true, false) } function onBrightnessSeqChanged() { if (!shell.osdBridgeReady) return var percent = Math.round(Ryoku.OsdFeed.brightness * 100) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/bar/widgets/Microphone.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/bar/widgets/Microphone.qml index 8630646202..4d4907a908 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/bar/widgets/Microphone.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/bar/widgets/Microphone.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Services.Pipewire import qs.Ui import Ryoku.Ui.Singletons +import shell.services as Ryoku BarWidget { id: root @@ -49,7 +50,9 @@ BarWidget { onWheelMoved: function(delta) { if (!root.source || !root.source.audio) return var step = 0.05 - root.source.audio.volume = Math.max(0, Math.min(1, root.volume + (delta > 0 ? step : -step))) + var value = Math.max(0, Math.min(1, root.volume + (delta > 0 ? step : -step))) + Ryoku.OsdFeed.expectMicVolume(value) + root.source.audio.volume = value } } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/panels/audio/Panel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/panels/audio/Panel.qml index a00a29ef6c..b24cc129c9 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/panels/audio/Panel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/nomarchy/plugins/panels/audio/Panel.qml @@ -7,6 +7,7 @@ import Quickshell.Services.Pipewire import qs.Ui import qs.Commons import "Model.js" as Model +import shell.services as Ryoku Panel { id: root @@ -454,7 +455,9 @@ Panel { function setInputVolume(v) { if (!source || !source.audio) return - source.audio.volume = Math.max(0, Math.min(1, v)) + var value = Math.max(0, Math.min(1, v)) + Ryoku.OsdFeed.expectMicVolume(value) + source.audio.volume = value } function toggleOutputMute() { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/cards/ControlsCard.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/cards/ControlsCard.qml index 60e5a106db..cc062d6cf6 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/cards/ControlsCard.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/cards/ControlsCard.qml @@ -95,9 +95,9 @@ Item { K.Switch { checked: root.currentPage === "wifi" ? Network.wifiRadio - : (!!Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) + : BluetoothPower.enabled enabled: root.currentPage === "wifi" - ? Network.wifiPresent : !!Bluetooth.defaultAdapter + ? Network.wifiPresent : BluetoothPower.available onToggled: value => { if (root.currentPage === "wifi") Network.setWifiEnabled(value); diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/modules/workspaces/Strip.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/modules/workspaces/Strip.qml index 6bfc260658..53a8cd9327 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/modules/workspaces/Strip.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/modules/workspaces/Strip.qml @@ -29,8 +29,9 @@ Item { return square ? Math.min(3 * u, height / 2) : height / 2; } readonly property color occupiedColor: Theme.blend(K.Style.tint16, K.Style.base) - readonly property string mode: ["dots", "numbers", "kanji", "greek", "dashes", "icons"].indexOf(style) >= 0 + readonly property string requestedMode: ["dots", "numbers", "kanji", "greek", "dashes", "icons"].indexOf(style) >= 0 ? style : "dots" + readonly property string mode: requestedMode === "kanji" && !UiPreferences.showJapaneseGloss ? "numbers" : requestedMode readonly property int activeIndex: { const rows = model || []; for (let index = 0; index < rows.length; ++index) { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/SettingsWindow.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/SettingsWindow.qml index ba65454355..b1d869db85 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/SettingsWindow.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/SettingsWindow.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import Ryoku.Blobs +import Ryoku.Ui.Singletons import shell.barkit as Pill import "../kit" as K import "pages" as P @@ -425,7 +426,7 @@ Scope { anchors.right: parent.right anchors.rightMargin: K.Style.s3 anchors.verticalCenter: parent.verticalCenter - text: navItem.modelData.kana + text: UiPreferences.text(navItem.modelData.kana, "") color: navItem.active ? K.Style.inkOnBone : K.Style.inkFaint font.family: K.Style.jp font.pixelSize: 12 diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/pages/PageHead.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/pages/PageHead.qml index 42d8fe5b97..93642f536f 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/pages/PageHead.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/orpheus/settings/pages/PageHead.qml @@ -1,5 +1,6 @@ import QtQuick import "../../kit" as K +import Ryoku.Ui.Singletons Column { id: head @@ -20,7 +21,7 @@ Column { } K.Label { - text: "力" + text: UiPreferences.brandMark("力") role: "micro" tone: "accent" font.family: K.Style.jp diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/bar/faces/vis/VisFace.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/bar/faces/vis/VisFace.qml index a7313dc81a..7024ec9529 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/bar/faces/vis/VisFace.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/bar/faces/vis/VisFace.qml @@ -4,6 +4,7 @@ import QtQuick.Window import QtQuick.Controls import Quickshell import Quickshell.Io +import "../../../../../../visualizer/Singletons" as VizCfg import "../../../reusables" import "../../../" @@ -142,7 +143,9 @@ Item { ? "start" : ((root.visAlignment === "bottom" || root.visAlignment === "right") ? "end" : "center") count: root.visContinuous ? root.sampleCount : root.barCount - color: root.isCompact ? Qt.lighter(ThemeBackend.mauve, 1.08) : ThemeBackend.mauve + color: VizCfg.Config.hasCustomColor ? VizCfg.Config.customColor + : (root.isCompact ? Qt.lighter(ThemeBackend.mauve, 1.08) : ThemeBackend.mauve) + gain: VizCfg.Config.gain previewDemo: Boolean(root.activeTarget && root.activeTarget.isPreview) // Bars: low frequencies first. Wave: low frequencies in the middle, mirrored diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/dock/Dock.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/dock/Dock.qml index 2af38a3a0f..6481a8c3b2 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/dock/Dock.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/dock/Dock.qml @@ -101,10 +101,11 @@ Variants { screen: dockScope.modelData WlrLayershell.namespace: "qs-dock" - // While the Stage Editor frames this monitor the desktop lifts - // to Top; the dock rides Overlay so it stays visible and - // clickable above it, like the other designs. - WlrLayershell.layer: universalDock.stageEditing ? WlrLayer.Overlay + // Shima's full-output chassis occupies Top and owns its frame. + // Its transparent edge mask otherwise receives clicks before + // this dock when the chassis loads later. + WlrLayershell.layer: universalDock.stageEditing || Ryoku.Config.barStyle === "iris" + ? WlrLayer.Overlay : (dockWindow.dockOnTop || dockWindow.editMode) ? WlrLayer.Top : WlrLayer.Bottom focusable: dockWindow.editMode color: "transparent" @@ -1010,7 +1011,7 @@ Variants { function launchApp(desktopId) { const entry = Ryoku.Dock.entryFor(desktopId); - if (entry) AppLaunch.run(entry, null); + if (entry) Ryoku.AppLaunch.run(entry, null); else Ryoku.Dock.activate(desktopId); } @@ -1586,7 +1587,7 @@ Variants { property bool isBeingDragged: btnMa.drag.active property int btnSize: dockWindow.s(dockWindow.dockElementSize) - readonly property var runningInfo: dockWindow.getRunningInfoForApp(model.desktop_id) + readonly property var runningInfo: dockWindow.getRunningInfoForApp(dockButton.desktop_id) readonly property bool isAppRunning: runningInfo !== null && runningInfo.hasWindows readonly property bool isAppHere: runningInfo !== null && runningInfo.count > 0 readonly property bool isAppFocused: runningInfo !== null && runningInfo.isFocused @@ -2021,7 +2022,7 @@ Variants { dockButton.flashOpacity = 0.4; btnFlashAnim.start(); if (typeof Sounds !== "undefined") Sounds.playSfx("reusables/iconbutton/click.wav"); - dockWindow.launchApp(model.desktop_id); + dockWindow.launchApp(dockButton.desktop_id); return; } @@ -2029,16 +2030,16 @@ Variants { let pt = dockButton.mapToItem(null, 0, 0); let globX = pt.x + (dockButton.width / 2); let globY = pt.y + (dockButton.height / 2); - let info = dockWindow.getRunningInfoForApp(model.desktop_id); + let info = dockWindow.getRunningInfoForApp(dockButton.desktop_id); let winList = dockWindow.menuWindows(info); DockAppMenuController.toggle( dockWindow.screen, globX, globY, dockWindow.dockPosition, - model.desktop_id, - model.name || (info ? info.appId : ""), - model.icon || "", + dockButton.desktop_id, + dockButton.name || (info ? info.appId : ""), + dockButton.icon || "", true, winList, dockButton.width @@ -2047,20 +2048,20 @@ Variants { } if (mouse.button === Qt.LeftButton) { - if (typeof DockAppMenuController !== "undefined" && DockAppMenuController.isVisible && !DockAppMenuController.isSameApp(DockAppMenuController.desktopId, model.desktop_id)) { + if (typeof DockAppMenuController !== "undefined" && DockAppMenuController.isVisible && !DockAppMenuController.isSameApp(DockAppMenuController.desktopId, dockButton.desktop_id)) { let pt = dockButton.mapToItem(null, 0, 0); let globX = pt.x + (dockButton.width / 2); let globY = pt.y + (dockButton.height / 2); - let info = dockWindow.getRunningInfoForApp(model.desktop_id); + let info = dockWindow.getRunningInfoForApp(dockButton.desktop_id); let winList = dockWindow.menuWindows(info); DockAppMenuController.show( dockWindow.screen, globX, globY, dockWindow.dockPosition, - model.desktop_id, - model.name || (info ? info.appId : ""), - model.icon || "", + dockButton.desktop_id, + dockButton.name || (info ? info.appId : ""), + dockButton.icon || "", true, winList, dockButton.width @@ -2068,7 +2069,7 @@ Variants { return; } - if (typeof DockAppMenuController !== "undefined" && DockAppMenuController.isVisible && DockAppMenuController.isSameApp(DockAppMenuController.desktopId, model.desktop_id)) { + if (typeof DockAppMenuController !== "undefined" && DockAppMenuController.isVisible && DockAppMenuController.isSameApp(DockAppMenuController.desktopId, dockButton.desktop_id)) { DockAppMenuController.hide(); } @@ -2076,7 +2077,7 @@ Variants { dockButton.flashOpacity = 0.4; btnFlashAnim.start(); if (typeof Sounds !== "undefined") Sounds.playSfx("reusables/iconbutton/click.wav"); - dockWindow.activateOrLaunch(model.desktop_id, mouse.modifiers); + dockWindow.activateOrLaunch(dockButton.desktop_id, mouse.modifiers); } } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/NotificationsTab.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/NotificationsTab.qml index 39a8ce6567..568f464039 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/NotificationsTab.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/NotificationsTab.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Io import Ryoku.Ui.Singletons +import shell.services as RyokuServices import "../../" import "../../reusables" import "../../reusables/guide" @@ -77,7 +78,7 @@ Item { return notificationsTabRoot.defaultNotificationSettings; } - property bool dnd: notifSettings && notifSettings.dnd !== undefined ? notifSettings.dnd : false + readonly property bool dnd: RyokuServices.Flags.dnd property string position: notifSettings && notifSettings.position !== undefined ? notifSettings.position : "top right" property int horizontalPosition: { if (position !== "custom") { @@ -96,8 +97,8 @@ Item { if (notifSettings && notifSettings.verticalPosition !== undefined) return notifSettings.verticalPosition; return presetTopV; } - property bool soundEnabled: notifSettings && notifSettings.sound !== undefined ? notifSettings.sound : true - property string selectedSound: notifSettings && notifSettings.soundFile !== undefined ? notifSettings.soundFile : "" + readonly property bool soundEnabled: Config.hostNotificationSoundEnabled + readonly property string selectedSound: Config.hostNotificationSoundFile property bool showEmptyGraphic: notifSettings && notifSettings.showEmptyGraphic !== undefined ? notifSettings.showEmptyGraphic : true property bool rightClickClose: notifSettings && notifSettings.rightClickClose !== undefined ? notifSettings.rightClickClose : true @@ -174,7 +175,6 @@ Item { let s = (typeof Config !== "undefined" && typeof Config.getSetting === "function") ? Config.getSetting("notifications", notificationsTabRoot.defaultNotificationSettings) : notificationsTabRoot.defaultNotificationSettings; - notificationsTabRoot.dnd = s.dnd !== undefined ? s.dnd : false; notificationsTabRoot.position = s.position !== undefined ? s.position : "top right"; if (notificationsTabRoot.position !== "custom") { notificationsTabRoot.horizontalPosition = notificationsTabRoot.position.indexOf("left") !== -1 ? presetLeftH : (notificationsTabRoot.position.indexOf("center") !== -1 ? presetCenterH : presetRightH); @@ -187,8 +187,6 @@ Item { screenSelector.horizontalPosition = notificationsTabRoot.horizontalPosition; screenSelector.verticalPosition = notificationsTabRoot.verticalPosition; } - notificationsTabRoot.soundEnabled = s.sound !== undefined ? s.sound : true; - notificationsTabRoot.selectedSound = s.soundFile !== undefined ? s.soundFile : ""; notificationsTabRoot.showEmptyGraphic = s.showEmptyGraphic !== undefined ? s.showEmptyGraphic : true; notificationsTabRoot.rightClickClose = s.rightClickClose !== undefined ? s.rightClickClose : true; notificationsTabRoot.notifSettings = s; @@ -240,37 +238,34 @@ Item { } stdout: StdioCollector { onStreamFinished: { + let defaults = [{ + "name": "Ryoku " + I18n.t("widgets.variants.default"), + "path": "" + }]; try { let txt = this.text.trim(); - if (txt !== "") { - let list = JSON.parse(txt); - if (Array.isArray(list)) { - notificationsTabRoot.soundList = list; - let found = false; - for (let i = 0; i < list.length; i++) { - if (list[i].path === notificationsTabRoot.selectedSound || list[i].name === notificationsTabRoot.selectedSound) { - notificationsTabRoot.selectedSound = list[i].path; - notificationsTabRoot.updateNotifSetting("soundFile", list[i].path); - found = true; - break; - } - } - if (!found) { - if (list.length > 0) { - notificationsTabRoot.selectedSound = list[0].path; - notificationsTabRoot.updateNotifSetting("soundFile", list[0].path); - } else { - notificationsTabRoot.selectedSound = ""; - notificationsTabRoot.updateNotifSetting("soundFile", ""); - } - } + let list = txt !== "" ? JSON.parse(txt) : []; + if (!Array.isArray(list)) list = []; + list.unshift(defaults[0]); + const selected = notificationsTabRoot.selectedSound; + let found = selected === ""; + for (let i = 1; i < list.length; i++) { + if (list[i].path === selected || list[i].name === selected) { + found = true; + break; } - } else { - notificationsTabRoot.soundList = []; - notificationsTabRoot.selectedSound = ""; - notificationsTabRoot.updateNotifSetting("soundFile", ""); } - } catch (e) {} + if (!found) { + let parts = selected.split("/"); + list.push({ + "name": parts[parts.length - 1] || selected, + "path": selected + }); + } + notificationsTabRoot.soundList = list; + } catch (e) { + notificationsTabRoot.soundList = defaults; + } } } } @@ -303,12 +298,9 @@ Item { let idx = out.indexOf('|'); let sPath = idx !== -1 ? out.substring(idx + 1).trim() : ""; if (sPath !== "") { - notificationsTabRoot.selectedSound = sPath; - notificationsTabRoot.updateNotifSetting("soundFile", sPath); + Config.setNotificationSoundFile(sPath); notificationsTabRoot.reloadSounds(); - if (typeof Sounds !== "undefined" && typeof Sounds.play === "function") { - Sounds.play(sPath); - } + Config.previewNotificationSound(sPath); } } soundPickerPopup.close(); @@ -358,6 +350,9 @@ Item { function onSettingsLoaded() { notificationsTabRoot.syncSettings(); } + function onHostNotificationSoundFileChanged() { + notificationsTabRoot.reloadSoundsDebounceTimer.restart(); + } } Flickable { @@ -725,8 +720,7 @@ Item { handleColor: ThemeBackend.crust handleOffColor: ThemeBackend.text onToggled: function(c) { - notificationsTabRoot.dnd = c; - notificationsTabRoot.updateNotifSetting("dnd", c); + RyokuServices.Flags.dnd = c; } } } @@ -787,8 +781,7 @@ Item { handleColor: ThemeBackend.crust handleOffColor: ThemeBackend.text onToggled: function(c) { - notificationsTabRoot.soundEnabled = c; - notificationsTabRoot.updateNotifSetting("sound", c); + Config.setNotificationSoundEnabled(c); } } } @@ -854,11 +847,8 @@ Item { onSelected: function(index, value) { if (index >= 0 && index < notificationsTabRoot.soundList.length) { let targetPath = notificationsTabRoot.soundList[index].path; - notificationsTabRoot.selectedSound = targetPath; - notificationsTabRoot.updateNotifSetting("soundFile", targetPath); - if (typeof Sounds !== "undefined" && typeof Sounds.play === "function") { - Sounds.play(targetPath); - } + Config.setNotificationSoundFile(targetPath); + Config.previewNotificationSound(targetPath); } } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/SoundPicker.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/SoundPicker.qml index b07874fa10..4c8b20a10e 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/SoundPicker.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/guide/notifications/SoundPicker.qml @@ -133,9 +133,7 @@ FilePicker { enabled: soundPickerRoot.selectedFilePath !== "" opacity: enabled ? 1.0 : 0.4 onClicked: { - if (typeof Sounds !== "undefined" && typeof Sounds.play === "function") { - Sounds.play(soundPickerRoot.selectedFilePath); - } + Config.previewNotificationSound(soundPickerRoot.selectedFilePath); } } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/network/NetworkPopup.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/network/NetworkPopup.qml index cbe957b4a6..edaf919abd 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/network/NetworkPopup.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/network/NetworkPopup.qml @@ -231,6 +231,15 @@ Item { window.rebuildBtData(false); } } + Connections { + target: BluetoothPower + function onAvailableChanged() { + window.rebuildBtData(false); + } + function onBlockedChanged() { + window.rebuildBtData(false); + } + } Connections { target: Bluetooth.defaultAdapter || null ignoreUnknownSignals: true @@ -1294,7 +1303,8 @@ Item { let adapter = Bluetooth.defaultAdapter; if (!adapter) { window.btMissCount++; - if (window.btMissCount >= 2) window.btPresent = false; + window.btPresent = BluetoothPower.available; + window.btPower = "off"; if (!isCache) validateActiveMode(); return; } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/Notification.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/Notification.qml index 8f424401e9..2322433e14 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/Notification.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/Notification.qml @@ -3,6 +3,7 @@ import Ryoku.Ui.Singletons as RyokuUi import QtQuick.Layouts import QtQuick.Controls import Quickshell +import shell.services as RyokuServices import "../" import "../reusables" @@ -60,19 +61,18 @@ Rectangle { property bool _soundPlayed: false function playPopupSound() { - if (_soundPlayed) return; - if (!typeRoot.isPopup) return; + if (_soundPlayed || !typeRoot.isPopup || RyokuServices.Flags.dnd + || !Config.hostNotificationSoundEnabledFor(typeRoot.urgency === 2)) return; if (Math.abs(Date.now() - typeRoot.timestamp) > 3000) return; - let cfg = Config.getSetting("notifications", { "sound": true, "soundFile": "", "dnd": false }); - if (cfg.dnd === true) return; - let soundEnabled = cfg.sound !== undefined ? cfg.sound : true; - let soundFile = cfg.soundFile !== undefined ? cfg.soundFile : ""; - if (!soundEnabled || soundFile === "") return; - - if (typeof Sounds !== "undefined") { - _soundPlayed = true; - Sounds.play(soundFile); - } + const n = delegateWrapper ? delegateWrapper.realNotif : null; + const hints = n && n.hints ? n.hints : {}; + if (hints["suppress-sound"] === true) return; + _soundPlayed = true; + Quickshell.execDetached([ + "ryoku-shell", + "sound", + typeRoot.urgency === 2 ? "notification-critical" : "notification" + ]); } onDelegateWrapperChanged: Qt.callLater(playPopupSound) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/NotificationPopups.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/NotificationPopups.qml index 06e2d91bf0..adcb25c9ea 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/NotificationPopups.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/notifications/NotificationPopups.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Ryoku.Ui.Singletons +import shell.services as RyokuServices import "../" import "../reusables" @@ -109,16 +110,12 @@ PanelWindow { item: popupContainer } - property bool dndEnabled: { - let n = Config.getSetting("notifications", { "dnd": false, "position": "top right" }); - return Boolean(n && n.dnd); - } + readonly property bool dndEnabled: RyokuServices.Flags.dnd Connections { target: Config function onSettingsLoaded() { - let n = Config.getSetting("notifications", { "dnd": false, "position": "top right" }); - popupWindow.dndEnabled = Boolean(n && n.dnd); + let n = Config.getSetting("notifications", { "position": "top right" }); popupWindow.position = (n && n.position !== undefined) ? n.position : "top right"; popupWindow.horizontalPosition = (n && n.horizontalPosition !== undefined) ? n.horizontalPosition : 95; popupWindow.verticalPosition = (n && n.verticalPosition !== undefined) ? n.verticalPosition : 5; diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/reusables/media/Visualizer.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/reusables/media/Visualizer.qml index cad7c450f4..34a45dfed3 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/reusables/media/Visualizer.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/reusables/media/Visualizer.qml @@ -14,6 +14,7 @@ Item { property string alignment: "end" property int count: 16 property color color: ThemeBackend.mauve + property real gain: 1.0 // Frequency mapping: low frequencies first, or in the middle and mirrored to both sides property bool mirrored: true @@ -87,7 +88,7 @@ Item { for (let i = 0; i < map.length; i++) { let v0 = source[map[i].idx0] || 0.0; let v1 = source[map[i].idx1] || 0.0; - let raw = v0 + (v1 - v0) * map[i].frac; + let raw = Math.min(1.0, (v0 + (v1 - v0) * map[i].frac) * root.gain); let val = raw < threshold ? 0.0 : Math.pow((raw - threshold) / (1 - threshold), gamma); out[i] = Math.max(0.0, Math.min(1.0, val)); diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml index a2ca1c49ee..25a73377c2 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/singletons/system/Config.qml @@ -20,6 +20,16 @@ Item { readonly property var defaultSettings: ({}) property bool dataReady: true property var rawSettings: ({}) + property int hostSettingsRevision: 0 + readonly property bool hostNotificationSoundEnabled: { + const revision = hostSettingsRevision; + return hostNotificationSoundEnabledFor(false); + } + readonly property string hostNotificationSoundFile: { + const revision = hostSettingsRevision; + return String(hostSetting("sounds.events.notification.file", "") || ""); + } + signal settingsLoaded() @@ -92,6 +102,52 @@ Item { } cur[parts[parts.length - 1]] = value; } + function nestedValue(obj, path, fallbackValue) { + let parts = typeof path === "string" ? path.split(".") : [path]; + let cur = obj; + for (let i = 0; i < parts.length; i++) { + if (!cur || typeof cur !== "object" || !cur.hasOwnProperty(parts[i])) { + return fallbackValue; + } + cur = cur[parts[i]]; + } + return cur !== undefined && cur !== null ? cur : fallbackValue; + } + + function hostSetting(path, fallbackValue) { + const soundsPath = String(path || "").replace(/^sounds\./, ""); + return nestedValue(Config.sounds || ({}), soundsPath, fallbackValue); + } + function hostNotificationSoundEnabledFor(critical) { + const event = critical ? "notification-critical" : "notification"; + return Boolean(hostSetting("sounds.enabled", true) + && hostSetting("sounds.events." + event + ".enabled", true)); + } + + + function setHostSetting(path, value) { + patchQueue(path, value); + } + + function setNotificationSoundEnabled(enabled) { + if (enabled) setHostSetting("sounds.enabled", true); + setHostSetting("sounds.events.notification.enabled", enabled); + setHostSetting("sounds.events.notification-critical.enabled", enabled); + } + + function setNotificationSoundFile(filePath) { + const path = String(filePath || "").replace(/^file:\/\//, ""); + setHostSetting("sounds.events.notification.file", path); + setHostSetting("sounds.events.notification-critical.file", path); + } + + function previewNotificationSound(filePath) { + callQueue("sounds.preview", { + "event": "notification", + "file": String(filePath || "").replace(/^file:\/\//, "") + }); + } + function sh(cmd) { Quickshell.execDetached(["bash", "-c", cmd]); @@ -163,6 +219,12 @@ Item { readonly property string sockPath: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/ryoku-shell.sock" + function callQueue(method, payload) { + ctl.queued += "call " + method + " " + JSON.stringify(payload || {}) + "\n"; + if (ctl.connected) ctl.flushQueued(); + else ctl.connected = true; + } + function patchQueue(path, value) { ctl.queued += "call settings.patch " + JSON.stringify({ path: path, value: value }) + "\n"; @@ -181,10 +243,12 @@ Item { onConnectionStateChanged: if (connected) flushQueued() } + Connections { target: Config function onPythonChanged() { config.pull(); } function onFontFamilyChanged() { config.pull(); } + function onSettingsLoaded() { config.hostSettingsRevision += 1; } } Component.onCompleted: { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFace.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFace.qml index 84ba694c74..85a8412e2d 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFace.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFace.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Io +import "../../../../../../visualizer/Singletons" as VizCfg import "../../../reusables" import "../../../" @@ -48,6 +49,8 @@ Item { Visualizer { anchors.fill: parent active: root.isSubscribed + color: VizCfg.Config.hasCustomColor ? VizCfg.Config.customColor : ThemeBackend.mauve + gain: VizCfg.Config.gain count: root.activeBars spacing: root.barSpacing maxLength: height * 0.96 diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFaceContinuous.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFaceContinuous.qml index 0b60c01f68..7e14cd6706 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFaceContinuous.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/python/widgets/faces/visualizer/VisualizerFaceContinuous.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts import QtQuick.Shapes import Quickshell import Quickshell.Io +import "../../../../../../visualizer/Singletons" as VizCfg import "../../../reusables" import "../../../" @@ -46,6 +47,8 @@ Item { anchors.fill: parent active: root.isSubscribed continuous: true + color: VizCfg.Config.hasCustomColor ? VizCfg.Config.customColor : ThemeBackend.mauve + gain: VizCfg.Config.gain count: 64 rise: 0.25 fall: 0.12 diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml index 866d74a047..14c982955e 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml @@ -16,6 +16,7 @@ Item { property bool omarchyCurrentRootResolved: false readonly property string themeNamePath: omarchyCurrentRoot + "/theme.name" readonly property string colorsPath: (Quickshell.env("XDG_CACHE_HOME") || (Quickshell.env("HOME") + "/.cache")) + "/ryoku/colors.json" + readonly property string previewColorsPath: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" readonly property string shellConfigPath: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/shell.json" readonly property string themeJsonPath: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/theme.json" // Palette resolution state (the desktop Scheme's chain): the live wallpaper @@ -23,6 +24,7 @@ Item { // compiled base captured before the daemon's first write so turning the // wallpaper off reverts to the shipped look. property var _wallColors: ({}) + property var _previewColors: ({}) property var _namedPalette: null property bool _followWallpaper: true property var _basePalette: null @@ -2796,11 +2798,13 @@ Item { // omarchy's U+E900 needs a font Ryoku doesn't ship, so it's absent here and // any stale "omarchy" icon id falls back to ryoku via the validators below. function launcherLogoIconGlyph(id) { + if (id === "ryoku") + return UiPreferences.brandMark("力") var c = launcherLogoIconCodes[id] - return String.fromCodePoint(c !== undefined ? c : launcherLogoIconCodes["ryoku"]) + return c !== undefined ? String.fromCodePoint(c) : UiPreferences.brandMark("力") } function launcherLogoIconFont(id) { - return id === "ryoku" ? "Noto Sans CJK JP" : mono + return id === "ryoku" && UiPreferences.showJapaneseGloss ? "Noto Sans CJK JP" : mono } function launcherLogoIconSize(id) { if (id === "arch") return 17 @@ -3385,7 +3389,10 @@ Item { function _applyPalette() { theme._captureBase(); - Palette.applyResolved(theme, theme._basePalette, theme._namedPalette, theme._wallColors, theme._followWallpaper); + Palette.applyResolved(theme, theme._basePalette, + Object.keys(theme._previewColors).length ? null : theme._namedPalette, + Object.keys(theme._previewColors).length ? theme._previewColors : theme._wallColors, + Object.keys(theme._previewColors).length ? true : theme._followWallpaper); } Process { @@ -3409,6 +3416,19 @@ Item { onFileChanged: { paletteWatcher.reload(); theme.reloadCurrentThemeFiles() } } + FileView { + id: previewWatcher + path: theme.previewColorsPath + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + theme._previewColors = Palette.parseAll(text()) || {}; + theme._applyPalette(); + } + } + // A fixed named theme publishes its palette here; retint when it changes. FileView { id: namedPaletteWatcher diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcHead.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcHead.qml index e9423f5fbe..d6e77f65b4 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcHead.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcHead.qml @@ -60,7 +60,7 @@ Item { anchors.left: nameText.right anchors.leftMargin: head.tk ? head.tk.gap : 12 anchors.baseline: nameText.baseline - text: head.gloss + text: UiPreferences.text(head.gloss, "") color: Tokens.inkFaint font.family: Tokens.jp font.pixelSize: Tokens.fBody diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcRail.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcRail.qml index 5709b810ac..b86aa7408b 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcRail.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/CcRail.qml @@ -88,7 +88,7 @@ Item { } UiText { anchors.verticalCenter: parent.verticalCenter - text: item.modelData.gloss || "" + text: UiPreferences.text(item.modelData.gloss || "", "") color: item.on ? Tokens.inkOnBoneDim : Tokens.inkFaint font.family: Tokens.jp font.pixelSize: Tokens.fTiny @@ -121,7 +121,7 @@ Item { UiText { anchors.verticalCenter: parent.verticalCenter - text: "\u529b" + text: UiPreferences.brandMark("\u529b") color: Tokens.sun font.family: Tokens.jp font.pixelSize: Tokens.fBody diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/kit/CcWidgetList.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/kit/CcWidgetList.qml index abd4fdc99b..792dea0b0f 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/kit/CcWidgetList.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/kit/CcWidgetList.qml @@ -254,7 +254,7 @@ SettingCard { } UiText { anchors.verticalCenter: parent.verticalCenter - text: wr.modelData.gloss || "" + text: UiPreferences.text(wr.modelData.gloss || "", "") color: Tokens.inkFaint font.family: Tokens.jp font.pixelSize: Tokens.fSmall diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml index cf64113e36..dc005ac8ee 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml @@ -88,7 +88,7 @@ Item { Repeater { model: [ { mode: "text", label: I18n.tr("Wordmark"), glyph: "RYOKU" }, - { mode: "icon", label: I18n.tr("Glyph"), glyph: "\u529b" } + { mode: "icon", label: I18n.tr("Glyph"), glyph: UiPreferences.brandMark("\u529b") } ] delegate: Rectangle { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/LayoutRoute.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/LayoutRoute.qml index 3871af0499..99e7452959 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/LayoutRoute.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/LayoutRoute.qml @@ -648,7 +648,8 @@ Item { } UiText { anchors.verticalCenter: parent.verticalCenter - text: (prow.modelData.gloss || "") + (prow.modelData.category ? " " + String(prow.modelData.category).toUpperCase() : "") + text: UiPreferences.text(prow.modelData.gloss || "", "") + + (prow.modelData.category ? " " + String(prow.modelData.category).toUpperCase() : "") color: Tokens.inkFaint font.family: Tokens.jp font.pixelSize: Tokens.fTiny diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index 4f1d4d5a9c..0f1d380ee9 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -308,9 +308,10 @@ Item { Text { visible: root.workspaceStyle === "kanji" anchors.centerIn: parent - text: (Number(wsKey) >= 1 && Number(wsKey) <= 10) - ? ["一","二","三","四","五","六","七","八","九","十"][Number(wsKey) - 1] - : wsKey + text: UiPreferences.showJapaneseGloss + && Number(wsKey) >= 1 && Number(wsKey) <= 10 + ? ["一","二","三","四","五","六","七","八","九","十"][Number(wsKey) - 1] + : wsKey color: isFocused ? wsWidget.contentColor : isOccupied ? Qt.rgba(wsWidget.contentColor.r, wsWidget.contentColor.g, wsWidget.contentColor.b, 0.7) : Qt.rgba(wsWidget.contentColor.r, wsWidget.contentColor.g, wsWidget.contentColor.b, 0.3) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/BluetoothPanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/BluetoothPanel.qml index d63efe5757..4ac259e9a6 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/BluetoothPanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/BluetoothPanel.qml @@ -223,7 +223,7 @@ PanelWindow { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - enabled: btPanel.adapter !== null + enabled: BluetoothPower.available onClicked: BluetoothPower.toggle() } } @@ -242,7 +242,7 @@ PanelWindow { UiText { visible: !btPanel.btOn width: parent.width; horizontalAlignment: Text.AlignHCenter - text: I18n.tr("Bluetooth is off") + text: BluetoothPower.blocked ? I18n.tr("Bluetooth is blocked") : I18n.tr("Bluetooth is off") color: Qt.rgba(root.ink.r, root.ink.g, root.ink.b, 0.35) font.family: root.mono; font.pixelSize: 11 topPadding: 4; bottomPadding: 4 diff --git a/ryoku/shell/quickshell/shell/modules/bar/framebars/menus/MenuAudioInput.qml b/ryoku/shell/quickshell/shell/modules/bar/framebars/menus/MenuAudioInput.qml index f079859050..f22e5395de 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/framebars/menus/MenuAudioInput.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/framebars/menus/MenuAudioInput.qml @@ -38,7 +38,11 @@ Item { valueLabel: !root.haveSource ? "" : (root.source.audio.muted ? I18n.tr("off") : Math.round(root.source.audio.volume * 100) + "%") peakNode: root.source peakEnabled: root.open && !!root.source - onMoved: v => { if (root.haveSource) root.source.audio.volume = v; } + onMoved: v => { + if (!root.haveSource) return; + OsdFeed.expectMicVolume(v); + root.source.audio.volume = v; + } onIconTapped: { if (root.haveSource) root.source.audio.muted = !root.source.audio.muted; } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/framebars/widgets/RailStatus.qml b/ryoku/shell/quickshell/shell/modules/bar/framebars/widgets/RailStatus.qml index 72b936b666..979b1163a0 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/framebars/widgets/RailStatus.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/framebars/widgets/RailStatus.qml @@ -115,7 +115,9 @@ Item { Quickshell.execDetached(["ryoku-shell", "audio", steps > 0 ? "up" : "down"]); } else if (audioIn && Audio.source && Audio.source.audio) { const v = Audio.source.audio.volume + (steps > 0 ? 0.05 : -0.05); - Audio.source.audio.volume = Math.max(0, Math.min(1, v)); + const next = Math.max(0, Math.min(1, v)); + OsdFeed.expectMicVolume(next); + Audio.source.audio.volume = next; } } diff --git a/ryoku/shell/quickshell/shell/modules/bar/popouts/AudioPopout.qml b/ryoku/shell/quickshell/shell/modules/bar/popouts/AudioPopout.qml index a8d6acedf1..1ed42103b4 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/popouts/AudioPopout.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/popouts/AudioPopout.qml @@ -137,7 +137,11 @@ Item { valueLabel: !root.haveSource ? "" : (root.source.audio.muted ? I18n.tr("off") : Math.round(root.source.audio.volume * 100) + "%") peakNode: root.source peakEnabled: root.open && !!root.source - onMoved: v => { if (root.haveSource) root.source.audio.volume = v; } + onMoved: v => { + if (!root.haveSource) return; + OsdFeed.expectMicVolume(v); + root.source.audio.volume = v; + } onIconTapped: { if (root.haveSource) root.source.audio.muted = !root.source.audio.muted; } } Menus.AudioDevicePicker { diff --git a/ryoku/shell/quickshell/shell/modules/bar/popouts/BluetoothPopout.qml b/ryoku/shell/quickshell/shell/modules/bar/popouts/BluetoothPopout.qml index f507d1b13f..fb754cb877 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/popouts/BluetoothPopout.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/popouts/BluetoothPopout.qml @@ -29,8 +29,7 @@ Item { readonly property var adapter: Bluetooth.defaultAdapter readonly property bool hasAdapter: root.adapter !== null readonly property bool adapterOn: root.hasAdapter && root.adapter.enabled - readonly property bool blocked: root.hasAdapter && typeof BluetoothAdapterState !== "undefined" - && root.adapter.state === BluetoothAdapterState.Blocked + readonly property bool blocked: BluetoothPower.blocked readonly property bool discovering: root.adapterOn && root.adapter.discovering readonly property var devices: (root.adapterOn && Bluetooth.devices) ? Bluetooth.devices.values : [] diff --git a/ryoku/shell/quickshell/shell/modules/bar/popouts/DockPreviewPopout.qml b/ryoku/shell/quickshell/shell/modules/bar/popouts/DockPreviewPopout.qml index f9d7c1401b..1244910ad1 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/popouts/DockPreviewPopout.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/popouts/DockPreviewPopout.qml @@ -51,7 +51,8 @@ Popout { // ---- shared Popout wiring (hover-driven, welded to the rail edge) ------- edge: DockPreview.edge hoverOpen: false // no frame-edge band; the dock icon is the trigger - closeDelay: 300 // grace to cross from the icon onto the strip + closeDelay: 300 // short close once the strip has been reached + arrivalDelay: 1200 // enough time to cross from a separate dock surface edgeGap: DockPreview.margin > 0 ? DockPreview.margin : 10 * root.s radius: Theme.radiusWindow triggerHovered: root.cls !== "" diff --git a/ryoku/shell/quickshell/shell/modules/bar/popouts/MusicHoverPopout.qml b/ryoku/shell/quickshell/shell/modules/bar/popouts/MusicHoverPopout.qml index 2ea11eca8e..328ae6ee99 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/popouts/MusicHoverPopout.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/popouts/MusicHoverPopout.qml @@ -39,6 +39,7 @@ Popout { edge: MusicPreview.edge hoverOpen: false closeDelay: 300 + arrivalDelay: 1200 edgeGap: MusicPreview.margin > 0 ? MusicPreview.margin : 10 * root.s radius: Theme.radiusWindow triggerHovered: MusicPreview.hovered && root.present diff --git a/ryoku/shell/quickshell/shell/modules/bar/popouts/Popout.qml b/ryoku/shell/quickshell/shell/modules/bar/popouts/Popout.qml index 06413c7592..2ad563e785 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/popouts/Popout.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/popouts/Popout.qml @@ -110,22 +110,36 @@ Item { // bar module): a click-pinned popout must close the moment it's unpinned // (close button, Escape, keybind re-toggle), even under the pointer. readonly property bool hovered: (hoverOpen && triggerHH.hovered) || triggerHovered - || (bodyHH.hovered && (hoverOpen || centered || closeDelay > 0)) + || (bodyHH.hovered && (hoverOpen || centered || closeDelay > 0 || arrivalDelay > 0)) // host gates this off while a centre surface is open or a window is // fullscreen, so an edge hover never fights a modal surface. property bool active: true readonly property bool shouldOpen: active && (hovered || pinned) - // hover-intent close grace: hold the popout open this many ms after - // shouldOpen drops, so crossing the ~2px blob-border rim just outside the - // body's hover region never flickers it shut. 0 = close at once (clicks). + // Once the pointer has reached a detached body, the short close delay keeps + // its rim from flickering. Before that first arrival, allow a slower trip + // across the gap between the trigger surface and the popout. property int closeDelay: 0 + property int arrivalDelay: closeDelay + property bool bodyTouched: false property bool heldOpen: false - onHeldOpenChanged: if (heldOpen) { heldW = openW; heldH = openH } + onHeldOpenChanged: { + if (heldOpen) { + heldW = openW; + heldH = openH; + } else { + bodyTouched = false; + } + } onShouldOpenChanged: { - if (shouldOpen) { closeGrace.stop(); heldOpen = true; } - else if (closeDelay > 0) closeGrace.restart(); - else heldOpen = false; + if (shouldOpen) { + closeGrace.stop(); + heldOpen = true; + } else if (Math.max(closeDelay, arrivalDelay) > 0) { + closeGrace.restart(); + } else { + heldOpen = false; + } } Component.onCompleted: heldOpen = shouldOpen @@ -242,7 +256,11 @@ Item { } ] - Timer { id: closeGrace; interval: root.closeDelay; onTriggered: root.heldOpen = false } + Timer { + id: closeGrace + interval: root.bodyTouched ? root.closeDelay : Math.max(root.closeDelay, root.arrivalDelay) + onTriggered: root.heldOpen = false + } // blob body = a BlobRect in the shared group. it anchors at the outer frame // edge and grows inward, exactly like shell.qml's pillBlob at the top: the @@ -312,7 +330,10 @@ Item { opacity: root.heldOpen ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Motion.effects; easing.type: Easing.BezierSpline; easing.bezierCurve: Motion.effectsCurve } } - HoverHandler { id: bodyHH } + HoverHandler { + id: bodyHH + onHoveredChanged: if (hovered) root.bodyTouched = true + } Item { id: contentInner diff --git a/ryoku/shell/quickshell/shell/modules/confirm/RyokuConfirmationDialog.qml b/ryoku/shell/quickshell/shell/modules/confirm/RyokuConfirmationDialog.qml index 4d7c21cf05..e414fcd12d 100644 --- a/ryoku/shell/quickshell/shell/modules/confirm/RyokuConfirmationDialog.qml +++ b/ryoku/shell/quickshell/shell/modules/confirm/RyokuConfirmationDialog.qml @@ -5,139 +5,224 @@ import Quickshell import Quickshell.Wayland import shell.services import Ryoku.Ui.Singletons +import "../desktop/Singletons" as Desktop -// Session-action confirmation dialog (contract 13 sec 2c). A ryoku-dialog layer -// surface holding one centred box: a 2px outline, radiusWindow corners, a centred -// wrapping title at the large label size (fontLg) with 40px vertical and 20px -// horizontal margins, then a two-button row where both buttons expand to split -// the row 50/50. Button order is negative (Cancel) on the LEFT, positive on the -// right. There is deliberately no Escape and no outside-click dismissal: the two -// buttons are the only exits, matching the reference's modal two-button prompt. -// The GTK modal flag has no layer-shell equivalent, so the box is self-masked -// (only it catches input) rather than blocking the whole screen; a stray click -// outside is a no-op and never dismisses. No reveal motion: the reference dialog -// simply appears (contract 13 sec 5 lists no dialog animation). +// Shared confirmation for desktop and bar session actions. PanelWindow { id: root - required property var modelData // the screen this instance covers - property string action: "" // "logout" | "reboot" | "shutdown" | "" + required property var modelData + property string action: "" property string message: "" property string positiveLabel: "" property string negativeLabel: I18n.tr("Cancel") + property string focusedChoice: "confirm" signal confirmed(string action) signal cancelled(string action) - // measured reference geometry (contract 13 sec 2c); fixed px like the frame, - // which does not scale with the monitor or font scale. - readonly property int titleVMargin: 40 - readonly property int titleHMargin: 20 - readonly property int rowSpacing: 12 + function activateFocused() { + if (root.focusedChoice === "confirm") + root.confirmed(root.action); + else + root.cancelled(root.action); + } screen: modelData visible: root.action !== "" color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Top - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: root.action !== "" ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None WlrLayershell.namespace: "ryoku-dialog" - anchors { top: true; bottom: true; left: true; right: true } - // Only the box catches input; everything else clicks through. No path clears - // the dialog on an outside click, so a stray press is a no-op. mask: Region { - x: box.x - y: box.y - width: box.width - height: box.height + x: card.x + y: card.y + width: card.width + height: card.height } - // Both buttons: a surface pill (radiusWidget, 8px padding) with a 14px label. - // The row sets each width to half, so they expand equally. The subtle outline - // and hover fill render the GTK button frame the reference draws. - component DialogButton: Rectangle { - id: btn - required property string label - signal clicked - height: btnLabel.implicitHeight + 2 * Theme.paddingMd - radius: Theme.radiusWidget - color: btnArea.containsMouse ? Theme.surfaceContainerHigh : Theme.surface - border.width: 1 - border.color: Theme.outlineVariant - - Text { - id: btnLabel - anchors.centerIn: parent - text: I18n.tr(btn.label) - color: Theme.onSurface - font.family: Theme.fontPrimary - font.pixelSize: Theme.fontSm - } + onActionChanged: if (root.action !== "") { + root.focusedChoice = "confirm"; + keyboard.forceActiveFocus(); + } - MouseArea { - id: btnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: btn.clicked() + Item { + id: keyboard + anchors.fill: parent + focus: root.action !== "" + Keys.onEscapePressed: root.cancelled(root.action) + Keys.onReturnPressed: root.activateFocused() + Keys.onEnterPressed: root.activateFocused() + Keys.onPressed: event => { + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { + root.focusedChoice = root.focusedChoice === "confirm" ? "cancel" : "confirm"; + event.accepted = true; + } } - } - Rectangle { - id: box - anchors.centerIn: parent - - // Box sizes to content: the title's natural width plus its horizontal - // margins sets the inner width; the border wraps it. - readonly property real innerWidth: title.implicitWidth + 2 * root.titleHMargin - radius: Theme.radiusWindow - color: Theme.surface - border.width: Theme.borderWidth - border.color: Theme.outline - opacity: Theme.windowOpacity - width: innerWidth + 2 * Theme.borderWidth - height: col.height + 2 * Theme.borderWidth - - Column { - id: col - x: Theme.borderWidth - y: Theme.borderWidth - width: box.innerWidth - spacing: root.rowSpacing - - // Title cell: the text centred with 40px above and below it. - Item { - width: parent.width - height: title.implicitHeight + 2 * root.titleVMargin - - Text { - id: title - anchors.centerIn: parent - width: parent.width - 2 * root.titleHMargin - text: root.message - color: Theme.onSurface - font.family: Theme.fontPrimary - font.pixelSize: Theme.fontLg - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - } + Rectangle { + id: card + anchors.centerIn: parent + width: Math.min(Tokens.s7 * 8, root.width - Tokens.s4 * 2) + height: content.implicitHeight + Tokens.s5 * 2 + radius: Tokens.radius + color: Tokens.paper + border.width: Tokens.border + border.color: Tokens.line + opacity: root.action !== "" ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Tokens.dur(140) } } + transform: Scale { + origin.x: card.width / 2 + origin.y: card.height / 2 + xScale: root.action !== "" ? 1 : 0.96 + yScale: root.action !== "" ? 1 : 0.96 + Behavior on xScale { NumberAnimation { duration: Tokens.dur(140); easing.type: Easing.OutCubic } } + Behavior on yScale { NumberAnimation { duration: Tokens.dur(140); easing.type: Easing.OutCubic } } } - // Button row: negative (Cancel) left, positive right, each half wide. - Row { - width: parent.width - spacing: 0 + Column { + id: content + anchors { left: parent.left; right: parent.right; top: parent.top; margins: Tokens.s5 } + spacing: Tokens.s5 + + Row { + width: parent.width + spacing: Tokens.s2 + Text { + visible: UiPreferences.showJapaneseGloss + text: "卓上" + color: Tokens.inkFaint + font.family: Tokens.jp + font.pixelSize: Tokens.fBody + } + Text { + text: I18n.tr("Desktop").toUpperCase() + color: Tokens.inkDim + font.family: Tokens.ui + font.pixelSize: Tokens.fMicro + font.weight: Font.DemiBold + font.letterSpacing: Tokens.trackMark + anchors.verticalCenter: parent.verticalCenter + } + Rectangle { + width: Math.max(0, content.width - parent.children[0].width + - parent.children[1].width - Tokens.s2 * 3) + height: Tokens.border + color: Tokens.line + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + width: parent.width + spacing: Tokens.s4 + Rectangle { + width: Tokens.rowH + height: Tokens.rowH + radius: Tokens.radius + color: Tokens.tint5 + border.width: Tokens.border + border.color: Tokens.lineSoft + Text { + anchors.centerIn: parent + text: root.action === "shutdown" ? "power_settings_new" + : root.action === "reboot" ? "restart_alt" : "logout" + color: Tokens.ink + font.family: Desktop.Theme.iconFont + font.pixelSize: Tokens.fValue + } + } + Column { + width: parent.width - Tokens.rowH - Tokens.s4 + anchors.verticalCenter: parent.verticalCenter + spacing: Tokens.s1 + Text { + width: parent.width + text: root.positiveLabel + color: Tokens.ink + font.family: Tokens.display + font.pixelSize: Tokens.fValue + elide: Text.ElideRight + } + Text { + width: parent.width + text: root.message + color: Tokens.inkMuted + font.family: Tokens.ui + font.pixelSize: Tokens.fSmall + wrapMode: Text.WordWrap + } + } + } - DialogButton { - width: parent.width / 2 - label: root.negativeLabel - onClicked: root.cancelled(root.action) + Rectangle { + width: parent.width + height: Tokens.border + color: Tokens.lineSoft } - DialogButton { - width: parent.width / 2 - label: root.positiveLabel - onClicked: root.confirmed(root.action) + + Row { + anchors.right: parent.right + spacing: Tokens.s2 + + Rectangle { + id: cancelButton + width: Math.max(Tokens.s7 * 2, cancelText.implicitWidth + Tokens.s4 * 2) + height: Tokens.rowH + radius: Tokens.radius + color: cancelMouse.containsPress ? Tokens.tint16 + : cancelMouse.containsMouse ? Tokens.tint10 : Tokens.paper + border.width: Tokens.border + border.color: keyboard.activeFocus && root.focusedChoice === "cancel" + ? Tokens.ink : Tokens.line + Text { + id: cancelText + anchors.centerIn: parent + text: root.negativeLabel.toUpperCase() + color: Tokens.ink + font.family: Tokens.ui + font.pixelSize: Tokens.fMicro + font.weight: Font.DemiBold + font.letterSpacing: Tokens.trackLabel + } + MouseArea { + id: cancelMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.cancelled(root.action) + } + } + + Rectangle { + id: confirmButton + width: Math.max(Tokens.s7 * 2, confirmText.implicitWidth + Tokens.s4 * 2) + height: Tokens.rowH + radius: Tokens.radius + color: confirmMouse.containsPress ? Tokens.tint16 : Tokens.bone + border.width: Tokens.border + border.color: keyboard.activeFocus && root.focusedChoice === "confirm" + ? Tokens.ink : Tokens.lineOnBone + Text { + id: confirmText + anchors.centerIn: parent + text: root.positiveLabel.toUpperCase() + color: Tokens.inkOnBone + font.family: Tokens.ui + font.pixelSize: Tokens.fMicro + font.weight: Font.DemiBold + font.letterSpacing: Tokens.trackLabel + } + MouseArea { + id: confirmMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.confirmed(root.action) + } + } } } } diff --git a/ryoku/shell/quickshell/shell/modules/desktop/Desktop.qml b/ryoku/shell/quickshell/shell/modules/desktop/Desktop.qml index c1d83a22c2..2268b8db26 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/Desktop.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/Desktop.qml @@ -51,6 +51,7 @@ Scope { // the monitor this instance renders on, bound by the main shell per screen. property var screen + property bool surfaceReady: false // controller hook; the desktop layer defaults on. property bool active: true property bool widgetsEnabled: true @@ -89,14 +90,13 @@ Scope { return Config.widgetDepth(id, root.monitorName); } readonly property var stageState: Services.ShellState.forScreen(root.screen) - // The visualiser paints inside this surface (docs/stage.md) while the - // stage cuts this wall, or while the Stage Editor frames the monitor: the - // desktop lifts to Top then, so the inline look still clears open windows - // and the placement grip shares its surface. In overlay mode outside the - // editor it keeps its own raised surface. + // Cut layers and Stage composing keep the visualizer inside the frosted content. readonly property bool hostsVisualizer: VizCfg.Config.enabled && (root.stageComposing || (root.stageOn && !(root.stageState && root.stageState.visualizerOverlay))) + readonly property bool plainVisualizer: VizCfg.Config.enabled + && !root.hostsVisualizer + && !(root.stageState && root.stageState.visualizerOverlay) readonly property string monitorName: root.screen ? root.screen.name : "" // Edit widgets lifts this desktop above open windows. Per-widget locks stay // authoritative and can be toggled from the canvas. @@ -108,8 +108,8 @@ Scope { // monitor, the desktop is drawn as the card the toolbar frames (a scale + // translate about the usable area's centre), on the mode's one animated // scalar, so the desktop and the chrome shrink as one rectangle across two - // scene graphs. Off the edited screen it stays the identity. desktopContent - // carries the matrix. + // scene graphs. Off the edited screen it stays the identity. Both the + // frosted content and its plain visualizer use this matrix. readonly property bool stageEditing: Stage.GlobalStates.editModeMonitor === root.monitorName && (Stage.GlobalStates.editMode || Stage.GlobalStates.editProgress > 0) readonly property bool stageFramingHere: root.stageEditing @@ -138,9 +138,8 @@ Scope { easing.type: Easing.OutCubic } } - // Capturing a live desktop into a full-output blur layer forces every video - // or visualizer frame through another FBO pass. Keep the haze but flatten - // the glass while content below it is moving. + // Blur stays flat while video or a visualizer inside the frosted content + // would force every moving frame through another full-output FBO pass. readonly property bool deckFrostHasMotion: (root.videoUrl !== "" && !root.videoPaused) || root.wallpaperLive || root.hostsVisualizer @@ -313,8 +312,9 @@ Scope { const visualizerId = "" + w; if (visualizerId === "visualizer" || visualizerId.indexOf("visualizer:") === 0) { const index = visualizerId === "visualizer" - ? VizCfg.Config.active : parseInt(visualizerId.slice(11)); - const grip = !isNaN(index) ? vizGripRepeater.itemAt(index) : null; + ? VizCfg.Config.active + : VizCfg.Config.indexOfId(visualizerId.slice(11)); + const grip = index >= 0 ? vizGripRepeater.itemAt(index) : null; return VizCfg.Config.enabled && grip ? grip.boxItem : null; } if (String(w).indexOf("plugin:") === 0) { @@ -454,11 +454,8 @@ Scope { Config.selectMonitor(root.monitorName, true); StageCfg.StageSession.select(value); if (value === "visualizer" || value.indexOf("visualizer:") === 0) { - if (value.indexOf("visualizer:") === 0) { - const index = parseInt(value.slice(11)); - if (!isNaN(index)) - VizCfg.Config.setActive(index); - } + if (value.indexOf("visualizer:") === 0) + VizCfg.Config.setActive(value.slice(11)); StageCfg.StageSession.inspecting = ""; StageCfg.StageSession.openSection("visualizer"); return; @@ -488,9 +485,9 @@ Scope { if (p) { p.removeWidget(id); } else if (String(id).indexOf("visualizer:") === 0) { - const index = parseInt(String(id).slice(11)); - if (!isNaN(index)) - VizCfg.Config.removeVisualizer(index); + const instanceId = String(id).slice(11); + if (instanceId !== "") + VizCfg.Config.removeVisualizer(instanceId); } else { Config.setFor(root.monitorName, id + "Enabled", false); } @@ -541,16 +538,16 @@ Scope { // Each grip snapshots its own complete instance. The map keeps wheel and // easing settle timers independent when two instances are touched quickly. property var vizGestureBefore: ({}) - function stageVizGestureStart(index) { + function stageVizGestureStart(instanceId) { const p = root.stageProvider(); - const id = "visualizer:" + index; + const id = "visualizer:" + instanceId; const next = Object.assign({}, root.vizGestureBefore); next[id] = p ? p.snapshot(id) : null; root.vizGestureBefore = next; } - function stageVizGestureFinish(index) { + function stageVizGestureFinish(instanceId) { const p = root.stageProvider(); - const id = "visualizer:" + index; + const id = "visualizer:" + instanceId; const before = root.vizGestureBefore[id]; const next = Object.assign({}, root.vizGestureBefore); delete next[id]; @@ -642,18 +639,31 @@ Scope { visible: root.active color: "transparent" exclusionMode: ExclusionMode.Ignore - // Editing lifts the desktop above open windows so the stage is never - // obscured by whatever was in front; Done drops it back under them. - WlrLayershell.layer: root.stageComposing ? WlrLayer.Top : WlrLayer.Bottom + // Editing and desktop text entry lift above ordinary windows. A + // compositor may ignore an exclusive keyboard request on a bottom + // surface, so the lift lasts exactly as long as a hosted field needs it. + WlrLayershell.layer: (root.stageComposing || win.kbWanted > 0) + ? WlrLayer.Top : WlrLayer.Bottom WlrLayershell.namespace: "ryoku-widgets" - // Text fields and icon dialogs keep their existing counter. The editor - // borrows the grab only while no other surface or desktop control is - // asking for keys, so opening a search or inspector never loses its caret. - // Where a grab would also take every click, the editor asks on demand - // instead: the compositor hands it the keyboard as the pointer crosses - // the desktop, and the toolbar and catalogue on their own surface stay - // clickable (keyboardGrabSharesPointer in docs/compositors.md). + Connections { + target: win.contentItem?.Window.window ?? null + enabled: !root.surfaceReady + function onFrameSwapped() { root.surfaceReady = true } + } + // Text fields and icon dialogs keep their existing exclusive counter. + // A selected shortcut takes keys only while the pointer remains over + // the icon grid. Moving toward another app releases the grab before + // that app is clicked; moving back restores shortcut key navigation. property int kbWanted: 0 + readonly property bool shortcutKeyboardWanted: !!desktopIcons.item + && desktopIcons.item.hasSelection && desktopIcons.item.pointerOverShortcut + && !desktopIcons.item.dialogOpen && !root.stageComposing + onShortcutKeyboardWantedChanged: if (shortcutKeyboardWanted) { + Qt.callLater(() => { + if (shortcutKeyboardWanted && desktopIcons.item) + desktopIcons.item.forceActiveFocus(); + }); + } readonly property bool hostedWidgetTyping: { const owner = String(Stage.GlobalStates.widgetTypingOwner || ""); return root.monitorName !== "" && owner.indexOf(root.monitorName + "|") === 0; @@ -668,9 +678,11 @@ Scope { onKbWantedChanged: if (kbWanted === 0 && !stageKeyboardWanted) root.kbRestore() onStageKeyboardWantedChanged: if (!stageKeyboardWanted && kbWanted === 0) root.kbRestore() WlrLayershell.keyboardFocus: kbWanted > 0 ? WlrKeyboardFocus.Exclusive - : !stageKeyboardWanted ? WlrKeyboardFocus.None - : Wm.caps.keyboardGrabSharesPointer === true ? WlrKeyboardFocus.Exclusive - : WlrKeyboardFocus.OnDemand + : stageKeyboardWanted + ? (Wm.caps.keyboardGrabSharesPointer === true + ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand) + : shortcutKeyboardWanted ? WlrKeyboardFocus.Exclusive + : WlrKeyboardFocus.None anchors { top: true; left: true; right: true; bottom: true } @@ -724,10 +736,8 @@ Scope { guides.flash(v, h); } - // The Stage Editor's card: every visible layer of the desktop (wallpaper, - // stage, widgets, editor frames) in one container that carries the mode's - // shrink matrix, so the desktop becomes the object the chrome frames and - // the widgets under the cursor scale together. Identity when not editing. + // The Stage Editor's card: wallpaper, stage, widgets and editor frames + // carry the same matrix as the plain visualizer above the frost. Item { id: desktopContent anchors.fill: parent @@ -797,8 +807,6 @@ Scope { visible: root.stageOn } - // While the stage is on (or the Stage Editor frames this monitor), the - // visualizer shares this surface so Stage cut-outs can layer around it. Item { id: vizHost z: root.stageCut @@ -806,7 +814,11 @@ Scope { anchors.fill: parent visible: root.hostsVisualizer opacity: 1 - 0.75 * root.stageFramingDim - Viz.InlineVisualizer { anchors.fill: parent } + Loader { + anchors.fill: parent + active: root.hostsVisualizer + sourceComponent: Viz.InlineVisualizer { anchors.fill: parent } + } } // One grip and one frame per rendered instance. Selecting either makes @@ -815,9 +827,10 @@ Scope { id: vizGripRepeater model: VizCfg.Config.enabled ? VizCfg.Config.count : 0 delegate: Viz.VisualizerGrip { + id: vizGrip required property int index z: 5.6 - instanceIndex: index + instanceId: VizCfg.Config.idAt(vizGrip.index) composing: root.stageComposing && (Stage.GlobalStates.editDrawerSection === "widgets" || Stage.GlobalStates.editDrawerSection === "visualizer") @@ -825,12 +838,12 @@ Scope { enabled: !root.stageFramingHere opacity: 1 - 0.75 * root.stageFramingDim onActivated: { - VizCfg.Config.setActive(index); - StageCfg.StageSession.select("visualizer:" + index); + VizCfg.Config.setActive(vizGrip.instanceId); + StageCfg.StageSession.select("visualizer:" + vizGrip.instanceId); } - onGestureStarted: root.stageVizGestureStart(index) - onGestureFinished: root.stageVizGestureFinish(index) - onMenuRequested: root.stageOpenSettings("visualizer:" + index) + onGestureStarted: root.stageVizGestureStart(vizGrip.instanceId) + onGestureFinished: root.stageVizGestureFinish(vizGrip.instanceId) + onMenuRequested: root.stageOpenSettings("visualizer:" + vizGrip.instanceId) } } @@ -838,7 +851,7 @@ Scope { model: VizCfg.Config.enabled ? VizCfg.Config.count : 0 delegate: StageOutline { required property int index - readonly property string instanceId: "visualizer:" + index + readonly property string instanceId: "visualizer:" + VizCfg.Config.idAt(index) // Both repeaters follow the same model, in no guaranteed order: // re-resolve when the grips' count changes, not once at creation. readonly property var gripItem: vizGripRepeater.count > index @@ -851,7 +864,7 @@ Scope { || Stage.GlobalStates.editDrawerSection === "visualizer") box: slotItem ? Qt.rect(slotItem.x, slotItem.y, slotItem.width, slotItem.height) : Qt.rect(0, 0, 0, 0) - title: I18n.tr("Visualizer") + " " + (index + 1) + title: I18n.tr("Visualizer") + " " + VizCfg.Config.labelAt(index) selected: StageCfg.StageSession.contains(instanceId) primary: StageCfg.StageSession.selected === instanceId targetItem: gripItem @@ -862,7 +875,7 @@ Scope { Stage.GlobalStates.editProgress, Stage.GlobalStates.editDrawerProgress).width / Math.max(1, win.width)) onPicked: modifiers => { - VizCfg.Config.setActive(index); + VizCfg.Config.setActive(VizCfg.Config.idAt(index)); stageSelection.pick(instanceId, modifiers); } onSettings: root.stageOpenSettings(instanceId) @@ -915,7 +928,7 @@ Scope { && row.id !== "visualizer").map(row => row.id).concat( VizCfg.Config.enabled ? Array.from({ length: VizCfg.Config.count }, - (_, i) => "visualizer:" + i) : []) + (_, i) => "visualizer:" + VizCfg.Config.idAt(i)) : []) itemFor: id => root.slotFor(id) provider: StageIsland.Config.widgetProvider onRemoveRequested: id => root.stageRemoveWidget(id) @@ -1785,12 +1798,8 @@ Scope { z: root.stageOn ? 2.5 : -1 opacity: 1 - 0.75 * root.stageFramingDim enabled: !root.stageFramingHere - readonly property bool keyboardNeeded: !!item && (item.hasSelection || item.dialogOpen) - onKeyboardNeededChanged: { - win.kbWanted += keyboardNeeded ? 1 : -1; - if (keyboardNeeded && item) - item.forceActiveFocus(); - } + readonly property bool keyboardNeeded: !!item && item.dialogOpen + onKeyboardNeededChanged: win.kbWanted += keyboardNeeded ? 1 : -1 Component.onDestruction: if (keyboardNeeded) win.kbWanted -= 1 sourceComponent: StageShortcuts.DesktopShortcutsLayer { anchors.fill: parent @@ -1964,6 +1973,19 @@ Scope { strength: root.deckFrost } + // The plain look shares the desktop surface but stays outside its blur. + Item { + anchors.fill: parent + transform: Matrix4x4 { matrix: root.stageMatrix } + visible: root.plainVisualizer + opacity: 1 - 0.75 * root.stageFramingDim + Loader { + anchors.fill: parent + active: root.plainVisualizer + sourceComponent: Viz.InlineVisualizer { anchors.fill: parent } + } + } + // The Stage Editor's card: the reference's EditModeCard (the live // wallpaper blurred and dimmed around the shrunk desktop, its rounded // corner and its shadow), drawn over the desktop and cut out to the diff --git a/ryoku/shell/quickshell/shell/modules/desktop/DesktopContextMenu.qml b/ryoku/shell/quickshell/shell/modules/desktop/DesktopContextMenu.qml index 24325b68e7..6677d8a794 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/DesktopContextMenu.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/DesktopContextMenu.qml @@ -6,6 +6,7 @@ import shell.services as Services import "../stage/Singletons" as StageCfg import inir import "Singletons" +import stage.modules.ii.background.shortcuts as StageShortcuts // The system desktop right-click menu: right-clicking bare wallpaper on any bar // style opens this. It carries the iRiS desktop menu's structure -- a quick row @@ -26,7 +27,15 @@ Item { // launcher open, and its wallpaper feeds the Wallpaper tile's thumbnail. property var desktop: null - function openAt(x, y) { shell.px = x; shell.py = y; shell.open = true; } + property var pendingFolderPoint: null + property var pendingApplicationPoint: null + property string page: "" + function openAt(x, y) { + menu.pendingFolderPoint = null; + menu.pendingApplicationPoint = null; + menu.page = ""; + shell.px = x; shell.py = y; shell.open = true; + } function close() { shell.open = false; } // The launcher and the Stage Editor open on the monitor the menu is on: the @@ -48,11 +57,27 @@ Item { menu.close(); } function newFolder() { - const openX = shell.px; - const openY = shell.py; + menu.pendingFolderPoint = Qt.point(shell.px, shell.py); + menu.close(); + } + function finishNewFolder() { + const point = menu.pendingFolderPoint; + menu.pendingFolderPoint = null; + if (point && menu.desktop) + menu.desktop.newDesktopFolder(point.x, point.y); + } + function newApplication() { + menu.pendingApplicationPoint = Qt.point(shell.px, shell.py); + menu.page = "application"; + } + function leaveApplicationPage() { + menu.page = ""; + shell.forceActiveFocus(); + } + function finishNewApplication() { + menu.pendingApplicationPoint = null; + menu.leaveApplicationPage(); menu.close(); - if (menu.desktop) - menu.desktop.newDesktopFolder(openX, openY); } function changeWallpaper() { Services.ShellState.requestSurfaceActive("wallpaper", null); @@ -105,34 +130,48 @@ Item { DesktopMenu { id: shell - cardWidth: powerActions.implicitWidth + Theme.s4 * 2 - title: I18n.tr("Desktop") + cardWidth: menu.page === "application" ? 400 : powerActions.implicitWidth + Theme.s4 * 2 + onShowingChanged: { + if (!showing && menu.pendingFolderPoint) + menu.finishNewFolder(); + if (!showing) + menu.pendingApplicationPoint = null; + } + title: menu.page === "application" ? I18n.tr("New application") : I18n.tr("Desktop") gloss: "卓上" - // ── quick row: the wallpaper picker and the launcher ── MenuQuick { + visible: menu.page === "" items: [ { icon: "wallpaper", label: I18n.tr("Wallpaper"), image: menu.wallpaperThumb, action: () => menu.changeWallpaper() }, { icon: "search", label: I18n.tr("Search"), action: () => menu.openSearch() } ] } - MenuSection {} + MenuSection { visible: menu.page === "" } - MenuRow { icon: "dashboard_customize"; label: I18n.tr("Edit desktop"); accent: true; onTriggered: menu.editDesktop() } - MenuRow { icon: "create_new_folder"; label: I18n.tr("New folder"); onTriggered: menu.newFolder() } - MenuRow { icon: "tune"; label: I18n.tr("Quick controls"); onTriggered: menu.quickControls() } + MenuRow { visible: menu.page === ""; icon: "dashboard_customize"; label: I18n.tr("Edit desktop"); accent: true; onTriggered: menu.editDesktop() } + MenuRow { visible: menu.page === ""; icon: "create_new_folder"; label: I18n.tr("New folder"); onTriggered: menu.newFolder() } + MenuRow { + visible: menu.page === "" + icon: "add_to_home_screen" + label: I18n.tr("New application") + closeOnTrigger: false + onTriggered: menu.newApplication() + } + MenuRow { visible: menu.page === ""; icon: "tune"; label: I18n.tr("Quick controls"); onTriggered: menu.quickControls() } // iRiS conveniences: Studio and Edit iRiS on the iris bar style only. On // every other style they hide, and the Column skips them with no gap. - MenuRow { visible: menu.iris; icon: "palette"; label: I18n.tr("Studio"); onTriggered: menu.openIrisStudio() } - MenuRow { visible: menu.iris; icon: "edit"; label: I18n.tr("Edit Shima"); onTriggered: menu.editIris() } + MenuRow { visible: menu.page === "" && menu.iris; icon: "palette"; label: I18n.tr("Shima Studio"); onTriggered: menu.openIrisStudio() } + MenuRow { visible: menu.page === "" && menu.iris; icon: "edit"; label: I18n.tr("Edit Shima"); onTriggered: menu.editIris() } - MenuSection {} - MenuRow { icon: "settings"; label: I18n.tr("Settings"); accent: true; closeOnTrigger: false; onTriggered: menu.openSettings() } - MenuRow { icon: "refresh"; label: I18n.tr("Reload shell"); closeOnTrigger: false; onTriggered: menu.refreshShell() } + MenuSection { visible: menu.page === "" } + MenuRow { visible: menu.page === ""; icon: "settings"; label: I18n.tr("General settings"); accent: true; closeOnTrigger: false; onTriggered: menu.openSettings() } + MenuRow { visible: menu.page === ""; icon: "refresh"; label: I18n.tr("Reload desktop"); closeOnTrigger: false; onTriggered: menu.refreshShell() } - MenuSection {} + MenuSection { visible: menu.page === "" } MenuQuick { + visible: menu.page === "" id: powerActions columns: 3 items: [ @@ -141,5 +180,16 @@ Item { { icon: "power_settings_new", label: I18n.tr("Power off"), action: () => menu.confirmSessionAction("shutdown") } ] } + + StageShortcuts.DesktopShortcutAppPicker { + visible: menu.page === "application" + width: 368 + screenName: menu.activeMonitor() + folderId: "" + targetX: menu.pendingApplicationPoint ? menu.pendingApplicationPoint.x : NaN + targetY: menu.pendingApplicationPoint ? menu.pendingApplicationPoint.y : NaN + onApplicationAdded: menu.finishNewApplication() + onDone: menu.leaveApplicationPage() + } } } diff --git a/ryoku/shell/quickshell/shell/modules/desktop/DesktopMenu.qml b/ryoku/shell/quickshell/shell/modules/desktop/DesktopMenu.qml index 1a09fba469..2f5178e39c 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/DesktopMenu.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/DesktopMenu.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import "Singletons" +import Ryoku.Ui.Singletons as Ui // The desktop right-click chrome, shared by every context menu the widget // layer spawns (the desktop and clock menu, and each plugin tile's menu). One @@ -101,7 +102,7 @@ Item { spacing: Theme.s2 Text { id: seal - visible: menu.gloss.length > 0 + visible: Ui.UiPreferences.showJapaneseGloss && menu.gloss.length > 0 anchors.verticalCenter: parent.verticalCenter text: menu.gloss color: Theme.faint diff --git a/ryoku/shell/quickshell/shell/modules/desktop/MenuSection.qml b/ryoku/shell/quickshell/shell/modules/desktop/MenuSection.qml index e184e75c7c..4acbe84dea 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/MenuSection.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/MenuSection.qml @@ -1,5 +1,6 @@ import QtQuick import "Singletons" +import Ryoku.Ui.Singletons as Ui // The rule yields all available width to the section name before collapsing, // so translated labels stay readable in a narrow docked inspector. @@ -25,7 +26,7 @@ Item { TextMetrics { id: glossMetrics - text: sec.gloss + text: Ui.UiPreferences.text(sec.gloss, "") font.family: Theme.fontJp font.pixelSize: 9 } @@ -41,7 +42,7 @@ Item { Text { id: gl - visible: sec.gloss.length > 0 && sec.label.length > 0 + visible: Ui.UiPreferences.showJapaneseGloss && sec.gloss.length > 0 && sec.label.length > 0 x: tick.x + tick.width + 7 width: visible ? glossMetrics.advanceWidth : 0 anchors.verticalCenter: parent.verticalCenter diff --git a/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Scheme.qml b/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Scheme.qml index fc56494e9d..6f77fe8f2a 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Scheme.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Scheme.qml @@ -27,6 +27,7 @@ Singleton { property bool matchWallpaper: true property var namedScheme: null property var wall: ({}) + property var preview: ({}) // A palette value is only usable when it is a non-empty hex string; a null, // missing or half-written role falls through to the next layer, so a partial @@ -36,6 +37,8 @@ Singleton { // Resolve one role through the layer chain: a selected named scheme wins, // then the live wallpaper palette while Match wallpaper is on, then the base. function role(key, base) { + if (usable(preview[key])) + return preview[key]; if (namedScheme && usable(namedScheme[key])) return namedScheme[key]; if (matchWallpaper && usable(wall[key])) @@ -157,6 +160,18 @@ Singleton { onFileChanged: reload() onLoaded: root.refreshWall() } + FileView { + id: previewFile + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } FileView { id: shellFile path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/shell.json" diff --git a/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Theme.qml b/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Theme.qml index ce9d9792f1..e465b8f8c5 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/Singletons/Theme.qml @@ -84,7 +84,7 @@ Singleton { // brand mark + name, user-overridable via ~/.config/ryoku/brand.json (Shell -> // Global). defaults to the 力 seal / "Ryoku". BrandMark renders `mark`, or // `markSource` (an image) when set. Ryoku's own apps never read these. - readonly property string mark: Config.markText.length > 0 ? Config.markText : "\u529b" + readonly property string mark: Ui.UiPreferences.brandMark(Config.markText) readonly property string markSource: Config.markImage readonly property bool markTint: Config.markTint readonly property string brandName: Config.brandName.length > 0 ? Config.brandName : "Ryoku" diff --git a/ryoku/shell/quickshell/shell/modules/desktop/iris/IrisCanvasProvider.qml b/ryoku/shell/quickshell/shell/modules/desktop/iris/IrisCanvasProvider.qml index fd6dc7ee1e..17a284b84e 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/iris/IrisCanvasProvider.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/iris/IrisCanvasProvider.qml @@ -58,5 +58,5 @@ Item { Component { id: cEditorial; EditorialWidget { anchors.fill: parent; configEntryName: "editorial"; visible: true; ryokuHosted: true; ryokuStyle: prov.ryokuStyle; outputName: prov.outputName; screenWidth: prov.screenW; screenHeight: prov.screenH; scaledScreenWidth: prov.screenW; scaledScreenHeight: prov.screenH; wallpaperScale: 1; ryokuHostX: prov.hostX; ryokuHostY: prov.hostY; ryokuInkOverride: prov.inkOverride; ryokuAccentOverride: prov.accentOverride; ryokuScaleOverride: prov.scaleOverride; ryokuRadiusOverride: prov.radiusOverride; ryokuOptionOverrides: prov.optionOverrides } } Component { id: cImage; ImageConverterWidget { anchors.fill: parent; configEntryName: "imageConverter"; visible: true; ryokuHosted: true; ryokuStyle: prov.ryokuStyle; outputName: prov.outputName; screenWidth: prov.screenW; screenHeight: prov.screenH; scaledScreenWidth: prov.screenW; scaledScreenHeight: prov.screenH; wallpaperScale: 1; ryokuHostX: prov.hostX; ryokuHostY: prov.hostY; ryokuInkOverride: prov.inkOverride; ryokuAccentOverride: prov.accentOverride; ryokuScaleOverride: prov.scaleOverride; ryokuRadiusOverride: prov.radiusOverride; ryokuOptionOverrides: prov.optionOverrides } } Component { id: cJp; JapaneseTypographyWidget { anchors.fill: parent; configEntryName: "japaneseTypography"; visible: true; ryokuHosted: true; ryokuStyle: prov.ryokuStyle; outputName: prov.outputName; screenWidth: prov.screenW; screenHeight: prov.screenH; scaledScreenWidth: prov.screenW; scaledScreenHeight: prov.screenH; wallpaperScale: 1; ryokuHostX: prov.hostX; ryokuHostY: prov.hostY; ryokuInkOverride: prov.inkOverride; ryokuAccentOverride: prov.accentOverride; ryokuScaleOverride: prov.scaleOverride; ryokuRadiusOverride: prov.radiusOverride; ryokuOptionOverrides: prov.optionOverrides } } - Component { id: cViz; VisualizerWidget { anchors.fill: parent; configEntryName: "visualizer"; visible: true; ryokuHosted: true; ryokuStyle: prov.ryokuStyle; outputName: prov.outputName; screenWidth: prov.screenW; screenHeight: prov.screenH; scaledScreenWidth: prov.screenW; scaledScreenHeight: prov.screenH; wallpaperScale: 1; ryokuHostX: prov.hostX; ryokuHostY: prov.hostY; ryokuInkOverride: prov.inkOverride; ryokuAccentOverride: prov.accentOverride; ryokuScaleOverride: prov.scaleOverride; ryokuRadiusOverride: prov.radiusOverride; ryokuOptionOverrides: prov.optionOverrides } } + Component { id: cViz; VisualizerWidget { anchors.fill: parent; configEntryName: "visualizer"; visible: true; ryokuHosted: true; ryokuStyle: prov.ryokuStyle; outputName: prov.outputName; screenWidth: prov.screenW; screenHeight: prov.screenH; scaledScreenWidth: prov.screenW; scaledScreenHeight: prov.screenH; wallpaperScale: 1; ryokuHostX: prov.hostX; ryokuHostY: prov.hostY; ryokuInkOverride: prov.inkOverride; ryokuAccentOverride: prov.accentOverride; ryokuScaleOverride: prov.scaleOverride; ryokuRadiusOverride: prov.radiusOverride; ryokuSlotRadius: prov.slotRadius; ryokuOptionOverrides: prov.optionOverrides } } } diff --git a/ryoku/shell/quickshell/shell/modules/desktop/options/VisualizerOptions.qml b/ryoku/shell/quickshell/shell/modules/desktop/options/VisualizerOptions.qml index 53549739df..28ebd42a7d 100644 --- a/ryoku/shell/quickshell/shell/modules/desktop/options/VisualizerOptions.qml +++ b/ryoku/shell/quickshell/shell/modules/desktop/options/VisualizerOptions.qml @@ -48,7 +48,7 @@ Column { // box, the release records one undo entry through the editor's provider // (none outside the editor, where there is no undo stack to join). property var placeBefore: null - readonly property string instanceId: "visualizer:" + opts.cfg.active + readonly property string instanceId: "visualizer:" + opts.cfg.activeId function placeBegin() { if (opts.placeBefore === null && opts.provider) opts.placeBefore = opts.provider.snapshot(opts.instanceId); @@ -95,9 +95,9 @@ Column { } MenuRow { label: I18n.tr("Instance") - value: (opts.cfg.active + 1) + " / " + opts.cfg.count + value: opts.cfg.labelAt(opts.cfg.active) + " / " + opts.cfg.count closeOnTrigger: false - onTriggered: opts.cfg.setActive((opts.cfg.active + 1) % opts.cfg.count) + onTriggered: opts.cfg.setActive(opts.cfg.idAt((opts.cfg.active + 1) % opts.cfg.count)) } MenuRow { label: I18n.tr("Add instance") @@ -109,7 +109,7 @@ Column { visible: opts.cfg.count > 1 label: I18n.tr("Remove instance") closeOnTrigger: false - onTriggered: opts.cfg.removeVisualizer(opts.cfg.active) + onTriggered: opts.cfg.removeVisualizer(opts.cfg.activeId) } // The visual gallery is the first decision in Look. There is no second // "open gallery" state to hunt through. diff --git a/ryoku/shell/quickshell/shell/modules/dock/DockItem.qml b/ryoku/shell/quickshell/shell/modules/dock/DockItem.qml index 009d7a7afa..166236b083 100644 --- a/ryoku/shell/quickshell/shell/modules/dock/DockItem.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/DockItem.qml @@ -226,11 +226,8 @@ Item { return; } if (button === Qt.MiddleButton) { - const entry = DesktopEntries.heuristicLookup(item.className); - if (entry) { - AppLaunch.run(entry, null); - item.launchBounce(); - } + Dock.launch(item.className); + item.launchBounce(); return; } if (item.count === 0) diff --git a/ryoku/shell/quickshell/shell/modules/dock/DockSurface.qml b/ryoku/shell/quickshell/shell/modules/dock/DockSurface.qml index 288830eb13..f9c1018f7f 100644 --- a/ryoku/shell/quickshell/shell/modules/dock/DockSurface.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/DockSurface.qml @@ -14,11 +14,12 @@ import shell.modules.stage.frame // strip when autohide is on. // // Reveal is the point of this surface. The band shows when the dock is not -// autohiding, or the pointer is on it, or nothing is focused, or a pin is being -// dragged -- and is forced away under a fullscreen window unless the pointer is -// in the peek strip. Hiding slides the band out by its own depth, leaving a 3 px -// peek inside the input mask; the empty margins stay click-through because the -// mask is only the band rect unioned with that strip. +// autohiding, or the pointer is on it, or its workspace is empty, or nothing is +// focused, or a pin is being dragged -- and is forced away under a fullscreen +// window unless the pointer is in the peek strip. Hiding slides the band out by +// its own depth, leaving a 3 px peek inside the input mask; the empty margins +// stay click-through because the mask is only the band rect unioned with that +// strip. // // While the Stage Editor frames this monitor the dock is part of the desktop // preview, so it holds revealed and rises above the lifted desktop. @@ -55,8 +56,7 @@ PanelWindow { visible: Dock.cfg("enabled", false) // `screen` is PanelWindow's own property, set per monitor from shell.qml. WlrLayershell.namespace: "ryoku-dock" - // Staying on one layer avoids remapping the dock when the editor opens. - WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.layer: dock.stageEditing ? WlrLayer.Overlay : WlrLayer.Bottom exclusionMode: ExclusionMode.Normal // A pinned (non-autohide) dock reserves its depth; an autohiding one floats // over the desktop and reserves nothing. @@ -75,6 +75,11 @@ PanelWindow { readonly property bool monFullscreen: Wm.outputHasFullscreen(dock.screen ? dock.screen.name : "") readonly property bool pointerInside: band.hovered || peekHover.hovered readonly property string screenName: dock.screen ? dock.screen.name : "" + readonly property bool workspaceEmpty: { + if (!Dock.cfg("revealOnEmpty", true)) return false; + const active = (Wm.workspaces ?? []).find(ws => ws.active && ws.output === dock.screenName); + return active !== undefined && !(Wm.windows ?? []).some(w => w.workspace === active.id); + } readonly property bool menuHere: Dock.menuOpen && Dock.menuScreen === dock.screenName readonly property bool revealed: { // The editor is showing this dock's settings page: it holds open. @@ -85,6 +90,7 @@ PanelWindow { return dock.pointerInside; return !dock.autohide || dock.pointerInside + || dock.workspaceEmpty || !Dock.anyFocused || band.dragging || dock.menuHere; diff --git a/ryoku/shell/quickshell/shell/modules/dock/UniversalDockHost.qml b/ryoku/shell/quickshell/shell/modules/dock/UniversalDockHost.qml index 21e0b2d352..f9cee30af1 100644 --- a/ryoku/shell/quickshell/shell/modules/dock/UniversalDockHost.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/UniversalDockHost.qml @@ -1,6 +1,7 @@ import QtQuick import shell.services import "../stage/Singletons" as StageCfg +import stage as Stage import "../bar/barstyles/python/dock" as PythonDock import inir.modules.iris.dock as ShimaDock Item { @@ -23,6 +24,8 @@ Item { // edits an invisible thing. readonly property bool stageEditing: StageCfg.StageSession.onMonitor( root.screen ? root.screen.name : "") + || (Stage.GlobalStates.editModeMonitor === (root.screen?.name ?? "") + && Stage.GlobalStates.editProgress > 0) width: 0 height: 0 diff --git a/ryoku/shell/quickshell/shell/modules/dock/ii/Dock.qml b/ryoku/shell/quickshell/shell/modules/dock/ii/Dock.qml index 490aba2be5..963a762c44 100755 --- a/ryoku/shell/quickshell/shell/modules/dock/ii/Dock.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/ii/Dock.qml @@ -364,7 +364,11 @@ Scope { exclusiveZone: (dock.pinned && reveal) ? unmagnifiedThickness : 0 WlrLayershell.namespace: "quickshell:dock" - WlrLayershell.layer: WlrLayer.Overlay + // Pinned docks remain part of the desktop. An auto-hidden dock must + // own its edge above app windows or neither its reveal strip nor the + // revealed tray can receive the pointer. + WlrLayershell.layer: (GlobalStates.editMode || GlobalStates.editProgress > 0) + ? WlrLayer.Overlay : (dock.pinned ? WlrLayer.Bottom : WlrLayer.Top) color: "transparent" readonly property bool popupOnThisScreen: { const popup = Stage.GlobalStates.iiDockPopup; diff --git a/ryoku/shell/quickshell/shell/modules/dock/ii/DockContent.qml b/ryoku/shell/quickshell/shell/modules/dock/ii/DockContent.qml index c7ed675cd1..0480155c0a 100755 --- a/ryoku/shell/quickshell/shell/modules/dock/ii/DockContent.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/ii/DockContent.qml @@ -516,6 +516,11 @@ Item { return lookup; } + // Repeater.count can settle before itemAt() exposes the delegates. Island + // surfaces read this revision so their endpoint bindings retry as each + // wrapper becomes available instead of waiting for an app-list change. + property int _itemDelegateRevision: 0 + function _islandKind(item) { if (!item) return "single"; @@ -1370,6 +1375,7 @@ Item { return root.isVertical ? columnItemRepeater : itemRepeater; } function getItemWrapper(index) { + void root._itemDelegateRevision; var repeater = _getActiveRepeater(); return repeater ? repeater.itemAt(index) : null; } @@ -2203,7 +2209,7 @@ Item { const result = []; for (const app of allApps) { const toplevels = Array.from(app.toplevels ?? []).filter(toplevel => { - const name = windowOutputs.get(toplevel); + const name = windowOutputs.get(toplevel) ?? toplevel.output; // Keep an unattributed window visible until Wm can join it. return name === undefined || name === "" || name === targetName; }); @@ -3118,6 +3124,8 @@ Item { // each dock item (and its nested Loader tree) exists once. model: root.isVertical ? null : dockItemModel delegate: unifiedItemDelegate + onItemAdded: root._itemDelegateRevision++ + onItemRemoved: root._itemDelegateRevision++ } } @@ -3137,6 +3145,8 @@ Item { // same active repeater. model: root.isVertical ? dockItemModel : null delegate: unifiedItemDelegate + onItemAdded: root._itemDelegateRevision++ + onItemRemoved: root._itemDelegateRevision++ } } } diff --git a/ryoku/shell/quickshell/shell/modules/dock/ii/bridge/DockStore.qml b/ryoku/shell/quickshell/shell/modules/dock/ii/bridge/DockStore.qml index 380aa0e7a3..a2b703ffb7 100644 --- a/ryoku/shell/quickshell/shell/modules/dock/ii/bridge/DockStore.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/ii/bridge/DockStore.qml @@ -82,7 +82,7 @@ Singleton { property var _memo: ({ state: "", data: {}, defaults: {} }) function snapshot(): string { - return JSON.stringify(ShellCfg.Config.dock ?? {}); + return ShellCfg.Dock.iconRev + ":" + JSON.stringify(ShellCfg.Config.dock ?? {}); } function pull(path: string, fallback: var): var { @@ -126,7 +126,12 @@ Singleton { if (path === "order") { const own = root.frame().order; if (own !== undefined && own !== null) - return Array.from(own); + return Array.from(own, entry => { + const key = String(entry); + const prefix = key.startsWith("app:") ? "app:" + : key.startsWith("runningApp:") ? "runningApp:" : ""; + return prefix ? prefix + ShellCfg.Dock.canonicalId(key.substring(prefix.length)) : key; + }); const built = ["pin"]; const pins = root.sharedPinned(); for (let i = 0; i < pins.length; i++) diff --git a/ryoku/shell/quickshell/shell/modules/dock/ii/services/TaskbarApps.qml b/ryoku/shell/quickshell/shell/modules/dock/ii/services/TaskbarApps.qml index 3c5cd07a25..8b320b2d42 100644 --- a/ryoku/shell/quickshell/shell/modules/dock/ii/services/TaskbarApps.qml +++ b/ryoku/shell/quickshell/shell/modules/dock/ii/services/TaskbarApps.qml @@ -4,7 +4,9 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import stage.services +import inir.services as InirServices import ".." +import Ryoku.Ui.Singletons as RyokuUi Singleton { id: root @@ -16,7 +18,7 @@ Singleton { if (!appId) return null if (_desktopEntryCache.hasOwnProperty(appId)) return _desktopEntryCache[appId] - const entry = DesktopEntries.heuristicLookup(appId) + const entry = InirServices.AppSearch.lookupDesktopEntry(appId, true) _desktopEntryCache[appId] = entry ?? null return _desktopEntryCache[appId] } @@ -34,15 +36,17 @@ Singleton { target: DesktopEntries function onApplicationsChanged() { root.invalidateDesktopEntryCache() } } + Connections { + target: InirServices.AppSearch + function onListChanged() { root.invalidateDesktopEntryCache() } + } // ── App ID normalization ────────────────────────────────────────────── // Strips the .desktop suffix and lowercases for consistent comparisons function normalizeAppId(appId) { if (!appId) return "" - let id = appId.toLowerCase().trim() - if (id.endsWith(".desktop")) - id = id.substring(0, id.length - 8) - return id + const entry = getCachedDesktopEntry(appId) + return String(entry?.id || appId).toLowerCase().trim().replace(/\.desktop$/i, "") } // ── Pinned app helpers ──────────────────────────────────────────────── @@ -282,32 +286,59 @@ Singleton { for (const appId of pinnedApps) { if (appId) pinnedMap.set(appId, { pinned: true, toplevels: [] }) } - - for (const toplevel of ToplevelManager.toplevels.values) { - if (!toplevel?.appId) continue - if (ignoredRegexes.some(re => re.test(toplevel.appId))) continue - - const normToplevel = normalizeAppId(toplevel.appId) + const directWindows = new Map() + function addWindow(appId, toplevel) { + if (!appId || ignoredRegexes.some(re => re.test(appId))) return + const normToplevel = normalizeAppId(appId) let matchedKey = null for (const key of pinnedMap.keys()) { if (normalizeAppId(key) === normToplevel) { matchedKey = key; break } } - + if (matchedKey === null) { + for (const key of pinnedMap.keys()) { + const startupClass = getCachedDesktopEntry(key)?.startupClass + if (startupClass && String(startupClass).toLowerCase() === String(appId).toLowerCase().replace(/\.desktop$/i, "")) { + matchedKey = key + break + } + } + } if (matchedKey !== null) { pinnedMap.get(matchedKey).toplevels.push(toplevel) } else { - const id = toplevel.appId - if (!unpinnedMap.has(id)) - unpinnedMap.set(id, { pinned: false, toplevels: [] }) - unpinnedMap.get(id).toplevels.push(toplevel) + if (!unpinnedMap.has(normToplevel)) + unpinnedMap.set(normToplevel, { appId: appId, pinned: false, toplevels: [] }) + unpinnedMap.get(normToplevel).toplevels.push(toplevel) } } + for (const toplevel of ToplevelManager.toplevels.values) { + if (!toplevel?.appId) continue + const norm = normalizeAppId(toplevel.appId) + directWindows.set(norm, (directWindows.get(norm) ?? 0) + 1) + addWindow(toplevel.appId, toplevel) + } + const providerWindows = new Map() + for (const window of (RyokuUi.Wm.windows ?? [])) { + if (!window?.appId) continue + const norm = normalizeAppId(window.appId) + const count = (providerWindows.get(norm) ?? 0) + 1 + providerWindows.set(norm, count) + if (count <= (directWindows.get(norm) ?? 0)) continue + const windowId = window.id + addWindow(window.appId, window.toplevel ?? { + appId: window.appId, + output: window.output, + activated: RyokuUi.Wm.focusedWindow?.id === windowId, + activate: () => RyokuUi.Wm.focusWindow(windowId), + close: () => RyokuUi.Wm.closeWindow(windowId) + }) + } const values = [] for (const [key, value] of pinnedMap) values.push({ appId: key, toplevels: value.toplevels, pinned: true }) - for (const [key, value] of unpinnedMap) - values.push({ appId: key, toplevels: value.toplevels, pinned: false }) + for (const [, value] of unpinnedMap) + values.push({ appId: value.appId, toplevels: value.toplevels, pinned: false }) return values } } diff --git a/ryoku/shell/quickshell/shell/modules/launcher/shared/Singletons/Scheme.qml b/ryoku/shell/quickshell/shell/modules/launcher/shared/Singletons/Scheme.qml index 815814cc25..785084fd95 100644 --- a/ryoku/shell/quickshell/shell/modules/launcher/shared/Singletons/Scheme.qml +++ b/ryoku/shell/quickshell/shell/modules/launcher/shared/Singletons/Scheme.qml @@ -18,17 +18,19 @@ import Quickshell.Io * first matugen run; they sit inside the band, so they render unchanged. */ Singleton { + id: root + property var preview: ({}) // The shell surface ramp, matched to the wallpaper's terminal background, // tone-mapped into the dark band (tiles lighter, recesses darker, hairline // borders lighter still). - readonly property color base: shade(adapter.background) + readonly property color base: shade(preview.background || adapter.background) readonly property color elevated: tone(base, 0.05) readonly property color deep: tone(base, -0.03) readonly property color line: tone(base, 0.14) // Alias kept for the blob fill in shell.qml. readonly property color surface: base - readonly property color accent: legible(vivid(adapter.color4), elevated, 3.0) - readonly property color accent2: legible(vivid(adapter.color5), elevated, 3.0) + readonly property color accent: legible(vivid(preview.color4 || preview.primary || adapter.color4), elevated, 3.0) + readonly property color accent2: legible(vivid(preview.color5 || preview.secondary || adapter.color5), elevated, 3.0) // Tone-map the wallpaper background into the shell's dark band, hue kept. // HSV value inside [0.08, 0.26] passes through; pure black lifts to a soft @@ -100,4 +102,15 @@ Singleton { property color color5: "#e83b30" } } + FileView { + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } } diff --git a/ryoku/shell/quickshell/shell/modules/launcher/shared/providers/actions/catalog.js b/ryoku/shell/quickshell/shell/modules/launcher/shared/providers/actions/catalog.js index ac0f4fac90..a51972da6e 100644 --- a/ryoku/shell/quickshell/shell/modules/launcher/shared/providers/actions/catalog.js +++ b/ryoku/shell/quickshell/shell/modules/launcher/shared/providers/actions/catalog.js @@ -31,7 +31,7 @@ var CATALOG = [ { id: "media-next", name: "Next Track", category: "Media", icon: "skip-next", exec: ["playerctl", "next"] }, { id: "media-previous", name: "Previous Track", category: "Media", icon: "skip-prev", exec: ["playerctl", "previous"] }, { id: "recognize-music", name: "Recognize Music", category: "Media", icon: "music", exec: ["ryoku-cmd-songrec"] }, - { id: "media-visualizer", name: "Audio Visualizer", category: "Media", icon: "wave", exec: ["ryoku-shell", "visualizer-overlay"] }, + { id: "media-visualizer", name: "Audio Visualizer", category: "Media", icon: "wave", exec: ["ryoku-shell", "visualizer"] }, { id: "open-settings", name: "Ryoku Settings", category: "Settings", icon: "settings", exec: ["sh", "-c", "flock -n -o /tmp/ryoku-hub.lock qs -c hub"] }, { id: "keybind-legend", name: "Keybind Reference", category: "Settings", icon: "keyboard", exec: ["sh", "-c", "flock -n -o /tmp/ryoku-keys.lock qs -c keys"] }, diff --git a/ryoku/shell/quickshell/shell/modules/launcher/variants/main/BrandMark.qml b/ryoku/shell/quickshell/shell/modules/launcher/variants/main/BrandMark.qml index 270533f1dd..ad6df0df05 100644 --- a/ryoku/shell/quickshell/shell/modules/launcher/variants/main/BrandMark.qml +++ b/ryoku/shell/quickshell/shell/modules/launcher/variants/main/BrandMark.qml @@ -1,11 +1,12 @@ import QtQuick import "../../shared/Singletons" +import Ryoku.Ui.Singletons Text { property real size: 13 property int weight: Font.Medium - text: "力" + text: UiPreferences.brandMark("力") color: Theme.brand font.family: Theme.font font.weight: weight diff --git a/ryoku/shell/quickshell/shell/modules/launcher/variants/main/Preview.qml b/ryoku/shell/quickshell/shell/modules/launcher/variants/main/Preview.qml index dedc6948e7..0990c092cf 100644 --- a/ryoku/shell/quickshell/shell/modules/launcher/variants/main/Preview.qml +++ b/ryoku/shell/quickshell/shell/modules/launcher/variants/main/Preview.qml @@ -50,7 +50,7 @@ Item { anchors.left: parent.left anchors.leftMargin: Tokens.s4 anchors.verticalCenter: parent.verticalCenter - text: "力" + text: UiPreferences.brandMark("力") color: Tokens.sun font.family: Tokens.jp font.pixelSize: Tokens.fRow diff --git a/ryoku/shell/quickshell/shell/modules/launcher/variants/okshell/Main.qml b/ryoku/shell/quickshell/shell/modules/launcher/variants/okshell/Main.qml index 8de6271217..9ebe1a5b11 100644 --- a/ryoku/shell/quickshell/shell/modules/launcher/variants/okshell/Main.qml +++ b/ryoku/shell/quickshell/shell/modules/launcher/variants/okshell/Main.qml @@ -17,11 +17,14 @@ Scope { // ── palette: the live matugen roles ───────────────────────────────────── property var pal: ({}) // colors.json: the live wallpaper palette + property var preview: ({}) property var named: null // shell.json themePalette: the active coded theme // Resolve like the pill's Theme: a selected coded theme wins, then the live // wallpaper palette, then the compiled default. Without the coded-theme layer // the launcher only tracked the wallpaper and ignored the fixed presets. function role(k, d) { + if (typeof root.preview[k] === "string" && root.preview[k].length > 0) + return root.preview[k]; if (root.named && typeof root.named[k] === "string" && root.named[k].length > 0) return root.named[k]; const v = root.pal[k]; @@ -49,6 +52,17 @@ Scope { } } + FileView { + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } // the active coded theme's palette, resolved by the daemon into shell.json's // top-level themePalette key (absent for the dynamic Wallpaper/Default variants). FileView { diff --git a/ryoku/shell/quickshell/shell/modules/notifications/NotificationCard.qml b/ryoku/shell/quickshell/shell/modules/notifications/NotificationCard.qml index f7d2b509ae..3a2dfca16a 100644 --- a/ryoku/shell/quickshell/shell/modules/notifications/NotificationCard.qml +++ b/ryoku/shell/quickshell/shell/modules/notifications/NotificationCard.qml @@ -9,15 +9,15 @@ import "../../components" // (contract 07 sec 2.3). A bordered surface tile with a header (app name, time, // and an open/expand/close cluster), a bold summary, an optional body, and one // primary button per action. The reference text-only look remains the default; -// the opt-in image setting adds a token-sized thumbnail without reserving space -// when no source resolves. +// the opt-in image setting adds a token-sized thumbnail row when an image, +// application icon, or fallback glyph resolves. // // Popups pass `compact: true`: the body clamps to two lines and the actions -// hide, so the toast stays a tidy glance; the expand chevron slides the card -// open to the full body and its action buttons (Motion.rowReveal). The open -// button surfaces the freedesktop "default" action, which is otherwise -// unreachable from a toast. The history panel leaves `compact` false, so it is -// unchanged bar the open button appearing when an app sent a default action. +// hide, so the toast stays a tidy glance; the expand chevron reveals the full +// body and its action buttons. The open button surfaces the freedesktop +// "default" action, which is otherwise unreachable from a toast. The history +// panel leaves `compact` false, so it is unchanged bar the open button appearing +// when an app sent a default action. Rectangle { id: card @@ -41,12 +41,46 @@ Rectangle { // Per-monitor UI scale, threaded from the popup surface (default 1 = no-op, // so the history panel that composes this card unchanged stays identical). property real us: 1 - - // The height Behavior below must not animate the card's initial layout: a - // fresh toast would otherwise grow tall as it arrives and churn the stack. - // Armed after creation, it eases only the user-driven expand/collapse. - property bool ready: false - Component.onCompleted: card.ready = true + // Only explicit expand/collapse clicks ease the frame height. Image loads + // and every other layout change snap to their measured height so the popup + // surface never trails content that has already moved. + readonly property real targetImplicitHeight: body.implicitHeight + Theme.paddingMd * 2 * card.us + property real animatedImplicitHeight: 0 + property bool userHeightChange: false + Component.onCompleted: card.animatedImplicitHeight = card.targetImplicitHeight + function toggleExpanded() { + card.userHeightChange = true; + card.expanded = !card.expanded; + heightAnimationStart.restart(); + } + function animateUserHeightChange() { + heightAnimation.stop(); + const targetHeight = card.targetImplicitHeight; + heightAnimation.from = card.animatedImplicitHeight; + heightAnimation.to = targetHeight; + heightAnimation.start(); + card.userHeightChange = false; + } + onTargetImplicitHeightChanged: { + if (card.userHeightChange) { + heightAnimationStart.restart(); + return; + } + heightAnimation.stop(); + card.animatedImplicitHeight = card.targetImplicitHeight; + } + Timer { + id: heightAnimationStart + interval: 16 + onTriggered: card.animateUserHeightChange() + } + NumberAnimation { + id: heightAnimation + target: card + property: "animatedImplicitHeight" + duration: Motion.rowReveal + easing.type: Motion.rowRevealCurve + } // Only real actions get a button: the freedesktop "default" action (surfaced // as the open button, not a row button) and any action with no label are @@ -117,11 +151,7 @@ Rectangle { border.width: card.unifiedFrame ? 0 : Theme.borderWidth * card.us border.color: Theme.outline color: card.unifiedFrame ? "transparent" : Theme.surface - implicitHeight: body.implicitHeight + Theme.paddingMd * 2 * card.us - // Ease the expand/collapse: the body clamp and the actions toggle change the - // content height in a step, and this glides the card (and the stack it sits - // in) between the two heights instead of snapping. - Behavior on implicitHeight { enabled: card.ready; NumberAnimation { duration: Motion.rowReveal; easing.type: Motion.rowRevealCurve } } + implicitHeight: card.animatedImplicitHeight // A hidden, unclamped copy of the body, used only to learn whether the real // body would overflow the compact clamp (so the chevron appears only when it @@ -232,7 +262,7 @@ Rectangle { glyph: "expand_more" visible: card.expandable iconRotation: card.expanded ? 180 : 0 - onClicked: card.expanded = !card.expanded + onClicked: card.toggleExpanded() } HeaderButton { @@ -317,8 +347,8 @@ Rectangle { } // Actions: one primary button per action, full width (contract 07 sec - // 2.3). Hidden while a popup is compact and collapsed; the expand chevron - // brings them in, fading up as the card grows. + // 2.3). Hidden while a popup is compact and collapsed; the expand + // chevron brings them in with a short fade. Column { id: actionCol width: parent.width diff --git a/ryoku/shell/quickshell/shell/modules/notifications/NotificationPopups.qml b/ryoku/shell/quickshell/shell/modules/notifications/NotificationPopups.qml index 88462d36d0..1c2b472cbd 100644 --- a/ryoku/shell/quickshell/shell/modules/notifications/NotificationPopups.qml +++ b/ryoku/shell/quickshell/shell/modules/notifications/NotificationPopups.qml @@ -229,12 +229,22 @@ PanelWindow { && Designs.notificationReady && win.notificationUrl !== "" && win.notificationUrl !== win.failedNotificationUrl + // ListView keeps its first delegate measurement unless a later + // Loader height is pushed through the delegate and force-laid out. + property real shippedHeight: 0 + function syncShippedHeight() { + const item = shippedLoader.item; + popupDelegate.shippedHeight = item + ? Math.max(item.implicitHeight, item.targetImplicitHeight) + : 0; + } width: ListView.view ? ListView.view.width : implicitWidth implicitHeight: storeLoader.visible && storeLoader.item ? storeLoader.item.implicitHeight - : shippedLoader.item ? shippedLoader.item.implicitHeight : 0 + : popupDelegate.shippedHeight height: implicitHeight + onHeightChanged: if (ListView.view) ListView.view.forceLayout() transformOrigin: win.position === "Left" ? Item.TopLeft : win.position === "Center" ? Item.Top : win.position === "BottomRight" ? Item.BottomRight : Item.TopRight @@ -243,9 +253,10 @@ PanelWindow { id: shippedLoader width: popupDelegate.width - height: item ? item.implicitHeight : 0 + height: popupDelegate.shippedHeight active: !popupDelegate.storeRuns || storeLoader.status === Loader.Error visible: active + onItemChanged: popupDelegate.syncShippedHeight() sourceComponent: Component { NotificationCard { width: popupDelegate.width @@ -263,6 +274,11 @@ PanelWindow { } } } + Connections { + target: shippedLoader.item + function onImplicitHeightChanged() { popupDelegate.syncShippedHeight(); } + function onTargetImplicitHeightChanged() { popupDelegate.syncShippedHeight(); } + } Loader { id: storeLoader diff --git a/ryoku/shell/quickshell/shell/modules/osd/OsdSource.qml b/ryoku/shell/quickshell/shell/modules/osd/OsdSource.qml index 2963f27745..d10a3604e3 100644 --- a/ryoku/shell/quickshell/shell/modules/osd/OsdSource.qml +++ b/ryoku/shell/quickshell/shell/modules/osd/OsdSource.qml @@ -105,7 +105,10 @@ QtObject { property Connections audioEvents: Connections { target: root.audio - function onVolumesChanged() { root.flash(false); } + function onVolumesChanged() { + if (!root.isMic || (root.audio && OsdFeed.acceptsMicVolume(root.audio.volume))) + root.flash(false); + } function onMutedChanged() { root.flash(false); } } @@ -118,6 +121,10 @@ QtObject { target: root.isVolume ? OsdFeed : null function onPreviewSeqChanged() { root.flash(true); } } + property Connections micRequests: Connections { + target: root.isMic ? OsdFeed : null + function onMicRequested() { root.flash(true); } + } // KeyboardLayout only carries the layout name; the old signatureChanged // handler named a signal it never had, so a switch never showed. diff --git a/ryoku/shell/quickshell/shell/modules/overview/Singletons/Scheme.qml b/ryoku/shell/quickshell/shell/modules/overview/Singletons/Scheme.qml index 609b9aa458..1afe4851d4 100644 --- a/ryoku/shell/quickshell/shell/modules/overview/Singletons/Scheme.qml +++ b/ryoku/shell/quickshell/shell/modules/overview/Singletons/Scheme.qml @@ -12,11 +12,13 @@ import Quickshell.Io * right before the first matugen run. */ Singleton { - readonly property color base: shade(adapter.background) + id: root + property var preview: ({}) + readonly property color base: shade(preview.background || adapter.background) readonly property color elevated: tone(base, 0.05) readonly property color deep: tone(base, -0.03) readonly property color line: tone(base, 0.14) - readonly property color accent: legible(vivid(adapter.color4), elevated, 3.0) + readonly property color accent: legible(vivid(preview.color4 || preview.primary || adapter.color4), elevated, 3.0) // Tone-map the wallpaper background into the shell's dark band, hue kept: // HSV value inside [0.08, 0.26] passes through, pure black lifts to a soft @@ -82,4 +84,15 @@ Singleton { property color color4: "#e2342a" } } + FileView { + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } } diff --git a/ryoku/shell/quickshell/shell/modules/overview/Singletons/Theme.qml b/ryoku/shell/quickshell/shell/modules/overview/Singletons/Theme.qml index eb8463dc37..3f14b78728 100644 --- a/ryoku/shell/quickshell/shell/modules/overview/Singletons/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/overview/Singletons/Theme.qml @@ -1,6 +1,7 @@ pragma Singleton import QtQuick import Quickshell +import Ryoku.Ui.Singletons as Ui // Overview palette + geometry, a focused twin of the shell Theme (docs/ui-ux.md // tokens). Accent follows the wallpaper when Match wallpaper is on; the fixed @@ -41,7 +42,7 @@ Singleton { // brand mark + name, user-overridable via ~/.config/ryoku/brand.json (Shell -> // Global). defaults to the 力 seal / "Ryoku". BrandMark renders `mark`, or // `markSource` (an image) when set. Ryoku's own apps never read these. - readonly property string mark: Config.markText.length > 0 ? Config.markText : "\u529b" + readonly property string mark: Ui.UiPreferences.brandMark(Config.markText) readonly property string markSource: Config.markImage readonly property bool markTint: Config.markTint readonly property string brandName: Config.brandName.length > 0 ? Config.brandName : "Ryoku" diff --git a/ryoku/shell/quickshell/shell/modules/sidebar/ControlsBoard.qml b/ryoku/shell/quickshell/shell/modules/sidebar/ControlsBoard.qml index c01b583f1d..e96a56ee6a 100644 --- a/ryoku/shell/quickshell/shell/modules/sidebar/ControlsBoard.qml +++ b/ryoku/shell/quickshell/shell/modules/sidebar/ControlsBoard.qml @@ -31,36 +31,45 @@ Item { SessionActions.run(action); } - Column { - id: overview - anchors.left: parent.left - anchors.right: parent.right + Flickable { + id: overviewScroll + anchors.fill: parent visible: root.currentPage === "controls" - spacing: Tokens.s3 * root.s - - Repeater { - model: SidebarState.visibleSections - - delegate: Loader { - id: sectionLoader - required property var modelData - width: overview.width - active: root.currentPage === "controls" - sourceComponent: modelData.id === "vitals" ? vitalsSection - : modelData.id === "connections" ? connectionsSection - : modelData.id === "powerProfile" ? profileSection - : modelData.id === "media" ? mediaSection - : modelData.id === "levels" ? levelsSection - : bottomSection - height: item ? item.implicitHeight : 0 - opacity: active ? 1 : 0 - transform: Translate { y: sectionLoader.active ? 0 : Tokens.s2 * root.s } - - Behavior on opacity { - enabled: root.motionAllowed - NumberAnimation { - duration: SidebarState.motionDuration(Tokens.swap) - easing.type: Tokens.ease + clip: true + contentWidth: width + contentHeight: overview.implicitHeight + (overview.implicitHeight > height ? Tokens.s2 * root.s : 0) + boundsBehavior: Flickable.StopAtBounds + acceptedButtons: Qt.LeftButton + + Column { + id: overview + width: overviewScroll.width + spacing: Tokens.s3 * root.s + + Repeater { + model: SidebarState.visibleSections + + delegate: Loader { + id: sectionLoader + required property var modelData + width: overview.width + active: root.currentPage === "controls" + sourceComponent: modelData.id === "vitals" ? vitalsSection + : modelData.id === "connections" ? connectionsSection + : modelData.id === "powerProfile" ? profileSection + : modelData.id === "media" ? mediaSection + : modelData.id === "levels" ? levelsSection + : bottomSection + height: item ? item.implicitHeight : 0 + opacity: active ? 1 : 0 + transform: Translate { y: sectionLoader.active ? 0 : Tokens.s2 * root.s } + + Behavior on opacity { + enabled: root.motionAllowed + NumberAnimation { + duration: SidebarState.motionDuration(Tokens.swap) + easing.type: Tokens.ease + } } } } diff --git a/ryoku/shell/quickshell/shell/modules/sidebar/ControlsConnections.qml b/ryoku/shell/quickshell/shell/modules/sidebar/ControlsConnections.qml index a35d4a3b70..0ce47efe49 100644 --- a/ryoku/shell/quickshell/shell/modules/sidebar/ControlsConnections.qml +++ b/ryoku/shell/quickshell/shell/modules/sidebar/ControlsConnections.qml @@ -45,7 +45,8 @@ Item { ? I18n.tr("%1 · %2 GHz · %3%").arg(Network.activeSsid).arg(band).arg(strength) : I18n.tr("%1 · %2%").arg(Network.activeSsid).arg(strength); } - readonly property string bluetoothDetail: !root.adapter ? I18n.tr("No adapter") + readonly property string bluetoothDetail: BluetoothPower.blocked ? I18n.tr("Blocked") + : !root.adapter ? I18n.tr("No adapter") : !root.adapter.enabled ? I18n.tr("Radio off") : root.connectedDevices.length === 1 ? BtLink.label(root.connectedDevices[0]) : root.connectedDevices.length > 1 ? I18n.tr("%1 connected").arg(root.connectedDevices.length) diff --git a/ryoku/shell/quickshell/shell/modules/sidebar/SidebarPlugins.qml b/ryoku/shell/quickshell/shell/modules/sidebar/SidebarPlugins.qml index 54a955d889..77f7796c13 100644 --- a/ryoku/shell/quickshell/shell/modules/sidebar/SidebarPlugins.qml +++ b/ryoku/shell/quickshell/shell/modules/sidebar/SidebarPlugins.qml @@ -8,6 +8,7 @@ Item { id: root property bool active: false + property bool discovered: false property var plugins: [] property var availablePlugins: [] @@ -21,14 +22,8 @@ Item { width: 0 height: 0 - onActiveChanged: { - if (root.active) root.reload(); - else discover.running = false; - } - function reload() { - discover.running = false; - discover.running = root.active; + root.discovered = false; } function normalizedEntry(plugin) { @@ -93,12 +88,13 @@ Item { Process { id: discover command: ["bash", root.discoverScript, "--all"] - running: root.active + running: root.active && !root.discovered stdout: StdioCollector { onStreamFinished: { var parsed = []; try { parsed = JSON.parse(text || "[]"); } catch (error) { parsed = []; } root.syncPlugins(Array.isArray(parsed) ? parsed : []); + root.discovered = true; } } } @@ -106,7 +102,7 @@ Item { FileView { path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/plugins.json" - watchChanges: root.active + watchChanges: true atomicWrites: true printErrors: false onFileChanged: root.reload() @@ -114,7 +110,7 @@ Item { FileView { path: root.stateHome + "/ryoku/store/revision.json" - watchChanges: root.active + watchChanges: true atomicWrites: true printErrors: false onFileChanged: root.reload() diff --git a/ryoku/shell/quickshell/shell/modules/sidebar/cards/SystemBluetoothPage.qml b/ryoku/shell/quickshell/shell/modules/sidebar/cards/SystemBluetoothPage.qml index 2b874006f7..0e73d1e390 100644 --- a/ryoku/shell/quickshell/shell/modules/sidebar/cards/SystemBluetoothPage.qml +++ b/ryoku/shell/quickshell/shell/modules/sidebar/cards/SystemBluetoothPage.qml @@ -16,8 +16,8 @@ Item { signal backRequested() readonly property var adapter: Bluetooth.defaultAdapter - readonly property bool adapterAvailable: root.adapter !== null - readonly property bool adapterEnabled: root.adapterAvailable && root.adapter.enabled + readonly property bool adapterAvailable: BluetoothPower.available + readonly property bool adapterEnabled: root.adapter !== null && root.adapter.enabled readonly property bool scanning: root.adapterEnabled && root.adapter.discovering property var discoveryAdapter: null property string busyAddress: "" @@ -369,7 +369,8 @@ Item { spacing: Tokens.s1 * root.s Text { width: parent.width - text: !root.adapterAvailable ? I18n.tr("No Bluetooth hardware") + text: BluetoothPower.blocked ? I18n.tr("Bluetooth is blocked") + : !root.adapterAvailable ? I18n.tr("No Bluetooth hardware") : !root.adapterEnabled ? I18n.tr("Bluetooth is off") : root.connectedDevices.length === 1 ? BtLink.label(root.connectedDevices[0]) : root.connectedDevices.length > 1 ? I18n.tr("%1 devices connected").arg(root.connectedDevices.length) diff --git a/ryoku/shell/quickshell/shell/modules/stage/StageEditorHost.qml b/ryoku/shell/quickshell/shell/modules/stage/StageEditorHost.qml index 84d784f994..aac50d47b6 100644 --- a/ryoku/shell/quickshell/shell/modules/stage/StageEditorHost.qml +++ b/ryoku/shell/quickshell/shell/modules/stage/StageEditorHost.qml @@ -54,15 +54,18 @@ Scope { Connections { target: GlobalStates function onEditModeChanged() { - if (!GlobalStates.editMode && StageCfg.StageSession.widgets) + if (!GlobalStates.editMode && GlobalStates._editReopenMonitor === "" + && StageCfg.StageSession.widgets) StageCfg.StageSession.leave(); } function onEditModeMonitorChanged() { - if (GlobalStates.editMode - && StageCfg.StageSession.widgets + if (StageCfg.StageSession.widgets && GlobalStates.editModeMonitor !== "" - && StageCfg.StageSession.monitor !== GlobalStates.editModeMonitor) + && StageCfg.StageSession.monitor !== GlobalStates.editModeMonitor) { + StageCfg.StageSession.selection = []; + StageCfg.StageSession.inspecting = ""; StageCfg.StageSession.monitor = GlobalStates.editModeMonitor; + } } } } diff --git a/ryoku/shell/quickshell/shell/modules/stage/StageVisualizerPage.qml b/ryoku/shell/quickshell/shell/modules/stage/StageVisualizerPage.qml index f86d151799..51fb916a32 100644 --- a/ryoku/shell/quickshell/shell/modules/stage/StageVisualizerPage.qml +++ b/ryoku/shell/quickshell/shell/modules/stage/StageVisualizerPage.qml @@ -37,32 +37,33 @@ Item { return s.length === 1 ? "0" + s : s; }).join("").toUpperCase(); } - function selectInstance(index) { + function selectInstance(instanceId) { page.finishPlace(); - page.cfg.setActive(index); - StageCfg.StageSession.select("visualizer:" + index); + if (!page.cfg.setActive(instanceId)) + return; + StageCfg.StageSession.select("visualizer:" + instanceId); } function addInstance() { if (page.cfg.count >= page.cfg.maxVisualizers) return; - const index = page.provider ? page.provider.addVisualizer() : page.cfg.addVisualizer(); - if (index < 0) + const instanceId = page.provider ? page.provider.addVisualizer() : page.cfg.addVisualizer(); + if (!instanceId) return; page.cfg.setEnabled(true); - page.selectInstance(index); + page.selectInstance(instanceId); } function toggleShown() { - const id = "visualizer:" + page.cfg.active; + const id = "visualizer:" + page.cfg.activeId; if (page.cfg.enabled) { StageIsland.Config.removeWidgetInstance("visualizer"); StageCfg.StageSession.remove(id); } else { StageIsland.Config.addWidgetToDesktop("visualizer"); - page.selectInstance(page.cfg.active); + page.selectInstance(page.cfg.activeId); } } function placeChange(change) { - const id = "visualizer:" + page.cfg.active; + const id = "visualizer:" + page.cfg.activeId; if (page.placeBefore === null || page.placeId !== id) { page.finishPlace(); page.placeId = id; @@ -80,7 +81,7 @@ Item { } function placeOnce(change) { page.finishPlace(); - const id = "visualizer:" + page.cfg.active; + const id = "visualizer:" + page.cfg.activeId; const before = page.provider ? page.provider.snapshot(id) : null; change(); if (page.provider) @@ -104,7 +105,7 @@ Item { function onActiveChanged() { if (page.placeBefore) page.finishPlace(); - StageCfg.StageSession.select("visualizer:" + page.cfg.active); + StageCfg.StageSession.select("visualizer:" + page.cfg.activeId); } } @@ -282,11 +283,17 @@ Item { options: { const out = []; for (let i = 0; i < page.cfg.count; ++i) - out.push(String(i + 1)); + out.push(page.cfg.labelAt(i)); return out; } - current: String(page.cfg.active + 1) - onChose: key => page.selectInstance(parseInt(key) - 1) + current: page.cfg.labelAt(page.cfg.active) + onChose: label => { + for (let i = 0; i < page.cfg.count; ++i) + if (page.cfg.labelAt(i) === label) { + page.selectInstance(page.cfg.idAt(i)); + return; + } + } } Btn { text: I18n.tr("Add") diff --git a/ryoku/shell/quickshell/shell/modules/stage/StageWidgetPage.qml b/ryoku/shell/quickshell/shell/modules/stage/StageWidgetPage.qml index 088ba9b37a..ef33408894 100644 --- a/ryoku/shell/quickshell/shell/modules/stage/StageWidgetPage.qml +++ b/ryoku/shell/quickshell/shell/modules/stage/StageWidgetPage.qml @@ -255,9 +255,9 @@ Item { function routeVisualizer() { if (page.target.indexOf("visualizer:") !== 0) return false; - const index = Number(page.target.slice("visualizer:".length)); - if (isFinite(index)) - VizCfg.Config.setActive(index); + const instanceId = page.target.slice("visualizer:".length); + if (instanceId !== "") + VizCfg.Config.setActive(instanceId); StageCfg.StageSession.inspecting = ""; StageCfg.StageSession.openSection("visualizer"); return true; diff --git a/ryoku/shell/quickshell/shell/modules/stage/StageWidgetProvider.qml b/ryoku/shell/quickshell/shell/modules/stage/StageWidgetProvider.qml index a3b899be87..1a5ca672b2 100644 --- a/ryoku/shell/quickshell/shell/modules/stage/StageWidgetProvider.qml +++ b/ryoku/shell/quickshell/shell/modules/stage/StageWidgetProvider.qml @@ -180,13 +180,13 @@ Scope { function addVisualizer() { const v = VizCfg.Config; if (v.count >= v.maxVisualizers) - return -1; + return ""; const wasEnabled = v.enabled; - const index = v.addVisualizer(); - if (index < 0) - return -1; + const stableId = v.addVisualizer(); + if (!stableId) + return ""; v.setEnabled(true); - const id = "visualizer:" + index; + const id = "visualizer:" + stableId; const after = root.snapshot(id); GlobalStates.editHistoryPush({ "undo": () => { @@ -196,7 +196,7 @@ Scope { }, "redo": () => root.restore(id, after) }); - return index; + return stableId; } // The drop point is the widget's top-left in screen px; the visualiser has @@ -222,12 +222,13 @@ Scope { root.restore(instanceId, null); } - function visualizerIndex(instanceId) { + function visualizerId(instanceId) { const text = "" + instanceId; - if (text.indexOf("visualizer:") !== 0) - return -1; - const index = parseInt(text.slice(11)); - return isNaN(index) ? -1 : index; + return text.indexOf("visualizer:") === 0 ? text.slice(11) : ""; + } + function visualizerIndex(instanceId) { + const stableId = root.visualizerId(instanceId); + return stableId === "" ? -1 : VizCfg.Config.indexOfId(stableId); } // An indexed visualizer snapshot is its complete settings object plus the @@ -248,6 +249,7 @@ Scope { return null; return { viz: true, + id: VizCfg.Config.idAt(vi), index: vi, count: VizCfg.Config.count, data: VizCfg.Config.cloneData(VizCfg.Config.dataAt(vi)) @@ -260,7 +262,7 @@ Scope { return { visualizers: true, enabled: VizCfg.Config.enabled, - active: VizCfg.Config.active, + active: VizCfg.Config.activeId, instances: all }; } @@ -288,20 +290,21 @@ Scope { root.enqueue([root.placeTool, pid, "enabled", "true"]); return; } - const vi = root.visualizerIndex(instanceId); - if (vi >= 0) { + const stableId = root.visualizerId(instanceId); + if (stableId !== "") { const v = VizCfg.Config; + const vi = v.indexOfId(stableId); if (snap === null || snap === undefined) { - v.setActive(vi); - v.removeVisualizer(vi); + if (vi >= 0) + v.removeVisualizer(stableId); return; } - if (v.count < snap.count) + if (vi < 0) v.insertVisualizer(snap.index, snap.data); else - v.replaceVisualizer(snap.index, snap.data); + v.replaceVisualizer(vi, snap.data); v.setEnabled(true); - v.setActive(snap.index); + v.setActive(snap.id || stableId); return; } if (instanceId === "visualizer") { diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/AuraMotion.qml b/ryoku/shell/quickshell/shell/modules/visualizer/AuraMotion.qml index 33c6a299f3..701810688d 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/AuraMotion.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/AuraMotion.qml @@ -117,7 +117,10 @@ Item { var source = motion.frozen && Spectrum.placementHolds === 0 ? [] : Spectrum.levels; - var shaped = AuraMath.applyProfile(source, + var driven = new Array(source.length); + for (var s = 0; s < source.length; s++) + driven[s] = Math.min(1, Math.pow(Math.max(0, source[s]), 0.72) * motion.cfg.gain); + var shaped = AuraMath.applyProfile(driven, motion.cfg.auraProfile, motion.cfg.auraAccent); var raw = AuraMath.sectors(shaped); diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/InlineVisualizer.qml b/ryoku/shell/quickshell/shell/modules/visualizer/InlineVisualizer.qml index 549c26f6c9..18d9a58f4c 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/InlineVisualizer.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/InlineVisualizer.qml @@ -2,9 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick import "Singletons" -// The visualiser stack without its own surface: the desktop hosts one at -// the visualizer's scene z while parallax owns the background, so the -// bars sit between the parallax layers. Item { id: root @@ -13,7 +10,8 @@ Item { delegate: VisualizerView { id: vizView anchors.fill: parent - cfg: VizItem { data: Config.dataAt(vizView.index) } + instanceId: Config.idAt(vizView.index) + cfg: VizItem { data: Config.dataById(vizView.instanceId) } } } } diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Config.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Config.qml index 50bf99c109..979b84f356 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Config.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Config.qml @@ -7,8 +7,8 @@ import Quickshell.Io // Live config for the desktop visualiser. One or more visualisers can share the // desktop: the PRIMARY is the flat top-level config (the one Ryoku Settings' // Visualizer tab still edits and reset-to-defaults mirrors), and any EXTRAS ride -// an `extras` list, each a full per-viz object. `active` is the instance the -// on-desktop editor is tuning. The renderer paints `list` (primary + extras); +// an `extras` list, each a full per-viz object. `activeId` names the stable +// instance the on-desktop editor is tuning. The renderer paints `list` (primary + extras); // the editor reads/writes the active instance through here. // // JSON at ~/.config/ryoku/visualizer.json, watched, so a save in Settings (or a @@ -49,7 +49,7 @@ Singleton { ] function primaryData() { var d = { - "style": adapter.style, "shape": adapter.shape, + "id": adapter.instanceId, "style": adapter.style, "shape": adapter.shape, "color": adapter.color, "color2": adapter.color2, "gradient": adapter.gradient, "bars": adapter.bars, "thickness": adapter.thickness, "bloom": adapter.bloom, "reflection": adapter.reflection, "idleWave": adapter.idleWave, @@ -64,22 +64,58 @@ Singleton { return d; } function dataAt(index) { - if (index <= 0) - return root.primaryData(); - return (adapter.extras || [])[index - 1] || ({}); + const i = Math.round(Number(index)); + return isFinite(i) && i >= 0 && i < root.count ? root.list[i] : ({}); + } + function indexOfId(instanceId) { + const wanted = "" + (instanceId || ""); + if (wanted === "") + return -1; + const rows = root.list; + for (let i = 0; i < rows.length; ++i) + if (("" + (rows[i].id || "")) === wanted) + return i; + return -1; + } + function idAt(index) { + const i = Math.round(Number(index)); + const row = isFinite(i) && i >= 0 && i < root.count ? root.list[i] : null; + return row ? "" + (row.id || "") : ""; + } + function dataById(instanceId) { + const index = root.indexOfId(instanceId); + return index >= 0 ? root.dataAt(index) : ({}); + } + function labelAt(index) { + const stableId = root.idAt(index); + const match = /^viz-(\d+)$/.exec(stableId); + return match ? match[1] : stableId; + } + function allocateId() { + let serial = Math.max(1, Math.round(adapter.nextId)); + let stableId = ""; + do { + stableId = "viz-" + serial; + serial++; + } while (root.indexOfId(stableId) >= 0); + adapter.nextId = serial; + return stableId; } property var previews: ({}) - function previewAt(index) { - return root.previews["" + index] || null; + function previewAt(instanceId) { + return root.previews["" + instanceId] || null; } - function setPreview(index, nx, ny) { + function setPreview(instanceId, nx, ny) { + const key = "" + instanceId; + if (key === "") + return; const next = Object.assign({}, root.previews); - next["" + index] = { x: nx, y: ny }; + next[key] = { x: nx, y: ny }; root.previews = next; } - function clearPreview(index) { - const key = "" + index; - if (root.previews[key] === undefined) + function clearPreview(instanceId) { + const key = "" + instanceId; + if (key === "" || root.previews[key] === undefined) return; const next = Object.assign({}, root.previews); delete next[key]; @@ -87,13 +123,17 @@ Singleton { } readonly property var list: { var out = [root.primaryData()]; - var ex = adapter.extras || []; + var ex = root.cloneData(adapter.extras); for (var i = 0; i < ex.length; i++) out.push(ex[i]); return out; } readonly property int count: root.list.length - readonly property int active: Math.max(0, Math.min(root.count - 1, adapter.active)) + readonly property int active: { + const found = root.indexOfId(adapter.activeId); + return found >= 0 ? found : 0; + } + readonly property string activeId: root.idAt(root.active) readonly property var activeData: root.list[root.active] // Never more than this many at once; each adds a full-screen GPU pass. @@ -151,7 +191,7 @@ Singleton { readonly property bool hasCustomColor: act.hasCustomColor readonly property color customColor: act.customColor readonly property string colorHex: act.hasCustomColor ? act.rawColor : "" - readonly property bool gradient: adapter.active <= 0 ? (adapter.gradient === true) : (act.val("gradient", false) === true) + readonly property bool gradient: root.active === 0 ? (adapter.gradient === true) : (act.val("gradient", false) === true) readonly property bool hasColor2: act.hasColor2 readonly property color color2Value: act.color2Value readonly property string color2Hex: act.hasColor2 ? act.rawColor2 : "" @@ -164,7 +204,7 @@ Singleton { if (root.active <= 0) { adapter[key] = value; } else { - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); var e = Object.assign({}, arr[root.active - 1]); e[key] = value; arr[root.active - 1] = e; @@ -314,8 +354,10 @@ Singleton { root.poke("x", b.x); root.poke("y", b.y); } - function moveBoxAt(index, nx, ny, aspect) { - const i = Math.max(0, Math.min(root.count - 1, Math.round(index))); + function moveBoxAt(instanceId, nx, ny, aspect) { + const i = root.indexOfId(instanceId); + if (i < 0) + return; const data = root.dataAt(i); const w = Number(data.w); const h = Number(data.h); @@ -329,7 +371,7 @@ Singleton { adapter.x = b.x; adapter.y = b.y; } else { - const arr = (adapter.extras || []).slice(); + const arr = root.cloneData(adapter.extras); const entry = Object.assign({}, arr[i - 1]); entry.x = b.x; entry.y = b.y; @@ -350,7 +392,7 @@ Singleton { adapter.x = b.x; adapter.y = b.y; } else { - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); var e = Object.assign({}, arr[root.active - 1]); e.w = b.w; e.h = b.h; @@ -388,27 +430,34 @@ Singleton { function cloneData(o) { return JSON.parse(JSON.stringify(o || {})); } - function setActive(i) { - adapter.active = Math.max(0, Math.min(root.count - 1, i)); + function setActive(instance) { + const idx = typeof instance === "number" + ? Math.max(0, Math.min(root.count - 1, Math.round(instance))) + : root.indexOfId(instance); + if (idx < 0) + return false; + adapter.activeId = root.idAt(idx); settle.restart(); + return true; } function addVisualizer() { if (root.count >= root.maxVisualizers) - return -1; - var arr = (adapter.extras || []).slice(); + return ""; + var arr = root.cloneData(adapter.extras); // Seed from the active instance, nudged so the new one is not hidden // exactly under it, and given a fresh box so it is easy to grab. var seed = root.cloneData(root.activeData); + seed.id = root.allocateId(); seed.x = Math.max(-0.2, Math.min(0.8, (seed.x || 0) + 0.06)); seed.y = Math.max(-0.2, Math.min(0.8, (seed.y || 0.58) - 0.12)); arr.push(seed); adapter.extras = arr; - adapter.active = arr.length; + adapter.activeId = seed.id; settle.restart(); - return adapter.active; + return seed.id; } function applyPrimary(o) { - adapter.style = o.style; adapter.shape = o.shape; + adapter.instanceId = "" + (o.id || adapter.instanceId); adapter.style = o.style; adapter.shape = o.shape; adapter.color = o.color || ""; adapter.color2 = o.color2 || ""; adapter.gradient = o.gradient === true; adapter.bars = o.bars; adapter.thickness = o.thickness; adapter.bloom = o.bloom; @@ -430,11 +479,11 @@ Singleton { if (idx === 0) { root.applyPrimary(copy); } else { - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); arr[idx - 1] = copy; adapter.extras = arr; } - adapter.active = idx; + adapter.activeId = copy.id; settle.restart(); } function insertVisualizer(i, data) { @@ -443,18 +492,18 @@ Singleton { var idx = Math.max(0, Math.min(root.count, i)); var copy = root.cloneData(data); if (idx === 0) { - var arr0 = (adapter.extras || []).slice(); + var arr0 = root.cloneData(adapter.extras); arr0.unshift(root.cloneData(root.primaryData())); root.applyPrimary(copy); adapter.extras = arr0; } else { - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); arr.splice(idx - 1, 0, copy); adapter.extras = arr; } - adapter.active = idx; + adapter.activeId = copy.id; settle.restart(); - return idx; + return copy.id; } function replaceAll(instances, selected, on) { var rows = root.cloneData(instances); @@ -462,27 +511,37 @@ Singleton { rows = [root.primaryData()]; root.applyPrimary(rows[0]); adapter.extras = rows.slice(1, root.maxVisualizers); - adapter.active = Math.max(0, Math.min(rows.length - 1, selected || 0)); + const selectedId = typeof selected === "number" + ? "" + (rows[Math.max(0, Math.min(rows.length - 1, selected))].id || "") + : "" + (selected || rows[0].id || ""); + adapter.activeId = selectedId; adapter.enabled = on === true; settle.restart(); } - function removeVisualizer(i) { - var idx = Math.max(0, Math.min(root.count - 1, i === undefined ? root.active : i)); + function removeVisualizer(instance) { + const idx = instance === undefined ? root.active + : (typeof instance === "number" + ? Math.max(0, Math.min(root.count - 1, Math.round(instance))) + : root.indexOfId(instance)); + if (idx < 0) + return false; if (root.count === 1) { root.setEnabled(false); return true; } + const selectedId = root.activeId; if (idx === 0) { - var ex = (adapter.extras || []).slice(); + var ex = root.cloneData(adapter.extras); var promoted = ex.shift(); root.applyPrimary(promoted); adapter.extras = ex; } else { - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); arr.splice(idx - 1, 1); adapter.extras = arr; } - adapter.active = Math.max(0, Math.min(idx, root.count - 1)); + adapter.activeId = root.indexOfId(selectedId) >= 0 + ? selectedId : root.idAt(Math.min(idx, root.count - 1)); settle.restart(); return true; } @@ -570,10 +629,13 @@ Singleton { property string auraProfile: "flat" property real auraAccent: 0.7 property real auraSensitivity: 0.72 - // Additional visualisers beyond the primary, each a full per-viz - // object, and which instance the desktop editor is tuning. + // Stable identity is stored with each instance. The active pointer is + // an id, never a list position, so removing one cannot retarget the + // editor or make another instance inherit its selection. + property string instanceId: "" + property int nextId: 1 property var extras: [] - property int active: 0 + property string activeId: "" } } @@ -618,6 +680,64 @@ Singleton { return true; } + function ensureIdentities(raw) { + const source = raw || {}; + const rows = root.cloneData(adapter.extras); + const seen = ({}); + let serial = Math.max(1, Math.round(adapter.nextId)); + const candidates = [adapter.instanceId].concat(rows.map(row => row ? row.id : "")); + for (let i = 0; i < candidates.length; ++i) { + const match = /^viz-(\d+)$/.exec("" + (candidates[i] || "")); + if (match) + serial = Math.max(serial, parseInt(match[1]) + 1); + } + function claim(candidate) { + let stableId = "" + (candidate || ""); + if (stableId === "" || seen[stableId]) { + do { + stableId = "viz-" + serial; + serial++; + } while (seen[stableId]); + } + seen[stableId] = true; + return stableId; + } + + let changed = false; + const primaryId = claim(adapter.instanceId); + if (primaryId !== adapter.instanceId) { + adapter.instanceId = primaryId; + changed = true; + } + for (let j = 0; j < rows.length; ++j) { + const oldId = "" + ((rows[j] || {}).id || ""); + const stableId = claim(oldId); + if (stableId !== oldId) { + rows[j] = Object.assign({}, rows[j] || {}, { id: stableId }); + changed = true; + } + } + if (changed) + adapter.extras = rows; + if (adapter.nextId !== serial) { + adapter.nextId = serial; + changed = true; + } + + const allIds = [adapter.instanceId].concat(rows.map(row => "" + (row.id || ""))); + let selected = "" + (adapter.activeId || ""); + if (allIds.indexOf(selected) < 0) { + const legacyIndex = Math.max(0, Math.min(allIds.length - 1, + typeof source.active === "number" ? Math.round(source.active) : 0)); + selected = allIds[legacyIndex]; + } + if (adapter.activeId !== selected) { + adapter.activeId = selected; + changed = true; + } + return changed; + } + // A box saved while overhang was still allowed (or by hand) is folded back // inside the screen once, primary and extras alike; true when one moved. // Best-effort aspect from whatever screen is up first; a stored box off by @@ -632,7 +752,7 @@ Singleton { adapter.x = b.x; adapter.y = b.y; adapter.w = b.w; adapter.h = b.h; moved = true; } - var arr = (adapter.extras || []).slice(); + var arr = root.cloneData(adapter.extras); for (var i = 0; i < arr.length; i++) { var e = arr[i] || {}; var f = root.fitBox(Number(e.x) || 0, Number(e.y) || 0, Number(e.w) || 1, Number(e.h) || 0.42, @@ -648,11 +768,21 @@ Singleton { } Component.onCompleted: { + let raw = {}; + if (file.text()) { + try { + raw = JSON.parse(file.text()) || {}; + } catch (e) { + raw = {}; + } + } + var write = root.ensureIdentities(raw); if (!file.text()) { file.writeAdapter(); return; } - var write = root.migrate(); + if (root.migrate()) + write = true; if (adapter.style === "circle") { adapter.style = "orb"; write = true; diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Scheme.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Scheme.qml index edabb828a4..0059513877 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Scheme.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Scheme.qml @@ -31,6 +31,7 @@ Singleton { // JsonAdapter does not repopulate reliably for a lazily-created singleton. property var namedScheme: null property var wall: ({}) + property var preview: ({}) // A palette value is only usable when it is a non-empty hex string; a null, // missing or half-written role falls through to the next layer, so a partial @@ -40,6 +41,8 @@ Singleton { // Resolve one role through the layer chain: a selected named scheme wins, // then the live wallpaper palette while Match wallpaper is on, then the base. function role(key, base) { + if (usable(preview[key])) + return preview[key]; if (namedScheme && usable(namedScheme[key])) return namedScheme[key]; if (matchWallpaper && usable(wall[key])) @@ -128,6 +131,17 @@ Singleton { onFileChanged: reload() onLoaded: root.refreshWall() } + FileView { + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: { + try { root.preview = JSON.parse(text() || "{}") || {} } + catch (e) { root.preview = {} } + } + } FileView { id: shellFile path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/shell.json" diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Spectrum.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Spectrum.qml index 4ec2bbd75b..77212d1539 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Spectrum.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Spectrum.qml @@ -3,13 +3,14 @@ import QtQuick import Quickshell import Ryoku.Blobs import shell.services +import "." as VizCfg Singleton { id: root - property bool active: false - property int bars: 64 - property int fps: 60 + readonly property bool active: VizCfg.Config.enabled && Perf.visualizerResident + readonly property int bars: VizCfg.Config.maxBars + readonly property int fps: VizCfg.Config.fps property int placementHolds: 0 readonly property bool analysing: root.active @@ -25,11 +26,11 @@ Singleton { bars: root.bars framerate: root.fps method: "pipewire" - source: "auto" - autosens: false - sensitivity: 180 - noiseReduction: 45 - peakFollow: false + source: Audio.musicSink !== "" ? Audio.musicSink + ".monitor" : "auto" + autosens: true + sensitivity: 150 + noiseReduction: 25 + peakFollow: true staleMs: 260 } } diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/VizItem.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/VizItem.qml index cd0776900b..3e885db2b0 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/VizItem.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/VizItem.qml @@ -19,6 +19,8 @@ QtObject { return (d && d[key] !== undefined && d[key] !== null) ? d[key] : def; } + readonly property string instanceId: "" + item.val("id", "") + // The looks the renderer knows; the polar three sit at the tail so one index // decides the family, `frame` is the whole-screen bar ring, and `aura` is the // whole-screen edge field drawn by its own shader rather than the SDF one. diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Waveform.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Waveform.qml index dd8be14d77..47a5701e48 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Waveform.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Singletons/Waveform.qml @@ -2,17 +2,21 @@ pragma Singleton import QtQuick import Quickshell import Ryoku.Blobs +import shell.services +import "." as VizCfg Singleton { id: root - property bool active: false + readonly property bool active: VizCfg.Config.enabled && Perf.visualizerResident + && VizCfg.Config.anyLine && (!Perf.visualizerFrozen || VizCfg.Spectrum.placementHolds > 0) readonly property list samples: capture.samples readonly property bool running: capture.running readonly property bool silent: capture.silent WaveCapture { id: capture + source: Audio.musicSink active: root.active } } diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/Visualizer.qml b/ryoku/shell/quickshell/shell/modules/visualizer/Visualizer.qml index 83fb1df069..8db314e226 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/Visualizer.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/Visualizer.qml @@ -138,39 +138,6 @@ Item { readonly property bool compactSurface: !root.raised && Spectrum.placementHolds === 0 && !root.hasPlacementPreview() - // One shared analyser serves every visualizer instance and follows the - // visualizer performance policy inside Spectrum. - Binding { - target: Spectrum - property: "active" - value: root.active || root.suppressed - } - - // one shared cava for every instance, at the largest band count any of them - // wants; each Motion resamples down. changing it restarts cava. - Binding { - target: Spectrum - property: "bars" - value: Config.maxBars - } - - // cava's framerate follows the render ceiling: no point sampling faster than - // the spectrum draws. - Binding { - target: Spectrum - property: "fps" - value: Config.fps - } - - // The scope capture is tied to the same hard policy as the visualizer feed. - // Placement is the one override: a frozen line cannot be aimed. - Binding { - target: Waveform - property: "active" - value: (root.active || root.suppressed) && Config.anyLine - && (!Performance.visualizerFrozen || Spectrum.placementHolds > 0) - } - PanelWindow { id: win @@ -218,7 +185,8 @@ Item { delegate: VisualizerView { id: vizView anchors.fill: parent - cfg: VizItem { data: Config.dataAt(vizView.index) } + instanceId: Config.idAt(vizView.index) + cfg: VizItem { data: Config.dataById(vizView.instanceId) } } } } diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerGrip.qml b/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerGrip.qml index c27ed56b35..fafe681d0f 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerGrip.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerGrip.qml @@ -10,9 +10,10 @@ import "Singletons" Item { id: win - // The visualizer instance this grip owns. - required property int instanceIndex - // Whether the Stage Editor frames this desktop right now. + // The visualizer instance this grip owns. Its roster index may change when + // another instance is deleted; identity and interaction routing do not. + required property string instanceId + readonly property int instanceIndex: Config.indexOfId(win.instanceId) required property bool composing property var stageController: null @@ -34,8 +35,8 @@ Item { visible: win.composing && Config.enabled && win.instanceIndex < Config.count readonly property real handle: Ui.Tokens.s4 - readonly property var instance: Config.dataAt(win.instanceIndex) || ({}) - readonly property var preview: Config.previewAt(win.instanceIndex) + readonly property var instance: Config.dataById(win.instanceId) + readonly property var preview: Config.previewAt(win.instanceId) function value(key, fallback) { const v = win.instance[key]; return v === undefined || v === null ? fallback : v; @@ -72,7 +73,7 @@ Item { if (win.aura || !win.composing || !win.stageController) return false; const bounds = win.stageController.widgetDragStarted( - "visualizer:" + win.instanceIndex); + "visualizer:" + win.instanceId); if (bounds.active === false) return false; win.stageMoveActive = true; @@ -103,7 +104,7 @@ Item { let py = Math.max(win.groupDragMinY, Math.min(win.groupDragMaxY, footprint.clampY(ny))); const bounded = win.stageController.widgetDragMoved( - "visualizer:" + win.instanceIndex, px, py, modifiers); + "visualizer:" + win.instanceId, px, py, modifiers); footprint.stagePreviewPosition(bounded.x, bounded.y); } function stageEndMove(modifiers) { @@ -113,14 +114,14 @@ Item { const px = Math.round(footprint.x); const py = Math.round(footprint.y); const handled = win.stageController.widgetDragEnded( - "visualizer:" + win.instanceIndex, px, py, modifiers); + "visualizer:" + win.instanceId, px, py, modifiers); if (!handled) { footprint.stageCommitPosition(px, py); footprint.stageEndPreview(); } } else { win.stageController.widgetDragCancelled( - "visualizer:" + win.instanceIndex); + "visualizer:" + win.instanceId); } win.stageMoveActive = false; win.dragging = false; @@ -133,8 +134,8 @@ Item { if (!win.stageMoveActive) return; win.stageController.widgetDragCancelled( - "visualizer:" + win.instanceIndex); - Config.clearPreview(win.instanceIndex); + "visualizer:" + win.instanceId); + Config.clearPreview(win.instanceId); win.stageMoveActive = false; win.dragging = false; } @@ -177,7 +178,7 @@ Item { function stageBeginResize(corner, point, modifiers) { if (win.aura || !win.composing) return; - Config.setActive(win.instanceIndex); + Config.setActive(win.instanceId); win.activated(); win.resizeStartW = win.vw; win.resizeStartH = win.vh; @@ -252,7 +253,7 @@ Item { Connections { target: Config function onActiveChanged() { - if (Config.active === win.instanceIndex || win.gesture === "") + if (Config.activeId === win.instanceId || win.gesture === "") return; win.gesture = ""; grab.mode = ""; @@ -299,17 +300,17 @@ Item { return; const point = footprint.boxPosition( footprint.clampX(px), footprint.clampY(py)); - Config.setPreview(win.instanceIndex, point.x, point.y); + Config.setPreview(win.instanceId, point.x, point.y); } function stageEndPreview() { - Config.clearPreview(win.instanceIndex); + Config.clearPreview(win.instanceId); } function stageCommitPosition(px, py) { if (win.aura) return; const point = footprint.boxPosition( footprint.clampX(px), footprint.clampY(py)); - Config.moveBoxAt(win.instanceIndex, point.x, point.y, win.aspect); + Config.moveBoxAt(win.instanceId, point.x, point.y, win.aspect); } } // The turned footprint, exposed for the desktop: the edit frame boxes this @@ -411,7 +412,7 @@ Item { property real pressAngle: 0 onPressed: (m) => { - Config.setActive(win.instanceIndex); + Config.setActive(win.instanceId); win.activated(); if (m.button === Qt.RightButton) { win.menuRequested(win.box.x, win.box.y); @@ -472,7 +473,7 @@ Item { WheelHandler { acceptedModifiers: Qt.ControlModifier onWheel: (w) => { - Config.setActive(win.instanceIndex); + Config.setActive(win.instanceId); win.activated(); if (!win.wheelHold) { win.wheelHold = true; diff --git a/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerView.qml b/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerView.qml index ce0df32fae..3c8f116935 100644 --- a/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerView.qml +++ b/ryoku/shell/quickshell/shell/modules/visualizer/VisualizerView.qml @@ -17,13 +17,16 @@ Item { // one draw path serves the primary and each extra visualiser alike. required property VizItem cfg - // Which instance this is in Config.list, injected by the Repeater delegate. + // The stable instance id is independent of the roster position. Repeater + // delegates can move after a delete without inheriting another view's + // preview, selection, or governor state. + required property string instanceId required property int index readonly property string style: root.cfg.styleId readonly property bool aura: root.style === "aura" readonly property bool polar: field.polar - readonly property var preview: Config.previewAt(root.index) + readonly property var preview: Config.previewAt(root.instanceId) // What the placement overlay needs: the look's box, and a colour lit for the // same wallpaper. The edge field owns the whole screen, like the frame, so @@ -52,7 +55,7 @@ Item { readonly property int govTier: root.aura ? auraMotion.govTier : motion.govTier readonly property bool govDriving: root.aura ? auraMotion.active : motion.active function publishGovTier() { - if (root.index === Config.active) + if (root.instanceId === Config.activeId) Config.activeGovTier = (root.govDriving && Config.adaptive) ? root.govTier : 0; } onGovTierChanged: root.publishGovTier() @@ -197,7 +200,7 @@ Item { transientStrength: root.cfg.auraTransient beatGlow: root.cfg.auraBeatGlow compression: root.cfg.auraCompression - sensitivity: root.cfg.auraSensitivity * root.cfg.gain + sensitivity: root.cfg.auraSensitivity idleMotion: root.cfg.auraIdleMotion pulseStrength: 0.9 primaryColor: root.auraTriad[0] diff --git a/ryoku/shell/quickshell/shell/services/AppLaunch.qml b/ryoku/shell/quickshell/shell/services/AppLaunch.qml index 59252427fa..4c55d6bc82 100644 --- a/ryoku/shell/quickshell/shell/services/AppLaunch.qml +++ b/ryoku/shell/quickshell/shell/services/AppLaunch.qml @@ -20,8 +20,11 @@ Singleton { if (!target) return; const argv = Array.from(target.command || []).map(arg => String(arg)); - if (argv.length === 0) + if (argv.length === 0) { + if (entry?.id) + Spawn.runApp(["gtk-launch", String(entry.id)], { label: entry.id }); return; + } const command = entry && entry.runInTerminal ? ["ryoku-app", "terminal", "--"].concat(argv) : argv; diff --git a/ryoku/shell/quickshell/shell/services/Audio.qml b/ryoku/shell/quickshell/shell/services/Audio.qml index d3dc10952b..0eb5702974 100644 --- a/ryoku/shell/quickshell/shell/services/Audio.qml +++ b/ryoku/shell/quickshell/shell/services/Audio.qml @@ -111,11 +111,37 @@ Singleton { return !!(n && typeof n.name === "string"); } + // The sink music actually enters, which is not the default one behind Easy Effects or a similar filter. + property string musicSink: "" + readonly property var liveLinks: Pipewire.ready ? Pipewire.links.values : [] + readonly property var callApps: ["vesktop", "discord", "webrtc", "teams", "zoom", "slack", "telegram"] + + function playbackSink() { + var fallback = ""; + for (var i = 0; i < root.liveLinks.length; i++) { + var link = root.liveLinks[i]; + var from = link && link.source; + var to = link && link.target; + if (!root.isPlayStream(from) || !to || !to.isSink || to.isStream || !root.alive(to)) + continue; + var app = ("" + from.name).toLowerCase(); + if (!root.callApps.some(name => app.indexOf(name) >= 0)) + return "" + to.name; + if (fallback === "") + fallback = "" + to.name; + } + return fallback; + } + function syncAudioLists() { root.outputs = root.liveOutputs.slice(); root.inputs = root.liveInputs.slice(); root.streams = root.liveStreams.slice(); root.captureStreams = root.liveCaptureStreams.slice(); + const sink = root.playbackSink(); + const known = root.nodes.some(n => n && n.isSink && !n.isStream && ("" + n.name) === root.musicSink); + if (sink !== "" || !known) + root.musicSink = sink; } Timer { @@ -129,6 +155,7 @@ Singleton { onLiveInputsChanged: listSettle.restart() onLiveStreamsChanged: listSettle.restart() onLiveCaptureStreamsChanged: listSettle.restart() + onLiveLinksChanged: listSettle.restart() Component.onCompleted: root.syncAudioLists() diff --git a/ryoku/shell/quickshell/shell/services/AudioBars.qml b/ryoku/shell/quickshell/shell/services/AudioBars.qml index ff8601bf30..54a5240d34 100644 --- a/ryoku/shell/quickshell/shell/services/AudioBars.qml +++ b/ryoku/shell/quickshell/shell/services/AudioBars.qml @@ -26,10 +26,10 @@ Singleton { bars: root.bars framerate: root.fps method: "pipewire" - source: "auto" - autosens: false - sensitivity: 180 - noiseReduction: 45 + source: Audio.musicSink !== "" ? Audio.musicSink + ".monitor" : "auto" + autosens: true + sensitivity: 150 + noiseReduction: 25 peakFollow: true staleMs: 120 } diff --git a/ryoku/shell/quickshell/shell/services/BluetoothPower.qml b/ryoku/shell/quickshell/shell/services/BluetoothPower.qml index 8df06d8f43..74ca995f84 100644 --- a/ryoku/shell/quickshell/shell/services/BluetoothPower.qml +++ b/ryoku/shell/quickshell/shell/services/BluetoothPower.qml @@ -3,18 +3,44 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Bluetooth +import Quickshell.Io Singleton { + id: root + readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false - readonly property bool available: Bluetooth.defaultAdapter !== null + readonly property bool available: Bluetooth.defaultAdapter !== null || root.blocked + readonly property bool blocked: root.softBlocked || root.hardBlocked + || Bluetooth.defaultAdapter?.state === BluetoothAdapterState.Blocked + property bool softBlocked: false + property bool hardBlocked: false function setEnabled(on: bool): void { - if (!Bluetooth.defaultAdapter) - return; Quickshell.execDetached(["ryoku-bluetooth-power", on ? "on" : "off"]); } function toggle(): void { setEnabled(!enabled); } + + Connections { + target: Bluetooth + function onDefaultAdapterChanged(): void { + rfkillState.running = true; + } + } + + Process { + id: rfkillState + command: ["rfkill", "-n", "-o", "TYPE,SOFT,HARD", "list", "bluetooth"] + running: true + stdout: StdioCollector { + onStreamFinished: { + const rows = text.trim().split("\n").map(line => line.trim().split(/\s+/)) + .filter(fields => fields[0] === "bluetooth"); + root.softBlocked = rows.some(fields => fields[1] === "blocked"); + root.hardBlocked = rows.some(fields => fields[2] === "blocked"); + } + } + } } diff --git a/ryoku/shell/quickshell/shell/services/Config.qml b/ryoku/shell/quickshell/shell/services/Config.qml index 280a24388b..8cff97447c 100644 --- a/ryoku/shell/quickshell/shell/services/Config.qml +++ b/ryoku/shell/quickshell/shell/services/Config.qml @@ -16,6 +16,8 @@ import Ryoku.Ui.Singletons // per-monitor scale `s` where they're read. Singleton { id: root + signal settingsLoaded() + // frame = the painted border band. Its enable, opacity, band thickness and // corner radius are live knobs; the rail geometry lives in frameBars. @@ -55,6 +57,7 @@ Singleton { property alias batteryAlerts: adapter.batteryAlerts property alias barStyle: adapter.barStyle property alias notifications: adapter.notifications + readonly property var sounds: adapter.sounds // obi: per-widget visibility for the Obi bar style, edited in Bar Studio. // A map of widgetId -> bool; an absent key reads as shown, so the bar is @@ -300,7 +303,10 @@ Singleton { printErrors: false atomicWrites: true onFileChanged: reload() - onLoaded: root.refreshThemePalette() + onLoaded: { + root.refreshThemePalette(); + root.settingsLoaded(); + } JsonAdapter { id: adapter @@ -327,6 +333,7 @@ Singleton { "popup_window_margins": 0, "show_images": false }) + property var sounds: ({}) property string barStyle: "orpheus" property string launcherTarget: "studio" property var obi: ({}) @@ -346,6 +353,7 @@ Singleton { "edge": "auto", "edgeGap": 0, "autohide": false, + "revealOnEmpty": true, "size": 44, "pinned": [], "magnify": true, diff --git a/ryoku/shell/quickshell/shell/services/Dock.qml b/ryoku/shell/quickshell/shell/services/Dock.qml index 96488171d3..110583d5cd 100644 --- a/ryoku/shell/quickshell/shell/services/Dock.qml +++ b/ryoku/shell/quickshell/shell/services/Dock.qml @@ -122,6 +122,7 @@ Singleton { function _syncClients(force) { const wins = Array.from(Wm.windows ?? []); + const toplevels = Array.from(Wm.toplevels ?? []); const focused = Wm.focusedWindow; const focusedId = focused ? String(focused.id ?? "") : ""; const records = []; @@ -138,8 +139,30 @@ Singleton { workspace: String(w?.workspace ?? "") }); } + const frameCounts = new Map(); + for (const record of records) { + const id = root.canonicalId(record.specialId || record.appId); + frameCounts.set(id, (frameCounts.get(id) || 0) + 1); + } + const directCounts = new Map(); + let activeToplevel = null; + for (const toplevel of toplevels) { + if (!toplevel?.appId) + continue; + const id = root.canonicalId(toplevel.appId === "org.quickshell" + ? (root.quickshellApps[toplevel.title] || toplevel.appId) : toplevel.appId); + if (!id) + continue; + directCounts.set(id, (directCounts.get(id) || 0) + 1); + if (toplevel.activated) + activeToplevel = toplevel; + } + for (const [id, count] of directCounts) { + for (let i = frameCounts.get(id) || 0; i < count; ++i) + records.push({ id: "pending:" + id + ":" + i, appId: id, specialId: "", workspace: "" }); + } records.sort((a, b) => a.id < b.id ? -1 : (a.id > b.id ? 1 : 0)); - const signature = focusedId + "\u0001" + records.map(record => + const signature = (focusedId || (activeToplevel ? "active:" + activeToplevel.appId : "")) + "\u0001" + records.map(record => record.id + "\u0000" + record.appId + "\u0000" + record.specialId + "\u0000" + record.workspace).join("\u0001"); if (force !== true && signature === root._windowSignature) @@ -154,16 +177,18 @@ Singleton { } root._windowSignature = signature; root._clients = result; - root._focusedClient = focused; + root._focusedClient = focused || activeToplevel; root._activeClass = focused ? root.canonicalId((focused.appId === "org.quickshell" ? root.quickshellApps[focused.title] : "") || focused.appId) - : ""; + : activeToplevel ? root.canonicalId(activeToplevel.appId === "org.quickshell" + ? (root.quickshellApps[activeToplevel.title] || activeToplevel.appId) : activeToplevel.appId) : ""; } Connections { target: Wm function onWindowsChanged() { root._syncClients(false); } + function onToplevelsChanged() { root._syncClients(false); } function onFocusedWindowChanged() { root._syncClients(false); } } Component.onCompleted: root._syncClients(true) @@ -339,6 +364,11 @@ Singleton { return resolved; } + function launch(className) { + const entry = root.entryFor(className); + if (entry) + AppLaunch.run(entry, null); + } // No clients -> launch; focused already -> cycle by address; else focus, // preferring a client on the active workspace. function activate(className) { @@ -349,9 +379,13 @@ Singleton { if (root.sameApp(root.classOf(wins[i]), id)) matches.push(wins[i]); if (matches.length === 0) { - const entry = DesktopEntries.heuristicLookup(id); - if (entry) - AppLaunch.run(entry, null); + const direct = Array.from(Wm.toplevels ?? []).find(toplevel => + root.sameApp(toplevel?.appId === "org.quickshell" + ? (root.quickshellApps[toplevel.title] || toplevel.appId) : toplevel?.appId, id)); + if (direct) + direct.activate(); + else + root.launch(id); return; } matches.sort((a, b) => a.id < b.id ? -1 : (a.id > b.id ? 1 : 0)); @@ -400,10 +434,9 @@ Singleton { } function closeMenu() { root.menuClass = ""; } function menuActOpen() { - if (root.menuCount > 0) { - const e = DesktopEntries.heuristicLookup(root.menuClass); - if (e) AppLaunch.run(e, null); - } else { + if (root.menuCount > 0) + root.launch(root.menuClass); + else { root.activate(root.menuClass); } root.closeMenu(); diff --git a/ryoku/shell/quickshell/shell/services/OsdFeed.qml b/ryoku/shell/quickshell/shell/services/OsdFeed.qml index a77e2895e2..fd56a3e81e 100644 --- a/ryoku/shell/quickshell/shell/services/OsdFeed.qml +++ b/ryoku/shell/quickshell/shell/services/OsdFeed.qml @@ -16,6 +16,20 @@ Singleton { property real brightness: 0 property int brightnessSeq: 0 property int previewSeq: 0 + property real expectedMicVolume: -1 + property double expectedMicVolumeAt: 0 + + function expectMicVolume(value) { + root.expectedMicVolume = value + root.expectedMicVolumeAt = Date.now() + } + + function acceptsMicVolume(value) { + return root.expectedMicVolumeAt > 0 + && Date.now() - root.expectedMicVolumeAt < 1500 + && Math.abs(value - root.expectedMicVolume) < 0.015 + } + signal volumeRequested() signal brightnessRequested() diff --git a/ryoku/shell/quickshell/shell/services/Perf.qml b/ryoku/shell/quickshell/shell/services/Perf.qml index e233da31c4..65e0cf4384 100644 --- a/ryoku/shell/quickshell/shell/services/Perf.qml +++ b/ryoku/shell/quickshell/shell/services/Perf.qml @@ -120,7 +120,7 @@ Singleton { visualizerResidencyGrace.stop(); if (root.visualizerHardFrozen) { root.visualizerResident = false; - } else if (root.sounding || !root.unloadVisualizerWhenSilent) { + } else if (root.sounding || !root.unloadVisualizerWhenSilent || root.tier === root.tierPerformance) { root.visualizerResident = true; } else if (root.visualizerResident) { visualizerResidencyGrace.restart(); @@ -142,6 +142,7 @@ Singleton { } onUnloadVisualizerWhenSilentChanged: root.syncVisualizerResidency() onVisualizerHardFrozenChanged: root.syncVisualizerResidency() + onTierChanged: root.syncVisualizerResidency() Timer { id: audioGrace; interval: 4000; onTriggered: root.audioIdle = true } Timer { @@ -149,12 +150,14 @@ Singleton { interval: 30000 onTriggered: { if (root.unloadVisualizerWhenSilent && !root.sounding - && !root.visualizerHardFrozen) + && !root.visualizerHardFrozen && root.tier !== root.tierPerformance) root.visualizerResident = false; } } - readonly property bool visualizerFrozen: root.visualizerHardFrozen || audioIdle - readonly property bool pillFrozen: root.visualizerHardFrozen || audioIdle + // Performance keeps the analysers running even when playback detection misses a stream. + readonly property bool idleFreeze: audioIdle && root.tier !== root.tierPerformance + readonly property bool visualizerFrozen: root.visualizerHardFrozen || root.idleFreeze + readonly property bool pillFrozen: root.visualizerHardFrozen || root.idleFreeze // Ambient motion: the bar's stream drifting on a passive sine while the desktop // is SILENT (with audio playing it reacts regardless -- that is streamLive's diff --git a/ryoku/shell/quickshell/shell/services/Scheme.qml b/ryoku/shell/quickshell/shell/services/Scheme.qml index bb101e6cee..96b7c786e5 100644 --- a/ryoku/shell/quickshell/shell/services/Scheme.qml +++ b/ryoku/shell/quickshell/shell/services/Scheme.qml @@ -22,13 +22,21 @@ Singleton { // cannot carry it: half the role names start with "on", which QML's // signal-handler grammar rejects as adapter property names. property var roles: ({}) + property var committedRoles: ({}) + property var previewRoles: ({}) function refreshRoles() { try { const t = file.text(); - roles = t && t.length ? (JSON.parse(t) || {}) : {}; + committedRoles = t && t.length ? (JSON.parse(t) || {}) : {}; } catch (e) { - roles = {}; + committedRoles = {}; } + roles = Object.keys(previewRoles).length ? previewRoles : committedRoles; + } + function refreshPreview() { + try { previewRoles = JSON.parse(previewFile.text() || "{}") || {}; } + catch (e) { previewRoles = {}; } + roles = Object.keys(previewRoles).length ? previewRoles : committedRoles; } // Verbatim role when present, compiled default before the first write. @@ -104,5 +112,15 @@ Singleton { onLoaded: w.refreshRoles() } + FileView { + id: previewFile + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/dev/null") + "/ryoku-wall-preview.json" + blockLoading: true + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: w.refreshPreview() + } + Component.onCompleted: refreshRoles() } diff --git a/ryoku/shell/quickshell/shell/services/Theme.qml b/ryoku/shell/quickshell/shell/services/Theme.qml index c7a36ee099..3d8d9b4dd8 100644 --- a/ryoku/shell/quickshell/shell/services/Theme.qml +++ b/ryoku/shell/quickshell/shell/services/Theme.qml @@ -1,6 +1,7 @@ pragma Singleton import QtQuick import Quickshell +import Ryoku.Ui.Singletons as Ui /** * Shell-wide style tokens: 30 Material colour roles plus shadow/scrim, sizing, @@ -42,6 +43,8 @@ Singleton { // Resolve one colour role through the layer chain: a selected named scheme // wins, then the live wallpaper palette, then the compiled Solitude base. function role(key, base) { + if (Scheme.previewRoles[key] !== undefined) + return Scheme.previewRoles[key]; if (namedScheme && namedScheme[key] !== undefined) return namedScheme[key]; if (matchWallpaper) @@ -216,7 +219,7 @@ Singleton { // brand mark, user-overridable via ~/.config/ryoku/brand.json (Shell -> // Global). BrandMark renders `mark`, or `markSource` (an image) when set. - readonly property string mark: Config.markText.length > 0 ? Config.markText : "\u529b" + readonly property string mark: Ui.UiPreferences.brandMark(Config.markText) readonly property string markSource: Config.markImage readonly property bool markTint: Config.markTint diff --git a/ryoku/shell/quickshell/shell/shell.qml b/ryoku/shell/quickshell/shell/shell.qml index 8cb72fa5cd..60a46d9438 100644 --- a/ryoku/shell/quickshell/shell/shell.qml +++ b/ryoku/shell/quickshell/shell/shell.qml @@ -63,6 +63,19 @@ ShellRoot { // its outer Store Scene exists; a nested load error must still fall back. readonly property bool gokkoRunning: Config.barStyle === "gokko" && BarProducts.gokkoReady + property var desktopLayersReady: ({}) + function markDesktopLayerReady(name) { + const next = Object.assign({}, desktopLayersReady); + next[name] = true; + desktopLayersReady = next; + } + readonly property bool docksMayMount: { + const screens = ShellState.screens; + if (!screens.length) return false; + for (const screen of screens) + if (!desktopLayersReady[screen.name]) return false; + return true; + } // One deferred-build wave for the cheap event-driven surfaces (the OSDs and // the notification column), armed 1.5 s into boot so a keypress or an // arriving toast never waits on a component build. Everything heavier is @@ -199,6 +212,7 @@ ShellRoot { Desktop { id: desktop screen: perScreen.modelData + onSurfaceReadyChanged: if (surfaceReady) root.markDesktopLayerReady(perScreen.modelData.name) active: true widgetsEnabled: Tokens.widgetsEnabledFor(perScreen.modelData.name) && !root.gokkoRunning @@ -229,30 +243,16 @@ ShellRoot { } } - // Stage now renders entirely inside the desktop surface (one stack: - // backdrop, layers, widgets), so there is no separate Background - // surface here (docs/stage.md). A silent visualizer is unloaded on - // every output when the performance policy asks for it; live audio - // re-creates the surfaces immediately. Placement runs in the Stage - // Editor now: the desktop hosts the look while the mode frames this - // monitor, so this surface exists for the plain and overlay cases only. + // Only Above windows needs a separate surface; the plain look stays on the desktop. LazyLoader { id: vizLoader activeAsync: VizCfg.Config.enabled && Perf.visualizerResident + && !desktop.hostsVisualizer && !desktop.plainVisualizer Visualizer { id: perScreenViz screen: perScreen.modelData - mode: !VizCfg.Config.enabled ? "off" - : (perScreen.st && perScreen.st.visualizerOverlay ? "overlay" : "desktop") - // The desktop hosts the visualizer behind the cut-outs while the - // stage is on, and inside the lifted desktop while the Stage - // Editor frames this monitor; this surface steps aside (cava - // keeps running). A rebuilt monitor stack (DPMS off/on, a lid - // close/open) starts with no wallpaper frame, so - // hostsVisualizer briefly reads false even for a stage wallpaper: - // stay suppressed until the frame lands, or the surface maps - // full-screen and pulses for a few seconds on every resume. - suppressed: desktop.hostsVisualizer || !wallpaper.reloadReady + mode: "overlay" + suppressed: !wallpaper.reloadReady } } @@ -280,7 +280,9 @@ ShellRoot { active: controlsLoader.showNow } } - Timer { id: controlsHold; interval: SidebarState.exitDuration + 48 } + // The controls tree is expensive to construct; keep it across ordinary + // toggles while its active-gated probes and surfaces remain stopped. + Timer { id: controlsHold; interval: 15000 } LazyLoader { @@ -323,7 +325,7 @@ ShellRoot { UniversalDockHost { id: dockLoader screen: perScreen.modelData - surfaceVisible: !root.gokkoRunning + surfaceVisible: !root.gokkoRunning && desktop.surfaceReady } // The dock's right-click context menu: a full-screen overlay on the @@ -567,7 +569,7 @@ ShellRoot { // The ii host publishes actions and mounts its auxiliary surfaces only for // the selected, enabled design. Other docks do not instantiate the port. LazyLoader { - active: !root.gokkoRunning + active: !root.gokkoRunning && root.docksMayMount && Dock.design === "ii" && Dock.cfg("enabled", false) LazyLoader { activeAsync: true @@ -719,6 +721,8 @@ ShellRoot { // device restore + ddc prewarm and re-arming the persisted Keep-Awake / // Game Mode external inhibitors. Component.onCompleted: { + Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/"); Devices.restore(); root.syncCaffeine(Flags.keepAwake ? "start" : "stop"); if (Flags.gameMode) diff --git a/ryoku/shell/quickshell/stage/GlobalStates.qml b/ryoku/shell/quickshell/stage/GlobalStates.qml index 77041250b1..df48be03ff 100644 --- a/ryoku/shell/quickshell/stage/GlobalStates.qml +++ b/ryoku/shell/quickshell/stage/GlobalStates.qml @@ -327,15 +327,18 @@ Singleton { } property string _editReopenMonitor: "" + property string _editReopenSection: "" property string _editReopenPage: "" - property bool _editReopenWallpaper: false + property bool _editReopenDrawer: false function switchEditMonitor(monitorName) { if (!root.editMode || !monitorName || monitorName === root.editModeMonitor) return; root._editReopenMonitor = monitorName; - root._editReopenWallpaper = root.editDrawerOpen && root.editDrawerSection === "wallpaper" - && !root.editLockPreview; - root._editReopenPage = root._editReopenWallpaper ? root.editDrawerPage : ""; + root._editReopenDrawer = root.editDrawerOpen && !root.editLockPreview; + root._editReopenSection = root.editDrawerSection === "widget" + ? "widgets" : root.editDrawerSection; + root._editReopenPage = root.editDrawerSection === "widget" + ? "" : root.editDrawerPage; root.closeEditMode(); editReopenTimer.restart(); } @@ -345,17 +348,20 @@ Singleton { repeat: false onTriggered: { const monitor = root._editReopenMonitor; - const wallpaper = root._editReopenWallpaper; + const section = root._editReopenSection; const page = root._editReopenPage; + const drawer = root._editReopenDrawer; root._editReopenMonitor = ""; - root._editReopenWallpaper = false; + root._editReopenSection = ""; root._editReopenPage = ""; + root._editReopenDrawer = false; if (monitor === "") return; - if (wallpaper) - root.openEditCatalogue("wallpaper", monitor, page); - else - root.openEditMode(monitor); + root.editDrawerSection = section; + root.editDrawerPage = page; + root.openEditMode(monitor); + if (root.editMode) + root.editDrawerOpen = drawer; } } diff --git a/ryoku/shell/quickshell/stage/modules/common/Config.qml b/ryoku/shell/quickshell/stage/modules/common/Config.qml index a951c40537..4e24b39547 100644 --- a/ryoku/shell/quickshell/stage/modules/common/Config.qml +++ b/ryoku/shell/quickshell/stage/modules/common/Config.qml @@ -4367,6 +4367,7 @@ Singleton { property bool keepSorted: false // re-sort whenever an icon comes or goes property bool stacks: false // one stack per kind: apps, folders, files property bool hidden: false // a clean desktop; the icons stay stored + property string openOn: "double" // single | double property string labels: "always" // always | hover | never property int labelLines: 1 // 1 | 2 property string labelStyle: "auto" // auto | shadow | pill diff --git a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutAppPicker.qml b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutAppPicker.qml index aab0baf5d7..70587d937c 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutAppPicker.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutAppPicker.qml @@ -15,8 +15,12 @@ ColumnLayout { required property string screenName required property string folderId property var refreshKey: null + property real targetX: NaN + property real targetY: NaN + readonly property bool desktopMode: folderId === "" property string query: "" signal done() + signal applicationAdded(string appId) readonly property bool writable: Persistent.ready && !Persistent.blockWrites // The list depends on the search only; each row reads its own placement, @@ -43,15 +47,21 @@ ColumnLayout { function toggle(appId) { if (!root.writable) return; + const app = DesktopShortcuts.application(appId); + if (!app) + return; + if (root.desktopMode) { + if (DesktopShortcuts.add(root.screenName, [app], root.targetX, root.targetY)) + root.applicationAdded(appId); + return; + } const location = root.placement(appId); if (location.where === "folder" && location.folderId === root.folderId) { if (DesktopShortcuts.canTakeOut(root.screenName, root.folderId)) DesktopShortcuts.takeOut(root.screenName, root.folderId, appId); return; } - const app = DesktopShortcuts.application(appId); - if (app) - DesktopShortcuts.addToFolder(root.screenName, root.folderId, [app]); + DesktopShortcuts.addToFolder(root.screenName, root.folderId, [app]); } spacing: Tokens.s2 @@ -62,7 +72,7 @@ ColumnLayout { toolbar: true placeholder: Translation.tr("Search applications") onEdited: root.query = text - Component.onCompleted: Qt.callLater(() => grabFocus()) + Component.onCompleted: if (root.visible) Qt.callLater(() => grabFocus()) } Item { diff --git a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcuts.qml b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcuts.qml index d32b4b328e..6e58200825 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcuts.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcuts.qml @@ -8,7 +8,7 @@ import stage.services import stage.modules.common import stage.modules.ii.editMode import Ryoku.Ui.Singletons -import shell.services +import shell.services as ShellServices Singleton { id: root @@ -1108,7 +1108,7 @@ Singleton { } else if (entry.path) { Spawn.runApp(["gio", "launch", entry.path], { label: entry.id }); } else { - AppLaunch.run(TaskbarApps.getCachedDesktopEntry(entry.id), null); + ShellServices.AppLaunch.run(TaskbarApps.getCachedDesktopEntry(entry.id), null); } Qt.callLater(() => root.countLaunch(entry.id)); } diff --git a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutsLayer.qml b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutsLayer.qml index 594d6e6a4a..c3f7018de5 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutsLayer.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/background/shortcuts/DesktopShortcutsLayer.qml @@ -30,6 +30,14 @@ Item { opacity: root.iconsHidden ? 0 : 1 visible: root.opacity > 0.01 enabled: !root.iconsHidden + readonly property bool pointerOverShortcut: { + for (let i = 0; i < iconRepeater.count; ++i) { + const tile = iconRepeater.itemAt(i); + if (tile && tile.hovered) + return true; + } + return false; + } Behavior on opacity { NumberAnimation { duration: Tokens.swap; easing.type: Tokens.ease } } @@ -91,13 +99,13 @@ Item { property real surfaceScale: 1 readonly property real counterScale: 1 / Math.max(0.2, Math.min(5, root.surfaceScale)) // ── Click-to-open ────────────────────────────────────────────────────── - // Windows/KDE semantics: the first click SELECTS and arms, the second - // click of the pair opens. One timer for the whole layer (never one per - // icon), keyed by the armed id — clicking a different icon just - // re-arms. Qt.styleHints is absent in this runtime, so the window is - // the conventional 400 ms. + // Single-click mode opens immediately. Double-click mode selects and arms + // on the first click, then opens a second click on that icon. One timer for + // the whole layer keeps the pair keyed to one id; Qt.styleHints is absent + // in this runtime, so the window is the conventional 400 ms. property string armedId: "" readonly property bool iconsLocked: Config.options.background.desktopIconsLocked ?? false + readonly property bool singleClickOpen: root.options.openOn === "single" Timer { id: clickTimer interval: 400 @@ -180,10 +188,10 @@ Item { } } - // The keys ride the window's OnDemand keyboard (the host holds it while - // hasSelection is true). When the band picked widgets as well, the canvas - // holds the focus and Delete stays the widgets' — the context menu's - // Remove still takes the whole icon selection either way. + // The host holds the keyboard while a selection exists and the pointer is + // over a shortcut; moving away releases it before another surface is + // clicked. When a marquee picked widgets too, the canvas owns Delete — the + // context menu's Remove still takes the whole icon selection. focus: root.hasSelection onHasSelectionChanged: { if (root.hasSelection) @@ -384,10 +392,12 @@ Item { } function openEntry(entry) { const tile = root.tileFor(entry.id); - if (entry.type === "group" && tile) + if (entry.type === "group" && tile) { root.openPopup(entry.id, tile.x, tile.y, tile.width, tile.height); - else + } else { + root.clearSelection(); DesktopShortcuts.launch(entry); + } } function tileFor(id) { for (let i = 0; i < iconRepeater.count; ++i) { @@ -1063,9 +1073,12 @@ Item { root.clearSelection(); const p = root.mapFromItem(gesture, mouse.x, mouse.y); root.openContext(tile.entry.id, p.x, p.y); + } else if (root.singleClickOpen) { + root.selectedIds = [tile.entry.id]; + root.armedId = ""; + clickTimer.stop(); + root.openEntry(tile.entry); } else if (root.armedId !== tile.entry.id) { - // A single click selects. The second click within the - // desktop double-click interval opens the item. root.selectedIds = [tile.entry.id]; root.armedId = tile.entry.id; clickTimer.restart(); diff --git a/ryoku/shell/quickshell/stage/modules/ii/background/widgets/system/BluetoothPillWidget.qml b/ryoku/shell/quickshell/stage/modules/ii/background/widgets/system/BluetoothPillWidget.qml index 4f6e91d2c1..87965f147e 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/background/widgets/system/BluetoothPillWidget.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/background/widgets/system/BluetoothPillWidget.qml @@ -78,7 +78,7 @@ AbstractBackgroundWidget { anchors.right: parent.right anchors.rightMargin: 14 anchors.verticalCenter: parent.verticalCenter - text: root.isActive ? ((BluetoothStatus.firstActiveDevice && BluetoothStatus.firstActiveDevice.name) ? BluetoothStatus.firstActiveDevice.name : Translation.tr("Bluetooth")) : Translation.tr("Off") + text: BluetoothStatus.blocked ? Translation.tr("Blocked") : root.isActive ? ((BluetoothStatus.firstActiveDevice && BluetoothStatus.firstActiveDevice.name) ? BluetoothStatus.firstActiveDevice.name : Translation.tr("Bluetooth")) : Translation.tr("Off") font.pixelSize: 24 font.weight: Font.Bold color: root.contentColor diff --git a/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDesktopIconsPage.qml b/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDesktopIconsPage.qml index 13bd2f8530..f8b3f19456 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDesktopIconsPage.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDesktopIconsPage.qml @@ -58,6 +58,23 @@ StyledFlickable { onToggled: Config.options.background.desktopIconsLocked = v } } + SettingRow { + width: parent.width + divider: true + label: Translation.tr("Open desktop icons") + desc: Translation.tr("Choose whether one click selects or opens an icon") + block: true + Seg { + width: parent.width + options: ["single", "double"] + labels: ({ + "single": Translation.tr("Single click"), + "double": Translation.tr("Double click") + }) + current: root.options.openOn + onChose: key => root.options.openOn = key + } + } SettingRow { width: parent.width divider: true diff --git a/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDockAppearancePage.qml b/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDockAppearancePage.qml index 3b83e08084..044912b764 100644 --- a/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDockAppearancePage.qml +++ b/ryoku/shell/quickshell/stage/modules/ii/editMode/EditDockAppearancePage.qml @@ -132,6 +132,18 @@ StyledFlickable { onActivated: root.setCfg("autohide", !root.cfg("autohide", false)) } + EditPanelRow { + visible: root.design === "ryoku" + Layout.fillWidth: true + first: false + last: false + symbol: "desktop_windows" + title: Translation.tr("Stay visible on empty workspaces") + trailingKind: "switch" + switchChecked: root.cfg("revealOnEmpty", true) + onActivated: root.setCfg("revealOnEmpty", !root.cfg("revealOnEmpty", true)) + } + EditPanelRow { Layout.fillWidth: true first: false diff --git a/ryoku/shell/quickshell/stage/services/AppSearch.qml b/ryoku/shell/quickshell/stage/services/AppSearch.qml index 39acd97209..9c010cb0e8 100644 --- a/ryoku/shell/quickshell/stage/services/AppSearch.qml +++ b/ryoku/shell/quickshell/stage/services/AppSearch.qml @@ -60,6 +60,17 @@ Singleton { arr.sort((a, b) => (a.name || "").localeCompare(b.name || "")); return arr; } + function lookupDesktopEntry(appId, includeNoDisplay = false) { + const id = String(appId ?? "").trim(); + if (!id) + return null; + const plain = id.replace(/\.desktop$/i, ""); + const exact = root.list.find(entry => entry.id === id || entry.id === plain); + if (exact) + return exact; + const match = DesktopEntries.heuristicLookup(id); + return match && (includeNoDisplay || !match.noDisplay) ? match : null; + } readonly property var preppedNames: list.map(a => ({ name: Fuzzy.prepare(`${a.name} `), diff --git a/ryoku/shell/quickshell/stage/services/BluetoothStatus.qml b/ryoku/shell/quickshell/stage/services/BluetoothStatus.qml index f1e74735af..b3d5aa3d05 100644 --- a/ryoku/shell/quickshell/stage/services/BluetoothStatus.qml +++ b/ryoku/shell/quickshell/stage/services/BluetoothStatus.qml @@ -10,7 +10,7 @@ import stage Singleton { id: root - readonly property bool available: Bluetooth.adapters.values.length > 0 + readonly property bool available: Bluetooth.adapters.values.length > 0 || root.blocked readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0 @@ -20,11 +20,12 @@ Singleton { readonly property string adapterId: root.adapter?.adapterId ?? "" readonly property string adapterName: root.adapter?.name ?? "" /** - * True while the radio is killswitched. BlueZ reports both kinds of block as - * the same adapter state, so `hardBlocked` is what decides whether the user - * can do anything about it — a soft block is what `setEnabled` clears. + * True while any Bluetooth radio is killswitched. Platform rfkill switches + * can remove the BlueZ adapter entirely, so the kernel state remains + * authoritative while there is no `defaultAdapter`. */ - readonly property bool blocked: root.adapter?.state === BluetoothAdapterState.Blocked + readonly property bool blocked: root.softBlocked || root.hardBlocked + || root.adapter?.state === BluetoothAdapterState.Blocked property bool softBlocked: false property bool hardBlocked: false readonly property bool discovering: root.adapter?.discovering ?? false @@ -43,7 +44,7 @@ Singleton { root.adapter.discovering = false; } - onBlockedChanged: rfkillState.running = true + onAdapterChanged: rfkillState.running = true onAdapterIdChanged: root.readAdapterAddress() Component.onCompleted: { rfkillState.running = true; @@ -68,11 +69,10 @@ Singleton { command: ["rfkill", "-n", "-o", "TYPE,SOFT,HARD", "list", "bluetooth"] stdout: StdioCollector { onStreamFinished: { - // "bluetooth blocked unblocked" — one line per radio. - const line = text.trim().split("\n").find(entry => entry.includes("bluetooth")) ?? ""; - const fields = line.trim().split(/\s+/); - root.softBlocked = fields[1] === "blocked"; - root.hardBlocked = fields[2] === "blocked"; + const rows = text.trim().split("\n").map(line => line.trim().split(/\s+/)) + .filter(fields => fields[0] === "bluetooth"); + root.softBlocked = rows.some(fields => fields[1] === "blocked"); + root.hardBlocked = rows.some(fields => fields[2] === "blocked"); } } } @@ -89,8 +89,6 @@ Singleton { // === Power control === function setEnabled(on: bool): void { - if (!Bluetooth.defaultAdapter) - return; Quickshell.execDetached(["ryoku-bluetooth-power", on ? "on" : "off"]); } diff --git a/ryoku/shell/quickshell/stage/services/Translation.qml b/ryoku/shell/quickshell/stage/services/Translation.qml index c4e0771b20..90505a2013 100644 --- a/ryoku/shell/quickshell/stage/services/Translation.qml +++ b/ryoku/shell/quickshell/stage/services/Translation.qml @@ -1,162 +1,11 @@ pragma Singleton import QtQuick -import Quickshell -import Quickshell.Io -import stage.modules.common - -Singleton { - id: root - - property var translations: ({}) - property var generatedTranslations: ({}) - property var availableLanguages: ["en_US"] - property var availableGeneratedLanguages: [] - property var allAvailableLanguages: { - const combined = new Set([...root.availableLanguages, ...root.availableGeneratedLanguages]); - return Array.from(combined).sort(); - } - property bool isScanning: scanLanguagesProcess.running - property bool isLoading: false - property string translationKeepSuffix: "/*keep*/" - property string translationsDir: Quickshell.shellPath("translations") - property string generatedTranslationsDir: Directories.shellConfig + "/translations" - - property string languageCode: { - var configLang = Config?.options.language.ui ?? "auto"; - - if (configLang !== "auto") - return configLang; - - return Qt.locale().name; - } - - TranslationScanner { - id: scanLanguagesProcess - translationsDir: root.translationsDir - onLanguagesScanned: (languages) => { - root.availableLanguages = [...languages]; - } - } - - TranslationScanner { - id: scanGeneratedLanguagesProcess - translationsDir: root.generatedTranslationsDir - fallbackLanguages: [] - onLanguagesScanned: (languages) => { - root.availableGeneratedLanguages = [...languages]; - generatedTranslationFileView.reread(); - } - } - - onLanguageCodeChanged: { - print("[Translation] Language changed to", root.languageCode); - translationFileView.languageCode = root.languageCode; - generatedTranslationFileView.languageCode = root.languageCode; - translationFileView.reread(); - generatedTranslationFileView.reread(); - } - - TranslationReader { - id: translationFileView - translationsDir: root.translationsDir - languageCode: root.languageCode - onContentLoaded: (data) => { - Qt.callLater(() => { - root.translations = data; - root.isLoading = false; - }); - } - } - - TranslationReader { - id: generatedTranslationFileView - translationsDir: root.generatedTranslationsDir - languageCode: root.languageCode - onContentLoaded: (data) => { - Qt.callLater(() => { - root.generatedTranslations = data; - root.isLoading = false; - }); - } - } +import Ryoku.Ui.Singletons as RyokuUi +QtObject { + readonly property string languageCode: RyokuUi.I18n.lang function tr(text) { - // Special cases - if (!text) return ""; - var key = text.toString(); - if (root.isLoading || (!root?.translations?.hasOwnProperty(key) && !root?.generatedTranslations?.hasOwnProperty(key))) - return key; - - // Normal cases - var translation = root.translations[key] || root.generatedTranslations[key] || key; - // print(key, "-> [", root.translations[key], root.generatedTranslations[key], key, "] ->", translation); - if (translation.endsWith(root.translationKeepSuffix)) { - translation = translation.substring(0, translation.length - root.translationKeepSuffix.length).trim(); - } - return translation; - } - - component TranslationScanner: Process { - id: translationScanner - required property string translationsDir - // Reported when the directory cannot be listed at all. The shell's own catalogue always - // has English; a user override directory that does not exist yet has nothing. - property var fallbackLanguages: ["en_US"] - signal languagesScanned(var languages) - - command: ["find", translationScanner.translationsDir, "-name", "*.json", "-exec", "basename", "{}", ".json", ";"] - running: true - - stdout: StdioCollector { - id: languagesCollector - onStreamFinished: { - const output = languagesCollector.text; - const files = output.trim().split('\n').map(f => f.trim()); - translationScanner.languagesScanned(files); - } - } - - onExited: (exitCode, exitStatus) => { - if (exitCode !== 0) { - translationScanner.languagesScanned(translationScanner.fallbackLanguages); - } - } - } - - component TranslationReader: FileView { - id: translationReader - required property string translationsDir - property string languageCode: root.languageCode - signal contentLoaded(var data) - - function reread() { // Proper reload in case the file was incorrect before - translationReader.path = ""; - const availableLanguages = translationReader === generatedTranslationFileView - ? root.availableGeneratedLanguages - : root.availableLanguages; - if (!availableLanguages || !availableLanguages.includes(translationReader.languageCode)) { - translationReader.contentLoaded({}); - return; - } - translationReader.path = `${translationReader.translationsDir}/${translationReader.languageCode}.json`; - translationReader.reload(); - } - path: "" - - onLoaded: { - var textContent = ""; - try { - textContent = text(); - var jsonData = JSON.parse(textContent); - translationReader.contentLoaded(jsonData); - } catch (e) { - console.log("[Translation] Failed to load translations:", e); - translationReader.contentLoaded({}); - } - } - onLoadFailed: error => { - translationReader.contentLoaded({}); - } + return RyokuUi.I18n.tr(text) } } diff --git a/ryoku/shell/quickshell/welcome/shell.qml b/ryoku/shell/quickshell/welcome/shell.qml index 07a69a7364..49984158c0 100644 --- a/ryoku/shell/quickshell/welcome/shell.qml +++ b/ryoku/shell/quickshell/welcome/shell.qml @@ -3,6 +3,9 @@ import Quickshell import Ryoku.Ui.Singletons ShellRoot { + Component.onCompleted: Quickshell.watchFiles = !Quickshell.shellDir.startsWith( + (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/quickshell/") + FloatingWindow { id: win diff --git a/ryoku/shell/ryogami/daemon/apply.go b/ryoku/shell/ryogami/daemon/apply.go index eb63351b5a..30be7fd252 100644 --- a/ryoku/shell/ryogami/daemon/apply.go +++ b/ryoku/shell/ryogami/daemon/apply.go @@ -42,7 +42,10 @@ func (d *daemon) applyWallpaperReasonRecord(reason, wpType, path, mode string, o if record { d.previewMu.Lock() defer d.previewMu.Unlock() - d.preview = nil + if d.preview != nil { + d.preview = nil + writeWallPreviewPalette(nil) + } } if pickOnly { if !record { diff --git a/ryoku/shell/ryogami/daemon/daemon.go b/ryoku/shell/ryogami/daemon/daemon.go index eb4d6be75f..e6cac779b1 100644 --- a/ryoku/shell/ryogami/daemon/daemon.go +++ b/ryoku/shell/ryogami/daemon/daemon.go @@ -109,6 +109,10 @@ func (d *daemon) broadcast(name string, data interface{}) { if err != nil { return } + if name == "ryogami.wall.toggle" || name == "ryogami.wall.settings" { + d.events.publishControl(string(b)) + return + } d.events.publish(string(b)) } @@ -140,6 +144,7 @@ func runDaemon() error { return err } fmt.Fprintf(os.Stderr, "ryogami: listening on %s\n", sock) + writeWallPreviewPalette(nil) d := &daemon{ cfg: cfg, @@ -436,16 +441,10 @@ func (d *daemon) serveRequests(conn net.Conn, r *bufio.Reader, first string) { return false } if was == nil && events != nil { + d.events.setControl(events) if d.ui != nil { d.ui.subscribed(connectionID, func(name string, data map[string]interface{}) { - b, err := json.Marshal(event{Event: name, Data: data}) - if err != nil { - return - } - select { - case events <- string(b): - default: - } + d.broadcast(name, data) }) } subscribed <- events diff --git a/ryoku/shell/ryogami/daemon/daemon_conn_test.go b/ryoku/shell/ryogami/daemon/daemon_conn_test.go index da91c5db9e..a3862559c0 100644 --- a/ryoku/shell/ryogami/daemon/daemon_conn_test.go +++ b/ryoku/shell/ryogami/daemon/daemon_conn_test.go @@ -354,3 +354,28 @@ func TestPickerOpenDuringLaunchIsDeliveredAfterSubscribe(t *testing.T) { t.Fatal("subscribed picker did not report ready") } } + +func TestPickerToggleSurvivesFullEventQueue(t *testing.T) { + d := &daemon{events: newEventHub()} + events := d.events.subscribe() + d.events.setControl(events) + defer d.events.unsubscribe(events) + + for i := range cap(events) { + d.broadcast("ryogami.wall.cached", map[string]interface{}{"index": i}) + } + d.broadcast("ryogami.wall.toggle", map[string]interface{}{}) + + found := false + for range cap(events) { + var ev capturedEvent + if line := <-events; json.Unmarshal([]byte(line), &ev) != nil { + t.Fatalf("invalid event: %s", line) + } else if ev.Event == "ryogami.wall.toggle" { + found = true + } + } + if !found { + t.Fatal("picker toggle was dropped behind a full subscriber queue") + } +} diff --git a/ryoku/shell/ryogami/daemon/preview.go b/ryoku/shell/ryogami/daemon/preview.go index c0ac104a6d..b1cd186923 100644 --- a/ryoku/shell/ryogami/daemon/preview.go +++ b/ryoku/shell/ryogami/daemon/preview.go @@ -1,8 +1,10 @@ package main import ( + "encoding/json" "fmt" "os" + "path/filepath" ) // The wall preview: the picker paints a still it has not committed, so the @@ -21,6 +23,7 @@ type previewState struct { path string prevName string committed wallFrame + epoch uint64 } func mergePreviewOutputs(current, next []string, fresh bool) []string { @@ -74,6 +77,7 @@ func (d *daemon) previewWallpaper(path string, outputs []string) error { d.preview = &previewState{prevName: prevName, committed: committed} } d.preview.outputs = mergePreviewOutputs(d.preview.outputs, outputs, fresh) + d.preview.epoch++ d.preview.path = path return nil } @@ -90,7 +94,25 @@ func (d *daemon) previewWallpaperKey(key string, outputs []string) error { if entry.Type != "static" { return fmt.Errorf("unsupported type for preview: %s", entry.Type) } - return d.previewWallpaper(entry.Path, outputs) + if err := d.previewWallpaper(entry.Path, outputs); err != nil { + return err + } + d.previewMu.Lock() + if d.preview == nil || d.preview.path != entry.Path { + d.previewMu.Unlock() + return nil + } + epoch := d.preview.epoch + state := d.preview + d.previewMu.Unlock() + go func() { + colors, err := d.previewColorsFor(key) + if err != nil { + return + } + d.publishPreviewPalette(state, epoch, entry.Path, colors) + }() + return nil } // clearPreview ends the live preview, repainting the committed wallpaper on @@ -105,6 +127,7 @@ func (d *daemon) clearPreview() error { return nil } d.preview = nil + writeWallPreviewPalette(nil) d.surface.restorePreview(st.committed, st.outputs) if st.prevName != "" { d.setCurrent(st.prevName) @@ -112,6 +135,30 @@ func (d *daemon) clearPreview() error { return nil } +func (d *daemon) publishPreviewPalette(state *previewState, epoch uint64, path string, colors map[string]string) { + d.previewMu.Lock() + defer d.previewMu.Unlock() + if d.preview == state && d.preview.epoch == epoch && d.preview.path == path { + writeWallPreviewPalette(colors) + } +} + +func writeWallPreviewPalette(colors map[string]string) { + runtimeDir := os.Getenv("XDG_RUNTIME_DIR") + if runtimeDir == "" { + return + } + data := []byte("{}") + if colors != nil { + var err error + data, err = json.Marshal(colors) + if err != nil { + return + } + } + _ = os.WriteFile(filepath.Join(runtimeDir, "ryoku-wall-preview.json"), data, 0o600) +} + // repaintCommitted repaints the committed wallpaper on the given outputs, // resolving each the way startup restore does: the stored state first, with // the active workspace's assignment overlaid on the outputs it owns. A diff --git a/ryoku/shell/ryogami/daemon/preview_test.go b/ryoku/shell/ryogami/daemon/preview_test.go index 25ab1e594c..9f7222a3c4 100644 --- a/ryoku/shell/ryogami/daemon/preview_test.go +++ b/ryoku/shell/ryogami/daemon/preview_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) // The wall preview, pinned down: a preview is a paint, never an apply. It @@ -323,3 +324,94 @@ func TestPreviewByKeyResolvesEntry(t *testing.T) { t.Fatalf("preview state=%+v, want it still tracking the keyed still after the rejections", st) } } + +func TestPreviewPaletteIsIsolatedCachedAndRestored(t *testing.T) { + runtimeDir := t.TempDir() + t.Setenv("XDG_RUNTIME_DIR", runtimeDir) + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + d := previewTestDaemon(t) + previewTestStill(t, d, "old.png") + wall := previewTestStill(t, d, "new.png") + d.store.mutate("new.png", func(e *Entry) { e.Thumb = wall; e.Mtime = 42 }) + committed := []byte(`{"primary":"#123456"}`) + if err := os.MkdirAll(filepath.Dir(themeColorsPath()), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(themeColorsPath(), committed, 0o644); err != nil { + t.Fatal(err) + } + previewMu.Lock() + oldCache := previewCache + previewCache = map[string]cachedPreview{} + previewMu.Unlock() + oldGenerator := previewColorGenerator + t.Cleanup(func() { + previewColorGenerator = oldGenerator + previewMu.Lock() + previewCache = oldCache + previewMu.Unlock() + }) + generated := make(chan struct{}, 4) + previewColorGenerator = func(string) (map[string]string, error) { + generated <- struct{}{} + return map[string]string{"primary": "#abcdef", "background": "#222222"}, nil + } + previewPath := filepath.Join(runtimeDir, "ryoku-wall-preview.json") + for range 2 { + if err := d.previewWallpaperKey("new.png", nil); err != nil { + t.Fatal(err) + } + deadline := time.Now().Add(time.Second) + for { + b, err := os.ReadFile(previewPath) + if err == nil && strings.Contains(string(b), "#abcdef") { + break + } + if time.Now().After(deadline) { + t.Fatal("preview palette did not reach the shell-only runtime file") + } + time.Sleep(5 * time.Millisecond) + } + if err := d.clearPreview(); err != nil { + t.Fatal(err) + } + b, err := os.ReadFile(previewPath) + if err != nil || string(b) != "{}" { + t.Fatalf("cancel palette = %s, %v", b, err) + } + b, err = os.ReadFile(themeColorsPath()) + if err != nil || string(b) != string(committed) { + t.Fatalf("committed palette changed during preview: %s, %v", b, err) + } + } + select { + case <-generated: + default: + t.Fatal("preview did not generate a palette") + } + select { + case <-generated: + t.Fatal("repeating a preview generated its palette twice") + default: + } +} + +func TestClearedPreviewDropsLatePalette(t *testing.T) { + runtimeDir := t.TempDir() + t.Setenv("XDG_RUNTIME_DIR", runtimeDir) + d := previewTestDaemon(t) + wall := previewTestStill(t, d, "late.png") + if err := d.previewWallpaper(wall, nil); err != nil { + t.Fatal(err) + } + state := d.preview + epoch := state.epoch + if err := d.clearPreview(); err != nil { + t.Fatal(err) + } + d.publishPreviewPalette(state, epoch, wall, map[string]string{"primary": "#abcdef"}) + b, err := os.ReadFile(filepath.Join(runtimeDir, "ryoku-wall-preview.json")) + if err != nil || string(b) != "{}" { + t.Fatalf("late palette repainted after cancel: %s, %v", b, err) + } +} diff --git a/ryoku/shell/ryogami/daemon/theme_preview.go b/ryoku/shell/ryogami/daemon/theme_preview.go index aad093ffd0..d93c48625b 100644 --- a/ryoku/shell/ryogami/daemon/theme_preview.go +++ b/ryoku/shell/ryogami/daemon/theme_preview.go @@ -13,6 +13,8 @@ import ( const previewCommandTimeout = 10 * time.Second +var previewColorGenerator = matugenPreviewColors + func matugenPreviewColors(img string) (map[string]string, error) { if img == "" { return nil, fmt.Errorf("no image to preview") @@ -54,7 +56,7 @@ func (d *daemon) previewColorsFor(key string) (map[string]string, error) { } img := entry.Thumb if img == "" { - return nil, fmt.Errorf("no thumbnail for %q", key) + img = entry.Path } previewMu.Lock() if c, ok := previewCache[key]; ok && c.mtime == entry.Mtime { @@ -74,7 +76,7 @@ func (d *daemon) previewColorsFor(key string) (map[string]string, error) { } previewMu.Unlock() - colors, err := matugenPreviewColors(img) + colors, err := previewColorGenerator(img) if err != nil { return nil, err } diff --git a/ryoku/shell/ryogami/daemon/topics.go b/ryoku/shell/ryogami/daemon/topics.go index 8f78eeb4d1..c3d6aed99f 100644 --- a/ryoku/shell/ryogami/daemon/topics.go +++ b/ryoku/shell/ryogami/daemon/topics.go @@ -54,8 +54,9 @@ func (t *stateTopic) unsubscribe(ch chan string) { // eventHub fans broadcast events (already-serialized Event JSON lines) out to // subscribed connections. No retention: events are notifications, not state. type eventHub struct { - mu sync.Mutex - subs map[chan string]struct{} + mu sync.Mutex + subs map[chan string]struct{} + control chan string } func newEventHub() *eventHub { @@ -74,6 +75,17 @@ func (h *eventHub) unsubscribe(ch chan string) { h.mu.Lock() defer h.mu.Unlock() delete(h.subs, ch) + if h.control == ch { + h.control = nil + } +} + +func (h *eventHub) setControl(ch chan string) { + h.mu.Lock() + defer h.mu.Unlock() + if _, subscribed := h.subs[ch]; subscribed { + h.control = ch + } } func (h *eventHub) publish(line string) { @@ -86,3 +98,26 @@ func (h *eventHub) publish(line string) { } } } + +// publishControl makes room for a user action when the picker has filled its +// event queue. Other subscribers keep the ordinary lossy behavior: an +// irrelevant picker control never displaces one of their queued events. +func (h *eventHub) publishControl(line string) { + h.mu.Lock() + defer h.mu.Unlock() + for ch := range h.subs { + select { + case ch <- line: + continue + default: + } + if ch != h.control { + continue + } + select { + case <-ch: + default: + } + ch <- line + } +} diff --git a/ryoku/shell/ryogami/daemon/workspaces.go b/ryoku/shell/ryogami/daemon/workspaces.go index 556b8b0623..c45445da00 100644 --- a/ryoku/shell/ryogami/daemon/workspaces.go +++ b/ryoku/shell/ryogami/daemon/workspaces.go @@ -477,7 +477,10 @@ func (d *daemon) assignWorkspace(p map[string]interface{}) (workspaceWall, error } d.previewMu.Lock() defer d.previewMu.Unlock() - d.preview = nil + if d.preview != nil { + d.preview = nil + writeWallPreviewPalette(nil) + } wall, err := d.workspaces.assign(target, workspaceWallParam(p, target.Output)) if err != nil { return workspaceWall{}, err @@ -497,7 +500,10 @@ func (d *daemon) unassignWorkspace(p map[string]interface{}) (bool, error) { } d.previewMu.Lock() defer d.previewMu.Unlock() - d.preview = nil + if d.preview != nil { + d.preview = nil + writeWallPreviewPalette(nil) + } removed := d.workspaces.unassign(target) if removed { if current, err := d.workspaces.current(target.Output); err == nil && sameWorkspace(current, target) { diff --git a/ryoku/shell/ryogami/picker/qml/PickerState.qml b/ryoku/shell/ryogami/picker/qml/PickerState.qml index 1187fe2880..111fd8fdef 100644 --- a/ryoku/shell/ryogami/picker/qml/PickerState.qml +++ b/ryoku/shell/ryogami/picker/qml/PickerState.qml @@ -286,7 +286,7 @@ QtObject { Daemon.call("wall.preview.clear", {}) } } - if (state._bool(state._shellHover)) + if (state._bool(state._shellHover) && !state._bool(state._livePreview)) Daemon.call("palette.hover", key ? { key: key } : ({})) } diff --git a/ryoku/shell/ryogami/picker/qml/browser/BrowserTabs.qml b/ryoku/shell/ryogami/picker/qml/browser/BrowserTabs.qml index 347973e285..7db034b4ad 100644 --- a/ryoku/shell/ryogami/picker/qml/browser/BrowserTabs.qml +++ b/ryoku/shell/ryogami/picker/qml/browser/BrowserTabs.qml @@ -31,7 +31,7 @@ Item { Text { anchors.verticalCenter: parent.verticalCenter - text: "力" + text: UiPreferences.brandMark("力") color: Theme.primary font.family: Theme.jp font.pixelSize: Theme.fs(18) diff --git a/ryoku/shell/ryogami/picker/qml/chrome/filterbar/CollectionTabs.qml b/ryoku/shell/ryogami/picker/qml/chrome/filterbar/CollectionTabs.qml index 0ed465b3e1..4d2170dfc4 100644 --- a/ryoku/shell/ryogami/picker/qml/chrome/filterbar/CollectionTabs.qml +++ b/ryoku/shell/ryogami/picker/qml/chrome/filterbar/CollectionTabs.qml @@ -61,7 +61,7 @@ Item { id: kanji anchors.horizontalCenter: parent.horizontalCenter y: (tab.hovered ? 4 : 6) * Theme.scale - text: tab.modelData.jp + text: UiPreferences.text(tab.modelData.jp, "") font.family: Theme.jp font.pixelSize: Theme.fs(9.5) font.letterSpacing: 2 diff --git a/ryoku/shell/scripts/ryoku-cmd-game-mode b/ryoku/shell/scripts/ryoku-cmd-game-mode index 9d7550a3ae..3f6759fb13 100755 --- a/ryoku/shell/scripts/ryoku-cmd-game-mode +++ b/ryoku/shell/scripts/ryoku-cmd-game-mode @@ -11,12 +11,8 @@ # stop restore, clear # toggle # -# revert = exact. compositor changes are runtime overrides the seam applies; a -# reload rereads the config and clears them, so nothing to track. the wifi helper -# saves + restores the prior power-save state, ryoku-game-tune saves + restores -# every knob it touches, and the prior power profile is written down here. -# `start` is safe to re-run after a relogin (the compositor comes back fresh, -# request is still on disk). +# The request is stored before the compositor is stripped, so config reloads +# and login keep it stripped until stop clears the request before restoring. # # AC only, on purpose. every lever here trades power and heat for latency, and # on battery that is a bad deal the user cannot see: deep idle states off and a @@ -39,11 +35,8 @@ tune_helper="${RYOKU_GAME_TUNE_BIN-$(command -v ryoku-game-tune || true)}" idle_helper="${RYOKU_IDLE_BIN-$(command -v ryoku-idle || true)}" shell_helper="${RYOKU_SHELL_BIN-$(command -v ryoku-shell || true)}" -# The window manager is reached only through the seam, asked by capability and -# never by name. The live decoration strip below is a runtime config override; a -# compositor that cannot evaluate its config live has no equivalent, so game mode -# still boosts power there and leaves the desktop's look alone instead of -# dispatching into a socket that will not answer. +# The provider owns the live decoration strip, while its config reads the +# persisted request on every reload. Other compositors keep their own look. ryoku_bin="${RYOKU_BIN-$(command -v ryoku || true)}" stripped=0 @@ -70,27 +63,23 @@ on_battery() { } # ── compositor ──────────────────────────────────────────────────────────────── -# The decoration strip is a runtime config override the seam applies through -# decoration.gameMode: it kills present-time cost (blur, shadow, rounding, -# opacity blending, animations) and flips to a low-latency path with immediate -# tearing on a sole fullscreen window and fullscreen-only VRR. It also keeps a -# fullscreen window rendering while it is off-screen, so a game on another -# workspace keeps being scheduled instead of stalling on the frame callbacks an -# invisible surface never gets (its audio would keep playing, input would die, -# and multiplayer would time out). The strip is gated on liveConfigEval, so a -# compositor with a file-only config leaves the desktop's look alone and game -# mode still boosts power there. off reloads the config, which rereads it and -# drops every override. +# On compositors with live config evaluation, the provider strips decoration. +# A failed strip is an error, not a successful Game Mode activation. deco_strip() { wm_can liveConfigEval || return 0 - "$ryoku_bin" wm act decoration.gameMode on >/dev/null 2>&1 && stripped=1 || true + local error + if ! error=$("$ryoku_bin" wm act decoration.gameMode on 2>&1); then + printf 'Game Mode: compositor strip failed: %s\n' "$error" >&2 + notify "力 Game Mode" "Could not turn off compositor effects: $error" + return 1 + fi + stripped=1 } deco_restore() { wm_can configReload || return 0 - "$ryoku_bin" wm act decoration.gameMode off >/dev/null 2>&1 || true + "$ryoku_bin" wm act decoration.gameMode off >&2 } - # The active provider owns the governor, EPP and platform_profile as a set, so # asking it for `performance` is the whole CPU story and needs no root. Route # both reads and writes through ryoku-shell so game mode follows the same @@ -160,12 +149,16 @@ start() { return 1 fi - mkdir -p "$state_dir" 2>/dev/null || true - deco_strip + mkdir -p "$state_dir" || return 1 + printf 'enabled\n' >"$state_file" || return 1 + if ! deco_strip; then + rm -f "$state_file" + deco_restore + return 1 + fi profile_fast tune apply wifi_set off - printf 'enabled\n' >"$state_file" 2>/dev/null || true if (( stripped )); then notify "力 Game Mode" "On - performance profile, deep idle off, compositor stripped" else @@ -174,11 +167,16 @@ start() { } stop() { - deco_restore - tune restore - profile_restore - wifi_set on - rm -f "$state_file" 2>/dev/null || true + local failed=0 + rm -f "$state_file" || failed=1 + deco_restore || failed=1 + tune restore || failed=1 + profile_restore || failed=1 + wifi_set on || failed=1 + if (( failed )); then + notify "力 Game Mode" "Could not fully restore the desktop" + return 1 + fi notify "力 Game Mode" "Off - desktop restored" } diff --git a/ryoku/ui/Chips.qml b/ryoku/ui/Chips.qml index 60316537bb..85b7d3448b 100644 --- a/ryoku/ui/Chips.qml +++ b/ryoku/ui/Chips.qml @@ -22,6 +22,7 @@ Flow { } spacing: 5 + height: childrenRect.height Repeater { model: chips.options diff --git a/ryoku/ui/Empty.qml b/ryoku/ui/Empty.qml index fb08e6bcb6..b7e9a070f0 100644 --- a/ryoku/ui/Empty.qml +++ b/ryoku/ui/Empty.qml @@ -18,7 +18,7 @@ Column { Motif { anchors.fill: parent; kind: "rings"; strength: 0.14 } Text { anchors.centerIn: parent - text: "空" + text: UiPreferences.text("空", "") color: Tokens.inkDim font.family: Tokens.jp font.pixelSize: 42 diff --git a/ryoku/ui/Marginalia.qml b/ryoku/ui/Marginalia.qml index a912715b51..4f45713482 100644 --- a/ryoku/ui/Marginalia.qml +++ b/ryoku/ui/Marginalia.qml @@ -27,7 +27,7 @@ Row { } Text { - visible: mg.kana !== "" + visible: UiPreferences.showJapaneseGloss && mg.kana !== "" anchors.verticalCenter: parent.verticalCenter text: mg.kana; color: Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 12 diff --git a/ryoku/ui/SettingCard.qml b/ryoku/ui/SettingCard.qml index ef50239345..99f6e6368c 100644 --- a/ryoku/ui/SettingCard.qml +++ b/ryoku/ui/SettingCard.qml @@ -51,7 +51,7 @@ Item { anchors.verticalCenter: parent.verticalCenter } Text { - visible: card.kana !== "" + visible: UiPreferences.showJapaneseGloss && card.kana !== "" text: card.kana color: Tokens.inkFaint font.family: Tokens.jp; font.pixelSize: 12 diff --git a/ryoku/ui/Singletons/UiPreferences.qml b/ryoku/ui/Singletons/UiPreferences.qml new file mode 100644 index 0000000000..529a14d208 --- /dev/null +++ b/ryoku/ui/Singletons/UiPreferences.qml @@ -0,0 +1,50 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io + +// Cross-surface presentation preferences from the daemon-owned shell store. +// Readers watch the file; the daemon remains its only writer. +Singleton { + id: root + + property bool showJapaneseGloss: true + + function containsJapanese(value) { + return /[\u3040-\u30ff\u3400-\u9fff]/.test(String(value || "")); + } + + function text(value, fallback) { + const source = String(value || ""); + return root.showJapaneseGloss || !root.containsJapanese(source) + ? source : String(fallback || ""); + } + + function shippedText(value, shippedValue, fallback) { + const source = String(value || ""); + return !root.showJapaneseGloss && source === String(shippedValue || "") + ? String(fallback || "") : source; + } + + function brandMark(value) { + const source = String(value || "") || "力"; + return !root.showJapaneseGloss && source === "力" ? "R" : source; + } + + function load() { + var data = {}; + try { data = JSON.parse(store.text()) || {}; } catch (e) {} + root.showJapaneseGloss = data.showJapaneseGloss !== false; + } + + FileView { + id: store + path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/ryoku/shell.json" + blockLoading: true + watchChanges: true + printErrors: false + onLoaded: root.load() + onFileChanged: reload() + onLoadFailed: root.showJapaneseGloss = true + } +} diff --git a/ryoku/ui/Singletons/qmldir b/ryoku/ui/Singletons/qmldir index 8e892bdd45..1d19a82924 100644 --- a/ryoku/ui/Singletons/qmldir +++ b/ryoku/ui/Singletons/qmldir @@ -11,3 +11,4 @@ singleton Spawn Spawn.qml singleton Version Version.qml singleton Wm Wm.qml singleton TimeFormat TimeFormat.qml +singleton UiPreferences UiPreferences.qml diff --git a/ryoku/ui/Watermark.qml b/ryoku/ui/Watermark.qml index 3d838f6f03..8f020238b3 100644 --- a/ryoku/ui/Watermark.qml +++ b/ryoku/ui/Watermark.qml @@ -21,7 +21,7 @@ Item { Text { id: glyph - text: wm.text + text: UiPreferences.showJapaneseGloss ? wm.text : "" anchors { right: parent.right; bottom: parent.bottom } anchors.rightMargin: -Math.round(wm.width * 0.05) anchors.bottomMargin: -Math.round(wm.height * 0.16) diff --git a/ryoku/wm/hyprland/act.go b/ryoku/wm/hyprland/act.go index 2887e5e7b6..c1218112d0 100644 --- a/ryoku/wm/hyprland/act.go +++ b/ryoku/wm/hyprland/act.go @@ -301,9 +301,9 @@ func runAct(args []string) error { } switch state { case "on": - return evalLua(gameModeLua) + return evalLua(`dofile(` + luaStr(filepath.Join(hyprModulesDir(), "game_mode.lua")) + `)`) case "off": - // A reload rereads the config and clears every eval override on set. + // The caller clears the persisted request before restoring defaults. _, err := ctl("reload") return err } @@ -405,30 +405,6 @@ func summon(title string) error { return nil } -// gameModeLua is the exact eval the ported ryoku-cmd-game-mode ran for its -// decoration strip: it kills present-time cost (blur, shadow, rounding, opacity -// blending, animations) and flips to the low-latency path, an immediate window -// rule for tearing on a sole fullscreen window, fullscreen-only VRR, and -// render_unfocused so a fullscreen game keeps drawing while off-screen instead -// of stalling on frame callbacks it never receives. decoration.gameMode off -// reloads the config, which rereads it and clears every override here. -const gameModeLua = ` -hl.config({ - general = { allow_tearing = true }, - decoration = { - rounding = 0, - active_opacity = 1.0, - inactive_opacity = 1.0, - blur = { enabled = false }, - shadow = { enabled = false }, - }, - animations = { enabled = false }, - misc = { vrr = 2 }, -}) -hl.window_rule({ name = "game-mode-immediate", match = { class = ".*" }, immediate = true }) -hl.window_rule({ name = "game-mode-render-unfocused", match = { fullscreen = true }, render_unfocused = true }) -` - // touchpad locks or unlocks every touchpad the FN touchpad key drives. Ported // from ryoku-cmd-touchpad: the Lua config provider rejects `hyprctl keyword`, so // device state is flipped through `hyprctl eval` against the same hl.device API diff --git a/ryoku/wm/hyprland/act_test.go b/ryoku/wm/hyprland/act_test.go index f6fb5e8b30..b830e637c4 100644 --- a/ryoku/wm/hyprland/act_test.go +++ b/ryoku/wm/hyprland/act_test.go @@ -71,10 +71,9 @@ func TestActEmitsLuaDialect(t *testing.T) { []string{"switchxkblayout", "all", "next"}}, {"reload config only", []string{"config.reload", "config-only"}, []string{"reload", "config-only"}}, - // game mode strips decorations through eval and restores them with a - // reload, exactly what the ported ryoku-cmd-game-mode ran. + // The same shipped Lua module runs live and on config reload. {"game mode on", []string{"decoration.gameMode", "on"}, - []string{"eval", gameModeLua}}, + []string{"eval", `dofile(` + luaStr(filepath.Join(hyprModulesDir(), "game_mode.lua")) + `)`}}, {"game mode off", []string{"decoration.gameMode", "off"}, []string{"reload"}}, } diff --git a/ryoku/wm/niri/config_binds.go b/ryoku/wm/niri/config_binds.go index 99fdfddfc9..30a207a93e 100644 --- a/ryoku/wm/niri/config_binds.go +++ b/ryoku/wm/niri/config_binds.go @@ -25,12 +25,13 @@ import ( // catalogue copy when niri's mechanic differs from the neutral description. In a // family action, {n} is the workspace number, substituted per expanded chord. type niriBind struct { - action string - reason string // set when action == ""; the Unhonored copy - label string // replaces the catalogue Label when niri's mechanic differs - hint string // replaces the catalogue Hint when niri's mechanic differs - noRepeat bool // repeat=false - cooldownMs int // cooldown-ms=N, for wheel binds + action string + reason string // set when action == ""; the Unhonored copy + label string // replaces the catalogue Label when niri's mechanic differs + hint string // replaces the catalogue Hint when niri's mechanic differs + noRepeat bool // repeat=false + ignoreInhibitor bool // allow-inhibiting=false + cooldownMs int // cooldown-ms=N, for wheel binds } func spawnArgs(args ...string) string { @@ -165,7 +166,7 @@ func shippedBinds() map[string]niriBind { "shell.rashin": {action: spawnSh("ryoku-summon Rashin flock -n -o /tmp/rashin-app.lock rashin-app")}, "shell.cheatsheet": {action: spawnSh("pkill -x -f 'qs -c keys' 2>/dev/null || " + wm.QmlEnv + " flock -n -o /tmp/ryoku-keys.lock qs -c keys")}, "shell.lock": {action: spawnArgs("ryoku-shell", "lock")}, - "shell.quicksettings": {action: spawnArgs("ryoku-shell", "quicksettings")}, + "shell.quicksettings": {action: spawnArgs("ryoku-shell", "quicksettings"), ignoreInhibitor: true}, "shell.wallpaper": {action: spawnArgs("ryogami", "wallpaper", "ui")}, "shell.wallpaperRandom": {action: spawnArgs("ryogami", "wallpaper", "random")}, "shell.ryovm": {action: spawnSh("ryoku-summon ryovm " + wm.QmlEnv + " flock -n -o /tmp/ryovm.lock qs -c ryovm")}, @@ -173,7 +174,7 @@ func shippedBinds() map[string]niriBind { "shell.visualizer": {action: spawnArgs("ryoku-shell", "visualizer")}, "shell.visualizerOverlay": {action: spawnArgs("ryoku-shell", "visualizer-overlay")}, "shell.visualizerPlace": {action: spawnArgs("ryoku-shell", "visualizer-place")}, - "shell.voice": {action: spawnArgs("ryoku-shell", "voice")}, + "shell.voice": {action: spawnArgs("ryoku-shell", "voice"), noRepeat: true}, "shell.settings": {action: spawnArgs("ryoku-shell", "hub", "open")}, "shell.stageEditor": {action: spawnArgs("ryoku-shell", "hub", "open", "widgets")}, "shell.screenshot": {action: spawnSh(wm.QmlEnv + " ryoku-cmd-ryoshot")}, @@ -181,14 +182,14 @@ func shippedBinds() map[string]niriBind { "shell.screenshotMonitor": {action: spawnSh(wm.QmlEnv + " RYOSHOT_MODE=monitor ryoku-cmd-ryoshot")}, "shell.colorPicker": {action: spawnArgs("hyprpicker", "-a")}, "shell.restartAudio": {action: spawnArgs("ryoku-restart-audio")}, - "shell.inhibitShortcuts": {action: "toggle-keyboard-shortcuts-inhibit"}, + "shell.inhibitShortcuts": {action: "toggle-keyboard-shortcuts-inhibit", ignoreInhibitor: true}, // Media. Locked in the catalogue, so genBinds emits allow-when-locked. "media.volumeUp": {action: spawnArgs("ryoku-volume", "up")}, "media.volumeDown": {action: spawnArgs("ryoku-volume", "down")}, - "media.mute": {action: spawnArgs("wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle")}, - "media.micMute": {action: spawnArgs("wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle")}, - "media.play": {action: spawnArgs("playerctl", "play-pause")}, + "media.mute": {action: spawnArgs("wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"), noRepeat: true}, + "media.micMute": {action: spawnArgs("wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle"), noRepeat: true}, + "media.play": {action: spawnArgs("playerctl", "play-pause"), noRepeat: true}, "media.next": {action: spawnArgs("playerctl", "next")}, "media.prev": {action: spawnArgs("playerctl", "previous")}, @@ -208,9 +209,9 @@ func shippedBinds() map[string]niriBind { // outBind is one resolved bind, ready for the config writer: the niri chord and // action apply writes plus the KDL properties niri's parser reads off the node. type outBind struct { - chord, action string - locked, noRepeat bool - cooldownMs int + chord, action string + locked, noRepeat, ignoreInhibitor bool + cooldownMs int } // resolveBinds expands the catalogue into the emitted bind set and folds the @@ -253,24 +254,25 @@ func resolveBinds(s niriStore) ([]outBind, []wm.Unhonored) { // user set on that keysym; among twins the earlier (higher priority) // resolution wins, matching the one-winner rule. type pendingTwin struct { - hubChord string - action string - locked bool - noRepeat bool - cooldownMs int + hubChord string + action string + locked bool + noRepeat bool + ignoreInhibitor bool + cooldownMs int } var twins []pendingTwin - emit := func(hubChord, niriChord, action string, locked, noRepeat bool, cooldownMs int) { + emit := func(hubChord, niriChord, action string, locked, noRepeat, ignoreInhibitor bool, cooldownMs int) { if niriChord == "" || claimed[niriChord] { return } claimed[niriChord] = true out = append(out, outBind{ chord: niriChord, action: action, - locked: locked, noRepeat: noRepeat, cooldownMs: cooldownMs, + locked: locked, noRepeat: noRepeat, ignoreInhibitor: ignoreInhibitor, cooldownMs: cooldownMs, }) if len(wm.NumpadAliases(hubChord)) > 0 { - twins = append(twins, pendingTwin{hubChord, action, locked, noRepeat, cooldownMs}) + twins = append(twins, pendingTwin{hubChord, action, locked, noRepeat, ignoreInhibitor, cooldownMs}) } } @@ -288,7 +290,7 @@ func resolveBinds(s niriStore) ([]outBind, []wm.Unhonored) { report = append(report, wm.Unhonored{Key: fmt.Sprintf("desktop.keybinds[%d]", i), Reason: fmt.Sprintf("niri cannot bind the chord %q.", k.Keys)}) continue } - emit(k.Keys, niriChord, action, false, false, 0) + emit(k.Keys, niriChord, action, false, false, false, 0) } cat := wm.ShippedBinds() @@ -334,7 +336,7 @@ func resolveBinds(s niriStore) ([]outBind, []wm.Unhonored) { continue } if niriChord, ok := toNiriChord(to); ok { - emit(to, niriChord, action, cb.Locked, nb.noRepeat, nb.cooldownMs) + emit(to, niriChord, action, cb.Locked, nb.noRepeat, nb.ignoreInhibitor, nb.cooldownMs) } } } @@ -348,7 +350,7 @@ func resolveBinds(s niriStore) ([]outBind, []wm.Unhonored) { for _, t := range twins { for _, alias := range wm.NumpadAliases(t.hubChord) { if aliasChord, ok := toNiriChord(alias); ok { - emit(alias, aliasChord, t.action, t.locked, t.noRepeat, t.cooldownMs) + emit(alias, aliasChord, t.action, t.locked, t.noRepeat, t.ignoreInhibitor, t.cooldownMs) } } } @@ -366,6 +368,9 @@ func genBinds(s niriStore) (string, []wm.Unhonored) { b.WriteString("binds {\n") for _, o := range out { var props []string + if o.ignoreInhibitor { + props = append(props, "allow-inhibiting=false") + } if o.locked { props = append(props, "allow-when-locked=true") } diff --git a/ryoku/wm/niri/config_binds_test.go b/ryoku/wm/niri/config_binds_test.go index d5a43fbaa1..08d7bf5dcf 100644 --- a/ryoku/wm/niri/config_binds_test.go +++ b/ryoku/wm/niri/config_binds_test.go @@ -63,6 +63,28 @@ func TestDirectionalFocusFallsThroughToAdjacentMonitor(t *testing.T) { } } +func TestRecoveryBindsBypassShortcutInhibitor(t *testing.T) { + rendered, _ := genBinds(loadStore("")) + for _, want := range []string{ + `Super+Escape allow-inhibiting=false { spawn "ryoku-shell" "quicksettings"; }`, + `Super+Shift+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }`, + } { + if !strings.Contains(rendered, want) { + t.Errorf("generated binds missing inhibitor recovery %q", want) + } + } +} + +func TestMicMuteKeyDoesNotRepeat(t *testing.T) { + if !defaultBinds()["media.micMute"].noRepeat { + t.Fatal("media.micMute must ignore key repeat or one press can toggle twice") + } + rendered, _ := genBinds(loadStore("")) + if want := `XF86AudioMicMute allow-when-locked=true repeat=false`; !strings.Contains(rendered, want) { + t.Errorf("generated binds missing %q", want) + } +} + func emittedChords(s niriStore) []string { out, _ := resolveBinds(s) chords := make([]string, len(out)) diff --git a/ryoku/wm/niri/config_kdl.go b/ryoku/wm/niri/config_kdl.go index 9837333121..17d3b761a1 100644 --- a/ryoku/wm/niri/config_kdl.go +++ b/ryoku/wm/niri/config_kdl.go @@ -24,9 +24,9 @@ func kdlNum(f float64) string { } // kdlFloat formats a float that niri's parser insists be a float literal, not an -// integer: a spring damping-ratio of 1 must read "1.0", or niri rejects it as an -// integer where a float scalar is required. A value that already carries a point -// or exponent is left as %g gives it. +// integer: a spring damping-ratio, a proportion or an opacity of 1 must read +// "1.0", or niri rejects it as an integer where a float scalar is required. A +// value that already carries a point or exponent is left as %g gives it. func kdlFloat(f float64) string { s := strconv.FormatFloat(f, 'g', -1, 64) if !strings.ContainsAny(s, ".eE") { diff --git a/ryoku/wm/niri/config_kdl_test.go b/ryoku/wm/niri/config_kdl_test.go index f8cf01b918..50ecea65a9 100644 --- a/ryoku/wm/niri/config_kdl_test.go +++ b/ryoku/wm/niri/config_kdl_test.go @@ -203,6 +203,50 @@ func TestLayoutExtrasEmitted(t *testing.T) { } } +// Niri rejects whole-number proportions and opacities written as integer KDL. +func TestWholeFloatsEmittedAsFloats(t *testing.T) { + s := defaultStore() + s.Niri.DefaultColumnWidth = 1 + s.Niri.PresetColumnWidths = Proportions{0.5, 1} + s.WindowRules = []WindowRule{ + {Class: "foo", Action: "columnwidth", Value: "1"}, + {Class: "bar", Action: "opacity", Value: "1"}, + } + out := string(genSettings(s)) + for _, want := range []string{ + " default-column-width { proportion 1.0; }", + "preset-column-widths {\n proportion 0.5\n proportion 1.0\n }", + "default-column-width { proportion 1.0; }", + "opacity 1.0", + } { + if !strings.Contains(out, want) { + t.Errorf("whole float missing %q\n%s", want, out) + } + } + for _, bad := range []string{"proportion 1\n", "proportion 1;", "opacity 1\n"} { + if strings.Contains(out, bad) { + t.Errorf("integer literal %q emitted where niri needs a float\n%s", bad, out) + } + } + s.Niri.PresetWindowHeights = Proportions{0.5, 1} + s.Niri.LayerRules = []LayerRule{{Namespace: "probe", Opacity: 1, CornerRadius: -1}} + s.AppOverrides = []AppOverride{{Class: "kitty", Opacity: 1, Rounding: -1, BorderSize: -1}} + s.WindowRules = append(s.WindowRules, WindowRule{Class: "baz", Action: "scrollfactor", Value: "1"}) + s.Input.Sensitivity = 1 + s.Input.MouseScrollFactor = 2 + s.Input.TouchScrollFactor = 2 + s.Niri.OverviewZoom = 1 + s.Niri.AnimationSlowdown = 2 + dir := niriHome(t) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "settings.kdl"), genSettings(s), 0o644); err != nil { + t.Fatal(err) + } + validateGen(t, dir, "settings.kdl") +} + func TestPreferNoCSD(t *testing.T) { if out := string(genSettings(defaultStore())); !strings.Contains(out, "\nprefer-no-csd\n") { t.Errorf("the default must ask apps to hide client-side title bars\n%s", out) diff --git a/ryoku/wm/niri/config_layout.go b/ryoku/wm/niri/config_layout.go index 8e2e70b64a..86376c0b38 100644 --- a/ryoku/wm/niri/config_layout.go +++ b/ryoku/wm/niri/config_layout.go @@ -24,7 +24,7 @@ func writeLayout(b *strings.Builder, a Appearance, n Niri) { fmt.Fprintf(b, " background-color %s\n", c) } if n.DefaultColumnWidth > 0 { - fmt.Fprintf(b, " default-column-width { proportion %s; }\n", kdlNum(n.DefaultColumnWidth)) + fmt.Fprintf(b, " default-column-width { proportion %s; }\n", kdlFloat(n.DefaultColumnWidth)) } if strings.ToLower(strings.TrimSpace(n.DefaultColumnDisplay)) == "tabbed" { b.WriteString(" default-column-display \"tabbed\"\n") @@ -41,14 +41,14 @@ func writeLayout(b *strings.Builder, a Appearance, n Niri) { if len(n.PresetColumnWidths) > 0 { b.WriteString(" preset-column-widths {\n") for _, w := range n.PresetColumnWidths { - fmt.Fprintf(b, " proportion %s\n", kdlNum(w)) + fmt.Fprintf(b, " proportion %s\n", kdlFloat(w)) } b.WriteString(" }\n") } if len(n.PresetWindowHeights) > 0 { b.WriteString(" preset-window-heights {\n") for _, h := range n.PresetWindowHeights { - fmt.Fprintf(b, " proportion %s\n", kdlNum(h)) + fmt.Fprintf(b, " proportion %s\n", kdlFloat(h)) } b.WriteString(" }\n") } diff --git a/ryoku/wm/niri/config_rules.go b/ryoku/wm/niri/config_rules.go index 13d1b9ed93..3bc9b0296c 100644 --- a/ryoku/wm/niri/config_rules.go +++ b/ryoku/wm/niri/config_rules.go @@ -40,7 +40,7 @@ func writeUserLayerRule(b *strings.Builder, r LayerRule) { b.WriteString("layer-rule {\n") fmt.Fprintf(b, " match namespace=%s\n", kdlStr(r.Namespace)) if r.Opacity >= 0 && r.Opacity <= 1 { - fmt.Fprintf(b, " opacity %s\n", kdlNum(r.Opacity)) + fmt.Fprintf(b, " opacity %s\n", kdlFloat(r.Opacity)) } if r.CornerRadius >= 0 { fmt.Fprintf(b, " geometry-corner-radius %d\n", r.CornerRadius) @@ -139,13 +139,13 @@ var builtinWindowRules = []builtinRule{ func writeOpacityRules(b *strings.Builder, a Appearance) { if a.ActiveOpacity > 0 && a.ActiveOpacity < 1 { b.WriteString("window-rule {\n") - fmt.Fprintf(b, " opacity %s\n", kdlNum(a.ActiveOpacity)) + fmt.Fprintf(b, " opacity %s\n", kdlFloat(a.ActiveOpacity)) b.WriteString("}\n\n") } if a.InactiveOpacity > 0 && a.InactiveOpacity < 1 { b.WriteString("window-rule {\n") b.WriteString(" match is-active=false\n") - fmt.Fprintf(b, " opacity %s\n", kdlNum(a.InactiveOpacity)) + fmt.Fprintf(b, " opacity %s\n", kdlFloat(a.InactiveOpacity)) b.WriteString("}\n\n") } } @@ -185,13 +185,13 @@ func windowRuleProps(r WindowRule) []string { case "norounding": return []string{"geometry-corner-radius 0"} case "opacity": - return []string{fmt.Sprintf("opacity %s", kdlNum(parseFloat(r.Value, 1)))} + return []string{fmt.Sprintf("opacity %s", kdlFloat(parseFloat(r.Value, 1)))} case "workspace": return []string{fmt.Sprintf("open-on-workspace %s", kdlStr(r.Value))} case "noborder": return []string{"border {", " off", "}"} case "columnwidth": - return []string{fmt.Sprintf("default-column-width { proportion %s; }", kdlNum(parseFloat(r.Value, 0.5)))} + return []string{fmt.Sprintf("default-column-width { proportion %s; }", kdlFloat(parseFloat(r.Value, 0.5)))} case "minsize": return sizeProps("min", r.Value) case "maxsize": @@ -258,7 +258,7 @@ func parseWH(s string) (int, int) { func appOverrideProps(a AppOverride) []string { var props []string if a.Opacity >= 0 && a.Opacity <= 1 { - props = append(props, fmt.Sprintf("opacity %s", kdlNum(a.Opacity))) + props = append(props, fmt.Sprintf("opacity %s", kdlFloat(a.Opacity))) } if a.Rounding >= 0 { props = append(props, fmt.Sprintf("geometry-corner-radius %d", a.Rounding)) diff --git a/system/CHANGELOG.md b/system/CHANGELOG.md index 6a1930e888..1990bdea3e 100644 --- a/system/CHANGELOG.md +++ b/system/CHANGELOG.md @@ -1,6 +1,18 @@ # Changelog: system/ ## Unreleased +- **Bluetooth can turn back on after a legacy rfkill block.** Power-off now + records the user's choice through BlueZ without soft-blocking platform radio + switches, while power-on clears old blocks and waits for the controller to + reappear (`hardware/bluetooth/ryoku-bluetooth-power`). +- **Power-profile switches no longer ask an administrator for a password.** + A dev deploy's `~/.local/bin/ryoku-power` can shadow the packaged helper, but + privileged writes now cross through the exact root-owned `/usr/bin/ryoku-power` + path authorized by polkit. Already-converged profiles still skip escalation + entirely (`hardware/power/ryoku-power`, `../tests/power.sh`). +- **Locking no longer piles up lock screens for logged-out sessions.** Old sessions + stop retrying their locks, and a new attempt reuses the lock screen already + running (`hardware/power/ryoku-power-cutover`, `../ryoku/shell/ipc/`). - **QMK keyboard lighting names the package each edition can install.** Arch and CachyOS use the `qmk-hid` AUR package while Fedora and Void translate it to their shipped providers (`../fedora/packages/translations.tsv`, diff --git a/system/hardware/CHANGELOG.md b/system/hardware/CHANGELOG.md index 0bb65ad04d..05c07c8e0b 100644 --- a/system/hardware/CHANGELOG.md +++ b/system/hardware/CHANGELOG.md @@ -17,6 +17,15 @@ headless one exits at once. ### Fixed +- `network/50-ryoku-dns.rules`: **the passwordless DNS grant no longer matches + any path ending in `ryoku-dns`.** The rule tested the program with a basename + regex, so any executable a local wheel user placed at a path ending in + `/ryoku-dns` ran as root through `pkexec` with no password: a local privilege + escalation (GHSA-683q-8wvh-7m89). The grant now pins the exact installed + helper, `action.lookup("program") === "/usr/bin/ryoku-dns"`, matching every + other `ryoku-*` polkit rule. Dev boxes keep working because `deploy.sh` + installs the helper to `/usr/bin` and the shell's `dnsHelperPath()` prefers + that path. - `input/62-ryoku-qmk-hid.rules`: **keyboard lighting returns after hotplug.** A connected QMK or Vial board triggers the saved lighting apply for each active local desktop user on systemd and Void diff --git a/system/hardware/bluetooth/ryoku-bluetooth-power b/system/hardware/bluetooth/ryoku-bluetooth-power index 8ac3bf4816..5b1c89bf52 100755 --- a/system/hardware/bluetooth/ryoku-bluetooth-power +++ b/system/hardware/bluetooth/ryoku-bluetooth-power @@ -26,32 +26,32 @@ saved_state() { esac } +controller_available() { + bluetoothctl list 2>/dev/null | command grep -q '^Controller[[:space:]]' +} + apply_state() { - local state=$1 changed=1 + local state=$1 attempt case $state in off) - if command -v bluetoothctl >/dev/null 2>&1 && bluetoothctl power off >/dev/null 2>&1; then - changed=0 - fi - if command -v rfkill >/dev/null 2>&1 && rfkill block bluetooth >/dev/null 2>&1; then - changed=0 - fi + command -v bluetoothctl >/dev/null 2>&1 \ + && bluetoothctl power off >/dev/null 2>&1 ;; on) - if command -v rfkill >/dev/null 2>&1 && rfkill unblock bluetooth >/dev/null 2>&1; then - changed=0 + if command -v rfkill >/dev/null 2>&1; then + rfkill unblock bluetooth >/dev/null 2>&1 || true fi - if command -v bluetoothctl >/dev/null 2>&1; then - if bluetoothctl power on >/dev/null 2>&1; then - changed=0 - else - sleep 0.4 - bluetoothctl power on >/dev/null 2>&1 && changed=0 + command -v bluetoothctl >/dev/null 2>&1 || return 1 + for ((attempt = 0; attempt < 50; attempt++)); do + if controller_available; then + bluetoothctl power on >/dev/null 2>&1 + return fi - fi + ((attempt == 49)) || sleep 0.1 + done + return 1 ;; esac - return "$changed" } set_state() { diff --git a/system/hardware/network/50-ryoku-dns.rules b/system/hardware/network/50-ryoku-dns.rules index 68c75fd503..8071f541b5 100644 --- a/system/hardware/network/50-ryoku-dns.rules +++ b/system/hardware/network/50-ryoku-dns.rules @@ -1,13 +1,14 @@ // Network panel DNS switch, authorized without a password so the provider -// buttons apply in one click. Scoped to any ryoku-dns helper (basename match, so -// a dev checkout's non-/usr/bin path still authorizes) for an active local user -// in the wheel group. The helper only writes a NetworkManager -// global-dns drop-in (a fixed provider list or validated addresses) and reverts -// to DHCP, the same network control NetworkManager already grants the active -// user, so a passwordless grant is safe. +// buttons apply in one click. Scoped to the exact installed helper path: a +// basename match would let any executable placed at a path ending in +// /ryoku-dns run as root (local privilege escalation). Active local wheel +// users only. The helper writes a NetworkManager global-dns drop-in (a fixed +// provider list or validated addresses) and reverts to DHCP, the same network +// control NetworkManager already grants the active user, so a passwordless +// grant is safe. polkit.addRule(function (action, subject) { if (action.id === "org.freedesktop.policykit.exec" && - /(^|\/)ryoku-dns$/.test(action.lookup("program")) && + action.lookup("program") === "/usr/bin/ryoku-dns" && subject.local && subject.active && subject.isInGroup("wheel")) { return polkit.Result.YES; } diff --git a/system/hardware/power/ryoku-power b/system/hardware/power/ryoku-power index d52e19f7a7..9e778b782a 100755 --- a/system/hardware/power/ryoku-power +++ b/system/hardware/power/ryoku-power @@ -15,13 +15,14 @@ # lets PCIe links enter deeper low-power states at idle. # # Both sysfs paths are root-owned; a write we cannot do directly re-execs the -# whole command once under pkexec, and 47-ryoku-power.rules grants exactly this -# binary to the active wheel user without a password so login autostart's -# `apply` never blocks on a prompt (the values it acts on are range-checked, so -# the grant stays safe). The desired state lives in ~/.config/ryoku/power.json -# (keys `chargeLimit`, `aspm`) so a user edit survives updates; `apply` is the -# idempotent convergent entry point that reads it back. A missing file or -# missing key means "leave the hardware alone", never a forced default. +# whole command once under pkexec, and 47-ryoku-power.rules grants exactly the +# packaged /usr/bin helper to the active wheel user without a password. A dev +# deploy also puts this helper in ~/.local/bin, but that user-writable copy must +# hand privileged work to /usr/bin so the rule still matches and mutable code is +# never run as root. The desired state lives in ~/.config/ryoku/power.json (keys +# `chargeLimit`, `aspm`) so a user edit survives updates; `apply` is the +# idempotent convergent entry point that reads it back. A missing file or missing +# key means "leave the hardware alone", never a forced default. # Laptop-only, like ryoku-idle: `apply` on a desktop does nothing. set -euo pipefail @@ -38,6 +39,7 @@ CONFIG="${RYOKU_POWER_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/ryoku/power.json CPU_DIR="${RYOKU_CPU_DIR:-/sys/devices/system/cpu}" PLATFORM_PROFILE_FILE="${RYOKU_PLATFORM_PROFILE_FILE:-/sys/firmware/acpi/platform_profile}" SHELL_HELPER="${RYOKU_SHELL_BIN-$(command -v ryoku-shell || true)}" +PRIVILEGED_HELPER="/usr/bin/ryoku-power" # pkexec resets HOME to root's and strips our environment, so a pkexec re-run of # `apply` would read /root's power.json instead of the caller's. Resolve the @@ -84,11 +86,11 @@ hw_laptop() { escalated() { [[ -n ${PKEXEC_UID:-} ]]; } # root-owned sysfs write: write directly when the target is already writable (the -# test seams point at a tmp dir we own) or we are already root; otherwise re-exec -# the whole command once under pkexec. After pkexec re-enters as root the direct -# branch below writes, so there is no re-exec loop. 47-ryoku-power.rules grants -# exactly this binary to the active wheel user without a password, so `apply` -# from login autostart never blocks on a prompt. +# test seams point at a tmp dir we own) or we are already root; otherwise run the +# packaged helper under pkexec. A dev deploy's ~/.local/bin copy shadows the +# package on PATH, but polkit deliberately grants only the root-owned /usr/bin +# helper. Always crossing the privilege boundary through that exact path keeps +# the narrow rule effective and never runs a user-writable script as root. priv_write() { local path="$1" value="$2" # Convergent: an already-correct value needs no write, and skipping it skips @@ -103,7 +105,7 @@ priv_write() { log "cannot write $path: not writable and pkexec is unavailable" exit 1 fi - exec pkexec "$(readlink -f "$0")" "${ORIG_ARGV[@]}" + exec pkexec "$PRIVILEGED_HELPER" "${ORIG_ARGV[@]}" } # ── config (~/.config/ryoku/power.json, parsed with jq, a desktop dependency) ── diff --git a/system/hardware/power/ryoku-power-cutover b/system/hardware/power/ryoku-power-cutover index e465a4c48b..bbf623151c 100755 --- a/system/hardware/power/ryoku-power-cutover +++ b/system/hardware/power/ryoku-power-cutover @@ -706,6 +706,22 @@ session_property() { return 3 } +# live, gone (closing or removed) or unknown (login1 could not answer). +session_liveness() { + local state + if state="$(session_property "$1" State)"; then + if [[ $state == active || $state == online ]]; then + echo live + else + echo gone + fi + elif (( $? == 3 )); then + echo gone + else + echo unknown + fi +} + # The DISPLAY of this login1 session's own X server, when there is one. # Xwayland appears with the first X client, so an environment snapshotted # before that moment carries no DISPLAY, and the manager bind must not strip @@ -1288,6 +1304,10 @@ secure_session_direct() { start_session_observer "$sid" return 0 fi + local existing_lock=0 + if [[ -n $(session_lock_pids "$sid") ]]; then + existing_lock=1 + fi path="$(session_environment_path "$sid")" discover_session_environment "$sid" || [[ -r $path ]] || { log "cannot resolve lock environment for login1 session $sid" >&2 @@ -1308,33 +1328,35 @@ secure_session_direct() { username=${USER:-$(id -un)} escaped_sid="$(escape_unit_component "$sid")" unit="ryoku-session-lock-$escaped_sid-$BASHPID.service" - if [[ $init_system == systemd ]]; then - if ! systemd-run --user --quiet --collect --unit="$unit" \ - --property=Type=exec --property=TimeoutStopSec=5s \ - /usr/bin/env -i \ - "HOME=${HOME:-}" "USER=$username" "LOGNAME=${LOGNAME:-$username}" \ - "SHELL=${SHELL:-/bin/bash}" "PATH=${PATH:-/usr/bin}" \ - "LANG=${LANG:-C.UTF-8}" "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp}" \ - "DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR:-/tmp}/bus" \ - QT_QPA_PLATFORM=wayland "RYOKU_QYLOCK_LOCK_SCRIPT=$lock_script" \ - "${assignments[@]}" "$lock_launcher"; then - log "could not start qylock for login1 session $sid" >&2 - return 1 - fi - else - if ! runit_transient_start "$unit" -- \ - /usr/bin/env -i \ - "HOME=${HOME:-}" "USER=$username" "LOGNAME=${LOGNAME:-$username}" \ - "SHELL=${SHELL:-/bin/bash}" "PATH=${PATH:-/usr/bin}" \ - "LANG=${LANG:-C.UTF-8}" "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp}" \ - "DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR:-/tmp}/bus" \ - QT_QPA_PLATFORM=wayland "RYOKU_QYLOCK_LOCK_SCRIPT=$lock_script" \ - "${assignments[@]}" "$lock_launcher"; then - log "could not start qylock for login1 session $sid" >&2 - return 1 + if (( existing_lock == 0 )); then + if [[ $init_system == systemd ]]; then + if ! systemd-run --user --quiet --collect --unit="$unit" \ + --property=Type=exec --property=TimeoutStopSec=5s \ + /usr/bin/env -i \ + "HOME=${HOME:-}" "USER=$username" "LOGNAME=${LOGNAME:-$username}" \ + "SHELL=${SHELL:-/bin/bash}" "PATH=${PATH:-/usr/bin}" \ + "LANG=${LANG:-C.UTF-8}" "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp}" \ + "DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR:-/tmp}/bus" \ + QT_QPA_PLATFORM=wayland "RYOKU_QYLOCK_LOCK_SCRIPT=$lock_script" \ + "${assignments[@]}" "$lock_launcher"; then + log "could not start qylock for login1 session $sid" >&2 + return 1 + fi + else + if ! runit_transient_start "$unit" -- \ + /usr/bin/env -i \ + "HOME=${HOME:-}" "USER=$username" "LOGNAME=${LOGNAME:-$username}" \ + "SHELL=${SHELL:-/bin/bash}" "PATH=${PATH:-/usr/bin}" \ + "LANG=${LANG:-C.UTF-8}" "XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp}" \ + "DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR:-/tmp}/bus" \ + QT_QPA_PLATFORM=wayland "RYOKU_QYLOCK_LOCK_SCRIPT=$lock_script" \ + "${assignments[@]}" "$lock_launcher"; then + log "could not start qylock for login1 session $sid" >&2 + return 1 + fi fi fi - for ((attempt = 0; attempt < 150; attempt++)); do + for ((attempt = 0; attempt < ${RYOKU_LOCK_PROOF_TRIES:-150}; attempt++)); do if "$proof_helper" status "$sid" >/dev/null 2>&1; then retire_proof_backed_unlock_guard "$sid" || return 1 start_session_observer "$sid" @@ -1356,6 +1378,9 @@ secure_session_direct() { rc=$? (( rc == 1 )) || return 1 fi + if (( existing_lock == 0 )); then + user_unit_stop "$unit" >/dev/null 2>&1 || true + fi log "login1 session $sid did not establish compositor-secure qylock" >&2 return 1 } @@ -1372,7 +1397,9 @@ session_lock_pids() { if [[ $cmdline != *ryoku-qylock-lock* && $cmdline != *quickshell*lock_shell.qml* && $cmdline != *quickshell-lockscreen/lock.sh* && - $cmdline != *qylock-next/lockscreen/lock.sh* ]]; then + $cmdline != *qylock-next/lockscreen/lock.sh* && + ( -z ${RYOKU_QYLOCK_LOCK_SCRIPT:-} || + $cmdline != *"$RYOKU_QYLOCK_LOCK_SCRIPT"* ) ]]; then continue fi process_sid="" @@ -1475,6 +1502,10 @@ current_active_user_session() { schedule_session_rebind() { local sid=$1 watcher=$2 unit + case "$(session_liveness "$sid")" in + gone) return 0 ;; + unknown) return 1 ;; + esac unit="ryoku-session-rebind@$sid.service" if user_unit_active "$unit"; then return 0 @@ -2456,9 +2487,17 @@ start_login_session() { (( EUID != 0 )) || { log "session startup must run as the graphical user" >&2; return 1; } local sid="${1:-${XDG_SESSION_ID:-}}" activity [[ $sid =~ ^[A-Za-z0-9_.-]+$ ]] || return 2 + case "$(session_liveness "$sid")" in + gone) return 0 ;; + unknown) return 1 ;; + esac user_guard_sid=$sid exec 8>"${XDG_RUNTIME_DIR:-/tmp}/ryoku-power-cutover.lock" flock 8 + case "$(session_liveness "$sid")" in + gone) return 0 ;; + unknown) return 1 ;; + esac # Any unexpected failure (errexit, a killed unit) must not strand the # durable sleep block: a leaked guard denies every later suspend with # logind's "Operation denied due to active block inhibitor" (#282, #285). diff --git a/tests/bluetooth-power-persist.sh b/tests/bluetooth-power-persist.sh index 2abfcf6032..13458311e2 100755 --- a/tests/bluetooth-power-persist.sh +++ b/tests/bluetooth-power-persist.sh @@ -9,6 +9,8 @@ trap 'rm -rf "$WORK"' EXIT mkdir -p "$WORK/bin" "$WORK/systemd" LOG="$WORK/commands" LIVE="$WORK/live" +BLOCKED="$WORK/blocked" +POLLS="$WORK/polls" STATE="$WORK/state/bluetooth-power" cat >"$WORK/bin/rfkill" <<'EOF' @@ -16,7 +18,11 @@ cat >"$WORK/bin/rfkill" <<'EOF' set -eu printf 'rfkill %s\n' "$*" >>"$RYOKU_TEST_LOG" case ${1:-} in - block) printf 'off\n' >"$RYOKU_TEST_LIVE" ;; + block) printf 'yes\n' >"$RYOKU_TEST_BLOCKED" ;; + unblock) + [[ ${RYOKU_TEST_RFKILL_UNBLOCK_STATUS:-0} == 0 ]] || exit "$RYOKU_TEST_RFKILL_UNBLOCK_STATUS" + printf 'no\n' >"$RYOKU_TEST_BLOCKED" + ;; esac EOF cat >"$WORK/bin/bluetoothctl" <<'EOF' @@ -24,15 +30,27 @@ cat >"$WORK/bin/bluetoothctl" <<'EOF' set -eu printf 'bluetoothctl %s\n' "$*" >>"$RYOKU_TEST_LOG" case ${1:-} in + list) + polls=0 + if IFS= read -r polls <"$RYOKU_TEST_POLLS"; then :; fi + polls=$((polls + 1)) + printf '%s\n' "$polls" >"$RYOKU_TEST_POLLS" + if ((polls >= RYOKU_TEST_APPEAR_AFTER)); then + printf 'Controller 00:11:22:33:44:55 test [default]\n' + fi + ;; power) + if [[ ${2:-} == on ]]; then + polls=0 + if IFS= read -r polls <"$RYOKU_TEST_POLLS"; then :; fi + ((polls >= RYOKU_TEST_APPEAR_AFTER)) || exit 1 + fi printf '%s\n' "$2" >"$RYOKU_TEST_LIVE" ;; show) - if [[ $(cat "$RYOKU_TEST_LIVE" 2>/dev/null || printf off) == on ]]; then - printf 'Powered: yes\n' - else - printf 'Powered: no\n' - fi + state=off + if IFS= read -r state <"$RYOKU_TEST_LIVE"; then :; fi + [[ $state == on ]] && printf 'Powered: yes\n' || printf 'Powered: no\n' ;; esac EOF @@ -56,9 +74,13 @@ exec "$@" EOF chmod +x "$WORK/bin/"* -export PATH="$WORK/bin:$PATH" +export PATH="$WORK/bin:/usr/bin:/bin" export RYOKU_TEST_LOG="$LOG" export RYOKU_TEST_LIVE="$LIVE" +export RYOKU_TEST_BLOCKED="$BLOCKED" +export RYOKU_TEST_POLLS="$POLLS" +export RYOKU_TEST_APPEAR_AFTER=1 +export RYOKU_TEST_RFKILL_UNBLOCK_STATUS=0 export RYOKU_BLUETOOTH_STATE_FILE="$STATE" export RYOKU_BLUETOOTH_POWER_BIN="$POWER" @@ -70,29 +92,72 @@ assert_eq() { fi } +reset_commands() { + : >"$LOG" + printf '0\n' >"$POLLS" +} + # No saved choice is a fresh install, which deliberately defaults to on. printf 'off\n' >"$LIVE" +printf 'no\n' >"$BLOCKED" +reset_commands "$POWER" restore -assert_eq on "$(cat "$LIVE")" 'fresh install default' +assert_eq on "$(<"$LIVE")" 'fresh install default' +assert_eq $'rfkill unblock bluetooth\nbluetoothctl list\nbluetoothctl power on' \ + "$(<"$LOG")" 'fresh install command sequence' -# Turning Bluetooth off records the choice and soft-blocks the radio. +# An unusable rfkill device must not prevent BlueZ power-on when the controller +# is already available. +export RYOKU_TEST_RFKILL_UNBLOCK_STATUS=1 +printf 'no\n' >"$BLOCKED" +reset_commands +"$POWER" on +assert_eq on "$(<"$LIVE")" 'power on despite failed unblock' +assert_eq $'rfkill unblock bluetooth\nbluetoothctl list\nbluetoothctl power on' \ + "$(<"$LOG")" 'failed unblock command sequence' +export RYOKU_TEST_RFKILL_UNBLOCK_STATUS=0 + +# Turning Bluetooth off records the choice through BlueZ without applying a +# persistent kernel soft block. +reset_commands "$POWER" off -assert_eq off "$(cat "$STATE")" 'saved off choice' -assert_eq off "$(cat "$LIVE")" 'live off choice' +assert_eq off "$(<"$STATE")" 'saved off choice' +assert_eq off "$(<"$LIVE")" 'live off choice' +assert_eq 'bluetoothctl power off' "$(<"$LOG")" 'off command sequence' # Simulate systemd restarting bluetoothd with AutoEnable=true. Restore must win. +reset_commands "$RESET" -assert_eq off "$(cat "$LIVE")" 'off after systemd login reset' +assert_eq off "$(<"$LIVE")" 'off after systemd login reset' +assert_eq $'systemctl restart bluetooth.service\nbluetoothctl power off' \ + "$(<"$LOG")" 'systemd reset command sequence' # Void has no systemd-rfkill boot restore, so its login reset applies the same # per-user state immediately after restarting bluetoothd through runit. +reset_commands RYOKU_SYSTEMD_RUNTIME="$WORK/no-systemd" "$RESET" -assert_eq off "$(cat "$LIVE")" 'off after runit login reset' +assert_eq off "$(<"$LIVE")" 'off after runit login reset' +assert_eq $'sv restart bluetoothd\nbluetoothctl power off' \ + "$(<"$LOG")" 'runit reset command sequence' -# An explicit on choice survives the same reset and remains the default action. +# Power-on waits for a controller that re-enumerates after rfkill is cleared. +export RYOKU_TEST_APPEAR_AFTER=3 +printf 'yes\n' >"$BLOCKED" +reset_commands "$POWER" on -assert_eq on "$(cat "$STATE")" 'saved on choice' -"$RESET" -assert_eq on "$(cat "$LIVE")" 'on after systemd login reset' +assert_eq on "$(<"$STATE")" 'saved on choice' +assert_eq on "$(<"$LIVE")" 'live on choice' +assert_eq $'rfkill unblock bluetooth\nbluetoothctl list\nbluetoothctl list\nbluetoothctl list\nbluetoothctl power on' \ + "$(<"$LOG")" 'delayed controller command sequence' + +# A saved on choice from an older build recovers a machine whose Bluetooth +# switches are still soft-blocked. +printf 'yes\n' >"$BLOCKED" +printf 'off\n' >"$LIVE" +reset_commands +"$POWER" restore +assert_eq on "$(<"$LIVE")" 'restore recovers legacy soft block' +assert_eq $'rfkill unblock bluetooth\nbluetoothctl list\nbluetoothctl list\nbluetoothctl list\nbluetoothctl power on' \ + "$(<"$LOG")" 'legacy restore command sequence' printf 'bluetooth power persistence checks passed\n' diff --git a/tests/game-mode.sh b/tests/game-mode.sh index 429395e45a..c9a6cee360 100755 --- a/tests/game-mode.sh +++ b/tests/game-mode.sh @@ -38,7 +38,7 @@ logger pkexec logger ryoku-wifi-powersave logger ryoku-game-tune -printf '#!/usr/bin/env bash\nexit 0\n' >"$bin/notify-send" +printf '#!/usr/bin/env bash\necho "notify-send $*" >> "%s"\nexit 0\n' "$calls" >"$bin/notify-send" # ryoku-shell stub with a real backing value. It exposes the same state and set # calls as the daemon, so a wrong restore is visible rather than merely logged. @@ -186,4 +186,45 @@ RYOKU_BIN="$tmp/ryoku-nolive" "$gm" stop [[ ! -e $deco ]] || fail "compositor without live config evaluation was changed" on && fail "stop did not clear the request without a compositor strip" +# A failed provider action must not advertise Game Mode as on or tune hardware. +cat >"$tmp/ryoku-broken" <<'EOF' +#!/usr/bin/env bash +case "$*" in + "wm status") echo "Capabilities: configReload, liveConfigEval" ;; + "wm act decoration.gameMode on") echo "eval rejected" >&2; exit 7 ;; +esac +EOF +chmod +x "$tmp/ryoku-broken" +: >"$calls" +RYOKU_BIN="$tmp/ryoku-broken" "$gm" start >"$tmp/failure.log" 2>&1 && + fail "start succeeded despite a rejected decoration strip" +on && fail "a rejected decoration strip left Game Mode on" +grep -qF 'eval rejected' "$tmp/failure.log" || fail "strip error was hidden" +grep -qF 'ryoku-game-tune apply' "$calls" && + fail "tuned hardware after the decoration strip failed" + +# A failed compositor reload cannot strand the power, tune or Wi-Fi restores. +cat >"$tmp/ryoku-restore-fails" <&2; exit 7 ;; +esac +EOF +chmod +x "$tmp/ryoku-restore-fails" +echo balanced >"$profile" +RYOKU_BIN="$tmp/ryoku-restore-fails" "$gm" start +[[ "$(cat "$profile")" == performance ]] || fail "failed-reload setup did not boost power" +: >"$calls" +RYOKU_BIN="$tmp/ryoku-restore-fails" "$gm" stop >"$tmp/restore-failure.log" 2>&1 && + fail "stop reported success despite failed compositor restore" +on && fail "failed compositor restore left Game Mode on" +grep -qF 'reload rejected' "$tmp/restore-failure.log" || fail "restore error was hidden" +grep -qF 'ryoku-game-tune restore' "$calls" || fail "failed reload skipped tune restore" +grep -qF 'ryoku-shell powerprofiles set balanced' "$calls" || fail "failed reload skipped profile restore" +grep -qF 'ryoku-wifi-powersave on' "$calls" || fail "failed reload skipped Wi-Fi restore" +grep -qF 'Off - desktop restored' "$calls" && + fail "failed reload announced successful desktop restoration" + echo "game-mode: all checks passed" diff --git a/tests/lock-date-rollover.qml b/tests/lock-date-rollover.qml index ff480a0b8c..3988b32e2f 100644 --- a/tests/lock-date-rollover.qml +++ b/tests/lock-date-rollover.qml @@ -20,6 +20,14 @@ ShellRoot { for (var child of item.children || []) if (hasText(child, value)) return true; return false; } + function findNamed(item, name) { + if (item.objectName === name) return item; + for (var child of item.children || []) { + var match = findNamed(child, name); + if (match) return match; + } + return null; + } function day(d) { return Qt.formatDate(d, "dddd").toUpperCase(); } Timer { interval: 250; running: true; repeat: true @@ -29,8 +37,16 @@ ShellRoot { if (theme.item.curDate === undefined) { console.error("FAIL: theme has no reactive curDate property"); Qt.quit(); return; } - if (theme.item.softwareCursorEnabled === undefined || theme.item.softwareCursorEnabled) { - console.error("FAIL: in-session lock enabled the greeter software cursor"); Qt.quit(); return; + if (theme.item.softwareCursorEnabled !== undefined) { + console.error("FAIL: theme still owns the greeter host cursor"); Qt.quit(); return; + } + var pill = root.findNamed(theme.item, "indicatorPill"); + var minuteRing = root.findNamed(theme.item, "minuteRing"); + var secondRing = root.findNamed(theme.item, "secondRing"); + if (!pill || !minuteRing || !secondRing + || minuteRing.parent !== pill.parent || secondRing.parent !== pill.parent + || minuteRing.z <= pill.z || secondRing.z <= pill.z) { + console.error("FAIL: clock rings must pass in front of the password pill"); Qt.quit(); return; } root.secStart = theme.item.smoothSecAngle; root.secTarget = root.secStart - 30; diff --git a/tests/lock-time-format.qml b/tests/lock-time-format.qml index bfbb1f174e..2a6960e8ef 100644 --- a/tests/lock-time-format.qml +++ b/tests/lock-time-format.qml @@ -24,8 +24,9 @@ ShellRoot { console.error("FAIL: clock pattern classification"); Qt.quit(); return; } Inir.Config.setNestedValue("time.format", "h:mm AP"); - if (Inir.Config.pending["general.clock_format_24_h"] !== false) { - console.error("FAIL: Shima 12-hour setting did not route to the global clock"); Qt.quit(); return; + if (Inir.Config.pending["general.clock_format_24_h"] !== false + || Inir.Config.pending["inir.time.format"] !== undefined) { + console.error("FAIL: Shima 12-hour setting did not route exclusively to the global clock"); Qt.quit(); return; } root.writeFormat("en_US", false); root.phase = 0; @@ -58,8 +59,9 @@ ShellRoot { console.error("FAIL: Spanish 12-hour theme"); Qt.quit(); return; } Inir.Config.setNestedValue("time.format", "HH:mm"); - if (Inir.Config.pending["general.clock_format_24_h"] !== true) { - console.error("FAIL: Shima 24-hour setting did not route to the global clock"); Qt.quit(); return; + if (Inir.Config.pending["general.clock_format_24_h"] !== true + || Inir.Config.pending["inir.time.format"] !== undefined) { + console.error("FAIL: Shima 24-hour setting did not route exclusively to the global clock"); Qt.quit(); return; } root.writeFormat("en_US", true); root.phase++; } else if (root.phase === 2 && Clock.TimeFormat.formatLocale === "en_US" && theme.item.clock24h) { diff --git a/tests/power-cutover.sh b/tests/power-cutover.sh index 6d002d3b33..4430281343 100755 --- a/tests/power-cutover.sh +++ b/tests/power-cutover.sh @@ -589,7 +589,7 @@ if [[ $name == systemd-run ]]; then printf 'systemd-run session-lock %s\n' "$sid" >>"$CUTOVER_LOG" if [[ ${DISAPPEAR_SESSION:-} == "$sid" ]]; then : >"$CUTOVER_STATE/gone-$sid" - else + elif [[ ${NO_LOCK_PROOF:-0} != 1 ]]; then : >"$CUTOVER_STATE/proof-$sid" fi [[ $* != *'/ryoku/qylock-next/lockscreen/lock.sh'* ]] || @@ -648,7 +648,8 @@ if [[ $name == systemd-run ]]; then fi if [[ ${REQUIRE_USER_GUARD:-0} == 1 && ! -e $CUTOVER_STATE/user-guard && - ! -e $CUTOVER_STATE/runit-inhibitor ]]; then + ! -e $CUTOVER_STATE/runit-inhibitor && + "$name ${1:-}" != "loginctl show-session" ]]; then exit 88 fi printf '%s %s\n' "$name" "$*" >>"$CUTOVER_LOG" @@ -709,7 +710,8 @@ case "$name:${1:-}" in fi ;; loginctl:show-session) - if [[ -e $CUTOVER_STATE/gone-${2:-} ]]; then + if [[ -e $CUTOVER_STATE/gone-${2:-} || + ${DEAD_SESSION:-} == "${2:-}" ]]; then exit 1 fi if [[ ${EXTRA_SESSION:-0} == 1 ]]; then @@ -781,6 +783,13 @@ EOF cat >"$tmp/qylock-proof" <<'EOF' #!/usr/bin/env bash set -euo pipefail +if [[ ${PROOF_AFTER_RETRY:-0} == 1 && ${1:-} == status ]]; then + if [[ -e $CUTOVER_STATE/retry-probed ]]; then + : >"$CUTOVER_STATE/proof-${2:-}" + else + : >"$CUTOVER_STATE/retry-probed" + fi +fi [[ ${1:-} == status && -e $CUTOVER_STATE/proof-${2:-} ]] EOF cat >"$tmp/bin/ryoku-wm-testwm" <<'EOF' @@ -971,9 +980,53 @@ grep -qxF \ fail "vanished pre-observer session did not stop its queued lock clients" [[ ! -e $tmp/runtime-user/ryoku-session.8.environment ]] \ || fail "vanished pre-observer session kept stale compositor environment" + +# A removed session must finish its rebind unit successfully, without ever +# acquiring an inhibitor or scheduling another lock. +: >"$tmp/calls" +DEAD_SESSION=8 CUTOVER_LOG="$tmp/calls" CUTOVER_STATE="$tmp/state" \ + XDG_RUNTIME_DIR="$tmp/runtime-user" PATH="$tmp/bin:$PATH" \ + bash -c 'source "$1"; start_login_session 8 && schedule_session_rebind 8 "$1"' _ "$helper" || + fail "removed session did not finish its startup and rebind cleanly" +if grep -Ev '^loginctl show-session 8( |$)' "$tmp/calls" | grep -q .; then + fail "removed session scheduled a new rebind or guard" +fi + +# The second direct attempt waits for the existing wrapper's proof rather than +# creating a second transient client for the same live session. rm -f "$tmp/state/gone-8" printf 'XDG_SESSION_ID=8\0XDG_SESSION_TYPE=wayland\0WAYLAND_DISPLAY=wayland-eight\0' \ >"$tmp/runtime-user/ryoku-session.8.environment" +mkdir -p "$tmp/lock-proc/8888" +printf 'Uid:\t%s\t%s\t%s\t%s\n' "$(id -u)" "$(id -u)" "$(id -u)" "$(id -u)" \ + >"$tmp/lock-proc/8888/status" +printf 'bash\0/home/user/.local/share/quickshell-lockscreen/lock.sh\0' \ + >"$tmp/lock-proc/8888/cmdline" +printf 'XDG_SESSION_ID=8\0' >"$tmp/lock-proc/8888/environ" +: >"$tmp/calls" +rm -f "$tmp/state/proof-8" "$tmp/state/retry-probed" +PROOF_AFTER_RETRY=1 EXTRA_SESSION=1 CUTOVER_LOG="$tmp/calls" CUTOVER_STATE="$tmp/state" \ + RYOKU_PROC_ROOT="$tmp/lock-proc" XDG_RUNTIME_DIR="$tmp/runtime-user" \ + RYOKU_QYLOCK_LOCK_SCRIPT="$tmp/qylock-lock" \ + RYOKU_QYLOCK_LAUNCHER="$tmp/qylock-lock" \ + RYOKU_QYLOCK_PROOF_HELPER="$tmp/qylock-proof" PATH="$tmp/bin:$PATH" \ + bash -c 'source "$1"; secure_session_direct 8' _ "$helper" +if grep -qF 'systemd-run session-lock 8' "$tmp/calls"; then + fail "retry spawned another lock client while the first wrapper was alive" +fi +rm -f "$tmp/state/proof-8" "$tmp/state/retry-probed" +: >"$tmp/calls" +NO_LOCK_PROOF=1 RYOKU_LOCK_PROOF_TRIES=2 EXTRA_SESSION=1 CUTOVER_LOG="$tmp/calls" \ + CUTOVER_STATE="$tmp/state" RYOKU_PROC_ROOT="$tmp/empty-lock-proc" \ + XDG_RUNTIME_DIR="$tmp/runtime-user" \ + RYOKU_QYLOCK_LOCK_SCRIPT="$tmp/qylock-lock" \ + RYOKU_QYLOCK_LAUNCHER="$tmp/qylock-lock" \ + RYOKU_QYLOCK_PROOF_HELPER="$tmp/qylock-proof" PATH="$tmp/bin:$PATH" \ + bash -c 'source "$1"; ! secure_session_direct 8' _ "$helper" +grep -q 'systemctl --user stop ryoku-session-lock-8-' "$tmp/calls" || + fail "failed lock proof left its transient lock client running" +printf 'XDG_SESSION_ID=8\0XDG_SESSION_TYPE=wayland\0WAYLAND_DISPLAY=wayland-eight\0' \ + >"$tmp/runtime-user/ryoku-session.8.environment" : >"$tmp/calls" rm -f "$tmp/state/proof-8" "$tmp/state/proof-9" "$tmp/state/session-observer" diff --git a/tests/power.sh b/tests/power.sh index 112a8e0f10..e09405c680 100755 --- a/tests/power.sh +++ b/tests/power.sh @@ -317,6 +317,54 @@ for c in "$cpuM"/cpu[0-9]*; do [[ "$(cat "$c/cpufreq/scaling_max_freq")" == 4001000 ]] || fail "apply with no profiles block wrote a CPU knob" done +# --- authorization: a PATH-shadowing dev helper escalates only the packaged path +rule="$here/../system/hardware/power/47-ryoku-power.rules" +grep -Fq 'action.lookup("program") === "/usr/bin/ryoku-power"' "$rule" \ + || fail "power rule does not stay pinned to the packaged helper" +grep -Fq 'subject.local && subject.active && subject.isInGroup("wheel")' "$rule" \ + || fail "power rule no longer requires an active local administrator" + +cpuN="$tmp/cpuN"; mk_cpu "$cpuN" 1 +cfgN="$tmp/cfgN/power.json"; mkdir -p "$(dirname "$cfgN")" +printf '{"profiles":{"balanced":{"governor":"performance"}}}\n' >"$cfgN" +chmod 0444 "$cpuN/cpu0/cpufreq/scaling_governor" +stubN="$tmp/stubN"; mkdir -p "$stubN"; pkexec_argv="$tmp/pkexec-argv" +cat >"$stubN/pkexec" <"$pkexec_argv" +EOF +chmod +x "$stubN/pkexec" +install -m755 "$helper" "$stubN/ryoku-power" +install -m755 "$(dirname "$helper")/ryoku-hw-laptop" "$stubN/ryoku-hw-laptop" +PATH="$stubN:$PATH" RYOKU_CPU_DIR="$cpuN" \ + RYOKU_PLATFORM_PROFILE_FILE="$tmp/no-ppN/platform_profile" \ + RYOKU_POWER_CONFIG="$cfgN" RYOKU_POWER_SUPPLY_DIR="$nops" \ + RYOKU_ASPM_POLICY_FILE="$noaspm" RYOKU_SHELL_BIN="$shell_stub" \ + ryoku-power apply-profile balanced \ + || fail "shadow helper escalation did not exit 0" +mapfile -t pkexec_args <"$pkexec_argv" +[[ ${pkexec_args[0]:-} == /usr/bin/ryoku-power ]] \ + || fail "pkexec program did not match the policy path: ${pkexec_args[0]:-missing}" +[[ ${pkexec_args[1]:-}:${pkexec_args[2]:-} == apply-profile:balanced ]] \ + || fail "pkexec did not preserve the profile command" + +# An already-converged profile needs no privileged write at all. +cpuO="$tmp/cpuO"; mk_cpu "$cpuO" 1 +cfgO="$tmp/cfgO/power.json"; mkdir -p "$(dirname "$cfgO")" +printf '{"profiles":{"balanced":{"governor":"powersave","epp":"power"}}}\n' >"$cfgO" +chmod 0444 "$cpuO/cpu0/cpufreq/scaling_governor" \ + "$cpuO/cpu0/cpufreq/energy_performance_preference" +markerO="$tmp/pkexec-converged" +printf '#!/usr/bin/env bash\ntouch "%s"\nexit 0\n' "$markerO" >"$stubN/pkexec" +chmod +x "$stubN/pkexec" +PATH="$stubN:$PATH" RYOKU_CPU_DIR="$cpuO" \ + RYOKU_PLATFORM_PROFILE_FILE="$tmp/no-ppO/platform_profile" \ + RYOKU_POWER_CONFIG="$cfgO" RYOKU_POWER_SUPPLY_DIR="$nops" \ + RYOKU_ASPM_POLICY_FILE="$noaspm" RYOKU_SHELL_BIN="$shell_stub" \ + "$helper" apply-profile balanced \ + || fail "converged apply-profile did not exit 0" +[[ ! -e $markerO ]] || fail "converged apply-profile invoked pkexec" + # --- the real user config was never created or modified ----------------------- now="absent"; [[ -e $realcfg ]] && now="$(cksum <"$realcfg")" [[ "$now" == "$realcfg_state" ]] || fail "the real $realcfg was created or modified" diff --git a/void/packages/repo/build-repo.sh b/void/packages/repo/build-repo.sh index 75c8484ac9..2fe966f8f6 100755 --- a/void/packages/repo/build-repo.sh +++ b/void/packages/repo/build-repo.sh @@ -7,7 +7,7 @@ SRCPKGS=$ROOT/void/packages/srcpkgs WORK=${RYOKU_XBPS_WORK:-${TMPDIR:-/tmp}/ryoku-xbps-$UID} OUT=${RYOKU_XBPS_OUT:-$SCRIPT_DIR/out} ARCH=${RYOKU_XBPS_ARCH:-x86_64} -VOID_PACKAGES_REF=${RYOKU_VOID_PACKAGES_REF:-ffbd8fde74e5f031be2655a9e52afb6669395b64} +VOID_PACKAGES_REF=${RYOKU_VOID_PACKAGES_REF:-7023eba97f43b92c4d36e830677d27ab33e2d4ae} VOID_PACKAGES_URL=${RYOKU_VOID_PACKAGES_URL:-https://github.com/void-linux/void-packages.git} KEY=${RYOKU_XBPS_KEY:-} KEYRING_DIR=${RYOKU_XBPS_KEYRING_DIR:-} diff --git a/void/packages/repo/container-runner.sh b/void/packages/repo/container-runner.sh index 95b1bba9a6..9066766117 100755 --- a/void/packages/repo/container-runner.sh +++ b/void/packages/repo/container-runner.sh @@ -106,7 +106,7 @@ run=("$ENGINE" run --rm --platform linux/amd64 --privileged -v "$HOST_WORK:/work" -v "$HOST_OUT:/out" -v "$source_archive:/run/ryoku-source.tar.gz:ro" - -e "RYOKU_VOID_PACKAGES_REF=${RYOKU_VOID_PACKAGES_REF:-ffbd8fde74e5f031be2655a9e52afb6669395b64}" + -e "RYOKU_VOID_PACKAGES_REF=${RYOKU_VOID_PACKAGES_REF:-7023eba97f43b92c4d36e830677d27ab33e2d4ae}" -e "RYOKU_XBPS_PACKAGES=${RYOKU_XBPS_PACKAGES:-}" -e "RYOKU_XBPS_WORKTREE=${RYOKU_XBPS_WORKTREE:-0}" -e "RYOKU_XBPS_SOURCE_ARCHIVE=/run/ryoku-source.tar.gz" diff --git a/void/packages/translations.tsv b/void/packages/translations.tsv index 0fe6852c33..7b7ff42fd6 100644 --- a/void/packages/translations.tsv +++ b/void/packages/translations.tsv @@ -79,9 +79,12 @@ hyprgrass - hyprland Hyprland is not packaged on Void. hypridle swayidle desktop hyprland - hyprland Ryoku's Hyprland needs GCC 15 and Void still ships GCC 14. hyprland-preview-share-picker - hyprland Hyprland is not packaged on Void. +hyprlang - hyprland Hyprland is not packaged on Void. hyprpicker hyprpicker desktop hyprpolkitagent - hyprland Hyprland is not packaged on Void. hyprsunset - hyprland Hyprland is not packaged on Void. +hyprtoolkit - hyprland Hyprland is not packaged on Void. +hyprutils - hyprland Hyprland is not packaged on Void. imagemagick ImageMagick desktop imgborders - hyprland Hyprland is not packaged on Void. inetutils inetutils optional @@ -117,6 +120,7 @@ libnotify libnotify desktop libpulse libpulseaudio pulseaudio-utils desktop libqalculate libqalculate desktop librsvg librsvg desktop +libstdc++ - hyprland Hyprland is not packaged on Void. libusb libusb optional libva libva desktop libvirt libvirt optional @@ -223,6 +227,7 @@ ryoku-shell @repo desktop Ryoku-owned package built from the checkout. ryomanager @repo optional Ryoku-owned package built from the checkout. ryotunes ryotunes optional repo=ryoku scrcpy scrcpy optional +sdbus-cpp - hyprland Hyprland is not packaged on Void. sddm sddm desktop shaderc shaderc desktop skwd-paper-bin skwd-paper-bin desktop repo=ryoku @@ -254,6 +259,7 @@ ttf-readex-pro ttf-readex-pro desktop repo=ryoku ttf-rubik-vf ttf-rubik-vf desktop repo=ryoku udev eudev desktop upower upower desktop +util-linux-libs - hyprland Hyprland is not packaged on Void. uv uv desktop v4l2loopback-dkms v4l2loopback optional vdirsyncer vdirsyncer desktop @@ -279,6 +285,7 @@ wlsunset wlsunset desktop wpa_supplicant wpa_supplicant desktop wtype wtype desktop xcb-util-cursor xcb-util-cursor desktop +xdg-desktop-portal - hyprland Hyprland is not packaged on Void. xdg-desktop-portal-gnome xdg-desktop-portal-gnome desktop xdg-desktop-portal-gtk xdg-desktop-portal-gtk desktop xdg-desktop-portal-hyprland - hyprland Hyprland is not packaged on Void.