mirror of
https://github.com/Noratrieb/colouncher.git
synced 2026-03-14 21:26:10 +01:00
spawn on color
This commit is contained in:
parent
aea9055b2f
commit
1943eec7c5
4 changed files with 1362 additions and 17 deletions
1086
Cargo.lock
generated
1086
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,10 +6,16 @@ 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"
|
||||||
wayland-client = "0.31.11"
|
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
102
src/desktop.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
183
src/main.rs
183
src/main.rs
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue