Add wlgrid: a window-thumbnail grid overlay for wlroots
A switcher to replace a wlthumbs + rofi pipeline, with the same look (gruvbox, ceil(sqrt(n)) columns capped at 4, 16:9 tiles, a yellow selection filling the element padding) but no thumbnails anywhere. Each window is captured straight into a wl_shm buffer that is handed to its own wl_subsurface, with wp_viewporter giving the compositor the rectangle to scale it into. So there is no PNG encode, no scaler, no full-resolution bitmap in this process, and the capture buffers are never even mapped here — the compositor writes those pages and samples them again for display. Opens in ~65ms for 8 windows (55ms of which is the compositor reading pixels back out of the GPU) and holds ~9MB of RSS. All capture sessions are opened before a single roundtrip and every frame goes in flight together, the same batching wlthumbs uses, because the readback is bandwidth-bound rather than latency-bound. sway remains the source of truth: the window list, the con_ids and the focusing all come from its IPC socket, joined to the Wayland side by foreign_toplevel_identifier. Navigation reads raw evdev keycodes so it is layout-independent, which does mean virtual-keyboard clients that invent their own keymap can't drive it; that resolves when filtering brings xkb. Labels, type-to-filter and live previews are next.
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
//! sway stays the source of truth for the window list and for focusing, exactly
|
||||
//! as the shell script this replaces did (`swaymsg -t get_tree` + `[con_id=N]
|
||||
//! focus`). The Wayland side only supplies pixels; the join between the two is
|
||||
//! `foreign_toplevel_identifier`, which sway reports per view.
|
||||
|
||||
use swayipc::{Connection, Node, NodeType};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Win {
|
||||
pub con_id: i64,
|
||||
pub app: String,
|
||||
pub title: String,
|
||||
/// ext-foreign-toplevel-list-v1 identifier; the key we match capture
|
||||
/// sources on.
|
||||
pub ft_id: String,
|
||||
}
|
||||
|
||||
impl Win {
|
||||
/// "title · app", the label rofigrid was given (app dropped when empty).
|
||||
/// Used once tiles are labelled.
|
||||
#[allow(dead_code)]
|
||||
pub fn label(&self) -> String {
|
||||
if self.app.is_empty() {
|
||||
self.title.clone()
|
||||
} else {
|
||||
format!("{} · {}", self.title, self.app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every view in the tree, in tree order (same traversal the jq filter did, so
|
||||
/// the grid keeps the ordering the muscle memory expects).
|
||||
pub fn windows(conn: &mut Connection) -> Result<Vec<Win>, swayipc::Error> {
|
||||
let mut out = Vec::new();
|
||||
collect(&conn.get_tree()?, &mut out);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn collect(node: &Node, out: &mut Vec<Win>) {
|
||||
let is_con = matches!(node.node_type, NodeType::Con | NodeType::FloatingCon);
|
||||
let class = node
|
||||
.window_properties
|
||||
.as_ref()
|
||||
.and_then(|p| p.class.clone());
|
||||
if is_con && (node.app_id.is_some() || class.is_some()) {
|
||||
// A view with no identifier can't be captured, but it still belongs in
|
||||
// the list: it gets a tile with no thumbnail.
|
||||
out.push(Win {
|
||||
con_id: node.id,
|
||||
app: node.app_id.clone().or(class).unwrap_or_default(),
|
||||
title: node.name.clone().unwrap_or_default(),
|
||||
ft_id: node.foreign_toplevel_identifier.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
for child in node.nodes.iter().chain(node.floating_nodes.iter()) {
|
||||
collect(child, out);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus(conn: &mut Connection, con_id: i64) -> Result<(), swayipc::Error> {
|
||||
for res in conn.run_command(format!("[con_id={con_id}] focus"))? {
|
||||
res?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user