Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

10 changed files with 81 additions and 1031 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
/target /target
.direnv/ .direnv/
cursors/*~

940
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,4 +10,3 @@ wayland-protocols-wlr = { version = "0.1.0", features = ["client"] }
log = { version = "0.4", optional = true } log = { version = "0.4", optional = true }
tempfile = "3.2" tempfile = "3.2"
image = "0.25.1"

View file

@ -3,16 +3,10 @@ A crosshair overlay for wlroots compositors.
A extremely stripped down version of [crossover](https://github.com/lacymorrow/crossover). A extremely stripped down version of [crossover](https://github.com/lacymorrow/crossover).
```sh Currently has no support for command line arguments or any customization.
wl-crosshair ./my-crosshair.png
```
### Preview (default cursor): ### Preview:
![image](https://github.com/lelgenio/wl-crosshair/assets/31388299/6e0aaa16-837b-40a8-9a13-ed808ea5db86) ![image](https://github.com/lelgenio/wl-crosshair/assets/31388299/6e0aaa16-837b-40a8-9a13-ed808ea5db86)
## TODO ### Why is it flickering when I put my cursor over it?
- [x] Make the crosshair Click-through https://github.com/lelgenio/wl-crosshair/pull/1 In wayland, windows cannot be "click-through", so in order to still send events we "close" the window when you hover it and show it in the next frame.
- [ ] Option to control size of crosshair
- [ ] Option to offset crosshair
- [ ] Configuration file
- [x] Support for loading custom crosshair images

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

View file

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1681202837,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1714091391, "lastModified": 1684242266,
"narHash": "sha256-68n3GBvlm1MIeJXadPzQ3v8Y9sIW3zmv8gI5w5sliC8=", "narHash": "sha256-uaCQ2k1bmojHKjWQngvnnnxQJMY8zi1zq527HdWgQf8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4c86138ce486d601d956a165e2f7a0fc029a03c1", "rev": "7e0743a5aea1dc755d4b761daf75b20aa486fdad",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -19,13 +19,6 @@
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with pkgs; [ makeWrapper ];
postInstall = ''
mkdir -p $out/share
cp -rv ${./cursors} $out/share/cursors
wrapProgram $out/bin/* \
--set WL_CROSSHAIR_IMAGE_PATH $out/share/cursors/inverse-v.png
'';
}; };
}; };

View file

@ -1,9 +1,8 @@
use std::{fs::File, io::Write, os::unix::prelude::AsRawFd}; use std::{fs::File, io::Write, os::unix::prelude::AsRawFd};
use image::{GenericImageView, Pixel};
use wayland_client::{ use wayland_client::{
protocol::{ protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_region::WlRegion, wl_registry, wl_seat, wl_shm, wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_shm,
wl_shm_pool, wl_surface, wl_shm_pool, wl_surface,
}, },
Connection, Dispatch, Proxy, QueueHandle, Connection, Dispatch, Proxy, QueueHandle,
@ -19,9 +18,7 @@ use wayland_protocols::xdg::shell::client::xdg_wm_base;
struct State { struct State {
running: bool, running: bool,
cursor_width: u32, cursor_size: u32,
cursor_height: u32,
image_path: String,
compositor: Option<wl_compositor::WlCompositor>, compositor: Option<wl_compositor::WlCompositor>,
base_surface: Option<wl_surface::WlSurface>, base_surface: Option<wl_surface::WlSurface>,
@ -29,30 +26,7 @@ struct State {
layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>, layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
buffer: Option<wl_buffer::WlBuffer>, buffer: Option<wl_buffer::WlBuffer>,
wm_base: Option<xdg_wm_base::XdgWmBase>, wm_base: Option<xdg_wm_base::XdgWmBase>,
} pointer: Option<wl_pointer::WlPointer>,
fn get_cursor_image_path() -> String {
if let Some(p) = std::env::args().skip(1).next() {
return p;
}
if let Ok(p) = std::env::var("WL_CROSSHAIR_IMAGE_PATH") {
return p;
}
[
std::option_env!("WL_CROSSHAIR_IMAGE_PATH").map(String::from),
Some("cursors/inverse-v.png".to_string()),
]
.into_iter()
.flatten()
.filter(|p|
std::fs::metadata(p)
.map(|m| m.is_file())
.unwrap_or(false)
)
.next()
.expect("Could not find a crosshair image, pass it as a cli argument or set WL_CROSSHAIR_IMAGE_PATH environment variable")
} }
fn main() { fn main() {
@ -66,15 +40,14 @@ fn main() {
let mut state = State { let mut state = State {
running: true, running: true,
cursor_width: 10, cursor_size: 10,
cursor_height: 10,
image_path: get_cursor_image_path(),
compositor: None, compositor: None,
base_surface: None, base_surface: None,
layer_shell: None, layer_shell: None,
layer_surface: None, layer_surface: None,
buffer: None, buffer: None,
wm_base: None, wm_base: None,
pointer: None,
}; };
event_queue.blocking_dispatch(&mut state).unwrap(); event_queue.blocking_dispatch(&mut state).unwrap();
@ -121,11 +94,10 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
} else if interface == wl_shm::WlShm::interface().name { } else if interface == wl_shm::WlShm::interface().name {
let shm = registry.bind::<wl_shm::WlShm, _, _>(name, version, qh, ()); let shm = registry.bind::<wl_shm::WlShm, _, _>(name, version, qh, ());
let (init_w, init_h) = (state.cursor_size, state.cursor_size);
let mut file = tempfile::tempfile().unwrap(); let mut file = tempfile::tempfile().unwrap();
state.draw(&mut file); draw(&mut file, (init_w, init_h));
let (init_w, init_h) = (state.cursor_width, state.cursor_height);
let pool = shm.create_pool(file.as_raw_fd(), (init_w * init_h * 4) as i32, qh, ()); let pool = shm.create_pool(file.as_raw_fd(), (init_w * init_h * 4) as i32, qh, ());
let buffer = pool.create_buffer( let buffer = pool.create_buffer(
0, 0,
@ -137,6 +109,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
(), (),
); );
state.buffer = Some(buffer); state.buffer = Some(buffer);
} else if interface == wl_seat::WlSeat::interface().name {
let seat = registry.bind::<wl_seat::WlSeat, _, _>(name, version, qh, ());
state.pointer = Some(seat.get_pointer(qh, ()));
} else if interface == xdg_wm_base::XdgWmBase::interface().name { } else if interface == xdg_wm_base::XdgWmBase::interface().name {
let wm_base = registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ()); let wm_base = registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ());
state.wm_base = Some(wm_base); state.wm_base = Some(wm_base);
@ -145,18 +120,66 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
} }
} }
impl Dispatch<WlRegion, ()> for State { impl Dispatch<wl_pointer::WlPointer, ()> for State {
fn event( fn event(
_: &mut Self, state: &mut Self,
_: &WlRegion, _: &wl_pointer::WlPointer,
_: <WlRegion as Proxy>::Event, event: wl_pointer::Event,
_: &(), _: &(),
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, qh: &QueueHandle<Self>,
) { ) {
eprintln!("WlPointer event {event:#?}");
match event {
wl_pointer::Event::Enter { .. } => {
if let Some(surface) = &state.base_surface {
surface.destroy();
}
state.base_surface = None;
}
wl_pointer::Event::Leave { .. } => {
let surface = state.compositor.as_ref().unwrap().create_surface(qh, ());
state.base_surface = Some(surface);
state.init_layer_surface(qh);
}
_ => {}
}
} }
} }
fn draw(tmp: &mut File, (buf_x, buf_y): (u32, u32)) {
let mut buf = std::io::BufWriter::new(tmp);
for y in 0..buf_y {
for x in 0..buf_x {
let ix = x as i32;
let iy = y as i32;
let dist = if x <= (buf_x / 2) {
ix + iy - (buf_y as i32)
} else {
iy - ix
};
let a: u32 = match dist.abs() {
0 => 0xFF,
1 => 0x88,
_ => 0x00,
};
let c: u32 = match dist.abs() {
0 => 0xFF,
1 => 0x88,
_ => 0x00,
};
let color = (a << 24) + (c << 16) + (c << 8) + c;
buf.write_all(&color.to_ne_bytes()).unwrap();
}
}
buf.flush().unwrap();
}
impl State { impl State {
fn init_layer_surface(&mut self, qh: &QueueHandle<State>) { fn init_layer_surface(&mut self, qh: &QueueHandle<State>) {
let layer = self.layer_shell.as_ref().unwrap().get_layer_surface( let layer = self.layer_shell.as_ref().unwrap().get_layer_surface(
@ -170,44 +193,14 @@ impl State {
// Center the window // Center the window
layer.set_anchor(Anchor::Top | Anchor::Right | Anchor::Bottom | Anchor::Left); layer.set_anchor(Anchor::Top | Anchor::Right | Anchor::Bottom | Anchor::Left);
layer.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None); layer.set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None);
layer.set_size(self.cursor_width, self.cursor_height); layer.set_size(self.cursor_size, self.cursor_size);
// A negative value means we will be centered on the screen // A negative value means we will be centered on the screen
// independently of any other xdg_layer_shell // independently of any other xdg_layer_shell
layer.set_exclusive_zone(-1); layer.set_exclusive_zone(-1);
// Set empty input region to allow clicking through the window.
if let Some(compositor) = &self.compositor {
let region = compositor.create_region(qh, ());
self.base_surface
.as_ref()
.unwrap()
.set_input_region(Some(&region));
}
self.base_surface.as_ref().unwrap().commit(); self.base_surface.as_ref().unwrap().commit();
self.layer_surface = Some(layer); self.layer_surface = Some(layer);
} }
fn draw(&mut self, tmp: &mut File) {
let mut buf = std::io::BufWriter::new(tmp);
let i = image::open(&self.image_path).unwrap();
self.cursor_width = i.width();
self.cursor_height = i.height();
for y in 0..self.cursor_height {
for x in 0..self.cursor_width {
let px = i.get_pixel(x, y).to_rgba();
let [r, g, b, a] = px.channels().try_into().unwrap();
let color = u32::from_be_bytes([a, r, g, b]);
buf.write_all(&color.to_le_bytes()).unwrap();
}
}
buf.flush().unwrap();
}
} }
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for State { impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for State {