spawn on color

This commit is contained in:
nora 2025-12-29 02:03:29 +01:00
parent aea9055b2f
commit 1943eec7c5
4 changed files with 1362 additions and 17 deletions

1086
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,9 @@ edition = "2024"
[dependencies] [dependencies]
env_logger = "0.11.8" env_logger = "0.11.8"
eyre = "0.6.12" eyre = "0.6.12"
freedesktop-file-parser = "0.3.1"
freedesktop-icons = "0.4.0"
image = "0.25.9"
log = "0.4.29" log = "0.4.29"
palette = "0.7.6" palette = "0.7.6"
smithay-client-toolkit = "0.20.0" smithay-client-toolkit = "0.20.0"
@ -13,3 +16,6 @@ wayland-client = "0.31.11"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
[profile.release]
debug = "line-tables-only"

102
src/desktop.rs Normal file
View file

@ -0,0 +1,102 @@
use eyre::{Context, Result};
use freedesktop_file_parser::{DesktopFile, EntryType};
use palette::{IntoColor, Oklab, Oklaba};
use std::{
collections::HashMap,
ffi::OsStr,
fs::DirEntry,
path::{Path, PathBuf},
};
fn walkdir(path: &Path, f: &mut impl FnMut(&DirEntry) -> Result<()>) -> Result<()> {
for entry in path.read_dir()? {
let entry = entry?;
f(&entry).wrap_err_with(|| format!("{}", entry.path().display()))?;
if entry.file_type()?.is_dir() {
walkdir(&entry.path(), f).wrap_err_with(|| format!("{}", path.display()))?;
}
}
Ok(())
}
pub(crate) fn find_desktop_files() -> Result<Vec<(DesktopFile, Oklab)>> {
// https://specifications.freedesktop.org/desktop-entry/latest/file-naming.html
let paths = std::env::var("XDG_DATA_DIRS").unwrap_or("/usr/local/share/:/usr/share/".into());
let paths = std::env::split_paths(&paths).map(PathBuf::from);
let mut results = HashMap::new();
for data_dir in paths {
let base = data_dir.join("applications");
if !base.try_exists()? {
continue;
}
walkdir(&base, &mut |file| {
if file.path().extension() != Some(OsStr::new("desktop")) {
return Ok(());
}
let path = file.path();
let id = path
.strip_prefix(&base)
.unwrap()
.to_str()
.unwrap()
.replace('/', "-");
let contents = std::fs::read_to_string(&path)?;
let file =
freedesktop_file_parser::parse(&contents).wrap_err("parsing .desktop file")?;
if !results.contains_key(&id) {
if file.entry.no_display != Some(true)
&& file.entry.hidden != Some(true)
&& let EntryType::Application(_) = file.entry.entry_type
&& let Some(icon) = &file.entry.icon
&& let Some(icon) = icon.get_icon_path()
&& icon.extension() != Some(OsStr::new("svg"))
{ dbg!(path);
let icon: image::DynamicImage = image::ImageReader::open(&icon)
.wrap_err_with(|| format!("{}", icon.display()))?
.decode()
.wrap_err_with(|| format!("decoding {}", icon.display()))?;
let color = average_color(&icon);
results.insert(id, (file, color));
}
}
Ok(())
})
.wrap_err_with(|| format!("{}", base.display()))?;
}
Ok(results.into_values().collect())
}
fn average_color(image: &image::DynamicImage) -> palette::Oklab {
use palette::cast::FromComponents;
let mut total_l = 0.0;
let mut total_a = 0.0;
let mut total_b = 0.0;
let image = image.to_rgba8();
let pixels = <&[palette::Srgba<u8>]>::from_components(&*image);
let mut count = 0.0;
for pixel in pixels {
let color: Oklaba = pixel.into_linear().into_color();
let weight = color.alpha;
total_l += color.l * weight;
total_a += color.a * weight;
total_b += color.b * weight;
count += weight;
}
Oklab {
l: total_l / count,
a: total_a / count,
b: total_b / count,
}
}

View file

