Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
81fbae91d0 |
|||
|
44fbc7a4d4 |
10 changed files with 798 additions and 115 deletions
|
|
@ -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}"
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
163
clients/quadraticpc/fht-compositor/default.nix
Normal file
163
clients/quadraticpc/fht-compositor/default.nix
Normal 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}"
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
83
clients/quadraticpc/fht-compositor/lone-modifier.patch
Normal file
83
clients/quadraticpc/fht-compositor/lone-modifier.patch
Normal 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
|
||||||
191
clients/quadraticpc/fht-compositor/resize-tiled-windows.patch
Normal file
191
clients/quadraticpc/fht-compositor/resize-tiled-windows.patch
Normal 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();
|
||||||
132
clients/quadraticpc/fht-compositor/scroll-accumulation.patch
Normal file
132
clients/quadraticpc/fht-compositor/scroll-accumulation.patch
Normal 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
27
flake.lock
generated
|
|
@ -306,11 +306,11 @@
|
||||||
"rust-overlay": []
|
"rust-overlay": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791704129,
|
"lastModified": 1792507568,
|
||||||
"narHash": "sha256-rSK4/dNK6AOEsQ4EozZgv07fTyi4q4RF9KoXIAlwcz0=",
|
"narHash": "sha256-RGXq8SkxpxU0TaMk3oXbJ5SwkRDY90tvN2zIiofAVMQ=",
|
||||||
"owner": "nferhat",
|
"owner": "nferhat",
|
||||||
"repo": "fht-compositor",
|
"repo": "fht-compositor",
|
||||||
"rev": "fc0243b7468d6ee16171c2f0ba19e1b6ddddd1a0",
|
"rev": "458dc48d96aa6d8aa0b834a8052f33c09700ffe8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -656,6 +656,26 @@
|
||||||
"type": "github"
|
"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": {
|
"git-hooks": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat_3",
|
"flake-compat": "flake-compat_3",
|
||||||
|
|
@ -1089,6 +1109,7 @@
|
||||||
"fht-compositor": "fht-compositor",
|
"fht-compositor": "fht-compositor",
|
||||||
"firefox-gnome-theme": "firefox-gnome-theme",
|
"firefox-gnome-theme": "firefox-gnome-theme",
|
||||||
"flake-parts": "flake-parts_5",
|
"flake-parts": "flake-parts_5",
|
||||||
|
"gaypanel": "gaypanel",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"lasuite-docs-proxy": "lasuite-docs-proxy",
|
"lasuite-docs-proxy": "lasuite-docs-proxy",
|
||||||
"lk-jwt-service-module": "lk-jwt-service-module",
|
"lk-jwt-service-module": "lk-jwt-service-module",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@
|
||||||
rust-overlay.follows = "";
|
rust-overlay.follows = "";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
gaypanel = {
|
||||||
|
url = "git+https://codeberg.org/pastthepixels/gaypanel";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
firefox-gnome-theme = {
|
firefox-gnome-theme = {
|
||||||
url = "github:rafaelmardojai/firefox-gnome-theme";
|
url = "github:rafaelmardojai/firefox-gnome-theme";
|
||||||
flake = false;
|
flake = false;
|
||||||
|
|
|
||||||
89
wrappers/desktop/gaypanel/default.nix
Normal file
89
wrappers/desktop/gaypanel/default.nix
Normal 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
|
||||||
|
''}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
104
wrappers/desktop/gaypanel/no-sound-panic.patch
Normal file
104
wrappers/desktop/gaypanel/no-sound-panic.patch
Normal 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() {
|
||||||
8
wrappers/desktop/walker.nix
Normal file
8
wrappers/desktop/walker.nix
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
wrappers.walker = {
|
||||||
|
basePackage = pkgs.walker;
|
||||||
|
# Walker refuses to run unless elephant is in PATH
|
||||||
|
pathAdd = [ pkgs.elephant ];
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue