From 44fbc7a4d40f9553819a48abd4ac37f82e51a86c Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 20 Oct 2026 10:50:11 -0400 Subject: [PATCH] new WM setup --- clients/quadraticpc/fht-compositor.nix | 112 ---------- .../quadraticpc/fht-compositor/default.nix | 164 +++++++++++++++ .../lone-modifier-and-scroll.patch | 187 +++++++++++++++++ .../fht-compositor/portal-version.patch | 13 ++ .../fht-compositor/resize-tiled-windows.patch | 191 ++++++++++++++++++ flake.lock | 27 ++- flake.nix | 4 + wrappers/desktop/gaypanel/default.nix | 89 ++++++++ .../desktop/gaypanel/no-sound-panic.patch | 104 ++++++++++ wrappers/desktop/walker.nix | 8 + 10 files changed, 784 insertions(+), 115 deletions(-) delete mode 100644 clients/quadraticpc/fht-compositor.nix create mode 100644 clients/quadraticpc/fht-compositor/default.nix create mode 100644 clients/quadraticpc/fht-compositor/lone-modifier-and-scroll.patch create mode 100644 clients/quadraticpc/fht-compositor/portal-version.patch create mode 100644 clients/quadraticpc/fht-compositor/resize-tiled-windows.patch create mode 100644 wrappers/desktop/gaypanel/default.nix create mode 100644 wrappers/desktop/gaypanel/no-sound-panic.patch create mode 100644 wrappers/desktop/walker.nix diff --git a/clients/quadraticpc/fht-compositor.nix b/clients/quadraticpc/fht-compositor.nix deleted file mode 100644 index 53eab9d..0000000 --- a/clients/quadraticpc/fht-compositor.nix +++ /dev/null @@ -1,112 +0,0 @@ -{ inputs, pkgs, ... }: -let - filename = "fht-compositor.toml"; -in -{ - imports = [ inputs.fht-compositor.nixosModules.default ]; - environment = { - systemPackages = with pkgs; [ - grim - slurp - ]; - - etc.${filename}.source = (pkgs.formats.toml { }).generate "config.toml" { - general = { - focus-follows-mouse = true; - inner-gaps = 10; - }; - - decorations = { - border = { - thickness = 4; - radius = 12; - }; - blur.disable = true; - }; - - keybinds = { - Alt-Tab = "focus-next-window"; - Super-f = "fullscreen-focused-window"; - Super-Ctrl-q = "quit"; - Super-Ctrl-r = "reload-config"; - - Print = { - action = "run-command"; - arg = "grim -g \"$(slurp)\" - | gradia"; - }; - - Super-t = { - action = "run-command"; - arg = "ptyxis --new-window"; - }; - Super-e = { - action = "run-command"; - arg = "nautilus"; - }; - - Super-Down = { - action = "change-window-proportion"; - arg = 0.5; - }; - Super-Up = { - action = "change-window-proportion"; - arg = -0.5; - }; - Super-Left = { - action = "change-mwfact"; - arg = -0.1; - }; - Super-Right = { - action = "change-mwfact"; - arg = 0.1; - }; - } - // builtins.listToAttrs ( - builtins.genList (i: { - name = "Super-${toString (i + 1)}"; - value = { - action = "focus-workspace"; - arg = i; - }; - }) 9 - ); - - mousebinds = { - Super-Left = "swap-tile"; - Super-wheeldown = "focus-previous-workspace"; - Super-wheelup = "focus-next-workspace"; - }; - - rules = [ - { - match-app-id = [ ".*" ]; - floating = false; - } - ]; - - outputs = { - HDMI-A-1.position.x = 0; - DP-3.position.x = 1920; - }; - - cursor = { - name = "GoogleDot-Blue"; - size = 24; - }; - }; - }; - - programs.fht-compositor = { - enable = true; - package = - inputs.fht-compositor.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs - (old: { - nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ]; - - postInstall = (old.postInstall or "") + '' - wrapProgram $out/bin/fht-compositor \ - --add-flags "-c /etc/${filename}" - ''; - }); - }; -} diff --git a/clients/quadraticpc/fht-compositor/default.nix b/clients/quadraticpc/fht-compositor/default.nix new file mode 100644 index 0000000..80c8124 --- /dev/null +++ b/clients/quadraticpc/fht-compositor/default.nix @@ -0,0 +1,164 @@ +{ + inputs, + pkgs, + lib, + ... +}: +let + filename = "fht-compositor.toml"; + + session-programs = lib.getExe ( + pkgs.writeShellApplication { + name = "session-programs"; + runtimeInputs = with pkgs; [ + procps + util-linux + ]; + text = '' + restart() { + pattern=$1 + shift + pkill -f "$pattern" || true + for _ in $(seq 50); do + pgrep -f "$pattern" >/dev/null || break + sleep 0.1 + done + setsid "$@" >/dev/null 2>&1 > = const { Cell::new(None) }; ++ /// Accumulated high-resolution scroll (in 1/120ths of a wheel notch) for scroll bindings, ++ /// so that they trigger once per notch rather than once per event. ++ static SCROLL_ACCUMULATOR: Cell<(f64, f64)> = const { Cell::new((0.0, 0.0)) }; ++} ++ + impl State { + fn handle_focus_follows_mouse(&mut self, under: Option<&PointerFocus>) { + if !self.fht.config.general.focus_follows_mouse { +@@ -244,6 +254,7 @@ + }) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false); ++ let mut lone_modifier_action = None; + let action = keyboard.input( + self, + keycode, +@@ -272,6 +283,20 @@ + return FilterResult::Forward; + } + ++ // Lone modifier taps. The key release is still forwarded to clients, and the ++ // action is run after the input is processed. ++ if let Some(raw) = raw { ++ if key_state == KeyState::Pressed { ++ LONE_MODIFIER.set(raw.is_modifier_key().then_some(raw)); ++ } else if LONE_MODIFIER.take() == Some(raw) { ++ lone_modifier_action = find_keyaction( ++ &state.fht.config.keybinds, ++ &ModifiersState::default(), ++ raw, ++ ); ++ } ++ } ++ + // Handle repeating keybinds + // FIXME: handle this properly, this breaks if there are two repeating keybinds + if key_state == KeyState::Released { +@@ -331,6 +356,9 @@ + if let Some((key_pattern, key_action)) = action { + self.process_key_action(key_action, key_pattern); + } ++ if let Some((key_pattern, key_action)) = lone_modifier_action { ++ self.process_key_action(key_action, key_pattern); ++ } + } + + fn on_pointer_motion(&mut self, event: B::PointerMotionEvent) { +@@ -505,6 +533,11 @@ + let state = wl_pointer::ButtonState::from(event.state()); + let pointer = self.fht.pointer.clone(); + ++ if state == wl_pointer::ButtonState::Pressed { ++ // e.g. Super+click shouldn't count as a lone Super tap ++ LONE_MODIFIER.set(None); ++ } ++ + if state == wl_pointer::ButtonState::Pressed && !pointer.is_grabbed() { + let pointer_loc = pointer.current_location(); + let focus = self.fht.get_pointer_focus(pointer_loc); +@@ -567,62 +600,67 @@ + // Check for mouse axis bindings FIRST + let modifiers = self.fht.keyboard.modifier_state().into(); + let mut handled = false; ++ // e.g. Super+scroll shouldn't count as a lone Super tap ++ LONE_MODIFIER.set(None); ++ ++ // High-resolution wheels send several events per notch, each with part of the 120 units ++ // of a notch, so accumulate them and only trigger the binding once per full notch. ++ let (mut horizontal_accumulated, mut vertical_accumulated) = SCROLL_ACCUMULATOR.get(); ++ let accumulate = |accumulated: &mut f64, discrete: f64| { ++ if accumulated.signum() != discrete.signum() { ++ // Changed direction, start over ++ *accumulated = 0.0; ++ } ++ *accumulated += discrete; ++ if accumulated.abs() >= 120.0 { ++ *accumulated -= 120.0 * accumulated.signum(); ++ true ++ } else { ++ false ++ } ++ }; + + // Check vertical axis bindings using discrete amounts +- if let Some(discrete) = vertical_amount_discrete { +- if discrete > 0.0 { +- let mouse_pattern = fht_compositor_config::MousePattern( +- modifiers, +- fht_compositor_config::MouseInput::Axis( +- fht_compositor_config::MouseAxis::WheelUp, +- ), +- ); +- if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { +- self.process_mouse_action(0, action, SERIAL_COUNTER.next_serial()); +- handled = true; +- } +- } else if discrete < 0.0 { +- let mouse_pattern = fht_compositor_config::MousePattern( +- modifiers, +- fht_compositor_config::MouseInput::Axis( +- fht_compositor_config::MouseAxis::WheelDown, +- ), +- ); +- if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { ++ if let Some(discrete) = vertical_amount_discrete.filter(|discrete| *discrete != 0.0) { ++ let mouse_axis = if discrete > 0.0 { ++ fht_compositor_config::MouseAxis::WheelUp ++ } else { ++ fht_compositor_config::MouseAxis::WheelDown ++ }; ++ let mouse_pattern = fht_compositor_config::MousePattern( ++ modifiers, ++ fht_compositor_config::MouseInput::Axis(mouse_axis), ++ ); ++ if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { ++ handled = true; ++ if accumulate(&mut vertical_accumulated, discrete) { + self.process_mouse_action(0, action, SERIAL_COUNTER.next_serial()); +- handled = true; + } + } + } + + // Check horizontal axis bindings using discrete amounts + if !handled { +- if let Some(discrete) = horizontal_amount_discrete { +- if discrete > 0.0 { +- let mouse_pattern = fht_compositor_config::MousePattern( +- modifiers, +- fht_compositor_config::MouseInput::Axis( +- fht_compositor_config::MouseAxis::WheelRight, +- ), +- ); +- if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { +- self.process_mouse_action(0, action, SERIAL_COUNTER.next_serial()); +- handled = true; +- } +- } else if discrete < 0.0 { +- let mouse_pattern = fht_compositor_config::MousePattern( +- modifiers, +- fht_compositor_config::MouseInput::Axis( +- fht_compositor_config::MouseAxis::WheelLeft, +- ), +- ); +- if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { ++ if let Some(discrete) = horizontal_amount_discrete.filter(|discrete| *discrete != 0.0) ++ { ++ let mouse_axis = if discrete > 0.0 { ++ fht_compositor_config::MouseAxis::WheelRight ++ } else { ++ fht_compositor_config::MouseAxis::WheelLeft ++ }; ++ let mouse_pattern = fht_compositor_config::MousePattern( ++ modifiers, ++ fht_compositor_config::MouseInput::Axis(mouse_axis), ++ ); ++ if let Some(action) = self.fht.config.mousebinds.get(&mouse_pattern).cloned() { ++ handled = true; ++ if accumulate(&mut horizontal_accumulated, discrete) { + self.process_mouse_action(0, action, SERIAL_COUNTER.next_serial()); +- handled = true; + } + } + } + } ++ SCROLL_ACCUMULATOR.set((horizontal_accumulated, vertical_accumulated)); + + // Only forward to pointer if we didn't handle it with a binding + if !handled { diff --git a/clients/quadraticpc/fht-compositor/portal-version.patch b/clients/quadraticpc/fht-compositor/portal-version.patch new file mode 100644 index 0000000..2ee6430 --- /dev/null +++ b/clients/quadraticpc/fht-compositor/portal-version.patch @@ -0,0 +1,13 @@ +--- a/src/portals/screencast.rs ++++ b/src/portals/screencast.rs +@@ -165,7 +165,9 @@ + (CursorMode::HIDDEN | CursorMode::EMBEDDED).bits() + } + +- #[zbus(property)] ++ // The spec calls this "version" (zbus would otherwise publish it as "Version"), and ++ // xdg-desktop-portal treats the portal as version 0 without it, rejecting cursor modes. ++ #[zbus(property, name = "version")] + pub fn version(&self) -> u32 { + PORTAL_VERSION + } diff --git a/clients/quadraticpc/fht-compositor/resize-tiled-windows.patch b/clients/quadraticpc/fht-compositor/resize-tiled-windows.patch new file mode 100644 index 0000000..50c8e84 --- /dev/null +++ b/clients/quadraticpc/fht-compositor/resize-tiled-windows.patch @@ -0,0 +1,191 @@ +--- a/src/space/workspace.rs ++++ b/src/space/workspace.rs +@@ -59,6 +59,11 @@ + window: Window, + initial_window_geometry: Rectangle, + edges: ResizeEdge, ++ /// The layout values when the resize started, used when resizing a tiled window. ++ initial_mwfact: f64, ++ initial_proportion: f64, ++ /// The sum of the proportions of the other tiles in the window's stack. ++ initial_peers_proportion: f64, + } + + #[derive(Debug)] +@@ -2009,23 +2014,70 @@ + return false; + }; + +- match (window.tiled(), self.current_layout()) { +- (_, WorkspaceLayout::Floating) | (false, _) => (), +- // We only do interactive resizes on floating windows +- (true, _) => return false, +- } +- ++ // Tiled windows are resized by changing the layout (see handle_interactive_resize_motion) + let loc = tile.visual_location(); + let size = window.size(); ++ let initial_proportion = tile.proportion(); ++ let initial_peers_proportion = self ++ .stack_peers(window) ++ .into_iter() ++ .map(|idx| self.tiles[idx].proportion()) ++ .sum(); + self.interactive_resize = Some(InteractiveResize { + window: window.clone(), + initial_window_geometry: Rectangle::new(loc, size), + edges, ++ initial_mwfact: self.mwfact, ++ initial_proportion, ++ initial_peers_proportion, + }); + + true + } + ++ /// Get the indices of the other tiles that share a stack with this window in the current layout, ++ /// IE. that split the stack's length with it according to their proportions. ++ fn stack_peers(&self, window: &Window) -> Vec { ++ // Matches how arrange_tiles orders and splits tiles. ++ let tiled: Vec = (0..self.tiles.len()) ++ .filter(|&idx| { ++ let window = self.tiles[idx].window(); ++ window.tiled() && !window.maximized() ++ }) ++ .collect(); ++ let Some(position) = tiled ++ .iter() ++ .position(|&idx| self.tiles[idx].window() == window) ++ else { ++ return vec![]; ++ }; ++ ++ let nmaster = self.nmaster.min(tiled.len()); ++ let layout = self.current_layout(); ++ let same_stack = |other: usize| match layout { ++ WorkspaceLayout::Tile | WorkspaceLayout::BottomStack => { ++ (position < nmaster) == (other < nmaster) ++ } ++ // The non-master tiles alternate between the left and right stacks ++ WorkspaceLayout::CenteredMaster => { ++ if position < nmaster { ++ other < nmaster ++ } else { ++ other >= nmaster && (other - nmaster) % 2 == (position - nmaster) % 2 ++ } ++ } ++ // The other layouts don't use proportions ++ _ => false, ++ }; ++ ++ tiled ++ .into_iter() ++ .enumerate() ++ .filter(|&(other, _)| other != position && same_stack(other)) ++ .map(|(_, idx)| idx) ++ .collect() ++ } ++ + /// Handle an interactive resize grab motion. + /// + /// `delta` is how much the cursor moved from its initial window location. +@@ -2052,11 +2104,53 @@ + return None; + } + +- match (window.tiled(), self.current_layout()) { +- (_, WorkspaceLayout::Floating) | (false, _) => (), +- // We switched from floating to tiled between the motion events +- // Can happen if the user uses a key action bound to toggle-window-floating +- (true, _) => return None, ++ let layout = self.current_layout(); ++ if window.tiled() && layout != WorkspaceLayout::Floating { ++ // For tiled windows, dragging along the master/stack split moves the split (mwfact) ++ // so it follows the cursor, and dragging across it grows/shrinks the window in its ++ // stack (proportion). Which axis is which depends on the layout. ++ let initial_size = interactive_resize.initial_window_geometry.size; ++ let work_area_size = self.work_area.size; ++ let (split_delta, split_length, stack_delta, stack_length, grow_edge) = match layout { ++ WorkspaceLayout::BottomStack => { ++ (delta.y, work_area_size.h, delta.x, initial_size.w, ResizeEdge::LEFT) ++ } ++ _ => (delta.x, work_area_size.w, delta.y, initial_size.h, ResizeEdge::TOP), ++ }; ++ // Grabbing the top (or left) part of the window grows it in that direction instead ++ let stack_delta = if interactive_resize.edges.intersects(grow_edge) { ++ -stack_delta ++ } else { ++ stack_delta ++ }; ++ ++ let mwfact = ++ interactive_resize.initial_mwfact + f64::from(split_delta) / f64::from(split_length.max(1)); ++ ++ // Solve for the proportion that gives the window exactly the length it was dragged to. ++ // The other tiles in the stack keep their proportions (`peers`) during the drag, and ++ // the window's length is `total * p / (p + peers)`, so `p = len * peers / (total - len)` ++ let initial_proportion = interactive_resize.initial_proportion; ++ let peers = interactive_resize.initial_peers_proportion; ++ // Nothing to resize against if the window is alone in its stack ++ if peers > 0.0 && initial_proportion > 0.0 && stack_length > 0 { ++ let initial_length = f64::from(stack_length); ++ let total_length = initial_length * (initial_proportion + peers) / initial_proportion; ++ // Always leave some room for the window and the other tiles in the stack ++ let min_length = (total_length * 0.05).max(1.0); ++ let peer_count = self.stack_peers(window).len() as f64; ++ let max_length = (total_length - min_length * peer_count.max(1.0)).max(min_length); ++ let length = (initial_length + f64::from(stack_delta)).clamp(min_length, max_length); ++ let proportion = length * peers / (total_length - length); ++ ++ if let Some(tile) = self.tiles.iter_mut().find(|tile| tile.window() == window) { ++ tile.set_proportion(proportion); ++ } ++ } ++ // This also re-arranges the tiles ++ self.set_mwfact(mwfact, false); ++ ++ return Some(true); + } + + let mut new_size = interactive_resize.initial_window_geometry.size; +@@ -2140,6 +2234,24 @@ + return false; + } + ++ // Rescale the proportions in the window's stack so they average out to 1. This keeps the ++ // same layout, but stops the values from drifting to extremes over repeated resizes, ++ // where fixed steps (like change-window-proportion) would barely do anything anymore. ++ if window.tiled() && self.current_layout() != WorkspaceLayout::Floating { ++ let mut stack = self.stack_peers(window); ++ if let Some(idx) = self.tiles.iter().position(|tile| tile.window() == window) { ++ stack.push(idx); ++ } ++ let total: f64 = stack.iter().map(|&idx| self.tiles[idx].proportion()).sum(); ++ if stack.len() > 1 && total > 0.0 { ++ let factor = stack.len() as f64 / total; ++ for idx in stack { ++ let proportion = self.tiles[idx].proportion(); ++ self.tiles[idx].set_proportion(proportion * factor); ++ } ++ } ++ } ++ + true + } + +--- a/src/input/actions.rs ++++ b/src/input/actions.rs +@@ -605,12 +605,8 @@ + } + MouseAction::ResizeTile => { + if let Some(window) = focus.and_then(|focus| focus.window) { ++ // Tiled windows are resized too, by changing the workspace layout + let workspace = self.fht.space.workspace_for_window(&window).unwrap(); +- match (window.tiled(), workspace.current_layout()) { +- (_, WorkspaceLayout::Floating) | (false, _) => (), +- // We only do interactive resizes on floating windows +- (true, _) => return, +- } + + let pointer_loc = self.fht.pointer.current_location(); + let loc = self.fht.space.window_location(&window).unwrap().to_f64(); diff --git a/flake.lock b/flake.lock index 7601d55..3e27acf 100644 --- a/flake.lock +++ b/flake.lock @@ -306,11 +306,11 @@ "rust-overlay": [] }, "locked": { - "lastModified": 1791704129, - "narHash": "sha256-rSK4/dNK6AOEsQ4EozZgv07fTyi4q4RF9KoXIAlwcz0=", + "lastModified": 1792487869, + "narHash": "sha256-4e5XvmR5lvSr/tSbSM1byipvKpblM5SJjSJWNgGAiSo=", "owner": "nferhat", "repo": "fht-compositor", - "rev": "fc0243b7468d6ee16171c2f0ba19e1b6ddddd1a0", + "rev": "a39a9a6e4f13307d034c5976ca5514beda7b21ac", "type": "github" }, "original": { @@ -656,6 +656,26 @@ "type": "github" } }, + "gaypanel": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1791581716, + "narHash": "sha256-XlLNC+8X7arvQHc0n3cJQfm11THhV8zCH8QofcQDxEE=", + "ref": "refs/heads/main", + "rev": "acea989ace01a2408d51a27e030ac15df148d08c", + "revCount": 298, + "type": "git", + "url": "https://codeberg.org/pastthepixels/gaypanel" + }, + "original": { + "type": "git", + "url": "https://codeberg.org/pastthepixels/gaypanel" + } + }, "git-hooks": { "inputs": { "flake-compat": "flake-compat_3", @@ -1089,6 +1109,7 @@ "fht-compositor": "fht-compositor", "firefox-gnome-theme": "firefox-gnome-theme", "flake-parts": "flake-parts_5", + "gaypanel": "gaypanel", "home-manager": "home-manager", "lasuite-docs-proxy": "lasuite-docs-proxy", "lk-jwt-service-module": "lk-jwt-service-module", diff --git a/flake.nix b/flake.nix index 35f2ac2..ad9c2e8 100755 --- a/flake.nix +++ b/flake.nix @@ -71,6 +71,10 @@ rust-overlay.follows = ""; }; }; + gaypanel = { + url = "git+https://codeberg.org/pastthepixels/gaypanel"; + inputs.nixpkgs.follows = "nixpkgs"; + }; firefox-gnome-theme = { url = "github:rafaelmardojai/firefox-gnome-theme"; flake = false; diff --git a/wrappers/desktop/gaypanel/default.nix b/wrappers/desktop/gaypanel/default.nix new file mode 100644 index 0000000..e8b3e7c --- /dev/null +++ b/wrappers/desktop/gaypanel/default.nix @@ -0,0 +1,89 @@ +{ + inputs, + pkgs, + ... +}: +let + bar = pkgs.writeText "top.blp" '' + using Gtk 4.0; + using Adw 1; + + $GayPanel panel { + anchor-direction: top; + monitor: "all"; + + CenterBox { + margin-start: 4; + margin-end: 4; + margin-top: 4; + margin-bottom: 4; + + [start] + $WorkspacesWidget { + show-labels: false; + } + + [center] + $ClockWidget { + styles [ + "pill", + "flat", + ] + + format: "%b %d %H:%M"; + } + + [end] + $SystemControlsWidget { + styles [ + "pill", + "flat", + ] + } + } + } + ''; + + # GNOME-style workspace dots, with the active one stretched into a pill + style = pkgs.writeText "style.css" '' + .workspaces button { + min-width: 8px; + min-height: 8px; + padding: 0; + border-radius: 99px; + background: alpha(var(--window-fg-color), 0.4); + transition: min-width 0.2s ease, background 0.2s ease; + } + + .workspaces button.selected { + min-width: 32px; + padding: 0; + background: var(--window-fg-color); + } + + .workspaces button label { + font-size: 0; + } + ''; +in +{ + wrappers.gaypanel = { + basePackage = + inputs.gaypanel.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs + (old: { + patches = (old.patches or [ ]) ++ [ + ./no-sound-panic.patch + ]; + }); + + prependFlags = [ + "--config" + # Copied rather than linked, since gaypanel ignores symlinks in bars/ + "${pkgs.runCommand "gaypanel-config" { } '' + mkdir -p $out/bars + cp ${bar} $out/bars/top.blp + cp ${style} $out/style.css + ''}" + ]; + }; +} diff --git a/wrappers/desktop/gaypanel/no-sound-panic.patch b/wrappers/desktop/gaypanel/no-sound-panic.patch new file mode 100644 index 0000000..8092b74 --- /dev/null +++ b/wrappers/desktop/gaypanel/no-sound-panic.patch @@ -0,0 +1,104 @@ +--- a/src/system/sound.rs ++++ b/src/system/sound.rs +@@ -6,25 +6,21 @@ + + use crate::{senders, system::SystemModule}; + +-/// Gets the default mixer. Throws an error if it doesn't exist (this means you probably don't have ALSA installed) ++/// Gets the default mixer. Returns None if it doesn't exist (yet), e.g. if the sound server hasn't started + macro_rules! mixer { + () => { +- Mixer::new("default", false).expect("[fatal] alsa: Could not find a mixer named \"default\". gaypanel assumes this is the name of the default mixer. \n[fatal] alsa: **If you have ALSA installed and running and you're *sure* of it, open an issue at codeberg.org/pastthepixels/gaypanel!**") ++ Mixer::new("default", false).ok() + }; + } + + /// Finds the first selem with a playback volume in a mixer. Assumes this is the "default" + /// e.g. this reflects the actual output volume. I don't know how alsa works. +-/// You must have a speaker of some kind or else this'll crap out ++/// Returns None if there's no speaker of some kind (yet) + macro_rules! sink { + ($m: expr) => { + $m.iter() + .flat_map(|e| Selem::new(e)) + .find(|s| s.has_playback_volume()) +- .expect(&format!( +- "[fatal] alsa: Could not find default sink for mixer {:?}", +- $m +- )) + }; + } + +@@ -54,9 +50,9 @@ + senders!(VolumeModule); + + fn get() -> Option { +- let mixer = mixer!(); ++ let mixer = mixer!()?; + // Sink stats +- let sink = sink!(mixer); ++ let sink = sink!(mixer)?; + let vol = { + let max = sink.get_playback_volume_range().1 as f64; + let raw_vol = sink +@@ -101,8 +97,12 @@ + + fn set(val: Self::ReturnType) { + // Sets sink volume +- let mixer = mixer!(); +- let sink = sink!(mixer); ++ let Some(mixer) = mixer!() else { ++ return; ++ }; ++ let Some(sink) = sink!(mixer) else { ++ return; ++ }; + let max = sink.get_playback_volume_range().1 as f64; + sink.set_playback_volume_all((val.0.volume * max) as i64) + .unwrap(); +@@ -118,8 +118,17 @@ + + fn listen() { + std::thread::spawn(|| { +- let mixer = mixer!(); +- let sink = sink!(mixer); ++ // Wait for the sound server to provide a mixer with a sink ++ let mixer = loop { ++ if let Some(mixer) = mixer!() { ++ let has_sink = sink!(mixer).is_some(); ++ if has_sink { ++ break mixer; ++ } ++ } ++ std::thread::sleep(std::time::Duration::from_secs(1)); ++ }; ++ let sink = sink!(mixer).unwrap(); + let source = source!(mixer); + let mut pollfds = mixer.get().unwrap(); + +--- a/src/widgets/controls.rs ++++ b/src/widgets/controls.rs +@@ -400,7 +400,10 @@ + gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL); + controller.connect_scroll(|_, _, b| { + glib::spawn_future_local(async move { +- let sound = (system::sound::VolumeModule::get().unwrap().0.volume ++ let Some(stats) = system::sound::VolumeModule::get() else { ++ return; ++ }; ++ let sound = (stats.0.volume + - b.signum() * VOLUME_SCROLL_STEP) + .clamp(0., 1.); + system::sound::VolumeModule::set(( +@@ -420,7 +423,10 @@ + // Setup volume + let this = glib::clone::Downgrade::downgrade(&self); + let rx = system::sound::VolumeModule::rx(); +- self.update_sound(system::sound::VolumeModule::get().unwrap()); ++ // The listener sends an update once sound is available, so it's fine to skip this ++ if let Some(stats) = system::sound::VolumeModule::get() { ++ self.update_sound(stats); ++ } + glib::spawn_future_local(async move { + while let Ok(stats) = rx.recv().await { + if let Some(this) = this.upgrade() { diff --git a/wrappers/desktop/walker.nix b/wrappers/desktop/walker.nix new file mode 100644 index 0000000..8dce699 --- /dev/null +++ b/wrappers/desktop/walker.nix @@ -0,0 +1,8 @@ +{ pkgs, ... }: +{ + wrappers.walker = { + basePackage = pkgs.walker; + # Walker refuses to run unless elephant is in PATH + pathAdd = [ pkgs.elephant ]; + }; +}