Compare commits

...

21 Commits

Author SHA1 Message Date
Ivan Molodetskikh dd967554d1 Bump version to 0.1.0 2024-01-27 14:10:31 +04:00
Ivan Molodetskikh 6d7c220137 Try harder to find an output for the screenshot UI
The mouse might be outside any outputs, let's try to open in that case
anyway.
2024-01-27 14:09:55 +04:00
Ivan Molodetskikh d77aac1afa Fix damage when rendering to texture 2024-01-27 10:50:40 +04:00
Ivan Molodetskikh 837a0a20fb Update README 2024-01-25 08:34:42 +04:00
Ivan Molodetskikh ecdf756b55 Name output render element better 2024-01-25 08:02:33 +04:00
Christian Meissl 73f3c160b2 use pixman for cursor plane rendering 2024-01-25 07:49:51 +04:00
Christian Meissl 5f99eb13ab Remove hack for fixed EGLDisplay issue 2024-01-25 07:49:51 +04:00
Christian Meissl 20326b093c Update smithay 2024-01-25 07:49:51 +04:00
Ivan Molodetskikh 467d92a4b4 github: Add a feature request link to start a discussion 2024-01-23 17:41:35 +04:00
Ivan Molodetskikh 15bb69c0b9 Update issue templates 2024-01-23 05:36:19 -08:00
Ivan Molodetskikh adfbfdffb3 Create a bug report template 2024-01-23 05:34:38 -08:00
Ivan Molodetskikh 087ed260c5 Update Smithay (find_popup_root_surface() panic fix) 2024-01-23 17:12:47 +04:00
Ivan Molodetskikh f5642ab733 Ignore popup grabs when IME keyboard grab is active
Doing this properly will require more refactors, potentially in Smithay.
For now let's just ignore popup grabs to make popups work.
2024-01-23 17:05:08 +04:00
Ivan Molodetskikh ab9706cb30 screencast: Emit MonitorsChanged 2024-01-23 12:02:52 +04:00
Ivan Molodetskikh 05f2a3709b srceencast: Send stream size
Kooha requires this (even though it's optional). Unfortunately, Kooha
also seems to want memfd recording so it doesn't work anyway.
2024-01-23 11:36:11 +04:00
Ivan Molodetskikh 743173ef64 config: Bump precision on the default widths
This seems to actually matter on my 2560x display.
2024-01-22 20:43:33 +04:00
Ivan Molodetskikh cbbb7a26fc Update Smithay, use device changed session resume code
Should fix most cases of monitors failing to light up after a TTY
switch.
2024-01-22 16:13:39 +04:00
sodiboo 18566e3366 Watch for canonical filename, not just mtime 2024-01-22 07:42:45 +04:00
Ivan Molodetskikh df48337d83 tty: Delay output config update until resume
We can't do anything while paused.
2024-01-21 10:25:39 +04:00
Ivan Molodetskikh f5e9b40140 tty: Check changes against pending connectors and mode
If we queued some DRM changes, they will be in pending. Also be more
resilient by removing unwrap.
2024-01-21 10:24:42 +04:00
Ivan Molodetskikh 5cacd03e85 Return error instead of broken screenshot for portal 2024-01-21 10:03:13 +04:00
17 changed files with 334 additions and 229 deletions
+21
View File
@@ -0,0 +1,21 @@
---
name: Bug report
about: Report a bug or a crash
title: ''
labels: bug
assignees: ''
---
<!-- Please describe the issue here at the top, then fill in the system information below. -->
### System Information
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
* niri version:
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
* GPU:
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
* CPU:
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
Generated
+49 -38
View File
@@ -1406,25 +1406,22 @@ dependencies = [
[[package]]
name = "input"
version = "0.8.3"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e74cd82cedcd66db78742a8337bdc48f188c4d2c12742cbc5cd85113f0b059"
checksum = "7911ce3db9c10c5ab4a35c49af778a5f9a827bd0f7371d9be56175d8dd2740d0"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.2",
"input-sys",
"io-lifetimes 1.0.11",
"libc",
"udev 0.7.0",
"udev",
]
[[package]]
name = "input-sys"
version = "1.17.0"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f6c2a17e8aba7217660e32863af87b0febad811d4b8620ef76b386603fddc2"
dependencies = [
"libc",
]
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
[[package]]
name = "instant"
@@ -1845,7 +1842,7 @@ dependencies = [
[[package]]
name = "niri"
version = "0.1.0-beta.1"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
@@ -1886,7 +1883,7 @@ dependencies = [
[[package]]
name = "niri-config"
version = "0.1.0-beta.1"
version = "0.1.0"
dependencies = [
"bitflags 2.4.2",
"knuffel",
@@ -1898,7 +1895,7 @@ dependencies = [
[[package]]
name = "niri-ipc"
version = "0.1.0-beta.1"
version = "0.1.0"
dependencies = [
"serde",
]
@@ -1974,7 +1971,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate 2.0.1",
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.48",
@@ -2125,6 +2122,12 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@@ -2188,6 +2191,24 @@ dependencies = [
"system-deps",
]
[[package]]
name = "pixman"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a24da0bec14f4e43a495c1837a3c358b87532e7fe66bd75c348b89f0451b6"
dependencies = [
"drm-fourcc",
"paste",
"pixman-sys",
"thiserror",
]
[[package]]
name = "pixman-sys"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a0483e89e81d7915defe83c51f23f6800594d64f6f4a21253ce87fd8444ada"
[[package]]
name = "pkg-config"
version = "0.3.29"
@@ -2301,9 +2322,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.76"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@@ -2459,13 +2480,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.2"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.3",
"regex-automata 0.4.4",
"regex-syntax 0.8.2",
]
@@ -2480,9 +2501,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
dependencies = [
"aho-corasick",
"memchr",
@@ -2658,9 +2679,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
@@ -2688,14 +2709,14 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smithay"
version = "0.3.0"
source = "git+https://github.com/Smithay/smithay.git#b7284bc6ca6afc782bd55a5c34ef3f902005951f"
source = "git+https://github.com/Smithay/smithay.git#8854dee7c2f49e9077f10d484b0de9a8e81c587c"
dependencies = [
"appendlist",
"bitflags 2.4.2",
@@ -2717,6 +2738,7 @@ dependencies = [
"libloading",
"libseat",
"once_cell",
"pixman",
"pkg-config",
"profiling",
"rand",
@@ -2726,7 +2748,7 @@ dependencies = [
"tempfile",
"thiserror",
"tracing",
"udev 0.8.0",
"udev",
"wayland-backend",
"wayland-egl",
"wayland-protocols",
@@ -2766,7 +2788,7 @@ dependencies = [
[[package]]
name = "smithay-drm-extras"
version = "0.1.0"
source = "git+https://github.com/Smithay/smithay.git#b7284bc6ca6afc782bd55a5c34ef3f902005951f"
source = "git+https://github.com/Smithay/smithay.git#8854dee7c2f49e9077f10d484b0de9a8e81c587c"
dependencies = [
"drm",
"edid-rs",
@@ -3062,17 +3084,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "udev"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a"
dependencies = [
"libc",
"libudev-sys",
"pkg-config",
]
[[package]]
name = "udev"
version = "0.8.0"
+5 -4
View File
@@ -1,5 +1,5 @@
[workspace.package]
version = "0.1.0-beta.1"
version = "0.1.0"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -49,8 +49,8 @@ keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.152"
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
logind-zbus = { version = "3.1.2", optional = true }
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
niri-config = { version = "0.1.0", path = "niri-config" }
niri-ipc = { version = "0.1.0", path = "niri-ipc" }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.18.0"
pipewire = { version = "0.7.2", optional = true }
@@ -80,6 +80,7 @@ features = [
"backend_winit",
"desktop",
"renderer_gl",
"renderer_pixman",
"renderer_multi",
"use_system_lib",
"wayland_frontend",
@@ -108,7 +109,7 @@ lto = "thin"
debug = false
[package.metadata.generate-rpm]
version = "0.1.0~beta.1"
version = "0.1.0"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+18 -8
View File
@@ -16,9 +16,12 @@ Opening a new window never causes existing windows to resize.
Every monitor has its own separate window strip.
Windows can never "overflow" onto an adjacent monitor.
Since windows go left-to-right horizontally, workspaces are arranged vertically.
Workspaces are dynamic and arranged vertically.
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Features
- Scrollable tiling
@@ -29,13 +32,17 @@ Every monitor has an independent set of workspaces, and there's always one empty
- Configurable layout: gaps, borders, struts, window sizes
- Live-reloading config
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
## Status
A lot of the essential functionality is implemented, plus some goodies on top.
Feel free to give niri a try.
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
Note that NVIDIA GPUs might have rendering issues.
## Inspiration
@@ -44,15 +51,14 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
## Building
> [!TIP]
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
>
> For NixOS users, check out https://github.com/sodiboo/niri-flake
> NixOS users, check out https://github.com/sodiboo/niri-flake
>
> For Arch users, there's an AUR package: https://aur.archlinux.org/packages/niri
First, install the dependencies for your distribution.
@@ -71,7 +77,9 @@ First, install the dependencies for your distribution.
sudo dnf install gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
```
Next, build niri with `cargo build --release`.
Next, get latest stable Rust: https://rustup.rs/
Then, build niri with `cargo build --release`.
### NixOS/Nix
@@ -202,4 +210,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
[PaperWM]: https://github.com/paperwm/PaperWM
[mako]: https://github.com/emersion/mako
[OBS]: https://flathub.org/apps/com.obsproject.Studio
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
+2 -2
View File
@@ -117,9 +117,9 @@ layout {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.333
proportion 0.33333
proportion 0.5
proportion 0.667
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
+40 -61
View File
@@ -20,7 +20,7 @@ use smithay::backend::drm::{
use smithay::backend::egl::context::ContextPriority;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -74,6 +74,8 @@ pub struct Tty {
// The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to
// the primary GPU.
primary_allocator: Option<DmabufAllocator<GbmAllocator<DrmDeviceFd>>>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
}
@@ -222,6 +224,7 @@ impl Tty {
devices: HashMap::new(),
dmabuf_global: None,
primary_allocator: None,
update_output_config_on_resume: false,
ipc_outputs: Rc::new(RefCell::new(HashMap::new())),
enabled_outputs: Arc::new(Mutex::new(HashMap::new())),
}
@@ -277,7 +280,7 @@ impl Tty {
self.libinput.suspend();
for device in self.devices.values() {
for device in self.devices.values_mut() {
device.drm.pause();
}
}
@@ -320,47 +323,13 @@ impl Tty {
device_list.remove(&node.dev_id());
// It hasn't been removed, update its state as usual.
let device = &self.devices[&node];
device.drm.activate();
// HACK: force reset the connectors to make resuming work across sleep.
let device = &self.devices[&node];
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(_conn, crtc)| crtc)
.collect();
for crtc in crtcs {
self.connector_disconnected(niri, node, crtc);
}
let device = self.devices.get_mut(&node).unwrap();
let _ = device.drm_scanner.scan_connectors(&device.drm);
let crtcs: Vec<_> = device
.drm_scanner
.crtcs()
.map(|(conn, crtc)| (conn.clone(), crtc))
.collect();
for (conn, crtc) in crtcs {
if let Err(err) = self.connector_connected(niri, node, conn, crtc) {
warn!("error connecting connector: {err:?}");
}
if let Err(err) = device.drm.activate(true) {
warn!("error activating DRM device: {err:?}");
}
// // Refresh the connectors.
// self.device_changed(node.dev_id(), niri);
// // Refresh the state on unchanged connectors.
// let device = self.devices.get_mut(&node).unwrap();
// for surface in device.surfaces.values_mut() {
// let compositor = &mut surface.compositor;
// if let Err(err) = compositor.surface().reset_state() {
// warn!("error resetting DRM surface state: {err}");
// }
// compositor.reset_buffers();
// }
// niri.queue_redraw_all();
// Refresh the connectors.
self.device_changed(node.dev_id(), niri);
}
// Add new devices.
@@ -370,7 +339,13 @@ impl Tty {
}
}
if self.update_output_config_on_resume {
self.on_output_config_changed(niri);
}
self.refresh_ipc_outputs();
niri.queue_redraw_all();
}
}
}
@@ -392,15 +367,9 @@ impl Tty {
let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;
let gbm = GbmDevice::new(device_fd)?;
let display = EGLDisplay::new(gbm.clone())?;
let display = unsafe { EGLDisplay::new(gbm.clone())? };
let egl_device = EGLDevice::device_for_display(&display)?;
// HACK: There's an issue in Smithay where the display created by GpuManager will be the
// same as the one we just created here, so when ours is dropped at the end of the scope,
// it will also close the long-lived display in GpuManager. Thus, we need to drop ours
// beforehand.
drop(display);
let render_node = egl_device
.try_get_render_node()?
.context("no render node")?;
@@ -735,13 +704,8 @@ impl Tty {
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
let surface = Surface {
name: output_name,
name: output_name.clone(),
compositor,
dmabuf_feedback,
vblank_frame: None,
@@ -755,6 +719,13 @@ impl Tty {
niri.add_output(output.clone(), Some(refresh_interval(mode)));
self.enabled_outputs
.lock()
.unwrap()
.insert(output_name, output.clone());
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
state.niri.activate_monitors(&state.backend);
@@ -794,6 +765,8 @@ impl Tty {
};
self.enabled_outputs.lock().unwrap().remove(&surface.name);
#[cfg(feature = "dbus")]
niri.on_enabled_outputs_changed();
}
fn on_vblank(
@@ -1039,7 +1012,7 @@ impl Tty {
// Hand them over to the DRM.
let drm_compositor = &mut surface.compositor;
match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) {
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4]) {
Ok(res) => {
if self
.config
@@ -1234,6 +1207,13 @@ impl Tty {
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::on_output_config_changed");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_output_config_on_resume = true;
return;
}
self.update_output_config_on_resume = false;
let mut to_disconnect = vec![];
let mut to_connect = vec![];
@@ -1255,12 +1235,11 @@ impl Tty {
}
// Check if we need to change the mode.
let connector = surface
.compositor
.current_connectors()
.into_iter()
.next()
.unwrap();
let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
else {
error!("surface pending connectors is empty");
continue;
};
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
@@ -1271,7 +1250,7 @@ impl Tty {
continue;
};
if surface.compositor.current_mode() == mode {
if surface.compositor.pending_mode() == mode {
continue;
}
+1 -1
View File
@@ -184,7 +184,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
+6 -20
View File
@@ -8,8 +8,7 @@ use std::sync::Mutex;
use anyhow::{anyhow, Context};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::texture::TextureBuffer;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
@@ -224,7 +223,7 @@ pub enum RenderCursor {
},
}
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
#[derive(Default)]
pub struct CursorTextureCache {
@@ -238,12 +237,11 @@ impl CursorTextureCache {
pub fn get(
&self,
renderer: &mut GlesRenderer,
icon: CursorIcon,
scale: i32,
cursor: &XCursor,
idx: usize,
) -> Option<TextureBuffer<GlesTexture>> {
) -> MemoryRenderBuffer {
self.cache
.borrow_mut()
.entry((icon, scale))
@@ -252,26 +250,14 @@ impl CursorTextureCache {
.frames()
.iter()
.map(|frame| {
let _span = tracy_client::span!("create TextureBuffer");
let buffer = TextureBuffer::from_memory(
renderer,
MemoryRenderBuffer::from_slice(
&frame.pixels_rgba,
Fourcc::Abgr8888,
Fourcc::Argb8888,
(frame.width as i32, frame.height as i32),
false,
scale,
Transform::Normal,
None,
);
match buffer {
Ok(x) => Some(x),
Err(err) => {
warn!("error creating a cursor texture: {err:?}");
None
}
}
)
})
.collect()
})[idx]
+3 -2
View File
@@ -5,7 +5,7 @@ use serde::Serialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo};
use zbus::{dbus_interface, fdo, SignalContext};
use super::Start;
@@ -112,7 +112,8 @@ impl DisplayConfig {
Ok((0, monitors, logical_monitors, HashMap::new()))
}
// FIXME: monitors-changed signal.
#[dbus_interface(signal)]
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
impl DisplayConfig {
+15 -1
View File
@@ -7,10 +7,11 @@ use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start;
use crate::utils::output_size;
#[derive(Clone)]
pub struct ScreenCast {
@@ -54,6 +55,13 @@ pub struct Stream {
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
}
#[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")]
struct StreamParameters {
/// Size of the stream in logical coordinates.
size: (i32, i32),
}
pub enum ScreenCastToNiri {
StartCast {
session_id: usize,
@@ -195,6 +203,12 @@ impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
#[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters {
let size = output_size(&self.output).into();
StreamParameters { size }
}
}
impl ScreenCast {
+1 -1
View File
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
+10
View File
@@ -13,6 +13,7 @@ use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::input_method::InputMethodSeat;
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::wlr_layer::Layer;
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
@@ -94,6 +95,15 @@ impl XdgShellHandler for State {
}
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
// a grab. It will likely need refactors in Smithay to support properly since grabs just
// replace each other.
// FIXME: do this properly.
if self.niri.seat.input_method().keyboard_grabbed() {
trace!("ignoring popup grab because IME has keyboard grabbed");
return;
}
let popup = PopupKind::Xdg(surface);
let Ok(root) = find_popup_root_surface(&popup) else {
return;
+1 -1
View File
@@ -338,7 +338,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
drop(cr);
let data = surface.take_data().unwrap();
let buffer = MemoryRenderBuffer::from_memory(
let buffer = MemoryRenderBuffer::from_slice(
&data,
Fourcc::Argb8888,
(width, height),
+1
View File
@@ -90,6 +90,7 @@ impl State {
TouchUp { .. } => (),
TouchCancel { .. } => (),
TouchFrame { .. } => (),
SwitchToggle { .. } => (),
Special(_) => (),
}
+139 -86
View File
@@ -13,12 +13,12 @@ use anyhow::Context;
use calloop::futures::Scheduler;
use niri_config::{Config, TrackLayout};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::utils::select_dmabuf_feedback;
use smithay::backend::renderer::element::utils::{select_dmabuf_feedback, RelocateRenderElement};
use smithay::backend::renderer::element::{
default_primary_scanout_output_compare, AsRenderElements, Element, Id, Kind, RenderElement,
RenderElementStates, UnderlyingStorage,
@@ -66,7 +66,7 @@ use smithay::wayland::compositor::{
};
use smithay::wayland::cursor_shape::CursorShapeManagerState;
use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::input_method::InputMethodManagerState;
use smithay::wayland::input_method::{InputMethodManagerState, InputMethodSeat};
use smithay::wayland::output::OutputManagerState;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState};
use smithay::wayland::pointer_gestures::PointerGesturesState;
@@ -90,9 +90,7 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
use crate::animation;
use crate::backend::tty::{SurfaceDmabufFeedback, TtyFrame, TtyRenderer, TtyRendererError};
use crate::backend::{Backend, RenderResult, Tty, Winit};
use crate::config_error_notification::{
ConfigErrorNotification, ConfigErrorNotificationRenderElement,
};
use crate::config_error_notification::ConfigErrorNotification;
use crate::cursor::{CursorManager, CursorTextureCache, RenderCursor, XCursor};
#[cfg(feature = "dbus")]
use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
@@ -106,7 +104,7 @@ use crate::input::{apply_libinput_settings, TabletData};
use crate::ipc::server::IpcServer;
use crate::layout::{Layout, MonitorRenderElement};
use crate::pw_utils::{Cast, PipeWire};
use crate::render_helpers::{NiriRenderer, PrimaryGpuTextureRenderElement};
use crate::render_helpers::NiriRenderer;
use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::{
center, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
@@ -326,7 +324,7 @@ impl State {
self.niri.cursor_manager.check_cursor_image_surface_alive();
self.niri.refresh_pointer_outputs();
self.niri.popups.cleanup();
self.niri.refresh_popup_grab();
self.refresh_popup_grab();
self.update_keyboard_focus();
self.refresh_pointer_focus();
@@ -420,6 +418,27 @@ impl State {
self.move_cursor(center(geo).to_f64());
}
pub fn refresh_popup_grab(&mut self) {
let keyboard_grabbed = self.niri.seat.input_method().keyboard_grabbed();
if let Some(grab) = &mut self.niri.popup_grab {
if grab.grab.has_ended() {
self.niri.popup_grab = None;
} else if keyboard_grabbed {
// HACK: remove popup grab if IME grabbed the keyboard, because we can't yet do
// popup grabs together with an IME grab.
// FIXME: do this properly.
grab.grab.ungrab(PopupUngrabStrategy::All);
self.niri.seat.get_pointer().unwrap().unset_grab(
self,
SERIAL_COUNTER.next_serial(),
get_monotonic_time().as_millis() as u32,
);
self.niri.popup_grab = None;
}
}
}
pub fn update_keyboard_focus(&mut self) {
let focus = if self.niri.is_locked() {
self.niri.lock_surface_focus()
@@ -1560,14 +1579,6 @@ impl Niri {
state.lock_surface.as_ref().map(|s| s.wl_surface()).cloned()
}
pub fn refresh_popup_grab(&mut self) {
if let Some(grab) = &self.popup_grab {
if grab.grab.has_ended() {
self.popup_grab = None;
}
}
}
/// Schedules an immediate redraw on all outputs if one is not already scheduled.
pub fn queue_redraw_all(&mut self) {
let outputs: Vec<_> = self.output_state.keys().cloned().collect();
@@ -1657,26 +1668,25 @@ impl Niri {
let pointer_pos =
(pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale);
let texture = self.cursor_texture_cache.get(
renderer.as_gles_renderer(),
icon,
scale,
&cursor,
idx,
);
let texture = self.cursor_texture_cache.get(icon, scale, &cursor, idx);
let mut pointer_elements = vec![];
if let Some(texture) = texture {
pointer_elements.push(OutputRenderElements::NamedPointer(
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
pointer_pos.to_f64(),
&texture,
None,
None,
None,
Kind::Cursor,
)),
));
let pointer_element = match MemoryRenderBufferRenderElement::from_buffer(
renderer,
pointer_pos.to_f64(),
&texture,
None,
None,
None,
Kind::Cursor,
) {
Ok(element) => Some(element),
Err(err) => {
warn!("error importing a cursor texture: {err:?}");
None
}
};
if let Some(element) = pointer_element {
pointer_elements.push(OutputRenderElements::NamedPointer(element));
}
(pointer_elements, pointer_pos)
@@ -2498,7 +2508,11 @@ impl Niri {
return;
}
let Some(default_output) = self.output_under_cursor() else {
let default_output = self
.output_under_cursor()
.or_else(|| self.layout.active_output().cloned())
.or_else(|| self.global_space.outputs().next().cloned());
let Some(default_output) = default_output else {
return;
};
@@ -2642,32 +2656,28 @@ impl Niri {
include_pointer: bool,
on_done: impl FnOnce(PathBuf) + Send + 'static,
) -> anyhow::Result<()> {
use std::cmp::max;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
let _span = tracy_client::span!("Niri::screenshot_all_outputs");
let mut elements = vec![];
let mut size = Size::from((0, 0));
let outputs: Vec<_> = self.global_space.outputs().cloned().collect();
for output in outputs {
let geom = self.global_space.output_geometry(&output).unwrap();
// FIXME: this does not work when outputs can have non-1 scale.
let geom = geom.to_physical(1);
size.w = max(size.w, geom.loc.x + geom.size.w);
size.h = max(size.h, geom.loc.y + geom.size.h);
// FIXME: support multiple outputs, needs fixing multi-scale handling and cropping.
anyhow::ensure!(outputs.len() == 1);
let output_elements = self.render::<GlesRenderer>(renderer, &output, include_pointer);
elements.extend(output_elements.into_iter().map(|elem| {
RelocateRenderElement::from_element(elem, geom.loc, Relocate::Relative)
}));
}
let output = outputs.into_iter().next().unwrap();
let geom = self.global_space.output_geometry(&output).unwrap();
// FIXME: scale.
let pixels = render_to_vec(renderer, size, Scale::from(1.), Fourcc::Abgr8888, &elements)?;
let output_scale = output.current_scale().integer_scale();
let geom = geom.to_physical(output_scale);
let size = geom.size;
let elements = self.render::<GlesRenderer>(renderer, &output, include_pointer);
let pixels = render_to_vec(
renderer,
size,
Scale::from(f64::from(output_scale)),
Fourcc::Abgr8888,
&elements,
)?;
let path = make_screenshot_path(&self.config.borrow())
.ok()
@@ -2763,6 +2773,44 @@ impl Niri {
constraint.activate();
});
}
#[cfg(feature = "dbus")]
pub fn on_enabled_outputs_changed(&self) {
let _span = tracy_client::span!("Niri::on_enabled_outputs_changed");
let Some(dbus) = &self.dbus else { return };
let Some(conn_display_config) = dbus.conn_display_config.clone() else {
return;
};
let res = thread::Builder::new()
.name("DisplayConfig MonitorsChanged Emitter".to_owned())
.spawn(move || {
use crate::dbus::mutter_display_config::DisplayConfig;
let _span = tracy_client::span!("MonitorsChanged");
let iface = match conn_display_config
.object_server()
.interface::<_, DisplayConfig>("/org/gnome/Mutter/DisplayConfig")
{
Ok(iface) => iface,
Err(err) => {
warn!("error getting DisplayConfig interface: {err:?}");
return;
}
};
async_io::block_on(async move {
if let Err(err) = DisplayConfig::monitors_changed(iface.signal_context()).await
{
warn!("error emitting MonitorsChanged: {err:?}");
}
});
});
if let Err(err) = res {
warn!("error spawning a thread to send MonitorsChanged: {err:?}");
}
}
}
pub struct ClientState {
@@ -2804,9 +2852,13 @@ fn render_to_texture(
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(scale);
element
.draw(&mut frame, src, dst, &[output_rect])
.context("error drawing element")?;
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
let sync_point = frame.finish().context("error finishing frame")?;
@@ -2869,9 +2921,13 @@ fn render_to_dmabuf(
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(scale);
element
.draw(&mut frame, src, dst, &[output_rect])
.context("error drawing element")?;
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
element
.draw(&mut frame, src, dst, &[damage])
.context("error drawing element")?;
}
}
let _sync_point = frame.finish().context("error finishing frame")?;
@@ -2884,10 +2940,11 @@ fn render_to_dmabuf(
pub enum OutputRenderElements<R: NiriRenderer> {
Monitor(MonitorRenderElement<R>),
Wayland(WaylandSurfaceRenderElement<R>),
NamedPointer(PrimaryGpuTextureRenderElement),
NamedPointer(MemoryRenderBufferRenderElement<R>),
SolidColor(SolidColorRenderElement),
ScreenshotUi(ScreenshotUiRenderElement),
ConfigErrorNotification(ConfigErrorNotificationRenderElement<R>),
// Used for the CPU-rendered panels.
RelocatedMemoryBuffer(RelocateRenderElement<MemoryRenderBufferRenderElement<R>>),
}
impl<R: NiriRenderer> Element for OutputRenderElements<R> {
@@ -2898,7 +2955,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.id(),
Self::SolidColor(elem) => elem.id(),
Self::ScreenshotUi(elem) => elem.id(),
Self::ConfigErrorNotification(elem) => elem.id(),
Self::RelocatedMemoryBuffer(elem) => elem.id(),
}
}
@@ -2909,7 +2966,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.current_commit(),
Self::SolidColor(elem) => elem.current_commit(),
Self::ScreenshotUi(elem) => elem.current_commit(),
Self::ConfigErrorNotification(elem) => elem.current_commit(),
Self::RelocatedMemoryBuffer(elem) => elem.current_commit(),
}
}
@@ -2920,7 +2977,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.geometry(scale),
Self::SolidColor(elem) => elem.geometry(scale),
Self::ScreenshotUi(elem) => elem.geometry(scale),
Self::ConfigErrorNotification(elem) => elem.geometry(scale),
Self::RelocatedMemoryBuffer(elem) => elem.geometry(scale),
}
}
@@ -2931,7 +2988,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.transform(),
Self::SolidColor(elem) => elem.transform(),
Self::ScreenshotUi(elem) => elem.transform(),
Self::ConfigErrorNotification(elem) => elem.transform(),
Self::RelocatedMemoryBuffer(elem) => elem.transform(),
}
}
@@ -2942,7 +2999,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.src(),
Self::SolidColor(elem) => elem.src(),
Self::ScreenshotUi(elem) => elem.src(),
Self::ConfigErrorNotification(elem) => elem.src(),
Self::RelocatedMemoryBuffer(elem) => elem.src(),
}
}
@@ -2957,7 +3014,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.damage_since(scale, commit),
Self::SolidColor(elem) => elem.damage_since(scale, commit),
Self::ScreenshotUi(elem) => elem.damage_since(scale, commit),
Self::ConfigErrorNotification(elem) => elem.damage_since(scale, commit),
Self::RelocatedMemoryBuffer(elem) => elem.damage_since(scale, commit),
}
}
@@ -2968,7 +3025,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.opaque_regions(scale),
Self::SolidColor(elem) => elem.opaque_regions(scale),
Self::ScreenshotUi(elem) => elem.opaque_regions(scale),
Self::ConfigErrorNotification(elem) => elem.opaque_regions(scale),
Self::RelocatedMemoryBuffer(elem) => elem.opaque_regions(scale),
}
}
@@ -2979,7 +3036,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.alpha(),
Self::SolidColor(elem) => elem.alpha(),
Self::ScreenshotUi(elem) => elem.alpha(),
Self::ConfigErrorNotification(elem) => elem.alpha(),
Self::RelocatedMemoryBuffer(elem) => elem.alpha(),
}
}
@@ -2990,7 +3047,7 @@ impl<R: NiriRenderer> Element for OutputRenderElements<R> {
Self::NamedPointer(elem) => elem.kind(),
Self::SolidColor(elem) => elem.kind(),
Self::ScreenshotUi(elem) => elem.kind(),
Self::ConfigErrorNotification(elem) => elem.kind(),
Self::RelocatedMemoryBuffer(elem) => elem.kind(),
}
}
}
@@ -3015,7 +3072,7 @@ impl RenderElement<GlesRenderer> for OutputRenderElements<GlesRenderer> {
Self::ScreenshotUi(elem) => {
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
}
Self::ConfigErrorNotification(elem) => elem.draw(frame, src, dst, damage),
Self::RelocatedMemoryBuffer(elem) => elem.draw(frame, src, dst, damage),
}
}
@@ -3026,7 +3083,7 @@ impl RenderElement<GlesRenderer> for OutputRenderElements<GlesRenderer> {
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
Self::SolidColor(elem) => elem.underlying_storage(renderer),
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
Self::ConfigErrorNotification(elem) => elem.underlying_storage(renderer),
Self::RelocatedMemoryBuffer(elem) => elem.underlying_storage(renderer),
}
}
}
@@ -3053,7 +3110,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
Self::ScreenshotUi(elem) => {
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
}
Self::ConfigErrorNotification(elem) => elem.draw(frame, src, dst, damage),
Self::RelocatedMemoryBuffer(elem) => elem.draw(frame, src, dst, damage),
}
}
@@ -3067,7 +3124,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
Self::NamedPointer(elem) => elem.underlying_storage(renderer),
Self::SolidColor(elem) => elem.underlying_storage(renderer),
Self::ScreenshotUi(elem) => elem.underlying_storage(renderer),
Self::ConfigErrorNotification(elem) => elem.underlying_storage(renderer),
Self::RelocatedMemoryBuffer(elem) => elem.underlying_storage(renderer),
}
}
}
@@ -3084,12 +3141,6 @@ impl<R: NiriRenderer> From<WaylandSurfaceRenderElement<R>> for OutputRenderEleme
}
}
impl<R: NiriRenderer> From<PrimaryGpuTextureRenderElement> for OutputRenderElements<R> {
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
Self::NamedPointer(x)
}
}
impl<R: NiriRenderer> From<SolidColorRenderElement> for OutputRenderElements<R> {
fn from(x: SolidColorRenderElement) -> Self {
Self::SolidColor(x)
@@ -3102,8 +3153,10 @@ impl<R: NiriRenderer> From<ScreenshotUiRenderElement> for OutputRenderElements<R
}
}
impl<R: NiriRenderer> From<ConfigErrorNotificationRenderElement<R>> for OutputRenderElements<R> {
fn from(x: ConfigErrorNotificationRenderElement<R>) -> Self {
Self::ConfigErrorNotification(x)
impl<R: NiriRenderer> From<RelocateRenderElement<MemoryRenderBufferRenderElement<R>>>
for OutputRenderElements<R>
{
fn from(x: RelocateRenderElement<MemoryRenderBufferRenderElement<R>>) -> Self {
Self::RelocatedMemoryBuffer(x)
}
}
+18 -4
View File
@@ -27,7 +27,18 @@ impl Watcher {
thread::Builder::new()
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
.spawn(move || {
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
// this "should" be as simple as mtime, but it does not quite work in practice;
// it doesn't work if the config is a symlink, and its target changes but the
// new target and old target have identical mtimes.
//
// in practice, this does not occur on any systems other than nix.
// because, on nix practically everything is a symlink to /nix/store
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
// so, symlink targets change frequently when mtime doesn't.
let mut last_props = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
.ok();
loop {
thread::sleep(Duration::from_millis(500));
@@ -36,8 +47,11 @@ impl Watcher {
break;
}
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
if last_mtime != Some(mtime) {
if let Ok(new_props) = path
.canonicalize()
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
{
if last_props.as_ref() != Some(&new_props) {
trace!("file changed: {}", path.to_string_lossy());
if let Err(err) = changed.send(()) {
@@ -45,7 +59,7 @@ impl Watcher {
break;
}
last_mtime = Some(mtime);
last_props = Some(new_props);
}
}
}