diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-06-20 12:04:10 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-09-01 23:47:19 -0700 |
| commit | 30b213601a4f71d65a2227fa68ffb1ab2a69f671 (patch) | |
| tree | e68d9db212c6a4ac610ec0f80bb3e5db83950a67 /src | |
| parent | 8eb34b2e185aa0e0affea450226369cd3f9e6a78 (diff) | |
| download | niri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.tar.gz niri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.tar.bz2 niri-30b213601a4f71d65a2227fa68ffb1ab2a69f671.zip | |
Implement the event stream IPC
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.rs | 2 | ||||
| -rw-r--r-- | src/input/mod.rs | 17 | ||||
| -rw-r--r-- | src/ipc/client.rs | 64 | ||||
| -rw-r--r-- | src/ipc/server.rs | 416 | ||||
| -rw-r--r-- | src/layout/mod.rs | 64 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 11 | ||||
| -rw-r--r-- | src/niri.rs | 31 | ||||
| -rw-r--r-- | src/protocols/foreign_toplevel.rs | 2 |
8 files changed, 506 insertions, 101 deletions
@@ -88,6 +88,8 @@ pub enum Msg { }, /// Get the configured keyboard layouts. KeyboardLayouts, + /// Start continuously receiving events from the compositor. + EventStream, /// Print the version of the running niri instance. Version, /// Request an error from the running niri instance. diff --git a/src/input/mod.rs b/src/input/mod.rs index 6e7f203c..38827415 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -16,7 +16,7 @@ use smithay::backend::input::{ TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, }; use smithay::backend::libinput::LibinputInputBackend; -use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState}; +use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState, XkbContextHandler}; use smithay::input::pointer::{ AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, @@ -539,13 +539,18 @@ impl State { } } Action::SwitchLayout(action) => { - self.niri.seat.get_keyboard().unwrap().with_xkb_state( - self, - |mut state| match action { + let keyboard = &self.niri.seat.get_keyboard().unwrap(); + let new_idx = keyboard.with_xkb_state(self, |mut state| { + match action { LayoutSwitchTarget::Next => state.cycle_next_layout(), LayoutSwitchTarget::Prev => state.cycle_prev_layout(), - }, - ); + }; + state.active_layout().0 + }); + + if let Some(server) = &self.niri.ipc_server { + server.keyboard_layout_switched(new_idx as u8); + } } Action::MoveColumnLeft => { self.niri.layout.move_left(); diff --git a/src/ipc/client.rs b/src/ipc/client.rs index e9312933..ea6121dc 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, bail, Context}; use niri_ipc::{ - KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, Socket, - Transform, + Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, + Socket, Transform, }; use serde_json::json; @@ -21,12 +21,13 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { }, Msg::Workspaces => Request::Workspaces, Msg::KeyboardLayouts => Request::KeyboardLayouts, + Msg::EventStream => Request::EventStream, Msg::RequestError => Request::ReturnError, }; let socket = Socket::connect().context("error connecting to the niri socket")?; - let reply = socket + let (reply, mut read_event) = socket .send(request) .context("error communicating with niri")?; @@ -37,6 +38,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { Socket::connect() .and_then(|socket| socket.send(Request::Version)) .ok() + .map(|(reply, _read_event)| reply) } _ => None, }; @@ -261,6 +263,62 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { println!("{is_active}{idx} {name}"); } } + Msg::EventStream => { + let Response::Handled = response else { + bail!("unexpected response: expected Handled, got {response:?}"); + }; + + if !json { + println!("Started reading events."); + } + + loop { + let event = read_event().context("error reading event from niri")?; + + if json { + let event = serde_json::to_string(&event).context("error formatting event")?; + println!("{event}"); + continue; + } + + match event { + Event::WorkspacesChanged { workspaces } => { + println!("Workspaces changed: {workspaces:?}"); + } + Event::WorkspaceActivated { id, focused } => { + let word = if focused { "focused" } else { "activated" }; + println!("Workspace {word}: {id}"); + } + Event::WorkspaceActiveWindowChanged { + workspace_id, + active_window_id, + } => { + println!( + "Workspace {workspace_id}: \ + active window changed to {active_window_id:?}" + ); + } + Event::WindowsChanged { windows } => { + println!("Windows changed: {windows:?}"); + } + Event::WindowOpenedOrChanged { window } => { + println!("Window opened or changed: {window:?}"); + } + Event::WindowClosed { id } => { + println!("Window closed: {id}"); + } + Event::WindowFocusChanged { id } => { + println!("Window focus changed: {id:?}"); + } + Event::KeyboardLayoutsChanged { keyboard_layouts } => { + println!("Keyboard layouts changed: {keyboard_layouts:?}"); + } + Event::KeyboardLayoutSwitched { idx } => { + println!("Keyboard layout switched: {idx}"); + } + } + } + } } Ok(()) diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 5d2c8524..af2c4fa2 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -1,15 +1,20 @@ +use std::cell::RefCell; +use std::collections::HashSet; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::{env, io, process}; use anyhow::Context; +use async_channel::{Receiver, Sender, TrySendError}; +use calloop::futures::Scheduler; use calloop::io::Async; use directories::BaseDirs; use futures_util::io::{AsyncReadExt, BufReader}; -use futures_util::{AsyncBufReadExt, AsyncWriteExt}; -use niri_ipc::{KeyboardLayouts, OutputConfigChanged, Reply, Request, Response}; -use smithay::desktop::Window; +use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _}; +use niri_ipc::state::{EventStreamState, EventStreamStatePart as _}; +use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace}; use smithay::input::keyboard::XkbContextHandler; use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; @@ -18,17 +23,38 @@ use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; use crate::backend::IpcOutputMap; +use crate::layout::workspace::WorkspaceId; use crate::niri::State; use crate::utils::version; +use crate::window::Mapped; + +// If an event stream client fails to read events fast enough that we accumulate more than this +// number in our buffer, we drop that event stream client. +const EVENT_STREAM_BUFFER_SIZE: usize = 64; pub struct IpcServer { pub socket_path: PathBuf, + event_streams: Rc<RefCell<Vec<EventStreamSender>>>, + event_stream_state: Rc<RefCell<EventStreamState>>, } struct ClientCtx { event_loop: LoopHandle<'static, State>, + scheduler: Scheduler<()>, ipc_outputs: Arc<Mutex<IpcOutputMap>>, - ipc_focused_window: Arc<Mutex<Option<Window>>>, + event_streams: Rc<RefCell<Vec<EventStreamSender>>>, + event_stream_state: Rc<RefCell<EventStreamState>>, +} + +struct EventStreamClient { + events: Receiver<Event>, + disconnect: Receiver<()>, + write: Box<dyn AsyncWrite + Unpin>, +} + +struct EventStreamSender { + events: Sender<Event>, + disconnect: Sender<()>, } impl IpcServer { @@ -60,7 +86,43 @@ impl IpcServer { }) .unwrap(); - Ok(Self { socket_path }) + Ok(Self { + socket_path, + event_streams: Rc::new(RefCell::new(Vec::new())), + event_stream_state: Rc::new(RefCell::new(EventStreamState::default())), + }) + } + + fn send_event(&self, event: Event) { + let mut streams = self.event_streams.borrow_mut(); + let mut to_remove = Vec::new(); + for (idx, stream) in streams.iter_mut().enumerate() { + match stream.events.try_send(event.clone()) { + Ok(()) => (), + Err(TrySendError::Closed(_)) => to_remove.push(idx), + Err(TrySendError::Full(_)) => { + warn!( + "disconnecting IPC event stream client \ + because it is reading events too slowly" + ); + to_remove.push(idx); + } + } + } + + for idx in to_remove.into_iter().rev() { + let stream = streams.swap_remove(idx); + let _ = stream.disconnect.send_blocking(()); + } + } + + pub fn keyboard_layout_switched(&self, new_idx: u8) { + let mut state = self.event_stream_state.borrow_mut(); + let state = &mut state.keyboard_layouts; + + let event = Event::KeyboardLayoutSwitched { idx: new_idx }; + state.apply(event.clone()); + self.send_event(event); } } @@ -90,10 +152,14 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) { } }; + let ipc_server = state.niri.ipc_server.as_ref().unwrap(); + let ctx = ClientCtx { event_loop: state.niri.event_loop.clone(), + scheduler: state.niri.scheduler.clone(), ipc_outputs: state.backend.ipc_outputs(), - ipc_focused_window: state.niri.ipc_focused_window.clone(), + event_streams: ipc_server.event_streams.clone(), + event_stream_state: ipc_server.event_stream_state.clone(), }; let future = async move { @@ -106,7 +172,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) { } } -async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> { +async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> { let (read, mut write) = stream.split(); let mut buf = String::new(); @@ -120,6 +186,7 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow: .context("error parsing request") .map_err(|err| err.to_string()); let requested_error = matches!(request, Ok(Request::ReturnError)); + let requested_event_stream = matches!(request, Ok(Request::EventStream)); let reply = match request { Ok(request) => process(&ctx, request).await, @@ -136,6 +203,46 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow: buf.push(b'\n'); write.write_all(&buf).await.context("error writing reply")?; + if requested_event_stream { + let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE); + let (disconnect_tx, disconnect_rx) = async_channel::bounded(1); + + // Spawn a task for the client. + let client = EventStreamClient { + events: events_rx, + disconnect: disconnect_rx, + write: Box::new(write) as _, + }; + let future = async move { + if let Err(err) = handle_event_stream_client(client).await { + warn!("error handling IPC event stream client: {err:?}"); + } + }; + if let Err(err) = ctx.scheduler.schedule(future) { + warn!("error scheduling IPC event stream future: {err:?}"); + } + + // Send the initial state. + { + let state = ctx.event_stream_state.borrow(); + for event in state.replicate() { + events_tx + .try_send(event) + .expect("initial event burst had more events than buffer size"); + } + } + + // Add it to the list. + { + let mut streams = ctx.event_streams.borrow_mut(); + let sender = EventStreamSender { + events: events_tx, + disconnect: disconnect_tx, + }; + streams.push(sender); + } + } + Ok(()) } @@ -149,23 +256,9 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { Response::Outputs(outputs.collect()) } Request::FocusedWindow => { - let window = ctx.ipc_focused_window.lock().unwrap().clone(); - let window = window.map(|window| { - let wl_surface = window.toplevel().expect("no X11 support").wl_surface(); - with_states(wl_surface, |states| { - let role = states - .data_map - .get::<XdgToplevelSurfaceData>() - .unwrap() - .lock() - .unwrap(); - - niri_ipc::Window { - title: role.title.clone(), - app_id: role.app_id.clone(), - } - }) - }); + let state = ctx.event_stream_state.borrow(); + let windows = &state.windows.windows; + let window = windows.values().find(|win| win.is_focused).cloned(); Response::FocusedWindow(window) } Request::Action(action) => { @@ -202,13 +295,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { Response::OutputConfigChanged(response) } Request::Workspaces => { - let (tx, rx) = async_channel::bounded(1); - ctx.event_loop.insert_idle(move |state| { - let workspaces = state.niri.layout.ipc_workspaces(); - let _ = tx.send_blocking(workspaces); - }); - let result = rx.recv().await; - let workspaces = result.map_err(|_| String::from("error getting workspace info"))?; + let state = ctx.event_stream_state.borrow(); + let workspaces = state.workspaces.workspaces.values().cloned().collect(); Response::Workspaces(workspaces) } Request::FocusedOutput => { @@ -238,23 +326,261 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply { Response::FocusedOutput(output) } Request::KeyboardLayouts => { - let (tx, rx) = async_channel::bounded(1); - ctx.event_loop.insert_idle(move |state| { - let keyboard = state.niri.seat.get_keyboard().unwrap(); - let layout = keyboard.with_xkb_state(state, |context| { - let layouts = context.keymap().layouts(); - KeyboardLayouts { - names: layouts.map(str::to_owned).collect(), - current_idx: context.active_layout().0 as u8, - } - }); - let _ = tx.send_blocking(layout); - }); - let result = rx.recv().await; - let layout = result.map_err(|_| String::from("error getting layout info"))?; + let state = ctx.event_stream_state.borrow(); + let layout = state.keyboard_layouts.keyboard_layouts.clone(); + let layout = layout.expect("keyboard layouts should be set at startup"); Response::KeyboardLayouts(layout) } + Request::EventStream => Response::Handled, }; Ok(response) } + +async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> { + let EventStreamClient { + events, + disconnect, + mut write, + } = client; + + while let Ok(event) = events.recv().await { + let mut buf = serde_json::to_vec(&event).context("error formatting event")?; + buf.push(b'\n'); + + let res = select_biased! { + _ = disconnect.recv().fuse() => return Ok(()), + res = write.write_all(&buf).fuse() => res, + }; + + match res { + Ok(()) => (), + // Normal client disconnection. + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()), + res @ Err(_) => res.context("error writing event")?, + } + } + + Ok(()) +} + +fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window { + let wl_surface = mapped.toplevel().wl_surface(); + with_states(wl_surface, |states| { + let role = states + .data_map + .get::<XdgToplevelSurfaceData>() + .unwrap() + .lock() + .unwrap(); + + niri_ipc::Window { + id: u64::from(mapped.id().get()), + title: role.title.clone(), + app_id: role.app_id.clone(), + workspace_id: workspace_id.map(|id| u64::from(id.0)), + is_focused: mapped.is_focused(), + } + }) +} + +impl State { + pub fn ipc_keyboard_layouts_changed(&mut self) { + let keyboard = self.niri.seat.get_keyboard().unwrap(); + let keyboard_layouts = keyboard.with_xkb_state(self, |context| { + let layouts = context.keymap().layouts(); + KeyboardLayouts { + names: layouts.map(str::to_owned).collect(), + current_idx: context.active_layout().0 as u8, + } + }); + + let Some(server) = &self.niri.ipc_server else { + return; + }; + + let mut state = server.event_stream_state.borrow_mut(); + let state = &mut state.keyboard_layouts; + + let event = Event::KeyboardLayoutsChanged { keyboard_layouts }; + state.apply(event.clone()); + server.send_event(event); + } + + pub fn ipc_refresh_layout(&mut self) { + self.ipc_refresh_workspaces(); + self.ipc_refresh_windows(); + } + + fn ipc_refresh_workspaces(&mut self) { + let Some(server) = &self.niri.ipc_server else { + return; + }; + + let _span = tracy_client::span!("State::ipc_refresh_workspaces"); + + let mut state = server.event_stream_state.borrow_mut(); + let state = &mut state.workspaces; + + let mut events = Vec::new(); + let layout = &self.niri.layout; + let focused_ws_id = layout.active_workspace().map(|ws| u64::from(ws.id().0)); + + // Check for workspace changes. + let mut seen = HashSet::new(); + let mut need_workspaces_changed = false; + for (mon, ws_idx, ws) in layout.workspaces() { + let id = u64::from(ws.id().0); + seen.insert(id); + + let Some(ipc_ws) = state.workspaces.get(&id) else { + // A new workspace was added. + need_workspaces_changed = true; + break; + }; + + // Check for any changes that we can't signal as individual events. + let output_name = mon.map(|mon| mon.output_name()); + if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX) + || ipc_ws.name != ws.name + || ipc_ws.output.as_ref() != output_name + { + need_workspaces_changed = true; + break; + } + + let active_window_id = ws.active_window().map(|win| u64::from(win.id().get())); + if ipc_ws.active_window_id != active_window_id { + events.push(Event::WorkspaceActiveWindowChanged { + workspace_id: id, + active_window_id, + }); + } + + // Check if this workspace became focused. + let is_focused = Some(id) == focused_ws_id; + if is_focused && !ipc_ws.is_focused { + events.push(Event::WorkspaceActivated { id, focused: true }); + continue; + } + + // Check if this workspace became active. + let is_active = mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx); + if is_active && !ipc_ws.is_active { + events.push(Event::WorkspaceActivated { id, focused: false }); + } + } + + // Check if any workspaces were removed. + if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) { + need_workspaces_changed = true; + } + + if need_workspaces_changed { + events.clear(); + + let workspaces = layout + .workspaces() + .map(|(mon, ws_idx, ws)| { + let id = u64::from(ws.id().0); + Workspace { + id, + idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX), + name: ws.name.clone(), + output: mon.map(|mon| mon.output_name().clone()), + is_active: mon.map_or(false, |mon| mon.active_workspace_idx == ws_idx), + is_focused: Some(id) == focused_ws_id, + active_window_id: ws.active_window().map(|win| u64::from(win.id().get())), + } + }) + .collect(); + + events.push(Event::WorkspacesChanged { workspaces }); + } + + for event in events { + state.apply(event.clone()); + server.send_event(event); + } + } + + fn ipc_refresh_windows(&mut self) { + let Some(server) = &self.niri.ipc_server else { + return; + }; + + let _span = tracy_client::span!("State::ipc_refresh_windows"); + + let mut state = server.event_stream_state.borrow_mut(); + let state = &mut state.windows; + + let mut events = Vec::new(); + let layout = &self.niri.layout; + + // Check for window changes. + let mut seen = HashSet::new(); + let mut focused_id = None; + layout.with_windows(|mapped, _, ws_id| { + let id = u64::from(mapped.id().get()); + seen.insert(id); + + if mapped.is_focused() { + focused_id = Some(id); + } + + let Some(ipc_win) = state.windows.get(&id) else { + let window = make_ipc_window(mapped, Some(ws_id)); + events.push(Event::WindowOpenedOrChanged { window }); + return; + }; + + let workspace_id = Some(u64::from(ws_id.0)); + let mut changed = ipc_win.workspace_id != workspace_id; + + let wl_surface = mapped.toplevel().wl_surface(); + changed |= with_states(wl_surface, |states| { + let role = states + .data_map + .get::<XdgToplevelSurfaceData>() + .unwrap() + .lock() + .unwrap(); + + ipc_win.title != role.title || ipc_win.app_id != role.app_id + }); + + if changed { + let window = make_ipc_window(mapped, Some(ws_id)); + events.push(Event::WindowOpenedOrChanged { window }); + return; + } + + if mapped.is_focused() && !ipc_win.is_focused { + events.push(Event::WindowFocusChanged { id: Some(id) }); + } + }); + + // Check for closed windows. + let mut ipc_focused_id = None; + for (id, ipc_win) in &state.windows { + if !seen.contains(id) { + events.push(Event::WindowClosed { id: *id }); + } + + if ipc_win.is_focused { + ipc_focused_id = Some(id); + } + } + + // Extra check for focus becoming None, since the checks above only work for focus becoming + // a different window. + if focused_id.is_none() && ipc_focused_id.is_some() { + events.push(Event::WindowFocusChanged { id: None }); + } + + for event in events { + state.apply(event.clone()); + server.send_event(event); + } + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index ac46d5b8..20c63d28 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -42,6 +42,7 @@ use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::output::{self, Output}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform}; +use workspace::WorkspaceId; pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch}; @@ -1094,13 +1095,13 @@ impl<W: LayoutElement> Layout<W> { mon.workspaces.iter().flat_map(|ws| ws.windows()) } - pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>)) { + pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, WorkspaceId)) { match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mon.workspaces { for win in ws.windows() { - f(win, Some(&mon.output)); + f(win, Some(&mon.output), ws.id()); } } } @@ -1108,7 +1109,7 @@ impl<W: LayoutElement> Layout<W> { MonitorSet::NoOutputs { workspaces } => { for ws in workspaces { for win in ws.windows() { - f(win, None); + f(win, None, ws.id()); } } } @@ -2484,39 +2485,38 @@ impl<W: LayoutElement> Layout<W> { } } - pub fn ipc_workspaces(&self) -> Vec<niri_ipc::Workspace> { + pub fn workspaces( + &self, + ) -> impl Iterator<Item = (Option<&Monitor<W>>, usize, &Workspace<W>)> + '_ { + let iter_normal; + let iter_no_outputs; + match &self.monitor_set { - MonitorSet::Normal { - monitors, - primary_idx: _, - active_monitor_idx: _, - } => { - let mut workspaces = Vec::new(); - - for monitor in monitors { - for (idx, workspace) in monitor.workspaces.iter().enumerate() { - workspaces.push(niri_ipc::Workspace { - idx: u8::try_from(idx + 1).unwrap_or(u8::MAX), - name: workspace.name.clone(), - output: Some(monitor.output.name()), - is_active: monitor.active_workspace_idx == idx, - }) - } - } + MonitorSet::Normal { monitors, .. } => { + let it = monitors.iter().flat_map(|mon| { + mon.workspaces + .iter() + .enumerate() + .map(move |(idx, ws)| (Some(mon), idx, ws)) + }); - workspaces + iter_normal = Some(it); + iter_no_outputs = None; + } + MonitorSet::NoOutputs { workspaces } => { + let it = workspaces + .iter() + .enumerate() + .map(|(idx, ws)| (None, idx, ws)); + + iter_normal = None; + iter_no_outputs = Some(it); } - MonitorSet::NoOutputs { workspaces } => workspaces - .iter() - .enumerate() - .map(|(idx, ws)| niri_ipc::Workspace { - idx: u8::try_from(idx + 1).unwrap_or(u8::MAX), - name: ws.name.clone(), - output: None, - is_active: false, - }) - .collect(), } + + let iter_normal = iter_normal.into_iter().flatten(); + let iter_no_outputs = iter_no_outputs.into_iter().flatten(); + iter_normal.chain(iter_no_outputs) } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index c948bdf2..17ce9a33 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -123,7 +123,7 @@ pub struct OutputId(String); static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new(); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct WorkspaceId(u32); +pub struct WorkspaceId(pub u32); impl WorkspaceId { fn next() -> WorkspaceId { @@ -528,6 +528,15 @@ impl<W: LayoutElement> Workspace<W> { self.output.as_ref() } + pub fn active_window(&self) -> Option<&W> { + if self.columns.is_empty() { + return None; + } + + let col = &self.columns[self.active_column_idx]; + Some(col.tiles[col.active_tile_idx].window()) + } + pub fn set_output(&mut self, output: Option<Output>) { if self.output == output { return; diff --git a/src/niri.rs b/src/niri.rs index 7b70b9de..1a0669f1 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -291,7 +291,6 @@ pub struct Niri { pub ipc_server: Option<IpcServer>, pub ipc_outputs_changed: bool, - pub ipc_focused_window: Arc<Mutex<Option<Window>>>, // Casts are dropped before PipeWire to prevent a double-free (yay). pub casts: Vec<Cast>, @@ -502,7 +501,12 @@ impl State { let mut niri = Niri::new(config.clone(), event_loop, stop_signal, display, &backend); backend.init(&mut niri); - Ok(Self { backend, niri }) + let mut state = Self { backend, niri }; + + // Initialize some IPC server state. + state.ipc_keyboard_layouts_changed(); + + Ok(state) } pub fn refresh_and_flush_clients(&mut self) { @@ -538,6 +542,7 @@ impl State { foreign_toplevel::refresh(self); self.niri.refresh_window_rules(); self.refresh_ipc_outputs(); + self.ipc_refresh_layout(); #[cfg(feature = "xdp-gnome-screencast")] self.niri.refresh_mapped_cast_outputs(); @@ -839,8 +844,6 @@ impl State { focus ); - let mut newly_focused_window = None; - // Tell the windows their new focus state for window rule purposes. if let KeyboardFocus::Layout { surface: Some(surface), @@ -856,12 +859,9 @@ impl State { { if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { mapped.set_is_focused(true); - newly_focused_window = Some(mapped.window.clone()); } } - *self.niri.ipc_focused_window.lock().unwrap() = newly_focused_window; - if let Some(grab) = self.niri.popup_grab.as_mut() { if Some(&grab.root) != focus.surface() { trace!( @@ -911,6 +911,10 @@ impl State { keyboard.with_xkb_state(self, |mut context| { context.set_layout(new_layout); }); + + if let Some(server) = &self.niri.ipc_server { + server.keyboard_layout_switched(new_layout.0 as u8); + } } } @@ -1076,6 +1080,8 @@ impl State { if let Err(err) = keyboard.set_xkb_config(self, xkb.to_xkb_config()) { warn!("error updating xkb config: {err:?}"); } + + self.ipc_keyboard_layouts_changed(); } if libinput_config_changed { @@ -1372,7 +1378,7 @@ impl State { } StreamTargetId::Window { id } => { let mut window = None; - self.niri.layout.with_windows(|mapped, _| { + self.niri.layout.with_windows(|mapped, _, _| { if u64::from(mapped.id().get()) != id { return; } @@ -1489,7 +1495,7 @@ impl State { let mut windows = HashMap::new(); - self.niri.layout.with_windows(|mapped, _| { + self.niri.layout.with_windows(|mapped, _, _| { let wl_surface = mapped .window .toplevel() @@ -1843,7 +1849,6 @@ impl Niri { ipc_server, ipc_outputs_changed: false, - ipc_focused_window: Arc::new(Mutex::new(None)), pipewire, casts: vec![], @@ -2811,7 +2816,7 @@ impl Niri { let mut seen = HashSet::new(); let mut output_changed = vec![]; - self.layout.with_windows(|mapped, output| { + self.layout.with_windows(|mapped, output, _| { seen.insert(mapped.window.clone()); let Some(output) = output else { @@ -3510,7 +3515,7 @@ impl Niri { let frame_callback_time = get_monotonic_time(); - self.layout.with_windows(|mapped, _| { + self.layout.with_windows(|mapped, _, _| { mapped.window.send_frame( output, frame_callback_time, @@ -3753,7 +3758,7 @@ impl Niri { let _span = tracy_client::span!("Niri::render_window_for_screen_cast"); let mut window = None; |
