| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- pub enum Backend {
- Auto,
- Wayland,
- X11,
- }
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct BackendDecision {
- pub backend: Backend,
- pub note: String,
- }
- #[derive(Debug, Clone, Default)]
- pub struct SessionEnv {
- pub wayland_display: bool,
- pub xdg_session_type: Option<String>,
- pub display: bool,
- pub xdg_current_desktop: Option<String>,
- pub xdg_session_desktop: Option<String>,
- pub desktop_session: Option<String>,
- pub oc_allow_wayland: Option<String>,
- pub oc_force_x11: Option<String>,
- pub oc_force_wayland: Option<String>,
- pub oc_linux_decorations: Option<String>,
- pub oc_force_decorations: Option<String>,
- pub oc_no_decorations: Option<String>,
- pub i3_sock: bool,
- }
- impl SessionEnv {
- pub fn capture() -> Self {
- Self {
- wayland_display: std::env::var_os("WAYLAND_DISPLAY").is_some(),
- xdg_session_type: std::env::var("XDG_SESSION_TYPE").ok(),
- display: std::env::var_os("DISPLAY").is_some(),
- xdg_current_desktop: std::env::var("XDG_CURRENT_DESKTOP").ok(),
- xdg_session_desktop: std::env::var("XDG_SESSION_DESKTOP").ok(),
- desktop_session: std::env::var("DESKTOP_SESSION").ok(),
- oc_allow_wayland: std::env::var("OC_ALLOW_WAYLAND").ok(),
- oc_force_x11: std::env::var("OC_FORCE_X11").ok(),
- oc_force_wayland: std::env::var("OC_FORCE_WAYLAND").ok(),
- oc_linux_decorations: std::env::var("OC_LINUX_DECORATIONS").ok(),
- oc_force_decorations: std::env::var("OC_FORCE_DECORATIONS").ok(),
- oc_no_decorations: std::env::var("OC_NO_DECORATIONS").ok(),
- i3_sock: std::env::var_os("I3SOCK").is_some(),
- }
- }
- }
- pub fn select_backend(env: &SessionEnv, prefer_wayland: bool) -> Option<BackendDecision> {
- if is_truthy(env.oc_force_x11.as_deref()) {
- return Some(BackendDecision {
- backend: Backend::X11,
- note: "Forcing X11 due to OC_FORCE_X11=1".into(),
- });
- }
- if is_truthy(env.oc_force_wayland.as_deref()) {
- return Some(BackendDecision {
- backend: Backend::Wayland,
- note: "Forcing native Wayland due to OC_FORCE_WAYLAND=1".into(),
- });
- }
- if !is_wayland_session(env) {
- return None;
- }
- if prefer_wayland {
- return Some(BackendDecision {
- backend: Backend::Wayland,
- note: "Wayland session detected; forcing native Wayland from settings".into(),
- });
- }
- if is_truthy(env.oc_allow_wayland.as_deref()) {
- return Some(BackendDecision {
- backend: Backend::Wayland,
- note: "Wayland session detected; forcing native Wayland due to OC_ALLOW_WAYLAND=1"
- .into(),
- });
- }
- Some(BackendDecision {
- backend: Backend::Auto,
- note: "Wayland session detected; using native Wayland first with X11 fallback (auto backend). Set OC_FORCE_X11=1 to force X11."
- .into(),
- })
- }
- pub fn use_decorations(env: &SessionEnv) -> bool {
- if let Some(mode) = decoration_override(env.oc_linux_decorations.as_deref()) {
- return match mode {
- DecorationOverride::Native => true,
- DecorationOverride::None => false,
- DecorationOverride::Auto => default_use_decorations(env),
- };
- }
- if is_truthy(env.oc_force_decorations.as_deref()) {
- return true;
- }
- if is_truthy(env.oc_no_decorations.as_deref()) {
- return false;
- }
- default_use_decorations(env)
- }
- fn default_use_decorations(env: &SessionEnv) -> bool {
- if is_known_tiling_session(env) {
- return false;
- }
- if !is_wayland_session(env) {
- return true;
- }
- is_full_desktop_session(env)
- }
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- enum DecorationOverride {
- Auto,
- Native,
- None,
- }
- fn decoration_override(value: Option<&str>) -> Option<DecorationOverride> {
- let value = value?.trim().to_ascii_lowercase();
- if matches!(value.as_str(), "auto") {
- return Some(DecorationOverride::Auto);
- }
- if matches!(
- value.as_str(),
- "native" | "server" | "de" | "wayland" | "on" | "true" | "1"
- ) {
- return Some(DecorationOverride::Native);
- }
- if matches!(
- value.as_str(),
- "none" | "off" | "false" | "0" | "client" | "csd"
- ) {
- return Some(DecorationOverride::None);
- }
- None
- }
- fn is_truthy(value: Option<&str>) -> bool {
- matches!(
- value.map(|v| v.trim().to_ascii_lowercase()),
- Some(v) if matches!(v.as_str(), "1" | "true" | "yes" | "on")
- )
- }
- fn is_wayland_session(env: &SessionEnv) -> bool {
- env.wayland_display
- || matches!(
- env.xdg_session_type.as_deref(),
- Some(value) if value.eq_ignore_ascii_case("wayland")
- )
- }
- fn is_full_desktop_session(env: &SessionEnv) -> bool {
- desktop_tokens(env).any(|value| {
- matches!(
- value.as_str(),
- "gnome"
- | "kde"
- | "plasma"
- | "xfce"
- | "xfce4"
- | "x-cinnamon"
- | "cinnamon"
- | "mate"
- | "lxqt"
- | "budgie"
- | "pantheon"
- | "deepin"
- | "unity"
- | "cosmic"
- )
- })
- }
- fn is_known_tiling_session(env: &SessionEnv) -> bool {
- if env.i3_sock {
- return true;
- }
- desktop_tokens(env).any(|value| {
- matches!(
- value.as_str(),
- "niri"
- | "sway"
- | "swayfx"
- | "hyprland"
- | "river"
- | "i3"
- | "i3wm"
- | "bspwm"
- | "dwm"
- | "qtile"
- | "xmonad"
- | "leftwm"
- | "dwl"
- | "awesome"
- | "herbstluftwm"
- | "spectrwm"
- | "worm"
- | "i3-gnome"
- )
- })
- }
- fn desktop_tokens<'a>(env: &'a SessionEnv) -> impl Iterator<Item = String> + 'a {
- [
- env.xdg_current_desktop.as_deref(),
- env.xdg_session_desktop.as_deref(),
- env.desktop_session.as_deref(),
- ]
- .into_iter()
- .flatten()
- .flat_map(|desktop| desktop.split(':'))
- .map(|value| value.trim().to_ascii_lowercase())
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn prefers_wayland_first_on_wayland_session() {
- let env = SessionEnv {
- wayland_display: true,
- display: true,
- ..Default::default()
- };
- let decision = select_backend(&env, false).expect("missing decision");
- assert_eq!(decision.backend, Backend::Auto);
- }
- #[test]
- fn force_x11_override_wins() {
- let env = SessionEnv {
- wayland_display: true,
- display: true,
- oc_force_x11: Some("1".into()),
- oc_allow_wayland: Some("1".into()),
- oc_force_wayland: Some("1".into()),
- ..Default::default()
- };
- let decision = select_backend(&env, true).expect("missing decision");
- assert_eq!(decision.backend, Backend::X11);
- }
- #[test]
- fn prefer_wayland_forces_wayland_backend() {
- let env = SessionEnv {
- wayland_display: true,
- display: true,
- ..Default::default()
- };
- let decision = select_backend(&env, true).expect("missing decision");
- assert_eq!(decision.backend, Backend::Wayland);
- }
- #[test]
- fn force_wayland_override_works_outside_wayland_session() {
- let env = SessionEnv {
- display: true,
- oc_force_wayland: Some("1".into()),
- ..Default::default()
- };
- let decision = select_backend(&env, false).expect("missing decision");
- assert_eq!(decision.backend, Backend::Wayland);
- }
- #[test]
- fn allow_wayland_forces_wayland_backend() {
- let env = SessionEnv {
- wayland_display: true,
- display: true,
- oc_allow_wayland: Some("1".into()),
- ..Default::default()
- };
- let decision = select_backend(&env, false).expect("missing decision");
- assert_eq!(decision.backend, Backend::Wayland);
- }
- #[test]
- fn xdg_session_type_wayland_is_detected() {
- let env = SessionEnv {
- xdg_session_type: Some("wayland".into()),
- ..Default::default()
- };
- let decision = select_backend(&env, false).expect("missing decision");
- assert_eq!(decision.backend, Backend::Auto);
- }
- #[test]
- fn returns_none_when_not_wayland_and_no_overrides() {
- let env = SessionEnv {
- display: true,
- xdg_current_desktop: Some("GNOME".into()),
- ..Default::default()
- };
- assert!(select_backend(&env, false).is_none());
- }
- #[test]
- fn prefer_wayland_setting_does_not_override_x11_session() {
- let env = SessionEnv {
- display: true,
- xdg_current_desktop: Some("GNOME".into()),
- ..Default::default()
- };
- assert!(select_backend(&env, true).is_none());
- }
- #[test]
- fn disables_decorations_on_niri() {
- let env = SessionEnv {
- xdg_current_desktop: Some("niri".into()),
- wayland_display: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn keeps_decorations_on_gnome() {
- let env = SessionEnv {
- xdg_current_desktop: Some("GNOME".into()),
- wayland_display: true,
- ..Default::default()
- };
- assert!(use_decorations(&env));
- }
- #[test]
- fn disables_decorations_when_session_desktop_is_tiling() {
- let env = SessionEnv {
- xdg_session_desktop: Some("Hyprland".into()),
- wayland_display: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn disables_decorations_for_unknown_wayland_session() {
- let env = SessionEnv {
- xdg_current_desktop: Some("labwc".into()),
- wayland_display: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn disables_decorations_for_dwm_on_x11() {
- let env = SessionEnv {
- xdg_current_desktop: Some("dwm".into()),
- display: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn disables_decorations_for_i3_on_x11() {
- let env = SessionEnv {
- xdg_current_desktop: Some("i3".into()),
- display: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn disables_decorations_for_i3sock_without_xdg_tokens() {
- let env = SessionEnv {
- display: true,
- i3_sock: true,
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn keeps_decorations_for_gnome_on_x11() {
- let env = SessionEnv {
- xdg_current_desktop: Some("GNOME".into()),
- display: true,
- ..Default::default()
- };
- assert!(use_decorations(&env));
- }
- #[test]
- fn no_decorations_override_wins() {
- let env = SessionEnv {
- xdg_current_desktop: Some("GNOME".into()),
- oc_no_decorations: Some("1".into()),
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn linux_decorations_native_override_wins() {
- let env = SessionEnv {
- xdg_current_desktop: Some("niri".into()),
- wayland_display: true,
- oc_linux_decorations: Some("native".into()),
- ..Default::default()
- };
- assert!(use_decorations(&env));
- }
- #[test]
- fn linux_decorations_none_override_wins() {
- let env = SessionEnv {
- xdg_current_desktop: Some("GNOME".into()),
- wayland_display: true,
- oc_linux_decorations: Some("none".into()),
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn linux_decorations_auto_uses_default_policy() {
- let env = SessionEnv {
- xdg_current_desktop: Some("sway".into()),
- wayland_display: true,
- oc_linux_decorations: Some("auto".into()),
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- #[test]
- fn linux_decorations_override_beats_legacy_overrides() {
- let env = SessionEnv {
- xdg_current_desktop: Some("GNOME".into()),
- wayland_display: true,
- oc_linux_decorations: Some("none".into()),
- oc_force_decorations: Some("1".into()),
- ..Default::default()
- };
- assert!(!use_decorations(&env));
- }
- }
|