#!/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, under their own names. HyprCosmic installs beside COSMIC rather # than over it: these are this fork's builds, and /usr/bin/cosmic-session and # /usr/bin/cosmic-comp are left to the distribution's packages. # # That is what keeps the stock session on the greeter's menu and working. The # alternative -- taking the cosmic-* names -- means erasing the distribution's # compositor and session to install this, which on Fedora also means erasing # cosmic-greeter, which is the display manager. A fork that can only be tried by # removing the desktop it forked is a fork nobody can back out of. # # cosmic-session takes the compositor as argv[1], so the pairing is arranged # here and needs no patch: see the exec at the end of this file. # # 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/hyprcosmic-session}" HYPRCOSMIC_COMP_BIN="${HYPRCOSMIC_COMP_BIN:-/usr/bin/hyprcosmic-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 # Seed the per-user configuration from the skeleton the package ships. # # waybar, the wallpaper and every keybinding come from # ~/.config/hyprcosmic/autostart. A machine that has never run HyprCosmic has no # such file, and what starts is a bare compositor: running, holding the display, # accepting input, with nothing drawn on the screen and no binding that opens # anything. Nothing reports an error, because as far as the session is concerned # nothing has gone wrong -- which is what makes it worth handling here rather # than in a note in the README. # # Only missing files are copied, and an existing one is never touched, not even # when the skeleton is newer. The file wins, one way; a login is not an # invitation to edit somebody's config. Two things follow from that: this is # safe to run on every session start, and deleting a file is how you ask for the # default back. # # Guarded throughout, like the log above. A session that cannot seed should # still start -- the user is no worse off than before this ran. HYPRCOSMIC_SKEL="${HYPRCOSMIC_SKEL:-/usr/share/hyprcosmic/skel}" HYPRCOSMIC_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" if [[ -d "$HYPRCOSMIC_SKEL" ]]; then # Paths under the skeleton mirror the layout under ~/.config exactly, so # the destination is the relative path and nothing here needs a list of # which file goes where. while IFS= read -r -d '' src; do rel="${src#"$HYPRCOSMIC_SKEL"/}" dest="$HYPRCOSMIC_CONFIG_HOME/$rel" if [[ -e "$dest" ]]; then continue fi # Every branch is an if rather than a && chain: set -e is on, and a # trailing false would take the whole session down with it. if mkdir -p "$(dirname "$dest")" 2>/dev/null && cp "$src" "$dest" 2>/dev/null; then echo " seeded: $dest" else echo " seed failed: $dest" fi done < <(find "$HYPRCOSMIC_SKEL" -type f -print0 2>/dev/null | sort -z) else echo " no skeleton at $HYPRCOSMIC_SKEL; ~/.config is left as it is" 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