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": []
|
||||
},
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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