Compare commits

..

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

11 changed files with 82 additions and 1054 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

@ -2,7 +2,6 @@
name = "wl-crosshair" name = "wl-crosshair"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT"
[dependencies] [dependencies]
wayland-client = "0.30.2" wayland-client = "0.30.2"
@ -11,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"

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 wl-crosshair contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,18 +1,12 @@
# wl-crosshair # wl-crosshair
A crosshair overlay for wlroots compositors (like sway). 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

12
flake.lock generated
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 {