mirror of
https://github.com/Noratrieb/colouncher.git
synced 2026-03-14 13:16: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]
|
||||
env_logger = "0.11.8"
|
||||
eyre = "0.6.12"
|
||||
freedesktop-file-parser = "0.3.1"
|
||||
freedesktop-icons = "0.4.0"
|
||||
image = "0.25.9"
|
||||
log = "0.4.29"
|
||||
palette = "0.7.6"
|
||||
smithay-client-toolkit = "0.20.0"
|
||||
wayland-client = "0.31.11"
|
||||
|
||||
[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 log::{info, warn};
|
||||
use palette::FromColor;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
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::{
|
||||
compositor::{CompositorHandler, CompositorState},
|
||||
output::{OutputHandler, OutputState},
|
||||
reexports::{calloop::EventLoop, calloop_wayland_source::WaylandSource},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
registry_handlers,
|
||||
seat::{
|
||||
SeatHandler, SeatState,
|
||||
pointer::{BTN_LEFT, PointerEventKind, PointerHandler},
|
||||
},
|
||||
shell::{
|
||||
WaylandSurface,
|
||||
wlr_layer::{
|
||||
|
|
@ -20,13 +30,22 @@ use smithay_client_toolkit::{
|
|||
use wayland_client::{
|
||||
Connection, QueueHandle,
|
||||
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<()> {
|
||||
env_logger::builder()
|
||||
.filter(None, log::LevelFilter::Info)
|
||||
.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 (globals, event_queue) = registry_queue_init(&conn).wrap_err("initializing connection")?;
|
||||
|
|
@ -42,7 +61,10 @@ fn main() -> Result<()> {
|
|||
layer_shell: LayerShell::bind(&globals, qh)
|
||||
.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")?,
|
||||
seat_state: SeatState::new(&globals, qh),
|
||||
|
||||
desktop_files,
|
||||
pointers: HashMap::new(),
|
||||
layer_surfaces: Vec::new(),
|
||||
};
|
||||
|
||||
|
|
@ -63,13 +85,18 @@ struct App {
|
|||
compositor_state: CompositorState,
|
||||
layer_shell: LayerShell,
|
||||
shm: Shm,
|
||||
seat_state: SeatState,
|
||||
|
||||
desktop_files: Vec<(DesktopFile, Oklab)>,
|
||||
pointers: HashMap<WlSeat, WlPointer>,
|
||||
layer_surfaces: Vec<OutputSurface>,
|
||||
}
|
||||
|
||||
struct OutputSurface {
|
||||
output: WlOutput,
|
||||
_layer_surface: LayerSurface,
|
||||
layer_surface: LayerSurface,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for App {
|
||||
|
|
@ -108,12 +135,15 @@ impl OutputHandler for App {
|
|||
Some("wallpaper"),
|
||||
Some(&output),
|
||||
);
|
||||
layer_surface.set_exclusive_zone(-1);
|
||||
layer_surface.set_anchor(Anchor::all());
|
||||
layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
|
||||
layer_surface.wl_surface().commit();
|
||||
self.layer_surfaces.push(OutputSurface {
|
||||
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;
|
||||
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 canvas = pool.mmap();
|
||||
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_output!(App);
|
||||
smithay_client_toolkit::delegate_compositor!(App);
|
||||
smithay_client_toolkit::delegate_layer!(App);
|
||||
smithay_client_toolkit::delegate_shm!(App);
|
||||
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