summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCrucible <crucible@localhost>2026-10-15 23:06:26 -0500
committerCrucible <crucible@localhost>2026-10-15 23:06:26 -0500
commit7fa0d2cfbafe2be6de7e11e911e806429a60bcf9 (patch)
tree96e786516fcdb8b1656afdbd37958ca551146f76
parent29235f1db35cf8b131ce6ae5696ce6f5e2195683 (diff)
downloadsrcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.tar.gz
srcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.tar.xz
srcux-7fa0d2cfbafe2be6de7e11e911e806429a60bcf9.zip
Publish srcux candidate 0000000698-20261015-225521
-rw-r--r--app-admin/scout/scout-0.4.0-r1.ebuild2
-rw-r--r--eclass/gclient-r1.eclass582
-rw-r--r--gui-apps/sendit/Manifest122
-rw-r--r--gui-apps/sendit/metadata.xml14
-rw-r--r--gui-apps/sendit/sendit-0.1.0-r67.ebuild227
-rw-r--r--gui-apps/wf-settings/Manifest1
-rw-r--r--gui-apps/wf-settings/metadata.xml9
-rw-r--r--gui-apps/wf-settings/wf-settings-0.11.0.ebuild29
-rw-r--r--gui-apps/wf-shell/Manifest2
-rw-r--r--gui-apps/wf-shell/files/wf-shell-0.11.0-async-ddc.patch597
-rw-r--r--gui-apps/wf-shell/files/wf-shell-0.11.0-panel-desktop.patch259
-rw-r--r--gui-apps/wf-shell/files/wf-shell-0.11.0-r8-shell-experience.patch4708
-rw-r--r--gui-apps/wf-shell/metadata.xml19
-rw-r--r--gui-apps/wf-shell/tests/ddc-startup/README.md35
-rw-r--r--gui-apps/wf-shell/tests/ddc-startup/fake-ddc.c47
-rw-r--r--gui-apps/wf-shell/tests/ddc-startup/test-startup.py134
-rw-r--r--gui-apps/wf-shell/tests/ddc-startup/title-client.c63
-rw-r--r--gui-apps/wf-shell/wf-shell-0.10.0-r1.ebuild60
-rw-r--r--gui-apps/wf-shell/wf-shell-0.11.0-r1.ebuild81
-rw-r--r--gui-apps/wf-shell/wf-shell-0.11.0-r11.ebuild98
-rw-r--r--gui-apps/wf-shell/wf-shell-0.11.0-r72.ebuild104
-rw-r--r--gui-apps/wf-shell/wf-shell-9999.ebuild95
-rw-r--r--gui-libs/pixdecor/Manifest1
-rw-r--r--gui-libs/pixdecor/pixdecor-0.1.0_p20261006.ebuild56
-rw-r--r--net-misc/airouter/Manifest1
-rw-r--r--net-misc/airouter/airouter-1.0.0_pre20261012_p6.ebuild78
-rw-r--r--profiles/desc/libreoffice_extensions.desc2
-rw-r--r--profiles/desc/uwsgi_plugins.desc4
-rw-r--r--profiles/package.mask/srcux-workarounds1
-rw-r--r--profiles/srcux/1.0/linux-gnu/arch/amd64/package.use/llvm2
-rw-r--r--profiles/srcux/1.0/linux-gnu/flavor/desktop/make.defaults5
-rw-r--r--profiles/srcux/1.0/linux-gnu/flavor/desktop/package.use21
-rw-r--r--sci-ml/ggml/Manifest1
-rw-r--r--sci-ml/ggml/ggml-0.26.0.ebuild125
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:
- ![Old look](/gallery/legacy.png)
-+
-+## 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, &center_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(&notifications_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,&registry_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
+}