Compare commits

..
Author SHA1 Message Date
81fbae91d0
clean up patches 2026-10-20 11:06:53 -04:00
44fbc7a4d4
new WM setup 2026-10-20 10:50:11 -04:00
10 changed files with 798 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,163 @@
{
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.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.patch
./scroll-accumulation.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,83 @@
--- a/src/state.rs
+++ b/src/state.rs
@@ -724,6 +724,9 @@
// We store both the timer and the keysym used to trigger the key action.
// When we remove the keysym from suppressed keys we stop it.
pub repeated_keyaction_timer: Option<(RegistrationToken, Keysym)>,
+ // 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.
+ pub lone_modifier: Option<Keysym>,
// To focus a layer-surface with an OnDemand keyboard exclusivity mode, the user must click
// on the layer surface. Then, when we update the keyboard focus, we check against the clicked
// layer surface
@@ -962,6 +965,7 @@
clock,
suppressed_keys: HashMap::new(),
repeated_keyaction_timer: None,
+ lone_modifier: None,
seat,
devices: vec![],
seat_state,
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -244,6 +244,7 @@
})
.map(|inhibitor| inhibitor.is_active())
.unwrap_or(false);
+ let mut lone_modifier_action = None;
let action = keyboard.input(
self,
keycode,
@@ -272,6 +273,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 {
+ state.fht.lone_modifier = raw.is_modifier_key().then_some(raw);
+ } else if state.fht.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 +346,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 +523,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
+ self.fht.lone_modifier = 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);
@@ -555,6 +578,9 @@
}
fn on_pointer_axis<B: InputBackend>(&mut self, event: B::PointerAxisEvent) {
+ // e.g. Super+scroll shouldn't count as a lone Super tap
+ self.fht.lone_modifier = None;
+
let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal);
let vertical_amount_discrete = event.amount_v120(Axis::Vertical);
let horizontal_amount = event

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();

View file

@ -0,0 +1,132 @@
--- a/src/state.rs
+++ b/src/state.rs
@@ -719,6 +719,9 @@
pub seat: Seat<State>,
pub keyboard: KeyboardHandle<State>,
pub pointer: PointerHandle<State>,
+ // Accumulated high-resolution scroll (in 1/120ths of a wheel notch), horizontal then vertical,
+ // for scroll bindings, so that they trigger once per notch rather than once per event.
+ pub scroll_binding_accumulator: (f64, f64),
pub clock: Clock<Monotonic>,
pub suppressed_keys: HashMap<u32, (KeyPattern, KeyAction)>,
// We store both the timer and the keysym used to trigger the key action.
@@ -967,6 +970,7 @@
seat_state,
keyboard,
pointer,
+ scroll_binding_accumulator: (0.0, 0.0),
mapped_layer_surfaces: HashMap::new(),
lock_state: LockState::Unlocked,
focused_on_demand_layer_shell: None,
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -569,56 +569,41 @@
let mut handled = 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_scroll(&mut self.fht.scroll_binding_accumulator.1, 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_scroll(&mut self.fht.scroll_binding_accumulator.0, discrete) {
self.process_mouse_action(0, action, SERIAL_COUNTER.next_serial());
- handled = true;
}
}
}
@@ -1080,6 +1065,25 @@
}
/// Returns `true` if a VT switch key has been handled.
+/// Accumulate high-resolution scroll for a scroll binding.
+///
+/// High-resolution wheels send several events per notch, each with part of the 120 units of a
+/// notch, so we accumulate them and only trigger the binding once per full notch. Returns whether
+/// the binding should trigger.
+fn accumulate_scroll(accumulated: &mut f64, v120: f64) -> bool {
+ if accumulated.signum() != v120.signum() {
+ // Changed direction, start over
+ *accumulated = 0.0;
+ }
+ *accumulated += v120;
+ if accumulated.abs() >= 120.0 {
+ *accumulated -= 120.0 * accumulated.signum();
+ true
+ } else {
+ false
+ }
+}
+
fn handle_vt_switch(backend: &mut Backend, key_state: KeyState, modified: Keysym) -> bool {
if key_state == KeyState::Released {
// We only do VT switch on presses.

27
flake.lock generated
View file

@ -306,11 +306,11 @@
"rust-overlay": []
},
"locked": {
"lastModified": 1791704129,
"narHash": "sha256-rSK4/dNK6AOEsQ4EozZgv07fTyi4q4RF9KoXIAlwcz0=",
"lastModified": 1792507568,
"narHash": "sha256-RGXq8SkxpxU0TaMk3oXbJ5SwkRDY90tvN2zIiofAVMQ=",
"owner": "nferhat",
"repo": "fht-compositor",
"rev": "fc0243b7468d6ee16171c2f0ba19e1b6ddddd1a0",
"rev": "458dc48d96aa6d8aa0b834a8052f33c09700ffe8",
"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 ];
};
}