new WM setup

This commit is contained in:
Henry Hiles 2026-10-20 10:50:11 -04:00
commit 44fbc7a4d4
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
10 changed files with 784 additions and 115 deletions

View file

@ -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}"
'';
});
};
}

View file

@ -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 </dev/null &
}
restart '/bin/elephant$' systemd-cat -t elephant ${lib.getExe pkgs.elephant}
restart 'walker.* --gapplication-service' walker --gapplication-service
restart 'gaypanel-wrapped_' systemd-cat -t gaypanel gaypanel
restart 'bin/swaybg ' ${lib.getExe pkgs.swaybg} -i ${../../../background.jpg} -m fill
'';
}
);
in
{
imports = [ inputs.fht-compositor.nixosModules.default ];
# Take keyboard focus when opened, so you can type into the search straight away
environment.etc."xdg/walker/config.toml".source = (pkgs.formats.toml { }).generate "walker.toml" {
force_keyboard_focus = true;
};
environment.etc.${filename}.source = (pkgs.formats.toml { }).generate "config.toml" {
autostart = [ session-programs ];
general = {
focus-follows-mouse = true;
inner-gaps = 10;
};
decorations = {
border = {
thickness = 4;
radius = 12;
};
blur.disable = true;
};
keybinds = {
Alt-Tab = "focus-next-window";
Super-q = "close-focused-window";
Super-f = "fullscreen-focused-window";
Super-Ctrl-q = "quit";
Super-Ctrl-r = {
action = "run-command";
arg = "fht-compositor ipc action reload-config; ${session-programs}";
};
# Runs when Super is tapped on its own (patched in, see lone-modifier-and-scroll.patch)
Super_L = {
action = "run-command";
arg = "walker";
};
Print = {
action = "run-command";
arg = "${lib.getExe pkgs.grim} -g \"$(${lib.getExe pkgs.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";
# Works on tiled windows too (patched in, see resize-tiled-windows.patch)
Super-Right = "resize-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: {
patches = (old.patches or [ ]) ++ [
./resize-tiled-windows.patch
./lone-modifier-and-scroll.patch
# Fixes screencasting (e.g. OBS window capture) being rejected by xdg-desktop-portal
./portal-version.patch
];
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ];
postInstall = (old.postInstall or "") + ''
wrapProgram $out/bin/fht-compositor \
--add-flags "-c /etc/${filename}"
'';
});
};
}

View file

@ -0,0 +1,187 @@
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -3,6 +3,7 @@
pub mod resize_tile_grab;
pub mod swap_tile_grab;
+use std::cell::Cell;
use std::collections::HashMap;
use std::time::Duration;
@@ -32,6 +33,15 @@
use crate::output::OutputExt;
use crate::state::State;
+thread_local! {
+ /// A modifier key that was pressed on its own, with no other key, button or scroll since.
+ /// Keybinds on just a modifier key (e.g. `Super_L`) run when it's released in that state.
+ static LONE_MODIFIER: Cell<Option<Keysym>> = 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<B: InputBackend>(&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 {

View file

@ -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
}

View file

@ -0,0 +1,191 @@
--- a/src/space/workspace.rs
+++ b/src/space/workspace.rs
@@ -59,6 +59,11 @@
window: Window,
initial_window_geometry: Rectangle<i32, Logical>,
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<usize> {
+ // Matches how arrange_tiles orders and splits tiles.
+ let tiled: Vec<usize> = (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();

27
flake.lock generated
View file

@ -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",

View file

@ -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;

View file

@ -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
''}"
];
};
}

View file

@ -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<Self::ReturnType> {
- 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() {

View file

@ -0,0 +1,8 @@
{ pkgs, ... }:
{
wrappers.walker = {
basePackage = pkgs.walker;
# Walker refuses to run unless elephant is in PATH
pathAdd = [ pkgs.elephant ];
};
}