diff options
| author | Crucible <crucible@localhost> | 2026-10-15 23:06:26 -0500 |
|---|---|---|
| committer | Crucible <crucible@localhost> | 2026-10-15 23:06:26 -0500 |
| commit | 7fa0d2cfbafe2be6de7e11e911e806429a60bcf9 (patch) | |
| tree | 96e786516fcdb8b1656afdbd37958ca551146f76 | |
| parent | 29235f1db35cf8b131ce6ae5696ce6f5e2195683 (diff) | |
| download | srcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.tar.gz srcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.tar.xz srcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.zip | |
Publish srcux candidate 0000000698-20261015-225521
34 files changed, 1121 insertions, 6464 deletions
diff --git a/app-admin/scout/scout-0.4.0-r1.ebuild b/app-admin/scout/scout-0.4.0-r1.ebuild index f2ce45b69e0a..4593c4008b12 100644 --- a/app-admin/scout/scout-0.4.0-r1.ebuild +++ b/app-admin/scout/scout-0.4.0-r1.ebuild @@ -10,7 +10,7 @@ HOMEPAGE="https://srcux.com/" SRC_URI="https://www.srcux.com/distfiles/${P}.tar.gz" LICENSE="GPL-2" SLOT="0" -KEYWORDS="~amd64 ~arm64" +KEYWORDS="amd64 ~arm64" REQUIRED_USE="${PYTHON_REQUIRED_USE}" RDEPEND="${PYTHON_DEPS} app-admin/eselect diff --git a/eclass/gclient-r1.eclass b/eclass/gclient-r1.eclass new file mode 100644 index 000000000000..ba1d07c61f3a --- /dev/null +++ b/eclass/gclient-r1.eclass @@ -0,0 +1,582 @@ +# Copyright 1999-2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +# @ECLASS: gclient-r1.eclass +# @MAINTAINER: +# PF4Public <PF4Public@users.noreply.github.com> +# @SUPPORTED_EAPIS: 8 +# @BLURB: Eclass for fetching and unpacking gclient workspaces. +# @DESCRIPTION: +# Eclass to ease the maintenance of ebuilds which manage their sources +# with gclient (from dev-util/depot-tools), the dependency manager used +# by projects such as Chromium. +# +# The behaviour mirrors git-r3.eclass: the git mirror cache that gclient +# maintains (a bare mirror for every repository in the dependency tree, +# which the workspaces are checked out from via shared clones) is stored +# in a persistent store directory, by default ${DISTDIR}/gclient-src. +# The store survives across builds, so subsequent syncs of the same (or +# a similar) tree only fetch the delta. The workspace itself (the +# checked-out sources) lives in ${WORKDIR} and is cleaned up with it. +# +# The ebuild sets the following variables. GC_SOLUTION_URI must be +# set before inheriting the eclass, which it also activates; without +# it the eclass stays dormant, which allows inheriting it +# unconditionally. GC_SOLUTION_NAME and GC_CHECKOUT_DIR are baked +# into GC_DIR/GC_SOLUTION_DIR at inherit time; the remaining variables +# are only read at unpack time, so they may be set at any point +# before src_unpack: +# +# - GC_SOLUTION_URI: URL of the main (solution) repository, +# e.g. https://chromium.googlesource.com/chromium/src, +# - GC_SOLUTION_NAME: name of the solution's checkout directory within +# the workspace. Defaults to the last component of GC_SOLUTION_URI +# (a trailing .git removed). Note that gclient resolves the paths of +# the DEPS dependencies relative to the workspace root, not to the +# solution directory: chromium's DEPS file prefixes all of its keys +# with the solution path ('src/...'), so the solution must be named +# 'src' (the default) for them to check out inside it, +# - GC_DEPS_FILE: name of the DEPS file in the main repository. +# Defaults to DEPS, +# - GC_REVISION: the git ref (commit, tag or branch) to sync the main +# solution to. When unset, the default branch (HEAD) is tracked; a +# commit hash or tag pins the solution, +# - GC_CLIENT_FILE: path to a ready-made .gclient file to use instead +# of the one generated from the variables above. Needed for projects +# with multiple solutions or extra .gclient keys (e.g. target_os). +# Note that the file's own cache_dir key, if present, takes +# precedence over GCLIENT_STORE_DIR, +# - GC_DEPS_ACTIONS: run the DEPS hooks during the sync (skipped by +# default), +# - GC_CUSTOM_VARS: custom variables for the generated .gclient file, +# a space-separated list of key=value pairs whose values are written +# verbatim (Python literals). They can gate entries of the DEPS file, +# e.g. to skip deps that the build does not need, +# - GC_CUSTOM_DEPS: per-dep overrides for the generated .gclient file; +# the value None omits the dep from the sync, i.e. it is neither +# checked out nor downloaded, a string overrides its URL, +# - GC_EXTRA_FLAGS: extra flags for 'gclient sync', e.g. a repeated +# --ignore-dep-type to skip an entire dep type. +# +# The build dependencies are added to BDEPEND via the function +# gclient_gen_dep(), guarded by the flag that selects the gclient +# unpack (the flag cannot be used in the dependency itself, as +# dependencies are evaluated before USE is resolved). +# +# The eclass then: +# +# - creates the .gclient configuration in the workspace pointing the +# git mirror cache at GCLIENT_STORE_DIR, +# - runs 'gclient sync' during src_unpack (via EXPORT_FUNCTIONS), +# checking out the solution and all of its dependencies to +# ${GC_SOLUTION_DIR}, +# - exports GC_DIR (the workspace), GC_SOLUTION_DIR and GC_VERSION +# (the commit the solution was synced to). +# +# DEPS hooks are skipped by default, as they frequently assume a +# developer environment; set GC_DEPS_ACTIONS to run them. +# +# Note that 'gclient sync' runs during src_unpack and needs network +# access. Under FEATURES=network-sandbox, portage only grants network +# in src_unpack to live ebuilds (which is how git-r3 gets it). The +# eclass does not set the live property itself, as it cannot know +# which ebuild mode fetches; the ebuild must add it, e.g. +# PROPERTIES+=" live" for a live ebuild, or conditionally for a +# dual-mode one, e.g. PROPERTIES+=" gclient? ( live )". +# +# Example: +# @CODE +# GC_SOLUTION_URI="https://chromium.googlesource.com/chromium/src" +# GC_REVISION="abcdef1234..." +# +# inherit gclient-r1 +# +# BDEPEND=" +# gclient? ( $(gclient_gen_dep) ) +# " +# +# S="${GC_SOLUTION_DIR}" +# +# src_prepare() { +# # the sources are already checked out in ${S} +# ... +# } +# @CODE + +case ${EAPI} in + 8) ;; + *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; +esac + +if [[ -z ${_GCLIENT_R1_ECLASS} ]]; then +_GCLIENT_R1_ECLASS=1 + +# @ECLASS_VARIABLE: GCLIENT_STORE_DIR +# @USER_VARIABLE +# @DEFAULT_UNSET +# @DESCRIPTION: +# Storage directory for the gclient git mirror cache. +# +# This is intended to be set by the user in make.conf. Ebuilds must not +# set it. +# +# GCLIENT_STORE_DIR=${DISTDIR}/gclient-src + +# @ECLASS_VARIABLE: GC_SOLUTION_URI +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# URL of the main (solution) repository to sync, e.g. +# https://chromium.googlesource.com/chromium/src. +# +# The eclass only activates when this variable is set; otherwise it +# stays dormant, so ebuilds may inherit it unconditionally. + +# @ECLASS_VARIABLE: GC_SOLUTION_NAME +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Name of the solution's checkout directory within the workspace. +# Defaults to the last component of GC_SOLUTION_URI (a trailing .git +# removed). +# +# gclient resolves the paths of the DEPS dependencies relative to the +# workspace root, not to the solution directory. Chromium's DEPS file +# prefixes all of its keys with the solution path ('src/...'), so the +# solution must keep the name 'src' (its default) for the dependencies +# to check out inside it. + +# @ECLASS_VARIABLE: GC_DEPS_FILE +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Name of the DEPS file in the main repository. Defaults to DEPS. + +# @ECLASS_VARIABLE: GC_REVISION +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# The git ref (commit, tag or branch) to sync the main solution to. +# When unset, the default branch (HEAD) is tracked. +# +# For a multi-solution workspace (see GC_CLIENT_FILE), use the +# name@ref form to disambiguate. + +# @ECLASS_VARIABLE: GC_CLIENT_FILE +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Path to a ready-made .gclient file to copy into the workspace +# instead of generating one from GC_SOLUTION_URI and friends. + +# @ECLASS_VARIABLE: GC_DEPS_ACTIONS +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# If set, the actions defined in the DEPS file (its pre- and +# post-update hooks) are run during the sync. They are skipped by +# default, as they frequently assume a developer environment. + +# @ECLASS_VARIABLE: GC_JOBS +# @USER_VARIABLE +# @DEFAULT_UNSET +# @DESCRIPTION: +# Number of parallel SCM jobs for 'gclient sync' (its -j flag). +# Defaults to a value gclient picks based on the number of CPUs. + +# @ECLASS_VARIABLE: GC_VERBOSE +# @USER_VARIABLE +# @DEFAULT_UNSET +# @DESCRIPTION: +# Number of -v flags passed to 'gclient sync'. Without it, gclient +# buffers each repository's output and only shows a "still working +# on" summary. The first -v streams that output as the sync happens: +# per-repository fetch progress with start/finish timestamps. gclient +# accepts the flag up to three times for even more logging. +# +# Can be set in make.conf, e.g. GC_VERBOSE=1. + +# @ECLASS_VARIABLE: GC_EXTRA_FLAGS +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Extra flags passed to 'gclient sync'. A repeated --ignore-dep-type +# (git, cipd, gcs) skips an entire dep type; ebuilds that provide the +# toolchain themselves can skip chromium's bundled-clang CIPD deps +# that way. Note that --no-history and --shallow are accepted by +# gclient but currently have no effect. + +# @ECLASS_VARIABLE: GC_CUSTOM_VARS +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Custom variables for the generated .gclient file, as a +# space-separated list of key=value pairs. Each value is written +# verbatim into the file, i.e. it must be a valid Python literal: +# False or True for booleans, 42 for a number, 'quoted' for a string. +# +# custom_vars can gate entries of the DEPS file, which ebuilds can use +# to skip deps the build does not need. E.g. chromium's DEPS downloads +# per-architecture build sysroots for the checkout architectures; an +# ebuild that builds with use_sysroot=false can skip the host one: +# +# @CODE +# GC_CUSTOM_VARS="checkout_x64=False" +# @CODE + +# @ECLASS_VARIABLE: GC_CUSTOM_DEPS +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Custom dependency overrides for the generated .gclient file, as a +# space-separated list of name=value pairs. The name is the gclient +# item name: for git deps it is the key of the solution's DEPS file, +# for deps of type gcs it is '<key>:<object_name>', since gclient +# models each object of a gcs dep (e.g. the per-platform tarballs) as +# a separate item. +# +# The value is a Python literal: None omits the item from the sync, +# i.e. it is neither checked out nor downloaded; a string overrides +# the item's URL (it may also point at a local path). +# +# Note that gcs object names are pinned per release; if they change, +# the entry silently stops matching, so update them when bumping +# GC_REVISION. +# +# @CODE +# GC_CUSTOM_DEPS=" +# src/third_party/rust-toolchain:Linux_x64/rust-toolchain-....tar.xz=None +# third_party/some-test-suite=None +# " +# @CODE + +# @ECLASS_VARIABLE: GC_SKIP_DEPS_ACTIONS +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# Names of DEPS actions (hooks) to suppress in the generated .gclient +# file, as a space-separated list. A custom hook entry matching a DEPS +# action by name (and carrying no action) replaces it with nothing, +# i.e. the DEPS action is not run. Use this to disable DEPS actions +# that a custom_var would otherwise activate, e.g. +# rust_force_head_revision=True skips chromium's prebuilt rust +# toolchain deps but activates the 'rust_tot' action, which would +# build the rust toolchain from source: +# +# @CODE +# GC_CUSTOM_VARS="rust_force_head_revision=True" +# GC_SKIP_DEPS_ACTIONS="rust_tot" +# @CODE + +# @ECLASS_VARIABLE: GC_CHECKOUT_DIR +# @PRE_INHERIT +# @DEFAULT_UNSET +# @DESCRIPTION: +# The workspace directory (where the .gclient file lives and the +# solutions are checked out into). +# +# GC_CHECKOUT_DIR=${WORKDIR}/${P} + +# @ECLASS_VARIABLE: GC_DIR +# @OUTPUT_VARIABLE +# @DESCRIPTION: +# The workspace directory. Same as GC_CHECKOUT_DIR after defaults are +# applied. + +# @ECLASS_VARIABLE: GC_SOLUTION_DIR +# @OUTPUT_VARIABLE +# @DESCRIPTION: +# The directory the main solution is checked out to, i.e. +# ${GC_DIR}/${GC_SOLUTION_NAME}. A suitable value for S. + +# @ECLASS_VARIABLE: GC_VERSION +# @OUTPUT_VARIABLE +# @DESCRIPTION: +# The commit the main solution was synced to. Set and exported by +# src_unpack. + +_gclient-r1_die_usage() { + die " +${ECLASS}: Set GC_SOLUTION_URI to the URL of the main repository +before inheriting ${ECLASS}. +" +} + +# @FUNCTION: _gclient-r1_is_commit +# @USAGE: <ref> +# @INTERNAL +# @DESCRIPTION: +# Return 0 if <ref> looks like a git commit hash (7 to 64 +# hexadecimal characters). +_gclient-r1_is_commit() { + local rev=${1} + local stripped=${rev//[0-9a-fA-F]/} + (( ${#rev} >= 7 && ${#rev} <= 64 )) && [[ -z ${stripped} ]] +} + +_gclient-r1_init() { + [[ ${GC_SOLUTION_URI} ]] || _gclient-r1_die_usage + + local uri=${GC_SOLUTION_URI%/} + local tail=${uri##*/} + [[ ${tail} ]] || + die "${ECLASS}: cannot derive GC_SOLUTION_NAME from URI '${GC_SOLUTION_URI}'" + + : "${GC_SOLUTION_NAME:=${tail%.git}}" + : "${GC_DEPS_FILE:=DEPS}" + : "${GC_CHECKOUT_DIR:=${WORKDIR}/${P}}" + + local distdir=${PORTAGE_ACTUAL_DISTDIR:-${DISTDIR}} + : "${GCLIENT_STORE_DIR:=${distdir}/gclient-src}" + + GC_DIR=${GC_CHECKOUT_DIR} + GC_SOLUTION_DIR=${GC_DIR}/${GC_SOLUTION_NAME} + export GC_DIR GC_SOLUTION_DIR GCLIENT_STORE_DIR +} +# Activate the eclass: only ebuilds that set GC_SOLUTION_URI get the +# gclient unpack; others may inherit this eclass without side effects. +if [[ -n ${GC_SOLUTION_URI-} ]]; then + _gclient-r1_init + + # ebuilds that need a different unpack sequence can override this + # by defining their own src_unpack after inheriting the eclass + src_unpack() { + gclient-r1_src_unpack + } + EXPORT_FUNCTIONS src_unpack +fi +unset -f _gclient-r1_init _gclient-r1_die_usage + +# @FUNCTION: gclient_gen_dep +# @USAGE: +# @DESCRIPTION: +# Generate the build dependencies of 'gclient sync' (git and +# dev-util/depot-tools). Ebuilds add the result to their BDEPEND, +# typically guarded by the flag that selects the gclient unpack: +# +# @CODE +# BDEPEND=" +# ... +# gclient? ( $(gclient_gen_dep) ) +# " +# @CODE +gclient_gen_dep() { + printf '%s\n' \ + ">=dev-vcs/git-2.46" \ + "dev-util/depot-tools" +} + +# @FUNCTION: gclient-sync +# @DESCRIPTION: +# Fetch the workspace: write the .gclient configuration into ${GC_DIR} +# (or copy GC_CLIENT_FILE) and run 'gclient sync', populating the git +# mirror cache in ${GCLIENT_STORE_DIR} and checking out the solution +# and all of its dependencies to ${GC_SOLUTION_DIR}. +# +# This is called automatically from src_unpack. Call it manually only +# if the ebuild needs to sync again at a later phase. +gclient-sync() { + debug-print-function ${FUNCNAME} "$@" + + # the mirror cache is the persistent part; create it if necessary + if [[ ! -d ${GCLIENT_STORE_DIR} ]]; then + local saved_umask + if [[ ${EVCS_UMASK} ]]; then + saved_umask=$(umask) + umask "${EVCS_UMASK}" || die "Bad options to umask: ${EVCS_UMASK}" + fi + ( + addwrite / + mkdir -p "${GCLIENT_STORE_DIR}" + ) || die "Unable to create ${GCLIENT_STORE_DIR}" + if [[ ${saved_umask} ]]; then + umask "${saved_umask}" || die + fi + fi + addwrite "${GCLIENT_STORE_DIR}" + # fallback for the cache location, see the .gclient file below + export GIT_CACHE_PATH="${GCLIENT_STORE_DIR}" + EVCS_STORE_DIRS+=( "${GCLIENT_STORE_DIR}" ) + + mkdir -p "${GC_DIR}" || die + + local -a sync_args=( sync --force ) + if [[ ! ${GC_DEPS_ACTIONS} ]]; then + sync_args+=( --nohooks --noprehooks ) + fi + if [[ ${GC_REVISION} ]]; then + sync_args+=( --revision "${GC_REVISION}" ) + fi + if [[ ${GC_JOBS} ]]; then + sync_args+=( --jobs "${GC_JOBS}" ) + fi + # gclient's -v is cumulative (up to -vvv) + local v + if [[ ${GC_VERBOSE} ]]; then + for ((v = 0; v < ${GC_VERBOSE} && v < 3; v++)); do + sync_args+=( -v ) + done + fi + if [[ ${GC_EXTRA_FLAGS} ]]; then + sync_args+=( ${GC_EXTRA_FLAGS} ) + fi + + if [[ ${GC_CLIENT_FILE} ]]; then + cp "${GC_CLIENT_FILE}" "${GC_DIR}/.gclient" || die + # the file's own cache_dir key wins; otherwise git's global + # cache.cachepath takes precedence over GIT_CACHE_PATH + local configured + configured=$(git config --type path cache.cachepath) || true + if [[ ${configured} && ${configured} != ${GCLIENT_STORE_DIR} ]]; then + ewarn "gclient-r1: git cache.cachepath=${configured} takes precedence" + ewarn "over GCLIENT_STORE_DIR=${GCLIENT_STORE_DIR} unless ${GC_CLIENT_FILE}" + ewarn "sets cache_dir itself." + fi + else + # custom_vars are written verbatim, i.e. they are Python + # literals in the generated .gclient file; a None custom_dep + # value omits the item, a string one overrides its URL + local custom_vars="" + local custom_deps="" + local skip_actions="" + local kv + for kv in ${GC_CUSTOM_VARS-}; do + [[ ${kv} == *=* ]] || + die "${ECLASS}: GC_CUSTOM_VARS entry '${kv}' is not key=value" + custom_vars+=" '${kv%%=*}' : ${kv#*=}, +" + done + for kv in ${GC_CUSTOM_DEPS-}; do + [[ ${kv} == *=* ]] || + die "${ECLASS}: GC_CUSTOM_DEPS entry '${kv}' is not name=value" + case ${kv#*=} in + None) + # omit the item from the sync + custom_deps+=" '${kv%%=*}' : None, +" ;; + "") + die "${ECLASS}: GC_CUSTOM_DEPS entry '${kv}' has an empty value; use None to omit the item" ;; + *) + custom_deps+=" '${kv%%=*}' : '${kv#*=}', +" ;; + esac + done + # a custom hook matching a DEPS action by name and carrying no + # action replaces it with nothing, i.e. suppresses it + for kv in ${GC_SKIP_DEPS_ACTIONS-}; do + skip_actions+=" {\"name\" : \"${kv}\"}, +" + done + if [[ -n ${skip_actions} ]]; then + skip_actions=" \"custom_hooks\" : [ +${skip_actions} ], +" + fi + + # cache_dir in the .gclient file takes precedence over git's + # global cache.cachepath and $GIT_CACHE_PATH, so the store + # location is deterministic + cat > "${GC_DIR}/.gclient" <<EOF +solutions = [ + { "name" : '${GC_SOLUTION_NAME}', + "url" : '${GC_SOLUTION_URI}', + "deps_file" : '${GC_DEPS_FILE}', + "custom_deps" : { +${custom_deps} }, + "custom_vars": { +${custom_vars} }, +${skip_actions} }, +] +cache_dir = '${GCLIENT_STORE_DIR}' +EOF + fi + + einfo "gclient sync: ${GC_SOLUTION_NAME} @ ${GC_REVISION:-HEAD}" + einfo " workspace: ${GC_DIR}" + einfo " mirror cache: ${GCLIENT_STORE_DIR}" + + ( + cd "${GC_DIR}" || exit 1 + # metrics are meaningless in a build environment, and with a + # missing config file gclient would print a warning and probe + # the network to check whether it runs on a Google bot + export DEPOT_TOOLS_METRICS=0 + echo "gclient ${sync_args[*]}" >&2 + gclient "${sync_args[@]}" + ) || die "gclient sync failed" +} + +gclient-r1_src_unpack() { + debug-print-function ${FUNCNAME} "$@" + + gclient-sync + + # record the synced commit of the solution + GC_VERSION=$(git -C "${GC_SOLUTION_DIR}" rev-parse HEAD 2>/dev/null) || + die "Unable to determine the synced commit of ${GC_SOLUTION_URI}" + export GC_VERSION + einfo "synced ${GC_SOLUTION_NAME} to commit ${GC_VERSION}" + + # mark the checkout as safe so later phases can access it + # (cf. git-r3, https://bugs.gentoo.org/879353) + local safe_dir + safe_dir=$(cd "${GC_SOLUTION_DIR}" && pwd) || die + if ! git config --global --get-all safe.directory 2>/dev/null | + grep -qx "${safe_dir}" + then + git config --global --add safe.directory "${safe_dir}" || die + fi +} + +# @FUNCTION: gclient-r1_pkg_needrebuild +# @DESCRIPTION: +# Return 0 (rebuild needed) when the tracked branch moved on since the +# last sync. Always returns 1 for a pinned commit. +gclient-r1_pkg_needrebuild() { + debug-print-function ${FUNCNAME} "$@" + + # a pinned commit can never become stale + if _gclient-r1_is_commit "${GC_REVISION-}"; then + return 1 + fi + + local ref=HEAD + if [[ ${GC_REVISION} ]]; then + # an immutable tag is stable + if git ls-remote "${GC_SOLUTION_URI}" "refs/tags/${GC_REVISION}" 2>/dev/null | + grep -q . + then + return 1 + fi + ref="refs/heads/${GC_REVISION}" + fi + + local new_commit_id + new_commit_id=$( + git ls-remote "${GC_SOLUTION_URI}" "${ref}" 2>/dev/null | + awk '{print $1; exit}' + ) + [[ ${new_commit_id} && ${GC_VERSION} ]] || die "Lookup failed" + + if [[ ${GC_VERSION} != ${new_commit_id} ]]; then + einfo "Update from ${GC_VERSION} to ${new_commit_id}" + else + einfo "Local and remote at ${GC_VERSION}" + fi + + [[ ${GC_VERSION} != ${new_commit_id} ]] +} + +# @FUNCTION: pkg_needrebuild +# @DESCRIPTION: +# Rebuild check for live ebuilds: returns 0 when the tracked branch +# moved on since the last sync. See gclient-r1_pkg_needrebuild. +# +# 'export' locally until this gets into EAPI +pkg_needrebuild() { + # dormant ebuilds have nothing to check + [[ -n ${GC_SOLUTION_URI-} ]] || return 1 + gclient-r1_pkg_needrebuild +} + +fi diff --git a/gui-apps/sendit/Manifest b/gui-apps/sendit/Manifest deleted file mode 100644 index c654d4d22f12..000000000000 --- a/gui-apps/sendit/Manifest +++ /dev/null @@ -1,122 +0,0 @@ -DIST anstream-1.0.0.crate 28916 BLAKE2B a9198a66bd3821349f83d7837ba7546a12a74a6f57e179e480bd6bf4a75848a45dac9a19a27c5e2cf01520c7e917d380b21ec16e4485f69bebe112383ba273f2 SHA512 36bf11821e8bc23607a54842b47950782806ebc7a56233e3c458345d79986745f6ba878e39964ed41404eb5be58f78d0bbcdceeb86d523ba40eedd15f7e1f29d -DIST anstyle-1.0.14.crate 17787 BLAKE2B ebb3b3986c06628ebfe71665216de68f123840093013fa16c030c25e22ff0731cdcd7161737635c532d45ad0307a1b7f951859a40dbe1c812b3e905550b33b90 SHA512 2e7bbaf0794d209f0f19fb8709214c1b1109c27e14d91a8adc232a9bdfe76e653b2b86e341d71f63903e034c81f6ed63ffe7f123f6d631c0f8d41b56d70037b9 -DIST anstyle-parse-1.0.0.crate 22042 BLAKE2B 418cb53754b325354a76d75cd73652d2e41408911bcfbadf91f668e7d6942d92ae3c379194f1d38bc6aa26a9ad98c92801213ffddf544fd2ce29a1180179a8c4 SHA512 4394616b0aa7a591eb9b9bb758abd2f067a3fe673b060c02857f6d2210a2276c476f32512aa59aee6c5383e3261f2654058ff619d957bee93d62540e10d81085 -DIST anstyle-query-1.1.5.crate 10264 BLAKE2B e1c1bad89ad4c09e48f9d7c417713953f7f468f10f7f61e351137be801b5986ab3d21406411b0b23c42af4a2b5881b85aa68dfc66d23cafca7dfcb88180104b7 SHA512 3b7b888df0b678bd4560981dd94a51a6af9a535cc1a869bb4577e832492bb6de718a3ac80ecf61f8857b3d0ed7b95e444e85bd00ef65a46c6a244d0b0eadd95d -DIST anstyle-wincon-3.0.11.crate 12638 BLAKE2B 01f1ce85205ed3e573daca145ae420e9f7d78867fc036133be74bbcc9b83231ecbc681b57eb98bec622bcbaaf4bb293d05d3021a29b49a0a71b8ab89b0ae9dfd SHA512 09c3215dc6cef75d6282078e68c5dbd51580b6c8db79a77932d2e74a51d29cb0ef23c31b8a73a71896a45eb5dc5f94aee209bd8f6e5df4c675dabe3cd304a96b -DIST anyhow-1.0.104.crate 48819 BLAKE2B b74402bcb5c566403e7f768a8a240755997ba3cc2093e132b79b7a6ec17ffcce1aad44ad0d96a451d2ae7bdf5cef655762548dd30ed3121f7b6f3e65bb98d461 SHA512 5de699d3fffa67fad1be5cb632de87c8e8ca026e480ace9eece3065e879c7996ddfd5e11b9a0b0ce002a880616e6de1de71ee73b268660cf30525bdf3ea86ea4 -DIST async-channel-2.5.0.crate 18624 BLAKE2B d835b923d9c99100e499d72d34515bbd041bfd087572ba681f0a61c9237f5d2af1d4abbeb9572ada694a3609aa19a14ba1160deb0d6411a542ee73cb6234c5fd SHA512 621e9540972a4a5d1b6cc0fb02a507cf66d704dab5411b60c44c2b65ab21c3f7f7bdf9c093e7498a71ada6573e429a4b426506c96c5fd9e3e297a76930ca2f83 -DIST autocfg-1.5.1.crate 18911 BLAKE2B 0547eaf76e14418f5cc105f1127ea6de3182e8960e808008aa536f5f85849dbfaec334fb8ace1ddfa95521ee289c494576eac7d57729f06385f911d16419872e SHA512 4d7c168dd8e98d74b09a2762dd95c9912b903e9785ecded165098fc58e20ff35c80facd7fe5a6a5523250fc34c1e5fc509ab93c7c68bc3b2334ec0dac3bcb840 -DIST bitflags-2.13.2.crate 51678 BLAKE2B 8bbeb87f475eae13e691f12d3cf34f865dffbbaf44e400f99a406b487b59be9b08b3de07e7a3bfcf2a5d04d3ffd9af23386317e964d062973347279bd738ef25 SHA512 362553b15e7f09d30f9aca4c1f991f040c446aea4ae36f46ec9ea460a0cd2b98052fca4b56d2ef48147cf1c73f3f8782cb579aeadfbfdd2a22b55cd4b096022b -DIST bumpalo-3.20.3.crate 98671 BLAKE2B 0b658a45105ef71b599d028f8ae735bb10e843c534eebfa35d156dcc0099d137bf24179ecb5cd7d334d60f1a735897e746041975c22a6877ce88831fba050e86 SHA512 c0e8ebd1c9eaf0a093370251a8a4993f73ed925a95ec852173649e6716741e38fbe1b1bbe15dd7704722fa67e3113f82a76b3e9edb5691f3b5c68a3a55b0c039 -DIST cairo-rs-0.22.9.crate 57302 BLAKE2B 0c7d801d4d522e24d3f0047c3c4106792c170bb56a2738a00d9ecd1b02d570b90353b2b8403823a736ece050e691f3aac2dfb7fc5b81a2cbfba569060402c83e SHA512 a62a72587c013c26621125eb8de0a68a57e153119aa1a5491d661fe9af55644b69c73b4f8f758eaadb16111d6421a0b0b6b1f2392190c73dc739e7576d96268a -DIST cairo-sys-rs-0.22.9.crate 14533 BLAKE2B 5feffcc2646b728ce1d4a63860b839d1af86ebca7ce87f24326c6985beb53f47ee106baad863c9a6d82fe7bfd16fdfaa9d101df8b26f703974e5cbb1f9876cd7 SHA512 521169462ccbeb205788d1790907e5c80748fff5eab2e4831328b00874a689017e411be43f93d4f49a46f8cc91828d5ce4de32696398806a30499058a6c16e2d -DIST cc-1.5.1.crate 108103 BLAKE2B 6531862889b6d20a7038d767818f86c209be6fe50f6fa200d4032f5450af0110c4298ee68ec50bec1f6b516a1491b79a273f68e972a0edeeb1ec8de54b943930 SHA512 3e498145f1a4f61e118d55ff9017a674b21e9f61f00b030e55d82f56f35433cba22dfc172a9bb68123c0b2e4ef0e3c648e0e202ed9f92e5348ec8ee7b07b1de9 -DIST cfg-expr-0.20.9.crate 45746 BLAKE2B 902a6ac7a689a5622721c6050ebf193a05712c380cab3ef6c966e693b08c0c6530c61c4461351262f6b7bd0cf8db76ee06c74ba49d1a6a6b43b545a1a00c26f7 SHA512 e5d28adaeb89510aaca07d7dfaf9b7450b057e83a4f2f5e8609c9b0a13c92eda70190b704b831b208eabf2a218dca99324d7d2393e6427341f6138dc70903dc0 -DIST cfg-if-1.0.5.crate 9578 BLAKE2B 34d99ee8193178424c3fe35b33990a621d88a5ce94e9166357a3a8b49fc73ad489f56dc52af5a21aa85a416b575db4e93819f3057e44022fcd6bc4fdedc2a8df SHA512 5f93906e4703366f5b064f44c5d834cd402240a7b48cd8f72a40bf198430501f5939507c8ec5235affbdb5004ab6fd13a5680f7716544c6ecc6ae9c19512d75d -DIST clap-4.6.7.crate 65831 BLAKE2B e4ae6a0985c452c2840711eff47611724485533f9ebe687b25f571d23a492d05f41adf520a75e5a2c364a18e3e1a8d3d90dba19409a197842d249a8ba78f17f0 SHA512 461ba233b8d6fcbba51b74a3954bd0c83cf4a29809f8cf73a56927a04b63e973506d42eab19db83ac2ecc4a381c265569665a7d8b81dd6753ecaa544095e6f2f -DIST clap_builder-4.6.7.crate 171709 BLAKE2B e0d4735f1b0d70a3f83af1555e06023a165e6dd0995e72f70d5df8a5bc2fcc2b1b4515c3257c319d983479b44925d3a3fb8253b6d928bf66e7bef99315d6b505 SHA512 b86750b56cae3f23fbece3b56a18b9c17f49b4564ffc0e3cc0d7f0af921a335f5c96f2ccb602032ca692d896d76ed2a47d72a0f10325861e69335ba8cf43eeaa -DIST clap_derive-4.6.7.crate 33942 BLAKE2B 0d6130cc5b7ca72e18e4451d284c7e77f6379f96b78dee1606438471e398e7995fc0b6ca94e9594a9c879430f968b3138c9a4c0b20095c153564e02f19caba45 SHA512 630d7d45654df92930db9e5bd39535f314ee4581e9a1dd7e2a067a6193828d00e7e286a14b269557d67afc9ed8f50361de8a0129673e2c478cfae66a992d9c04 -DIST clap_lex-1.1.1.crate 13572 BLAKE2B 9903f72059f27fc7cfa9cba6165a78f2efcfd413bae1c582364b85aa6f850d94c91bc1c5e200e4e4dd82a6aa1731c8cfb68c0385e95533cc077c0b63486996e5 SHA512 416d1f428e8fd275b6da84c77350b405c584883a96bde775a489782dfa3df2968a9988d528dbf05fa8acbe4cbda929f5a268e48e7c26b5563c63decc83fb114f -DIST colorchoice-1.0.5.crate 8187 BLAKE2B d6415c5b337e2f4f5176003082e6e8c52f55a45ff959376e7a3f30e5aab1ecd3f84f9cc5e7a8a720398c1b58f99a07cf042695adb0b35178931178cc8d77d2e8 SHA512 af3a5e7e837b280cd8f0578d2ba2a04443d4c5a445577f444af9edeef6cdb47de134f77b07ceda76ea6b2c73b9c10e1d7e3e1beabfd22b85869585c35db88bd5 -DIST concurrent-queue-2.5.0.crate 22654 BLAKE2B b490e76804e45d6a7af6bb4ec074fb370d9ec7cd67c7ecfb55d5ed53b9b437c174e678add915303559e902ac81c8751dccb2c5ef0543d5961713372fd2b08d3c SHA512 235704f9161dfac60d2a6d2cf96425e39c097dcde2f005187c1f044704eec66d8303d59352cd414a81a3ed0eed60421054b0070a9b8f69065a79a2519e913e5a -DIST crossbeam-utils-0.8.23.crate 47174 BLAKE2B 1a5ec35d66494cd04c1d35f13c95e5ef34e19597946882fe8baab13eca3da89bb6089c6c11f61d14214f4d1d54a3b6a81277f45460d120b61eff647bb3e436a6 SHA512 b6116e4440aa8e0012a3c67fba141ad76fb820fa56f4a76914da2c531b5c1dcd9bb7d8808f41379c67e2abdbf73268101fd6021d95cbcf9f1aee82e10aa5f18e -DIST equivalent-1.0.2.crate 7419 BLAKE2B 7ce4fb7b911961cd4ccfb48323eea4952110a069789c6bd177a63391c270df861afadd00c07db7b22768f0864f320e429e0200c433284f528336e2f81d071eff SHA512 8e0e2dc070794a85b276e93f9e4a65d3bbb8587b33fda211c34479a0b88504c913d8bef9e84d7996254aeabe1efe4ff1ef6259ff4fe3f9ccb90dd90070b3e4d4 -DIST errno-0.3.14.crate 12002 BLAKE2B 57fd842a717086ffdcfa5102a8c52bf8210672820366d78aadb6f159055917477e6771e9fbda88787278bf3a116bc7253dcb705b601e54038a93a1820c3c287a SHA512 1de95c9806323d63e7cc95111e67b3ba1ee4b9d47b70cec33485194574d6b584d22ff877dfbc244317822721291d537d9509ae5e46f1b45169cb478a78322366 -DIST event-listener-5.4.2.crate 35131 BLAKE2B f371ea2c5e90040f8f46f5a32ad255a3ef92d27408032d2c768f3e7f2a56a8cbd6e1457951e58559892d87d7e0daa5296b2bda9b026a1c5dede2569ba3b62a7e SHA512 d678f550bb06283eb38d8f7b02bed2070a8a0c840ea89c4d4e8c2559f2cc85e7b525298bffbfbfddaf0a49a324b35363c9348730233176f99b5b1d698b4aa63c -DIST event-listener-strategy-0.5.4.crate 16179 BLAKE2B b516f4b14bace1dcc9e7e6ad3c7b8ed1855530d0aecd1ed480b4392bc5e414a92634ca91b73e521e0344e264da2d7bb92ba6dc260cb38d532ac5b782569f7ea6 SHA512 bd5259083e87d6fe04fd0c3ff2b5a5ac3e6550ea05bf623b3c535465a2c1f2766bb66ee73444a80075efaa59a73b064def1f034e91e64c66aff4921eefc54caf -DIST field-offset-0.3.6.crate 10032 BLAKE2B b4b43d35f793c55e8c521de7bc1648501cfe46494fc17d0b6135ba3456c8d72a11b422a16735c681c7c6542084652c119661372628096d61197f6e387b66143a SHA512 3ec99bf5e860dc68f3fcd0abd2b0de2ae0b3174eb177458506aabeee9dc7b0b8b58b925e2c6108a8f18ab95f2e1952dd9474650a639e1c1d50ec3f72bbd30077 -DIST find-msvc-tools-0.1.14.crate 31390 BLAKE2B 4f16341326d5ace165e087b3c24d1400f43693ce9378b9f70ccdf1174d92b588173c700107ece3466090113f145e7fe7b8eaf937ded7bdbfc2d14122c7ab1c3e SHA512 7257d40931c65d7dfe2a1147c0d3d472efdf17d4142c7660f0e4529d1d818562d66ae95d4e233c7e73749f7e842a2c38e4c43fca59db9bca8f1f21056e403ed0 -DIST futures-channel-0.3.34.crate 33165 BLAKE2B a28cb51ce81e45d2fce112afd9711f410ca6679549b069cf42ef1db8b8e00e5b36d2cead4dea40276034ebbb7804367aeec8d24c720534ba07c9e435765d1bab SHA512 9fb24f58565bdf345f3efe1f61764da9e5df44f7949ce53c1de5e4d6d82d272e04e01b6bb3358c947d312a5a808287a18befda357adea2899337e939c26d9a19 -DIST futures-core-0.3.34.crate 14692 BLAKE2B bcec84e12b7979d5e10c8fec533d7eb8a49ba299461d4268ce77925f647bea4722312e59d001f5d5e779b376728f06d89ea5adb531cb672ab3ccdf897ebc0087 SHA512 05ac009da05e0d4cc41866a1ef95f7a3da03d037ac62dddc0df59c24cd3fb2d164982c95062f810982c3ed39e7130560ab58ea815db4235bb69c8809ba8e5566 -DIST futures-executor-0.3.34.crate 18560 BLAKE2B fcefc0926603a54620984efbb36abac31804fcc18eb277fe7ac0da3b44c77f723accf6caa5f077adc15191c6e3d7977ce6c928994f00ab510c3980764e8850ba SHA512 76c89dc491d6bcde3978ea947aa586551dd3e06cf2282156523963f70c48d12d9caba2414981caaa82f6a78842a7e63180fc90c8f39a80afb040524ea7864611 -DIST futures-io-0.3.34.crate 9189 BLAKE2B 41958b790d4cae75969e561726437428132c39df0de904a359f362416af927240ce85505b1abd3c94f097a8a9bc4696efd5c8d992ad4e0e1bdcec9a5a4469802 SHA512 d7dd8c610359a6edc11768d1183f38832047f3cb74eea3ee99a196fe3b58e170932b2526f906faea0af73c3e3c4d5379b1269bab20466bad435c99f7e5c076fa -DIST futures-macro-0.3.34.crate 11926 BLAKE2B c1e9e2866260cc6ea44235f0acbfa517e5cfac9fdd251a656f53d42f0af59e979da0abd7748d26a20354321097b4f63f9787866656e2ae7268e8cddec0e76076 SHA512 c77aad28eb741e0e9d5eb062034d57401297d9eca21bd05c24b267d5c0ca613255410b3d96288fb089b7478649834ab417bf27aec6efc19a477e1e4b51f86f80 -DIST futures-task-0.3.34.crate 11376 BLAKE2B 6365acd677c141312a1211b95726d3834307797ddaed85df01892f0493bc2e28585a07aea7ed742f28592cc0b1906235b65bdf826276020f6731e2705b68e3e3 SHA512 84d31a3391ad47f4c5306f37e09ab32975967253c65af251f8a45daf9363e6dd95437b7ab0ff68160d34adebf2d09f49449d184d627b8f6548ef02c9155c7db3 -DIST futures-util-0.3.34.crate 169782 BLAKE2B b71f64120f7cb304ddf8d6899ac3da21654ad0ef30803979b3c3583e5cb4f6484edbb411a5cb0c8fef33ec595aceee490dfc98ae0d8211faf90b1b57ceac7d91 SHA512 c5e6213da3767bd645cf27d91e5f8ae14040079e11ed30fc9332b404f058491687dbcf0a8fbfb3511ed463139db7879785bed0129e99de1fde6ee10c21aeb2aa -DIST gdk-pixbuf-0.22.0.crate 21521 BLAKE2B 96967697f15b05a3deea76189aefa101da9c36b23a4b06939cd2cf1bbcf28e758f336e7530052930f28a46b44d1fd9cca432a97f3ef7efa4745c003311e1b35f SHA512 2e8fe61819a491cb7d0d8f4a406c6bee860e6a5eea109c5dce6e7c569053c6d6608ba888c0a3a04b7d27c6c026be52203efd0ea0fa38e300834e968ddc7f871c -DIST gdk-pixbuf-sys-0.22.9.crate 13298 BLAKE2B ab1f69ea65e6657a85945c213739c3bd90bdc89d4639ea961191332f72c854348b6931f0623ba9acbf9a03c73d8d6146c16da965a7804c8f6ba3defd1ac5681b SHA512 a67ab123bac86bb862ebbfe4ea93061ed5e9fdcbc524e267ac5259d408a639d1e0930c70233b8a80a52e1aa5ef782edb5b69b6659fc75ec037ff1ba67d07828d -DIST gdk4-0.11.5.crate 104327 BLAKE2B 4fdbe9fa58730225127c5bdb288dcec384f9179bacb9d92cdc54e61385657d84ee2c248fad00dea723000dff38f036a241444d1e5ba68b44b2eb7382f697425a SHA512 4c4f9de21df4fdcda86bac3880f40f13701d68e96cc1cfc892137d22216d7e468dfea44fe74425763a459629160913bc3e450a9b1b9fc92ffc47e97379ea595f -DIST gdk4-sys-0.11.5.crate 77154 BLAKE2B 6d7caa64af50eb3dbdc2c4a5b832b30d284f2d87a04b9942186a431890be5d02065caf00c3a239af7f87d6878b08658b9ccf88cea913339f584cb22698a7ce26 SHA512 67d7762df4f0296e7545732445489cddfdb49d5797c7909ef70bcb96b5ee7bcbd055fb933ff4881165fb926b5f398ac1ad63580456fc06f2b88ca457fbe9d727 -DIST gio-0.22.10.crate 251905 BLAKE2B f6278904e32a474e1c3428dc0b3dea9c51ae1f239b48e2aedb4f389ad6fe7b180040841c7dbcaf5bd932ecc4b8de8761ad026bce4ac54d0b1899194847d83e53 SHA512 4d5fd7009ece5afd6a793c3a5391446d62c1f5ee0c123580497145ddd519e8f3772e7cc74b9dfd58cfefcfb616f7c69c70f3f50c302510e2f02fb1f962d62873 -DIST gio-sys-0.22.9.crate 83608 BLAKE2B b54bd852ca8e5a2ea8bcc2f47d3c6ed58f2c162ed1fbd39116845dc73151424c46aacad811547eead0fc6087567c773192af7f63bb2cee3bc9608ceb98a36c4a SHA512 24919afaf44ddb39beb59eedf2247ef55d84f03c9cb2cfcbe531c3521eedc493912216bc08ea085b8e329696e15afb9ccc29a2d7bcc4cf2958fb3940b3dd6577 -DIST gl-0.14.0.crate 10806 BLAKE2B ed22d9d8b902fa2dc39480d2721f7af91ecd2e6add6f6aa24f674791124bbe53a04e469588cba886d299c90cfd847f7e6e3695e411c168ae8c1154c51de5db35 SHA512 6a868c68a451e0bb9dd9ddac2412c4ae6cb6f262e911a158dbc95764d757955058ef8cc947e97ed97d50acf0bc315f14e5838839cbdc33bdb4f6fea76c82fb52 -DIST gl_generator-0.14.0.crate 22330 BLAKE2B d2e84bf81e7c4e2869c3e23872bd86466ede80d6fdbdadaddafb1ae9ab45672613cfda319b15720c35fc26ab103ba106a997ea6f5d4654bc9db19fb86f20d765 SHA512 98779d525a042b47ac373340c6fd2e019a670a5bce643128ae2cde2021c0bc8b2253064819c2b0ff444e62bfb3679dc70f11f268c660050cc891063ed6b45fd0 -DIST glib-0.22.10.crate 304373 BLAKE2B 923709d2596f8df58c36131159aee8dbf6b7b88bde5fa5e96e707e4df41601f0a92682dc4d82f4519b195967ad0d5a4592b6857abdb555e0c81772917d9e2525 SHA512 d843bea492264efbaa3bf92eb07c03f49ff575f6ae23388feb1df09694ef1960d8517cda831be334fe9fe755f0245bd8376f920da3674480f529902fcb838b5a -DIST glib-macros-0.22.9.crate 71824 BLAKE2B f12d2148804d17d76f1793bb4db4e64305397f20c539f943d511b4d03b8878283b2de2f5555c9c7f54de7a2dd2bea9cc225e96b8f87e21d65df6b8f9799564fb SHA512 2ac442cf94cf8bc1d1cfaa4062d560cacdd64203ec59e5b4bc735d37093afc18b79b952f930f81aaff384e98d79766b9cc51f6ff97b1edd679189b2c228ff170 -DIST glib-sys-0.22.9.crate 66967 BLAKE2B 5104165768634af95ff002bc4ba3426e897d9545c8c32236b9be4153954153add05e0c822ebfdf5c80b34d637f814055fb498a1683fd9cc26f7879488b1c9355 SHA512 471210e54739d6d8da6657a5a52e9b831730500b091fc7259b92ae55ea6be5dd86637bcebfde7a3e9f4e51b4e17135265a634cdae23709e3eb448a3bf44c05a7 -DIST glow-0.16.0.crate 162118 BLAKE2B e154ca8af73f1cddef5060020629d9ebe44c425895de611be1550e83c5f122e207c1691d89d00dc48f4f14ae7b45d47dcfb0b66326a48ed279fba1c44dea1dc1 SHA512 2ea60a3bf64ad63d94fdfbcb420257863e8139019be008c14c140c3ab6fc6fa6caa5136cf5de52e2f632837e1e4ba0949750f9fc994566e56c53cbde20d27b48 -DIST gobject-sys-0.22.9.crate 22055 BLAKE2B 6fca79379fd5252f8ae4a9f8cb5d9f18c4121ad2391b7d9955f2add0e23e77a148ee2681d7ec9ae3be1c6242508f99531d83eafcc5c632a5d6c4226e954cb3e1 SHA512 e0201840e6e2c1c4413fc3e56c6f390d01959443d4dca4f2926c77f9ec70b2cff5b412447ba3e06de6d94316ed2a56dbee16f339fcdbd6da8dc4e76ada28a453 -DIST graphene-rs-0.22.8.crate 21678 BLAKE2B a9a3a5e1b1924a050953ad6db7b4f2bfcff9c8dc8a9fb9127eae130d82c1d28dbc8a74f265fcf96a60ff578746875d7d11f0108937c252e034d6db68ea5e923b SHA512 364bfab40fb883a86652f96acee333627a8f96354dae24e1a689a63edf210ec63bf9acf2128575c37d93d45bad2bc482a6e030e439e81ae88da41b8151ec618f -DIST graphene-sys-0.22.9.crate 14847 BLAKE2B 562edb1b0290c6301d2e9e95717077a004d8c1cd68fb75e8b5a01145e014636e5022a4200ef8a25c3722df90800694d18e6effd19d7ee6422119aaa54325661e SHA512 2662443d30778395fe93e13e2d8202354be73e187ea2104495fc1787df18a3ae1c6d55c0df8b46ba8ed2f110820196f34d26848cb88c221cf43593a61b643dd6 -DIST gsk4-0.11.5.crate 43676 BLAKE2B 46b06ff912d7a0cd120fa7fc291dfc306c944a6bd5278ba0ea19bfedcd4ad21f4e4d4de30623e811348894f23f36bacbe46891ab88f49f46206b4466cff21bd9 SHA512 496ab7802efdc20a23b1ddb88a22001b9f01a29c822a12da2d3f4652e1cba0766ee5ccd93725d46237d66a411e18adbd5695b6462490ea646234c6bf315fa52a -DIST gsk4-sys-0.11.5.crate 21936 BLAKE2B 0ad96cf294c10d2486dde2418a5592cf20d2449e2b10fb331a69cb4989e2987b92c9a119b0dbb39b35f067c6d2652a463d02ff0d96e7031fa158bf71ff3992a1 SHA512 baac1b6a4a631a7813c6007988d267d3711917314bc9679f1319f2c910d835e17c40381de35e337846131e5743c625af6853b72e83f8cb722d65803de66ede9f -DIST gtk4-0.11.5.crate 507368 BLAKE2B 0066423f10128dc494fbd6d3228ba2137923aa56af41af722e1942d3fcd2e1f7293644affbbb9238a2ab4bd9592fe20842663ed27d4c2e9105d5ba29eaf258f3 SHA512 27f29b8fc3fa7596e800b7763cf9a8d138d667d724694f7182547c64c84a2ce7fac3a14612f1c8651bb76061f2b23fd367504f068c6ed186f7682258d5092b51 -DIST gtk4-layer-shell-0.8.1.crate 13584 BLAKE2B f9888ea8a0ad9f75934c89e00eed2540b25b706e8d630418dcf3f43621940a507fb308d712005fe2587ee14dffc1ec01b3bdad822254577f8b12eb11048d5791 SHA512 5b0dadc17f824dac628d02d135cda9d85b0ff147eca7135014ff870df636e16e6b5f75af001f47d58fd85e1a2d439950997d0967040decddb71377546aabe522 -DIST gtk4-layer-shell-sys-0.6.1.crate 9693 BLAKE2B fc7afcc3091122ec1dce47470b0e755f742d6f4ee2a441394e88c5adb22b66441dbc481d80e279129abdf667a212ab462e11760f386a64a7fe3a7cc6e4e0f2a0 SHA512 5699dbfd710c82ec9f69493d7ed1d6dbdc1be9364b853f37ed14e0af880c08959ebd65fb87047eb76a5f110bb16f4bde314254c25913531c11a906f3358597dc -DIST gtk4-macros-0.11.5.crate 22886 BLAKE2B 8eeb7c16a37918251651c620b8bed189bd276b382d297afcda63b68f9bd455e5b9e3449bec72237427ceca0e48525adcf7a38479bc5e9f6bb8cb330dbd024a93 SHA512 2663f4876469c7c3b5b4554af910db5db142f3ffa23d56d61e10d5f9a012a5374b73eba0109e7535caf6060430becfea39a9ab7018f56d3997800c9e9483ae89 -DIST gtk4-sys-0.11.5.crate 119220 BLAKE2B 54055b5c52fc7771aaba55a02acda36b5c83e3482300971a1e3972136d577a313e208ba083827a3fa80f6a4d63d6873dbe6579925d6cc95947d910b828e16b1a SHA512 3d289d580463fca49f5db7b53e63eb26db59edc97abb5af5de4e7aefb6a59cf670837584552a9952183a6dfc61fbf96c8424e0f332ea726df5111f587eee2d67 -DIST hashbrown-0.17.1.crate 155512 BLAKE2B 748d3658bf40d6905b47f695e64b70a0c506b9fb0207e023cd50448d5febe92bcbdf19ad8b9be7588f12dad2aba82eab4c8a7a1815adf0a00e17c39f6188f4c6 SHA512 4b6a5a76ec2f020fe34cfa5f5bf4b518ab7d278ece657804663972b6a5ebd64f775bd75d229b7e202702bed9d5b63ddef4de6e274fafe3129809791c6802526a -DIST heck-0.5.0.crate 11517 BLAKE2B 5365ec43b2239a76b33a174f1a4292ece4147f9d382a68c6c60db78fdc8bad0afb1d51a65bcb25e96675372faa4ea37c318265030b0546ba51942f7c929e1835 SHA512 f044fc9c3d22466629fd8f772ec0555350fd611c0cfadca51d99a3d2f10e155f77c1091916c8a95a6b9b499f366c2e99a5fbf45b010f988bfb9b2501bf9f6a76 -DIST indexmap-2.14.2.crate 103014 BLAKE2B e398c32cd8fa9a4cafaa0fb82f0111bbe4ec83b6413163b1fc716f4c9c446e4c5c53cd0519b5c20353cb8c7c478c970ea3b418ba856aed12f7d41f46bd91f27a SHA512 ecf5a1a132cda2be21cebf1c447e17213972cf6da1d0f73b25969dedd1d3c444d4124e01c0b31ca9a78c906a5ff68ddd04602b25be6db877f23ee6106e3a063e -DIST is_terminal_polyfill-1.70.2.crate 7548 BLAKE2B d84c45a94bc0ede333843ddb5206379f1a7d9df742c7f7940374d6b17b0a9c300e32fed5fe2bc58e6b3bc5d18b5fc58dd4e283c01c2f3b4f10f254b3cf7668c3 SHA512 d62f444e83950bdd441215c0ce681ff774692b35fec2af7181dfa243dd43fe42ff7e10a6e3b1d578712e4130dbfdf471ecdae73a3a584a2856a848d387b6c261 -DIST itoa-1.0.18.crate 15935 BLAKE2B 8e41446056ccd94979f6f5a8fe8c48d6c47abaab99a41715cca31816f859b56e0cbacb998f300b62be8b719a6470f8a087f1439b72f2f6662d1cdd3d552ede5b SHA512 8dbad156e2863c28e2b380901a92414ef00e39f1fcdb06d91a0f3fb73108c2f7f36b699ccd93287808c24c29cca7f1783d48d5ca95f2173502963c0abcb2d09c -DIST js-sys-0.3.106.crate 113772 BLAKE2B f005841ed62bb2885a2c0caf4bfa3ce736d78107e735713c6755b47c45721e33d4170f8c4d37b9c450052d1505e65712df1fea77b9ac8aa016c7504a5558240f SHA512 da648c817fb1a47ba93baa7f013482e606f90a18476a7d1e8daeea4ae0514bae9900da113dd16466374b03e87d2385bd77207bad0fa187ca5a531bb779fa2cf1 -DIST khronos_api-3.1.0.crate 599718 BLAKE2B 945600455c85874c37c7c395162a3e56531b417a1e6ce414386ecf31f8951fb40049604c27d719014b5379028e5da25fb9977c7487433cebcee4ccc49e00c7d5 SHA512 f9ed9e1bac648a82bc608bc7df339d3ef1db868c8ce8bda9598926839b3de9e3c11c2b45886b9a7dfe5ad6900770fd59c690bfe9a87f7c5b479945ffb8dfd292 -DIST libc-0.2.189.crate 851502 BLAKE2B 05155a249c6880ca96a19bbb7569d4ca7530f544504e4ccb48db1ebca0b70d84e99c6b9532adb9dfbb3d4d1187eca22698a6b2ba6ae40bebf5d6eafdae86c71d SHA512 0cf621171ed4e6bbfeb71fb582ebf037d62ab481bc406ca2fb766bd4a8416f220a189325a0d3cb3e3cb39d80e09312516ca6335fad71b329c5fccddabedf4110 -DIST log-0.4.34.crate 53395 BLAKE2B c79adc36efcefc23b0d4419364e4f6e31850afe766081283b97b9b8fe1c99159b275aaea6a06ad4b57bb0ea0f440b72e88aeac8425d316009ade570b8e027baa SHA512 6f51dc492f808262a67b28a1a80055aee15eb9437d0b0e21e0478cb94db358f751a61172f0c303d94fa949a1874e3f25bd7bc29d9724e3d43849a9f3f4b55d6e -DIST memchr-2.8.3.crate 99165 BLAKE2B 46e5fb49dbc9db3ab2f9a3f77d9e45f1a7dfc99f7bf0350a83e9a56599cbbc4fa6e4738407e177304d595b03727c9cc86969bbfe4e7b8ff5efda44a6c8861bc6 SHA512 2965717bb9e0451c8fcb3d81f509898918491a7b72a894bec5a0d48619cf24db4378e28c6f71f22dbb0f47f1e96662cd86adb0cf1ffad81a7efc2a973fbcd62f -DIST memoffset-0.9.1.crate 9032 BLAKE2B 0aab55fe084134bb599c52d77c96400db40949b1013e7037747ada4fcec8dc4a124b6f3755f04b36e057eb2fb4a6bd6f07d6eebcf166f8a71405ef434d802fbf SHA512 3a236c0f481e36973b9f805e454c2efe4dd375e6b4ee406b57145136c70d5fbf4e1183d563ebf3b5fbde7363bbf5f08f0d88e507aae5bda4cc75664ecd0e33aa -DIST nucleo-matcher-0.3.1.crate 50501 BLAKE2B 543d735117ea073ac8519017d56eeac51291a69a424177ae9361d6a9bb57851192b25291351c59c1f5dd4fe512171c414b29e011174533bf0fcfd6c99520f363 SHA512 16d7b5386950bfff1a491c45b1f5da5c840e6405c340704cb8c600766b7c1b77e0229ce11166f8975ab41b74480f68c38b3bd593bc53614b2e1226c78b36e651 -DIST once_cell-1.21.4.crate 35010 BLAKE2B 9117ed1d478e626b7b7be2e3ff8b68a382b051112c7ca4425367bdae86977671f8b5c576131c82fb437740b6d98b72501f718f4172d39781decac2385e8fbd4d SHA512 af67669b0107f44268ba74c355200cb2ed1aab235a6989a8bc54323eb2c9a45677010f8672e7790edadd4c981e939436e0c3a099d33c06dce9c14fd5ede86155 -DIST once_cell_polyfill-1.70.2.crate 7448 BLAKE2B 5a90f8eeff25a555987b84b9800f9e0b2aa94fb4827f76678ef45d344c4baeae5a079a288fd02a570d0f0099e9b2842d32dd206dbd609212496e77ede47a4d21 SHA512 9e0d07bcd40e4a5c271646d6fce71677acbfa4439b4e818bb4df823eb31eff976ba78239e3a7e0ad21e80ea8d0d3af3e74a7d0a0a55b308d4c777b9b5e1ef8ad -DIST pango-0.22.9.crate 50989 BLAKE2B d85aa2cd686e82d6094c51ac93eeabf711dc28aa418ec189b98d4db58d33656b52cb013afb56ee83266e99ba0986e0bf1d50a4b69f11661ab8d851fc1e56fe26 SHA512 37131e2c58b083189ae960be59d416dc0e2f4ea75b273b63e7306367179be63efdb93b901a25e8d13e98996abf6878dbbf4df5a2cb277ef7052e2245c63b2d7c -DIST pango-sys-0.22.9.crate 28857 BLAKE2B b20238b516673ee76d4f8a69461ba1ffd652ff33e6cafcad1ddf9bbbbbc6f59e3996832f8e7afe0b02289c7b3b4e8202e6b48aba2b5550929acabaa558bd479a SHA512 7fc8ff7e1cdacf116d957891bb11575960a5cecc5a0524c3e0ba0908f53fc7448b981df00efd351883919da19c626fb1726d719dcacdf589c3fd305e8a28f45c -DIST parking-2.2.1.crate 10685 BLAKE2B 3af8020bc27ea921d6023c14f20545f70a5a5a5a216d5f97458d76d3bfc8d9de937f3c92668d31a79de85ce08ed0236d440c64ca1e4fe0a7748f412b941c052b SHA512 2e49137b59000f0467aa9ee0648d53e06cd2b8319d6ea28bf04c621b873469739a34186840b02f08d770d109cca26e161b3de3e4341dd729701885307a6a8177 -DIST pin-project-lite-0.2.17.crate 31034 BLAKE2B 1d3bb970100c2a27c456bf771b4884f3c91c048310d27608161d3a02adf2f9cdddbc8674257c6b58c4ad6c0953e11cddf2fdf5813d95e5ab8425e71032556159 SHA512 0cff4338e3c7b71600fb41db013efe0d01b9cb3fdf95192dc16c5bea58a0f157207ef285ceb01b6b30956f9e751577c5d2a3803103f89a8a0cc6ea728d97a4df -DIST pkg-config-0.3.34.crate 21370 BLAKE2B ec02a21dcd793ed4e69ba6d8c761a6c4daa8a3f2c5eeeb7077f8babbc14dfd7c0c7eb088eb9f61730fc6eb0d3f1f6271b72077d380d7304e1153feef532f118a SHA512 b8c63db0782b888e4be96a48102ace87f3a1ca496290f958c7835bc8bc8a9f00a8a2ea384bcb21c184b58238a65fdef96304d5598cea6cc55aeaeab085bab49e -DIST proc-macro-crate-3.5.0.crate 12709 BLAKE2B 693372efbded5ff45bfc4bd2dfe4bbc28ef939485e88b57076d92c99fce2824dca88373b1ce9e6517e313f2402885cf470028cf7e619eaa291519b8eacf697d5 SHA512 8848a2234e503414f9e5e90e7ae6e8141b743205c1bdf31cc5211781414c8619b617bf78b61d880178fa1d7e0d1cb172ac03695d9f8c611619e953a1fbd0353f -DIST proc-macro2-1.0.107.crate 59588 BLAKE2B 91df0c573e3e1f09c1aeedcad0381cf18186d72c3d50b077b1d232fbb67d1c87ed09f3c6dc515140041c2dab5ce7706042c8235b93099df7d1c962d4a3a99160 SHA512 c109d5509624a57156e0eae22a3aec89a1a163623e8b4e376a13d872a46e4bf6855ee3cd4428c07cdf5c22f2284b6ce755f9cc7e075480e4587ce4f2d40fa903 -DIST quote-1.0.47.crate 31622 BLAKE2B d912f17bdb5f2db29f5349e976e12da52db9d2197dd9900cb00e907d1e9b06ab177e9b020c1d53f6a6cd087e3a973aed5af63eac282da77792b2a6d6e1b35b07 SHA512 544f4324d425ae011905e0d1b52e367d4660bc34457b2f802af94e1def1e6c4e39ffba3beeb40d75753967d15b6dccce18fea273e7f3d46b63d5c89898d19441 -DIST rustc_version-0.4.1.crate 12245 BLAKE2B 0ed627c560d88e5ade830cc5f72c62f6ba88be3e30d0f8db34761fcd0b284e5b2fc063c240149aabdfb12c8410ecdbe50cb10a3544500b5a020f95a31cd6709c SHA512 7aa8a487ca7411b6d23cae62f7568fa3474a7b1fd1589621071be3428a550ed3b7c4ad68277a8f86849e5900397d7d0e9d678f5d423742a045dc7c660fa6c598 -DIST rustversion-1.0.23.crate 21013 BLAKE2B 80c4938e8663b095668809e4c81f4621582c4951b8b42eecbecedf5577e7555bf80944b6753101f5f87bba6f9856781e402ffd952600820b1c5830a181066798 SHA512 95baec2c1d8475661677ea35a724c392ed10b1151a1f4522b44133c6d691824bf4a42327ebc9d631a1f64f944dc721843e0aa0e19354fc4fab89aa048f1c68f5 -DIST semver-1.0.28.crate 33064 BLAKE2B 03bb56f4f0ec6f4cb208277500eba03cc81a7b2a115d64849b2d154066f723d46f8625df7c512cfbb2e402a36cbc5a1627e8041ac2de92daacd4a92a6943979c SHA512 bb44373e60fccb83bc95e4277ea0abdd756a3f77b3334cf6d6b5fceac086910661ab94eb326cdc8fefce0c09220933bdf03bfd142c1594a92c9c1e4e931114b7 -DIST sendit-0.1.0-r67.tar.xz 309264 BLAKE2B 8cd7ebbf84aed05c0fff4e353283e52320882d1b88ecf64c4a1106f7b0afc9c3fa06a78367f2df3b1e86d3fc26e03f872f0bc94e2fc3988c6f617d3a5ba22b9a SHA512 e2d58154ad4f4740366c6ce607cc10d73d6e2202e5fbed7c92450b016a92507f6dc68952659ef5448ea73f43a0d5d1597c8d4ee986d83fe77842aada4b3bd749 -DIST serde-1.0.229.crate 83669 BLAKE2B 7f54b013f2273865493684c7c8d7d272ffbef903a1242fd81808c69b23444acd96ec0ebc43955cace91107331ac74f5e60c770464ce5b5cca92c383a810f757d SHA512 589b7523ecb57fe2003cbc4eb36adb07f663ce9c8620a7880a63630360c3d36e77388a270878493df0c383c1266ff855aebce424227a1c9da42f0fe4adaeeb30 -DIST serde_core-1.0.229.crate 63100 BLAKE2B 3126a7f8915a142f49a403dfc3b54341f67af7e533d94981e4b13450eb9c8d0befda14c91d4ed05a288a3b71b68430d8d5ec3263db6c146ce697046657b719ad SHA512 23122dd8663f6b4cecc8e6affe8b7a010abdd31428461c033a5db330bb7c2872c8a7e91ce0884923b640bea801a64044fff6ef532b260c05f31cb426b2518e66 -DIST serde_derive-1.0.229.crate 59864 BLAKE2B 28ab7ec0616bff52dbee3e05724b85db0fd70ae9bbb1b8f5124dfc9c923f12547e467a0b838ef57fdbd526d81c2718b2b8e7e75aa54f8e4ecace491362888968 SHA512 6ff319ba98b7631aede0c985cd47026d4795139beee9d53e5b00ccf0a7f6542e446b094821044fee852c7ff31c3721b964fa9425d15685714ed8f57534070e9d -DIST serde_json-1.0.151.crate 156556 BLAKE2B 3d9744a6d247000a03585ac59dbda46e804746a8ee5e912744d06c9a82828d9c3997bfa61876955896df732b219db4628f99aec669eb2f25459f2f37f7d5ede2 SHA512 351407187b98416a57d4d9b380cfdfd28acb10af0b3e9b9eaac3a4810ed4b58bc3572d97dc7cb4a45b0284466787ca82952ca6659fb37fb23afac40da0d4f315 -DIST serde_spanned-1.1.1.crate 11025 BLAKE2B 24329285e068484b2480d603a6ab7f041f111e03eaaed6db00ac94e36e608c810b7f19040265c51e9d881e65a107cdfde98068b5f1f56dbb8e1fe6677683ac58 SHA512 ed6f0cd52d97985458d530a1ff973aabef8b611a17e739d5011aa50293e4cc994af24992e07ac55e0d2a8fa8d0577ebdba4b42b920cde505e05dadf05382e545 -DIST shlex-2.0.1.crate 19332 BLAKE2B db2b6c5f7f30ad2f968d2dd386d39b362b8d19d7e17fffbb7407d6f35a4e165432b6607f3897c66cf8d32bf6e795ba7297080608c855b796fcf19842f4c509fa SHA512 d1bf35e2c5a93c718d3cc395fc4b06cc0c42d4891c0f37247de7e179492478020f0c57ae74c461d5bb1f833b071fd5c036d8d6d6ab8b61be6140cb64ae525921 -DIST signal-hook-0.3.18.crate 50862 BLAKE2B a0ae80afc2de0e3eb8b3c10241617cda08dcf2a6c06da3b385dfaf098ea4fb6c14f5d1aa6720325cff15d8ab195bf4d05966dc5a9e7155a1e43fcaf238ffb654 SHA512 1955288d7f47738007252ac2aed0fd05d1c16f1726ae2a97ac35b0ba5a6a82620b26ecc03c4760b12d86d4b83c7484a3830aa84530dc010d398fff436fda3137 -DIST signal-hook-registry-1.4.8.crate 21668 BLAKE2B abaed3ca8f07e1631aecaa4bfcc9dea94f2bcd2ab227c9e44d543663392d5e7c7b3beea4d0624ff48637d4fc7544a722ba3d4113d109a77ec1a3b3c0d06580c5 SHA512 6625d842e5040fb15c3a6a03994aabe8b3d0b752773805866858452fb03e5cab2663c89367caa15a34dc0ee7c2b830e302d9be664130c093bdbc17d7b61f045c -DIST slab-0.4.12.crate 19080 BLAKE2B 785a1b899fe01c8825f24d90b0b0bb31bded234810f59c71b7e44221387de33fcae220a7d0745f59f79c267673b76e7c8d9fa4c0dcbf8fc5d68be6562018d49b SHA512 cf7c971b99b043d8bc102732a109717cbbec114160e4f7cc4009c66739177778623403c33d6a32efd1620e0ac7b8743c36e0e5563082af7f92f1e4caeceea0d9 -DIST slotmap-1.1.1.crate 61862 BLAKE2B dba772f98dc54613ae98d7b6c828e21cbab06fe4dfb7c5068db8363f839bde485e6558567737300111d15b6e4dd5226bf0240f10439ea18d6dc717e6847b9cca SHA512 ad072ddd9edea34c3137f4f30d7f83c055b11567ebb4f505166bd7092260113f59bf215ff4566c1d567479d3d28f51f3751f2b939a768e658cc54a25ca49054a -DIST smallvec-1.16.2.crate 34874 BLAKE2B 66535147385d84068958d2d6a573d93b328c7c29768e4a87eab04f898651f760c48776563c4632f94b09290223ea56b21e839e8082d047246e34dcfcedaf60ef SHA512 672edd712fbc9643cc7a72590eeb6558a372a1ac29bb0f6b71f086c4c89618322cbf72292f1e1fbd06eff2ef1ac48b1c8c8c8a397d5641817f6b21a4d65adc3e -DIST strsim-0.11.1.crate 14266 BLAKE2B 252a9ede4241b165525486aa8855dece37af77f5b28e0e1858c4a5d2047db9fa958328db10989234aad69463ab51b2303785ec056c63ea8c95bf95e111ddabf2 SHA512 0cebe0155a92640e56db9a599ae62078cbb32e1d2da8bfa67ed0e8f410a7558dfcf7b3c2720ff5913282e291ecf076aed9fe9bf84c8d44e814a642b1bed3335c -DIST syn-3.0.6.crate 313821 BLAKE2B 5c33c101bd1ccd35c0f906b924f5ab6400fc173335da7f83299d0a4fb551c4e29f2a3f11def321db1647d46754fe8afe05faf8d4a544ccba3cf8a009d9e8f5ed SHA512 9c0c411c7300471ce6f34f57c482020d2d93f2a191ded0533b4ec28f7b74510488006535f620434375bc9fe151b1589c8c72654a4ffd759ab9d9f0a52faabb7a -DIST system-deps-8.0.0.crate 61632 BLAKE2B 6b1fbe53fc1fcb22e17e2c1a3c370ae2c556155b87d432aa1785c0de2e09cd13215a368b8c0bd3fdc5c0a2510863cca239108100b0a5df2d9e05123ae5360a32 SHA512 ac152569181c3f994a48706709e2ba5ef77bc152ed717b682779a8d55def729d8fc64b05596e33f047a88b3ca147e32842ac67bdf49d6550f1a1f2986fa4659c -DIST system-deps-9.0.0.crate 63166 BLAKE2B fc404a08df8e0e4264f7a163aa32c67447b77c109cfa4eb928e24799191ad79e50484584b06ae3ecb140261da9a47aabf0bcbee79894ec9a069393cee7f6bdfb SHA512 e9171f22ef1959aefd5b083722180200eb7435969aac80ef6214b5ed377799ecbe9de0de04f245d0d07ea4431f654a8a7fe6b85e4487a2496739ec0c7a507e43 -DIST target-lexicon-0.13.5.crate 26928 BLAKE2B f8c62cb91f68a635e6ce3fc254df9ce378b5e8f62c1147404c967484b9ed97fb33d7993b4671611db76566063e999d658d201f856f70196ec678d72cc758cf6d SHA512 956df27c01e1186c5356117ffd07810fc0b62fb919d5bfd215955ba85be00ef675212a3c0dc36679986be7b825442fec5c93c7643b7e1e19c04572e6463546fc -DIST toml-0.9.12+spec-1.1.0.crate 56485 BLAKE2B 1552fd92ece75ebf9e8e4914bd60e981333d6f22f5b09baa489bb206369a654c4246d3f8832d7a8e03d2e4abe85cb6871b9ac9289af278b767e200807a4400ed SHA512 214cef1049e5f569ae448041bd0b6989626ab685c580fb77dbb47a4a8a16d879801f88ef5d298b711a82018788a4099a0a008de7d025c362c9fd989098be9b89 -DIST toml-1.1.6+spec-1.1.0.crate 55947 BLAKE2B 827d93b54deeaef10f562ec6f9543aa501c554abed55bf152ae2e12acf856fbd298a4d84f9827fc83d0386f93ae800409b274e8b82e3ecc37c3c9bea914bc3ae SHA512 c5a53b18456ca54d55fcdca1241e70ab34c5364e7ab4f54b9c1ffb720bd31906eac456f781df058aa5b8d9c8a94a8b37abd7f4784cae73731d6f85723e3f3c0c -DIST toml_datetime-0.7.5+spec-1.1.0.crate 17975 BLAKE2B 78f3ba1af90ef25ba23491c1828e8c64d70ae9fdd4718e58c689c17848f9b74469d62471c7ba447dada7d749e1c63b153bc2850b9966cf1ac7ce87a6c7916861 SHA512 4db08a8865011ebec8351a5c42c7c9ce862956c04cf80d10644f68d727362699a4240db9e210cb6d558fecadb63da0fb41b356b5f0cd1142303069988a8642b0 -DIST toml_datetime-1.1.1+spec-1.1.0.crate 17982 BLAKE2B 0552b1cd242430876cbe3f1b4209c21d5b8aacf70c3235087dc742bccc55bd0994013b021a48ba69c4a2802bd458eae56cd8a96999f43affefff0a5f6b2958ca SHA512 74e13bc2f616b407d11e445e3a416dc7cd819774a5d747baede2d71ac42573ea29fd77cedd2648ff1b286b879053e764b8ad07cd9e9fe1164f6cc26f12931808 -DIST toml_edit-0.25.15+spec-1.1.0.crate 68207 BLAKE2B 15479673911e43fb4b32800f6ccfeff38258693f53c8e46993b038a71f7fc925b23e731952230943e0760acd23f232335526e817a0fc3b40f50320c66ec467e1 SHA512 ab2de29147f29b5c4c78f674017732d90de3a15343f9a7647c42ec53957800459961d666e3db907eb3fc71735996659981e01d68542ba8ea11fb66949a79292a -DIST toml_parser-1.1.3+spec-1.1.0.crate 35888 BLAKE2B be644d7de774a2ac2a460acc705a77db5ebb6a6694a39c379816a9be5a315333ef7c59f8d7c66744e629654d386f4e19f3c5d6ac065c0a2c480b663c0a94374b SHA512 1e59ee837016e095f93adb4a6ef63c33adc94c09c005cb68a0ac310c32696e4c342f3eabae9fc4f23d4f0fa1d6a958bbf61938ff214e03830f95e24b6066b164 -DIST toml_writer-1.1.2+spec-1.1.0.crate 18968 BLAKE2B 7de24bd46720ad9794677687acb1a581a9998aaa4449ad1477edc5770941bf3e32af5ff3e1ebcc7e18a7e682a2d1d711c77a0f37984efe33ce6d9da3abf1ca87 SHA512 baedae17f16c3b8f1780ac2936c60c3fe12b3df254b1f58ae50081e1e8a260b8bccbfd2299b93df2c58e97df8006ed6debb6351a3276a3020431b1da3f90e25f -DIST unicode-ident-1.0.26.crate 48425 BLAKE2B d615360eb4874b898657770cd8adcb39350e51dbc76a3109f5e0589a8017b89e19b13a49c0170643cd4988296482bdc293f22437d44f06a1e0af9b76b3a0fa1c SHA512 216d13c655e47dddf888fac997a6345bb7c6d1f0ab6b67ed06bfd9013a891fda8e9e8d71b763639957c15b951145ffd77243ee2056a20ed809d2cae48502c472 -DIST unicode-segmentation-1.13.3.crate 112325 BLAKE2B 4767480bbb171eb9f9cb026c4101c7db58f37ef3d0ba12233c612392b48e1c63efa61e66c321a4bf772dca214794906cfc82bafb87e59a755bb164b0b2304c45 SHA512 927f3d2662c40213c6d824c410e3559f180fba5736abdea566ad9f3ed357c5a82f84f63e7fb89c6b18dfba2903d2dc43663b79d0b9f7672a42926e0ba49c85d1 -DIST utf8parse-0.2.2.crate 13499 BLAKE2B 095b5d219ab8ff04c06fd6303e03d913ae36a57845f0b2ca3217a40e31a54cb0fb5ecedbde165d28f5f60f1553d8252986d7098fa83befc84a7cb20bf3b76144 SHA512 f3dbf78fe924f1dc3cf9498b6e43fb10174699463f31091a7a8136d8f31ec84fc00e80e3d8551b7e86257e8b3573cfddb56fc0de797fdb2cde0e962a8f239266 -DIST version-compare-0.2.1.crate 14245 BLAKE2B 92e952cfb544ef73c4ef0fd44238b414fd66fa71bfb3e0e0d2c849f5f4353570a0a9b8dc8750c5c30ce4e4c3f50a9becb188ee96c383a3e3f03b23349c2415be SHA512 43e6aa9e53c2e8b21aedcf3704aece741329bf85508ecbc20e4ee35651d8a819d9e28c2102172a5fc335ea3124c379eb5507407c09e6532124a56a3aaa1097d9 -DIST version_check-0.9.5.crate 15554 BLAKE2B 131e75cc287518831e142430e2a39b48e4275874f4473780d47a27552294097aa1cddb65c385583e1022478f940b495eb43c24a8b1617b8ac5a24af3340c0d72 SHA512 d11d5a2240ab0c67ea69db56561ce202201edeef9a8e5eda3d9ae7ab7bb5752d8f343fe7f6536d8383de8d55f9024efa84f66a0c21e69563b7a38c4a628014db -DIST wasm-bindgen-0.2.129.crate 70859 BLAKE2B 9eedeeda57ed080226c3c612e122de72ca04384467ac315061e37181e263aa2f89858aae383337acd50656324abf9612a47d8d4840ebd3c06d130d8a7772d6ce SHA512 337e70b6982e0accf33fb7d63532021410940436de44c39dccc30dc92d47c7233a0d5160aa74c37aa826ff17903fbaea033429dde5b9cd3f331b2919538ebc77 -DIST wasm-bindgen-macro-0.2.129.crate 9612 BLAKE2B 7e316d050860597f8db2cd4629876439dec11a7aeb7521291adae049ad6ebd4b5d8309f9ab9c2383bf901302a540ec76ac15e83822c2d63a8c3756b1a0773200 SHA512 d5f5be387917df42b8e92ebcf9f1d84029e0d8501ead91b8511dd98880799a449ae30c44d4bb08b07855f9500c0c44f43ccf39dec93761e00f032f32a4703935 -DIST wasm-bindgen-macro-support-0.2.129.crate 120366 BLAKE2B 546b2e9438e61af0875ad9ca809d09698afc1dd30e33d3693cb7bd4da2f4dd71a3a17217610d05354b50b2aa691fe348985ed2283c6958473fde83d7d1aa53c9 SHA512 1ea78197ffb0ef6657310d234d2c3d6481600a4b9f2d1eaa91a040f42ea025641e481fe56a04242cbe92c940c48c4e64efc84b632c9df7482d4b3fa1e2e47002 -DIST wasm-bindgen-shared-0.2.129.crate 13023 BLAKE2B cce639af764f0eb5770447c3a8a24fced658b88115804ebd48422ec1a4f8a05e5d01efc9d0dbdfa48786bdf492c1fc900c8cbd80506d34be83d4893b557ed8c9 SHA512 a95891daedf9fcc51210b4cf4df65035ac4594218952cc34b6f982f69e073ec52585afe4e5466651d61ddec5973eab756d10c365199bfb842aa034d2f39ef6a1 -DIST web-sys-0.3.106.crate 682645 BLAKE2B 2b4876368968ed9ba0a9c1189f6f12a4c00fd796b64069e2cf7f5a454e914c86902ec44b1d6eb148318b6479c3d09e9b1862383466279260aad25606e1dfa856 SHA512 1d114917ea921cccbcde61ac172c741085219eaa17f99c717961698926d66f870207d8e9d13239a5190629312a8b7d8af8a284a5c80b151a9ad40e303033c0ef -DIST windows-link-0.2.1.crate 6133 BLAKE2B d1ca714178e6fe12bb3b4caeed9a52a36890989e77ae2dcd6f2d18ae4a21fb8479d304cdff63bb0a5f28acf44eaf104821cff83cd66b3a0406a5f33a800e3641 SHA512 afb0c87beb8914fd9a3e52db6f2f64a840470b9751b66c83892c24cb1c46cda63fb16b1aeafcaa5a1fc7c5ba01da46be63e5227cc108dede8b0bd27785498a6b -DIST windows-sys-0.61.2.crate 2517186 BLAKE2B f4eab0b365aa1df8a353d634da5081237bef0f7b69f02ffcaba232a688e59dfe877c47114de64c81e3cbb929c2fea1143ed6bda56ce935e66d871a993ac29106 SHA512 e6c8f61f0724dafca7821ee9b9c07237ccfee69be269be2fa7318eb48b151285ecf63e860aa1fe303639222fe11268e3773f37c2dd79f923029af58c645edb3f -DIST winnow-0.7.15.crate 187092 BLAKE2B 2fa004d7d0a166202e27f9bdb57f7ba7d1a8d736a77518e8a49afc1c1574db93746a50d26f802f571c14e8c138857e1e13802c04308f9e06215517c012e9b275 SHA512 47c4abb8de7f024e2e85b1c0ed9ebe16b9de29ce82c536a133de9d64d99ac37191e4322221640785234a32b03a28bfa99ced738bad5088d39bfbbe2a64c99689 -DIST winnow-1.0.4.crate 188513 BLAKE2B b00e5c4a9216479db21c58abc52b358a0e00b757ea49cb410ba02896faaf1af8d46680a910f2155e7f1edb071538d9e33d9ccd304a07c26b106f75ed5ca63c64 SHA512 97d451051e2bd9bcb14f09ef6df6d2b187b768dcb4d07efd6678e4259e941dd95800f95ba0eeda09b589e95232099e91ea0f15849f35403e54a0081eae99ab5e -DIST xml-rs-0.8.29.crate 52816 BLAKE2B 6a7fe20b91ccdeb3ab933300addb222cb7d341628ac7bbe99c3a8c20c9c8464f3e2ed4bf3016e91c0e3a469bcac6555c553814ff80ab4d8a5326a7bdf6a1b5b7 SHA512 d62bed44644388aba7d3ef127dae693746c19583029685c58e2b24e3824db520c7ad62d3d2e42a18c309d7e9e5909662de8b71df401013e7beaa0964967e3150 -DIST zmij-1.0.23.crate 28612 BLAKE2B 5b1fda50a365ae6bba79ed8444f15dd184d0ba0d7b993c8f119355029b5d7cf6a4fe5d9f36eec722221725ed747cad5108eb707ef14920670ffa45843edcbfec SHA512 12f8d9726b232b6be68cefbe294505debb7a98715c11b95d89682a596fe1a02d9d7b2a6cdba97f2e8d47fa947ffbce9506a18568f2cab749ca3d03121e5fb66d diff --git a/gui-apps/sendit/metadata.xml b/gui-apps/sendit/metadata.xml deleted file mode 100644 index 5a2c0d86ac48..000000000000 --- a/gui-apps/sendit/metadata.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE pkgmetadata SYSTEM "https://srcux.com/dtd/metadata.dtd"> -<pkgmetadata> - <maintainer type="person"> - <email>ecsfwxvr@localhost</email> - <name>Local Maintainer</name> - </maintainer> - <longdescription> - Sendit is a Rust, GTK4 and GLES shell foundation for Wayfire, with - physical-member bars, live themes and a keyboard/mouse window launcher. - Owner-supplied source tarballs allow successive revisions to be built by Portage. - </longdescription> - <origin>srcux</origin> -</pkgmetadata> diff --git a/gui-apps/sendit/sendit-0.1.0-r67.ebuild b/gui-apps/sendit/sendit-0.1.0-r67.ebuild deleted file mode 100644 index 0825d30a0538..000000000000 --- a/gui-apps/sendit/sendit-0.1.0-r67.ebuild +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2026 Sendit contributors -# All rights reserved. Local private package. - -EAPI=8 - -RUST_MIN_VER="1.92.0" -CRATES=" - anstream@1.0.0 - anstyle-parse@1.0.0 - anstyle-query@1.1.5 - anstyle-wincon@3.0.11 - anstyle@1.0.14 - anyhow@1.0.104 - async-channel@2.5.0 - autocfg@1.5.1 - bitflags@2.13.2 - bumpalo@3.20.3 - cairo-rs@0.22.9 - cairo-sys-rs@0.22.9 - cc@1.5.1 - cfg-expr@0.20.9 - cfg-if@1.0.5 - clap@4.6.7 - clap_builder@4.6.7 - clap_derive@4.6.7 - clap_lex@1.1.1 - colorchoice@1.0.5 - concurrent-queue@2.5.0 - crossbeam-utils@0.8.23 - equivalent@1.0.2 - errno@0.3.14 - event-listener-strategy@0.5.4 - event-listener@5.4.2 - field-offset@0.3.6 - find-msvc-tools@0.1.14 - futures-channel@0.3.34 - futures-core@0.3.34 - futures-executor@0.3.34 - futures-io@0.3.34 - futures-macro@0.3.34 - futures-task@0.3.34 - futures-util@0.3.34 - gdk-pixbuf-sys@0.22.9 - gdk-pixbuf@0.22.0 - gdk4-sys@0.11.5 - gdk4@0.11.5 - gio-sys@0.22.9 - gio@0.22.10 - gl@0.14.0 - gl_generator@0.14.0 - glib-macros@0.22.9 - glib-sys@0.22.9 - glib@0.22.10 - glow@0.16.0 - gobject-sys@0.22.9 - graphene-rs@0.22.8 - graphene-sys@0.22.9 - gsk4-sys@0.11.5 - gsk4@0.11.5 - gtk4-layer-shell-sys@0.6.1 - gtk4-layer-shell@0.8.1 - gtk4-macros@0.11.5 - gtk4-sys@0.11.5 - gtk4@0.11.5 - hashbrown@0.17.1 - heck@0.5.0 - indexmap@2.14.2 - is_terminal_polyfill@1.70.2 - itoa@1.0.18 - js-sys@0.3.106 - khronos_api@3.1.0 - libc@0.2.189 - log@0.4.34 - memchr@2.8.3 - memoffset@0.9.1 - nucleo-matcher@0.3.1 - once_cell@1.21.4 - once_cell_polyfill@1.70.2 - pango-sys@0.22.9 - pango@0.22.9 - parking@2.2.1 - pin-project-lite@0.2.17 - pkg-config@0.3.34 - proc-macro-crate@3.5.0 - proc-macro2@1.0.107 - quote@1.0.47 - rustc_version@0.4.1 - rustversion@1.0.23 - semver@1.0.28 - serde@1.0.229 - serde_core@1.0.229 - serde_derive@1.0.229 - serde_json@1.0.151 - serde_spanned@1.1.1 - shlex@2.0.1 - signal-hook-registry@1.4.8 - signal-hook@0.3.18 - slab@0.4.12 - slotmap@1.1.1 - smallvec@1.16.2 - strsim@0.11.1 - syn@3.0.6 - system-deps@8.0.0 - system-deps@9.0.0 - target-lexicon@0.13.5 - toml@0.9.12+spec-1.1.0 - toml@1.1.6+spec-1.1.0 - toml_datetime@0.7.5+spec-1.1.0 - toml_datetime@1.1.1+spec-1.1.0 - toml_edit@0.25.15+spec-1.1.0 - toml_parser@1.1.3+spec-1.1.0 - toml_writer@1.1.2+spec-1.1.0 - unicode-ident@1.0.26 - unicode-segmentation@1.13.3 - utf8parse@0.2.2 - version-compare@0.2.1 - version_check@0.9.5 - wasm-bindgen-macro-support@0.2.129 - wasm-bindgen-macro@0.2.129 - wasm-bindgen-shared@0.2.129 - wasm-bindgen@0.2.129 - web-sys@0.3.106 - windows-link@0.2.1 - windows-sys@0.61.2 - winnow@0.7.15 - winnow@1.0.4 - xml-rs@0.8.29 - zmij@1.0.23 -" - -PYTHON_COMPAT=( python3_{13..14} ) - -inherit cargo desktop python-single-r1 xdg - -DESCRIPTION="Modular GTK4/GLES shell and launcher foundation for Wayfire" -# Owner-supplied immutable source archive; crates remain fetchable normally. -HOMEPAGE="" -SRC_URI="${PF}.tar.xz ${CARGO_CRATE_URIS//https:/fetch+https:}" -S="${WORKDIR}/${PF}" - -LICENSE="all-rights-reserved Apache-2.0 Apache-2.0-with-LLVM-exceptions MIT MPL-2.0 Unicode-3.0" -SLOT="0" -KEYWORDS="~amd64" -IUSE="test" -REQUIRED_USE="${PYTHON_REQUIRED_USE}" -RESTRICT="fetch mirror !test? ( test )" - -DEPEND=" - >=dev-libs/glib-2.66:2 - >=gui-libs/gtk-4.16:4[wayland,introspection] - >=x11-libs/gtk+-3.24:3[introspection] - >=gui-libs/gtk4-layer-shell-1.3.0 - >=media-video/wireplumber-0.5:0/0.5 - media-libs/graphene - media-libs/libglvnd - x11-libs/cairo - x11-libs/gdk-pixbuf:2[jpeg] - x11-libs/pango -" -RDEPEND="${DEPEND} - net-misc/iputils - >=gui-wm/wayfire-0.11.0 - >=gui-apps/wf-appearance-0.2.0 - gui-apps/grim - gui-apps/slurp - gui-apps/wl-clipboard - ${PYTHON_DEPS} - $(python_gen_cond_dep 'dev-python/pygobject:3[${PYTHON_USEDEP}]') - $(python_gen_cond_dep 'dev-python/tomlkit[${PYTHON_USEDEP}]') -" -BDEPEND="${PYTHON_DEPS} - virtual/pkgconfig - test? ( - sys-apps/dbus - app-misc/tmux - $(python_gen_cond_dep 'dev-python/tomlkit[${PYTHON_USEDEP}]') - ) -" - -pkg_setup() { - rust_pkg_setup - python-single-r1_pkg_setup -} - -pkg_nofetch() { - einfo "Obtain the private ${PF}.tar.xz from the owner and place it in ${DISTDIR}." -} - -src_configure() { - cargo_src_configure --frozen -} - -src_compile() { - cargo_src_compile --package sendit --package sendit-theme --package sendit-screenshot --package sendit-settings -} - -src_test() { - cargo_src_test --workspace - "${PYTHON}" -m unittest discover -s tests -p test_terminal_theme.py -v || die "Terminal adapter tests failed" -} - -src_install() { - cargo_src_install --path crates/sendit - cargo_src_install --path crates/sendit-theme - cargo_src_install --path crates/sendit-screenshot - cargo_src_install --path crates/sendit-settings - dosym sendit-theme /usr/bin/sendit-appearance - python_newscript scripts/session.py sendit-session - python_newscript scripts/terminal-theme.py sendit-terminal-theme - insinto /usr/share/sendit - doins -r themes - doins config/sendit.toml - # Installation does not edit user or compositor configuration. - domenu data/org.sendit.Shell.desktop data/org.sendit.Theme.desktop data/org.sendit.ThemeEditor.desktop data/org.sendit.Layouts.desktop data/org.sendit.Screenshot.desktop data/org.sendit.Settings.desktop - dodoc README.md THEMING.md DESIGN.md FOUNDATION.md BASELINE.md LICENSE - dodoc -r docs -} - -pkg_postinst() { - xdg_pkg_postinst - elog "Run Sendit Shell from your launcher, or 'sendit-session start' as your desktop user." - elog "After package updates: sendit-session restart" - elog "Wayfire [autostart]: autostart_sendit = /usr/bin/sendit-session start" - elog "'sendit preview' is only the windowed widget demo." - elog "This is a foundation prototype; installation does not enable autostart." - elog "Wayfire needs ipc, ipc-rules, vswitch and wm-actions; stitched members need the local topology extension." -} diff --git a/gui-apps/wf-settings/Manifest b/gui-apps/wf-settings/Manifest deleted file mode 100644 index c8c3cb8c3999..000000000000 --- a/gui-apps/wf-settings/Manifest +++ /dev/null @@ -1 +0,0 @@ -DIST wf-shell-srcux-0.11.0-r72.tar.xz 39702800 BLAKE2B 3e1eda3641e6afe893380ad4369b689a424f74d4f2827af246c5a5d6ec95e4636a7e5b27ad81ad3509798b837dd5a1aa6997c30916f16744d0872df672cb1ed4 SHA512 531a579551f0eb608a16b6d3c760065af9a84352ba01ad130c10cc8f7b8874f53718dc641bb3596f987564527e1532df8cd01579c591d2718c67e9a3c34b4eda diff --git a/gui-apps/wf-settings/metadata.xml b/gui-apps/wf-settings/metadata.xml deleted file mode 100644 index 76d06f5efe85..000000000000 --- a/gui-apps/wf-settings/metadata.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE pkgmetadata SYSTEM "https://srcux.com/dtd/metadata.dtd"> -<pkgmetadata> - <maintainer type="person"> - <email>ecsfwxvr@localhost</email> - <name>Local Maintainer</name> - </maintainer> - <origin>srcux</origin> -</pkgmetadata> diff --git a/gui-apps/wf-settings/wf-settings-0.11.0.ebuild b/gui-apps/wf-settings/wf-settings-0.11.0.ebuild deleted file mode 100644 index 86a14316aa93..000000000000 --- a/gui-apps/wf-settings/wf-settings-0.11.0.ebuild +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2026 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 -EAPI=8 -inherit meson xdg -DESCRIPTION="Wayfire desktop settings application" -HOMEPAGE="https://github.com/WayfireWM/wf-shell" -SRC_URI="wf-shell-srcux-0.11.0-r72.tar.xz" -S="${WORKDIR}/wf-shell-srcux-0.11.0-r72" -LICENSE="MIT" -SLOT="0" -KEYWORDS="~amd64" -RESTRICT="fetch mirror" -DEPEND=" - >=dev-cpp/gtkmm-4.12:4.0 - >=dev-cpp/tomlplusplus-3.4.0 - dev-libs/yyjson:= -" -RDEPEND="${DEPEND} - >=gui-apps/wf-shell-0.11.0-r72 - >=gui-libs/wayfire-plugins-custom-0.1.0-r10[window-state] -" -BDEPEND="virtual/pkgconfig" -src_configure() { - local emesonargs=( -Dcomponent=settings ) - meson_src_configure -} -pkg_nofetch() { - einfo "Place the local wf-shell-srcux-0.11.0-r72.tar.xz source release in ${DISTDIR}." -} diff --git a/gui-apps/wf-shell/Manifest b/gui-apps/wf-shell/Manifest index bb40eca84c4b..3035678213ac 100644 --- a/gui-apps/wf-shell/Manifest +++ b/gui-apps/wf-shell/Manifest @@ -1,2 +1,2 @@ +DIST wf-shell-0.10.0.tar.xz 9167372 BLAKE2B 47297fc78906cfad9a07394cda67cabb3f9c773bd2544dd5d9deb991e4d8cc4e939aec1796ea2851dabf8111818cb61b954b39fd2116494904dc082586b99096 SHA512 beaf41010262e36f663e5ac53d173853b7d90e6958b4e5ac5b98d8a626ed15b0c5c1b9cc0e9b6d98073e9cfb8abfa1cdbfcb0a1d5b778090f7d1be4d13ef7820 DIST wf-shell-0.11.0.tar.xz 39138216 BLAKE2B 9588361d33acbc199de9a6e082ab9ef9c5f2621ca1c513a3bcdb57eca8ac8f0d6a8d5b765e89b6139964c48d64e20a64e3ed7245ac758e2d964792236b344e78 SHA512 bdcef9b03e2a51bda27422041444e97ddd92217d8f676d725fe3420ab85c515e1e057ca0544f4578d9ffd73fc488316c1b652d987a9fe160621f3d85cdb43796 -DIST wf-shell-srcux-0.11.0-r72.tar.xz 39702800 BLAKE2B 3e1eda3641e6afe893380ad4369b689a424f74d4f2827af246c5a5d6ec95e4636a7e5b27ad81ad3509798b837dd5a1aa6997c30916f16744d0872df672cb1ed4 SHA512 531a579551f0eb608a16b6d3c760065af9a84352ba01ad130c10cc8f7b8874f53718dc641bb3596f987564527e1532df8cd01579c591d2718c67e9a3c34b4eda diff --git a/gui-apps/wf-shell/files/wf-shell-0.11.0-async-ddc.patch b/gui-apps/wf-shell/files/wf-shell-0.11.0-async-ddc.patch deleted file mode 100644 index 4b80d79dbe73..000000000000 --- a/gui-apps/wf-shell/files/wf-shell-0.11.0-async-ddc.patch +++ /dev/null @@ -1,597 +0,0 @@ ---- a/src/panel/widgets/brightness/ddcutil.cpp -+++ b/src/panel/widgets/brightness/ddcutil.cpp -@@ -1,234 +1,381 @@ --#include <ddcutil_types.h> --#include <ddcutil_c_api.h> --#include <ddcutil_status_codes.h> -- -+#include <algorithm> -+#include <condition_variable> -+#include <deque> - #include <iostream> -+#include <mutex> - #include <thread> - - #include "brightness.hpp" - #include "panel.hpp" - --#define VCP_BRIGHTNESS_CODE 0x10 -- --void show_err(std::string location, DDCA_Status status) --{ -- if (!status && (status != DDCRC_OK)) -- { -- std::cerr << location << " :" << ddca_rc_name(status) << " : " << ddca_rc_desc(status) << "\n"; -- } --} -+namespace -+{ -+constexpr uint8_t VCP_BRIGHTNESS_CODE = 0x10; -+ -+bool succeeded(const char *operation, DDCA_Status status) -+{ -+ if (status == DDCRC_OK) -+ { -+ return true; -+ } -+ -+ std::cerr << "DDC " << operation << ": " << ddca_rc_name(status) -+ << ": " << ddca_rc_desc(status) << '\n'; -+ return false; -+} -+ -+struct display_state -+{ -+ std::string connector, name; -+ double brightness = 0; -+}; - - class WfLightDdcaControl : public WfLightControl - { -- private: -- DDCA_Display_Ref ref; -- DDCA_Display_Info2 *info = nullptr; -- std::string connector = "..."; -- int max; -- double brightness_wish; -- bool wish_pending = false; -- -- int get_max() -- { -- return max; -- } -+ display_state display; - - public: -- WfLightDdcaControl(WayfireBrightness *parent, DDCA_Display_Ref _ref) : WfLightControl(parent) -- { -- ref = _ref; -- DDCA_Status status; -- -- // we want Info2 for the connector name -- status = ddca_get_display_info2(ref, &info); -- show_err("open display", status); -- -- assert(info != nullptr); -- // drm_card_connector is something like cardX-<connector-name> -- connector = std::string(info->drm_card_connector); -- connector = connector.substr(connector.find("-") + 1, connector.size()); -- -- DDCA_Display_Handle handle; -- ddca_open_display2(info->dref, false, &handle); -- -- DDCA_Non_Table_Vcp_Value value; -- status = ddca_get_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, &value); -- max = value.mh << 8 | value.ml; -+ WfLightDdcaControl(WayfireBrightness *parent, const display_state& state) : -+ WfLightControl(parent), display(state) -+ { -+ label.set_text(display.name); -+ set_scale_target_value(display.brightness); -+ signal_map().connect([this] () -+ { -+ DdcaSurveillor::get().refresh(display.connector); -+ }); -+ } -+ -+ std::string get_connector() override { return display.connector; } -+ std::string get_name() override { return display.name; } -+ double get_brightness() override { return get_scale_target_value(); } -+ -+ void set_brightness(double brightness) override -+ { -+ DdcaSurveillor::get().set_brightness(display.connector, brightness); -+ } -+}; -+} -+ -+struct DdcaSurveillor::impl -+{ -+ enum class action { scan, read, write }; -+ struct request -+ { -+ action type; -+ std::string connector; -+ double value = 0; -+ }; -+ struct result -+ { -+ bool rescan; -+ std::vector<display_state> displays; -+ }; -+ struct device -+ { -+ DDCA_Display_Ref ref; -+ int maximum = 0; -+ display_state state; -+ }; -+ -+ // Only the GTK thread uses these snapshots and controls. DDC references -+ // never leave the worker, where scans, reads and writes are serialized. -+ std::vector<display_state> displays; -+ std::map<std::string, std::vector<std::shared_ptr<WfLightControl>>> controls; -+ Glib::Dispatcher ready; -+ sigc::connection ready_connection; -+ std::mutex mutex; -+ std::condition_variable wake; -+ std::deque<request> requests; -+ std::deque<result> results; -+ bool stopping = false; -+ std::thread worker; -+ -+ void enqueue(request next) -+ { -+ std::lock_guard<std::mutex> lock(mutex); -+ if (stopping) -+ { -+ return; -+ } -+ -+ // Coalesce monitor notifications and slider motion while a slow I2C -+ // operation is in flight. The most recent requested value wins. -+ requests.erase(std::remove_if(requests.begin(), requests.end(), [&] (const request& old) -+ { -+ return old.type == next.type && old.connector == next.connector; -+ }), requests.end()); -+ requests.push_back(std::move(next)); -+ wake.notify_one(); -+ } -+ -+ bool read_device(device& dev) -+ { -+ DDCA_Display_Handle handle = nullptr; -+ if (!succeeded("open display", ddca_open_display2(dev.ref, false, &handle))) -+ { -+ return false; -+ } -+ -+ DDCA_Non_Table_Vcp_Value value{}; -+ auto status = ddca_get_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, &value); - ddca_close_display(handle); -- scale.set_target_value(get_brightness()); -- update_parent_icon(); -- label.set_text(get_name()); -- -- // MCCS doesn’t have anything to track changes to feature codes, so we -- // read the current value and adjust the visual when mapped. New thread -- // because MCCS actions can take quite some time and would stall the UI -- signal_map().connect([this] () -- { -- std::thread([this] () -- { -- set_scale_target_value(get_brightness()); -- }).detach(); -- }); -- } -- -- ~WfLightDdcaControl() -- { -- free(info); -- } -- -- std::string get_connector() -- { -- return connector; -- } -- -- std::string get_name() -- { -- auto name = std::string(info->model_name); -- -- if (name == "") -- { -- name = "Unnamed " + connector + " display"; -- } -- -- return name; -- } -- -- void set_brightness(double brightness) -- { -- brightness_wish = brightness; -- -- // set the value in another thread to not block panel, as it can take a while -- // even if we accidentally have another call go through, it’s no biggie -- if (!wish_pending) -- { -- wish_pending = true; -- std::thread(writevcp, this).detach(); -- } -- -- update_parent_icon(); -- -- for (auto control : DdcaSurveillor::get().ref_to_controls[ref]) -- { -- control->set_scale_target_value(brightness); -- } -- } -- -- static void writevcp(WfLightDdcaControl *control) -- { -- double target = control->brightness_wish; -- -- DDCA_Display_Handle handle; -- DDCA_Status status = ddca_open_display2(control->info->dref, false, &handle); -- show_err("open display", status); -- -- uint16_t value = (uint16_t)(control->get_max() * target); -- uint8_t sh = value >> 8; -- uint8_t sl = value & 0xFF; -- status = ddca_set_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, sh, sl); -- show_err("set brigthness", status); -- ddca_close_display(handle); -- -- // value changed again, re-run -- // for this to be infinite recursion, the user needs to constantly keep -- // moving the slider before the write is done, should be safe enough -- if (control->brightness_wish != target) -- { -- writevcp(control); -- } -- -- control->wish_pending = false; -- } -- -- double get_brightness() -- { -- DDCA_Display_Handle handle; -- DDCA_Status status = ddca_open_display2(info->dref, false, &handle); -- show_err("open display", status); -- -- DDCA_Non_Table_Vcp_Value value; -- status = ddca_get_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, &value); -- show_err("get brightness", status); -- ddca_close_display(handle); -- return (value.sh << 8 | value.sl) / 100.0; -+ if (!succeeded("read brightness", status)) -+ { -+ return false; -+ } -+ -+ dev.maximum = (value.mh << 8) | value.ml; -+ if (dev.maximum <= 0) -+ { -+ return false; -+ } -+ -+ dev.state.brightness = std::clamp(double((value.sh << 8) | value.sl) / dev.maximum, 0.0, 1.0); -+ return true; -+ } -+ -+ void run() -+ { -+ std::map<std::string, device> devices; -+ while (true) -+ { -+ request next; -+ { -+ std::unique_lock<std::mutex> lock(mutex); -+ wake.wait(lock, [&] { return stopping || !requests.empty(); }); -+ if (stopping) -+ { -+ return; -+ } -+ -+ next = std::move(requests.front()); -+ requests.pop_front(); -+ } -+ -+ result update{next.type == action::scan, {}}; -+ if (next.type == action::scan) -+ { -+ // Redetect invalidates every previous DDC reference. No other -+ // DDC operation can run concurrently with this worker. -+ devices.clear(); -+ DDCA_Display_Info_List *list = nullptr; -+ if (succeeded("detect displays", ddca_redetect_displays()) && -+ succeeded("list displays", ddca_get_display_info_list2(false, &list)) && list) -+ { -+ for (int i = 0; i < list->ct; ++i) -+ { -+ DDCA_Display_Info2 *info = nullptr; -+ auto ref = list->info[i].dref; -+ if (!succeeded("display information", ddca_get_display_info2(ref, &info)) || !info) -+ { -+ continue; -+ } -+ -+ std::string connector = info->drm_card_connector; -+ auto dash = connector.find('-'); -+ if (dash != std::string::npos) -+ { -+ connector.erase(0, dash + 1); -+ } -+ -+ std::string name = info->model_name; -+ ddca_free_display_info2(info); -+ if (connector.empty()) -+ { -+ continue; -+ } -+ -+ device dev{ref, 0, {connector, name.empty() ? "Unnamed " + connector + " display" : name}}; -+ if (read_device(dev)) -+ { -+ update.displays.push_back(dev.state); -+ devices.emplace(connector, std::move(dev)); -+ } -+ } -+ } -+ -+ if (list) -+ { -+ ddca_free_display_info_list(list); -+ } -+ } else -+ { -+ auto it = devices.find(next.connector); -+ if (it == devices.end()) -+ { -+ continue; -+ } -+ -+ auto& dev = it->second; -+ if (next.type == action::write) -+ { -+ DDCA_Display_Handle handle = nullptr; -+ if (!succeeded("open display", ddca_open_display2(dev.ref, false, &handle))) -+ { -+ continue; -+ } -+ -+ auto value = uint16_t(std::clamp(next.value, 0.0, 1.0) * dev.maximum); -+ auto status = ddca_set_non_table_vcp_value(handle, VCP_BRIGHTNESS_CODE, value >> 8, value & 255); -+ ddca_close_display(handle); -+ if (!succeeded("write brightness", status)) -+ { -+ continue; -+ } -+ -+ dev.state.brightness = next.value; -+ } else if (!read_device(dev)) -+ { -+ continue; -+ } -+ -+ update.displays.push_back(dev.state); -+ } -+ -+ { -+ std::lock_guard<std::mutex> lock(mutex); -+ if (stopping) -+ { -+ return; -+ } -+ -+ results.push_back(std::move(update)); -+ } -+ -+ ready.emit(); -+ } -+ } -+ -+ ~impl() -+ { -+ ready_connection.disconnect(); -+ { -+ std::lock_guard<std::mutex> lock(mutex); -+ stopping = true; -+ requests.clear(); -+ } -+ -+ wake.notify_one(); -+ if (worker.joinable()) -+ { -+ worker.join(); -+ } - } - }; - --DdcaSurveillor::DdcaSurveillor() --{ -- // ddcutil watcher threads have a tendency to segfault, so we use the gdk monitors -- // this means we can’t tweak parameters and we have to entirely re-scan the displays -- -- mon_ch_sig = WayfirePanelApp::get().signal_monitor_list_changed().connect([=] () -- { -- std::cout << "Monitors changed" << std::endl; -- rescan_mccs_monitors(0, 0, 0); -+DdcaSurveillor::DdcaSurveillor() : priv(std::make_unique<impl>()) -+{ -+ priv->ready_connection = priv->ready.connect([this] { apply_results(); }); -+ priv->worker = std::thread([worker = priv.get()] { worker->run(); }); -+ mon_ch_sig = WayfirePanelApp::get().signal_monitor_list_changed().connect([this] () -+ { -+ rescan_mccs_monitors(); - }); -- -- // waiting for idle means DdcaSurveillor will be initialised -- Glib::signal_idle().connect_once([=] () { rescan_mccs_monitors(0, 0, 0); }); -+ rescan_mccs_monitors(); - } - - DdcaSurveillor::~DdcaSurveillor() - { - mon_ch_sig.disconnect(); - clean_controls(); -+ priv.reset(); -+} -+ -+void DdcaSurveillor::rescan_mccs_monitors() -+{ -+ priv->enqueue({impl::action::scan, {}}); -+} -+ -+void DdcaSurveillor::refresh(const std::string& connector) -+{ -+ priv->enqueue({impl::action::read, connector}); -+} -+ -+void DdcaSurveillor::set_brightness(const std::string& connector, double value) -+{ -+ value = std::clamp(value, 0.0, 1.0); -+ priv->enqueue({impl::action::write, connector, value}); -+ for (auto& control : priv->controls[connector]) -+ { -+ control->set_scale_target_value(value); -+ } -+} -+ -+void DdcaSurveillor::apply_results() -+{ -+ std::deque<impl::result> updates; -+ { -+ std::lock_guard<std::mutex> lock(priv->mutex); -+ updates.swap(priv->results); -+ } -+ -+ for (auto& update : updates) -+ { -+ if (update.rescan) -+ { -+ clean_controls(); -+ priv->displays = std::move(update.displays); -+ for (auto widget : widgets) -+ { -+ catch_up_widget(widget); -+ } -+ } else -+ { -+ for (const auto& display : update.displays) -+ { -+ for (auto& known : priv->displays) -+ { -+ if (known.connector == display.connector) -+ { -+ known = display; -+ } -+ } -+ -+ for (auto& control : priv->controls[display.connector]) -+ { -+ control->set_scale_target_value(display.brightness); -+ } -+ } -+ } -+ } - } - - void DdcaSurveillor::clean_controls() - { -- for (auto widget : instance->widgets) -+ for (auto widget : widgets) - { - strip_widget(widget); - } --} -- --void DdcaSurveillor::rescan_mccs_monitors(const int pos, const int rem, const int add) --{ -- ddca_redetect_displays(); -- // remove monitor controls from the widgets -- instance->clean_controls(); -- auto display = Gdk::Display::get_default(); -- -- DDCA_Display_Info_List *display_list = NULL; -- ddca_get_display_info_list2(false, &display_list); -- -- for (int i = 0; i < display_list->ct; i++) -- { -- auto ref = display_list->info[i].dref; -- instance->ref_to_controls[ref]; -- -- for (auto widget : instance->widgets) -- { -- auto control = std::make_shared<WfLightDdcaControl>(widget, ref); -- instance->ref_to_controls[ref].push_back(control); -- widget->add_control(control); -- } -- } -- -- ddca_free_display_info_list(display_list); -+ -+ priv->controls.clear(); - } - - void DdcaSurveillor::catch_up_widget(WayfireBrightness *widget) - { -- /* NOOP */ -+ for (const auto& display : priv->displays) -+ { -+ auto control = std::make_shared<WfLightDdcaControl>(widget, display); -+ priv->controls[display.connector].push_back(control); -+ widget->add_control(control); -+ } - } - - void DdcaSurveillor::strip_widget(WayfireBrightness *widget) - { -- for (auto& [ref, controls] : ref_to_controls) -+ for (auto& [connector, controls] : priv->controls) - { - controls.erase(std::remove_if(controls.begin(), controls.end(), -- [widget] (const std::shared_ptr<WfLightControl>& c) -- { -- if (c->get_parent() == widget) -- { -- widget->rem_control(c); -+ [widget] (const std::shared_ptr<WfLightControl>& control) -+ { -+ if (control->get_parent() == widget) -+ { -+ widget->rem_control(control); - return true; - } - - return false; -- }), -- controls.end()); -+ }), controls.end()); - } - } - ---- a/src/panel/widgets/brightness/brightness.hpp -+++ b/src/panel/widgets/brightness/brightness.hpp -@@ -113,16 +113,20 @@ - DdcaSurveillor(); - void catch_up_widget(WayfireBrightness *widget); - void strip_widget(WayfireBrightness *widget); -- static void rescan_mccs_monitors(const int pos, const int rem, const int add); -+ void rescan_mccs_monitors(); -+ void apply_results(); - void clean_controls(); - - sigc::connection mon_ch_sig; - static inline std::unique_ptr<DdcaSurveillor> instance; -+ struct impl; -+ std::unique_ptr<impl> priv; - - public: - ~DdcaSurveillor(); - -- std::map<DDCA_Display_Ref, std::vector<std::shared_ptr<WfLightControl>>> ref_to_controls; -+ void refresh(const std::string& connector); -+ void set_brightness(const std::string& connector, double value); - - static DdcaSurveillor& get(); - }; diff --git a/gui-apps/wf-shell/files/wf-shell-0.11.0-panel-desktop.patch b/gui-apps/wf-shell/files/wf-shell-0.11.0-panel-desktop.patch deleted file mode 100644 index 47c83f89b182..000000000000 --- a/gui-apps/wf-shell/files/wf-shell-0.11.0-panel-desktop.patch +++ /dev/null @@ -1,259 +0,0 @@ ---- a/src/panel/widgets/window-title.cpp -+++ b/src/panel/widgets/window-title.cpp -@@ -11,6 +11,7 @@ - sigc::connection pending; - bool in_flight = false, dirty = false; - -+ public: - void refresh() - { - if (!client) return; -@@ -29,15 +30,13 @@ - client->send(R"({"method":"window-rules/list-views"})", [this] (wf::json_t response) - { - views = std::move(response); -- in_flight = false; -- if (dirty) -+ client->send(R"({"method":"panel-desktop/status"})", [this] (wf::json_t response) - { -- dirty = false; -- refresh(); -- } else -- { -- changed.emit(); -- } -+ desktops = std::move(response); -+ in_flight = false; -+ if (dirty) { dirty = false; refresh(); } -+ else changed.emit(); -+ }); - }); - }); - return false; -@@ -45,7 +44,7 @@ - } - - public: -- wf::json_t outputs, views; -+ wf::json_t outputs, views, desktops; - sigc::signal<void()> changed; - - static std::shared_ptr<WindowTitleTracker> get() -@@ -107,16 +106,48 @@ - auto resize = [this] { label.set_max_width_chars(std::clamp(max_chars.value(), 8, 256)); }; - max_chars.set_callback(resize); - resize(); -- container->append(label); -+ button.add_css_class("flat"); -+ button.add_css_class("window-title-button"); -+ button.set_child(label); -+ button.set_focusable(false); -+ button.signal_clicked().connect(sigc::mem_fun(*this, &WayfireWindowTitle::toggle_desktop)); -+ container->append(button); - tracker = WindowTitleTracker::get(); - changed = tracker->changed.connect([this] { refresh(); }); -+ show_desktop.set_callback([this] { refresh(); }); -+ auto ipc = WayfirePanelApp::get().get_ipc_server_instance(); -+ if (ipc && ipc->connected) action_client = ipc->create_client(); - refresh(); - } - - void WayfireWindowTitle::refresh() - { -- const auto title = shell::select_window_title(tracker->outputs, tracker->views, desktop, region); -+ const bool hidden = shell::desktop_is_shown(tracker->desktops, desktop, region); -+ const auto title = hidden ? "Desktop" : shell::select_window_title(tracker->outputs, tracker->views, desktop, region); - // Titles are plain text, including literal '<', '&' and markup-looking text. - label.set_text(title); -- label.set_tooltip_text(title); -+ button.set_sensitive(show_desktop.value() && action_client && tracker->desktops.is_array()); -+ button.set_tooltip_text(title + "\n" + (hidden ? "Click to restore windows on this monitor" : -+ "Click to show desktop on this monitor")); - } -+ -+void WayfireWindowTitle::toggle_desktop() -+{ -+ if (!action_client || !show_desktop.value()) return; -+ wf::json_t message; -+ message["method"] = "panel-desktop/toggle"; -+ message["data"]["output"] = desktop; -+ message["data"]["duration"] = std::clamp(duration.value(), 0, 2000); -+ if (region) -+ { -+ message["data"]["region"]["x"] = int64_t(region->x); -+ message["data"]["region"]["y"] = int64_t(region->y); -+ message["data"]["region"]["width"] = int64_t(region->width); -+ message["data"]["region"]["height"] = int64_t(region->height); -+ } -+ action_client->send(message.serialize(), [this] (wf::json_t response) -+ { -+ if (response["error"].is_string()) button.set_tooltip_text(response["error"].as_string()); -+ tracker->refresh(); -+ }); -+} ---- a/src/panel/widgets/window-title.hpp -+++ b/src/panel/widgets/window-title.hpp -@@ -3,18 +3,25 @@ - #include "widget.hpp" - #include "output-topology.hpp" - #include <gtkmm/label.h> -+#include <gtkmm/button.h> - - class WindowTitleTracker; -+class IPCClient; - - class WayfireWindowTitle : public WayfireWidget - { - const std::string desktop; - const std::optional<shell::OutputGeometry> region; - Gtk::Label label; -+ Gtk::Button button; - WfOption<int> max_chars{"panel/window_title_max_chars"}; -+ WfOption<bool> show_desktop{"panel/window_title_show_desktop"}; -+ WfOption<int> duration{"panel/window_title_desktop_duration"}; - std::shared_ptr<WindowTitleTracker> tracker; -+ std::shared_ptr<IPCClient> action_client; - sigc::connection changed; - void refresh(); -+ void toggle_desktop(); - - public: - WayfireWindowTitle(std::string desktop, std::optional<shell::OutputGeometry> region); ---- a/src/util/window-title.cpp -+++ b/src/util/window-title.cpp -@@ -4,6 +4,27 @@ - - namespace shell - { -+bool desktop_is_shown(const wf::json_reference_t& states, const std::string& desktop, -+ const std::optional<OutputGeometry>& region) -+{ -+ if (!states.is_array()) return false; -+ for (size_t i = 0; i < states.size(); ++i) -+ { -+ auto state = states[i]; -+ if (!state["output"].is_string() || state["output"].as_string() != desktop || -+ !state["hidden"].is_bool() || !state["hidden"].as_bool()) continue; -+ if (!region) -+ { -+ if (state["monitor"].is_string() && state["monitor"].as_string() == desktop) return true; -+ continue; -+ } -+ auto box = state["region"]; -+ if (ipc_number(box["x"]) == region->x && ipc_number(box["y"]) == region->y && -+ ipc_number(box["width"]) == region->width && ipc_number(box["height"]) == region->height) return true; -+ } -+ return false; -+} -+ - static bool flag(const wf::json_reference_t& value) - { - return value.is_bool() && value.as_bool(); ---- a/src/util/window-title.hpp -+++ b/src/util/window-title.hpp -@@ -4,6 +4,8 @@ - - namespace shell - { -+bool desktop_is_shown(const wf::json_reference_t& states, const std::string& desktop, -+ const std::optional<OutputGeometry>& region = std::nullopt); - // View geometry is relative to the current viewport of its desktop output. - // A member owns windows whose centers lie inside its region (half-open bounds). - std::string select_window_title(const wf::json_reference_t& outputs, ---- a/metadata/panel.xml -+++ b/metadata/panel.xml -@@ -44,6 +44,16 @@ - <_short>Maximum window title width in characters</_short> - <_long>The window-title widget shows the most recently focused visible window on this monitor. Long titles are ellipsized; the tooltip shows the full title.</_long> - <default>72</default><min>8</min><max>256</max> -+ </option> -+ <option name="window_title_show_desktop" type="bool"> -+ <_short>Click window title to show desktop</_short> -+ <_long>Slide this physical monitor's windows upward; click again to restore their layout. Requires the panel-desktop Wayfire plugin.</_long> -+ <default>true</default> -+ </option> -+ <option name="window_title_desktop_duration" type="int"> -+ <_short>Show desktop slide duration (ms)</_short> -+ <_long>Use 0 to show and restore the desktop without animation.</_long> -+ <default>280</default><min>0</min><max>2000</max> - </option> - <option name="widgets_center" type="string"> - <_short>Widgets Center</_short> ---- a/tests/window-title.cpp -+++ b/tests/window-title.cpp -@@ -101,5 +101,17 @@ - assert(shell::select_window_title(parse("null"),views,"VIRTUAL-1") == "Desktop"); - assert(title(parse("[]"),left) == "Desktop"); - assert(title(parse(R"({"error":"IPC unavailable"})"),left) == "Desktop"); -- std::cout << "Per-monitor title selection passed\n"; -+ auto states = parse(R"([ -+ {"output":"VIRTUAL-1","monitor":"DP-1","hidden":true,"region":{"x":0,"y":0,"width":3840,"height":2160}}, -+ {"output":"VIRTUAL-1","monitor":"DP-2","hidden":false,"region":{"x":3840,"y":0,"width":3840,"height":2160}} -+ ])"); -+ assert(shell::desktop_is_shown(states,"VIRTUAL-1",left)); -+ assert(!shell::desktop_is_shown(states,"VIRTUAL-1",middle)); -+ assert(!shell::desktop_is_shown(states,"VIRTUAL-1",right)); -+ assert(!shell::desktop_is_shown(states,"HDMI-A-1",left)); -+ assert(!shell::desktop_is_shown(states,"VIRTUAL-1")); -+ assert(!shell::desktop_is_shown(parse(R"({"error":"Plugin unavailable"})"),"VIRTUAL-1",left)); -+ states.append(parse(R"({"output":"VIRTUAL-1","monitor":"VIRTUAL-1","hidden":true})")); -+ assert(shell::desktop_is_shown(states,"VIRTUAL-1")); -+ std::cout << "Per-monitor title selection and show-desktop state passed\n"; - } ---- /dev/null -+++ b/docs/PANEL-DESKTOP.md -@@ -0,0 +1,54 @@ -+# panel-desktop -+ -+Wayfire plugin for the physical-monitor Show Desktop buttons in wf-shell's -+window-title widget. It uses the existing compositor plugin API; no core patch -+is added. Requires this system's Wayfire r10 physical-output scope support. -+ -+Click a panel title to slide that monitor's current-workspace windows upward. -+Click Desktop to restore them. Real windows stay in their original scene and -+Simple Tile trees, temporarily disabled; a noninteractive snapshot supplies the -+short slide animation. Geometry, tiling, fullscreen/maximize, minimized state and -+above-state are not changed by the action. Previously minimized windows are not -+included. Transient dialogs follow their parent. -+ -+External activation, a new window on that monitor, workspace switches, output -+layout changes, window closure, and plugin unloading restore affected windows. -+An unchanged output-layout notification does not cancel the desktop state. Each -+monitor can be toggled independently. Animation snapshots are released when the -+slide ends. A 0 ms duration disables animation. -+ -+Physical-scope windows follow their assigned connector. Other windows belong to -+the region containing their center, matching the title widget's ownership rule. -+A VIRTUAL-spanning window is one window: hiding it from its owning monitor hides -+its whole surface, including portions on other monitors. No per-monitor slicing -+of application windows is performed. -+ -+IPC methods: -+ -+- `panel-desktop/toggle`: `output` is the logical output name. Optional `region` -+ is an exact current physical member rectangle with integer x/y/width/height. -+ Without region, the action addresses the entire logical output. `duration` -+ is milliseconds, 0–2000, default 280. -+- `panel-desktop/status`: array of active groups, including output, monitor, -+ region, hidden, animating and the view IDs owned by that group. -+ -+wf-shell options: -+ -+``` -+[panel] -+window_title_show_desktop = true -+window_title_desktop_duration = 280 -+``` -+ -+Enable `panel-desktop` alongside `ipc` and `ipc-rules` in Wayfire's core/plugins. -+The panel button updates through the existing shared event-driven title tracker. -+No polling service is added. It stays unavailable when the plugin is absent. -+ -+The built-in `wm-actions/toggle_showdesktop` remains separate: it minimizes and -+restores windows across the logical output. A binding such as `<super> KEY_D` -+can be configured there. This package does not assign or change that binding. -+ -+Build: `meson setup build --prefix=/usr && meson compile -C build`. -+Gentoo: gui-libs/wayfire-panel-desktop-0.1.0 in the personal overlay ships the source -+snapshot and installs libpanel-desktop.so plus its metadata. wf-shell r9 adds the -+matching title-button controls. diff --git a/gui-apps/wf-shell/files/wf-shell-0.11.0-r8-shell-experience.patch b/gui-apps/wf-shell/files/wf-shell-0.11.0-r8-shell-experience.patch deleted file mode 100644 index 1d52b7830ed0..000000000000 --- a/gui-apps/wf-shell/files/wf-shell-0.11.0-r8-shell-experience.patch +++ /dev/null @@ -1,4708 +0,0 @@ -diff --git a/README.md b/README.md -index 4284829..6d958ec 100644 ---- a/README.md -+++ b/README.md -@@ -50,3 +50,8 @@ sudo ninja -C build install - - Past default look: -  -+ -+## Shell experience development -+ -+The optional control center, media widget, notification improvements, and Mist -+preset are described in [the implementation notes](docs/SHELL-EXPERIENCE.md). -diff --git a/data/css/meson.build b/data/css/meson.build -index 3b14dc0..6eb1d4d 100644 ---- a/data/css/meson.build -+++ b/data/css/meson.build -@@ -10,4 +10,5 @@ install_data('panel-selectors.css', install_dir: css_path) - install_data('panel-tray-zoom-hover.css', install_dir: css_path) - install_data('panel-volume-zoom-hover.css', install_dir: css_path) - install_data('panel-window-list-zoom-hover.css', install_dir: css_path) --install_data('user-font.css', install_dir: css_path) -\ No newline at end of file -+install_data('user-font.css', install_dir: css_path) -+install_data('wayfire-mist.css', install_dir: css_path) -diff --git a/data/css/wayfire-mist.css b/data/css/wayfire-mist.css -new file mode 100644 -index 0000000..49afd37 ---- /dev/null -+++ b/data/css/wayfire-mist.css -@@ -0,0 +1,69 @@ -+/* Wayfire Mist: shared semantic colors, matching the local Noctalia palette. */ -+@define-color wf_surface #000000; -+@define-color wf_card #131b18; -+@define-color wf_text #d8e2de; -+@define-color wf_muted #a2b1ab; -+@define-color wf_accent #91b8aa; -+@define-color wf_on_accent #07120f; -+@define-color wf_outline #293b35; -+@define-color wf_hover #253831; -+ -+.wf-panel, .wf-panel popover, .wf-dock { -+ font-family: "Liberation Sans"; -+ color: @wf_text; -+} -+.wf-panel { -+ background: alpha(@wf_surface, 0.94); -+ border: 1px solid @wf_outline; -+ border-radius: 12px; -+ padding: 4px 12px; -+} -+.wf-panel .left, .wf-panel .right, .wf-panel .center { border-spacing: 8px; } -+.wf-panel .wf-menu { padding: 4px 8px; border-radius: 10px; } -+.wf-panel .wf-menu:hover, .wf-panel .wf-menu.selected { background: @wf_hover; } -+.wf-panel popover > contents { -+ background: @wf_card; -+ color: @wf_text; -+ border: 1px solid @wf_outline; -+ border-radius: 16px; -+ padding: 16px; -+} -+.control-center-content, .media-controls, .notification-center-content { padding: 4px; } -+.control-center-content .title-2 { font-size: 1.3em; font-weight: bold; } -+.wf-panel .heading { font-weight: bold; } -+.wf-panel .dim-label { color: @wf_muted; } -+.wf-panel .quick-setting { -+ background: @wf_surface; -+ border: 1px solid @wf_outline; -+ border-radius: 12px; -+ padding: 12px 8px; -+} -+.wf-panel .quick-setting image { -gtk-icon-size: 24px; color: @wf_accent; } -+.wf-panel button, .wf-panel .wf-menu { transition: background-color 160ms ease; } -+.wf-panel button:hover { background: @wf_hover; } -+.wf-panel button:checked, .wf-panel scale highlight { -+ background: @wf_accent; -+ color: @wf_on_accent; -+} -+.wf-panel button:focus-visible, .wf-panel entry:focus-within { -+ outline: 2px solid @wf_accent; -+ outline-offset: 2px; -+} -+.wf-panel .notification { padding: 10px; border-radius: 10px; background: @wf_surface; } -+.wf-panel .app-button { padding: 6px; border-radius: 8px; } -+.wf-panel .app-category { border-radius: 8px; } -+.wf-panel .clock { font-weight: bold; } -+.wf-panel .workspace-switcher { background: transparent; } -+.wf-panel .workspace-switcher .workspace.active { background: @wf_accent; } -+.wf-panel .workspace-switcher .workspace.inactive { background: @wf_outline; } -+.wf-dock .box { background: alpha(@wf_card, 0.94); border: 1px solid @wf_outline; } -+.wf-dock button.activated { border-color: @wf_accent; } -+ -+.wf-panel button, .wf-panel entry, .wf-panel combobox { -+ color: @wf_text; -+ background: @wf_card; -+} -+.wf-panel .quick-setting { background-color: @wf_surface; } -+.wf-panel button:hover { background-color: @wf_hover; } -+.wf-panel button:checked { background-color: @wf_accent; color: @wf_on_accent; } -+.wf-panel button:disabled { color: @wf_muted; opacity: 0.55; } -diff --git a/data/presets/wayfire-mist.ini b/data/presets/wayfire-mist.ini -new file mode 100644 -index 0000000..f2cea99 ---- /dev/null -+++ b/data/presets/wayfire-mist.ini -@@ -0,0 +1,34 @@ -+# Preview preset. Pass with wf-panel -c /path/to/this/file -s /path/to/css-directory. -+# Use compositor scale 1; UI sizing is independent of game/display resolution. -+[panel] -+widgets_left = menu spacing4 workspace-switcher -+widgets_center = clock -+widgets_right = tray media notifications control-center -+position = top -+output_mode = all -+outputs = * -+minimal_height = 48 -+edge_margin = 16 -+background_color = rgba(0, 0, 0, 0.94) -+menu_list = true -+menu_fuzzy_search = true -+menu_show_categories = false -+menu_min_content_width = 420 -+menu_min_content_height = 480 -+menu_item_icon_size = 32 -+menu_icon_size = 32 -+tray_icon_size = 28 -+notifications_icon_size = 28 -+clock_font = Liberation Sans Bold 18 -+control_center_width = 440 -+# Standalone notifications widget owns the inbox; no duplicate inbox in this preset. -+control_center_notifications = false -+# This machine uses ConnMan. Override for other network managers. -+control_center_network_command = connman-gtk -+control_center_bluetooth_command = blueman-manager -+control_center_appearance_command = wf-appearance -+mixer_layout = vertical -+mixer_slider_length = 280 -+mixer_popup_on_change = false -+brightness_slider_length = 280 -+brightness_popup_on_change = false -diff --git a/docs/OUTPUT-TARGETS.md b/docs/OUTPUT-TARGETS.md -new file mode 100644 -index 0000000..d0de688 ---- /dev/null -+++ b/docs/OUTPUT-TARGETS.md -@@ -0,0 +1,113 @@ -+# Desktop outputs and physical regions -+ -+wf-panel consumes `window-rules/list-outputs` and its optional `members` -+metadata. One tracker subscribes to output addition, removal and layout changes. -+IPC metadata is intersected with the live GDK monitor list; connector prefixes -+are never used to identify a combined desktop. -+ -+## Configuration -+ -+One fixed bar per physical member in a horizontal stitched desktop: -+ -+```ini -+[panel] -+output_mode = physical -+outputs = DP-1 DP-2 DP-3 -+position = top -+full_span = true -+autohide = false -+edge_margin = 15 -+side_margin = 15 -+mirror_margin = false -+``` -+ -+This gives each bar top/right/bottom/left spacing of **15/15/0/15**. `edge_margin` -+is the outer-edge gap, `side_margin` supplies the left/right gaps, and -+`mirror_margin=false` avoids adding an extra gap between the bar and workarea. -+These values are logical pixels; at compositor scale 1 they are physical pixels. -+Settings reload live. Each physical bar also receives its connector name as a -+CSS class, so `.wf-panel.DP-2` can style one monitor's bar. -+ -+| Mode | Target | Rendering | -+| --- | --- | --- | -+| `all` (default) | Every application-visible Wayland desktop output | One bar per desktop; no IPC required | -+| `virtual` | Combined desktops with nonempty `members` metadata | One bar across each combined desktop | -+| `physical` | Standalone desktop outputs and physical member regions | One bar per target; member limits below | -+ -+`outputs` filters targets using comma/whitespace-separated exact connector names, -+or `*`. `DP-1` never matches `DP-10`; partial wildcards are unsupported. -+`virtual` and `physical` require the Wayfire `ipc` and `ipc-rules` plugins. -+Unavailable metadata produces a diagnostic instead of guessing. Masked -+connectors without a desktop/member receive no bar. Standalone headless/network -+outputs cannot currently be distinguished from hardware by this metadata. -+ -+## Horizontal member placement -+ -+A member bar is a separate layer-shell surface on its parent's real `wl_output`. -+Positive margins derived from member geometry place it within the monitor. -+No fake output or compositor patch is needed. The leftmost selected member owns -+one shared exclusive zone, sized to the tallest bar. Other member surfaces use -+exclusive zone `-1` to ignore that shared reservation and retain their own outer -+edge placement. Removal transfers the reservation to the remaining leftmost bar. -+Workspace widgets continue to control the parent's shared workspace set. -+ -+Supported members occupy the full height of their parent and are laid out -+horizontally. Bars must use top/bottom position, `full_span=true`, and -+`autohide=false`. Unsupported configurations are skipped with a diagnostic. -+Ordinary standalone outputs retain normal layer-shell/autohide behavior. -+The workarea is conservatively reserved across the whole parent desktop, -+including unselected members. Other clients reserving the same edge are outside -+this placement model. A shared dock on the opposite edge can coexist with it. -+ -+Widget popovers are sized and horizontally positioned within their member bar. -+The menu shortcut queries `window-rules/get_cursor_position` and opens the -+selected physical member or desktop output under the pointer. Pressing it again -+on that monitor closes the menu; moving the pointer to another selected monitor -+and pressing it moves the menu there. If cursor IPC is unavailable, it falls -+back to the leftmost selected bar on the protocol's target output. Clicking a -+launcher opens that monitor's own menu. Escape and outside clicks dismiss Apps, -+including clicks on empty desktop areas. While Apps is open, transparent input -+surfaces cover the desktop below its overlay-layer menu; they reserve no space -+and disappear when it closes. Their transparency overrides global GTK window -+background rules so opening Apps cannot cover the applications underneath. -+Brightness/output controls -+still receive the parent output; member-specific DDC routing is future work. -+wf-dock and wf-background retain their existing per-desktop behavior. -+ -+## Per-monitor window titles -+ -+Add `window-title` to `widgets_center` to show the most recently focused visible -+window for each selected monitor. A window belongs to the member containing its -+center; full-desktop bars consider the entire desktop output. Minimized, -+unmapped, off-workspace and shell surfaces are excluded. With no candidate the -+label reads `Desktop`; an untitled window falls back to its application ID. -+ -+One shared IPC tracker reacts to focus, title, geometry, workspace and output -+changes. The label uses plain text, including literal markup characters. Long -+titles are ellipsized, with the full title in the tooltip. Set -+`window_title_max_chars` (default 72) to limit width, and style `.window-title` -+with CSS. This is a read-only title label; it has no show-desktop click action. -+ -+## Future compositor interface -+ -+Arbitrary stacked/irregular regions, independent region workareas and region -+hotspots/autohide still need compositor-owned attachment semantics. That API -+should expose member identity/geometry, region-local anchors, popup constraints, -+reservations and events for regrouping/removal. No such API is fabricated here, -+and the concurrently developed Wayfire source tree is left untouched. -+ -+## Validation -+ -+`output-topology-test` covers integer/real geometry, exact filtering, -+name-independent classification, unavailable metadata, member placement/gaps, -+fractional boundary rounding, unsupported geometry and interleaved GDK/IPC -+removal. Private headless Wayfire checks exercised three member bars, the single -+workarea reservation, unequal bar heights, selected-member removal/ownership -+transfer, live physical/virtual switching, top/bottom placement, unsupported -+autohide rejection and a launcher staying inside its member. -+Menu checks cover pointer-selected shortcuts, repeated toggles, Escape with and -+without search text, clicks inside the search field, outside clicks on a bar, -+an application, empty desktop space or another member, and dismiss-surface cleanup. -+Visibility regression checks apply global GTK window-background CSS and compare -+application pixels, mapped/minimized state and geometry before, during and after -+opening Apps on a stitched three-member desktop. -diff --git a/docs/SHELL-EXPERIENCE.md b/docs/SHELL-EXPERIENCE.md -new file mode 100644 -index 0000000..564305e ---- /dev/null -+++ b/docs/SHELL-EXPERIENCE.md -@@ -0,0 +1,110 @@ -+# Shell experience: first implementation -+ -+The target is wf-shell as the everyday Wayfire shell, informed by Noctalia's -+control center, unified styling, and launcher, and Quickshell's reusable services -+and live iteration. Keep native GTK4 components and existing Wayfire protocols. -+The companion `wf-appearance` owns app appearance policy; compositor work remains -+in the separate Wayfire project. -+ -+## Implemented -+ -+- `window-title` widget: a centered, per-monitor recent-window title using one -+ shared IPC event stream, with workspace filtering, ellipsis and full tooltip. -+ -+- `control-center` widget with Audio, Display, Media, and optional Inbox pages. -+ Audio embeds the existing WirePlumber mixer: outputs, inputs, application -+ streams, mute, volume, and default-device selection. Display embeds existing -+ sysfs/DDC controls. No duplicate audio or brightness backend. -+- `media` widget and shared MPRIS service with player discovery/removal/restart, -+ metadata, player selection, previous/play/pause/next, capability checks, and -+ asynchronous error reporting. All output widgets share the same service. -+- Notification inbox with visible DND, clear-all, count, and empty state. DND is -+ shared across monitors. Closed notifications leave the daemon's map; clearing -+ them no longer leaves stale entries. Deferred row destruction avoids removing -+ a widget inside its own click callback. No replacement of another notification -+ daemon's bus ownership. -+- Optional Wayfire Mist CSS and a list/fuzzy-search launcher preset. The launcher -+ search engine already existed; the preset selects and styles it. -+- Fix for `--css` accidentally returning the config file path instead of the CSS -+ directory (which crashed previews with both options). -+- `wf-appearance` is a separate prototype at `/root/projects/wf-appearance`. -+ The control-center Appearance shortcut defaults to `wf-appearance` in PATH. -+- Metadata-driven `panel/output_mode` (`all`, `virtual`, `physical`) with exact -+ connector filtering and output lifecycle tracking. Horizontal combined-output -+ members support separate fixed bars with one shared workarea reservation; see -+ [output selection and remaining compositor requirements](OUTPUT-TARGETS.md). -+- The 0.11 release already accepts integer and real IPC geometry; those upstream -+ fixes are retained. Monitor removal handles an already-absent -+ monitor without dereferencing the end iterator. Output listeners survive panel -+ recreation, and config reload watches atomic file replacements from wf-appearance. -+ -+Network and Bluetooth tiles currently launch their settings applications. The -+local preset selects `connman-gtk` and `blueman-manager`; this is not native -+ConnMan or Bluetooth management. Configuration command fields accept executable -+arguments with quoting, not shell syntax. -+ -+## Preview and validation -+ -+The active development tree is `wf-shell-0.11.0`, based on the official release -+archive plus Gentoo's network null-pointer fix. Its baseline commit is `83fe5d6`. -+The earlier 77038bdf snapshot is retained as the original prototype. The release -+keeps its refactored launcher and popover API; the new widgets use that API. No compositor source, -+running Noctalia configuration, game settings, or display scale was changed. -+ -+``` -+meson setup build --prefix=/root/projects/wf-shell-polish/preview \ -+ -Dwayland-logout=false -Dweather=false -Dtests=true -+ninja -C build -+meson test -C build --print-errorlogs -+``` -+ -+The archive needs wf-json and gvc subproject sources. This machine's cached -+Gentoo distfiles supplied wf-json 70039e13 and gvc 5f9768a2 (the same pins as the -+installed package). GTK4, gtkmm, wf-config, Wayfire headers, WirePlumber, and -+DDC dependencies are installed. -+ -+The staged panel is `../preview/bin/wf-panel`. Its resource prefix is the same -+preview directory. After rebuilding, copy the panel and changed metadata there -+before previewing. Use a nested compositor/session for coexistence testing: -+ -+``` -+../preview/bin/wf-panel -c "$PWD/data/presets/wayfire-mist.ini" \ -+ -s "$PWD/build/preview-css" -+``` -+ -+`preview-css` contains only `wayfire-mist.css`; `data/css` contains many unrelated -+example styles and should not be used as the full CSS directory. -+The preset uses the standalone notifications widget; set -+`control_center_notifications=true` and restart the panel for the embedded inbox. -+Control-center width and shortcut commands are hot-reloaded. -+ -+Validation: full Meson build; isolated session-bus tests for MPRIS discovery, -+capabilities, property updates, reconnect/destruction, notification close/clear, -+and shared/idempotent DND; headless Wayfire rendering of the panel and control -+center; integer/real geometry and output-selection tests; headless live switching -+between all/virtual/physical modes, exact name filtering, and repeated panel -+recreation through atomic config replacements. Hardware audio routing, backlight writes, and physical multi-monitor -+hotplug still need a real-session acceptance pass. -+ -+## Next increments -+ -+The optional [integrated dock](INTEGRATED-DOCK.md) adds pinned launchers, running -+window groups, a perspective shelf, reflections and magnification inside the -+wf-panel process. It is enabled separately from the panel's widgets. -+ -+1. Native ConnMan/NetworkManager adapters, BlueZ status/pair/connect workflows; -+ an explicit unavailable-service state and authentication agent integration. -+2. Persistent notification history separated from toast lifetime, notification -+ expiry/transient/resident semantics, unread tracking, and per-app policy. -+3. Launcher providers (calculator, clipboard, emoji, files), pinned/recent apps, -+ and shell actions with keyboard navigation and a stable shell IPC API. -+4. MPRIS artwork/seek, shell OSDs, session/power controls, and reduced-motion -+ preferences. Add screenshot/accessibility checks for small/vertical outputs. -+5. Appearance service integration into application launching, portal/XSettings -+ precedence diagnostics, Qt palette backends, and per-app overrides. Validate -+ Star Citizen at 3840×2160 with compositor scale 1; app scaling does not repair -+ Wine virtual-desktop, XWayland, gamescope, or game resolution configuration. -+ -+References: https://docs.noctalia.dev/noctalia/control-center/, -+https://quickshell.org/, -+https://specifications.freedesktop.org/mpris-spec/latest/. -diff --git a/meson.build b/meson.build -index eda15dc..675d1b3 100644 ---- a/meson.build -+++ b/meson.build -@@ -84,3 +84,7 @@ subdir('metadata') - subdir('proto') - subdir('data') - subdir('src') -+ -+if get_option('tests') -+ subdir('tests') -+endif -diff --git a/meson_options.txt b/meson_options.txt -index 3c50471..4a625fb 100644 ---- a/meson_options.txt -+++ b/meson_options.txt -@@ -28,3 +28,4 @@ option( - value: false, - description: 'Install weather widgets and open-weather-fetch systemd service', - ) -+option('tests', type: 'boolean', value: false, description: 'Build isolated D-Bus service tests') -diff --git a/metadata/dock.xml b/metadata/dock.xml -index e04d975..a77913e 100644 ---- a/metadata/dock.xml -+++ b/metadata/dock.xml -@@ -3,6 +3,38 @@ - <plugin name="dock"> - <_short>Dock</_short> - <category>Shell</category> -+ <option name="enabled" type="bool"> -+ <_short>Run the integrated dock inside wf-panel</_short> -+ <_long>A centered bottom dock with launchers, running applications, magnification and reflections. Do not start wf-dock separately when using this option. Requires bottom position and full_span=false.</_long> -+ <default>false</default> -+ </option> -+ <option name="output" type="string"> -+ <_short>Integrated dock output</_short> -+ <_long>One compositor-visible output name, such as VIRTUAL-1. auto selects the first output hosting a panel. On a stitched desktop the dock is centered across the virtual output.</_long> -+ <default>auto</default> -+ </option> -+ <option name="launchers" type="string"> -+ <_short>Integrated dock launchers</_short> -+ <_long>Space-separated desktop file IDs or absolute paths; use | for a separator. Missing applications are skipped. Running apps follow pinned launchers.</_long> -+ <default></default> -+ </option> -+ <option name="layer" type="string"> -+ <_short>Integrated dock stacking layer</_short> -+ <_long>top uses the regular panel layer. overlay keeps the dock above application windows, including fullscreen windows. This option applies to the integrated dock in wf-panel.</_long> -+ <default>top</default> -+ <desc><value>top</value><_name>Top</_name></desc> -+ <desc><value>overlay</value><_name>Overlay</_name></desc> -+ </option> -+ <option name="zoom" type="double"> -+ <_short>Integrated dock maximum magnification</_short> -+ <default>1.5</default> -+ <min>1.0</min><max>2.0</max> -+ </option> -+ <option name="reflection" type="double"> -+ <_short>Integrated dock reflection opacity</_short> -+ <default>0.435</default> -+ <min>0.0</min><max>1.0</max> -+ </option> - <option name="css_path" type="string"> - <_short>CSS Path</_short> - <default></default> -diff --git a/metadata/panel.xml b/metadata/panel.xml -index 6da3f72..6e47873 100644 ---- a/metadata/panel.xml -+++ b/metadata/panel.xml -@@ -3,6 +3,33 @@ - <plugin name="panel"> - <_short>Panel</_short> - <category>Shell</category> -+ <group> -+ <_short>Control center</_short> -+ <option name="control_center_icon_size" type="int"> -+ <_short>Control center icon size</_short><default>28</default><minimum>1</minimum> -+ </option> -+ <option name="media_icon_size" type="int"> -+ <_short>Media icon size</_short><default>28</default><minimum>1</minimum> -+ </option> -+ <option name="control_center_width" type="int"> -+ <_short>Control center width</_short> -+ <default>420</default><minimum>360</minimum><maximum>800</maximum> -+ </option> -+ <option name="control_center_notifications" type="bool"> -+ <_short>Include notification inbox (restart panel to change)</_short> -+ <default>true</default> -+ </option> -+ <option name="control_center_network_command" type="string"> -+ <_short>Network settings command</_short><default>nm-connection-editor</default> -+ </option> -+ <option name="control_center_bluetooth_command" type="string"> -+ <_short>Bluetooth settings command</_short><default>blueman-manager</default> -+ </option> -+ <option name="control_center_appearance_command" type="string"> -+ <_short>Appearance and scaling settings command</_short><default>wf-appearance</default> -+ </option> -+ </group> -+ - <group> - <_short>General</_short> - <option name="css_path" type="string"> -@@ -13,6 +40,11 @@ - <_short>Widgets Left</_short> - <default>menu spacing4 launchers window-list</default> - </option> -+ <option name="window_title_max_chars" type="int"> -+ <_short>Maximum window title width in characters</_short> -+ <_long>The window-title widget shows the most recently focused visible window on this monitor. Long titles are ellipsized; the tooltip shows the full title.</_long> -+ <default>72</default><min>8</min><max>256</max> -+ </option> - <option name="widgets_center" type="string"> - <_short>Widgets Center</_short> - <default>none</default> -@@ -68,9 +100,17 @@ If full_span is off, both sides of the panel will take the same amount of space, - <_long>Amount of time (in milliseconds) the cursor needs to be out of the zone before hiding.</_long> - <default>500</default> - </option> -+ <option name="output_mode" type="string"> -+ <_short>Output targets</_short> -+ <_long>all: every Wayland desktop output; virtual: combined outputs identified by IPC member metadata; physical: standalone outputs and combined-output member regions. Member panels support horizontal full-height regions with full_span=true, autohide=false and top/bottom position. virtual and physical require ipc and ipc-rules.</_long> -+ <default>all</default> -+ <desc><value>all</value><_name>All desktop outputs</_name></desc> -+ <desc><value>virtual</value><_name>Combined virtual outputs</_name></desc> -+ <desc><value>physical</value><_name>Physical outputs</_name></desc> -+ </option> - <option name="outputs" type="string"> - <_short>Outputs on which to show a panel instance</_short> -- <_long>A comma separated list of output names on which to show panel instances. Set to * wildcard for all outputs.</_long> -+ <_long>A comma or whitespace separated list of exact target names, filtered by output_mode. Set to * for every eligible target. In physical mode, member connector names are used.</_long> - <default>*</default> - </option> - <option name="position" type="string"> -@@ -93,6 +133,12 @@ If full_span is off, both sides of the panel will take the same amount of space, - <_name>Right</_name> - </desc> - </option> -+ <option name="side_margin" type="int"> -+ <_short>Horizontal side margin</_short> -+ <_long>Left and right gaps for horizontal panels, relative to each selected output or physical member.</_long> -+ <default>0</default> -+ <min>0</min> -+ </option> - <option name="edge_margin" type="int"> - <_short>Edge margin</_short> - <_long>Space between the panel the edge</_long> -diff --git a/src/panel/meson.build b/src/panel/meson.build -index ee51c0a..31635c7 100644 ---- a/src/panel/meson.build -+++ b/src/panel/meson.build -@@ -6,7 +6,10 @@ widget_sources = [ - 'widgets/menu/category.cpp', - 'widgets/menu/categorybutton.cpp', - 'widgets/menu/item.cpp', -+ 'widgets/media.cpp', -+ 'widgets/control-center.cpp', - 'widgets/clock.cpp', -+ 'widgets/window-title.cpp', - 'widgets/command-output.cpp', - 'widgets/language.cpp', - 'widgets/launchers.cpp', -@@ -89,7 +92,7 @@ endif - - executable( - 'wf-panel', -- ['panel.cpp'] + widget_sources, -+ ['panel.cpp', 'integrated-dock.cpp'] + widget_sources, - dependencies: deps, - install: true, --) -\ No newline at end of file -+) -diff --git a/src/panel/output-tracker.hpp b/src/panel/output-tracker.hpp -new file mode 100644 -index 0000000..af476dc ---- /dev/null -+++ b/src/panel/output-tracker.hpp -@@ -0,0 +1,71 @@ -+#pragma once -+ -+#include "output-topology.hpp" -+#include "wf-ipc.hpp" -+ -+// One subscription/query stream for the panel process, regardless of bar count. -+class PanelOutputTracker : public IIPCSubscriber -+{ -+ std::shared_ptr<IPCClient> client; -+ sigc::connection pending; -+ std::function<void()> changed; -+ bool in_flight = false, refresh_again = false; -+ -+ public: -+ std::optional<shell::OutputTopology> topology; -+ -+ PanelOutputTracker(std::shared_ptr<WayfireIPC> ipc, std::function<void()> changed) : -+ changed(std::move(changed)) -+ { -+ if (ipc->connected) -+ { -+ client = ipc->create_client(); -+ client->subscribe(this, {"output-added", "output-removed", "output-layout-changed"}); -+ refresh(); -+ } -+ } -+ -+ ~PanelOutputTracker() -+ { -+ pending.disconnect(); -+ if (client) -+ client->unsubscribe(this); -+ } -+ -+ void on_event(wf::json_t) override -+ { -+ refresh(); -+ } -+ -+ void refresh() -+ { -+ if (!client) -+ return; -+ if (in_flight) -+ { -+ refresh_again = true; -+ return; -+ } -+ if (pending.connected()) -+ return; -+ -+ pending = Glib::signal_idle().connect([this] -+ { -+ in_flight = true; -+ wf::json_t request; -+ request["method"] = "window-rules/list-outputs"; -+ client->send(request.serialize(), [this] (wf::json_t response) -+ { -+ in_flight = false; -+ topology = shell::parse_output_topology(response); -+ changed(); -+ if (refresh_again) -+ { -+ refresh_again = false; -+ refresh(); -+ } -+ }); -+ return false; -+ }); -+ } -+}; -diff --git a/src/panel/panel.cpp b/src/panel/panel.cpp -index eef0910..e1c9ff8 100644 ---- a/src/panel/panel.cpp -+++ b/src/panel/panel.cpp -@@ -1,3 +1,6 @@ -+#include "output-tracker.hpp" -+#include "integrated-dock.hpp" -+#include "ipc-geometry.hpp" - #include <glibmm/main.h> - #include <gtkmm/window.h> - #include <gtkmm/headerbar.h> -@@ -19,7 +22,10 @@ - #include "widgets/command-output.hpp" - #include "widgets/language.hpp" - #include "widgets/menu/menu.hpp" -+#include "widgets/media.hpp" -+#include "widgets/control-center.hpp" - #include "widgets/clock.hpp" -+#include "widgets/window-title.hpp" - #ifdef HAVE_WEATHER - #include "widgets/weather.hpp" - #endif -@@ -53,6 +59,8 @@ class WayfirePanel::impl - WidgetContainer left_widgets, center_widgets, right_widgets; - - WayfireOutput *output; -+ std::optional<shell::OutputGeometry> region; -+ std::shared_ptr<PanelRegionGroup> region_group; - - WfOption<std::string> panel_position{"panel/position"}; - WfOption<bool> center{"panel/center"}; -@@ -151,7 +159,7 @@ class WayfirePanel::impl - - void create_window() - { -- window = std::make_unique<WayfireAutohidingWindow>(output, "panel"); -+ window = std::make_unique<WayfireAutohidingWindow>(output, "panel", region, region_group); - - window->add_css_class("wf-panel"); - panel_layer.set_callback(set_panel_layer); -@@ -200,6 +208,16 @@ class WayfirePanel::impl - - Widget widget_from_name(std::string name) - { -+ if (name == "control-center") -+ { -+ return std::make_unique<WayfireControlCenter>(output); -+ } -+ -+ if (name == "media") -+ { -+ return std::make_unique<WayfireMedia>(); -+ } -+ - if (name == "menu") - { - return Widget(new WayfireMenu(output)); -@@ -210,6 +228,11 @@ class WayfirePanel::impl - return Widget(new WayfireLaunchers()); - } - -+ if (name == "window-title") -+ { -+ return std::make_unique<WayfireWindowTitle>(output->monitor->get_connector(), region); -+ } -+ - if (name == "clock") - { - return Widget(new WayfireClock()); -@@ -381,7 +404,8 @@ class WayfirePanel::impl - WfOption<std::string> center_widgets_opt{"panel/widgets_center"}; - - public: -- impl(WayfireOutput *output) : output(output) -+ impl(WayfireOutput *output, std::optional<shell::OutputGeometry> region, -+ std::shared_ptr<PanelRegionGroup> group) : output(output), region(region), region_group(group) - { - create_window(); - } -@@ -457,13 +481,25 @@ class WayfirePanel::impl - } - } - -+ void toggle_menu() -+ { -+ for (auto *widgets : {&left_widgets, ¢er_widgets, &right_widgets}) -+ for (auto& widget : *widgets) -+ if (auto *menu = dynamic_cast<WayfireMenu*>(widget.get())) -+ { -+ menu->toggle_menu(); -+ return; -+ } -+ } -+ - std::shared_ptr<WayfireIPC> get_ipc_server_instance() - { - return panel_app->get_ipc_server_instance(); - } - }; - --WayfirePanel::WayfirePanel(WayfireOutput *output) : pimpl(new impl(output)) -+WayfirePanel::WayfirePanel(WayfireOutput *output, std::optional<shell::OutputGeometry> region, -+ std::shared_ptr<PanelRegionGroup> group) : pimpl(new impl(output, region, group)) - {} - - wl_surface*WayfirePanel::get_wl_surface() -@@ -486,6 +522,11 @@ void WayfirePanel::init_widgets() - pimpl->init_widgets(); - } - -+void WayfirePanel::toggle_menu() -+{ -+ pimpl->toggle_menu(); -+} -+ - void WayfirePanel::set_panel_app(WayfirePanelApp *panel_app) - { - pimpl->set_panel_app(panel_app); -@@ -494,79 +535,209 @@ void WayfirePanel::set_panel_app(WayfirePanelApp *panel_app) - class WayfirePanelApp::impl - { - public: -- std::map<WayfireOutput*, std::unique_ptr<WayfirePanel>> panels; -- WfOption<std::string> *panel_outputs = NULL; -+ using Key = std::pair<WayfireOutput*, std::string>; -+ std::map<Key, std::unique_ptr<WayfirePanel>> panels; -+ std::map<Key, std::optional<shell::OutputGeometry>> regions; -+ std::map<WayfireOutput*, std::shared_ptr<PanelRegionGroup>> groups; -+ std::unique_ptr<WfOption<std::string>> panel_outputs; -+ std::unique_ptr<WfOption<std::string>> output_mode; -+ std::unique_ptr<PanelOutputTracker> output_tracker; -+ std::map<WayfireOutput*, sigc::connection> menu_signals; -+ std::shared_ptr<IPCClient> menu_client; -+ std::string last_output_warning; -+ std::unique_ptr<IntegratedDock> dock; -+ -+ ~impl() -+ { -+ for (auto& [output, connection] : menu_signals) -+ connection.disconnect(); -+ } - }; - -+void WayfirePanelApp::request_menu_toggle(const std::string& fallback_output) -+{ -+ // Resolve against the current panel set when the reply arrives: outputs -+ // or widgets may have been removed while the IPC request was in flight. -+ auto dispatch = [this, fallback_output] (wf::json_t response) -+ { -+ std::optional<double> x, y; -+ if (response["pos"].is_object()) -+ { -+ x = ipc_number(response["pos"]["x"]); -+ y = ipc_number(response["pos"]["y"]); -+ } -+ WayfirePanel *target = nullptr; -+ double fallback_left = 0; -+ for (auto& [key, panel] : priv->panels) -+ { -+ Gdk::Rectangle desktop; -+ key.first->monitor->get_geometry(desktop); -+ const auto& region = priv->regions.at(key); -+ const double left = desktop.get_x() + (region ? region->x : 0); -+ const double top = desktop.get_y() + (region ? region->y : 0); -+ const double width = region ? region->width : desktop.get_width(); -+ const double height = region ? region->height : desktop.get_height(); -+ if (x && y && *x >= left && *x < left + width && *y >= top && *y < top + height) -+ { -+ target = panel.get(); -+ break; -+ } -+ if ((!x || !y) && key.first->monitor->get_connector() == fallback_output && -+ (!target || left < fallback_left)) -+ { -+ target = panel.get(); -+ fallback_left = left; -+ } -+ } -+ if (!target) -+ return; -+ for (auto& [key, panel] : priv->panels) -+ { -+ auto& window = dynamic_cast<WayfireAutohidingWindow&>(panel->get_window()); -+ if (panel.get() != target && window.has_active_popover()) -+ window.get_active_popover()->popdown(); -+ } -+ target->toggle_menu(); -+ }; -+ -+ if (!ipc_server || !ipc_server->connected) -+ { -+ dispatch(wf::json_t{}); -+ return; -+ } -+ if (!priv->menu_client) -+ priv->menu_client = ipc_server->create_client(); -+ wf::json_t request; -+ request["method"] = "window-rules/get_cursor_position"; -+ priv->menu_client->send(request.serialize(), dispatch); -+} -+ - void WayfirePanelApp::on_config_reload() - { - if (!priv->panel_outputs) - { -- priv->panel_outputs = new WfOption<std::string>("panel/outputs"); -+ priv->panel_outputs = std::make_unique<WfOption<std::string>>("panel/outputs"); -+ priv->output_mode = std::make_unique<WfOption<std::string>>("panel/output_mode"); - } - - for (auto& p : priv->panels) - { - p.second->handle_config_reload(); - } -+ if (priv->output_tracker) -+ update_panels(); -+ if (priv->dock) -+ priv->dock->reload(); - } - --bool WayfirePanelApp::panel_allowed_by_config(bool allowed, std::string output_name) -+void WayfirePanelApp::update_dock() - { -- if (allowed) -- { -- return priv->panel_outputs->value().find("*") != std::string::npos || -- priv->panel_outputs->value().find(output_name) != std::string::npos; -- } else -+ if (!ipc_server) return; // GTK activation has not finished yet. -+ WfOption<bool> enabled{"dock/enabled"}, full_span{"dock/full_span"}; -+ WfOption<std::string> selected{"dock/output"}, position{"dock/position"}; -+ WayfireOutput *target = nullptr; -+ if (enabled && position.value() == "bottom" && !full_span) - { -- return priv->panel_outputs->value().find("*") == std::string::npos && -- priv->panel_outputs->value().find(output_name) == std::string::npos; -+ for (auto& output : *get_wayfire_outputs()) -+ { -+ if (selected.value() == output->monitor->get_connector() || -+ (selected.value() == "auto" && std::any_of(priv->panels.begin(), priv->panels.end(), -+ [&] (auto& panel) { return panel.first.first == output.get(); }))) -+ { -+ target = output.get(); -+ break; -+ } -+ } - } -+ if (priv->dock && priv->dock->output() != target) priv->dock.reset(); -+ if (target && !priv->dock) priv->dock = std::make_unique<IntegratedDock>(target); - } - - void WayfirePanelApp::update_panels() - { -- for (auto& o : *get_wayfire_outputs()) -+ std::vector<std::string> visible; -+ for (auto& output : *get_wayfire_outputs()) -+ visible.push_back(output->monitor->get_connector()); -+ -+ const auto selection = shell::select_panel_targets( -+ priv->output_tracker ? priv->output_tracker->topology : std::nullopt, -+ visible, priv->output_mode->value(), priv->panel_outputs->value()); -+ std::map<impl::Key, std::optional<shell::OutputGeometry>> wanted; -+ auto warning = selection.error; -+ WfOption<std::string> position{"panel/position"}; -+ WfOption<bool> autohide{"panel/autohide"}, full_span{"panel/full_span"}; -+ WfOption<int> side_margin{"panel/side_margin"}; -+ for (const auto& target : selection.targets) - { -- auto output = o.get(); -- auto output_name = o->monitor->get_connector(); -- -- if (panel_allowed_by_config(false, output_name)) -+ for (auto& output : *get_wayfire_outputs()) - { -- std::cout << "Removing panel from output: " << output_name << std::endl; -- priv->panels.erase(output); -+ if (output->monitor->get_connector() != target.desktop) -+ continue; -+ if (target.region) -+ { -+ Gdk::Rectangle geometry; -+ output->monitor->get_geometry(geometry); -+ if (autohide || !full_span || (position.value() != "top" && position.value() != "bottom") || -+ !shell::region_panel_margins(*target.region, geometry.get_width(), geometry.get_height(), -+ side_margin)) -+ { -+ warning += " Physical region " + target.name + -+ " requires a horizontal full-height member, full_span=true, autohide=false and top/bottom position."; -+ continue; -+ } -+ } -+ wanted[{output.get(), target.name}] = target.region; - } -+ } - -- const auto it = std::find_if(priv->panels.begin(), priv->panels.end(), -- [&output_name] (const auto& panel) -- { -- return panel.first->monitor->get_connector() == output_name; -- }); -+ if (priv->output_tracker && warning != priv->last_output_warning) -+ { -+ if (!warning.empty()) -+ std::cerr << "wf-panel output selection: " << warning << std::endl; -+ priv->last_output_warning = warning; -+ } - -- if ((it == priv->panels.end()) && panel_allowed_by_config(true, output_name)) -+ for (auto it = priv->panels.begin(); it != priv->panels.end();) -+ { -+ auto found = wanted.find(it->first); -+ if (found == wanted.end() || !(found->second == priv->regions[it->first])) - { -- std::cout << "Adding panel for output: " << output_name << std::endl; -- priv->panels[output] = std::unique_ptr<WayfirePanel>( -- new WayfirePanel(output)); -- -- if (ipc_server) -- { -- priv->panels[output]->handle_config_reload(); -- priv->panels[output]->set_panel_app(this); -- priv->panels[output]->init_widgets(); -- } -+ priv->regions.erase(it->first); -+ it = priv->panels.erase(it); -+ } else -+ ++it; -+ } -+ for (const auto& [key, region] : wanted) -+ { -+ if (priv->panels.count(key)) -+ { -+ dynamic_cast<WayfireAutohidingWindow&>(priv->panels[key]->get_window()).refresh_region_geometry(); -+ continue; - } -+ auto& group = priv->groups[key.first]; -+ if (region && !group) -+ group = std::make_shared<PanelRegionGroup>(); -+ std::cout << "Adding panel for output: " << key.second << std::endl; -+ auto panel = std::make_unique<WayfirePanel>(key.first, region, region ? group : nullptr); -+ panel->get_window().add_css_class(key.second); -+ if (ipc_server) -+ { -+ panel->handle_config_reload(); -+ panel->set_panel_app(this); -+ panel->init_widgets(); -+ } -+ priv->regions[key] = region; -+ priv->panels[key] = std::move(panel); - } -+ update_dock(); - } - - void WayfirePanelApp::on_activate() - { - WayfireShellApp::on_activate(); - -- priv->panel_outputs->set_callback([=] () -- { -- update_panels(); -- }); -+ priv->panel_outputs->set_callback([this] { update_panels(); }); -+ priv->output_mode->set_callback([this] { update_panels(); }); - - if (!ipc_server) - { -@@ -580,7 +751,11 @@ void WayfirePanelApp::on_activate() - p.second->init_widgets(); - } - -- if (priv->panels.empty()) -+ priv->output_tracker = std::make_unique<PanelOutputTracker>(ipc_server, -+ [this] { update_panels(); }); -+ update_panels(); -+ -+ if (priv->panels.empty() && priv->output_mode->value() == "all") - { - std::cout << std::endl << - "WARNING: wf-panel outputs option did not match any outputs, " \ -@@ -611,6 +786,8 @@ void WayfirePanelApp::on_activate() - {"panel/mixer_icon_size", ".mixer .widget-icon"}, - {"panel/mixer_popup_icon_size", ".mute-toggle .default-icon, .default-button .default-icon"}, - {"panel/notifications_icon_size", ".notification-center .widget-icon"}, -+ {"panel/media_icon_size", ".media .widget-icon"}, -+ {"panel/control_center_icon_size", ".control-center .widget-icon"}, - {"panel/tray_icon_size", ".tray-button .widget-icon"}, - {"panel/brightness_icon_size", ".brightness .widget-icon"}, - {"panel/window_list_icon_size", ".toplevel-icon.widget-icon"}, -@@ -639,14 +816,19 @@ std::shared_ptr<WayfireIPC> WayfirePanelApp::get_ipc_server_instance() - - void WayfirePanelApp::handle_new_output(WayfireOutput *output) - { -+ const std::string name = output->monitor->get_connector(); -+ priv->menu_signals[output] = output->toggle_menu_signal().connect( -+ [this, name] { request_menu_toggle(name); }); - update_panels(); -+ if (priv->output_tracker) -+ priv->output_tracker->refresh(); - } - - WayfirePanel*WayfirePanelApp::panel_for_wl_output(wl_output *output) - { - for (auto& p : priv->panels) - { -- if (p.first->wo == output) -+ if (p.first.first->wo == output) - { - return p.second.get(); - } -@@ -657,7 +839,25 @@ WayfirePanel*WayfirePanelApp::panel_for_wl_output(wl_output *output) - - void WayfirePanelApp::handle_output_removed(WayfireOutput *output) - { -- priv->panels.erase(output); -+ if (priv->dock && priv->dock->output() == output) -+ priv->dock.reset(); -+ if (auto it = priv->menu_signals.find(output); it != priv->menu_signals.end()) -+ { -+ it->second.disconnect(); -+ priv->menu_signals.erase(it); -+ } -+ for (auto it = priv->panels.begin(); it != priv->panels.end();) -+ { -+ if (it->first.first == output) -+ { -+ priv->regions.erase(it->first); -+ it = priv->panels.erase(it); -+ } else -+ ++it; -+ } -+ priv->groups.erase(output); -+ if (priv->output_tracker) -+ priv->output_tracker->refresh(); - } - - WayfirePanelApp& WayfirePanelApp::get() -diff --git a/src/panel/panel.hpp b/src/panel/panel.hpp -index a74fd3b..efd2be7 100644 ---- a/src/panel/panel.hpp -+++ b/src/panel/panel.hpp -@@ -10,17 +10,20 @@ - #include "giomm/application.h" - #include "wf-shell-app.hpp" - #include "wf-ipc.hpp" -+#include "wf-autohide-window.hpp" - - class WayfirePanelApp; - class WayfirePanel - { - public: -- WayfirePanel(WayfireOutput *output); -+ WayfirePanel(WayfireOutput *output, std::optional<shell::OutputGeometry> region = std::nullopt, -+ std::shared_ptr<PanelRegionGroup> group = nullptr); - - wl_surface *get_wl_surface(); - Gtk::Window& get_window(); - void handle_config_reload(); - void init_widgets(); -+ void toggle_menu(); - void set_panel_app(WayfirePanelApp *panel_app); - std::shared_ptr<WayfireIPC> get_ipc_server_instance(); - -@@ -45,8 +48,9 @@ class WayfirePanelApp : public WayfireShellApp - void on_activate() override; - void handle_new_output(WayfireOutput *output) override; - void handle_output_removed(WayfireOutput *output) override; -- bool panel_allowed_by_config(bool allowed, std::string output_name); - void update_panels(); -+ void update_dock(); -+ void request_menu_toggle(const std::string& fallback_output); - void on_config_reload() override; - void reload_css(); - std::shared_ptr<WayfireIPC> get_ipc_server_instance(); -diff --git a/src/panel/widgets/brightness/brightness.cpp b/src/panel/widgets/brightness/brightness.cpp -index bf1f699..3a6f6db 100644 ---- a/src/panel/widgets/brightness/brightness.cpp -+++ b/src/panel/widgets/brightness/brightness.cpp -@@ -1,6 +1,5 @@ - #include <gdkmm/monitor.h> - #include <memory> --#include <pulse/proplist.h> - #include <wayland-client-core.h> - #include <wayland-client-protocol.h> - -@@ -86,7 +85,7 @@ void WfLightControl::update_parent_icon() - if (parent->ctrl_this_display.get() == this) - { - parent->update_icon(); -- if (parent->popup_on_change) -+ if (!parent->embedded && parent->popup_on_change) - { - parent->button->popup_timed(parent->popup_timeout * 1000); - } -@@ -105,7 +104,7 @@ void LightManager::rem_widget(WayfireBrightness *widget) - widgets.erase(find(widgets.begin(), widgets.end(), widget)); - } - --WayfireBrightness::WayfireBrightness(WayfireOutput *_output) -+WayfireBrightness::WayfireBrightness(WayfireOutput *_output, bool embedded) : embedded(embedded) - { - output = _output; - } -@@ -191,9 +190,17 @@ void WayfireBrightness::init(Gtk::Box *container) - scroll_gesture->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL); - button->add_controller(scroll_gesture); - -- button->set_popup_child(box); -- -- container->append(*button); -+ empty_label.set_text("No controllable displays detected"); -+ empty_label.add_css_class("dim-label"); -+ box.append(empty_label); -+ if (embedded) -+ { -+ container->append(box); -+ } else -+ { -+ button->set_popup_child(box); -+ container->append(*button); -+ } - - SysfsSurveillor::get().add_widget(this); - #ifdef HAVE_DDCUTIL -@@ -201,6 +208,7 @@ void WayfireBrightness::init(Gtk::Box *container) - #endif - - update_icon(); -+ hide_unused(); - } - - void WayfireBrightness::hide_unused() -@@ -209,6 +217,8 @@ void WayfireBrightness::hide_unused() - bool disp_show = ctrl_this_display != nullptr; - bool othr_show = other_box.get_children().size() != 1; // there is always the label - -+ empty_label.set_visible(!disp_show && !othr_show); -+ - if (disp_show) - { - display_box.show(); -diff --git a/src/panel/widgets/brightness/brightness.hpp b/src/panel/widgets/brightness/brightness.hpp -index 6e5d71b..535c382 100644 ---- a/src/panel/widgets/brightness/brightness.hpp -+++ b/src/panel/widgets/brightness/brightness.hpp -@@ -1,3 +1,5 @@ -+#pragma once -+ - #include "wf-popover.hpp" - #include <gtkmm.h> - #include <memory> -@@ -135,7 +137,7 @@ class WayfireBrightness : public WayfireWidget - - Gtk::Image icon; - Gtk::Box box, display_box, other_box; -- Gtk::Label display_label, other_label; -+ Gtk::Label display_label, other_label, empty_label; - Gtk::Separator disp_othr_sep; - sigc::connection popover_timeout; - -@@ -144,7 +146,8 @@ class WayfireBrightness : public WayfireWidget - void hide_unused(); - - public: -- WayfireBrightness(WayfireOutput *output); -+ const bool embedded; -+ WayfireBrightness(WayfireOutput *output, bool embedded = false); - ~WayfireBrightness(); - - std::unique_ptr<WayfireMenuWidget> button; -diff --git a/src/panel/widgets/control-center.cpp b/src/panel/widgets/control-center.cpp -new file mode 100644 -index 0000000..79f0fc0 ---- /dev/null -+++ b/src/panel/widgets/control-center.cpp -@@ -0,0 +1,100 @@ -+#include "control-center.hpp" -+ -+void WayfireControlCenter::shortcut(const char *label, const char *icon_name, WfOption<std::string>& command) -+{ -+ auto tile = Gtk::make_managed<Gtk::Button>(); -+ auto layout = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 6); -+ auto image = Gtk::make_managed<Gtk::Image>(); -+ image->set_from_icon_name(icon_name); -+ layout->append(*image); -+ layout->append(*Gtk::make_managed<Gtk::Label>(label)); -+ tile->set_child(*layout); -+ tile->add_css_class("quick-setting"); -+ tile->set_tooltip_text(std::string(label) + " settings"); -+ tile->signal_clicked().connect([this, &command] -+ { -+ try -+ { -+ if (command.value().empty()) -+ { -+ error.set_text("No application configured for this shortcut"); -+ error.show(); -+ return; -+ } -+ Glib::spawn_command_line_async(command.value()); -+ button->popdown(); -+ error.hide(); -+ } catch (const Glib::Error& e) -+ { -+ error.set_text(e.what()); -+ error.show(); -+ } -+ }); -+ shortcuts.append(*tile); -+} -+ -+void WayfireControlCenter::init(Gtk::Box *container) -+{ -+ button = std::make_unique<WayfireMenuWidget>("panel", "control-center"); -+ icon.set_from_icon_name("preferences-system-symbolic"); -+ icon.add_css_class("widget-icon"); -+ button->set_child(icon); -+ button->set_tooltip_text("Control center"); -+ button->set_keyboard_interactive(true); -+ button->open_on(1); -+ container->append(*button); -+ content.add_css_class("control-center-content"); -+ title.set_xalign(0); -+ title.add_css_class("title-2"); -+ content.append(title); -+ shortcuts.set_homogeneous(true); -+ shortcuts.set_spacing(8); -+ shortcut("Network", "network-wireless-symbolic", network_command); -+ shortcut("Bluetooth", "bluetooth-symbolic", bluetooth_command); -+ shortcut("Appearance", "preferences-desktop-theme-symbolic", appearance_command); -+ content.append(shortcuts); -+ error.set_wrap(true); -+ error.set_max_width_chars(40); -+ error.add_css_class("error"); -+ content.append(error); -+ error.hide(); -+#ifdef HAVE_WIREPLUMBER -+ auto mixer = std::make_unique<WayfireMixer>(true); -+ static_cast<WayfireWidget*>(mixer.get())->init(&sound); -+ widgets.push_back(std::move(mixer)); -+#else -+ sound.append(*Gtk::make_managed<Gtk::Label>("Audio controls require WirePlumber support")); -+#endif -+ auto brightness = std::make_unique<WayfireBrightness>(output, true); -+ static_cast<WayfireWidget*>(brightness.get())->init(&display); -+ widgets.push_back(std::move(brightness)); -+ pages.add(sound, "audio", "Audio"); -+ pages.add(display, "display", "Display"); -+ pages.add(media, "media", "Media"); -+ if (notifications) -+ { -+ auto center = std::make_unique<WayfireNotificationCenter>(true); -+ center->init(¬ifications_box); -+ widgets.push_back(std::move(center)); -+ pages.add(notifications_box, "notifications", "Inbox"); -+ } -+ pages.set_vhomogeneous(false); -+ pages.set_transition_type(Gtk::StackTransitionType::CROSSFADE); -+ pages.set_transition_duration(160); -+ tabs.set_stack(pages); -+ content.append(tabs); -+ scroll.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); -+ scroll.set_min_content_height(180); -+ scroll.set_max_content_height(480); -+ scroll.set_propagate_natural_height(true); -+ scroll.set_child(pages); -+ content.append(scroll); -+ button->set_popup_child(content); -+ handle_config_reload(); -+} -+ -+void WayfireControlCenter::handle_config_reload() -+{ -+ content.set_size_request(std::clamp(width.value(), 360, 800), -1); -+ for (auto& widget : widgets) widget->handle_config_reload(); -+} -diff --git a/src/panel/widgets/control-center.hpp b/src/panel/widgets/control-center.hpp -new file mode 100644 -index 0000000..5d40402 ---- /dev/null -+++ b/src/panel/widgets/control-center.hpp -@@ -0,0 +1,36 @@ -+#pragma once -+#include "widget.hpp" -+#include "wf-popover.hpp" -+#include "media.hpp" -+#include "brightness/brightness.hpp" -+#include "notifications/notification-center.hpp" -+#ifdef HAVE_WIREPLUMBER -+#include "mixer/mixer.hpp" -+#endif -+ -+class WayfireControlCenter : public WayfireWidget -+{ -+ WayfireOutput *output; -+ Gtk::Image icon; -+ Gtk::Box content{Gtk::Orientation::VERTICAL, 12}, shortcuts; -+ Gtk::Box sound{Gtk::Orientation::VERTICAL, 8}, display{Gtk::Orientation::VERTICAL, 8}; -+ Gtk::Box notifications_box{Gtk::Orientation::VERTICAL, 8}; -+ Gtk::Label title{"Control center"}, error; -+ Gtk::Stack pages; -+ Gtk::StackSwitcher tabs; -+ Gtk::ScrolledWindow scroll; -+ MediaControls media; -+ std::unique_ptr<WayfireMenuWidget> button; -+ std::vector<std::unique_ptr<WayfireWidget>> widgets; -+ WfOption<int> width{"panel/control_center_width"}; -+ WfOption<bool> notifications{"panel/control_center_notifications"}; -+ WfOption<std::string> network_command{"panel/control_center_network_command"}; -+ WfOption<std::string> bluetooth_command{"panel/control_center_bluetooth_command"}; -+ WfOption<std::string> appearance_command{"panel/control_center_appearance_command"}; -+ void shortcut(const char *label, const char *icon, WfOption<std::string>& command); -+ -+ public: -+ explicit WayfireControlCenter(WayfireOutput *output) : output(output) {} -+ void init(Gtk::Box *container) override; -+ void handle_config_reload() override; -+}; -diff --git a/src/panel/widgets/media.cpp b/src/panel/widgets/media.cpp -new file mode 100644 -index 0000000..a222efb ---- /dev/null -+++ b/src/panel/widgets/media.cpp -@@ -0,0 +1,105 @@ -+#include "media.hpp" -+ -+MediaControls::MediaControls() : Gtk::Box(Gtk::Orientation::VERTICAL, 8) -+{ -+ add_css_class("media-controls"); -+ append(players); -+ title.set_xalign(0); -+ title.set_ellipsize(Pango::EllipsizeMode::END); -+ title.set_max_width_chars(32); -+ title.add_css_class("heading"); -+ artist.set_xalign(0); -+ artist.set_ellipsize(Pango::EllipsizeMode::END); -+ artist.set_max_width_chars(32); -+ artist.add_css_class("dim-label"); -+ append(title); -+ append(artist); -+ controls.set_spacing(8); -+ controls.set_halign(Gtk::Align::CENTER); -+ const std::pair<Gtk::Button*, const char*> actions[] = { -+ {&previous, "Previous"}, {&play_pause, "PlayPause"}, {&next, "Next"}}; -+ for (auto item : actions) -+ { -+ controls.append(*item.first); -+ item.first->signal_clicked().connect([this, method = std::string(item.second)] -+ { -+ error.set_visible(false); -+ service->command(players.get_active_id(), method); -+ }); -+ } -+ previous.set_icon_name("media-skip-backward-symbolic"); -+ previous.set_tooltip_text("Previous track"); -+ next.set_icon_name("media-skip-forward-symbolic"); -+ next.set_tooltip_text("Next track"); -+ append(controls); -+ error.set_wrap(true); -+ error.add_css_class("error"); -+ error.set_max_width_chars(40); -+ append(error); -+ error.hide(); -+ players.signal_changed().connect([this] { if (!updating) refresh(); }); -+ changed = service->changed.connect(sigc::mem_fun(*this, &MediaControls::refresh)); -+ failed = service->error.connect([this] (std::string message) -+ { -+ error.set_text(message); -+ error.show(); -+ }); -+ refresh(); -+} -+ -+MediaControls::~MediaControls() -+{ -+ changed.disconnect(); -+ failed.disconnect(); -+} -+ -+void MediaControls::refresh() -+{ -+ updating = true; -+ std::string selected = players.get_active_id(); -+ players.remove_all(); -+ const auto& available = service->players(); -+ if (!available.count(selected)) -+ { -+ selected.clear(); -+ for (const auto& [name, player] : available) -+ if (selected.empty() || player.status == "Playing") selected = name; -+ } -+ for (const auto& [name, player] : available) -+ players.append(name, name.substr(std::string("org.mpris.MediaPlayer2.").size())); -+ players.set_active_id(selected); -+ players.set_visible(!available.empty()); -+ controls.set_visible(!available.empty()); -+ artist.set_visible(!available.empty()); -+ auto it = available.find(selected); -+ if (it == available.end()) -+ { -+ title.set_text("No media playing"); -+ } else -+ { -+ const auto& player = it->second; -+ title.set_text(player.title.empty() ? "Unknown track" : player.title); -+ title.set_tooltip_text(title.get_text()); -+ artist.set_text(player.artist.empty() ? player.status : player.artist); -+ bool playing = player.status == "Playing"; -+ play_pause.set_icon_name(playing ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"); -+ play_pause.set_tooltip_text(playing ? "Pause" : "Play"); -+ play_pause.set_sensitive(player.can_control && (playing ? player.can_pause : player.can_play)); -+ previous.set_sensitive(player.can_control && player.can_previous); -+ next.set_sensitive(player.can_control && player.can_next); -+ } -+ updating = false; -+} -+ -+void WayfireMedia::init(Gtk::Box *container) -+{ -+ button = std::make_unique<WayfireMenuWidget>("panel", "media"); -+ icon.set_from_icon_name("multimedia-player-symbolic"); -+ icon.add_css_class("widget-icon"); -+ button->set_child(icon); -+ button->set_tooltip_text("Media controls"); -+ button->set_popup_child(controls); -+ button->set_keyboard_interactive(true); -+ button->open_on(1); -+ container->append(*button); -+} -diff --git a/src/panel/widgets/media.hpp b/src/panel/widgets/media.hpp -new file mode 100644 -index 0000000..f3a0d01 ---- /dev/null -+++ b/src/panel/widgets/media.hpp -@@ -0,0 +1,30 @@ -+#pragma once -+#include "widget.hpp" -+#include "wf-popover.hpp" -+#include "mpris-service.hpp" -+ -+class MediaControls : public Gtk::Box -+{ -+ std::shared_ptr<MprisService> service = MprisService::get(); -+ Gtk::ComboBoxText players; -+ Gtk::Label title, artist, error; -+ Gtk::Box controls; -+ Gtk::Button previous, play_pause, next; -+ sigc::connection changed, failed; -+ bool updating = false; -+ void refresh(); -+ -+ public: -+ MediaControls(); -+ ~MediaControls(); -+}; -+ -+class WayfireMedia : public WayfireWidget -+{ -+ MediaControls controls; -+ Gtk::Image icon; -+ std::unique_ptr<WayfireMenuWidget> button; -+ -+ public: -+ void init(Gtk::Box *container) override; -+}; -diff --git a/src/panel/widgets/menu/menu.cpp b/src/panel/widgets/menu/menu.cpp -index 3c66e60..8d1ef03 100644 ---- a/src/panel/widgets/menu/menu.cpp -+++ b/src/panel/widgets/menu/menu.cpp -@@ -7,8 +7,8 @@ - #include <glibmm/spawn.h> - - #include "menu.hpp" --#include "gtk-utils.hpp" - #include "wf-autohide-window.hpp" -+#include "gtk-utils.hpp" - #include "wf-popover.hpp" - - #include "categorybutton.hpp" -@@ -470,8 +470,6 @@ void WayfireMenu::init(Gtk::Box *container) - main_image.add_css_class("default-icon"); - main_image.add_css_class("menu-icon"); - -- signals.push_back(output->toggle_menu_signal().connect(sigc::mem_fun(*this, &WayfireMenu::toggle_menu))); -- - // configuration reloading callbacks - menu_icon.set_callback([=] () { update_icon(); }); - flowbox_spacing.set_callback([=] () -@@ -489,6 +487,7 @@ void WayfireMenu::init(Gtk::Box *container) - button = std::make_unique<WayfireMenuWidget>("panel", "menu"); - button->set_child(main_image); - button->set_keyboard_interactive(true); -+ button->set_dismiss_on_outside_click(true); - button->add_css_class("menu-button"); - button->open_on(1); /* Open menu on left click */ - signals.push_back(button->signal_popup().connect( -diff --git a/src/panel/widgets/mixer/mixer-control.cpp b/src/panel/widgets/mixer/mixer-control.cpp -index 811d494..3c020c6 100644 ---- a/src/panel/widgets/mixer/mixer-control.cpp -+++ b/src/panel/widgets/mixer/mixer-control.cpp -@@ -73,7 +73,7 @@ void MixerControl::init() - { - // if the menu was popped up because of an external change - // and is now changed manually, don’t hide -- parent->button->popup(); -+ if (!parent->embedded) parent->button->popup(); - ignore = true; - // disregarding the return value prevents cases where ignore is read at an ncorrect time, - // with the only problem of having one update ignored if something goes very wrong -@@ -83,7 +83,7 @@ void MixerControl::init() - scale.set_user_changed_callback( - [this, id] () - { -- parent->button->popup(); -+ if (!parent->embedded) parent->button->popup(); - ignore = true; - WpCommon::get().set_volume(id, scale.get_target_value()); - }); -diff --git a/src/panel/widgets/mixer/mixer.cpp b/src/panel/widgets/mixer/mixer.cpp -index 7528448..0d3530e 100644 ---- a/src/panel/widgets/mixer/mixer.cpp -+++ b/src/panel/widgets/mixer/mixer.cpp -@@ -23,7 +23,7 @@ void WayfireMixer::reload_config() - set_spacing(sources_box); - set_spacing(streams_box); - -- if (layout.value() == "vertical") -+ if (embedded || layout.value() == "vertical") - { - master_box.set_orientation(Gtk::Orientation::VERTICAL); - } else // horizontal -@@ -31,6 +31,8 @@ void WayfireMixer::reload_config() - master_box.set_orientation(Gtk::Orientation::HORIZONTAL); - } - -+ if (embedded) return; -+ - // big matching operation - static WfOption<std::string> str_quick_target_choice{"panel/mixer_quick_target_choice"}; - static WfOption<std::string> str_wp_left_click_action{"panel/mixer_left_click_action"}; -@@ -183,7 +185,7 @@ void WayfireMixer::init(Gtk::Box *container) - in_streams_wall.add_css_class("in-streams"); - main_image.add_css_class("widget-icon"); - -- button->set_popup_child(master_box); -+ if (!embedded) button->set_popup_child(master_box); - - // scroll to change volume of the object targetted by the quick_target widget - auto scroll_gesture = Gtk::EventControllerScroll::create(); -@@ -230,7 +232,8 @@ void WayfireMixer::init(Gtk::Box *container) - reload_config(); - - // boxes hierarchy and labeling -- master_box.set_orientation(Gtk::Orientation::HORIZONTAL); -+ master_box.set_orientation(embedded ? Gtk::Orientation::VERTICAL : -+ (layout.value() == "vertical" ? Gtk::Orientation::VERTICAL : Gtk::Orientation::HORIZONTAL)); - - // column orientations - out_in_wall.set_orientation(Gtk::Orientation::VERTICAL); -@@ -265,7 +268,15 @@ void WayfireMixer::init(Gtk::Box *container) - streams_box.set_orientation(Gtk::Orientation::VERTICAL); - - // add to the actual container -- container->append(*button); -+ if (embedded) -+ { -+ container->append(master_box); -+ out_in_wall.set_orientation(Gtk::Orientation::HORIZONTAL); -+ in_streams_wall.set_orientation(Gtk::Orientation::HORIZONTAL); -+ } else -+ { -+ container->append(*button); -+ } - - // if there is no audio device nor application, the quick target will not be set - // and the widget will apear empty. Calling this here to always have the OOR icon. -diff --git a/src/panel/widgets/mixer/mixer.hpp b/src/panel/widgets/mixer/mixer.hpp -index c8f99ce..654f93e 100644 ---- a/src/panel/widgets/mixer/mixer.hpp -+++ b/src/panel/widgets/mixer/mixer.hpp -@@ -48,6 +48,9 @@ class WayfireMixer : public WayfireWidget - - public: - -+ explicit WayfireMixer(bool embedded = false) : embedded(embedded) {} -+ const bool embedded; -+ - WfOption<double> scroll_sensitivity{"panel/mixer_scroll_sensitivity"}; - WfOption<bool> invert_scroll{"panel/mixer_invert_scroll"}; - WfOption<bool> popup_on_change{"panel/mixer_popup_on_change"}; -@@ -60,7 +63,7 @@ class WayfireMixer : public WayfireWidget - * level on the widget icon and is concerned by the quick actions. - * configured by panel/mixer_quick_target_choice. idea: add pinning? - */ -- QuickTargetChoice quick_target_choice; -+ QuickTargetChoice quick_target_choice = QuickTargetChoice::DEFAULT_SINK; - std::unique_ptr<MixerControl> quick_target; - void set_quick_target_from(MixerControl *from); - -diff --git a/src/panel/widgets/mixer/wp-common.cpp b/src/panel/widgets/mixer/wp-common.cpp -index b3a9947..ac6dce6 100644 ---- a/src/panel/widgets/mixer/wp-common.cpp -+++ b/src/panel/widgets/mixer/wp-common.cpp -@@ -272,6 +272,8 @@ void WpCommon::on_mixer_changed(gpointer mixer_api, guint id, gpointer data) - - update_icons(); - -+ if (widget->embedded) continue; -+ - // if the mixer is currently being displayed, stop there - if ((widget->button->get_popup_child() == - &(widget->master_box)) && widget->button->is_popup_visible()) -diff --git a/src/panel/widgets/notifications/daemon.cpp b/src/panel/widgets/notifications/daemon.cpp -index cf7199b..c5ce34b 100644 ---- a/src/panel/widgets/notifications/daemon.cpp -+++ b/src/panel/widgets/notifications/daemon.cpp -@@ -58,7 +58,7 @@ dbus_method(Daemon::GetCapabilities) - { - static const auto value = Glib::Variant<std::tuple<std::vector<Glib::ustring>>>::create( - {{"action-icons", "actions", "body", "body-hyperlinks", "body-markup", "body-images", -- "persistance"}}); -+ "persistence"}}); - invocation->return_value(value); - } - -@@ -152,12 +152,12 @@ std::shared_ptr<Daemon> Daemon::Instance() - Daemon::Daemon() : - owner_id(Gio::DBus::own_name(Gio::DBus::BusType::SESSION, FDN_NAME, - sigc::mem_fun(*this, &Daemon::on_bus_acquired), -- {}, {}, Gio::DBus::BusNameOwnerFlags::REPLACE)) -+ {}, {}, Gio::DBus::BusNameOwnerFlags::NONE)) - {} - - Daemon::~Daemon() - { -- daemon_connection->unregister_object(object_id); -+ if (daemon_connection && object_id) daemon_connection->unregister_object(object_id); - Gio::DBus::unown_name(owner_id); - } - -@@ -173,11 +173,14 @@ void Daemon::closeNotification(Notification::id_type id, CloseReason reason) - return; - } - -+ const auto sender = notifications.at(id).additional_info.sender; -+ notifications.erase(id); - signal_notification_closed.emit(id); -- const auto & notification = notifications.at(id); -- const auto body = Glib::Variant<std::tuple<guint32, guint32>>::create({id, reason}); -- daemon_connection->emit_signal(FDN_PATH, FDN_NAME, "NotificationClosed", -- notification.additional_info.sender, body); -+ if (daemon_connection) -+ { -+ const auto body = Glib::Variant<std::tuple<guint32, guint32>>::create({id, reason}); -+ daemon_connection->emit_signal(FDN_PATH, FDN_NAME, "NotificationClosed", sender, body); -+ } - } - - void Daemon::invokeAction(Notification::id_type id, const Glib::ustring & action_key) -@@ -192,3 +195,17 @@ void Daemon::invokeAction(Notification::id_type id, const Glib::ustring & action - id).additional_info.sender, - body); - } -+ -+void Daemon::set_dnd(bool enabled) -+{ -+ if (dnd_enabled == enabled) return; -+ dnd_enabled = enabled; -+ state_changed.emit(); -+} -+ -+void Daemon::clear() -+{ -+ std::vector<Notification::id_type> ids; -+ for (const auto& [id, notification] : notifications) ids.push_back(id); -+ for (auto id : ids) closeNotification(id, CloseReason::Dismissed); -+} -diff --git a/src/panel/widgets/notifications/daemon.hpp b/src/panel/widgets/notifications/daemon.hpp -index 29c83bf..f30726b 100644 ---- a/src/panel/widgets/notifications/daemon.hpp -+++ b/src/panel/widgets/notifications/daemon.hpp -@@ -56,6 +56,10 @@ class Daemon - - const std::map<Notification::id_type, const Notification> & getNotifications() const; - void closeNotification(Notification::id_type id, CloseReason reason); -+ bool dnd() const { return dnd_enabled; } -+ void set_dnd(bool enabled); -+ void clear(); -+ sigc::signal<void()> state_changed; - void invokeAction(Notification::id_type id, const Glib::ustring & action_key); - - private: -@@ -63,8 +67,9 @@ class Daemon - - std::map<Notification::id_type, const Notification> notifications; - -+ bool dnd_enabled = false; - guint owner_id = 0; -- guint object_id; -+ guint object_id = 0; - Glib::RefPtr<Gio::DBus::Connection> daemon_connection; - - notification_signal signal_notification_new; -diff --git a/src/panel/widgets/notifications/notification-center.cpp b/src/panel/widgets/notifications/notification-center.cpp -index a3c4049..73ea606 100644 ---- a/src/panel/widgets/notifications/notification-center.cpp -+++ b/src/panel/widgets/notifications/notification-center.cpp -@@ -1,139 +1,139 @@ - #include "notification-center.hpp" - --#include <glibmm/main.h> --#include <gtkmm.h> -- --#include <gtk-utils.hpp> -- --#include "single-notification.hpp" --#include "wf-popover.hpp" -- - void WayfireNotificationCenter::init(Gtk::Box *container) - { -- button = std::make_unique<WayfireMenuWidget>("panel", "notification"); -- icon.add_css_class("widget-icon"); -- button->add_css_class("notification-center"); -- button->get_children()[0]->add_css_class("flat"); -- -- updateIcon(); -- button->set_child(icon); -- button->open_on(1); -- container->append(*button); -- -- scrolled_window.set_size_request(WIDTH, HEIGHT); -- -+ content.add_css_class("notification-center-content"); -+ title.add_css_class("heading"); -+ title.set_hexpand(true); -+ title.set_xalign(0); -+ header.set_spacing(8); -+ header.append(title); -+ header.append(clear_button); -+ content.append(header); -+ content.append(dnd_button); -+ empty.add_css_class("dim-label"); -+ content.append(empty); - box.set_valign(Gtk::Align::START); -- box.set_orientation(Gtk::Orientation::VERTICAL); - scrolled_window.set_child(box); -- button->set_popup_child(scrolled_window); -- -- button->set_tooltip_text("Middle click to toggle DND mode."); -- -- auto click_gesture = Gtk::GestureClick::create(); -- auto long_press = Gtk::GestureLongPress::create(); -- long_press->set_touch_only(true); -- long_press->signal_pressed().connect( -- [=] (double x, double y) -- { -- dnd_enabled = !dnd_enabled; -- updateIcon(); -- long_press->set_state(Gtk::EventSequenceState::CLAIMED); -- click_gesture->set_state(Gtk::EventSequenceState::DENIED); -- }); -- click_gesture->set_button(2); -- click_gesture->signal_pressed().connect([=] (int count, double x, double y) -+ scrolled_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); -+ scrolled_window.set_min_content_height(240); -+ scrolled_window.set_size_request(320, -1); -+ content.append(scrolled_window); -+ connections.push_back(clear_button.signal_clicked().connect([this] { daemon->clear(); })); -+ connections.push_back(dnd_button.signal_toggled().connect([this] - { -- click_gesture->set_state(Gtk::EventSequenceState::CLAIMED); -- }); -- click_gesture->signal_released().connect([=] (int count, double x, double y) -+ daemon->set_dnd(dnd_button.get_active()); -+ })); -+ if (embedded) - { -- dnd_enabled = !dnd_enabled; -- updateIcon(); -- }); -- button->add_controller(long_press); -- button->add_controller(click_gesture); -- -- for (const auto & [id, _] : daemon->getNotifications()) -+ container->append(content); -+ } else - { -- newNotification(id, false); -+ button = std::make_unique<WayfireMenuWidget>("panel", "notification"); -+ icon.add_css_class("widget-icon"); -+ button->add_css_class("notification-center"); -+ button->set_child(icon); -+ button->set_popup_child(content); -+ auto open = Gtk::GestureClick::create(); -+ open->set_button(1); -+ open->signal_released().connect([this] (int, double, double) -+ { -+ button->set_keyboard_interactive(true); -+ button->toggle(); -+ }); -+ button->add_controller(open); -+ container->append(*button); -+ auto click = Gtk::GestureClick::create(); -+ click->set_button(2); -+ click->signal_released().connect([this] (int, double, double) { daemon->set_dnd(!daemon->dnd()); }); -+ button->add_controller(click); -+ auto long_press = Gtk::GestureLongPress::create(); -+ long_press->set_touch_only(true); -+ long_press->signal_pressed().connect([this, click = click.get(), long_press = long_press.get()] (double, double) -+ { -+ daemon->set_dnd(!daemon->dnd()); -+ long_press->set_state(Gtk::EventSequenceState::CLAIMED); -+ click->set_state(Gtk::EventSequenceState::DENIED); -+ }); -+ button->add_controller(long_press); - } -- -- notification_new_conn = -- daemon->signalNotificationNew().connect([=] (Notification::id_type id) { newNotification(id); }); -- notification_replace_conn = -- daemon->signalNotificationReplaced().connect([=] (Notification::id_type id) -+ connections.push_back(daemon->state_changed.connect(sigc::mem_fun(*this, &WayfireNotificationCenter::update_state))); -+ connections.push_back(daemon->signalNotificationNew().connect(sigc::mem_fun(*this, &WayfireNotificationCenter::new_notification))); -+ connections.push_back(daemon->signalNotificationReplaced().connect([this] (Notification::id_type id) - { -- replaceNotification(id); -- }); -- notification_close_conn = -- daemon->signalNotificationClosed().connect([=] (Notification::id_type id) { closeNotification(id); }); -+ replaced.insert(id); -+ new_notification(id); -+ })); -+ connections.push_back(daemon->signalNotificationClosed().connect([this] (Notification::id_type) { schedule_refresh(); })); -+ refresh(); - } - --void WayfireNotificationCenter::newNotification(Notification::id_type id, bool show_popup) -+WayfireNotificationCenter::~WayfireNotificationCenter() - { -- const auto & notification = daemon->getNotifications().at(id); -- g_assert(notification_widgets.count(id) == 0); -- notification_widgets.insert({id, std::make_unique<WfSingleNotification>(notification)}); -- auto & widget = notification_widgets.at(id); -- box.append(*widget); -- widget->set_reveal_child(); -- if (show_popup && !dnd_enabled || (show_critical_in_dnd && (notification.hints.urgency == 2))) -- { -- if ((timeout > 0) && (!popover_timeout.empty() || !button->is_popup_visible())) -- { -- popover_timeout.disconnect(); -- popover_timeout = Glib::signal_timeout().connect( -- [=] -- { -- button->popdown(); -- button->set_keyboard_interactive(false); -- popover_timeout.disconnect(); -- return true; -- }, -- timeout * 1000); -- } -+ refresh_idle.disconnect(); -+ for (auto& connection : connections) connection.disconnect(); -+} - -- button->set_keyboard_interactive(false); -- button->popup(); -- } -+void WayfireNotificationCenter::schedule_refresh() -+{ -+ if (refresh_idle.connected()) return; -+ // Defer destruction of a row until its close-button callback has returned. -+ refresh_idle = Glib::signal_idle().connect([this] { refresh(); return false; }); - } - --void WayfireNotificationCenter::replaceNotification(Notification::id_type id) -+void WayfireNotificationCenter::refresh() - { -- if (notification_widgets.count(id) == 0) -+ const auto& notifications = daemon->getNotifications(); -+ for (auto it = notification_widgets.begin(); it != notification_widgets.end();) - { -- newNotification(id); -- return; -+ if (!notifications.count(it->first) || replaced.count(it->first)) -+ { -+ box.remove(*it->second); -+ it = notification_widgets.erase(it); -+ } else ++it; - } -- -- auto & widget = notification_widgets.at(id); -- widget->property_child_revealed().signal_changed().connect([=] -+ replaced.clear(); -+ for (const auto& [id, notification] : notifications) - { -- notification_widgets.erase(id); -- newNotification(id); -- }); -- widget->set_reveal_child(false); -+ if (notification_widgets.count(id)) continue; -+ auto row = std::make_unique<WfSingleNotification>(notification); -+ box.prepend(*row); -+ row->set_reveal_child(true); -+ notification_widgets.emplace(id, std::move(row)); -+ } -+ update_state(); - } - --void WayfireNotificationCenter::closeNotification(Notification::id_type id) -+void WayfireNotificationCenter::update_state() - { -- if (notification_widgets.count(id) == 0) -+ const auto count = daemon->getNotifications().size(); -+ title.set_text(count ? "Notifications · " + std::to_string(count) : "Notifications"); -+ empty.set_visible(count == 0); -+ scrolled_window.set_visible(count != 0); -+ clear_button.set_sensitive(count != 0); -+ dnd_button.set_active(daemon->dnd()); -+ if (button) - { -- return; -+ icon.set_from_icon_name(daemon->dnd() ? "notifications-disabled-symbolic" : -+ (count ? "mail-unread-symbolic" : "preferences-system-notifications-symbolic")); -+ button->set_tooltip_text(std::to_string(count) + " notifications" + -+ (daemon->dnd() ? " · Do not disturb" : "") + "\nMiddle click to toggle Do not disturb"); - } -- -- auto & widget = notification_widgets.at(id); -- widget->property_child_revealed().signal_changed().connect([=] { notification_widgets.erase(id); }); -- widget->set_reveal_child(false); - } - --void WayfireNotificationCenter::updateIcon() -+void WayfireNotificationCenter::new_notification(Notification::id_type id) - { -- if (dnd_enabled) -+ schedule_refresh(); -+ if (!button) return; -+ const auto& notification = daemon->getNotifications().at(id); -+ if (daemon->dnd() && !(show_critical_in_dnd && notification.hints.urgency == 2)) return; -+ // Timed popups never interrupt another open panel. -+ if (timeout > 0 && !button->is_popup_visible()) - { -- icon.set_from_icon_name("notifications-disabled"); -- } else -+ button->set_keyboard_interactive(false); -+ button->popup_timed(timeout * 1000); -+ } else if (timeout > 0 && !button->is_manual_popup()) - { -- icon.set_from_icon_name("notifications"); -+ button->popup_timed(timeout * 1000); - } - } -diff --git a/src/panel/widgets/notifications/notification-center.hpp b/src/panel/widgets/notifications/notification-center.hpp -index 7014fbc..64f5475 100644 ---- a/src/panel/widgets/notifications/notification-center.hpp -+++ b/src/panel/widgets/notifications/notification-center.hpp -@@ -1,48 +1,33 @@ --#ifndef NOTIFICATION_CENTER_HPP --#define NOTIFICATION_CENTER_HPP -- -+#pragma once - #include "../../widget.hpp" - #include "daemon.hpp" - #include "single-notification.hpp" -- --#include <gtkmm/scrolledwindow.h> - #include <wf-popover.hpp> - - class WayfireNotificationCenter : public WayfireWidget - { -- private: -- static const int WIDTH = 300, HEIGHT = 400; -- -+ const bool embedded; - const std::shared_ptr<Daemon> daemon = Daemon::Launch(); -- sigc::connection notification_new_conn; -- sigc::connection notification_replace_conn; -- sigc::connection notification_close_conn; -- -+ std::vector<sigc::connection> connections; -+ sigc::connection refresh_idle; - Gtk::Image icon; - std::unique_ptr<WayfireMenuWidget> button; -+ Gtk::Box content{Gtk::Orientation::VERTICAL, 12}, header, box{Gtk::Orientation::VERTICAL, 8}; -+ Gtk::Label title{"Notifications"}, empty{"You're all caught up"}; -+ Gtk::ToggleButton dnd_button{"Do not disturb"}; -+ Gtk::Button clear_button{"Clear all"}; - Gtk::ScrolledWindow scrolled_window; -- Gtk::Box box; -- -- std::map<Notification::id_type, std::unique_ptr<WfSingleNotification>> notification_widgets = {}; -- -- void newNotification(Notification::id_type id, bool show_popup = true); -- void replaceNotification(Notification::id_type id); -- void closeNotification(Notification::id_type id); -- void updateIcon(); -- -- sigc::connection popover_timeout; -+ std::map<Notification::id_type, std::unique_ptr<WfSingleNotification>> notification_widgets; -+ std::set<Notification::id_type> replaced; - WfOption<double> timeout{"panel/notifications_autohide_timeout"}; - WfOption<bool> show_critical_in_dnd{"panel/notifications_critical_in_dnd"}; -- bool dnd_enabled = false; -+ void schedule_refresh(); -+ void refresh(); -+ void update_state(); -+ void new_notification(Notification::id_type id); - - public: -+ explicit WayfireNotificationCenter(bool embedded = false) : embedded(embedded) {} - void init(Gtk::Box *container) override; -- ~WayfireNotificationCenter() override -- { -- notification_new_conn.disconnect(); -- notification_replace_conn.disconnect(); -- notification_close_conn.disconnect(); -- } -+ ~WayfireNotificationCenter() override; - }; -- --#endif -diff --git a/src/panel/widgets/window-list/toplevel.cpp b/src/panel/widgets/window-list/toplevel.cpp -index 7d11428..7b7adde 100644 ---- a/src/panel/widgets/window-list/toplevel.cpp -+++ b/src/panel/widgets/window-list/toplevel.cpp -@@ -734,13 +734,13 @@ class WayfireToplevel::impl - - void send_rectangle_hint() - { -- auto panel = WayfirePanelApp::get().panel_for_wl_output(window_list->output->wo); -+ auto panel = dynamic_cast<WayfireAutohidingWindow*>(button.get_root()); - auto w = button.get_width(); - auto h = button.get_height(); - if (panel && (w > 0) && (h > 0)) - { - double x, y; -- button.translate_coordinates(panel->get_window(), 0, 0, x, y); -+ button.translate_coordinates(*panel, 0, 0, x, y); - zwlr_foreign_toplevel_handle_v1_set_rectangle(handle, panel->get_wl_surface(), - x, y, w, h); - } -diff --git a/src/panel/widgets/workspace-switcher.cpp b/src/panel/widgets/workspace-switcher.cpp -index 3f13216..a1c9af5 100644 ---- a/src/panel/widgets/workspace-switcher.cpp -+++ b/src/panel/widgets/workspace-switcher.cpp -@@ -412,6 +412,9 @@ void WayfireWorkspaceSwitcher::render_workspace(wf::json_t workspace, int j, int - - void WayfireWorkspaceSwitcher::process_workspaces(wf::json_t workspace_data) - { -+ if (!workspace_data.is_array() || workspace_data.size() == 0) -+ return; -+ - size_t i = 0; - - this->grid_width = workspace_data[i]["workspace"]["grid_width"].as_int(); -@@ -467,6 +470,9 @@ void WayfireWorkspaceSwitcher::process_workspaces(wf::json_t workspace_data) - - void WayfireWorkspaceSwitcher::grid_process_workspaces(wf::json_t workspace_data) - { -+ if (!workspace_data.is_array() || workspace_data.size() == 0) -+ return; -+ - size_t i = 0; - - this->grid_width = workspace_data[i]["workspace"]["grid_width"].as_int(); -@@ -607,7 +613,9 @@ void WayfireWorkspaceSwitcher::add_view(wf::json_t view_data) - - auto v = Gtk::make_managed<WayfireWorkspaceWindow>(); - v->add_css_class("view"); -- v->add_css_class(view_data["app-id"].as_string()); -+ const auto app_id = view_data["app-id"].as_string(); -+ if (!app_id.empty()) -+ v->add_css_class(app_id); - v->id = view_data["id"].as_int(); - if (view_data["activated"].as_bool() || (v->id == this->active_view_id)) - { -@@ -692,7 +700,9 @@ void WayfireWorkspaceSwitcher::grid_add_view(wf::json_t view_data) - - auto v = Gtk::make_managed<WayfireWorkspaceWindow>(); - v->add_css_class("view"); -- v->add_css_class(view_data["app-id"].as_string()); -+ const auto app_id = view_data["app-id"].as_string(); -+ if (!app_id.empty()) -+ v->add_css_class(app_id); - v->id = view_data["id"].as_int(); - if (view_data["activated"].as_bool() || (v->id == this->active_view_id)) - { -diff --git a/src/util/ipc-geometry.hpp b/src/util/ipc-geometry.hpp -new file mode 100644 -index 0000000..a5ca005 ---- /dev/null -+++ b/src/util/ipc-geometry.hpp -@@ -0,0 +1,32 @@ -+#pragma once -+ -+#include <cmath> -+#include <limits> -+#include <optional> -+#include <wayfire/nonstd/json.hpp> -+ -+// Wayfire versions publish geometry as either JSON integers or real numbers. -+inline std::optional<double> ipc_number(const wf::json_reference_t& value) -+{ -+ double number; -+ if (value.is_double()) -+ number = value.as_double(); -+ else if (value.is_int64()) -+ number = value.as_int64(); -+ else if (value.is_uint64()) -+ number = value.as_uint64(); -+ else -+ return std::nullopt; -+ -+ return std::isfinite(number) ? std::optional<double>(number) : std::nullopt; -+} -+ -+inline int ipc_pixel(const wf::json_reference_t& value, int fallback = 0) -+{ -+ auto number = ipc_number(value); -+ if (!number || *number < std::numeric_limits<int>::min() || -+ *number > std::numeric_limits<int>::max()) -+ return fallback; -+ -+ return static_cast<int>(std::round(*number)); -+} -diff --git a/src/util/meson.build b/src/util/meson.build -index d53e368..8d6725e 100644 ---- a/src/util/meson.build -+++ b/src/util/meson.build -@@ -2,6 +2,9 @@ util = static_library( - 'util', - [ - 'gtk-utils.cpp', -+ 'mpris-service.cpp', -+ 'output-topology.cpp', -+ 'window-title.cpp', - 'icon-select.cpp', - 'wf-shell-app.cpp', - 'wf-autohide-window.cpp', -diff --git a/src/util/mpris-service.cpp b/src/util/mpris-service.cpp -new file mode 100644 -index 0000000..598633c ---- /dev/null -+++ b/src/util/mpris-service.cpp -@@ -0,0 +1,189 @@ -+#include "mpris-service.hpp" -+ -+namespace -+{ -+bool is_player(const std::string& name) -+{ -+ return name.rfind("org.mpris.MediaPlayer2.", 0) == 0; -+} -+ -+bool boolean_property(GDBusProxy *proxy, const char *name) -+{ -+ auto value = g_dbus_proxy_get_cached_property(proxy, name); -+ bool result = value && g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN) && -+ g_variant_get_boolean(value); -+ if (value) g_variant_unref(value); -+ return result; -+} -+} -+ -+std::shared_ptr<MprisService> MprisService::get() -+{ -+ static std::weak_ptr<MprisService> instance; -+ auto service = instance.lock(); -+ if (!service) -+ { -+ service = std::shared_ptr<MprisService>(new MprisService); -+ instance = service; -+ service->start(); -+ } -+ return service; -+} -+ -+MprisService::~MprisService() -+{ -+ cancel->cancel(); -+ names_changed.disconnect(); -+ for (auto& [name, player] : clients) player.changed.disconnect(); -+} -+ -+void MprisService::start() -+{ -+ auto weak = weak_from_this(); -+ Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::SESSION, -+ "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", -+ [weak] (const Glib::RefPtr<Gio::AsyncResult>& result) -+ { -+ auto self = weak.lock(); -+ if (!self) return; -+ try -+ { -+ self->bus = Gio::DBus::Proxy::create_for_bus_finish(result); -+ self->names_changed = self->bus->signal_signal().connect( -+ [weak] (const Glib::ustring&, const Glib::ustring& signal, -+ const Glib::VariantContainerBase& parameters) -+ { -+ auto self = weak.lock(); -+ if (!self || signal != "NameOwnerChanged") return; -+ const auto values = Glib::VariantBase::cast_dynamic< -+ Glib::Variant<std::tuple<Glib::ustring, Glib::ustring, Glib::ustring>>>(parameters).get(); -+ const std::string name = std::get<0>(values); -+ if (!is_player(name)) return; -+ self->remove(name); -+ if (!std::get<2>(values).empty()) self->add(name); -+ }); -+ auto bus = self->bus; -+ bus->call("ListNames", [weak, bus] (const Glib::RefPtr<Gio::AsyncResult>& result) -+ { -+ try -+ { -+ auto reply = bus->call_finish(result); -+ auto self = weak.lock(); -+ if (!self) return; -+ auto names = Glib::VariantBase::cast_dynamic< -+ Glib::Variant<std::tuple<std::vector<Glib::ustring>>>>(reply).get(); -+ for (const auto& name : std::get<0>(names)) -+ if (is_player(name)) self->add(name); -+ } catch (const Glib::Error& e) -+ { -+ if (auto self = weak.lock()) self->error.emit(e.what()); -+ } -+ }, self->cancel); -+ } catch (const Glib::Error& e) -+ { -+ self->error.emit(e.what()); -+ } -+ }, cancel); -+} -+ -+void MprisService::add(const std::string& name) -+{ -+ const auto generation = ++generations[name]; -+ auto weak = weak_from_this(); -+ Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::SESSION, name, -+ "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", -+ [weak, name, generation] (const Glib::RefPtr<Gio::AsyncResult>& result) -+ { -+ try -+ { -+ auto proxy = Gio::DBus::Proxy::create_for_bus_finish(result); -+ auto self = weak.lock(); -+ if (!self || self->generations[name] != generation || proxy->get_name_owner().empty()) return; -+ auto& player = self->clients[name]; -+ player.changed.disconnect(); -+ player.proxy = proxy; -+ player.changed = proxy->signal_properties_changed().connect( -+ [weak, name] (const Gio::DBus::Proxy::MapChangedProperties&, -+ const std::vector<Glib::ustring>&) -+ { -+ if (auto self = weak.lock()) self->refresh(name); -+ }); -+ self->refresh(name); -+ } catch (const Glib::Error&) -+ { -+ // A player may disappear between ListNames and proxy creation. -+ } -+ }, cancel, {}, Gio::DBus::ProxyFlags::DO_NOT_AUTO_START); -+} -+ -+void MprisService::remove(const std::string& name) -+{ -+ ++generations[name]; -+ auto it = clients.find(name); -+ if (it == clients.end()) return; -+ it->second.changed.disconnect(); -+ clients.erase(it); -+ changed.emit(); -+} -+ -+void MprisService::refresh(const std::string& name) -+{ -+ auto it = clients.find(name); -+ if (it == clients.end()) return; -+ auto& player = it->second; -+ auto proxy = player.proxy->gobj(); -+ player.title.clear(); -+ player.artist.clear(); -+ player.status.clear(); -+ auto status = g_dbus_proxy_get_cached_property(proxy, "PlaybackStatus"); -+ if (status && g_variant_is_of_type(status, G_VARIANT_TYPE_STRING)) -+ player.status = g_variant_get_string(status, nullptr); -+ if (status) g_variant_unref(status); -+ auto metadata = g_dbus_proxy_get_cached_property(proxy, "Metadata"); -+ if (metadata && g_variant_is_of_type(metadata, G_VARIANT_TYPE_VARDICT)) -+ { -+ const char *title; -+ if (g_variant_lookup(metadata, "xesam:title", "&s", &title)) player.title = title; -+ auto artists = g_variant_lookup_value(metadata, "xesam:artist", G_VARIANT_TYPE_STRING_ARRAY); -+ if (artists) -+ { -+ GVariantIter iter; -+ const char *artist; -+ g_variant_iter_init(&iter, artists); -+ while (g_variant_iter_next(&iter, "&s", &artist)) -+ { -+ if (!player.artist.empty()) player.artist += ", "; -+ player.artist += artist; -+ } -+ g_variant_unref(artists); -+ } -+ } -+ if (metadata) g_variant_unref(metadata); -+ player.can_control = boolean_property(proxy, "CanControl"); -+ player.can_play = boolean_property(proxy, "CanPlay"); -+ player.can_pause = boolean_property(proxy, "CanPause"); -+ player.can_next = boolean_property(proxy, "CanGoNext"); -+ player.can_previous = boolean_property(proxy, "CanGoPrevious"); -+ changed.emit(); -+} -+ -+void MprisService::command(const std::string& name, const std::string& method) -+{ -+ auto it = clients.find(name); -+ if (it == clients.end()) return; -+ const auto& player = it->second; -+ if (!player.can_control) return; -+ if (method == "Next" ? !player.can_next : method == "Previous" ? !player.can_previous : -+ method == "PlayPause" ? !(player.status == "Playing" ? player.can_pause : player.can_play) : true) -+ return; -+ auto proxy = player.proxy; -+ auto weak = weak_from_this(); -+ proxy->call(method, [weak, proxy] (const Glib::RefPtr<Gio::AsyncResult>& result) -+ { -+ try { proxy->call_finish(result); } -+ catch (const Glib::Error& e) -+ { -+ if (auto self = weak.lock()) self->error.emit(e.what()); -+ } -+ }, cancel); -+} -diff --git a/src/util/mpris-service.hpp b/src/util/mpris-service.hpp -new file mode 100644 -index 0000000..d7c507a ---- /dev/null -+++ b/src/util/mpris-service.hpp -@@ -0,0 +1,38 @@ -+#pragma once -+ -+#include <giomm.h> -+#include <map> -+#include <memory> -+#include <string> -+ -+// One subscription per player, shared by every panel/output. -+class MprisService : public std::enable_shared_from_this<MprisService> -+{ -+ public: -+ struct Player -+ { -+ Glib::RefPtr<Gio::DBus::Proxy> proxy; -+ sigc::connection changed; -+ std::string title, artist, status; -+ bool can_control = false, can_play = false, can_pause = false; -+ bool can_next = false, can_previous = false; -+ }; -+ -+ static std::shared_ptr<MprisService> get(); -+ ~MprisService(); -+ const std::map<std::string, Player>& players() const { return clients; } -+ sigc::signal<void()> changed; -+ sigc::signal<void(std::string)> error; -+ void command(const std::string& name, const std::string& method); -+ -+ private: -+ Glib::RefPtr<Gio::DBus::Proxy> bus; -+ Glib::RefPtr<Gio::Cancellable> cancel = Gio::Cancellable::create(); -+ sigc::connection names_changed; -+ std::map<std::string, Player> clients; -+ std::map<std::string, uint64_t> generations; -+ void start(); -+ void add(const std::string& name); -+ void remove(const std::string& name); -+ void refresh(const std::string& name); -+}; -diff --git a/src/util/output-topology.cpp b/src/util/output-topology.cpp -new file mode 100644 -index 0000000..accad30 ---- /dev/null -+++ b/src/util/output-topology.cpp -@@ -0,0 +1,129 @@ -+#include "output-topology.hpp" -+#include "ipc-geometry.hpp" -+#include <algorithm> -+#include <set> -+#include <sstream> -+#include <cmath> -+ -+namespace shell -+{ -+std::optional<OutputTopology> parse_output_topology(const wf::json_reference_t& data) -+{ -+ if (!data.is_array()) -+ return std::nullopt; -+ -+ OutputTopology result; -+ std::set<std::string> names; -+ for (size_t i = 0; i < data.size(); ++i) -+ { -+ auto output = data[i]; -+ if (!output.is_object() || !output["name"].is_string()) -+ return std::nullopt; -+ -+ DesktopOutput desktop{output["name"].as_string(), {}}; -+ if (desktop.name.empty() || !names.insert(desktop.name).second) -+ return std::nullopt; -+ -+ if (output.has_member("members")) -+ { -+ auto members = output["members"]; -+ if (!members.is_array()) -+ return std::nullopt; -+ -+ for (size_t j = 0; j < members.size(); ++j) -+ { -+ auto member = members[j]; -+ if (!member.is_object() || !member["name"].is_string() || -+ !member["geometry"].is_object()) -+ return std::nullopt; -+ -+ auto name = member["name"].as_string(); -+ auto geometry = member["geometry"]; -+ auto x = ipc_number(geometry["x"]), y = ipc_number(geometry["y"]); -+ auto w = ipc_number(geometry["width"]), h = ipc_number(geometry["height"]); -+ if (name.empty() || !names.insert(name).second || !x || !y || !w || !h || -+ *x < 0 || *y < 0 || *w <= 0 || *h <= 0) -+ return std::nullopt; -+ -+ desktop.members.push_back({name, {*x, *y, *w, *h}}); -+ } -+ } -+ -+ result.push_back(std::move(desktop)); -+ } -+ -+ return result; -+} -+ -+std::optional<RegionMargins> region_panel_margins(const OutputGeometry& r, -+ int width, int height, int margin) -+{ -+ if (!std::isfinite(r.x) || !std::isfinite(r.y) || !std::isfinite(r.width) || -+ !std::isfinite(r.height) || r.x < 0 || r.width <= 0 || r.y != 0 || -+ std::abs(r.height - height) > 0.5 || r.x + r.width > width + 0.5 || margin < 0) -+ return std::nullopt; -+ -+ const int start = std::lround(r.x), end = std::lround(r.x + r.width); -+ if (end - start <= 2 * static_cast<long long>(margin)) -+ return std::nullopt; -+ return RegionMargins{start + margin, width - end + margin}; -+} -+ -+bool output_name_matches(const std::string& names, const std::string& name) -+{ -+ auto separated = names; -+ std::replace(separated.begin(), separated.end(), ',', ' '); -+ std::istringstream tokens(separated); -+ std::string token; -+ while (tokens >> token) -+ if (token == "*" || token == name) -+ return true; -+ -+ return false; -+} -+ -+OutputSelection select_panel_targets(const std::optional<OutputTopology>& topology, -+ const std::vector<std::string>& visible, const std::string& mode, const std::string& names) -+{ -+ OutputSelection result; -+ if (mode != "all" && mode != "virtual" && mode != "physical") -+ { -+ result.error = "Unknown panel/output_mode: " + mode + "; use all, virtual or physical."; -+ return result; -+ } -+ -+ if (mode == "all") -+ { -+ for (const auto& name : visible) -+ if (output_name_matches(names, name)) -+ result.targets.push_back({name, name, std::nullopt}); -+ -+ return result; -+ } -+ -+ if (!topology) -+ { -+ result.error = "Output topology unavailable; virtual/physical selection requires Wayfire IPC metadata."; -+ return result; -+ } -+ -+ for (const auto& output : *topology) -+ { -+ if (std::find(visible.begin(), visible.end(), output.name) == visible.end()) -+ continue; -+ -+ if (mode == "physical" && !output.members.empty()) -+ { -+ for (const auto& member : output.members) -+ if (output_name_matches(names, member.name)) -+ result.targets.push_back({member.name, output.name, member.geometry}); -+ } else if ((mode == "virtual") == !output.members.empty() && -+ output_name_matches(names, output.name)) -+ { -+ result.targets.push_back({output.name, output.name, std::nullopt}); -+ } -+ } -+ -+ return result; -+} -+} -diff --git a/src/util/output-topology.hpp b/src/util/output-topology.hpp -new file mode 100644 -index 0000000..9f55524 ---- /dev/null -+++ b/src/util/output-topology.hpp -@@ -0,0 +1,65 @@ -+#pragma once -+ -+#include <optional> -+#include <string> -+#include <vector> -+#include <wayfire/nonstd/json.hpp> -+ -+namespace shell -+{ -+struct OutputGeometry -+{ -+ double x, y, width, height; -+ bool operator==(const OutputGeometry& other) const -+ { -+ return x == other.x && y == other.y && width == other.width && height == other.height; -+ } -+}; -+ -+struct OutputMember -+{ -+ std::string name; -+ OutputGeometry geometry; // Output-local logical coordinates, not desktop-global. -+}; -+ -+struct DesktopOutput -+{ -+ std::string name; -+ std::vector<OutputMember> members; -+}; -+ -+using OutputTopology = std::vector<DesktopOutput>; -+ -+// nullopt means unavailable or malformed metadata; an empty vector is a valid -+// topology with no desktop outputs. Never infer a combined output from its name. -+std::optional<OutputTopology> parse_output_topology(const wf::json_reference_t& data); -+bool output_name_matches(const std::string& names, const std::string& name); -+ -+struct PanelTarget -+{ -+ std::string name; -+ std::string desktop; -+ std::optional<OutputGeometry> region; -+}; -+ -+// Ordinary layer-shell can place fixed, full-span bars on horizontal members. -+// Other layouts need compositor-owned region attachment/workareas. -+struct RegionMargins -+{ -+ int left, right; -+}; -+std::optional<RegionMargins> region_panel_margins(const OutputGeometry& region, -+ int desktop_width, int desktop_height, int side_margin); -+ -+struct OutputSelection -+{ -+ std::vector<PanelTarget> targets; -+ std::string error; -+}; -+ -+// all: ordinary Wayland desktop outputs; virtual: combined desktop outputs; -+// physical: standalone desktop outputs and physical members of combined outputs. -+// Only desktops present in the GDK monitor list are considered ready. -+OutputSelection select_panel_targets(const std::optional<OutputTopology>& topology, -+ const std::vector<std::string>& visible, const std::string& mode, const std::string& names); -+} -diff --git a/src/util/wf-autohide-window.cpp b/src/util/wf-autohide-window.cpp -index 0920ccc..4aa6ddb 100644 ---- a/src/util/wf-autohide-window.cpp -+++ b/src/util/wf-autohide-window.cpp -@@ -12,7 +12,8 @@ - #include <assert.h> - - WayfireAutohidingWindow::WayfireAutohidingWindow(WayfireOutput *output, -- const std::string& section) : -+ const std::string& section, std::optional<shell::OutputGeometry> region, -+ std::shared_ptr<PanelRegionGroup> group) : - force_show_popup{"panel/force_show_popup"}, - position{section + "/position"}, - full_span{section + "/full_span"}, -@@ -28,6 +29,13 @@ WayfireAutohidingWindow::WayfireAutohidingWindow(WayfireOutput *output, - adjacent_edge_hotspot_size{section + "/adjacent_edge_hotspot_size"} - { - this->output = output; -+ this->region = region; -+ this->region_group = group; -+ if (section == "panel") -+ { -+ side_margin = std::make_unique<WfOption<int>>("panel/side_margin"); -+ side_margin->set_callback([this] { update_position(); }); -+ } - assert(output->monitor->is_valid()); - this->set_decorated(false); - -@@ -48,6 +56,21 @@ WayfireAutohidingWindow::WayfireAutohidingWindow(WayfireOutput *output, - this->minimal_width.set_callback(set_size); - set_size(); - -+ if (region) -+ { -+ gtk_layer_set_exclusive_zone(gobj(), -1); -+ signals.push_back(output->monitor->property_geometry().signal_changed().connect( -+ [this] { update_position(); })); -+ signals.push_back(signal_realize().connect([this] -+ { -+ signals.push_back(get_surface()->property_height().signal_changed().connect( -+ [this] { update_region_reservation(); })); -+ update_region_reservation(); -+ })); -+ add_css_class(output->monitor->get_connector()); -+ return; -+ } -+ - auto pointer_gesture = Gtk::EventControllerMotion::create(); - signals.push_back(pointer_gesture->signal_enter().connect([=] (double x, double y) - { -@@ -92,24 +115,16 @@ WayfireAutohidingWindow::WayfireAutohidingWindow(WayfireOutput *output, - - - -- static const zwf_output_v2_listener listener = { -- .enter_fullscreen = [] (void *data, zwf_output_v2*) -- {}, -- .leave_fullscreen = [] (void *data, zwf_output_v2*) -- {}, -- .toggle_menu = [] (void *data, zwf_output_v2*) -- { -- ((WayfireAutohidingWindow*)data)->output->toggle_menu_signal().emit(); -- }, -- }; -- zwf_output_v2_add_listener(output->output, &listener, this); -- - // Set css match - add_css_class(output->monitor->get_connector()); - } - - WayfireAutohidingWindow::~WayfireAutohidingWindow() - { -+ pending_show.disconnect(); -+ pending_hide.disconnect(); -+ if (region_group) -+ region_group->remove(this); - if (this->edge_hotspot) - { - zwf_hotspot_v2_destroy(this->edge_hotspot); -@@ -253,8 +268,24 @@ void WayfireAutohidingWindow::update_position() - } - } - -- if (!output->output) -+ if (side_margin && ((edge == GTK_LAYER_SHELL_EDGE_TOP) || (edge == GTK_LAYER_SHELL_EDGE_BOTTOM))) -+ { -+ gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, side_margin->value()); -+ gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, side_margin->value()); -+ } -+ -+ if (region) - { -+ Gdk::Rectangle desktop; -+ output->monitor->get_geometry(desktop); -+ auto margins = shell::region_panel_margins(*region, desktop.get_width(), desktop.get_height(), -+ side_margin ? side_margin->value() : 0); -+ if (margins) -+ { -+ gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_LEFT, margins->left); -+ gtk_layer_set_margin(gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, margins->right); -+ } -+ update_region_reservation(); - return; - } - -@@ -267,6 +298,9 @@ void WayfireAutohidingWindow::update_position() - get_allocated_height_or_width = &Gtk::Widget::get_allocated_height; - } - -+ if (!output->output) -+ return; -+ - /* When the position changes, show an animation from the new edge. */ - autohide_animation.animate(-(this->*get_allocated_height_or_width)(), 0); - start_draw_timer(); -@@ -492,6 +526,11 @@ void WayfireAutohidingWindow::setup_auto_exclusive_zone() - - void WayfireAutohidingWindow::update_auto_exclusive_zone() - { -+ if (region) -+ { -+ update_region_reservation(); -+ return; -+ } - if (this->auto_exclusive_zone) - { - gtk_layer_auto_exclusive_zone_enable(this->gobj()); -@@ -534,7 +573,7 @@ void WayfireAutohidingWindow::decrease_autohide() - - bool WayfireAutohidingWindow::should_autohide() const - { -- return autohide_counter && !this->active_button && !this->input_inside_panel; -+ return !region && autohide_counter && !this->active_button && !this->input_inside_panel; - } - - bool WayfireAutohidingWindow::m_do_hide() -@@ -559,6 +598,8 @@ gboolean WayfireAutohidingWindow::update_animation(Glib::RefPtr<Gdk::FrameClock> - - void WayfireAutohidingWindow::schedule_hide(int delay) - { -+ if (region) -+ return; - pending_show.disconnect(); - if (delay == 0) - { -@@ -583,6 +624,8 @@ bool WayfireAutohidingWindow::m_do_show() - - void WayfireAutohidingWindow::schedule_show(int delay) - { -+ if (region) -+ return; - pending_hide.disconnect(); - if (delay == 0) - { -@@ -635,7 +678,7 @@ void WayfireAutohidingWindow::set_active_popover(WayfireMenuWidget& button) - } - - /* Set this panel to forcibly show over fullscreen apps */ -- if (button.is_manual_popup() && force_show_popup.value()) -+ if (button.is_manual_popup() && (force_show_popup.value() || button.dismiss_on_outside_click)) - { - gtk_layer_set_layer(gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY); - } -@@ -650,6 +693,12 @@ void WayfireAutohidingWindow::set_active_popover(WayfireMenuWidget& button) - } - - schedule_show(0); -+ if (region && get_surface()) -+ { -+ // Fixed member bars skip the autohide animation, which normally -+ // commits these layer-shell changes before the popup takes its grab. -+ wl_surface_commit(get_wl_surface()); -+ } - } - - void WayfireAutohidingWindow::unset_active_popover() -@@ -688,6 +737,9 @@ void WayfireAutohidingWindow::unset_active_popover() - { - gtk_layer_set_layer(gobj(), GTK_LAYER_SHELL_LAYER_BACKGROUND); - } -+ -+ if (region && get_surface()) -+ wl_surface_commit(get_wl_surface()); - } - - WayfireMenuWidget*WayfireAutohidingWindow::get_active_popover() -@@ -732,3 +784,48 @@ void WayfireAutohidingWindow::update_autohide() - last_autohide_value = output->output && autohide_opt; - setup_auto_exclusive_zone(); - } -+ -+void WayfireAutohidingWindow::update_region_reservation() -+{ -+ if (region_group && get_surface()) -+ region_group->update(this, get_surface()->get_height() + (mirror_margin ? edge_margin.value() : 0)); -+} -+ -+void PanelRegionGroup::update(WayfireAutohidingWindow *window, int height) -+{ -+ heights[window] = std::max(1, height); -+ apply(); -+} -+ -+void PanelRegionGroup::remove(WayfireAutohidingWindow *window) -+{ -+ heights.erase(window); -+ apply(); -+} -+ -+void PanelRegionGroup::apply() -+{ -+ int maximum = 1; -+ for (const auto& [window, height] : heights) -+ maximum = std::max(maximum, height); -+ for (const auto& [window, height] : heights) -+ { -+ // Non-reserving members must ignore the shared workarea, otherwise -+ // they would be pushed below the first bar. -+ int zone = window == primary() ? maximum : -1; -+ if (gtk_layer_get_exclusive_zone(window->gobj()) != zone) -+ { -+ gtk_layer_set_exclusive_zone(window->gobj(), zone); -+ window->queue_draw(); -+ } -+ } -+} -+ -+WayfireAutohidingWindow *PanelRegionGroup::primary() const -+{ -+ WayfireAutohidingWindow *result = nullptr; -+ for (const auto& [window, height] : heights) -+ if (!result || window->region->x < result->region->x) -+ result = window; -+ return result; -+} -diff --git a/src/util/wf-autohide-window.hpp b/src/util/wf-autohide-window.hpp -index f921ba7..c4ef76a 100644 ---- a/src/util/wf-autohide-window.hpp -+++ b/src/util/wf-autohide-window.hpp -@@ -6,6 +6,9 @@ - #include <wayfire/util/duration.hpp> - - #include "wf-option-wrap.hpp" -+#include "output-topology.hpp" -+#include <map> -+#include <cmath> - #include "wf-popover.hpp" - - struct WayfireOutput; -@@ -16,6 +19,20 @@ struct zwf_hotspot_v2; - #define WF_WINDOW_POSITION_LEFT "left" - #define WF_WINDOW_POSITION_RIGHT "right" - -+class WayfireAutohidingWindow; -+// One reservation per parent desktop, sized to the tallest member bar. -+class PanelRegionGroup -+{ -+ friend class WayfireAutohidingWindow; -+ public: -+ void update(WayfireAutohidingWindow *window, int height); -+ void remove(WayfireAutohidingWindow *window); -+ private: -+ std::map<WayfireAutohidingWindow*, int> heights; -+ void apply(); -+ WayfireAutohidingWindow *primary() const; -+}; -+ - struct WayfireAutohidingWindowHotspotCallbacks; - /** - * A window which is anchored to an edge of the screen, and can autohide. -@@ -40,7 +57,12 @@ class WayfireAutohidingWindow : public Gtk::Window - * 9. section/edge_hotspot_size - * 10.section/adjacent_edge_hotspot_size - */ -- WayfireAutohidingWindow(WayfireOutput *output, const std::string& section); -+ WayfireAutohidingWindow(WayfireOutput *output, const std::string& section, -+ std::optional<shell::OutputGeometry> region = std::nullopt, -+ std::shared_ptr<PanelRegionGroup> group = nullptr); -+ bool is_region_panel() const { return region.has_value(); } -+ int region_height() const { return region ? std::lround(region->height) : 0; } -+ void refresh_region_geometry() { if (region) update_position(); } - WayfireAutohidingWindow(WayfireAutohidingWindow&&) = delete; - WayfireAutohidingWindow(const WayfireAutohidingWindow&) = delete; - WayfireAutohidingWindow& operator =(const WayfireAutohidingWindow&) = delete; -@@ -85,7 +107,12 @@ class WayfireAutohidingWindow : public Gtk::Window - } - - private: -+ friend class PanelRegionGroup; - WayfireOutput *output = nullptr; -+ std::optional<shell::OutputGeometry> region; -+ std::shared_ptr<PanelRegionGroup> region_group; -+ std::unique_ptr<WfOption<int>> side_margin; -+ void update_region_reservation(); - - std::vector<sigc::connection> signals; - -diff --git a/src/util/wf-ipc.cpp b/src/util/wf-ipc.cpp -index e392f72..101fd6b 100644 ---- a/src/util/wf-ipc.cpp -+++ b/src/util/wf-ipc.cpp -@@ -76,6 +76,9 @@ void WayfireIPC::disconnect() - - void WayfireIPC::send(const std::string& message) - { -+ if (!connected) -+ return; -+ - send_message(message); - response_handlers.push(0); - } -diff --git a/src/util/wf-popover.cpp b/src/util/wf-popover.cpp -index 18473a7..1d726de 100644 ---- a/src/util/wf-popover.cpp -+++ b/src/util/wf-popover.cpp -@@ -1,6 +1,7 @@ - #include <cstddef> - #include <cstdio> - #include <memory> -+#include <algorithm> - #include <gtk4-layer-shell.h> - - #include "wf-popover.hpp" -@@ -51,6 +52,60 @@ bool WayfireMenuWidget::is_manual_popup() - return this->autohide; - } - -+void WayfireMenuWidget::show_dismiss_windows() -+{ -+ dismiss_windows.clear(); -+ const auto display = Gdk::Display::get_default(); -+ if (!dismiss_css) -+ { -+ dismiss_css = Gtk::CssProvider::create(); -+ dismiss_css->load_from_data( -+ "window.wf-menu-dismiss-surface, window.wf-menu-dismiss-surface > contents {" -+ " background: transparent; box-shadow: none; border: none; }"); -+ // This is an input-only surface, not a themed window. User GTK CSS -+ // commonly paints all windows: if it wins, the Cairo clear below can -+ // leave transparent pixels with an opaque region that hides the apps -+ // underneath. Keep both the CSS background and opacity metadata clear. -+ Gtk::StyleContext::add_provider_for_display(display, dismiss_css, -+ GTK_STYLE_PROVIDER_PRIORITY_USER + 1); -+ } -+ auto monitors = display->get_monitors(); -+ for (guint i = 0; i < monitors->get_n_items(); ++i) -+ { -+ auto monitor = std::dynamic_pointer_cast<Gdk::Monitor>(monitors->get_object(i)); -+ auto window = std::make_unique<Gtk::Window>(); -+ window->set_decorated(false); -+ window->add_css_class("wf-menu-dismiss-surface"); -+ auto canvas = Gtk::make_managed<Gtk::DrawingArea>(); -+ canvas->set_draw_func([] (const Cairo::RefPtr<Cairo::Context>& context, int, int) -+ { -+ // An empty GtkWindow may never attach a buffer. An explicit -+ // transparent Cairo node keeps this input surface mapped. -+ context->set_operator(Cairo::Context::Operator::SOURCE); -+ context->set_source_rgba(0, 0, 0, 0); -+ context->paint(); -+ }); -+ window->set_child(*canvas); -+ gtk_layer_init_for_window(window->gobj()); -+ gtk_layer_set_namespace(window->gobj(), "wf-menu-dismiss"); -+ gtk_layer_set_monitor(window->gobj(), monitor->gobj()); -+ gtk_layer_set_layer(window->gobj(), GTK_LAYER_SHELL_LAYER_TOP); -+ gtk_layer_set_keyboard_mode(window->gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); -+ gtk_layer_set_exclusive_zone(window->gobj(), -1); -+ for (auto edge : {GTK_LAYER_SHELL_EDGE_LEFT, GTK_LAYER_SHELL_EDGE_RIGHT, -+ GTK_LAYER_SHELL_EDGE_TOP, GTK_LAYER_SHELL_EDGE_BOTTOM}) -+ gtk_layer_set_anchor(window->gobj(), edge, true); -+ auto click = Gtk::GestureClick::create(); -+ click->set_button(0); -+ click->signal_pressed().connect([this] (int, double, double) { popdown(); }); -+ window->add_controller(click); -+ // The owning panel and its popup use the overlay layer while this -+ // top-layer input surface is visible. Keep it alive until the gesture ends. -+ window->present(); -+ dismiss_windows.push_back(std::move(window)); -+ } -+} -+ - void WayfireMenuWidget::popup(bool autohide) - { - this->autohide = autohide; -@@ -61,26 +116,51 @@ void WayfireMenuWidget::popup(bool autohide) - return; - } - -- if (panel->is_active_popover(*this)) -+ if (panel->is_active_popover(*this) || (!use_menu && !use_widget)) - { - return; - } - -+ if (autohide && dismiss_on_outside_click) -+ show_dismiss_windows(); -+ -+ // Set up the parent before mapping the popup. Changing the layer or -+ // keyboard mode afterwards can steal the popup's input grab. -+ panel->set_active_popover(*this); -+ auto& popup = use_menu ? static_cast<Gtk::Popover&>(menu) : popover; -+ popup.set_autohide(autohide); -+ -+ if (panel->is_region_panel()) -+ { -+ // GTK sees the combined desktop as one monitor. Keep member popups -+ // inside their bar instead of letting them straddle a physical bezel. -+ const int available = std::max(1, panel->get_width() - 24); -+ scroll.set_max_content_width(available); -+ scroll.set_max_content_height(std::max(1, panel->region_height() - panel->get_height() - 48)); -+ // Hidden widgets measure as zero in GTK. Make it visible before -+ // measuring; both changes are committed in the same main-loop turn. -+ popup.set_visible(true); -+ int minimum, natural, min_baseline, nat_baseline; -+ popup.measure(Gtk::Orientation::HORIZONTAL, -1, minimum, natural, min_baseline, nat_baseline); -+ double x = 0, y = 0; -+ if (translate_coordinates(*panel, 0, 0, x, y)) -+ { -+ const double left = x + get_width() / 2.0 - natural / 2.0; -+ const double clamped = std::clamp(left, 0.0, double(std::max(0, panel->get_width() - natural))); -+ popup.set_offset(std::lround(clamped - left), 0); -+ } -+ } -+ - if (use_menu) - { - menu.popup(); - } else if (use_widget) - { -- popover.set_autohide(autohide); - popover.popup(); -- } else -- { -- return; - } - - add_css_class("selected"); - -- panel->set_active_popover(*this); - popup_signal.emit(); - } - -@@ -117,6 +197,8 @@ void WayfireMenuWidget::popup_timed(int millis) - void WayfireMenuWidget::popdown() - { - cancel_timer(); -+ for (auto& window : dismiss_windows) -+ window->hide(); - auto panel = get_panel(this); - if (!panel) - { -@@ -270,6 +352,9 @@ WayfireMenuWidget::WayfireMenuWidget(const std::string& section, std::string nam - - WayfireMenuWidget::~WayfireMenuWidget() - { -+ dismiss_windows.clear(); -+ if (dismiss_css) -+ Gtk::StyleContext::remove_provider_for_display(Gdk::Display::get_default(), dismiss_css); - for (auto signal : signals) - { - signal.disconnect(); -diff --git a/src/util/wf-popover.hpp b/src/util/wf-popover.hpp -index 1e361e6..ad1b8f1 100644 ---- a/src/util/wf-popover.hpp -+++ b/src/util/wf-popover.hpp -@@ -23,6 +23,10 @@ class WayfireMenuWidget : public Gtk::Box - Gtk::PopoverMenu menu; - bool use_menu = false, use_widget = false; - bool interactive = false; -+ bool dismiss_on_outside_click = false; -+ std::vector<std::unique_ptr<Gtk::Window>> dismiss_windows; -+ Glib::RefPtr<Gtk::CssProvider> dismiss_css; -+ void show_dismiss_windows(); - WfOption<std::string> panel_position; - Glib::RefPtr<Gtk::GestureClick> previous_controller = nullptr; - -@@ -95,6 +99,9 @@ class WayfireMenuWidget : public Gtk::Box - * By default, the menu button interacts with the keyboard. - */ - void set_keyboard_interactive(bool interactive); -+ // Cover empty desktop areas too, where a compositor may not dismiss a -+ // native popup because an outside click does not change keyboard focus. -+ void set_dismiss_on_outside_click(bool enabled) { dismiss_on_outside_click = enabled; } - - /** @return Whether the menu button interacts with the keyboard */ - bool is_keyboard_interactive() const; -diff --git a/src/util/wf-shell-app.cpp b/src/util/wf-shell-app.cpp -index a59efe9..5eb0d83 100644 ---- a/src/util/wf-shell-app.cpp -+++ b/src/util/wf-shell-app.cpp -@@ -37,7 +37,7 @@ std::string WayfireShellApp::get_css_config_dir() - { - if (cmdline_css.has_value()) - { -- return cmdline_config.value(); -+ return cmdline_css.value(); - } - - std::string config_dir; -@@ -126,7 +126,7 @@ bool WayfireShellApp::parse_cssfile(const Glib::ustring & option_name, - } - - #define INOT_BUF_SIZE (1024 * sizeof(inotify_event)) --char buf[INOT_BUF_SIZE]; -+alignas(inotify_event) char buf[INOT_BUF_SIZE]; - - static void do_reload_css(WayfireShellApp *app) - { -@@ -144,9 +144,21 @@ static void do_reload_config(WayfireShellApp *app) - /* Handle inotify event */ - static bool handle_inotify_event(WayfireShellApp *app, Glib::IOCondition cond) - { -- /* read, but don't use */ -- read(app->inotify_fd, buf, INOT_BUF_SIZE); -- do_reload_config(app); -+ const auto length = read(app->inotify_fd, buf, INOT_BUF_SIZE); -+ const auto filename = std::filesystem::path(app->get_config_file()).filename().string(); -+ bool changed = false; -+ for (ssize_t offset = 0; offset + static_cast<ssize_t>(sizeof(inotify_event)) <= length;) -+ { -+ const auto *event = reinterpret_cast<const inotify_event*>(buf + offset); -+ if (event->len && filename == event->name && -+ (event->mask & (IN_CLOSE_WRITE | IN_MOVED_TO))) -+ changed = true; -+ -+ offset += sizeof(inotify_event) + event->len; -+ } -+ -+ if (changed) -+ do_reload_config(app); - - return true; - } -@@ -217,9 +229,10 @@ void WayfireShellApp::on_activate() - inotify_css_fd = inotify_init(); - do_reload_css(this); - -- inotify_add_watch(inotify_fd, -- get_config_file().c_str(), -- IN_CLOSE_WRITE); -+ // Watch the directory so atomic replacements from editors/wf-appearance -+ // keep reloading after the original config inode has been removed. -+ const auto config_directory = std::filesystem::absolute(get_config_file()).parent_path(); -+ inotify_add_watch(inotify_fd, config_directory.c_str(), IN_CLOSE_WRITE | IN_MOVED_TO); - inotify_add_watch(inotify_css_fd, - get_css_config_dir().c_str(), - IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE); -@@ -342,9 +355,9 @@ void WayfireShellApp::rem_output(GMonitor monitor) - auto it = std::find_if(monitors.begin(), monitors.end(), - [monitor] (auto& output) { return output->monitor == monitor; }); - -- handle_output_removed(it->get()); - if (it != monitors.end()) - { -+ handle_output_removed(it->get()); - monitors.erase(it); - } else - { -@@ -416,6 +429,17 @@ WayfireOutput::WayfireOutput(const GMonitor& monitor, - { - this->output = - zwf_shell_manager_v2_get_wf_output(zwf_manager, this->wo); -+ // The protocol object outlives individual panels and can have one listener. -+ // Fan out events here so output-mode reloads never leave a stale window pointer. -+ static const zwf_output_v2_listener listener = { -+ .enter_fullscreen = [] (void*, zwf_output_v2*) {}, -+ .leave_fullscreen = [] (void*, zwf_output_v2*) {}, -+ .toggle_menu = [] (void *data, zwf_output_v2*) -+ { -+ static_cast<WayfireOutput*>(data)->m_toggle_menu_signal.emit(); -+ }, -+ }; -+ zwf_output_v2_add_listener(this->output, &listener, this); - } else - { - this->output = nullptr; -diff --git a/tests/meson.build b/tests/meson.build -new file mode 100644 -index 0000000..53ef4c5 ---- /dev/null -+++ b/tests/meson.build -@@ -0,0 +1,21 @@ -+service_test = executable('shell-services-test', -+ 'shell-services.cpp', -+ '../src/util/mpris-service.cpp', -+ '../src/panel/widgets/notifications/daemon.cpp', -+ '../src/panel/widgets/notifications/notification-info.cpp', -+ include_directories: include_directories('../src', '../src/util'), -+ cpp_args: ['-UNDEBUG'], dependencies: gtkmm) -+test('shell-services', find_program('dbus-run-session'), args: ['--', service_test], timeout: 30) -+ -+output_test = executable('output-topology-test', -+ 'output-topology.cpp', '../src/util/output-topology.cpp', -+ include_directories: include_directories('../src/util'), cpp_args: ['-UNDEBUG'], dependencies: json) -+test('output-topology', output_test) -+ -+window_title_test = executable('window-title-test', -+ 'window-title.cpp', '../src/util/window-title.cpp', -+ include_directories: include_directories('../src/util'), cpp_args: ['-UNDEBUG'], dependencies: json) -+test('window-title', window_title_test) -+ -+dock_test = executable('dock-layout-test', 'dock-layout.cpp', cpp_args: ['-UNDEBUG']) -+test('dock-layout', dock_test) -diff --git a/tests/output-topology.cpp b/tests/output-topology.cpp -new file mode 100644 -index 0000000..f059054 ---- /dev/null -+++ b/tests/output-topology.cpp -@@ -0,0 +1,94 @@ -+#include "output-topology.hpp" -+#include "ipc-geometry.hpp" -+#include <cassert> -+#include <iostream> -+ -+static wf::json_t parse(const char *text) -+{ -+ wf::json_t result; -+ assert(!wf::json_t::parse_string(text, result)); -+ return result; -+} -+ -+int main() -+{ -+ auto numbers = parse(R"([1280, 1280.0, -12.75, null, "bad", 1e30])"); -+ assert(ipc_pixel(numbers[size_t(0)]) == 1280); -+ assert(ipc_pixel(numbers[1]) == 1280); -+ assert(ipc_pixel(numbers[2]) == -13); -+ assert(ipc_number(numbers[2]) == -12.75); -+ assert(!ipc_number(numbers[3])); -+ assert(!ipc_number(numbers[4])); -+ assert(ipc_pixel(numbers[5], 1) == 1); -+ -+ // Per-member margins use output-local coordinates. Every member keeps -+ // its own side gaps even though all surfaces share one wl_output. -+ for (int member = 0; member < 3; ++member) -+ { -+ auto margins = shell::region_panel_margins({member * 3840.0, 0, 3840, 2160}, 11520, 2160, 15); -+ assert(margins && margins->left == member * 3840 + 15); -+ assert(11520 - margins->left - margins->right == 3810); -+ } -+ assert(!shell::region_panel_margins({0, 1080, 3840, 1080}, 3840, 2160, 15)); -+ assert(!shell::region_panel_margins({0, 0, 3840, 1080}, 3840, 2160, 15)); -+ assert(!shell::region_panel_margins({3840, 0, 3840, 2160}, 3840, 2160, 15)); -+ assert(!shell::region_panel_margins({0, 0, 20, 2160}, 3840, 2160, 15)); -+ assert(!shell::region_panel_margins({0, 0, 3840, 2160}, 3840, 2160, -1)); -+ auto fractional = shell::region_panel_margins({1280.5, 0, 1280.5, 1440}, 2561, 1440, 15); -+ assert(fractional && fractional->left == 1296 && fractional->right == 15); -+ -+ auto json = parse(R"([ -+ {"name":"Desk with spaces", "members":[ -+ {"name":"DP-1", "geometry":{"x":0,"y":0,"width":3840,"height":2160}}, -+ {"name":"DP-10", "geometry":{"x":3840.0,"y":0.0,"width":3840.0,"height":2160.0}} -+ ]}, -+ {"name":"HDMI-A-2"} -+ ])"); -+ auto topology = shell::parse_output_topology(json); -+ assert(topology && topology->size() == 2); -+ assert((*topology)[0].members[1].geometry.x == 3840.0); -+ const std::vector<std::string> visible = {"Desk with spaces", "HDMI-A-2"}; -+ -+ auto all = shell::select_panel_targets(topology, visible, "all", "*"); -+ assert(all.error.empty() && all.targets.size() == 2); -+ assert(!all.targets[0].region); -+ auto combined = shell::select_panel_targets(topology, visible, "virtual", "*"); -+ assert(combined.targets.size() == 1 && combined.targets[0].name == "Desk with spaces"); -+ auto physical = shell::select_panel_targets(topology, visible, "physical", "*"); -+ assert(physical.targets.size() == 3); -+ assert(physical.targets[0].region && physical.targets[0].desktop == "Desk with spaces"); -+ assert(physical.targets[1].name == "DP-10"); -+ assert(!physical.targets[2].region); -+ -+ auto filtered = shell::select_panel_targets(topology, visible, "physical", "DP-1, HDMI-A-2"); -+ assert(filtered.targets.size() == 2 && filtered.targets[1].name == "HDMI-A-2"); -+ assert(!shell::output_name_matches("DP-10", "DP-1")); -+ assert(!shell::output_name_matches("DP-*", "DP-1")); -+ assert(shell::output_name_matches("DP-1 DP-10", "DP-10")); -+ assert(!shell::output_name_matches("", "DP-1")); -+ -+ // GDK and IPC may announce grouping/removal in either order. Never create a -+ // panel for a stale parent while waiting for the other event stream. -+ auto removed = shell::select_panel_targets(topology, {"HDMI-A-2"}, "physical", "*"); -+ assert(removed.targets.size() == 1 && !removed.targets[0].region); -+ auto ungrouped = shell::parse_output_topology(parse(R"([{"name":"DP-1"},{"name":"DP-10"}])")); -+ auto plain = shell::select_panel_targets(ungrouped, {"DP-1","DP-10"}, "physical", "*"); -+ assert(plain.targets.size() == 2 && !plain.targets[0].region); -+ assert(shell::select_panel_targets(ungrouped, {"DP-1"}, "virtual", "*").targets.empty()); -+ -+ // Names alone carry no type information, including old STITCH and new VIRTUAL names. -+ auto named = shell::parse_output_topology(parse(R"([{"name":"VIRTUAL-1"},{"name":"STITCH-1"}])")); -+ assert(shell::select_panel_targets(named, {"VIRTUAL-1","STITCH-1"}, "virtual", "*").targets.empty()); -+ assert(shell::select_panel_targets(std::nullopt, visible, "physical", "*").targets.empty()); -+ assert(!shell::select_panel_targets(std::nullopt, visible, "physical", "*").error.empty()); -+ assert(shell::select_panel_targets(std::nullopt, visible, "all", "*").targets.size() == 2); -+ assert(!shell::select_panel_targets(topology, visible, "typo", "*").error.empty()); -+ -+ assert(!shell::parse_output_topology(parse(R"({"error":"no method"})"))); -+ assert(!shell::parse_output_topology(parse(R"([{"name":"x", "members":{}}])"))); -+ assert(!shell::parse_output_topology(parse(R"([{"name":"x"},{"name":"x"}])"))); -+ assert(!shell::parse_output_topology(parse(R"([{"name":"x","members":[{"name":"DP-1","geometry":{"x":0,"y":0,"width":0,"height":10}}]}])"))); -+ assert(!shell::parse_output_topology(parse(R"([{"name":"x","members":[{"name":"DP-1","geometry":{"x":0,"y":0,"width":"10","height":10}}]}])"))); -+ assert(shell::parse_output_topology(parse("[]"))->empty()); -+ std::cout << "Geometry compatibility and output-selection tests passed\n"; -+} -diff --git a/tests/shell-services.cpp b/tests/shell-services.cpp -new file mode 100644 -index 0000000..15993d0 ---- /dev/null -+++ b/tests/shell-services.cpp -@@ -0,0 +1,140 @@ -+#include "mpris-service.hpp" -+#include "panel/widgets/notifications/daemon.hpp" -+#include <glib.h> -+#include <cassert> -+#include <functional> -+#include <iostream> -+ -+static void until(std::function<bool()> ready) -+{ -+ const auto deadline = g_get_monotonic_time() + 5 * G_TIME_SPAN_SECOND; -+ while (!ready() && g_get_monotonic_time() < deadline) -+ { -+ while (g_main_context_iteration(nullptr, false)) {} -+ g_usleep(1000); -+ } -+ assert(ready()); -+} -+ -+static int commands = 0; -+static GVariant *property(GDBusConnection*, const char*, const char*, const char*, const char *name, GError**, void*) -+{ -+ if (std::string(name) == "PlaybackStatus") return g_variant_new_string("Playing"); -+ if (std::string(name) == "Metadata") -+ { -+ GVariantBuilder metadata; -+ g_variant_builder_init(&metadata, G_VARIANT_TYPE_VARDICT); -+ g_variant_builder_add(&metadata, "{sv}", "xesam:title", g_variant_new_string("A test track")); -+ const char *artists[] = {"Artist one", "Artist two", nullptr}; -+ g_variant_builder_add(&metadata, "{sv}", "xesam:artist", g_variant_new_strv(artists, -1)); -+ return g_variant_builder_end(&metadata); -+ } -+ return g_variant_new_boolean(std::string(name) != "CanGoPrevious"); -+} -+ -+static void method(GDBusConnection*, const char*, const char*, const char*, const char*, GVariant*, GDBusMethodInvocation *invocation, void*) -+{ -+ ++commands; -+ g_dbus_method_invocation_return_value(invocation, nullptr); -+} -+ -+static unsigned notify(GDBusConnection *connection) -+{ -+ unsigned id = 0; -+ g_dbus_connection_call(connection, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", -+ "org.freedesktop.Notifications", "Notify", -+ g_variant_new("(susssasa{sv}i)", "Test", 0u, "", "Test notification", "Body", nullptr, nullptr, -1), -+ G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, -+ [] (GObject *object, GAsyncResult *result, void *data) -+ { -+ GError *error = nullptr; -+ auto reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), result, &error); -+ if (error) std::cerr << error->message << '\n'; -+ assert(reply); -+ g_variant_get(reply, "(u)", static_cast<unsigned*>(data)); -+ g_variant_unref(reply); -+ }, &id); -+ until([&] { return id != 0; }); -+ return id; -+} -+ -+int main() -+{ -+ Gio::init(); -+ auto connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); -+ assert(connection); -+ const char *xml = R"(<node><interface name="org.mpris.MediaPlayer2.Player"> -+ <method name="PlayPause"/><method name="Next"/><method name="Previous"/> -+ <property name="PlaybackStatus" type="s" access="read"/> -+ <property name="Metadata" type="a{sv}" access="read"/> -+ <property name="CanControl" type="b" access="read"/> -+ <property name="CanPlay" type="b" access="read"/> -+ <property name="CanPause" type="b" access="read"/> -+ <property name="CanGoNext" type="b" access="read"/> -+ <property name="CanGoPrevious" type="b" access="read"/> -+ </interface></node>)"; -+ auto info = g_dbus_node_info_new_for_xml(xml, nullptr); -+ GDBusInterfaceVTable table = {}; -+ table.method_call = method; -+ table.get_property = property; -+ auto object = g_dbus_connection_register_object(connection, "/org/mpris/MediaPlayer2", info->interfaces[0], &table, nullptr, nullptr, nullptr); -+ assert(object); -+ auto service = MprisService::get(); -+ assert(service == MprisService::get()); -+ const std::string name = "org.mpris.MediaPlayer2.wftest"; -+ auto owner = g_bus_own_name_on_connection(connection, name.c_str(), G_BUS_NAME_OWNER_FLAGS_NONE, nullptr, nullptr, nullptr, nullptr); -+ until([&] { return service->players().size() == 1; }); -+ assert(service->players().at(name).title == "A test track"); -+ assert(service->players().at(name).artist == "Artist one, Artist two"); -+ service->command(name, "Previous"); // Unsupported actions must not reach the player. -+ service->command(name, "Next"); -+ until([] { return commands == 1; }); -+ -+ GVariantBuilder changed, invalidated; -+ g_variant_builder_init(&changed, G_VARIANT_TYPE_VARDICT); -+ g_variant_builder_add(&changed, "{sv}", "CanControl", g_variant_new_boolean(false)); -+ g_variant_builder_init(&invalidated, G_VARIANT_TYPE_STRING_ARRAY); -+ g_dbus_connection_emit_signal(connection, nullptr, "/org/mpris/MediaPlayer2", -+ "org.freedesktop.DBus.Properties", "PropertiesChanged", -+ g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed, &invalidated), nullptr); -+ until([&] { return !service->players().at(name).can_control; }); -+ service->command(name, "Next"); -+ assert(commands == 1); -+ g_bus_unown_name(owner); -+ until([&] { return service->players().empty(); }); -+ owner = g_bus_own_name_on_connection(connection, name.c_str(), G_BUS_NAME_OWNER_FLAGS_NONE, nullptr, nullptr, nullptr, nullptr); -+ until([&] { return service->players().size() == 1; }); -+ g_bus_unown_name(owner); -+ service.reset(); // Pending async work must be safe after the last view disappears. -+ -+ auto daemon = Daemon::Launch(); -+ assert(daemon == Daemon::Launch()); -+ until([&] -+ { -+ auto reply = g_dbus_connection_call_sync(connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", -+ "org.freedesktop.DBus", "NameHasOwner", g_variant_new("(s)", "org.freedesktop.Notifications"), -+ G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, 500, nullptr, nullptr); -+ gboolean owned = false; -+ if (reply) { g_variant_get(reply, "(b)", &owned); g_variant_unref(reply); } -+ return bool(owned); -+ }); -+ auto id = notify(connection); -+ assert(daemon->getNotifications().count(id) == 1); -+ int dnd_changes = 0; -+ auto signal = daemon->state_changed.connect([&] { ++dnd_changes; }); -+ daemon->set_dnd(true); -+ daemon->set_dnd(true); -+ assert(Daemon::Instance()->dnd() && dnd_changes == 1); -+ daemon->closeNotification(id, Daemon::Dismissed); -+ assert(daemon->getNotifications().empty()); -+ notify(connection); -+ notify(connection); -+ daemon->clear(); -+ assert(daemon->getNotifications().empty()); -+ signal.disconnect(); -+ daemon.reset(); -+ g_dbus_connection_unregister_object(connection, object); -+ g_dbus_node_info_unref(info); -+ g_object_unref(connection); -+ std::cout << "Media discovery, capabilities, updates, reconnect and notification/DND tests passed\n"; -+} -diff --git a/wf-shell.ini.example b/wf-shell.ini.example -index d806c5c..de5cbdf 100644 ---- a/wf-shell.ini.example -+++ b/wf-shell.ini.example -@@ -17,6 +17,18 @@ fade_duration = 1000 - - - [panel] -+# Output target policy: all (default), virtual, physical. -+# virtual selects combined desktops by IPC member metadata, not by output name. -+# physical supports fixed top/bottom bars on horizontal members; see docs/OUTPUT-TARGETS.md. -+# output_mode = all -+# outputs = * -+# For top/right/bottom/left gaps of 15/15/0/15: -+# edge_margin = 15 -+# side_margin = 15 -+# mirror_margin = false -+# autohide = false -+# Use widgets_center = window-title for the most recently focused visible window -+# on each selected monitor. window_title_max_chars defaults to 72. - # widgets_* is a space-separated list of widgets to be displayed - # at the corresponding part of the panel - # Supported widgets are: -@@ -73,6 +85,11 @@ autohide_duration = 300 - - # A comma separated list of output names on which to show panel instances. Set to * wildcard for all outputs. - # outputs = * -+# For top/right/bottom/left gaps of 15/15/0/15: -+# edge_margin = 15 -+# side_margin = 15 -+# mirror_margin = false -+# autohide = false - - # layer can be top, bottom, overlay or background - layer = top -@@ -398,6 +415,14 @@ commands_output_max_chars = 10 - # workspace_switcher_render_views = true - - [dock] -+# Optional perspective dock hosted inside wf-panel (do not also start wf-dock). -+# See docs/INTEGRATED-DOCK.md for controls and current limitations. -+enabled = false -+output = auto -+layer = top -+launchers = -+zoom = 1.5 -+reflection = 0.435 - # if dock should autohide. Default true - autohide = true - -@@ -629,3 +654,12 @@ instant_unlock_always = false - # Font used for the button - instant_unlock_font = default - -+# Additional panel widgets: control-center, media -+# Set these in the [panel] section: -+# control_center_width = 420 -+# control_center_notifications = true # restart panel after changing -+# control_center_network_command = nm-connection-editor # or connman-gtk -+# control_center_bluetooth_command = blueman-manager -+# control_center_appearance_command = wf-appearance -+# Commands accept quoted arguments; shell expressions are not expanded. -+# A complete optional appearance preset lives in data/presets/wayfire-mist.ini. -diff --git a/docs/GENTOO.md b/docs/GENTOO.md -new file mode 100644 -index 0000000..fc76cc4 ---- /dev/null -+++ b/docs/GENTOO.md -@@ -0,0 +1,53 @@ -+# Gentoo packages -+ -+The personal overlay contains: -+ -+- `gui-apps/wf-shell-0.11.0-r8`: the official 0.11.0 release archive, Gentoo's -+ network null-pointer fix, and `files/wf-shell-0.11.0-r8-shell-experience.patch`. -+- `gui-apps/wf-appearance-0.1.0`: a versioned source snapshot under the package's -+ `files/wf-appearance-0.1.0/`, with the Python module, CSS data, tests, license, -+ command-line launcher and desktop entry. -+ -+Neither ebuild reads `/root/projects`. Both build without a graphical session. -+The release tarball includes wf-json and gvc, so the shell no longer needs the -+separate August snapshot distfiles. Keep those old Manifest entries for the -+existing pre-release ebuild. -+ -+```sh -+emerge -av '=gui-apps/wf-shell-0.11.0-r8::personal' \ -+ '=gui-apps/wf-appearance-0.1.0::personal' -+``` -+ -+The shell defaults to `appearance ddcutil pipewire pulseaudio`. `pipewire` supplies -+the embedded audio mixer; `pulseaudio` supplies the older standalone volume -+widget. `appearance` pulls in the companion app and can be disabled if desired. -+`test` enables headless service/topology tests. The appearance package uses -+`PYTHON_SINGLE_TARGET` and runs policy tests under `USE=test`. -+ -+The packages install no session autostart and do not overwrite user settings. -+The optional [integrated dock](INTEGRATED-DOCK.md) runs inside wf-panel when -+`dock/enabled=true`; it does not require launching a second executable. -+The appearance launcher is `/usr/bin/wf-appearance`, with an Appearance & Scaling -+entry in the application menu. The control-center shortcut resolves it via PATH. -+ -+To preview the optional style in a separate/test Wayland session: -+ -+```sh -+wf-panel -c /usr/share/wf-shell/presets/wayfire-mist.ini \ -+ -s /usr/share/wf-shell/themes/wayfire-mist -+``` -+ -+Do not pass the entire `/usr/share/wf-shell/css` directory as the theme: it holds -+independent example styles. For an existing configuration, add the desired -+widgets and use `panel/css_path` to select the Mist CSS. The optional preset uses -+ConnMan/Blueman shortcuts; choose installed settings tools for your session. -+ -+Output selection, horizontal physical-member bars and the remaining limitations are -+documented in [OUTPUT-TARGETS.md](OUTPUT-TARGETS.md). GTK text sizing, Qt/Electron -+launch wrappers, and current appearance limitations are documented in the -+companion README installed under `/usr/share/doc/wf-appearance-0.1.0/`. -+ -+The release development tree is `/root/projects/wf-shell-polish/wf-shell-0.11.0`. -+The initial snapshot remains available for reference. Future shell changes need -+to refresh the release feature patch; future appearance releases should use a new -+versioned snapshot directory and ebuild. -diff --git a/docs/INTEGRATED-DOCK.md b/docs/INTEGRATED-DOCK.md -new file mode 100644 -index 0000000..6da1324 ---- /dev/null -+++ b/docs/INTEGRATED-DOCK.md -@@ -0,0 +1,77 @@ -+# Integrated dock -+ -+`wf-panel` can host a centered bottom dock in the same process as the bars. -+The dock draws a dark perspective shelf, faded icon reflections, a smooth -+neighboring-icon magnification wave, and white running-window indicators. -+Application icons retain their original colors. -+ -+```ini -+[dock] -+enabled = true -+output = VIRTUAL-1 -+layer = overlay -+position = bottom -+full_span = false -+autohide = true -+autohide_duration = 180 -+autohide_show_delay = 150 -+autohide_hide_delay = 500 -+minimal_height = 0 -+minimal_width = 0 -+icon_height = 64 -+edge_margin = 10 -+mirror_margin = false -+zoom = 1.5 -+reflection = 0.435 -+launchers = Alacritty.desktop librewolf.desktop | org.gnome.Calculator.desktop -+``` -+ -+`enabled` defaults to false. Do not start the separate `wf-dock` executable -+when this is enabled. The legacy executable remains available. -+ -+`layer=top` (the default) uses the regular panel layer. Choose `layer=overlay` -+to keep the dock above application windows, including always-on-top and -+fullscreen windows. Layer changes apply immediately on configuration reload. -+ -+`output` names one compositor-visible output. With a three-member horizontal -+`VIRTUAL-1`, a single dock is centered across that virtual desktop, while -+`panel/output_mode=physical` can still put a bar on each member. `auto` chooses -+the first output hosting a panel. A missing explicit output leaves the dock -+absent until that output returns. This first implementation supports bottom -+placement and `full_span=false`; it does not replicate onto physical members. -+ -+`launchers` is a space-separated list of desktop file IDs or absolute desktop -+file paths. `|` inserts a separator. Missing apps are skipped. Paths containing -+spaces should be installed under an application ID in the XDG applications -+directory instead. Desktop file IDs and StartupWMClass associate running windows -+with launchers. Other applications appear after the pinned group, in stable -+order, including minimized windows and windows on other workspaces/outputs. -+ -+- Click a launcher to start it or focus its window. -+- Click an already focused single-window application to minimize it. -+- Click an active group, or scroll over it, to cycle its windows. -+- Middle-click a launcher to start another instance (subject to the app's own -+ single-instance behavior). -+ -+`zoom` is clamped to 1–2; 1 disables magnification. `reflection` ranges from -+0–1; 0 disables reflections. Icons shrink to fit a narrow output. Transparent -+padding passes clicks through to applications. The floating dock does not -+reserve workarea space. Autohide uses the existing Wayfire shell edge hotspots. -+ -+The view subscription uses the panel's shared Wayfire IPC transport (`ipc` and -+`ipc-rules`; minimizing also needs `wm-actions`). Without IPC, pinned launchers -+still work but running windows cannot be listed or controlled. Icon rasterization -+is cached at the output's scale, stale icons are discarded, and the animation -+frame callback stops when its wave settles. There is no permanent frame timer -+or view polling loop. -+ -+This is an initial pointer-operated dock, not a Cairo-Dock plugin host. Cairo -+applets, drag-and-drop reordering, context menus, and keyboard/accessibility -+navigation are not implemented yet. Pin order is edited in the configuration. -+ -+Validation includes layout/hit-testing tests and an isolated Wayfire session: -+launching desktop files, StartupWMClass grouping, minimize/restore, new instances, -+cycling groups, transparent pixels and click-through under global GTK user CSS, -+output/configuration changes, edge reveal/autohide, and the existing Apps menu -+interaction and visibility regressions. Physical hotplug remains a live hardware -+acceptance test. -diff --git a/src/panel/dock-layout.hpp b/src/panel/dock-layout.hpp -new file mode 100644 -index 0000000..1d9fef6 ---- /dev/null -+++ b/src/panel/dock-layout.hpp -@@ -0,0 +1,52 @@ -+#pragma once -+ -+#include <algorithm> -+#include <cmath> -+#include <vector> -+ -+namespace shell -+{ -+struct DockSlot -+{ -+ double x, width, scale; -+ bool separator; -+}; -+ -+// Sample the wave against resting positions, so enlarged icons cannot feed -+// their moving positions back into the animation and make it oscillate. -+inline std::vector<DockSlot> dock_layout(const std::vector<bool>& separators, -+ double icon, double zoom, double width, double pointer, double amount) -+{ -+ double base = 0; -+ const double gap = icon * .09375; -+ for (bool separator : separators) base += separator ? icon * .24 : icon + gap; -+ double x = (width - base) / 2, expanded = 0; -+ std::vector<DockSlot> result; -+ for (bool separator : separators) -+ { -+ double rest = separator ? icon * .24 : icon + gap; -+ double distance = std::abs(x + rest / 2 - pointer) / (icon * 2.5); -+ double wave = distance < 1 ? (1 + std::cos(distance * 3.141592653589793)) / 2 : 0; -+ double scale = separator ? 1 : 1 + (zoom - 1) * std::clamp(amount, 0.0, 1.0) * wave; -+ double size = separator ? rest : icon * scale + gap; -+ result.push_back({0, size, scale, separator}); -+ expanded += size; -+ x += rest; -+ } -+ x = (width - expanded) / 2; -+ for (auto& slot : result) -+ { -+ slot.x = x; -+ x += slot.width; -+ } -+ return result; -+} -+ -+inline int dock_hit(const std::vector<DockSlot>& slots, double x) -+{ -+ for (size_t i = 0; i < slots.size(); ++i) -+ if (!slots[i].separator && x >= slots[i].x && x < slots[i].x + slots[i].width) -+ return i; -+ return -1; -+} -+} -diff --git a/src/panel/integrated-dock.cpp b/src/panel/integrated-dock.cpp -new file mode 100644 -index 0000000..bc31c53 ---- /dev/null -+++ b/src/panel/integrated-dock.cpp -@@ -0,0 +1,565 @@ -+#include "integrated-dock.hpp" -+#include "dock-layout.hpp" -+#include "panel.hpp" -+#include "ipc-geometry.hpp" -+#include <gtkmm.h> -+#include <giomm/desktopappinfo.h> -+#include <giomm/appinfomonitor.h> -+#include <gtk4-layer-shell.h> -+#include <cairomm/region.h> -+#include <iostream> -+#include <map> -+#include <set> -+#include <sstream> -+ -+namespace -+{ -+std::string app_key(std::string value) -+{ -+ value = Glib::ustring(value).lowercase(); -+ if (value.size() > 8 && value.substr(value.size() - 8) == ".desktop") -+ value.resize(value.size() - 8); -+ return value; -+} -+ -+bool flag(const wf::json_reference_t& value) -+{ -+ return value.is_bool() && value.as_bool(); -+} -+ -+struct DockView -+{ -+ uint64_t id; -+ bool active, minimized; -+ double focus; -+ std::string title; -+}; -+ -+struct DockEntry -+{ -+ std::string key, label; -+ Glib::RefPtr<Gio::DesktopAppInfo> app; -+ std::vector<DockView> views; -+ Cairo::RefPtr<Cairo::ImageSurface> icon; -+ bool separator = false; -+}; -+} -+ -+struct IntegratedDock::Impl : IIPCSubscriber -+{ -+ WayfireOutput *output; -+ WayfireAutohidingWindow window; -+ Gtk::DrawingArea canvas; -+ Glib::RefPtr<Gtk::CssProvider> css; -+ Glib::RefPtr<Gtk::IconTheme> theme; -+ Glib::RefPtr<Gio::AppInfoMonitor> app_monitor; -+ sigc::connection pending, theme_changed, apps_changed, scale_changed, geometry_changed; -+ std::shared_ptr<IPCClient> client; -+ WfOption<std::string> launchers{"dock/launchers"}; -+ WfOption<std::string> layer{"dock/layer"}; -+ WfOption<int> icon_size{"dock/icon_height"}; -+ WfOption<double> zoom_option{"dock/zoom"}, reflection_option{"dock/reflection"}; -+ std::map<std::string, Glib::RefPtr<Gio::DesktopAppInfo>> apps; -+ std::map<std::string, Cairo::RefPtr<Cairo::ImageSurface>> icons; -+ std::vector<DockEntry> entries; -+ std::vector<std::string> running_order; -+ wf::json_t views; -+ bool in_flight = false, dirty = false, inside = false; -+ double pointer = -10000, wave_pointer = -10000, amount = 0; -+ double icon = 64, zoom = 1.5; -+ int height = 160, hover = -1; -+ unsigned tick = 0; -+ gint64 last_frame = 0; -+ std::string pressed_key; -+ -+ explicit Impl(WayfireOutput *output) : output(output), window(output, "dock") -+ { -+ window.add_css_class("wf-integrated-dock"); -+ window.set_title("wf-shell Dock"); -+ gtk_layer_set_namespace(window.gobj(), "wf-dock"); -+ apply_layer(); -+ window.set_auto_exclusive_zone(false); -+ // User GTK themes can paint window > contents opaque. The outer -+ // surface must remain transparent, independently of the shelf below. -+ css = Gtk::CssProvider::create(); -+ css->load_from_data(R"( -+ window.wf-integrated-dock, window.wf-integrated-dock > contents { -+ background: transparent; box-shadow: none; border: none; -+ padding: 0; margin: 0; min-width: 0; min-height: 0; -+ } -+ )"); -+ Gtk::StyleContext::add_provider_for_display(window.get_display(), css, -+ GTK_STYLE_PROVIDER_PRIORITY_USER + 1); -+ window.set_child(canvas); -+ canvas.set_draw_func(sigc::mem_fun(*this, &Impl::draw)); -+ auto motion = Gtk::EventControllerMotion::create(); -+ motion->signal_motion().connect([this] (double x, double) { move(x); }); -+ motion->signal_enter().connect([this] (double x, double) { inside = true; move(x); }); -+ motion->signal_leave().connect([this] { inside = false; hover = -1; animate(); }); -+ canvas.add_controller(motion); -+ auto click = Gtk::GestureClick::create(); -+ click->set_button(0); -+ click->signal_pressed().connect([this] (int, double x, double) -+ { -+ int index = shell::dock_hit(layout(), x); -+ pressed_key = index >= 0 ? entries[index].key : ""; -+ }); -+ click->signal_released().connect([this, gesture = click.get()] (int, double x, double) -+ { -+ int index = shell::dock_hit(layout(), x); -+ if (index >= 0 && !pressed_key.empty() && entries[index].key == pressed_key) -+ activate(entries[index], gesture->get_current_button() == 2, -+ gesture->get_current_button() == 1); -+ pressed_key.clear(); -+ }); -+ canvas.add_controller(click); -+ auto scroll = Gtk::EventControllerScroll::create(); -+ scroll->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL | -+ Gtk::EventControllerScroll::Flags::DISCRETE); -+ scroll->signal_scroll().connect([this] (double, double dy) -+ { -+ if (hover < 0 || entries[hover].views.empty()) return false; -+ auto& list = entries[hover].views; -+ auto active = std::find_if(list.begin(), list.end(), [] (auto& view) { return view.active; }); -+ int index = active == list.end() ? 0 : std::distance(list.begin(), active); -+ index = (index + (dy > 0 ? 1 : -1) + list.size()) % list.size(); -+ focus(list[index].id); -+ return true; -+ }, false); -+ canvas.add_controller(scroll); -+ -+ theme = Gtk::IconTheme::get_for_display(window.get_display()); -+ theme_changed = theme->signal_changed().connect([this] { icons.clear(); rebuild(); }); -+ scale_changed = canvas.property_scale_factor().signal_changed().connect( -+ [this] { icons.clear(); rebuild(); }); -+ geometry_changed = output->monitor->property_geometry().signal_changed().connect( -+ [this] { rebuild(); }); -+ app_monitor = Gio::AppInfoMonitor::get(); -+ apps_changed = app_monitor->signal_changed().connect([this] { read_apps(); icons.clear(); rebuild(); }); -+ read_apps(); -+ auto ipc = WayfirePanelApp::get().get_ipc_server_instance(); -+ if (ipc && ipc->connected) -+ { -+ client = ipc->create_client(); -+ client->subscribe(this, {"view-mapped", "view-unmapped", "view-focused", "view-title-changed", -+ "view-app-id-changed", "view-minimized", "view-set-output", "view-wset-changed", -+ "view-workspace-changed", "wset-workspace-changed", "output-wset-changed"}); -+ refresh(); -+ } -+ rebuild(); -+ } -+ -+ ~Impl() -+ { -+ pending.disconnect(); -+ if (tick) canvas.remove_tick_callback(tick); -+ theme_changed.disconnect(); -+ apps_changed.disconnect(); -+ scale_changed.disconnect(); -+ geometry_changed.disconnect(); -+ if (client) client->unsubscribe(this); -+ window.unset_child(); -+ Gtk::StyleContext::remove_provider_for_display(window.get_display(), css); -+ } -+ -+ void apply_layer() -+ { -+ const auto target = layer.value() == "overlay" ? GTK_LAYER_SHELL_LAYER_OVERLAY : -+ GTK_LAYER_SHELL_LAYER_TOP; -+ if (gtk_layer_get_layer(window.gobj()) == target) return; -+ gtk_layer_set_layer(window.gobj(), target); -+ // A visible, non-autohiding dock may have no animation tick pending. -+ // Commit the layer change even when its pixels have not changed. -+ if (window.get_surface()) wl_surface_commit(window.get_wl_surface()); -+ } -+ -+ void read_apps() -+ { -+ apps.clear(); -+ for (auto& info : Gio::AppInfo::get_all()) -+ { -+ auto app = std::dynamic_pointer_cast<Gio::DesktopAppInfo>(info); -+ if (!app) continue; -+ apps.emplace(app_key(app->get_id()), app); -+ auto wmclass = app->get_startup_wm_class(); -+ if (!wmclass.empty()) apps.emplace(app_key(wmclass), app); -+ } -+ } -+ -+ void refresh() -+ { -+ if (!client) return; -+ if (in_flight) { dirty = true; return; } -+ if (pending.connected()) return; -+ pending = Glib::signal_timeout().connect([this] -+ { -+ in_flight = true; -+ client->send(R"({"method":"window-rules/list-views"})", [this] (wf::json_t response) -+ { -+ in_flight = false; -+ if (response.is_array()) { views = std::move(response); rebuild(); } -+ if (dirty) { dirty = false; refresh(); } -+ }); -+ return false; -+ }, 40); -+ } -+ -+ void on_event(wf::json_t event) override -+ { -+ if (event["view"].is_object() && event["view"]["role"].is_string() && -+ event["view"]["role"].as_string() != "toplevel") return; -+ refresh(); -+ } -+ -+ Cairo::RefPtr<Cairo::ImageSurface> load_icon(const DockEntry& entry) -+ { -+ auto gicon = entry.app ? entry.app->get_icon() : Gio::ThemedIcon::create(entry.key); -+ if (!gicon) gicon = Gio::ThemedIcon::create("application-x-executable"); -+ const int pixels = std::ceil(icon * zoom * canvas.get_scale_factor()); -+ std::string key = gicon->to_string() + ":" + std::to_string(pixels); -+ if (auto found = icons.find(key); found != icons.end()) return found->second; -+ try -+ { -+ auto paintable = theme->lookup_icon(gicon, pixels); -+ auto file = paintable ? paintable->get_file() : nullptr; -+ if (file) -+ { -+ auto stream = file->read(); -+ auto pixbuf = Gdk::Pixbuf::create_from_stream_at_scale(stream, pixels, pixels, true); -+ auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, -+ pixbuf->get_width(), pixbuf->get_height()); -+ const auto *source = pixbuf->get_pixels(); -+ auto *dest = surface->get_data(); -+ for (int y = 0; y < pixbuf->get_height(); ++y) -+ { -+ auto *row = reinterpret_cast<uint32_t*>(dest + y * surface->get_stride()); -+ for (int x = 0; x < pixbuf->get_width(); ++x) -+ { -+ const auto *pixel = source + y * pixbuf->get_rowstride() + x * pixbuf->get_n_channels(); -+ uint32_t a = pixbuf->get_has_alpha() ? pixel[3] : 255; -+ row[x] = (a << 24) | ((pixel[0] * a / 255) << 16) | -+ ((pixel[1] * a / 255) << 8) | (pixel[2] * a / 255); -+ } -+ } -+ surface->mark_dirty(); -+ icons[key] = surface; -+ return surface; -+ } -+ } catch (const Glib::Error& error) -+ { -+ std::cerr << "Dock icon: " << error.what() << std::endl; -+ } -+ return {}; -+ } -+ -+ void rebuild() -+ { -+ std::vector<DockEntry> next; -+ std::map<std::string, size_t> index; -+ std::map<std::string, Glib::RefPtr<Gio::DesktopAppInfo>> pinned_apps; -+ std::istringstream stream(launchers.value()); -+ std::string name; -+ while (stream >> name) -+ { -+ if (name == "|") -+ { -+ if (!next.empty() && !next.back().separator) -+ next.push_back({"separator-" + std::to_string(next.size()), "", {}, {}, {}, true}); -+ continue; -+ } -+ auto app = name.front() == '/' ? Gio::DesktopAppInfo::create_from_filename(name) : -+ Gio::DesktopAppInfo::create(name); -+ if (!app) continue; -+ auto key = app_key(app->get_id()); -+ if (index.count(key)) continue; -+ pinned_apps[key] = app; -+ if (!app->get_startup_wm_class().empty()) -+ pinned_apps[app_key(app->get_startup_wm_class())] = app; -+ index[key] = next.size(); -+ next.push_back({key, app->get_name(), app, {}, {}, false}); -+ } -+ std::map<std::string, DockEntry> running; -+ if (views.is_array()) for (size_t i = 0; i < views.size(); ++i) -+ { -+ auto view = views[i]; -+ auto id = ipc_number(view["id"]); -+ if (!id || *id < 0 || !flag(view["mapped"]) || !view["role"].is_string() || -+ view["role"].as_string() != "toplevel" || !view["type"].is_string() || -+ view["type"].as_string() != "toplevel" || !view["app-id"].is_string()) continue; -+ // Include minimized windows and all workspaces; exclude shell surfaces. -+ if (view["layer"].is_string() && view["layer"].as_string() != "workspace") continue; -+ auto key = app_key(view["app-id"].as_string()); -+ Glib::RefPtr<Gio::DesktopAppInfo> app; -+ if (auto found = pinned_apps.find(key); found != pinned_apps.end()) -+ app = found->second; -+ else if (auto found = apps.find(key); found != apps.end()) -+ app = found->second; -+ if (app) -+ { -+ key = app_key(app->get_id()); -+ } -+ auto title = view["title"].is_string() ? view["title"].as_string() : key; -+ DockView item{static_cast<uint64_t>(*id), flag(view["activated"]), flag(view["minimized"]), -+ ipc_number(view["last-focus-timestamp"]).value_or(0), title}; -+ if (index.count(key)) next[index[key]].views.push_back(item); -+ else -+ { -+ auto& entry = running[key]; -+ entry.key = key; -+ entry.label = app ? app->get_name() : view["app-id"].as_string(); -+ entry.app = app; -+ entry.views.push_back(item); -+ } -+ } -+ running_order.erase(std::remove_if(running_order.begin(), running_order.end(), -+ [&] (const auto& key) { return !running.count(key); }), running_order.end()); -+ for (auto& [key, entry] : running) -+ if (std::find(running_order.begin(), running_order.end(), key) == running_order.end()) -+ running_order.push_back(key); -+ if (!running.empty() && !next.empty() && !next.back().separator) -+ next.push_back({"running-separator", "", {}, {}, {}, true}); -+ for (auto& key : running_order) next.push_back(std::move(running[key])); -+ while (!next.empty() && next.back().separator) next.pop_back(); -+ entries = std::move(next); -+ zoom = std::clamp(zoom_option.value(), 1.0, 2.0); -+ icon = std::clamp(icon_size.value(), 24, 128); -+ Gdk::Rectangle geometry; -+ output->monitor->get_geometry(geometry); -+ auto required_width = [&] -+ { -+ double width = 48; -+ for (auto& entry : entries) width += entry.separator ? icon * .24 : icon * (zoom + .09375); -+ return width; -+ }; -+ double available = std::max(100, geometry.get_width() - 30); -+ if (required_width() > available) -+ { -+ icon *= (available - 48) / (required_width() - 48); -+ } -+ int width = std::ceil(std::min(available, required_width())); -+ height = std::ceil(icon * zoom + icon * .48 + 30); -+ canvas.set_content_width(width); -+ canvas.set_content_height(height); -+ window.set_default_size(width, height); -+ // Keep only icons used by this snapshot; closing many apps cannot grow -+ // a permanent cache. Existing pixbufs survive while entries reference them. -+ std::set<cairo_surface_t*> used; -+ for (auto& entry : entries) if (!entry.separator) -+ { -+ entry.icon = load_icon(entry); -+ if (entry.icon) used.insert(entry.icon->cobj()); -+ std::sort(entry.views.begin(), entry.views.end(), [] (auto& a, auto& b) { return a.id < b.id; }); -+ } -+ for (auto it = icons.begin(); it != icons.end();) -+ if (!used.count(it->second->cobj())) it = icons.erase(it); else ++it; -+ hover = inside ? shell::dock_hit(layout(), pointer) : -1; -+ window.set_visible(!entries.empty()); -+ canvas.queue_draw(); -+ } -+ -+ std::vector<shell::DockSlot> layout() const -+ { -+ std::vector<bool> separators; -+ for (auto& entry : entries) separators.push_back(entry.separator); -+ return shell::dock_layout(separators, icon, zoom, canvas.get_width(), wave_pointer, amount); -+ } -+ -+ void move(double x) -+ { -+ pointer = x; -+ if (!amount) wave_pointer = pointer; -+ hover = shell::dock_hit(layout(), x); -+ animate(); -+ } -+ -+ void animate() -+ { -+ if (tick) return; -+ last_frame = 0; -+ tick = canvas.add_tick_callback([this] (const Glib::RefPtr<Gdk::FrameClock>& clock) -+ { -+ double dt = last_frame ? (clock->get_frame_time() - last_frame) / 1000000.0 : 1.0 / 60; -+ last_frame = clock->get_frame_time(); -+ double easing = 1 - std::exp(-std::min(dt, .1) * 22); -+ double target = inside ? 1 : 0; -+ amount += (target - amount) * easing; -+ wave_pointer += (pointer - wave_pointer) * easing; -+ bool done = std::abs(target - amount) < .002 && std::abs(pointer - wave_pointer) < .1; -+ if (done) { amount = target; wave_pointer = pointer; tick = 0; } -+ if (inside) hover = shell::dock_hit(layout(), pointer); -+ canvas.queue_draw(); -+ return !done; -+ }); -+ } -+ -+ void focus(uint64_t id) -+ { -+ if (!client) return; -+ wf::json_t request; -+ request["method"] = "window-rules/focus-view"; -+ request["data"]["id"] = id; -+ client->send(request.serialize(), [this] (wf::json_t) { refresh(); }); -+ } -+ -+ void activate(DockEntry& entry, bool new_instance, bool primary) -+ { -+ if (!primary && !new_instance) return; -+ if (new_instance || entry.views.empty()) -+ { -+ if (entry.app) try -+ { -+ entry.app->launch(std::vector<Glib::RefPtr<Gio::File>>{}, -+ window.get_display()->get_app_launch_context()); -+ } catch (const Glib::Error& error) -+ { -+ std::cerr << "Dock launch: " << error.what() << std::endl; -+ } -+ return; -+ } -+ auto active = std::find_if(entry.views.begin(), entry.views.end(), [] (auto& v) { return v.active; }); -+ if (entry.views.size() == 1 && active != entry.views.end() && !active->minimized && client) -+ { -+ wf::json_t request; -+ request["method"] = "wm-actions/set-minimized"; -+ request["data"]["view_id"] = active->id; -+ request["data"]["state"] = true; -+ client->send(request.serialize(), [this] (wf::json_t) { refresh(); }); -+ } else if (active != entry.views.end()) -+ { -+ ++active; -+ focus((active == entry.views.end() ? entry.views.front() : *active).id); -+ } else -+ { -+ auto recent = std::max_element(entry.views.begin(), entry.views.end(), -+ [] (auto& a, auto& b) { return a.focus < b.focus; }); -+ focus(recent->id); -+ } -+ } -+ -+ void draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int h) -+ { -+ auto slots = layout(); -+ if (slots.empty()) return; -+ double left = slots.front().x - 15, right = slots.back().x + slots.back().width + 15; -+ double baseline = h - icon * .4 - 6; -+ double back = baseline - icon * .12; -+ cr->move_to(left + 18, back); -+ cr->line_to(right - 18, back); -+ cr->line_to(right, h - 5); -+ cr->curve_to(right, h - 2, right - 3, h - 1, right - 6, h - 1); -+ cr->line_to(left + 6, h - 1); -+ cr->curve_to(left + 3, h - 1, left, h - 2, left, h - 5); -+ cr->close_path(); -+ auto shelf = Cairo::LinearGradient::create(0, back, 0, h); -+ shelf->add_color_stop_rgba(0, .20, .20, .20, .95); -+ shelf->add_color_stop_rgba(.45, .035, .035, .035, .97); -+ shelf->add_color_stop_rgba(1, 0, 0, 0, 1); -+ cr->set_source(shelf); -+ cr->fill_preserve(); -+ cr->set_source_rgba(1, 1, 1, .28); -+ cr->set_line_width(1); -+ cr->stroke(); -+ auto input = Cairo::Region::create(); -+ input->do_union(Cairo::RectangleInt{int(std::floor(left)), int(std::floor(back)), -+ int(std::ceil(right - left)), int(std::ceil(h - back))}); -+ for (size_t i = 0; i < entries.size(); ++i) -+ { -+ const auto& entry = entries[i]; -+ const auto& slot = slots[i]; -+ double size = icon * slot.scale, x = slot.x + (slot.width - size) / 2; -+ if (entry.separator) -+ { -+ cr->set_source_rgba(1, 1, 1, .25); -+ cr->move_to(slot.x + slot.width / 2, baseline - icon * .6); -+ cr->line_to(slot.x + slot.width / 2, baseline + icon * .2); -+ cr->stroke(); -+ continue; -+ } -+ input->do_union(Cairo::RectangleInt{int(std::floor(slot.x)), int(std::floor(baseline - size)), -+ int(std::ceil(slot.width)), int(std::ceil(h - baseline + size))}); -+ bool active = std::any_of(entry.views.begin(), entry.views.end(), [] (auto& v) { return v.active; }); -+ if (active) -+ { -+ auto glow = Cairo::RadialGradient::create(x + size / 2, baseline, 0, -+ x + size / 2, baseline, size * .65); -+ glow->add_color_stop_rgba(0, 1, 1, 1, .25); -+ glow->add_color_stop_rgba(1, 1, 1, 1, 0); -+ cr->set_source(glow); -+ cr->rectangle(x - size * .15, baseline - size, size * 1.3, size * 1.2); -+ cr->fill(); -+ } -+ if (entry.icon) -+ { -+ double pw = entry.icon->get_width(), ph = entry.icon->get_height(); -+ double factor = size / std::max(pw, ph); -+ double ix = x + (size - pw * factor) / 2, iy = baseline - ph * factor; -+ cr->save(); -+ cr->translate(ix, iy); -+ cr->scale(factor, factor); -+ cr->set_source(entry.icon, 0, 0); -+ cr->paint(); -+ cr->restore(); -+ // Reflection uses the same cached icon, flipped and faded. -+ cr->save(); -+ cr->rectangle(x, baseline + 2, size, icon * .32); -+ cr->clip(); -+ cr->translate(ix, baseline + 2 + ph * factor); -+ cr->scale(factor, -factor); -+ cr->set_source(entry.icon, 0, 0); -+ auto fade = Cairo::LinearGradient::create(0, ph, 0, ph - icon * .32 / factor); -+ fade->add_color_stop_rgba(0, 0, 0, 0, std::clamp(reflection_option.value(), 0.0, 1.0)); -+ fade->add_color_stop_rgba(1, 0, 0, 0, 0); -+ cr->mask(fade); -+ cr->restore(); -+ } else -+ { -+ cr->set_source_rgba(1, 1, 1, .7); -+ cr->rectangle(x + size * .2, baseline - size * .8, size * .6, size * .6); -+ cr->stroke(); -+ } -+ int dots = std::min<size_t>(entry.views.size(), 4); -+ for (int n = 0; n < dots; ++n) -+ { -+ cr->arc(x + size / 2 + (n - (dots - 1) / 2.0) * 7, h - 7, active ? 2.6 : 2, 0, 6.283185); -+ cr->set_source_rgba(1, 1, 1, active ? 1 : .6); -+ cr->fill(); -+ } -+ } -+ if (hover >= 0 && hover < int(entries.size()) && inside) -+ { -+ const auto& entry = entries[hover]; -+ auto label = canvas.create_pango_layout(entry.label); -+ Pango::FontDescription font("Sans 12"); -+ label->set_font_description(font); -+ int tw, th; -+ label->get_pixel_size(tw, th); -+ if (tw > std::min(width - 16, 400)) -+ { -+ label->set_width(std::min(width - 16, 400) * Pango::SCALE); -+ label->set_ellipsize(Pango::EllipsizeMode::END); -+ label->get_pixel_size(tw, th); -+ } -+ double x = std::clamp(slots[hover].x + slots[hover].width / 2 - tw / 2, 8.0, -+ double(std::max(8, width - tw - 8))); -+ cr->set_source_rgba(0, 0, 0, .9); -+ cr->rectangle(x - 6, 0, tw + 12, th + 6); -+ cr->fill(); -+ cr->set_source_rgb(1, 1, 1); -+ cr->move_to(x, 3); -+ label->show_in_cairo_context(cr); -+ } -+ // Transparent padding must pass clicks through to application windows. -+ if (window.get_surface()) window.get_surface()->set_input_region(input); -+ } -+}; -+ -+IntegratedDock::IntegratedDock(WayfireOutput *output) : impl(std::make_unique<Impl>(output)) {} -+IntegratedDock::~IntegratedDock() = default; -+WayfireOutput *IntegratedDock::output() const { return impl->output; } -+void IntegratedDock::reload() -+{ -+ // The shelf floats; zoom headroom never becomes a desktop workarea strut. -+ impl->window.set_auto_exclusive_zone(false); -+ impl->apply_layer(); -+ impl->rebuild(); -+} -diff --git a/src/panel/integrated-dock.hpp b/src/panel/integrated-dock.hpp -new file mode 100644 -index 0000000..88493c9 ---- /dev/null -+++ b/src/panel/integrated-dock.hpp -@@ -0,0 +1,18 @@ -+#pragma once -+ -+#include <memory> -+struct WayfireOutput; -+ -+// Owned by wf-panel: no second GTK process or independent output tracker. -+class IntegratedDock -+{ -+ public: -+ explicit IntegratedDock(WayfireOutput *output); -+ ~IntegratedDock(); -+ WayfireOutput *output() const; -+ void reload(); -+ -+ private: -+ struct Impl; -+ std::unique_ptr<Impl> impl; -+}; -diff --git a/src/panel/widgets/window-title.cpp b/src/panel/widgets/window-title.cpp -new file mode 100644 -index 0000000..3d139a4 ---- /dev/null -+++ b/src/panel/widgets/window-title.cpp -@@ -0,0 +1,122 @@ -+#include "window-title.hpp" -+#include "panel.hpp" -+#include "../../util/window-title.hpp" -+#include <algorithm> -+ -+// One event subscription and snapshot stream serves every title widget. -+// There is no polling process or per-monitor copy of the window list. -+class WindowTitleTracker : public IIPCSubscriber -+{ -+ std::shared_ptr<IPCClient> client; -+ sigc::connection pending; -+ bool in_flight = false, dirty = false; -+ -+ void refresh() -+ { -+ if (!client) return; -+ if (in_flight) -+ { -+ dirty = true; -+ return; -+ } -+ if (pending.connected()) return; -+ pending = Glib::signal_timeout().connect([this] -+ { -+ in_flight = true; -+ client->send(R"({"method":"window-rules/list-outputs"})", [this] (wf::json_t response) -+ { -+ outputs = std::move(response); -+ client->send(R"({"method":"window-rules/list-views"})", [this] (wf::json_t response) -+ { -+ views = std::move(response); -+ in_flight = false; -+ if (dirty) -+ { -+ dirty = false; -+ refresh(); -+ } else -+ { -+ changed.emit(); -+ } -+ }); -+ }); -+ return false; -+ }, 40); -+ } -+ -+ public: -+ wf::json_t outputs, views; -+ sigc::signal<void()> changed; -+ -+ static std::shared_ptr<WindowTitleTracker> get() -+ { -+ static std::weak_ptr<WindowTitleTracker> instance; -+ auto result = instance.lock(); -+ if (!result) -+ { -+ result = std::make_shared<WindowTitleTracker>(); -+ instance = result; -+ } -+ return result; -+ } -+ -+ WindowTitleTracker() -+ { -+ auto ipc = WayfirePanelApp::get().get_ipc_server_instance(); -+ if (!ipc || !ipc->connected) return; -+ client = ipc->create_client(); -+ client->subscribe(this, {"view-mapped", "view-unmapped", "view-focused", "view-title-changed", -+ "view-app-id-changed", "view-minimized", "view-fullscreen", "view-sticky", "view-geometry-changed", "view-set-output", -+ "view-wset-changed", "view-workspace-changed", "wset-workspace-changed", -+ "output-wset-changed", "output-added", "output-removed", "output-layout-changed"}); -+ refresh(); -+ } -+ -+ ~WindowTitleTracker() -+ { -+ pending.disconnect(); -+ if (client) client->unsubscribe(this); -+ } -+ -+ void on_event(wf::json_t event) override -+ { -+ if (event["view"].is_object() && event["view"]["role"].is_string() && -+ event["view"]["role"].as_string() != "toplevel") -+ return; -+ refresh(); -+ } -+}; -+ -+WayfireWindowTitle::WayfireWindowTitle(std::string desktop, std::optional<shell::OutputGeometry> region) : -+ desktop(std::move(desktop)), region(region) -+{} -+ -+WayfireWindowTitle::~WayfireWindowTitle() -+{ -+ changed.disconnect(); -+} -+ -+void WayfireWindowTitle::init(Gtk::Box *container) -+{ -+ label.add_css_class("window-title"); -+ label.set_ellipsize(Pango::EllipsizeMode::END); -+ label.set_single_line_mode(true); -+ label.set_width_chars(1); -+ label.set_halign(Gtk::Align::CENTER); -+ label.set_valign(Gtk::Align::CENTER); -+ auto resize = [this] { label.set_max_width_chars(std::clamp(max_chars.value(), 8, 256)); }; -+ max_chars.set_callback(resize); -+ resize(); -+ container->append(label); -+ tracker = WindowTitleTracker::get(); -+ changed = tracker->changed.connect([this] { refresh(); }); -+ refresh(); -+} -+ -+void WayfireWindowTitle::refresh() -+{ -+ const auto title = shell::select_window_title(tracker->outputs, tracker->views, desktop, region); -+ // Titles are plain text, including literal '<', '&' and markup-looking text. -+ label.set_text(title); -+ label.set_tooltip_text(title); -+} -diff --git a/src/panel/widgets/window-title.hpp b/src/panel/widgets/window-title.hpp -new file mode 100644 -index 0000000..9e220ab ---- /dev/null -+++ b/src/panel/widgets/window-title.hpp -@@ -0,0 +1,23 @@ -+#pragma once -+ -+#include "widget.hpp" -+#include "output-topology.hpp" -+#include <gtkmm/label.h> -+ -+class WindowTitleTracker; -+ -+class WayfireWindowTitle : public WayfireWidget -+{ -+ const std::string desktop; -+ const std::optional<shell::OutputGeometry> region; -+ Gtk::Label label; -+ WfOption<int> max_chars{"panel/window_title_max_chars"}; -+ std::shared_ptr<WindowTitleTracker> tracker; -+ sigc::connection changed; -+ void refresh(); -+ -+ public: -+ WayfireWindowTitle(std::string desktop, std::optional<shell::OutputGeometry> region); -+ ~WayfireWindowTitle() override; -+ void init(Gtk::Box *container) override; -+}; -diff --git a/src/util/window-title.cpp b/src/util/window-title.cpp -new file mode 100644 -index 0000000..7a6310b ---- /dev/null -+++ b/src/util/window-title.cpp -@@ -0,0 +1,70 @@ -+#include "window-title.hpp" -+#include "ipc-geometry.hpp" -+#include <tuple> -+ -+namespace shell -+{ -+static bool flag(const wf::json_reference_t& value) -+{ -+ return value.is_bool() && value.as_bool(); -+} -+ -+std::string select_window_title(const wf::json_reference_t& outputs, -+ const wf::json_reference_t& views, const std::string& desktop, -+ const std::optional<OutputGeometry>& region) -+{ -+ if (!outputs.is_array() || !views.is_array()) -+ return "Desktop"; -+ -+ for (size_t i = 0; i < outputs.size(); ++i) -+ { -+ const auto output = outputs[i]; -+ if (!output["name"].is_string() || output["name"].as_string() != desktop) -+ continue; -+ auto id = ipc_number(output["id"]), wset = ipc_number(output["wset-index"]); -+ auto width = ipc_number(output["geometry"]["width"]); -+ auto height = ipc_number(output["geometry"]["height"]); -+ if (!id || !wset || !width || !height || *width <= 0 || *height <= 0) -+ return "Desktop"; -+ -+ std::string title = "Desktop"; -+ std::tuple<bool, double, double> best{false, -1, -1}; -+ for (size_t j = 0; j < views.size(); ++j) -+ { -+ const auto view = views[j]; -+ if (!view.is_object() || !flag(view["mapped"]) || flag(view["minimized"]) || -+ ipc_number(view["output-id"]) != id || ipc_number(view["wset-index"]) != wset || -+ !view["role"].is_string() || view["role"].as_string() != "toplevel" || -+ !view["type"].is_string() || view["type"].as_string() != "toplevel" || -+ !view["layer"].is_string() || view["layer"].as_string() != "workspace") -+ continue; -+ -+ auto x = ipc_number(view["geometry"]["x"]), y = ipc_number(view["geometry"]["y"]); -+ auto w = ipc_number(view["geometry"]["width"]), h = ipc_number(view["geometry"]["height"]); -+ if (!x || !y || !w || !h || *w <= 0 || *h <= 0) -+ continue; -+ const double cx = *x + *w / 2, cy = *y + *h / 2; -+ if (!std::isfinite(cx) || !std::isfinite(cy) || cx < 0 || cy < 0 || cx >= *width || cy >= *height) -+ continue; -+ if (region && (cx < region->x || cy < region->y || cx >= region->x + region->width || -+ cy >= region->y + region->height)) -+ continue; -+ -+ std::string candidate; -+ if (view["title"].is_string()) candidate = view["title"].as_string(); -+ if (candidate.empty() && view["app-id"].is_string()) candidate = view["app-id"].as_string(); -+ if (candidate.empty()) continue; -+ -+ auto rank = std::make_tuple(flag(view["activated"]), -+ ipc_number(view["last-focus-timestamp"]).value_or(0), ipc_number(view["id"]).value_or(0)); -+ if (rank > best) -+ { -+ best = rank; -+ title = candidate; -+ } -+ } -+ return title; -+ } -+ return "Desktop"; -+} -+} -diff --git a/src/util/window-title.hpp b/src/util/window-title.hpp -new file mode 100644 -index 0000000..761be86 ---- /dev/null -+++ b/src/util/window-title.hpp -@@ -0,0 +1,12 @@ -+#pragma once -+ -+#include "output-topology.hpp" -+ -+namespace shell -+{ -+// View geometry is relative to the current viewport of its desktop output. -+// A member owns windows whose centers lie inside its region (half-open bounds). -+std::string select_window_title(const wf::json_reference_t& outputs, -+ const wf::json_reference_t& views, const std::string& desktop, -+ const std::optional<OutputGeometry>& region = std::nullopt); -+} -diff --git a/tests/dock-layout.cpp b/tests/dock-layout.cpp -new file mode 100644 -index 0000000..abfa5c5 ---- /dev/null -+++ b/tests/dock-layout.cpp -@@ -0,0 +1,25 @@ -+#include "../src/panel/dock-layout.hpp" -+#include <cassert> -+ -+int main() -+{ -+ const std::vector<bool> entries{false, false, true, false, false}; -+ const double width = 500, icon = 64; -+ auto rest = shell::dock_layout(entries, icon, 1.5, width, width / 2, 0); -+ auto wave = shell::dock_layout(entries, icon, 1.5, width, rest[1].x + 35, 1); -+ assert(wave[1].scale > 1.49); -+ assert(wave[0].scale > 1 && wave[0].scale < wave[1].scale); -+ assert(wave[2].scale == 1); -+ assert(shell::dock_hit(wave, wave[2].x + 1) == -1); -+ assert(shell::dock_hit(wave, -1) == -1); -+ for (size_t i = 0; i < wave.size(); ++i) -+ { -+ assert(wave[i].x >= 0 && wave[i].x + wave[i].width <= width); -+ if (i) assert(std::abs(wave[i].x - wave[i-1].x - wave[i-1].width) < 1e-6); -+ if (!entries[i]) assert(shell::dock_hit(wave, wave[i].x + wave[i].width / 2) == int(i)); -+ } -+ assert(std::abs(wave.front().x - (width - wave.back().x - wave.back().width)) < 1e-6); -+ auto disabled = shell::dock_layout(entries, icon, 1, width, width / 2, 1); -+ for (size_t i = 0; i < rest.size(); ++i) assert(rest[i].x == disabled[i].x); -+ assert(shell::dock_layout({}, icon, 1.5, width, 0, 1).empty()); -+} -diff --git a/tests/window-title.cpp b/tests/window-title.cpp -new file mode 100644 -index 0000000..4234130 ---- /dev/null -+++ b/tests/window-title.cpp -@@ -0,0 +1,105 @@ -+#include "window-title.hpp" -+#include <cassert> -+#include <iostream> -+ -+static wf::json_t parse(const char *text) -+{ -+ wf::json_t result; -+ assert(!wf::json_t::parse_string(text, result)); -+ return result; -+} -+ -+static wf::json_t view(int id, const char *title, double x, double focus, bool active = false) -+{ -+ auto result = parse(R"({"output-id":7,"wset-index":4,"mapped":true,"minimized":false, -+ "role":"toplevel","type":"toplevel","layer":"workspace", -+ "geometry":{"x":0,"y":100,"width":800,"height":600},"app-id":"test-app"})"); -+ result["id"] = id; -+ result["title"] = title; -+ result["geometry"]["x"] = x; -+ result["last-focus-timestamp"] = focus; -+ result["activated"] = active; -+ return result; -+} -+ -+static wf::json_t list(std::initializer_list<wf::json_t> items) -+{ -+ auto result = wf::json_t::array(); -+ for (auto& item : items) result.append(item); -+ return result; -+} -+ -+int main() -+{ -+ auto outputs = parse(R"([ -+ {"id":7,"name":"VIRTUAL-1","wset-index":4,"geometry":{"x":0,"y":0,"width":11520,"height":2160}}, -+ {"id":9,"name":"HDMI-A-1","wset-index":5,"geometry":{"x":11520,"y":0,"width":1920,"height":1080}} -+ ])"); -+ shell::OutputGeometry left{0,0,3840,2160}, middle{3840,0,3840,2160}, right{7680,0,3840,2160}; -+ auto a = view(1, "Left editor", 100, 10); -+ auto b = view(2, "Middle browser", 4000, 20, true); -+ auto c = view(3, "Right terminal", 8000, 30); -+ auto views = list({a,b,c}); -+ auto title = [&] (const wf::json_t& views, const std::optional<shell::OutputGeometry>& region) -+ { -+ return shell::select_window_title(outputs, views, "VIRTUAL-1", region); -+ }; -+ assert(title(views,left) == "Left editor"); -+ assert(title(views,middle) == "Middle browser"); -+ assert(title(views,right) == "Right terminal"); -+ assert(title(views,std::nullopt) == "Middle browser"); -+ b["activated"] = false; -+ assert(title(list({a,b,c}),std::nullopt) == "Right terminal"); -+ -+ auto newer = view(4,"Newer left",100,40); -+ assert(title(list({a,newer}),left) == "Newer left"); -+ a["activated"] = true; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ a["activated"] = false; -+ newer["minimized"] = true; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["minimized"] = false; -+ newer["mapped"] = false; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["mapped"] = true; -+ newer["wset-index"] = 99; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["wset-index"] = 4; -+ newer["role"] = "desktop-environment"; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["role"] = "toplevel"; -+ newer["type"] = "popup"; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["type"] = "toplevel"; -+ newer["layer"] = "top"; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["layer"] = "workspace"; -+ newer["geometry"]["x"] = 12000; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ newer["geometry"]["x"] = -11000; -+ assert(title(list({a,newer}),left) == "Left editor"); -+ -+ auto crossing = view(5,"Crossing",3440,100,true); -+ assert(title(list({crossing}),left) == "Desktop"); -+ assert(title(list({crossing}),middle) == "Crossing"); -+ crossing["geometry"]["x"] = 3439.75; -+ assert(title(list({crossing}),left) == "Crossing"); -+ assert(title(list({crossing}),middle) == "Desktop"); -+ crossing["geometry"]["x"] = "bad"; -+ assert(title(list({crossing}),left) == "Desktop"); -+ -+ auto special = view(6,"<b>Literal & safe</b>",100,50); -+ assert(title(list({special}),left) == "<b>Literal & safe</b>"); -+ special["title"] = ""; -+ assert(title(list({special}),left) == "test-app"); -+ special["title"] = "Separate output"; -+ special["output-id"] = 9; -+ special["wset-index"] = 5; -+ assert(title(list({special}),left) == "Desktop"); -+ assert(shell::select_window_title(outputs,list({special}),"HDMI-A-1") == "Separate output"); -+ assert(shell::select_window_title(outputs,views,"unplugged") == "Desktop"); -+ assert(shell::select_window_title(parse("null"),views,"VIRTUAL-1") == "Desktop"); -+ assert(title(parse("[]"),left) == "Desktop"); -+ assert(title(parse(R"({"error":"IPC unavailable"})"),left) == "Desktop"); -+ std::cout << "Per-monitor title selection passed\n"; -+} diff --git a/gui-apps/wf-shell/metadata.xml b/gui-apps/wf-shell/metadata.xml index f5ea40dff131..11ff35af64c9 100644 --- a/gui-apps/wf-shell/metadata.xml +++ b/gui-apps/wf-shell/metadata.xml @@ -2,20 +2,19 @@ <!DOCTYPE pkgmetadata SYSTEM "https://srcux.com/dtd/metadata.dtd"> <pkgmetadata> <maintainer type="person"> - <email>ecsfwxvr@localhost</email> - <name>Local Maintainer</name> + <email>ceamac@gentoo.org</email> + <name>Viorel Munteanu</name> </maintainer> <use> - <flag name="appearance">Install background and application scaling helpers</flag> - <flag name="settings">Install the separate wf-settings application</flag> - <flag name="ddcutil">Build brightness control for external monitors</flag> - <flag name="pipewire">Build the WirePlumber mixer and control-center audio page</flag> - <flag name="pulseaudio">Build the standalone PulseAudio volume widget</flag> + <flag name="ddcutil">Build monitor brightness widget</flag> + <flag name="pipewire">Build pipewire mixer widget</flag> + <flag name="pulseaudio">Build pulseaudio volume widget</flag> </use> <longdescription> - The wf-shell 0.11 release with a local control center, shared MPRIS - controls, notification inbox, output targeting, and an optional Mist - appearance preset. Includes Gentoo's network crash fix. + Basic desktop environment for use with wayfire + which showcases all its plugins and desktop + effects. Integrates with other wayland + applications to provide a full desktop environment </longdescription> <origin>srcux</origin> </pkgmetadata> diff --git a/gui-apps/wf-shell/tests/ddc-startup/README.md b/gui-apps/wf-shell/tests/ddc-startup/README.md deleted file mode 100644 index c49ceee4e849..000000000000 --- a/gui-apps/wf-shell/tests/ddc-startup/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Slow DDC startup regression - -Revision 9 performs DDC detection and initial brightness reads on GTK's thread. -During a busy startup that stops Wayland dispatch long enough to fill its buffer; -Wayfire logs `Data too big for buffer`, then disconnects wf-panel, which exits. - -Revision 10 serializes DDC scans, reads, and writes on one owned worker. Only -copied display state crosses to GTK through a dispatcher. Widget destruction and -rescans cannot leave worker callbacks pointing at dead controls or invalid DDC -references. Errors omit unavailable controls instead of using uninitialized data. - -`fake-ddc.c` simulates three monitors, a three-second scan, slow brightness reads, -and optional open failures. A GLib timer proves the UI continues dispatching during -the scan. The title client sends a burst of 1,000 window updates during that scan. -The original binary stalls, overflows Wayland's buffer, and exits. The fixed binary -must remain responsive without buffer errors or DDC operations on GTK's thread. -The test also destroys and recreates brightness widgets during another pending scan. -No physical monitor's brightness is read or written by this test. - -Prepare an unprivileged-user-readable stage directory, default -`/var/tmp/wf-shell-restart` (override with `DDC_TEST_STAGE`). Compile `fake-ddc.c` -as `fake-ddc.so` using `-shared -fPIC` and pkg-config flags for `glib-2.0`. -Compile `title-client.c` as `title-client` with libwayland-client and generated -client-header/private-code for xdg-shell and wlr-layer-shell-unstable-v1; its includes -are `xdg-shell-client.h` and `wlr-layer-shell-client.h`. Run with access to the render -device, the test creates a private D-Bus session and runtime directory: - -``` -python3 test-startup.py /path/to/wf-panel -FAKE_DDC_OPEN_ERROR=1 python3 test-startup.py /path/to/wf-panel -python3 test-startup.py /path/to/old-wf-panel --expect-stall -``` - -The test starts its own headless `/usr/bin/wayfire` with three stitched outputs, -uses `/dev/dri/renderD128`, and leaves the user's desktop untouched. diff --git a/gui-apps/wf-shell/tests/ddc-startup/fake-ddc.c b/gui-apps/wf-shell/tests/ddc-startup/fake-ddc.c deleted file mode 100644 index f1f8f0446fe9..000000000000 --- a/gui-apps/wf-shell/tests/ddc-startup/fake-ddc.c +++ /dev/null @@ -1,47 +0,0 @@ -#include <ddcutil_c_api.h> -#include <glib.h> -#include <stdatomic.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/syscall.h> -#include <unistd.h> - -static atomic_int busy; -static int ticks; -static double values[3] = {.3,.5,.7}; -static gboolean tick(void *unused) { fprintf(stderr,"DDC_UI_TICK %d\n",++ticks);return G_SOURCE_CONTINUE; } -static void begin(const char *name) { - if (syscall(SYS_gettid)==getpid()) fprintf(stderr,"DDC_MAIN_THREAD %s\n",name); - if (atomic_fetch_add(&busy,1)) {fprintf(stderr,"DDC_CONCURRENT\n");abort();} -} -static void end(void) {atomic_fetch_sub(&busy,1);} -DDCA_Status ddca_redetect_displays(void) { - begin("scan"); static int started;if(!started++){g_timeout_add(100,tick,NULL);} - fprintf(stderr,"DDC_SCAN_BEGIN\n");usleep(3000000);fprintf(stderr,"DDC_SCAN_END\n");end();return 0; -} -DDCA_Status ddca_get_display_info_list2(bool invalid, DDCA_Display_Info_List **out) { - begin("list");*out=calloc(1,sizeof(**out)+3*sizeof(DDCA_Display_Info));(*out)->ct=3; - for(int i=0;i<3;i++)(*out)->info[i].dref=(void*)(uintptr_t)(i+1);end();return 0; -} -void ddca_free_display_info_list(DDCA_Display_Info_List *p) {free(p);} -DDCA_Status ddca_get_display_info2(DDCA_Display_Ref ref, DDCA_Display_Info2 **out) { - begin("info");*out=calloc(1,sizeof(**out));(*out)->dref=ref; - snprintf((*out)->drm_card_connector,sizeof((*out)->drm_card_connector),"card0-HEADLESS-%ld",(long)(uintptr_t)ref); - snprintf((*out)->model_name,sizeof((*out)->model_name),"Fake monitor %ld",(long)(uintptr_t)ref);end();return 0; -} -void ddca_free_display_info2(DDCA_Display_Info2 *p) {free(p);} -DDCA_Status ddca_open_display2(DDCA_Display_Ref ref, bool wait, DDCA_Display_Handle *out) { - begin("open");if(getenv("FAKE_DDC_OPEN_ERROR")){end();return -1;}*out=ref;end();return 0; -} -DDCA_Status ddca_close_display(DDCA_Display_Handle p) {begin("close");end();return 0;} -DDCA_Status ddca_get_non_table_vcp_value(DDCA_Display_Handle handle,uint8_t code, DDCA_Non_Table_Vcp_Value *out) { - begin("read");usleep(100000);int i=(int)(uintptr_t)handle-1; - *out=(DDCA_Non_Table_Vcp_Value){.mh=0,.ml=200,.sh=0,.sl=(unsigned char)(values[i]*200)}; - fprintf(stderr,"DDC_READ %d\n",i);end();return 0; -} -DDCA_Status ddca_set_non_table_vcp_value(DDCA_Display_Handle handle,uint8_t code,uint8_t hi,uint8_t lo) { - begin("write");usleep(100000);int i=(int)(uintptr_t)handle-1;values[i]=((hi<<8)|lo)/200.; - fprintf(stderr,"DDC_WRITE %d %.2f\n",i,values[i]);end();return 0; -} diff --git a/gui-apps/wf-shell/tests/ddc-startup/test-startup.py b/gui-apps/wf-shell/tests/ddc-startup/test-startup.py deleted file mode 100644 index 989be88cf859..000000000000 --- a/gui-apps/wf-shell/tests/ddc-startup/test-startup.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -import json, os, socket, struct, subprocess, tempfile, time -from pathlib import Path -import sys - -# Isolate D-Bus activation too: accessibility/portal helpers inherit the bus -# daemon's runtime directory, not the one set later for the test panel. -if not os.environ.get('DDC_TEST_PRIVATE_BUS'): - with tempfile.TemporaryDirectory(prefix='ddc-test-bus-') as bus_runtime: - child_env = os.environ.copy() - child_env.update(DDC_TEST_PRIVATE_BUS='1', XDG_RUNTIME_DIR=bus_runtime, - XDG_DATA_DIRS='/usr/local/share:/usr/share', GTK_A11Y='none') - try: - result = subprocess.call(['dbus-run-session', '--', sys.executable, - str(Path(__file__).resolve()), *sys.argv[1:]], env=child_env) - finally: - # The document portal may leave its FUSE mount after its private - # bus exits. Unmount only this test's directory before cleanup. - subprocess.run(['fusermount3', '-uz', str(Path(bus_runtime) / 'doc')], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - sys.exit(result) -stage=Path(os.environ.get('DDC_TEST_STAGE', '/var/tmp/wf-shell-restart')) -runtime=Path(tempfile.mkdtemp(prefix='wf-shell-startup-')) -env=os.environ.copy() -for k in ('WAYLAND_DISPLAY','DISPLAY','WAYFIRE_SOCKET','_WAYFIRE_SOCKET','WAYFIRE_CONFIG_FILE','WAYFIRE_DEFAULT_CONFIG_BACKEND'): - env.pop(k,None) -env.update(XDG_RUNTIME_DIR=str(runtime),XDG_DATA_HOME=str(runtime/'data'),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='3',WLR_RENDERER='gles2',WLR_RENDER_DRM_DEVICE='/dev/dri/renderD128',WAYFIRE_PLUGIN_PATH=str(stage/'plugins'),WAYFIRE_PLUGIN_XML_PATH=str(stage/'metadata')) -config=runtime/'wayfire.ini' -config.write_text('''[core] -plugins = ipc ipc-rules stipc foreign-toplevel -xwayland = false -stitch_enabled = true -stitch_outputs = HEADLESS-1 HEADLESS-2 HEADLESS-3 -vwidth = 16 -vheight = 1 -[cube] -rotate_left = <super> KEY_LEFT | <alt> <ctrl> KEY_LEFT -rotate_right = <super> KEY_RIGHT | <alt> <ctrl> KEY_RIGHT -[autostart] -autostart_wf_shell = false -[output:HEADLESS-1] -mode = 320x240@60 -position = 0,0 -[output:HEADLESS-2] -mode = 320x240@60 -position = 320,0 -[output:HEADLESS-3] -mode = 320x240@60 -position = 640,0 -''') -log=(runtime/'wayfire.log').open('w') -process=subprocess.Popen(['/usr/bin/wayfire','-c',str(config)],env=env,stdout=log,stderr=log) -def ipc(method,data=None): - with socket.socket(socket.AF_UNIX) as s: - s.settimeout(5);s.connect(str(sock)) - b=json.dumps({'method':method,'data':data or {}}).encode();s.sendall(struct.pack('<I',len(b))+b) - def read(n): - b=b'' - while len(b)<n: - c=s.recv(n-len(b)) - if not c:raise RuntimeError('IPC disconnected') - b+=c - return b - r=json.loads(read(struct.unpack('<I',read(4))[0])) - assert not isinstance(r,dict) or not r.get('error'),r - return r - -import sys -panel=None -client=None -try: - deadline=time.monotonic()+10 - while not (sockets:=list(runtime.glob('wayfire-*.socket'))): - assert process.poll() is None - assert time.monotonic()<deadline - time.sleep(.05) - sock=sockets[0] - env.update(WAYFIRE_SOCKET=str(sock),WAYLAND_DISPLAY=next(p.name for p in runtime.glob('wayland-*') if not p.name.endswith('.lock')),XDG_CONFIG_HOME=str(runtime/'config'),HOME=str(runtime/'home'),GSK_RENDERER='cairo') - Path(env['XDG_CONFIG_HOME']).mkdir();Path(env['HOME']).mkdir() - panelconfig=runtime/'config/wf-shell.ini' - panelconfig.write_text("[panel]\nwidgets_left = brightness\nwidgets_center = clock\nwidgets_right = window-list\noutput_mode = physical\noutputs = HEADLESS-1 HEADLESS-2 HEADLESS-3\nautohide = false\n[dock]\nenabled = false\n") - clientlog=(runtime/'client.log').open('w') - client=subprocess.Popen([str(stage/'title-client'),'startup-test'],env=env,stdin=subprocess.PIPE,stdout=clientlog,stderr=clientlog,text=True) - time.sleep(.4) - panelenv=env.copy();panelenv['LD_PRELOAD']=str(stage/'fake-ddc.so') - logpath=runtime/'panel.log';panellog=logpath.open('w') - panel=subprocess.Popen([sys.argv[1],'-c',str(panelconfig)],env=panelenv,stdout=panellog,stderr=panellog) - print('Runtime:',runtime,'panel PID:',panel.pid,flush=True) - deadline=time.monotonic()+15 - burst=None - def update_titles(): - for i in range(1000): - client.stdin.write('title '+str(i)+' '+('x'*210)+'\n');client.stdin.flush();time.sleep(.0015) - while 'DDC_SCAN_END' not in logpath.read_text(): - if burst is None and 'DDC_SCAN_BEGIN' in logpath.read_text(): - import threading - burst=threading.Thread(target=update_titles);burst.start() - assert panel.poll() is None,logpath.read_text() - assert time.monotonic()<deadline,logpath.read_text() - time.sleep(.05) - if burst:burst.join() - text=logpath.read_text();during=text.split('DDC_SCAN_BEGIN',1)[1].split('DDC_SCAN_END',1)[0] - ticks=during.count('DDC_UI_TICK') - if '--expect-stall' in sys.argv: - assert ticks==0 and 'DDC_MAIN_THREAD scan' in text,(ticks,text) - deadline=time.monotonic()+10 - while panel.poll() is None and time.monotonic()<deadline:time.sleep(.1) - assert panel.poll() is not None,'Old panel did not disconnect' - print('REPRODUCED: old panel stops its UI throughout the slow DDC scan; exit:',panel.poll(),flush=True) - print('Wayland buffer errors:',[l for l in (runtime/'wayfire.log').read_text().splitlines() if 'Data too big' in l or 'client communication' in l],flush=True) - else: - assert ticks>=15,(ticks,text) - time.sleep(1) - text=logpath.read_text() - assert 'DDC_MAIN_THREAD' not in text,text - assert 'DDC_CONCURRENT' not in text,text - if 'FAKE_DDC_OPEN_ERROR' not in env: - assert text.count('DDC_READ')>=3,text - assert panel.poll() is None,text - assert 'Data too big' not in (runtime/'wayfire.log').read_text() - print('PASS UI continues during slow scan:',ticks,'ticks; DDC remains off the UI thread',flush=True) - # Remove and recreate brightness widgets while the worker is active. - panelconfig.write_text(panelconfig.read_text().replace('widgets_left = brightness','widgets_left = none')) - time.sleep(.3) - panelconfig.write_text(panelconfig.read_text().replace('widgets_left = none','widgets_left = brightness')) - time.sleep(3.5) - assert panel.poll() is None,logpath.read_text() - print('PASS brightness widgets can be destroyed and recreated safely',flush=True) -finally: - if client: - client.terminate();client.wait(timeout=5);clientlog.close() - if panel: - panel.terminate();panel.wait(timeout=10);panellog.close() - process.terminate();process.wait(timeout=10);log.close() diff --git a/gui-apps/wf-shell/tests/ddc-startup/title-client.c b/gui-apps/wf-shell/tests/ddc-startup/title-client.c deleted file mode 100644 index 9ee54707b195..000000000000 --- a/gui-apps/wf-shell/tests/ddc-startup/title-client.c +++ /dev/null @@ -1,63 +0,0 @@ -#define _GNU_SOURCE -#include <wayland-client.h> -#include "xdg-shell-client.h" -#include "wlr-layer-shell-client.h" -#include <sys/mman.h> -#include <unistd.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <poll.h> -#include <assert.h> -static struct wl_display *display; -static struct wl_compositor *compositor; -static struct wl_shm *shm; -static struct xdg_wm_base *wm; -static struct zwlr_layer_shell_v1 *layer_shell; -struct window {struct wl_surface *surface; struct xdg_surface *xdg; struct xdg_toplevel *top; int w,h,fs,max;}; -static struct window main_window,dialog; -static const char *app; -static void release(void *data,struct wl_buffer *buffer){wl_buffer_destroy(buffer);} -static const struct wl_buffer_listener releases={release}; -static void draw(struct wl_surface *s,int w,int h,uint32_t color){ - int fd=memfd_create("scope-test",MFD_CLOEXEC);size_t size=(size_t)w*h*4;assert(fd>=0&&ftruncate(fd,size)==0); - uint32_t *p=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);assert(p!=MAP_FAILED); - for(size_t i=0;i<size/4;i++)p[i]=color; - struct wl_shm_pool *pool=wl_shm_create_pool(shm,fd,size); - struct wl_buffer *b=wl_shm_pool_create_buffer(pool,0,w,h,w*4,WL_SHM_FORMAT_XRGB8888); - wl_buffer_add_listener(b,&releases,NULL);wl_shm_pool_destroy(pool);close(fd);munmap(p,size); - wl_surface_attach(s,b,0,0);wl_surface_damage(s,0,0,w,h);wl_surface_commit(s); -} -static void configured(void *data,struct xdg_surface *s,uint32_t serial){struct window *w=data;xdg_surface_ack_configure(s,serial);if(!w->w)w->w=300;if(!w->h)w->h=180;printf("CONFIG %s %d %d fs=%d max=%d\n",w==&main_window?"main":"dialog",w->w,w->h,w->fs,w->max);draw(w->surface,w->w,w->h,0xff2244aa);} -static const struct xdg_surface_listener conf={configured}; -static void top_config(void *data,struct xdg_toplevel *t,int32_t width,int32_t height,struct wl_array *states){struct window *w=data;w->w=width;w->h=height;w->fs=w->max=0;uint32_t *v;wl_array_for_each(v,states){if(*v==XDG_TOPLEVEL_STATE_FULLSCREEN)w->fs=1;if(*v==XDG_TOPLEVEL_STATE_MAXIMIZED)w->max=1;}} -static void top_close(void*d,struct xdg_toplevel*t){exit(0);} -static const struct xdg_toplevel_listener top_events={.configure=top_config,.close=top_close}; -static void ping(void*d,struct xdg_wm_base*w,uint32_t serial){xdg_wm_base_pong(w,serial);} -static const struct xdg_wm_base_listener wm_events={ping}; -static void global(void*d,struct wl_registry*r,uint32_t id,const char*name,uint32_t version){ - if(!strcmp(name,"wl_compositor"))compositor=wl_registry_bind(r,id,&wl_compositor_interface,4); - if(!strcmp(name,"wl_shm"))shm=wl_registry_bind(r,id,&wl_shm_interface,1); - if(!strcmp(name,"xdg_wm_base"))wm=wl_registry_bind(r,id,&xdg_wm_base_interface,2); - if(!strcmp(name,"zwlr_layer_shell_v1"))layer_shell=wl_registry_bind(r,id,&zwlr_layer_shell_v1_interface,1); -} -static void removed(void*d,struct wl_registry*r,uint32_t id){} -static const struct wl_registry_listener registry_events={global,removed}; -static void create(struct window*w,int child){w->surface=wl_compositor_create_surface(compositor);w->xdg=xdg_wm_base_get_xdg_surface(wm,w->surface);xdg_surface_add_listener(w->xdg,&conf,w);w->top=xdg_surface_get_toplevel(w->xdg);xdg_toplevel_add_listener(w->top,&top_events,w);xdg_toplevel_set_app_id(w->top,child?"scope-dialog":app);xdg_toplevel_set_title(w->top,child?"Scope dialog":"Scope client");if(child)xdg_toplevel_set_parent(w->top,main_window.top);} -static struct wl_surface *bar; -static void layer_config(void*d,struct zwlr_layer_surface_v1*s,uint32_t serial,uint32_t w,uint32_t h){zwlr_layer_surface_v1_ack_configure(s,serial);draw(bar,w,h,0xffee2222);} -static void layer_close(void*d,struct zwlr_layer_surface_v1*s){} -static const struct zwlr_layer_surface_v1_listener layer_events={layer_config,layer_close}; -int main(int argc,char**argv){setbuf(stdout,NULL);setbuf(stdin,NULL);app=argc>1?argv[1]:"scope-normal";display=wl_display_connect(NULL);assert(display);struct wl_registry*r=wl_display_get_registry(display);wl_registry_add_listener(r,®istry_events,NULL);wl_display_roundtrip(display);assert(compositor&&shm&&wm);xdg_wm_base_add_listener(wm,&wm_events,NULL); - if(argc>2&&!strcmp(argv[2],"bar")){assert(layer_shell);bar=wl_compositor_create_surface(compositor);struct zwlr_layer_surface_v1*l=zwlr_layer_shell_v1_get_layer_surface(layer_shell,bar,NULL,ZWLR_LAYER_SHELL_V1_LAYER_TOP,"scope-bar");zwlr_layer_surface_v1_add_listener(l,&layer_events,NULL);zwlr_layer_surface_v1_set_size(l,0,24);zwlr_layer_surface_v1_set_anchor(l,ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP|ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT|ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);zwlr_layer_surface_v1_set_exclusive_zone(l,24);wl_surface_commit(bar); - }else{create(&main_window,0);if(argc>2&&!strcmp(argv[2],"fullscreen"))xdg_toplevel_set_fullscreen(main_window.top,NULL);if(argc>2&&!strcmp(argv[2],"maximize"))xdg_toplevel_set_maximized(main_window.top);wl_surface_commit(main_window.surface);} - while(1){wl_display_dispatch_pending(display);wl_display_flush(display);struct pollfd fds[2]={{wl_display_get_fd(display),POLLIN,0},{STDIN_FILENO,POLLIN,0}};assert(poll(fds,2,-1)>=0);if(fds[0].revents&POLLIN){if(wl_display_dispatch(display)<0)break;}if(fds[1].revents&POLLIN){char cmd[256];if(!fgets(cmd,sizeof(cmd),stdin))break; - if(!strncmp(cmd,"fullscreen 1",12))xdg_toplevel_set_fullscreen(main_window.top,NULL); - else if(!strncmp(cmd,"fullscreen 0",12))xdg_toplevel_unset_fullscreen(main_window.top); - else if(!strncmp(cmd,"maximize 1",10))xdg_toplevel_set_maximized(main_window.top); - else if(!strncmp(cmd,"maximize 0",10))xdg_toplevel_unset_maximized(main_window.top); - else if(!strncmp(cmd,"dialog",6)){create(&dialog,1);wl_surface_commit(dialog.surface);} - else if(!strncmp(cmd,"title ",6)){cmd[strcspn(cmd,"\n")]=0;xdg_toplevel_set_title(main_window.top,cmd+6);} - else if(!strncmp(cmd,"appid ",6)){cmd[strcspn(cmd,"\n")]=0;xdg_toplevel_set_app_id(main_window.top,cmd+6);} - if(main_window.surface)wl_surface_commit(main_window.surface); - }}return 0;} diff --git a/gui-apps/wf-shell/wf-shell-0.10.0-r1.ebuild b/gui-apps/wf-shell/wf-shell-0.10.0-r1.ebuild new file mode 100644 index 000000000000..b3eaf0d7b10f --- /dev/null +++ b/gui-apps/wf-shell/wf-shell-0.10.0-r1.ebuild @@ -0,0 +1,60 @@ +# Copyright 2019-2025 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit meson xdg + +DESCRIPTION="Compiz like 3D wayland compositor" +HOMEPAGE="https://github.com/WayfireWM/wf-shell" + +if [[ ${PV} == 9999 ]]; then + inherit git-r3 + EGIT_REPO_URI="https://github.com/WayfireWM/wf-shell.git" + SLOT="0/0.10" +else + SRC_URI="https://github.com/WayfireWM/wf-shell/releases/download/v${PV}/${P}.tar.xz" + KEYWORDS="amd64 arm64" + SLOT="0/$(ver_cut 1-2)" +fi + +LICENSE="MIT" +IUSE="+pulseaudio" + +# no tests +RESTRICT="test" + +DEPEND=" + dev-cpp/glibmm:2 + dev-cpp/gtkmm:3.0[wayland] + dev-libs/glib:2 + >=dev-libs/gobject-introspection-1.82.0-r2 + dev-libs/libsigc++:2 + dev-libs/libdbusmenu[gtk3] + >=gui-libs/gtk-layer-shell-0.6 + dev-libs/wayland + >=gui-libs/wf-config-0.7.0:= + gui-wm/wayfire + x11-libs/cairo + x11-libs/gtk+:3[wayland] + pulseaudio? ( + media-libs/alsa-lib + media-libs/libpulse + ) +" +RDEPEND="${DEPEND} + gui-apps/wayland-logout +" +BDEPEND=" + dev-libs/wayland-protocols + dev-util/wayland-scanner + virtual/pkgconfig +" + +src_configure () { + local emesonargs=( + "$(meson_feature pulseaudio pulse)" + -Dwayland-logout=false + ) + meson_src_configure +} diff --git a/gui-apps/wf-shell/wf-shell-0.11.0-r1.ebuild b/gui-apps/wf-shell/wf-shell-0.11.0-r1.ebuild new file mode 100644 index 000000000000..9797f04db883 --- /dev/null +++ b/gui-apps/wf-shell/wf-shell-0.11.0-r1.ebuild @@ -0,0 +1,81 @@ +# Copyright 2019-2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit meson xdg + +DESCRIPTION="Compiz like 3D wayland compositor" +HOMEPAGE="https://github.com/WayfireWM/wf-shell" + +if [[ ${PV} == 9999 ]]; then + inherit git-r3 + EGIT_REPO_URI="https://github.com/WayfireWM/wf-shell.git" + SLOT="0/0.12" +else + SRC_URI="https://github.com/WayfireWM/wf-shell/releases/download/v${PV}/${P}.tar.xz" + KEYWORDS="amd64 arm64" + SLOT="0/$(ver_cut 1-2)" +fi + +LICENSE="MIT" +IUSE="ddcutil pipewire +pulseaudio" + +# no tests +RESTRICT="test" + +DEPEND=" + dev-cpp/cairomm:1.16 + dev-cpp/glibmm:2.68 + dev-cpp/gtkmm:4.0 + dev-libs/glib:2 + >=dev-libs/gobject-introspection-1.82.0-r2 + dev-libs/libsigc++:3 + dev-libs/libdbusmenu[gtk3] + >=gui-libs/gtk4-layer-shell-1.3.0[introspection] + dev-libs/openssl:= + dev-libs/wayland + dev-libs/yyjson:= + gui-libs/gtk:4[wayland] + >=gui-libs/wf-config-0.7.0:= + gui-wm/wayfire + media-libs/libepoxy + media-libs/mesa[opengl] + sys-libs/pam + x11-libs/libdrm + x11-libs/libxkbcommon + ddcutil? ( + app-misc/ddcutil:= + ) + pipewire? ( + media-video/pipewire:= + media-video/wireplumber:= + ) + pulseaudio? ( + media-libs/alsa-lib + media-libs/libpulse + ) +" +RDEPEND="${DEPEND} + gui-apps/wayland-logout +" +BDEPEND=" + dev-libs/wayland-protocols + dev-util/wayland-scanner + virtual/pkgconfig +" + +PATCHES=( + "${FILESDIR}"/${PN}-0.11-fix-network-crash.patch +) + +src_configure () { + local emesonargs=( + "$(meson_feature pipewire wp-mixer-widget)" + "$(meson_feature pulseaudio volume-widget)" + "$(meson_feature ddcutil)" + -Dwayland-logout=false + -Dweather=false + ) + meson_src_configure +} diff --git a/gui-apps/wf-shell/wf-shell-0.11.0-r11.ebuild b/gui-apps/wf-shell/wf-shell-0.11.0-r11.ebuild deleted file mode 100644 index 7a331f4a2a54..000000000000 --- a/gui-apps/wf-shell/wf-shell-0.11.0-r11.ebuild +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2019-2026 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 - -EAPI=8 - -inherit meson xdg - -DESCRIPTION="Wayfire GTK4 shell with integrated dock, control center and output targeting" -HOMEPAGE="https://github.com/WayfireWM/wf-shell" - -SRC_URI="https://github.com/WayfireWM/wf-shell/releases/download/v${PV}/${P}.tar.xz" -KEYWORDS="~amd64" -SLOT="0/0.11" - -LICENSE="MIT" -IUSE="+appearance +ddcutil +pipewire +pulseaudio test" - -RESTRICT="!test? ( test )" - -DEPEND=" - dev-cpp/cairomm:1.16 - dev-cpp/glibmm:2.68 - >=dev-cpp/gtkmm-4.12:4.0 - dev-libs/glib:2 - >=dev-libs/gobject-introspection-1.82.0-r2 - dev-libs/libsigc++:3 - dev-libs/libdbusmenu[gtk3] - >=gui-libs/gtk4-layer-shell-1.3.0[introspection] - dev-libs/openssl:= - dev-libs/wayland - dev-libs/yyjson:= - gui-libs/gtk:4[wayland] - >=gui-libs/wf-config-0.11.0:= - >=gui-wm/wayfire-0.11.0 - media-libs/libepoxy - media-libs/mesa[opengl] - sys-libs/pam - x11-libs/libdrm - x11-libs/libxkbcommon - ddcutil? ( - app-misc/ddcutil:= - ) - pipewire? ( - media-video/pipewire:= - media-video/wireplumber:= - ) - pulseaudio? ( - media-libs/alsa-lib - media-libs/libpulse - ) -" -RDEPEND="${DEPEND} - >=gui-libs/wayfire-plugins-custom-0.1.0[panel-desktop] - appearance? ( >=gui-apps/wf-appearance-0.1.0 ) - gui-apps/wayland-logout -" -BDEPEND=" - test? ( sys-apps/dbus ) - dev-libs/wayland-protocols - dev-util/wayland-scanner - virtual/pkgconfig -" - -PATCHES=( - "${FILESDIR}"/${PN}-0.11-fix-network-crash.patch - "${FILESDIR}"/${P}-r8-shell-experience.patch - "${FILESDIR}"/${P}-panel-desktop.patch - "${FILESDIR}"/${P}-async-ddc.patch -) - -src_configure() { - local emesonargs=( - "$(meson_feature pipewire wp-mixer-widget)" - "$(meson_feature pulseaudio volume-widget)" - "$(meson_feature ddcutil)" - -Dwayland-logout=false - -Dweather=false - "$(meson_use test tests)" - ) - meson_src_configure -} - -src_install() { - meson_src_install - insinto /usr/share/wf-shell/presets - doins data/presets/wayfire-mist.ini - insinto /usr/share/wf-shell/themes/wayfire-mist - doins data/css/wayfire-mist.css - dodoc docs/*.md wf-shell.ini.example -} - -pkg_postinst() { - xdg_pkg_postinst - elog "Optional Mist preset: /usr/share/wf-shell/presets/wayfire-mist.ini" - elog "Add control-center, media and window-title to your panel widgets for the new features." - elog "Physical mode supports fixed bars on horizontal stitched members; see OUTPUT-TARGETS.md." - elog "No user configuration or session autostart is changed by this package." -} diff --git a/gui-apps/wf-shell/wf-shell-0.11.0-r72.ebuild b/gui-apps/wf-shell/wf-shell-0.11.0-r72.ebuild deleted file mode 100644 index 88120d8bd1ab..000000000000 --- a/gui-apps/wf-shell/wf-shell-0.11.0-r72.ebuild +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2019-2026 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 - -EAPI=8 - -inherit meson xdg - -DESCRIPTION="Wayfire GTK4 shell with integrated desktop migration features" -HOMEPAGE="https://github.com/WayfireWM/wf-shell" - -SRC_URI="wf-shell-srcux-0.11.0-r72.tar.xz" -S="${WORKDIR}/wf-shell-srcux-0.11.0-r72" -KEYWORDS="~amd64" -SLOT="0/0.11" - -LICENSE="MIT" -IUSE="+settings +appearance +ddcutil +pipewire +pulseaudio test" - -RESTRICT="fetch mirror !test? ( test )" - -DEPEND=" - test? ( dev-qt/qtbase:6[widgets] ) - dev-cpp/cairomm:1.16 - dev-cpp/glibmm:2.68 - dev-cpp/nlohmann_json - >=dev-cpp/tomlplusplus-3.4.0 - >=dev-cpp/gtkmm-4.12:4.0 - dev-libs/glib:2 - >=dev-libs/gobject-introspection-1.82.0-r2 - dev-libs/libsigc++:3 - dev-libs/libdbusmenu[gtk3] - >=gui-libs/gtk4-layer-shell-1.3.0[introspection] - dev-libs/openssl:= - dev-libs/wayland - dev-libs/yyjson:= - dev-db/sqlite:3 - gui-libs/gtk:4[wayland] - >=gui-libs/wf-config-0.11.0:= - >=gui-wm/wayfire-0.11.0-r64 - >=gui-libs/pixdecor-9999-r1 - media-libs/libepoxy - media-libs/mesa[opengl] - sys-libs/pam - x11-libs/libdrm - x11-libs/libxkbcommon - ddcutil? ( - app-misc/ddcutil:= - ) - pipewire? ( - media-video/pipewire:= - media-video/wireplumber:= - ) - pulseaudio? ( - media-libs/alsa-lib - media-libs/libpulse - ) -" -RDEPEND="${DEPEND} - >=gui-libs/wayfire-plugins-custom-0.1.0-r9[panel-desktop,window-state] - >=gui-libs/xdg-desktop-portal-wlr-0.8.1-r1 - appearance? ( >=gui-apps/wf-appearance-0.2.0-r4 ) - gui-apps/wayland-logout - gui-apps/qt6ct -" -PDEPEND="settings? ( >=gui-apps/wf-settings-0.11.0 )" -BDEPEND=" - test? ( sys-apps/dbus ) - dev-libs/wayland-protocols - dev-util/wayland-scanner - virtual/pkgconfig -" - - -src_configure() { - local emesonargs=( - -Dcomponent=shell - "$(meson_feature pipewire wp-mixer-widget)" - "$(meson_feature pulseaudio volume-widget)" - "$(meson_feature ddcutil)" - -Dwayland-logout=false - -Dweather=false - "$(meson_use test tests)" - ) - meson_src_configure -} - -src_install() { - meson_src_install - insinto /usr/share/wf-shell/presets - doins data/presets/wayfire-mist.ini - insinto /usr/share/wf-shell/themes/wayfire-mist - doins data/css/wayfire-mist.css - dodoc docs/*.md wf-shell.ini.example -} - -pkg_postinst() { - xdg_pkg_postinst - elog "See STAGE8-CUTOVER.md for the one-time visual transfer and qualification." - elog "No user configuration or session autostart is changed by this package." -} - -pkg_nofetch() { - einfo "Place the local wf-shell-srcux-0.11.0-r72.tar.xz source release in ${DISTDIR}." -} diff --git a/gui-apps/wf-shell/wf-shell-9999.ebuild b/gui-apps/wf-shell/wf-shell-9999.ebuild new file mode 100644 index 000000000000..b2c53b2a7ab3 --- /dev/null +++ b/gui-apps/wf-shell/wf-shell-9999.ebuild @@ -0,0 +1,95 @@ +# Copyright 2019-2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit meson xdg + +DESCRIPTION="Compiz like 3D wayland compositor" +HOMEPAGE="https://github.com/WayfireWM/wf-shell" + +if [[ ${PV} == 9999 ]]; then + inherit git-r3 + EGIT_REPO_URI="https://github.com/WayfireWM/wf-shell.git" + SLOT="0/0.12" +else + SRC_URI="https://github.com/WayfireWM/wf-shell/releases/download/v${PV}/${P}.tar.xz" + KEYWORDS="~amd64" + SLOT="0/$(ver_cut 1-2)" +fi + +LICENSE="MIT" +IUSE="ddcutil pipewire +pulseaudio" + +# no tests +RESTRICT="test" + +DEPEND=" + dev-cpp/cairomm:1.16 + dev-cpp/glibmm:2.68 + dev-cpp/gtkmm:4.0 + dev-libs/glib:2 + >=dev-libs/gobject-introspection-1.82.0-r2 + dev-libs/libsigc++:3 + dev-libs/libdbusmenu[gtk3] + >=gui-libs/gtk4-layer-shell-1.3.0[introspection] + dev-libs/openssl:= + dev-libs/wayland + dev-libs/yyjson:= + gui-libs/gtk:4[wayland] + >=gui-libs/wf-config-0.7.0:= + gui-wm/wayfire + media-libs/libepoxy + media-libs/mesa[opengl] + sys-libs/pam + x11-libs/libdrm + x11-libs/libxkbcommon + ddcutil? ( + app-misc/ddcutil:= + ) + pipewire? ( + media-video/pipewire:= + media-video/wireplumber:= + ) + pulseaudio? ( + media-libs/alsa-lib + media-libs/libpulse + ) +" +RDEPEND="${DEPEND} + gui-apps/wayland-logout +" +BDEPEND=" + dev-libs/wayland-protocols + dev-util/wayland-scanner + virtual/pkgconfig +" + +src_prepare() { + default + + if [[ $PV != *9999* ]]; then + # wf-json is bundled as a subproject + # no need to unbundle it, it's a static library containing a wrapper to yyjson + rmdir subprojects/wf-json || die + mv "${WORKDIR}"/wf-json-${WF_JSON_COMMIT} subprojects/wf-json || die + + if use pulseaudio; then + # bundled subproject for the volume widget + # static, written to be used as a subproject + rmdir subprojects/gvc || die + mv "${WORKDIR}"/libgnome-volume-control-${GVC_COMMIT} subprojects/gvc || die + fi + fi +} + +src_configure () { + local emesonargs=( + "$(meson_feature pipewire wp-mixer-widget)" + "$(meson_feature pulseaudio volume-widget)" + "$(meson_feature ddcutil)" + -Dwayland-logout=false + -Dweather=false + ) + meson_src_configure +} diff --git a/gui-libs/pixdecor/Manifest b/gui-libs/pixdecor/Manifest new file mode 100644 index 000000000000..a649f2d6cba4 --- /dev/null +++ b/gui-libs/pixdecor/Manifest @@ -0,0 +1 @@ +DIST pixdecor-0.1.0_p20261006.gh.tar.gz 56131 BLAKE2B 20b13c76a1ca603acc460ba3bdd66e92f2db79912061485c4666bd4aec8dde7c10dff22df90a4287359b7c1a93a00de8dbe6461d5e228f4ef163fb24391d36ca SHA512 a6e22e7e2fc041d5ad3886904314a0e17499b42d00a965ce45f0e6701945935afda122a66e6dbcc9229c48c5b1f0c663cec11bbe75a7e31987ead23eced4326b diff --git a/gui-libs/pixdecor/pixdecor-0.1.0_p20261006.ebuild b/gui-libs/pixdecor/pixdecor-0.1.0_p20261006.ebuild new file mode 100644 index 000000000000..3a2b04f69e65 --- /dev/null +++ b/gui-libs/pixdecor/pixdecor-0.1.0_p20261006.ebuild @@ -0,0 +1,56 @@ +# Copyright 2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +inherit meson + +DESCRIPTION="Wayfire decorator with rounded corners, shadows, and animated effects" +HOMEPAGE="https://github.com/soreau/pixdecor" +# Snapshot of the commit pixdecor-9999-r2 pins (2026-10-06); upstream has no releases. +COMMIT="222e61d971b045a821cf212a6e221367c05d966b" +SRC_URI="https://github.com/soreau/pixdecor/archive/${COMMIT}.tar.gz -> ${P}.gh.tar.gz" +S="${WORKDIR}/${PN}-${COMMIT}" + +PATCHES=( "${FILESDIR}/${PN}-sculpted-frames.patch" "${FILESDIR}/${PN}-shell-theme.patch" ) + +LICENSE="MIT" +SLOT="0" +KEYWORDS="~amd64" +# Upstream has no test suite. +RESTRICT="test" + +RDEPEND=" + dev-libs/glib:2 + dev-libs/wayland + gui-libs/wf-config:= + gui-libs/wlroots:0.20= + >=gui-wm/wayfire-0.11.0:=[gles3] + media-libs/libglvnd + x11-libs/cairo + x11-libs/pango + x11-libs/pixman +" +DEPEND=" + ${RDEPEND} + media-libs/glm + sys-kernel/linux-headers +" +BDEPEND=" + sys-devel/gettext + virtual/pkgconfig +" + +src_install() { + meson_src_install + exeinto "/usr/$(get_libdir)/wayfire/pixdecor-shell-r2" + doexe "${BUILD_DIR}/src/libpixdecor.so" + dodoc README.md +} + +pkg_postinst() { + elog "Add pixdecor to the plugins list in the [core] section of wayfire.ini." + elog "Disable other decoration plugins; Pixdecor owns window frames." + elog "Frame settings are in WCM -> Pix Decoration -> Window frames." + elog "Sculpted adds border depth and pinstripe color/opacity." +} diff --git a/net-misc/airouter/Manifest b/net-misc/airouter/Manifest index 7e5b20a27a01..25bb7ba00823 100644 --- a/net-misc/airouter/Manifest +++ b/net-misc/airouter/Manifest @@ -11,3 +11,4 @@ DIST airouter-0.1.2-r8.tar.zst 87713824 BLAKE2B c48ffb4f9f91bc87d79cae7e43fe5b9a DIST airouter-0.1.2-r9.tar.zst 87680153 BLAKE2B 66cef0000fdb9101dec8c96aaefd5a9022fb37dd2564a7beb4295b9b2cce5143d82e7c8832575f16ea9ab0f2c1c06fcf346081024025a6ffd76d82aaa838a110 SHA512 ed96a2ce94faf462bf4ce22f043cf3b570f744bb328a0afadd075a9fa7891d0f936ca0aeeed078d6958501eafe142f0e315a4bd2a81e01a999c5ca1f6c236d4f DIST mime_guess-2.0.5.crate 27166 BLAKE2B a8ec67e16a336e7d07c9aaa55025c24c3d4a62aae25213ce768786d9f292b76930132a880d662cd9a5d63aa899e9d488ab76fa9d99350e9f448ef484705ba4d9 SHA512 433c876e28394282cfbe14bb9543616cd296879f253ed331c15ace8a7d9f3629f5b596c2d5a64823a97ca9b774acc711b65006c46d54d1e9ee0553cbf1f13b7c DIST airouter-1.0.0_pre20261012.tar.gz 685451 BLAKE2B a07b1dd5d20ffc86d959394b74431cfc4bf3aee4d76a161e97ece035ba3460cded3cbd7df695e0b94956ae54cb4e5a9e98d47ee69277684664451a000b90d448 SHA512 c6110b5749cfc8ec7ab827303e537c0d5690ba803f022f1380de4d2fe5220590282d6d055e1de9f3809d00d1462c12d1ead276aabb09ecadb519bdaa8d005bb7 +DIST airouter-1.0.0_pre20261012_p6.tar.gz 796203 BLAKE2B c9f233815c85e3b17a10ba23d43f5e856f725b1e6222a8924b8d7a90bca2a72c26bda5a80c13f56ac458d7e350cd9f4f57cf2b2898bfbdbc4290005a34ed709d SHA512 b10b9433d04cc94c1ec13cb50c51bbacb27e3e5df813df351fad6788c3746e3dd5bfec980c45efbbc22ca81c2b33101a156a9bda337e417769cae1b4faba2e7f diff --git a/net-misc/airouter/airouter-1.0.0_pre20261012_p6.ebuild b/net-misc/airouter/airouter-1.0.0_pre20261012_p6.ebuild new file mode 100644 index 000000000000..b68fda2cdd95 --- /dev/null +++ b/net-misc/airouter/airouter-1.0.0_pre20261012_p6.ebuild @@ -0,0 +1,78 @@ +EAPI=8 + +inherit cmake flag-o-matic + +DESCRIPTION="AIRouter runtime (C++; replaces the Rust 0.1.x airouterd)" +HOMEPAGE="https://trunkmasters.com/airouter" +SRC_URI="https://www.srcux.com/distfiles/${P}.tar.gz" + +# Mirrors the Rust net-misc/airouter; the license declaration is an owner decision. +LICENSE="MIT" +SLOT="0" +KEYWORDS="~amd64" + +# The build pins exact dependency versions (M0 host pins). Compiler pins are +# enforced by CMake itself (GCC 14.4.1 or Clang 23.1.3). +DEPEND=" + =dev-libs/boost-1.91.0* + =dev-libs/openssl-3.6.5* + =dev-db/postgresql-18.6* + =dev-cpp/nlohmann_json-3.12.0* + =dev-cpp/tomlplusplus-3.4.0* + =dev-cpp/yaml-cpp-0.9.0* +" +# chronyc reports are parsed in their verified CSV shapes (ADR-05 [time]). +RDEPEND=" + ${DEPEND} + >=acct-group/airouter-1 + >=acct-user/airouter-1 + app-admin/sudo + =net-misc/chrony-4.9* + >=dev-lang/python-3.11 +" +BDEPEND=" + =dev-cpp/catch-3.15.3* + >=dev-lang/python-3.11 +" + +src_configure() { + # The project refuses in-source builds and requires an absolute build root. + export AIROUTER_CPP_BUILD_ROOT="${WORKDIR}/build" + # GCC 14 at -O2 reports -Wmismatched-new-delete (a -Werror error here) inside + # Boost.Asio's recycling awaitable-frame allocator: operator new and operator delete + # are a matched pair, but GCC sees the inlined aligned_alloc. The CMake presets + # (-O0, -O3) do not trigger it. Only this one diagnostic is disabled; -Werror stays. + append-cxxflags -Wno-mismatched-new-delete + local mycmakeargs=( -DAIROUTER_SANITIZER=none ) + cmake_src_configure +} + +src_test() { + # Unit and digest-vector tests run here. The PostgreSQL/bwrap end-to-end and the + # install-layout tests run in CI, outside the Portage sandbox. + cmake_src_test -R '^m1-(unit|digest-vectors)$' +} + +src_install() { + cmake_src_install + newinitd "${ED}"/usr/share/airouter/openrc/airouterd.initd airouterd + newconfd "${ED}"/usr/share/airouter/openrc/airouterd.confd airouterd + insinto /etc/airouter + newins packaging/airouterd.toml.example airouterd.toml.example + # /var/lib/airouter is the acct-user home (owned there); the service's + # start_pre creates its receipts, run and log directories with checkpath. +} + +pkg_postinst() { + elog "Copy /etc/airouter/airouterd.toml.example to airouterd.toml," + elog "apply the schema with airouter-migrate (as the migrator role, from" + elog "/usr/share/airouter/sql), keep execution.cgroup equal to AIROUTERD_CGROUP" + elog "in /etc/conf.d/airouterd (the service delegates it at start)," + elog "create the [history] directory on its own filesystem (for example a ZFS dataset" + elog "outside the database's snapshots and backups; the daemon refuses a shared one)," + elog "set [time] to the NTS-authenticated chronyd (its socket, uid, config, the" + elog "config's sha256sum and the addresses its NTS server name resolves to), install" + elog "/usr/share/airouter/sudoers.airouter as /etc/sudoers.d/airouterd with" + elog "those addresses (it lets the service read chronyd's reports as ntp)," + elog "then: rc-service airouterd start." +} diff --git a/profiles/desc/libreoffice_extensions.desc b/profiles/desc/libreoffice_extensions.desc index 95b8ad3381d8..87f7da7a6e41 100644 --- a/profiles/desc/libreoffice_extensions.desc +++ b/profiles/desc/libreoffice_extensions.desc @@ -6,5 +6,5 @@ # Keep it sorted. nlpsolver - LIBREOFFICE_EXTENSIONS option to build non-linear solver for calc scripting-beanshell - LIBREOFFICE_EXTENSIONS option to build beanshell scripts parser -scripting-javascript - LIBREOFFICE_EXTENSIONS option to build javascript scripts parser +scripting-javascript - LIBREOFFICE_EXTENSIONS option to build javascript scripts parser wiki-publisher - LIBREOFFICE_EXTENSIONS option to build mediawiki integration diff --git a/profiles/desc/uwsgi_plugins.desc b/profiles/desc/uwsgi_plugins.desc index 49d27a351fa6..a32239a307f1 100644 --- a/profiles/desc/uwsgi_plugins.desc +++ b/profiles/desc/uwsgi_plugins.desc @@ -51,8 +51,8 @@ router_redis - uWSGI internal routing, redis support router_rewrite - uWSGI internal routing, URL rewrite support router_spnego - uWSGI internal routing, spnego auth support router_static - uWSGI internal routing, static files serving support -router_uwsgi - uWSGI internal routing, -router_xmldir - uWSGI internal routing, +router_uwsgi - uWSGI internal routing, +router_xmldir - uWSGI internal routing, rpc - Allow calling functions on a remote uWSGI server/cluster rrdtool - Store uWSGI stats in a rrd fashion rsyslog - Send logs to Unix standard syslog residing on a remote server diff --git a/profiles/package.mask/srcux-workarounds b/profiles/package.mask/srcux-workarounds index 873aef750291..318f9ae9b394 100644 --- a/profiles/package.mask/srcux-workarounds +++ b/profiles/package.mask/srcux-workarounds @@ -26,5 +26,6 @@ # hosts with the Rust AIRouter are not upgraded by a world update; unmask per host # (bravo first, for the two-host qualification gate). =net-misc/airouter-1.0.0_pre20261012 +=net-misc/airouter-1.0.0_pre20261012_p6 =acct-user/airouter-1 =acct-group/airouter-1 diff --git a/profiles/srcux/1.0/linux-gnu/arch/amd64/package.use/llvm b/profiles/srcux/1.0/linux-gnu/arch/amd64/package.use/llvm deleted file mode 100644 index f20cfea3cf67..000000000000 --- a/profiles/srcux/1.0/linux-gnu/arch/amd64/package.use/llvm +++ /dev/null @@ -1,2 +0,0 @@ -sys-devel/clang llvm_targets_AMDGPU llvm_targets_BPF llvm_targets_NVPTX -sys-devel/llvm llvm_targets_AMDGPU llvm_targets_BPF llvm_targets_NVPTX
\ No newline at end of file diff --git a/profiles/srcux/1.0/linux-gnu/flavor/desktop/make.defaults b/profiles/srcux/1.0/linux-gnu/flavor/desktop/make.defaults index 7e7a5989c4a3..1b48b26cf6f7 100644 --- a/profiles/srcux/1.0/linux-gnu/flavor/desktop/make.defaults +++ b/profiles/srcux/1.0/linux-gnu/flavor/desktop/make.defaults @@ -15,3 +15,8 @@ USE="${USE} udev" # TODO USE="${USE} libnotify startup-notification xcb" + +# 2026-10-15: dbus and opengl, as in Gentoo's desktop profile. gui/* turns on wayland, +# and without them www-client/firefox (wayland? ( dbus )) and dev-qt/qtbase +# (wayland? ( opengl )) fail REQUIRED_USE on every srcux desktop. +USE="${USE} dbus opengl" diff --git a/profiles/srcux/1.0/linux-gnu/flavor/desktop/package.use b/profiles/srcux/1.0/linux-gnu/flavor/desktop/package.use index 29b32cf6aeed..a30971dc8e19 100644 --- a/profiles/srcux/1.0/linux-gnu/flavor/desktop/package.use +++ b/profiles/srcux/1.0/linux-gnu/flavor/desktop/package.use @@ -93,3 +93,24 @@ net-nds/openldap minimal # Palica (2021-07-08) # This seems to be new requirement for freetype on desktop >=media-libs/freetype-2.10.4 harfbuzz + +# 2026-10-15: features/video enables ffmpeg globally; pipewire requires extra with it +# (REQUIRED_USE ffmpeg? ( extra )). +media-video/pipewire extra + +# 2026-10-15: these require a GUI toolkit with dbus/opengl (make.defaults), which srcux +# desktops do not enable globally (Gentoo's desktop profile sets gui/gtk); keep their +# previous defaults. +dev-games/mygui -opengl +games-board/gnubg -opengl +media-video/clapper -opengl +media-video/mkvtoolnix -dbus +net-p2p/bitcoin-core -dbus +net-p2p/gridcoin -dbus + +# 2026-10-15: as in Gentoo's desktop profile (kde-frameworks/kio requires it). +dev-qt/qtbase libproxy + +# 2026-10-15: net-libs/nodejs-26.12.0 (pulled in by firefox) requires it; Gentoo has no +# default for it yet. Drop once a profile or the ebuild enables it. +dev-cpp/simdutf atomic-base64 diff --git a/sci-ml/ggml/Manifest b/sci-ml/ggml/Manifest index ffdd91e5ab71..4cc4e8ce2683 100644 --- a/sci-ml/ggml/Manifest +++ b/sci-ml/ggml/Manifest @@ -1,2 +1,3 @@ DIST ggml-0.24.0.tar.gz 4010435 BLAKE2B 52ff0bcb6ceb000c05369a18c6a07bcbd1da07133aec2efaaaf1f8ee0fc70c67b44e8b6eb430aa48d8982ff93782f600bd2689fde98a1c14d07bd4941bee3cd6 SHA512 dd4cad3e3e39ea0f599a6361c4d2fa74c2879e3548c022a995f63d5f54ceac579930eace7037be8df9be9a261d92a0fd7be073a9b28c874d3c660344cb9a50d4 DIST ggml-0.25.3.tar.gz 4103121 BLAKE2B be368597848d35f62dff0c4bed0c219fd70c28808d2c44071d74c762ab8296a7279fb036899ea711826ea1743ca7f0928f0dfe662156bbfe2933557acb2edcbd SHA512 2344cf53dadeb1f1b2fd392aa8ae1850992fa127740a0a78b085be11708fe3c6423bfdd94009d3409c4a99fac7f525d8dfca0ba955526e3ae9b1024a5e73bcbe +DIST ggml-0.26.0.tar.gz 4211759 BLAKE2B 06e89b228d57ed659580aec2be3dadd468b6236dc5d623920b2977372a495ac3e33f25c8d90875dbe353b2aa71724cafc16b6e11d5542a26f3cadf1c91d8efdc SHA512 d786cdb4b8251c68c37b15196102b909509d04991102c20323513c8dc288bfa6d0ef838bb5d87d87b7eec6e5763c516b7cfd53b189928f783182bff5cced4645 diff --git a/sci-ml/ggml/ggml-0.26.0.ebuild b/sci-ml/ggml/ggml-0.26.0.ebuild new file mode 100644 index 000000000000..573d807f0a22 --- /dev/null +++ b/sci-ml/ggml/ggml-0.26.0.ebuild @@ -0,0 +1,125 @@ +# Copyright 2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +ROCM_VERSION=7.2 +inherit cuda cmake rocm toolchain-funcs flag-o-matic + +DESCRIPTION="Tensor library for machine learning" +HOMEPAGE="https://ggml.ai/" +SRC_URI="https://github.com/ggml-org/${PN}/archive/refs/tags/v${PV}.tar.gz + -> ${P}.tar.gz" + +LICENSE="MIT" +SLOT="0/${PV}" +KEYWORDS="~amd64 ~arm64" + +X86_CPU_FLAGS=( + amx_bf16 + amx_int8 + amx_tile + avx + avx_vnni + avx2 + avx512_bf16 + avx512bw + avx512f + avx512vbmi + avx512_vnni + bmi2 + fma3 + f16c + sse4_2 +) +CPU_FLAGS=( "${X86_CPU_FLAGS[@]/#/cpu_flags_x86_}" ) +IUSE="${CPU_FLAGS[*]} cuda openmp rocm test vulkan" + +REQUIRED_USE="rocm? ( ${ROCM_REQUIRED_USE} )" + +RESTRICT="!test? ( test )" + +# Should be >=sci-libs/hipBLAS-${ROCM_VERSION}[${ROCM_USEDEP}] +# But pkgcheck can't elaborate that +RDEPEND=" + cuda? ( + dev-util/nvidia-cuda-toolkit:= + ) + vulkan? ( media-libs/vulkan-loader ) + rocm? ( + >=dev-util/hip-${ROCM_VERSION} + >=sci-libs/hipBLAS-${ROCM_VERSION} + ) +" +DEPEND="${RDEPEND} + vulkan? ( dev-util/vulkan-headers ) +" +BDEPEND="vulkan? ( media-libs/shaderc )" + +PATCHES=( "${FILESDIR}/ggml-0.24.0-const-fp16-loads.patch" ) + +pkg_pretend() { + [[ ${MERGE_TYPE} != binary ]] && use openmp && tc-check-openmp +} + +pkg_setup() { + [[ ${MERGE_TYPE} != binary ]] && use openmp && tc-check-openmp +} + +src_prepare() { + cmake_src_prepare + + if use cuda; then + cuda_src_prepare + filter-lto #bug 983121 + fi +} + +src_configure() { + local mycmakeargs=( + -DGGML_BACKEND_DL=OFF + -DGGML_BUILD_EXAMPLES=OFF + -DGGML_NATIVE=OFF + -DGGML_HIP_MMQ_MFMA=OFF + + # CPU Flags + -DGGML_AMX_BF16=$(usex cpu_flags_x86_amx_bf16) + -DGGML_AMX_INT8=$(usex cpu_flags_x86_amx_int8) + -DGGML_AMX_TILE=$(usex cpu_flags_x86_amx_tile) + -DGGML_AVX=$(usex cpu_flags_x86_avx) + -DGGML_AVX_VNNI=$(usex cpu_flags_x86_avx_vnni) + -DGGML_AVX2=$(usex cpu_flags_x86_avx2) + -DGGML_AVX512_BF16=$(usex cpu_flags_x86_avx512_bf16) + -DGGML_AVX512_VBMI=$(usex cpu_flags_x86_avx512vbmi) + -DGGML_AVX512_VNNI=$(usex cpu_flags_x86_avx512_vnni) + -DGGML_BMI2=$(usex cpu_flags_x86_bmi2) + -DGGML_FMA=$(usex cpu_flags_x86_fma3) + -DGGML_F16C=$(usex cpu_flags_x86_f16c) + -DGGML_SSE42=$(usex cpu_flags_x86_sse4_2) + + -DGGML_CUDA=$(usex cuda) + -DGGML_OPENMP=$(usex openmp) + -DGGML_HIP=$(usex rocm) + -DGGML_VULKAN=$(usex vulkan) + + -DGGML_BUILD_TESTS=$(usex test) + ) + + # Enable AVX512 if ANY of the avx512 flags are present + if use cpu_flags_x86_avx512f || use cpu_flags_x86_avx512bw; then + mycmakeargs+=( -DGGML_AVX512=ON ) + else + mycmakeargs+=( -DGGML_AVX512=OFF ) + fi + + if use cuda; then + cuda_add_sandbox -w + addpredict "/dev/char/" + cuda_sanitize + mycmakeargs+=( + -DCMAKE_CUDA_FLAGS="${NVCCFLAGS}" + ) + fi + + cmake_src_configure +} |
