Add area screenshots and preserve stacking on hover focus

This commit is contained in:
Samuel Onoja committed 2026-09-26 23:22:38 +01:00
1 parent 8dc895448d
commit 5d6c610fec
9 files changed
+98 -15

No files matched your search

+8
View File
@@ -24,6 +24,7 @@ and native Settings app.
- Built-in bar and control center - Built-in bar and control center
- Native Settings app - Native Settings app
- Desktop widgets - Desktop widgets
- Area screenshots saved to disk and copied to the clipboard
- Themes, blur and gradient borders - Themes, blur and gradient borders
- Multi-monitor support and fractional scaling - Multi-monitor support and fractional scaling
- Live configuration reload - Live configuration reload
@@ -50,6 +51,7 @@ Requirements (Fedora package names):
- Default terminal: `foot` (or configure another terminal). - Default terminal: `foot` (or configure another terminal).
- Optional: `zenity` for wallpaper browsing, `swayidle` for idle actions, - Optional: `zenity` for wallpaper browsing, `swayidle` for idle actions,
and `swaylock` or `swaylock-effects` for locking. and `swaylock` or `swaylock-effects` for locking.
- Screenshots: `grim`, `slurp`, and `wl-clipboard`.
For a login-screen session alongside your existing desktop: For a login-screen session alongside your existing desktop:
@@ -77,8 +79,14 @@ Built-in defaults (your configuration can override them):
| Super + Shift + F | True fullscreen | | Super + Shift + F | True fullscreen |
| Super + Tab | Open workspace and window overview | | Super + Tab | Open workspace and window overview |
| Super + M | Switch scrolling / tree layout | | Super + M | Switch scrolling / tree layout |
| Super + Shift + S | Select a screenshot area |
| Super + Shift + E | Log out immediately—save your work first | | Super + Shift + E | Log out immediately—save your work first |
In an installed session, screenshots are saved to `Pictures/Screenshots` and
copied to the clipboard. Press Escape to cancel the selection.
When focus follows the mouse is enabled, hovering changes keyboard focus without
raising the window; clicking raises it within its layer.
## Make it yours ## Make it yours
Open **Control Center → Settings**, or run `ferese-settings`. Open **Control Center → Settings**, or run `ferese-settings`.
+17 -4
View File
@@ -895,7 +895,13 @@ impl Config {
} }
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> { pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
let mut commands = HashMap::from([("terminal".to_owned(), vec!["foot".to_owned()])]); let mut commands = HashMap::from([
("terminal".to_owned(), vec!["foot".to_owned()]),
(
"screenshot".to_owned(),
vec!["ferese-screenshot".to_owned()],
),
]);
commands.extend(self.commands.clone()); commands.extend(self.commands.clone());
validate_commands(&commands)?; validate_commands(&commands)?;
@@ -1398,6 +1404,7 @@ fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
fn default_bindings() -> Vec<BindingConfig> { fn default_bindings() -> Vec<BindingConfig> {
let mut bindings = vec![ let mut bindings = vec![
binding("Super+Enter", "spawn", Some("terminal")), binding("Super+Enter", "spawn", Some("terminal")),
binding("Super+Shift+S", "spawn", Some("screenshot")),
binding("Super+Q", "close", None), binding("Super+Q", "close", None),
binding("Super+F", "toggle-maximized", None), binding("Super+F", "toggle-maximized", None),
binding("Super+Shift+F", "toggle-fullscreen", None), binding("Super+Shift+F", "toggle-fullscreen", None),
@@ -2069,7 +2076,7 @@ mod tests {
let input = config.input_settings().unwrap(); let input = config.input_settings().unwrap();
let bindings = config.bindings(&input).unwrap(); let bindings = config.bindings(&input).unwrap();
assert_eq!(bindings.len(), 42); assert_eq!(bindings.len(), 43);
for (shift, action) in [ for (shift, action) in [
(false, BindingAction::ToggleMaximized), (false, BindingAction::ToggleMaximized),
(true, BindingAction::ToggleFullscreen), (true, BindingAction::ToggleFullscreen),
@@ -2089,6 +2096,12 @@ mod tests {
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
&& binding.action == BindingAction::Spawn(vec!["foot".to_owned()]) && binding.action == BindingAction::Spawn(vec!["foot".to_owned()])
})); }));
assert!(bindings.iter().any(|binding| {
binding.modifiers.logo
&& binding.modifiers.shift
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s)
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()])
}));
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.modifiers.logo binding.modifiers.logo
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Tab) && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Tab)
@@ -2105,7 +2118,7 @@ mod tests {
let input = replaced.input_settings().unwrap(); let input = replaced.input_settings().unwrap();
let bindings = replaced.bindings(&input).unwrap(); let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 42); assert_eq!(bindings.len(), 43);
assert!(bindings.iter().any(|binding| { assert!(bindings.iter().any(|binding| {
binding.action binding.action
== BindingAction::Spawn(vec![ == BindingAction::Spawn(vec![
@@ -2117,7 +2130,7 @@ mod tests {
let input = unbound.input_settings().unwrap(); let input = unbound.input_settings().unwrap();
let bindings = unbound.bindings(&input).unwrap(); let bindings = unbound.bindings(&input).unwrap();
assert_eq!(bindings.len(), 41); assert_eq!(bindings.len(), 42);
assert!( assert!(
!bindings !bindings
.iter() .iter()
+14 -6
View File
@@ -217,7 +217,7 @@ impl Ferese {
crate::dimming::DimAnimation::new(f64::from(opacity)), crate::dimming::DimAnimation::new(f64::from(opacity)),
)); ));
pointer.unset_grab(self, serial, event.time() as u32); pointer.unset_grab(self, serial, event.time() as u32);
self.focus_window_at(pointer.current_location(), serial); self.focus_window_at(pointer.current_location(), serial, true);
crate::backends::direct::render_all(self); crate::backends::direct::render_all(self);
} }
@@ -254,7 +254,7 @@ impl Ferese {
} }
if event.state() == ButtonState::Pressed && !pointer.is_grabbed() { if event.state() == ButtonState::Pressed && !pointer.is_grabbed() {
self.focus_window_at(pointer.current_location(), serial); self.focus_window_at(pointer.current_location(), serial, true);
} }
pointer.button( pointer.button(
@@ -320,7 +320,7 @@ impl Ferese {
let Some(location) = self.absolute_event_position(&event) else { let Some(location) = self.absolute_event_position(&event) else {
return; return;
}; };
self.focus_window_at(location, SERIAL_COUNTER.next_serial()); self.focus_window_at(location, SERIAL_COUNTER.next_serial(), true);
let touch = self.seat.get_touch().expect("seat has touch capability"); let touch = self.seat.get_touch().expect("seat has touch capability");
let serial = SERIAL_COUNTER.next_serial(); let serial = SERIAL_COUNTER.next_serial();
@@ -387,7 +387,7 @@ impl Ferese {
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64()) Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
} }
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial) { fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
if self.session_lock.active { if self.session_lock.active {
self.focus_output_at(position); self.focus_output_at(position);
self.focus_lock_surface(); self.focus_lock_surface();
@@ -412,7 +412,15 @@ impl Ferese {
return; return;
} }
self.focused_window = focused; self.focused_window = focused;
self.raise_window(&window, true); if raise {
self.raise_window(&window, true);
} else {
// Hover transfers keyboard focus without changing the persistent
// stack: an exposed window must not cover the floats above it.
for mapped in self.space.elements() {
mapped.set_activated(mapped == &window);
}
}
let surface = window let surface = window
.toplevel() .toplevel()
.expect("mapped window has a toplevel") .expect("mapped window has a toplevel")
@@ -455,7 +463,7 @@ impl Ferese {
return; return;
} }
self.focus_window_at(position, SERIAL_COUNTER.next_serial()); self.focus_window_at(position, SERIAL_COUNTER.next_serial(), false);
} }
fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> { fn clamp_pointer_position(&self, requested: Point<f64, Logical>) -> Point<f64, Logical> {
+10 -2
View File
@@ -127,7 +127,7 @@ angle = 135.0
| `[input]` key | Type | Default | Meaning | | `[input]` key | Type | Default | Meaning |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer | | `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer without raising them; click to raise |
| `xkb_layout` | nonempty string | `"us"` | XKB layout | | `xkb_layout` | nonempty string | `"us"` | XKB layout |
| `xkb_variant` | string | `""` | XKB variant | | `xkb_variant` | string | `""` | XKB variant |
| `xkb_options` | string array | `[]` | XKB options | | `xkb_options` | string array | `[]` | XKB options |
@@ -178,7 +178,13 @@ Super+Shift+H/J/K/L move; Super+Ctrl+H/J/K/L resize; Super+1–9 workspace;
Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center; Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen; Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
Super+M layout; Super+Shift+Space floating; Super+Tab overview; Super+M layout; Super+Shift+Space floating; Super+Tab overview;
Super+Shift+E immediate logout. Super+Shift+S area screenshot; Super+Shift+E immediate logout.
The screenshot shortcut runs `ferese-screenshot`: drag to select an area, or
press Escape to cancel. Captures are saved under your Pictures directory in
`Screenshots` and copied to the clipboard as PNG images. It requires `grim`,
`slurp`, and `wl-copy` (from `wl-clipboard`). Override the `screenshot` command
to use another screenshot tool.
## Window rules ## Window rules
@@ -273,5 +279,7 @@ ending the session from another TTY.
These are launch-time environment switches, not TOML keys. Enable only for These are launch-time environment switches, not TOML keys. Enable only for
trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`, trusted clients: `FERESE_ENABLE_INPUT_METHOD=1`,
`FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`. `FERESE_ENABLE_SHORTCUT_INHIBIT=1`, `FERESE_ENABLE_SCREENCOPY=1`.
The installed session launcher enables screencopy for screenshots unless
`FERESE_ENABLE_SCREENCOPY=0` is explicitly set in its environment.
Text input is available by default; Ctrl+Alt+Escape releases an active shortcut Text input is available by default; Ctrl+Alt+Escape releases an active shortcut
inhibitor. Use `feresectl --help` for runtime control commands. inhibitor. Use `feresectl --help` for runtime control commands.
+6
View File
@@ -5,6 +5,7 @@
On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build On Fedora with SDDM, install a Rust toolchain (1.93 or newer), native build
dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also dependencies, `desktop-file-utils`, D-Bus and `foot`. Wallpaper browsing also
needs `zenity`; locking needs swaylock or swaylock-effects. needs `zenity`; locking needs swaylock or swaylock-effects.
Area screenshots need `grim`, `slurp`, and `wl-clipboard`.
From the checkout, run as your normal user: From the checkout, run as your normal user:
@@ -35,6 +36,11 @@ its DRM backend inside an active graphical session.
Logs are stored in `${XDG_STATE_HOME:-~/.local/state}/ferese/`. Keep an existing Logs are stored in `${XDG_STATE_HOME:-~/.local/state}/ferese/`. Keep an existing
desktop available while testing scaling, monitor hotplug, suspend and locking. desktop available while testing scaling, monitor hotplug, suspend and locking.
The session launcher enables the screencopy protocol for screenshot tools.
Set `FERESE_ENABLE_SCREENCOPY=0` in the session environment to disable it.
When enabled, Wayland clients can capture the unlocked desktop. Capture remains
blocked while the session is locked. Launcher changes take effect at next login.
## Logout and recovery ## Logout and recovery
Save your work before **Super+Shift+E** or `feresectl exit`: logout is immediate. Save your work before **Super+Shift+E** or `feresectl exit`: logout is immediate.
+2 -1
View File
@@ -2,6 +2,7 @@
# Unspecified monitors use automatic placement and preferred modes. # Unspecified monitors use automatic placement and preferred modes.
[commands] [commands]
terminal = ["foot"] terminal = ["foot"]
screenshot = ["ferese-screenshot"]
[input] [input]
xkb_layout = "us" xkb_layout = "us"
@@ -17,7 +18,7 @@ opacity = 0.95
text_primary = "#F0F3FA" text_primary = "#F0F3FA"
text_muted = "#AAB4C7" text_muted = "#AAB4C7"
# Super+Enter opens Foot. Super+Shift+E logs out. # Super+Enter opens Foot. Super+Shift+S selects a screenshot. Super+Shift+E logs out.
# See README.md for monitor profiles and additional bindings. # See README.md for monitor profiles and additional bindings.
[[window_rules]] [[window_rules]]
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Select an area, save it in Pictures/Screenshots, and copy it to the clipboard.
set -euo pipefail
umask 077
fail() {
echo "ferese-screenshot: $*" >&2
if command -v notify-send >/dev/null; then
notify-send --app-name=Ferese --urgency=critical 'Screenshot failed' "$*" || true
fi
exit 1
}
for tool in slurp grim wl-copy flock; do
command -v "$tool" >/dev/null || fail "Required command not found: $tool"
done
: "${XDG_RUNTIME_DIR:?A Wayland login session is required}"
exec 9>"$XDG_RUNTIME_DIR/ferese-screenshot.lock"
flock -n 9 || exit 0
# Escape cancels without creating a file or changing the clipboard.
geometry=$(slurp) || exit 0
[[ -n $geometry ]] || exit 0
pictures=$(xdg-user-dir PICTURES 2>/dev/null) || pictures="$HOME/Pictures"
directory=${pictures:-$HOME/Pictures}/Screenshots
mkdir -p -- "$directory" || fail 'Could not create the screenshot directory.'
file=$(mktemp "$directory/Screenshot-$(date +%Y-%m-%d_%H-%M-%S)-XXXXXX.png") || fail 'Could not create the screenshot file.'
trap 'rm -f -- "$file"' EXIT
grim -g "$geometry" "$file" || fail 'Could not capture the screen. Log back into Ferese after enabling screen capture.'
trap - EXIT
# Do not pass the selector lock to wl-copy's background clipboard process.
wl-copy --type image/png <"$file" 9>&- || fail "Saved to $file, but could not copy to the clipboard."
if command -v notify-send >/dev/null; then
notify-send --app-name=Ferese 'Screenshot saved and copied' "$file" || true
fi
printf '%s\n' "$file"
+2
View File
@@ -26,6 +26,8 @@ export XDG_SESSION_DESKTOP=ferese
export XDG_SESSION_TYPE=wayland export XDG_SESSION_TYPE=wayland
export MOZ_ENABLE_WAYLAND=1 export MOZ_ENABLE_WAYLAND=1
export QT_QPA_PLATFORM=wayland export QT_QPA_PLATFORM=wayland
# Used by the area screenshot shortcut; explicitly set 0 to disable capture.
export FERESE_ENABLE_SCREENCOPY=${FERESE_ENABLE_SCREENCOPY:-1}
export PATH="$release_dir:$PATH" export PATH="$release_dir:$PATH"
unset WAYLAND_SOCKET unset WAYLAND_SOCKET
if [[ $backend == drm ]]; then if [[ $backend == drm ]]; then
+3 -2
View File
@@ -19,7 +19,7 @@ session_entry=/usr/share/wayland-sessions/ferese.desktop
[[ ! -e $release_dir ]] || { echo "Release already exists: $release_dir" >&2; exit 1; } [[ ! -e $release_dir ]] || { echo "Release already exists: $release_dir" >&2; exit 1; }
# Refuse to overwrite unrelated installations. # Refuse to overwrite unrelated installations.
for name in ferese ferese-shell ferese-settings feresectl ferese-session ferese-lock; do for name in ferese ferese-shell ferese-settings feresectl ferese-session ferese-lock ferese-screenshot; do
link=/usr/local/bin/$name link=/usr/local/bin/$name
if [[ -e $link || -L $link ]]; then if [[ -e $link || -L $link ]]; then
[[ -L $link && $(readlink -- "$link") == "$install_root/current/$name" ]] || { [[ -L $link && $(readlink -- "$link") == "$install_root/current/$name" ]] || {
@@ -54,6 +54,7 @@ for name in ferese ferese-shell ferese-settings feresectl; do
done done
install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session" install -m 0755 -- "$repo_dir/packaging/ferese-session" "$release_dir/ferese-session"
install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock" install -m 0755 -- "$repo_dir/packaging/ferese-lock" "$release_dir/ferese-lock"
install -m 0755 -- "$repo_dir/packaging/ferese-screenshot" "$release_dir/ferese-screenshot"
install -m 0644 -- "$repo_dir/packaging/config.toml" "$release_dir/config.example.toml" install -m 0644 -- "$repo_dir/packaging/config.toml" "$release_dir/config.example.toml"
if [[ -L $install_root/current ]]; then if [[ -L $install_root/current ]]; then
@@ -61,7 +62,7 @@ if [[ -L $install_root/current ]]; then
fi fi
ln -s -- "releases/$release_id" "$install_root/current-$release_id" ln -s -- "releases/$release_id" "$install_root/current-$release_id"
mv -Tf -- "$install_root/current-$release_id" "$install_root/current" mv -Tf -- "$install_root/current-$release_id" "$install_root/current"
for name in ferese ferese-shell ferese-settings feresectl ferese-session ferese-lock; do for name in ferese ferese-shell ferese-settings feresectl ferese-session ferese-lock ferese-screenshot; do
ln -sfn -- "$install_root/current/$name" "/usr/local/bin/$name" ln -sfn -- "$install_root/current/$name" "/usr/local/bin/$name"
done done
install -m 0644 -- "$repo_dir/packaging/ferese.desktop" "$session_entry" install -m 0644 -- "$repo_dir/packaging/ferese.desktop" "$session_entry"