Files
hyprcosmic-session/data/start-hyprcosmic
T
gitops 73867a5d6a Install as the session, at upstream's paths
Eight files: upstream's six, where upstream puts them, plus this fork's
start-hyprcosmic and hyprcosmic.desktop. Replaces the private libexec
prefix, matching the same change in cosmic-comp.

A machine that installs this gets its cosmic-session from here, so there
is nothing else on disk to provide the systemd target, the mimeapps list
or the dconf profile. Omitting them produced a session that started and
then had no user target for the portals to attach to, which surfaces much
later and much less clearly than a missing file.

Both session entries are installed and both are served by these binaries.
Replacing COSMIC need not mean removing its shell, and keeping
cosmic.desktop costs one file while preserving the one property worth
keeping from the beside-install layout: if the HyDE shell will not start,
there is still something on the greeter's menu that will.

start-hyprcosmic now names /usr/bin for both binaries, still through
overridable variables so the check below it can say which path was
missing and so the session still runs out of a build tree.

The dconf asymmetry is deliberate and now asserted in CI, because it is
easy to get backwards once both scripts are installed: start-cosmic is
sed'd to an absolute profile path, as upstream does; start-hyprcosmic is
not, because it searches /etc/dconf/profile and XDG_DATA_DIRS at runtime
and pinning a build prefix would break an administrator's override.
2026-08-10 18:21:42 +07:00

303 lines
14 KiB
Bash
Executable File

