srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-17 01:54:00 +0200
committersrdusr <[email protected]>2025-08-17 01:54:00 +0200
commitb8376bd631a72f7484c24663699a7d4c813f600e (patch)
treec8c84725e3c66598df3f619dd96aa69b6eede26e /crates/wayland/src/state
parentbd6aadf4d166c05f22f1ab6cdfe2e815e071f162 (diff)
downloadsrdwm-b8376bd631a72f7484c24663699a7d4c813f600e.tar.gz
srdwm-b8376bd631a72f7484c24663699a7d4c813f600e.zip
Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menus
Live testing found v1 genuinely broken, not just rough: 1. Icons weren't rendering reliably at all - ensure_desktop_icons only ever computed the grid's origin once, on whichever render pass happened to be first. AGS's own top bar registers its exclusive zone after that first pass, so origin got permanently baked in at the pre-bar geometry. Confirmed live via a temporary diagnostic log. Fixed by re-deriving origin from the primary monitor's current geometry on every call instead of just the first. 2. Fixed icons (Home/Computer/Trash) always sorted before real files -- confirmed wrong via direct question. The whole list now sorts alphabetically by label, case-insensitive, fixed icons included. 3. "Set as Wallpaper" was the wrong feature: removed entirely (DesktopMenuAction::SetWallpaper, general.wallpaper_command, is_image_path). The user wants that handled by their real file manager once opened, not reimplemented here. Also adds real menu functionality per "where are all the options": Rename (inline text edit, new CompState::renaming_icon field and keyboard redirect mirroring NativeLock::password's existing precedent), Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new trash.rs module, same-filesystem case, no confirmation - this is the reversible move-to-trash, not a permanent delete), Empty Trash on the Trash icon, and Open Terminal Here / Open in File Manager on the bare-desktop menu (new general.terminal config key). 133 wayland-crate tests (up from 106), full workspace build and clippy clean.
Diffstat (limited to 'crates/wayland/src/state')
-rw-r--r--crates/wayland/src/state/desktop_icons.rs200
-rw-r--r--crates/wayland/src/state/mod.rs8
2 files changed, 190 insertions, 18 deletions
diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs
index b1b802e..bcef073 100644
--- a/crates/wayland/src/state/desktop_icons.rs
+++ b/crates/wayland/src/state/desktop_icons.rs
@@ -3,24 +3,49 @@
//! shape as `state/menu.rs`'s `ContextMenu`/`SnapFlyout` glue.
use super::*;
-use crate::desktop_icons::{DesktopIcons, CELL_HEIGHT, CELL_WIDTH, GRID_MARGIN};
+use crate::desktop_icons::{DesktopIcons, IconKind, CELL_HEIGHT, CELL_WIDTH, GRID_MARGIN};
use crate::desktop_menu::{DesktopMenu, DesktopMenuAction};
+/// Common terminal binaries tried, in order, when `general.terminal` is
+/// unset - first one actually found on `$PATH` wins. There's no `xdg-
+/// open`-equivalent dispatcher for "a shell" the way there is for a file,
+/// so unlike `file_manager`'s empty-means-`xdg-open` fallback, this needs
+/// a real candidate list.
+const TERMINAL_CANDIDATES: &[&str] = &["alacritty", "kitty", "wezterm", "foot", "gnome-terminal", "konsole", "xterm"];
+
impl CompState {
- /// Populates `self.desktop_icons` on first call (or after `general.
- /// desktop_icons` was off and just turned on), once the primary
- /// monitor's own geometry is actually known - a no-op every other
- /// call, cheap enough to check unconditionally at the top of a render
- /// pass. Does nothing at all when the config flag is off.
+ /// Populates `self.desktop_icons` on first call, then keeps its
+ /// `origin` continuously re-anchored to the primary monitor's own
+ /// *current* usable geometry on every later call - cheap enough
+ /// (one tuple comparison) to run unconditionally at the top of every
+ /// render pass. Does nothing at all when the config flag is off.
+ ///
+ /// The re-anchoring is the fix for a real, reported bug: this used to
+ /// return immediately once `self.desktop_icons` was `Some`, computing
+ /// `origin` exactly once, on whichever render pass happened to be
+ /// first. AGS's own top bar registers its exclusive zone (`general.
+ /// desktop_icons`' fix's own PR: `srd monitors`, `Monitor::geometry`
+ /// already excludes it) only once that separate client has connected
+ /// and committed - reliably *after* this compositor's own first
+ /// render pass, confirmed live via a temporary diagnostic log: origin
+ /// baked in at `(1936, 16)` (bar not yet registered, `geometry.y` was
+ /// still `0`) and never moved again even once `srd monitors` reported
+ /// the bar's real 34-40px reservation moments later - reported live
+ /// as "Home is still being overlapped by AGS's top bar." Re-deriving
+ /// `origin` every call (not rebuilding the icon list - render
+ /// position reads `origin` fresh at push time, never baked into a
+ /// cached glyph buffer) closes this permanently, for the bar, a dock
+ /// on any edge, or any later exclusive-zone change alike.
pub(crate) fn ensure_desktop_icons(&mut self) {
if !self.wm.borrow().desktop_icons_enabled {
return;
}
- if self.desktop_icons.is_some() {
- return;
- }
let Some(monitor) = self.wm.borrow().monitors().iter().find(|m| m.primary).cloned() else { return };
let origin = (monitor.geometry.x + GRID_MARGIN, monitor.geometry.y + GRID_MARGIN);
+ if let Some(icons) = &mut self.desktop_icons {
+ icons.origin = origin;
+ return;
+ }
let rows = ((monitor.geometry.height as i32 - 2 * GRID_MARGIN) / CELL_HEIGHT).max(1);
let saved = crate::desktop_icons_state::load();
let icons = crate::desktop_icons::rescan(&saved, rows);
@@ -55,11 +80,19 @@ impl CompState {
let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
let theme = self.wm.borrow().theme;
let label_color = (240, 240, 240);
+ // While this exact icon is mid-rename, show the live-edited buffer
+ // (with a trailing caret) instead of its real, on-disk label - so
+ // typing is visible without touching the filesystem until Enter
+ // actually commits it (`desktop_icon_rename_key`).
+ let label: std::borrow::Cow<str> = match &self.renaming_icon {
+ Some((rid, buf)) if rid == id => format!("{buf}_").into(),
+ _ => icon.label.as_str().into(),
+ };
let data = decoration::render_desktop_icon(
CELL_WIDTH as u32,
CELL_HEIGHT as u32,
icon.kind,
- &icon.label,
+ &label,
icon.selected,
theme.titlebar_fg_focused,
label_color,
@@ -184,15 +217,120 @@ impl CompState {
}
}
- pub(crate) fn set_desktop_icon_as_wallpaper(&mut self, id: &str) {
+ /// Enters inline rename mode for `id`, pre-filled with its current
+ /// label - only ever called for a real file/folder icon (`desktop_
+ /// menu.rs`'s own `open_for_icon` never offers Rename for anything
+ /// else), but harmless if that assumption is ever wrong: `commit_
+ /// icon_rename` re-checks the icon's kind before touching the
+ /// filesystem.
+ pub(crate) fn start_rename_icon(&mut self, id: &str) {
let Some(icons) = &self.desktop_icons else { return };
let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
- let target = icon.target.display().to_string();
- let command = self.wm.borrow().wallpaper_command.clone();
- if command.is_empty() {
+ self.renaming_icon = Some((id.to_string(), icon.label.clone()));
+ self.rebuild_icon_buffer(id);
+ }
+
+ /// Routes one keystroke into the in-progress rename buffer - same
+ /// shape as `native_lock_key`'s own `BackSpace`/`Return`/`Escape`/
+ /// printable-character handling.
+ pub(crate) fn desktop_icon_rename_key(&mut self, name: &str, utf8: &str) {
+ let Some((id, mut buffer)) = self.renaming_icon.take() else { return };
+ match name {
+ "BackSpace" => {
+ buffer.pop();
+ self.renaming_icon = Some((id.clone(), buffer));
+ self.rebuild_icon_buffer(&id);
+ }
+ "Return" | "KP_Enter" => self.commit_icon_rename(&id, &buffer),
+ "Escape" => self.rebuild_icon_buffer(&id),
+ _ => {
+ if !utf8.is_empty() && utf8.chars().all(|c| !c.is_control()) {
+ buffer.push_str(utf8);
+ }
+ self.renaming_icon = Some((id.clone(), buffer));
+ self.rebuild_icon_buffer(&id);
+ }
+ }
+ }
+
+ /// Renames the real file/folder on disk and carries its saved grid
+ /// cell forward under the new name, so a rename doesn't also bump the
+ /// icon to a fresh default position. A blank name, or renaming
+ /// anything other than a real file/folder (shouldn't happen - see
+ /// `start_rename_icon`'s own doc comment), just cancels with no
+ /// filesystem change.
+ fn commit_icon_rename(&mut self, id: &str, new_name: &str) {
+ let new_name = new_name.trim();
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ if new_name.is_empty() || !matches!(icon.kind, IconKind::Folder | IconKind::File) {
+ self.rebuild_icon_buffer(id);
+ return;
+ }
+ let Some(parent) = icon.target.parent() else {
+ self.rebuild_icon_buffer(id);
+ return;
+ };
+ let new_path = parent.join(new_name);
+ if let Err(e) = std::fs::rename(&icon.target, &new_path) {
+ log::warn!("desktop_icons: couldn't rename {:?} to {new_name:?}: {e}", icon.target);
+ self.rebuild_icon_buffer(id);
+ return;
+ }
+ let cell = icon.cell;
+ crate::desktop_icons_state::save_icon(new_name, cell);
+ self.desktop_icon_buffers.remove(id);
+ self.refresh_desktop_icons();
+ }
+
+ /// Moves the real file/folder into `~/.local/share/Trash` - see
+ /// `trash.rs`'s own module doc comment for why this needs no
+ /// confirmation.
+ pub(crate) fn delete_desktop_icon(&mut self, id: &str) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ if let Err(e) = crate::trash::move_to_trash(&icon.target) {
+ log::warn!("desktop_icons: couldn't move {:?} to trash: {e}", icon.target);
return;
}
- spawn_shell(&format!("{command} {}", shell_quote(&target)));
+ self.desktop_icon_buffers.remove(id);
+ self.refresh_desktop_icons();
+ }
+
+ pub(crate) fn empty_trash(&mut self) {
+ let Ok(home) = std::env::var("HOME").map(std::path::PathBuf::from) else { return };
+ crate::trash::empty(&home);
+ self.desktop_icon_buffers.remove("trash");
+ }
+
+ /// Spawns `general.terminal` (or the first of `TERMINAL_CANDIDATES`
+ /// found on `$PATH`) with `~/Desktop` as its working directory.
+ pub(crate) fn open_terminal_here(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = format!("{home}/Desktop");
+ let configured = self.wm.borrow().terminal.clone();
+ let command = if !configured.is_empty() {
+ Some(configured)
+ } else {
+ TERMINAL_CANDIDATES.iter().find(|bin| on_path(bin)).map(|s| s.to_string())
+ };
+ match command {
+ Some(command) => spawn_shell_in_dir(&command, &desktop),
+ None => log::warn!("desktop_icons: no terminal found on $PATH and general.terminal is unset"),
+ }
+ }
+
+ /// Opens `~/Desktop` itself via `file_manager`/`xdg-open` - the
+ /// concrete path to a real file manager's own richer menu.
+ pub(crate) fn open_desktop_in_file_manager(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = format!("{home}/Desktop");
+ let file_manager = self.wm.borrow().file_manager.clone();
+ if file_manager.is_empty() {
+ spawn_shell(&format!("xdg-open {}", shell_quote(&desktop)));
+ } else {
+ spawn_shell(&format!("{file_manager} {}", shell_quote(&desktop)));
+ }
}
/// Creates `~/Desktop/New Folder`, de-duplicated as `New Folder (2)`,
@@ -217,8 +355,7 @@ impl CompState {
pub(crate) fn open_desktop_icon_menu(&mut self, icon_id: &str, pos: (i32, i32)) {
let Some(icons) = &self.desktop_icons else { return };
let Some(icon) = icons.icons.iter().find(|i| i.id == icon_id) else { return };
- let wallpaper_command = self.wm.borrow().wallpaper_command.clone();
- let menu = DesktopMenu::open_for_icon(icon, pos, &wallpaper_command);
+ let menu = DesktopMenu::open_for_icon(icon, pos);
self.build_desktop_menu_buffer(menu);
}
@@ -244,8 +381,12 @@ impl CompState {
pub(crate) fn run_desktop_menu_action(&mut self, action: DesktopMenuAction) {
match action {
DesktopMenuAction::OpenIcon(id) => self.open_desktop_icon(&id),
- DesktopMenuAction::SetWallpaper(id) => self.set_desktop_icon_as_wallpaper(&id),
+ DesktopMenuAction::Rename(id) => self.start_rename_icon(&id),
+ DesktopMenuAction::Delete(id) => self.delete_desktop_icon(&id),
+ DesktopMenuAction::EmptyTrash => self.empty_trash(),
DesktopMenuAction::NewFolder => self.new_desktop_folder(),
+ DesktopMenuAction::OpenTerminalHere => self.open_terminal_here(),
+ DesktopMenuAction::OpenInFileManager => self.open_desktop_in_file_manager(),
DesktopMenuAction::Refresh => self.refresh_desktop_icons(),
}
}
@@ -302,6 +443,29 @@ fn spawn_shell(command: &str) {
}
}
+/// Same fire-and-forget shell spawn as `spawn_shell`, but with a working
+/// directory - `open_terminal_here`'s own reason to exist as a separate
+/// function rather than reusing `spawn_shell` with a `cd` prefix, which
+/// would need its own quoting for `dir`.
+fn spawn_shell_in_dir(command: &str, dir: &str) {
+ #[cfg(unix)]
+ let result = std::process::Command::new("sh").arg("-c").arg(command).current_dir(dir).spawn();
+ #[cfg(windows)]
+ let result = std::process::Command::new("cmd").arg("/C").arg(command).current_dir(dir).spawn();
+ if let Err(e) = result {
+ log::warn!("desktop_icons: spawn '{command}' in {dir:?} failed: {e}");
+ }
+}
+
+/// Whether `bin` resolves to a real, regular file somewhere on `$PATH` --
+/// enough to pick a terminal candidate without a full `which`-equivalent
+/// (no executable-bit/`PATHEXT` check on Windows; this whole feature is
+/// Linux/BSD-desktop-shaped already, see `TERMINAL_CANDIDATES`' own
+/// entries).
+fn on_path(bin: &str) -> bool {
+ std::env::var_os("PATH").map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(bin).is_file())).unwrap_or(false)
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 66f9ec6..0fb303e 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -372,6 +372,14 @@ pub(crate) struct CompState {
/// `DesktopIcon::id` instead of `WindowId` since a desktop icon isn't
/// a window - see `CompState::is_double_click`'s own doc comment.
pub(crate) last_icon_click: Option<(String, u32)>,
+ /// An in-progress inline rename: the icon's own id, and the live-
+ /// edited name buffer - same shape as `NativeLock::password`
+ /// (`native_lock.rs`), the existing precedent for redirecting real
+ /// keyboard input into a plain string buffer instead of the normally-
+ /// focused client. `None` whenever no rename is in progress; the
+ /// keyboard handler checks this the same way it already checks
+ /// `state.lock.locked`.
+ pub(crate) renaming_icon: Option<(String, String)>,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,