Add a config file, and size the grid to the display

Colours, border and thumbnail size come from ~/.config/wl-pick/config
now, since ten more flags would have made a keybinding unreadable — the
split the rofi setup this replaces already used: look in a file,
behaviour on the command line. Flat `key = value` lines, so no TOML
dependency for something with nothing to nest, and a flag still beats the
file.

Sizes take sway's units. `600px` is absolute; `70ppt` is a percentage of
the display the grid appears on, resolved on every run rather than baked
in, so one config suits monitors of different sizes. That needed the
overlay to know which display it is on, so it now asks sway for the
focused one and maps there explicitly, at that display's scale, instead
of letting the compositor choose and taking the largest scale in use —
which was wrong on any mixed-DPI setup.

tile-width and tile-height are maxima. Given only a width, the height
follows the display's aspect: a 16:9 cell, inherited from a rofi theme
written for a landscape screen, wasted about half of every cell on a
portrait monitor. And if the grid would outgrow the display, tiles now
shrink together, keeping their shape, so thirty windows produce small
tiles rather than a surface larger than the screen. The surface is capped
at the display as a backstop, because on a small screen the padding and
label rows can exceed it no matter how small the tiles get.

Two bugs the tests caught while writing this:

- Stripping comments at the first '#' ate colour values, so
  `selection = #d79921 # note` parsed as empty. A comment is now a '#'
  followed by whitespace or end of line; a colour is '#' then a hex
  digit, so the two cannot collide.
- Twelve tiles at the old fixed size fit a 1280x800 screen, so the first
  version of the shrink test proved nothing. It now uses numbers that
  genuinely overflow.

Also: failing to reach sway said only "No such file or directory", which
tells a first-time user nothing; it now names sway and what it wanted.
This commit is contained in:
Milad Alizadeh
2026-08-31 17:29:06 +01:00
parent c22e3cdd31
commit 487acb8b6a
10 changed files with 654 additions and 126 deletions
+34 -17
View File
@@ -38,26 +38,43 @@ fn collect(node: &Node, out: &mut Vec<Target>) {
}
}
/// The active displays, and the scale the overlay should render at: the largest
/// in use, rounded up, since a buffer can be downscaled but not invented.
pub struct Displays {
pub(crate) names: Vec<String>,
pub(crate) scale: i32,
/// One active display: what the overlay needs to size itself against.
///
/// The overlay maps on the focused display, so percentages and the buffer scale
/// are resolved against *that* one — on a mixed-DPI, mixed-size setup the
/// numbers differ per monitor, and taking the largest of everything would be
/// wrong on all but one.
#[derive(Clone, Debug)]
pub struct Display {
pub name: String,
/// Logical size, which is what layer-shell and pointer events speak in.
pub width: i32,
pub height: i32,
/// Integer scale to render at: a buffer can be downscaled, not invented.
pub scale: i32,
pub focused: bool,
}
pub fn displays(conn: &mut Connection) -> Result<Displays, swayipc::Error> {
let active: Vec<_> = conn
pub fn displays(conn: &mut Connection) -> Result<Vec<Display>, swayipc::Error> {
Ok(conn
.get_outputs()?
.into_iter()
.filter(|o| o.active)
.collect();
Ok(Displays {
scale: active
.iter()
.map(|o| o.scale.unwrap_or(1.0).ceil() as i32)
.max()
.unwrap_or(1)
.max(1),
names: active.into_iter().map(|o| o.name).collect(),
})
.map(|o| Display {
name: o.name,
width: o.rect.width,
height: o.rect.height,
scale: (o.scale.unwrap_or(1.0).ceil() as i32).max(1),
focused: o.focused,
})
.collect())
}
/// The display the overlay will appear on: the focused one, or any active one if
/// sway reports none focused.
pub fn focused(displays: &[Display]) -> Option<&Display> {
displays
.iter()
.find(|d| d.focused)
.or_else(|| displays.first())
}