#!/usr/bin/bash
#
# Launch a HyprCosmic session: cosmic-comp for window management, HyDE's shell
# on top.
#
# Deliberately separate from /usr/bin/start-cosmic rather than patching it.
# The stock COSMIC session entry keeps working untouched, so a broken
# HyprCosmic config is always one logout away from being escaped — which
# matters when the thing you are replacing is your desktop.
set -e
# Re-exec through the user's login shell, before anything else runs.
#
# The greeter starts this script with a minimal environment: what pam and
# systemd put there, and nothing from /etc/profile or the user's own login
# files. start-cosmic re-execs itself through $SHELL to pick those up, and not
# doing the same here is why the two sessions disagree about the environment.
# see: https://github.com/pop-os/cosmic-session/issues/23
#
# The measurable difference on this machine is XDG_DATA_DIRS. Without this pass
# it arrives empty; /etc/profile.d/flatpak.sh is what appends the two flatpak
# exports directories, and those are where flatpak applications keep the
# .desktop files and icons the launcher looks for. PATH is untouched either way
# -- /etc/profile only appends what is missing -- so this adds and does not
# rearrange.
#
# First in the file, deliberately. The login files are allowed to set
# XDG_CACHE_HOME, which is where the log below decides to go, so this has to
# happen while there is still nothing to lose. The cost is one extra process
# and, measured here, about 35ms.
#
# The guard is an exported variable rather than start-cosmic's positional
# `--in-login-shell`. A flag has to survive being quoted through another shell
# to be seen on the way back in; if it ever failed to, the script would re-exec
# itself forever and the login would hang instead of failing. An exported
# variable survives exec by definition, so the recursion cannot happen.
#
# `exec -l` is bash's own flag and this script is bash, so start-cosmic's extra
# `bash -c` hop buys nothing. The inner `exec` means the login shell replaces
# itself with the session rather than sitting there as its parent for the rest
# of the login.
#
# Whatever goes wrong here is unlogged, because the log does not exist yet --
# hence checking that the shell is real and runnable first rather than finding
# out from a session that dies silently.
if [[ -z "${HYPRCOSMIC_LOGIN_SHELL:-}" && -n "${SHELL:-}" && -x "${SHELL}" ]]; then
case "${SHELL##*/}" in
nologin | false)
# An account with no usable login shell still gets a desktop.
;;
*)
export HYPRCOSMIC_LOGIN_SHELL=1
exec -l "${SHELL}" -c "exec $(printf '%q' "$0")"
;;
esac
fi
# The binaries. HyprCosmic replaces COSMIC rather than installing beside it, so
# these are the ordinary paths -- this fork's cosmic-session *is* the machine's
# cosmic-session, and there is no second copy under a private prefix.
#
# Still named in variables rather than called bare. Two reasons: the check below
# can then say which path was missing, and both stay overridable from the
# environment, which is how the session is run out of a build tree without
# installing it.
HYPRCOSMIC_SESSION_BIN="${HYPRCOSMIC_SESSION_BIN:-/usr/bin/cosmic-session}"
HYPRCOSMIC_COMP_BIN="${HYPRCOSMIC_COMP_BIN:-/usr/bin/cosmic-comp}"
if [[ ! -x "$HYPRCOSMIC_SESSION_BIN" ]]; then
echo "start-hyprcosmic: $HYPRCOSMIC_SESSION_BIN is missing or not executable" >&2
echo "start-hyprcosmic: log out and choose the stock COSMIC session" >&2
exit 1
fi
# Capture this session's output to a file that outlives it.
#
# A session that fails drops you straight back to the greeter, taking its
# stderr with it -- and by then you cannot read the journal from inside a
# session that no longer exists. A plain file in the cache directory is still
# there afterwards, whatever happened.
#
# A plain redirect, not `tee` into a process substitution: tee would be a child
# process the session writes through, so if it ever died the session would take
# a SIGPIPE. Nothing about diagnostics should be able to kill the desktop.
#
# Every step is guarded. If any of it fails the session starts anyway with no
# log -- losing diagnostics is an inconvenience, refusing to log in is not.
#
# The previous log is kept as .1, so a second attempt does not erase the
# evidence from the first.
HYPRCOSMIC_LOG="${XDG_CACHE_HOME:-$HOME/.cache}/hyprcosmic/session.log"
if mkdir -p "$(dirname "$HYPRCOSMIC_LOG")" 2>/dev/null; then
if [[ -f "$HYPRCOSMIC_LOG" ]]; then
mv -f "$HYPRCOSMIC_LOG" "${HYPRCOSMIC_LOG}.1" 2>/dev/null || true
fi
if : >>"$HYPRCOSMIC_LOG" 2>/dev/null; then
exec >>"$HYPRCOSMIC_LOG" 2>&1
echo "=== hyprcosmic session starting $(date -Is) ==="
echo " session bin: $HYPRCOSMIC_SESSION_BIN"
echo " comp bin: $HYPRCOSMIC_COMP_BIN"
echo " autostart: ${XDG_CONFIG_HOME:-$HOME/.config}/hyprcosmic/autostart"
fi
fi
# Selects the component set in cosmic-session's profile module: cosmic-panel,
# cosmic-launcher, cosmic-app-library, cosmic-workspaces, cosmic-bg and
# cosmic-files-applet are skipped; waybar and friends come from
# ~/.config/hyprcosmic/autostart.
export HYPRCOSMIC_PROFILE=hyprcosmic
# Keep COSMIC's identity: portals, cosmic-settings and any COSMIC app still
# key off this, and the compositor underneath really is COSMIC.
export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-COSMIC}"
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-hyprcosmic}"
export XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
# Toolkit hints, same set start-cosmic exports.
#
# These are what make non-GTK applications behave under a Wayland session at
# all: Firefox on Wayland rather than XWayland, Qt preferring the Wayland
# platform plugin and falling back to xcb, and Java AWT not expecting a
# reparenting window manager (without it, Swing windows come up blank or
# mispositioned). Nothing here is COSMIC-specific -- it is the generic Wayland
# session contract, and leaving it out is why Qt and Java apps looked different
# under HyprCosmic than under the stock session.
#
# `:-` rather than a plain assignment, matching the XDG lines above: if
# something in the environment already made a deliberate choice, it keeps it.
export _JAVA_AWT_WM_NONREPARENTING="${_JAVA_AWT_WM_NONREPARENTING:-1}"
export GDK_BACKEND="${GDK_BACKEND:-wayland,x11}"
export MOZ_ENABLE_WAYLAND="${MOZ_ENABLE_WAYLAND:-1}"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland;xcb}"
export QT_AUTO_SCREEN_SCALE_FACTOR="${QT_AUTO_SCREEN_SCALE_FACTOR:-1}"
export QT_ENABLE_HIGHDPI_SCALING="${QT_ENABLE_HIGHDPI_SCALING:-1}"
# Qt platform theme: CuteCosmic if it is installed, qt6ct/qt5ct if not.
#
# Kept pointing at COSMIC's theme rather than HyDE's, because this fork themes
# GTK/waybar/rofi and has no Qt pipeline: switching to qt5ct here would hand Qt
# apps to a configuration nothing in this project writes. CuteCosmic at least
# follows the COSMIC palette that cosmic.conf does drive.
if [[ -z "${QT_QPA_PLATFORMTHEME:-}" ]]; then
export QT_QPA_PLATFORMTHEME=cosmic
for qt_plugin_path in /usr/lib{*,/*}/qt6/plugins; do
if [[ -f "${qt_plugin_path}/platformthemes/libcutecosmictheme.so" ]]; then
export QT_QPA_PLATFORMTHEME=cosmic
break
elif [[ -f "${qt_plugin_path}/platformthemes/libqt6ct.so" ]] ||
[[ -f "${qt_plugin_path}/platformthemes/libqt5ct.so" ]]; then
# Keep looking; CuteCosmic later in the glob still wins.
# "qt5ct" is the value both qt5ct and qt6ct answer to.
export QT_QPA_PLATFORMTHEME=qt5ct
fi
done
unset qt_plugin_path
fi
# dconf profile, which is where COSMIC's own settings live.
#
# The profile file -- `user-db:cosmic` layered above the usual user and system
# databases -- is shipped by the cosmic-session package, under
# /usr/share/dconf/profile/cosmic on Fedora.
#
# Naming a profile that cannot be found is not a soft failure. dconf logs
# "using null configuration" and then every GSettings read falls back to schema
# defaults while writes go nowhere: a whole desktop's settings, apparently
# blank. Measured here -- a missing profile dumps zero keys where the default
# profile dumps a full set -- which is why this is guarded and start-cosmic's
# unconditional export is not copied.
#
# The bare name is used rather than an absolute path. start-cosmic has its
# prefix rewritten in at install time by the Justfile, but dconf searches
# /etc/dconf/profile first and XDG_DATA_DIRS after, so the bare name keeps an
# administrator's /etc override working instead of pinning one prefix.
dconf_profile_found=""
IFS=: read -r -a xdg_data_dirs <<<"${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" || :
for dir in /etc "${xdg_data_dirs[@]}"; do
if [[ -f "${dir}/dconf/profile/cosmic" ]]; then
dconf_profile_found=1
break
fi
done
if [[ -n "${dconf_profile_found}" ]]; then
export DCONF_PROFILE=cosmic
else
echo "start-hyprcosmic: no dconf profile 'cosmic' found; leaving DCONF_PROFILE unset" >&2
fi
unset dconf_profile_found xdg_data_dirs dir
# gnome-keyring, and the SSH agent it provides.
#
# PAM starts the daemon at login but brings up only its `control` socket, so
# without this a HyprCosmic session has no ssh-agent at all: SSH_AUTH_SOCK is
# unset, anything over SSH asks for the passphrase every time, and callers of
# the secrets API find nothing listening. `--start` attaches to the daemon that
# is already running and brings up whichever components are missing.
#
# The daemon writes `NAME=value` to stdout and chatter like
# "discover_other_daemon: 1" to stderr, so the 2>/dev/null is load-bearing for
# the parse, not just for tidiness.
#
# start-cosmic evals that stdout, but its eval is
# `eval "$(... > /dev/null 2>&1)"` -- the redirect is *inside* the substitution,
# so the eval always runs on an empty string and the block works only because
# of the socket checks that follow it. Rather than repair that by evaluating
# whatever a daemon chooses to write to stdout, read out the one variable we
# actually want.
if [[ -d "/run/user/$(id -u)/keyring" ]]; then
keyring_ssh_sock=""
if command -v gnome-keyring-daemon >/dev/null 2>&1; then
keyring_ssh_sock="$(
gnome-keyring-daemon --start --components=pkcs11,secrets,ssh 2>/dev/null |
sed -n 's/^SSH_AUTH_SOCK=//p' | tail -n1
)"
else
echo "start-hyprcosmic: gnome-keyring-daemon not found in PATH" >&2
fi
# start-cosmic's rule, kept: set the correct socket or set none at all,
# never a wrong one. The daemon's own answer goes first because it is
# authoritative about where it just put the socket; the two fixed paths are
# the fallback for when it told us nothing.
for candidate in \
"${keyring_ssh_sock}" \
"/run/user/$(id -u)/gcr/ssh" \
"/run/user/$(id -u)/keyring/ssh"
do
if [[ -n "${candidate}" && -S "${candidate}" ]]; then
export SSH_AUTH_SOCK="${candidate}"
break
fi
done
unset keyring_ssh_sock candidate
fi
# Same hygiene as start-cosmic: a failed unit left by a previous graphical
# session will otherwise block this one from starting.
if command -v systemctl >/dev/null; then
for unit in $(systemctl --user --no-legend --state=failed --plain list-units | cut -f1 -d' '); do
partof="$(systemctl --user show -p PartOf --value "$unit")"
for target in cosmic-session.target graphical-session.target; do
if [ "$partof" = "$target" ]; then
systemctl --user reset-failed "$unit"
break
fi
done
done
# Hand the session's identity to `systemd --user`, which is what actually
# launches the portal.
#
# Exporting XDG_CURRENT_DESKTOP above only reaches processes started *by
# this script*. The user manager is older than the session and has its own
# environment block, so anything it activates -- xdg-desktop-portal, and
# through it xdg-desktop-portal-cosmic, which is `SystemdService=` in its
# D-Bus service file -- never saw the variable.
#
# xdg-desktop-portal picks a backend by matching the `UseIn=` line of
# /usr/share/xdg-desktop-portal/portals/*.portal against
# XDG_CURRENT_DESKTOP. With the variable unset, `UseIn=COSMIC` did not
# match, no backend loaded, and org.freedesktop.portal.Screenshot was never
# exported -- so pressing PrtScr made cosmic-screenshot unwrap an error and
# abort with "COSMIC screenshot crashed". Nothing logged a missing portal;
# the only symptom was the crash.
#
# Ordering is fine: xdg-desktop-portal.service is `After=` and
# `Requisite=graphical-session.target`, which cosmic-session brings up
# later, so the portal cannot start before this import.
#
# SSH_AUTH_SOCK rides along for the same reason: a user unit or a
# systemd-activated app that wants the agent has to find it in the manager's
# environment, not in this script's. It is skipped harmlessly when the
# keyring block above declined to set it.
#
# `||:` throughout -- losing a portal is bad, refusing to log in is worse.
systemctl --user import-environment \
XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \
DCONF_PROFILE SSH_AUTH_SOCK ||:
fi
# Same variables again for dbus-daemon's own activation environment, which
# covers any backend whose .service file has no `SystemdService=` line.
#
# Only when a bus already exists. In the dbus-run-session branch below the bus
# is created *after* this point, so calling it here would talk to the wrong bus
# or none at all. That branch is the fallback path anyway; a normal graphical
# login arrives with DBUS_SESSION_BUS_ADDRESS already set.
if [[ -n "${DBUS_SESSION_BUS_ADDRESS}" ]] && command -v dbus-update-activation-environment >/dev/null; then
dbus-update-activation-environment \
XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \
DCONF_PROFILE SSH_AUTH_SOCK ||:
fi
# cosmic-session takes the compositor to launch as its first argument
# (main.rs: `args.next().unwrap_or_else(|| String::from("cosmic-comp"))`),
# which is how a forked cosmic-comp gets used without replacing the system one.
if [[ -z "${DBUS_SESSION_BUS_ADDRESS}" ]]; then
exec /usr/bin/dbus-run-session -- "${HYPRCOSMIC_SESSION_BIN}" "${HYPRCOSMIC_COMP_BIN}"
else
exec "${HYPRCOSMIC_SESSION_BIN}" "${HYPRCOSMIC_COMP_BIN}"
fi