@ -1,14 +1,24 @@
use std::time::Duration; mod desktop;
use eyre::{Context, Result}; use std::{
use log::{info, warn}; collections::HashMap,
use palette::FromColor; time::{Duration, Instant},
};
use eyre::{Context, Result, bail};
use freedesktop_file_parser::{DesktopFile, EntryType};
use log::{error, info, warn};
use palette::{FromColor, IntoColor, Oklab, color_difference::EuclideanDistance};
use smithay_client_toolkit::{ use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState}, compositor::{CompositorHandler, CompositorState},
output::{OutputHandler, OutputState}, output::{OutputHandler, OutputState},
reexports::{calloop::EventLoop, calloop_wayland_source::WaylandSource}, reexports::{calloop::EventLoop, calloop_wayland_source::WaylandSource},
registry::{ProvidesRegistryState, RegistryState}, registry::{ProvidesRegistryState, RegistryState},
registry_handlers, registry_handlers,
seat::{
SeatHandler, SeatState,
pointer::{BTN_LEFT, PointerEventKind, PointerHandler},
},
shell::{ shell::{
WaylandSurface, WaylandSurface,
wlr_layer::{ wlr_layer::{
@ -20,13 +30,22 @@ use smithay_client_toolkit::{
use wayland_client::{ use wayland_client::{
Connection, QueueHandle, Connection, QueueHandle,
globals::registry_queue_init, globals::registry_queue_init,
protocol::{wl_buffer, wl_output::WlOutput, wl_shm}, protocol::{wl_buffer, wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, wl_shm},
}; };
fn main() -> Result<()> { fn main() -> Result<()> {
env_logger::builder() env_logger::builder()
.filter(None, log::LevelFilter::Info) .filter(None, log::LevelFilter::Info)
.init(); .init();
let now = Instant::now();
let desktop_files = desktop::find_desktop_files().wrap_err("loading .desktop files")?;
info!(
"Loaded {} desktop icons in {:?}",
desktop_files.len(),
now.elapsed()
);
let conn = Connection::connect_to_env().wrap_err("can't connect to Wayland socket")?; let conn = Connection::connect_to_env().wrap_err("can't connect to Wayland socket")?;
let (globals, event_queue) = registry_queue_init(&conn).wrap_err("initializing connection")?; let (globals, event_queue) = registry_queue_init(&conn).wrap_err("initializing connection")?;
@ -42,7 +61,10 @@ fn main() -> Result<()> {
layer_shell: LayerShell::bind(&globals, qh) layer_shell: LayerShell::bind(&globals, qh)
.wrap_err("failed to bind zwlr_layer_shell_v1 global, does the compositor not support layer shell?")?, .wrap_err("failed to bind zwlr_layer_shell_v1 global, does the compositor not support layer shell?")?,
shm: Shm::bind(&globals, qh).wrap_err("failed to bind shm")?, shm: Shm::bind(&globals, qh).wrap_err("failed to bind shm")?,
seat_state: SeatState::new(&globals, qh),
desktop_files,
pointers: HashMap::new(),
layer_surfaces: Vec::new(), layer_surfaces: Vec::new(),
}; };
@ -63,13 +85,18 @@ struct App {
compositor_state: CompositorState, compositor_state: CompositorState,
layer_shell: LayerShell, layer_shell: LayerShell,
shm: Shm, shm: Shm,
seat_state: SeatState,
desktop_files: Vec<(DesktopFile, Oklab)>,
pointers: HashMap<WlSeat, WlPointer>,
layer_surfaces: Vec<OutputSurface>, layer_surfaces: Vec<OutputSurface>,
} }
struct OutputSurface { struct OutputSurface {
output: WlOutput, output: WlOutput,
_layer_surface: LayerSurface, layer_surface: LayerSurface,
width: u32,
height: u32,
} }
impl ProvidesRegistryState for App { impl ProvidesRegistryState for App {
@ -108,12 +135,15 @@ impl OutputHandler for App {
Some("wallpaper"), Some("wallpaper"),
Some(&output), Some(&output),
); );
layer_surface.set_exclusive_zone(-1);
layer_surface.set_anchor(Anchor::all()); layer_surface.set_anchor(Anchor::all());
layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None); layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
layer_surface.wl_surface().commit(); layer_surface.wl_surface().commit();
self.layer_surfaces.push(OutputSurface { self.layer_surfaces.push(OutputSurface {
output, output,
_layer_surface: layer_surface, layer_surface,
width: 0,
height: 0,
}); });
} }
@ -216,6 +246,16 @@ impl LayerShellHandler for App {
) { ) {
let (width, height) = configure.new_size; let (width, height) = configure.new_size;
info!("Reconfiguring surface to {}x{}", width, height); info!("Reconfiguring surface to {}x{}", width, height);
if let Some(surface) = self
.layer_surfaces
.iter_mut()
.find(|surface| surface.layer_surface == *layer)
{
surface.width = width;
surface.height = height;
}
let mut pool = RawPool::new(width as usize * height as usize * 4, &self.shm).unwrap(); let mut pool = RawPool::new(width as usize * height as usize * 4, &self.shm).unwrap();
let canvas = pool.mmap(); let canvas = pool.mmap();
canvas canvas
@ -272,9 +312,138 @@ impl ShmHandler for App {
} }
} }
impl SeatHandler for App {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_seat: wayland_client::protocol::wl_seat::WlSeat,
) {
}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wayland_client::protocol::wl_seat::WlSeat,
capability: smithay_client_toolkit::seat::Capability,
) {
if capability == smithay_client_toolkit::seat::Capability::Pointer {
self.pointers.insert(
seat.clone(),
self.seat_state.get_pointer(qh, &seat).unwrap(),
);
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
seat: wayland_client::protocol::wl_seat::WlSeat,
capability: smithay_client_toolkit::seat::Capability,
) {
if capability == smithay_client_toolkit::seat::Capability::Pointer {
self.pointers.remove(&seat);
}
}
fn remove_seat(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_seat: wayland_client::protocol::wl_seat::WlSeat,
) {
}
}
impl PointerHandler for App {
fn pointer_frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_pointer: &wayland_client::protocol::wl_pointer::WlPointer,
events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
) {
for event in events {
match event.kind {
PointerEventKind::Release {
button: BTN_LEFT, ..
} => {
let Some(surface) = self
.layer_surfaces
.iter()
.find(|surface| *surface.layer_surface.wl_surface() == event.surface)
else {
return;
};
let srgb = color_for_pixel(
event.position.0 as u32,
event.position.1 as u32,
surface.width,
surface.height,
);
let oklab: Oklab = srgb.into_format::<f32>().into_color();
let best_match = self.desktop_files.iter().min_by_key(|(_, icon_color)| {
(oklab.distance(*icon_color) * 1000000.0) as u32
});
if let Some(best_match) = best_match
&& let EntryType::Application(app) = &best_match.0.entry.entry_type
&& let Some(exec) = &app.exec
{
// lol terrible implementation that works well enough
// https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html
let exec = exec.replace("%U", "").replace("%F", "");
if exec.contains("%") {
warn!(
"Trying to execute insuffiently substituded command-line, refusing: {}",
exec
);
return;
}
if let Err(err) = spawn(&exec) {
error!("Failed to spawn program: {}: {:?}", exec, err);
}
}
}
_ => {}
}
}
}
}
fn spawn(cmd: &str) -> Result<()> {
info!("Spawning program: {cmd}");
let output = std::process::Command::new("niri")
.arg("msg")
.arg("action")
.arg("spawn-sh")
.arg("--")
.arg(cmd)
.output()
.wrap_err("executing niri msg action spawn-sh")?;
if !output.status.success() {
bail!(
"niri returned error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
smithay_client_toolkit::delegate_registry!(App); smithay_client_toolkit::delegate_registry!(App);
smithay_client_toolkit::delegate_output!(App); smithay_client_toolkit::delegate_output!(App);
smithay_client_toolkit::delegate_compositor!(App); smithay_client_toolkit::delegate_compositor!(App);
smithay_client_toolkit::delegate_layer!(App); smithay_client_toolkit::delegate_layer!(App);
smithay_client_toolkit::delegate_shm!(App); smithay_client_toolkit::delegate_shm!(App);
wayland_client::delegate_noop!(App: ignore wl_buffer::WlBuffer); wayland_client::delegate_noop!(App: ignore wl_buffer::WlBuffer);
smithay_client_toolkit::delegate_seat!(App);
smithay_client_toolkit::delegate_pointer!(App);