Implement niri msg action

This commit is contained in:
Ivan Molodetskikh 2024-02-10 09:33:32 +04:00
parent 3486fa5536
commit 93e16a6582
14 changed files with 366 additions and 74 deletions

2
Cargo.lock generated
View File

@ -2093,6 +2093,7 @@ dependencies = [
"bitflags 2.4.2",
"knuffel",
"miette",
"niri-ipc",
"smithay",
"tracing",
"tracy-client",
@ -2102,6 +2103,7 @@ dependencies = [
name = "niri-ipc"
version = "0.1.1"
dependencies = [
"clap",
"serde",
]

View File

@ -12,6 +12,7 @@ repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
anyhow = "1.0.79"
bitflags = "2.4.2"
clap = { version = "4.4.18", features = ["derive"] }
serde = { version = "1.0.196", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
@ -45,7 +46,7 @@ async-channel = { version = "2.1.1", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.2"
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
clap = { version = "4.4.18", features = ["derive", "string"] }
clap = { workspace = true, features = ["string"] }
directories = "5.0.1"
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
@ -55,7 +56,7 @@ libc = "0.2.153"
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.1", path = "niri-config" }
niri-ipc = { version = "0.1.1", path = "niri-ipc" }
niri-ipc = { version = "0.1.1", path = "niri-ipc", features = ["clap"] }
notify-rust = { version = "4.10.0", optional = true }
pangocairo = "0.19.1"
pipewire = { version = "0.7.2", optional = true }

View File

@ -11,6 +11,7 @@ repository.workspace = true
bitflags.workspace = true
knuffel = "3.2.0"
miette = "5.10.0"
niri-ipc = { version = "0.1.1", path = "../niri-ipc" }
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true

View File

@ -6,6 +6,7 @@ use std::str::FromStr;
use bitflags::bitflags;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{LayoutSwitchTarget, SizeChange};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::{Keysym, XkbConfig};
@ -512,6 +513,7 @@ bitflags! {
}
}
// Remember to add new actions to the CLI enum too.
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub enum Action {
Quit,
@ -578,7 +580,7 @@ pub enum Action {
SwitchPresetColumnWidth,
MaximizeColumn,
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
SwitchLayout(#[knuffel(argument)] LayoutAction),
SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget),
ShowHotkeyOverlay,
MoveWorkspaceToMonitorLeft,
MoveWorkspaceToMonitorRight,
@ -586,18 +588,76 @@ pub enum Action {
MoveWorkspaceToMonitorUp,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
SetFixed(i32),
SetProportion(f64),
AdjustFixed(i32),
AdjustProportion(f64),
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)]
pub enum LayoutAction {
Next,
Prev,
impl From<niri_ipc::Action> for Action {
fn from(value: niri_ipc::Action) -> Self {
match value {
niri_ipc::Action::Quit => Self::Quit,
niri_ipc::Action::PowerOffMonitors => Self::PowerOffMonitors,
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
niri_ipc::Action::Screenshot => Self::Screenshot,
niri_ipc::Action::ScreenshotScreen => Self::ScreenshotScreen,
niri_ipc::Action::ScreenshotWindow => Self::ScreenshotWindow,
niri_ipc::Action::CloseWindow => Self::CloseWindow,
niri_ipc::Action::FullscreenWindow => Self::FullscreenWindow,
niri_ipc::Action::FocusColumnLeft => Self::FocusColumnLeft,
niri_ipc::Action::FocusColumnRight => Self::FocusColumnRight,
niri_ipc::Action::FocusColumnFirst => Self::FocusColumnFirst,
niri_ipc::Action::FocusColumnLast => Self::FocusColumnLast,
niri_ipc::Action::FocusWindowDown => Self::FocusWindowDown,
niri_ipc::Action::FocusWindowUp => Self::FocusWindowUp,
niri_ipc::Action::FocusWindowOrWorkspaceDown => Self::FocusWindowOrWorkspaceDown,
niri_ipc::Action::FocusWindowOrWorkspaceUp => Self::FocusWindowOrWorkspaceUp,
niri_ipc::Action::MoveColumnLeft => Self::MoveColumnLeft,
niri_ipc::Action::MoveColumnRight => Self::MoveColumnRight,
niri_ipc::Action::MoveColumnToFirst => Self::MoveColumnToFirst,
niri_ipc::Action::MoveColumnToLast => Self::MoveColumnToLast,
niri_ipc::Action::MoveWindowDown => Self::MoveWindowDown,
niri_ipc::Action::MoveWindowUp => Self::MoveWindowUp,
niri_ipc::Action::MoveWindowDownOrToWorkspaceDown => {
Self::MoveWindowDownOrToWorkspaceDown
}
niri_ipc::Action::MoveWindowUpOrToWorkspaceUp => Self::MoveWindowUpOrToWorkspaceUp,
niri_ipc::Action::ConsumeOrExpelWindowLeft => Self::ConsumeOrExpelWindowLeft,
niri_ipc::Action::ConsumeOrExpelWindowRight => Self::ConsumeOrExpelWindowRight,
niri_ipc::Action::ConsumeWindowIntoColumn => Self::ConsumeWindowIntoColumn,
niri_ipc::Action::ExpelWindowFromColumn => Self::ExpelWindowFromColumn,
niri_ipc::Action::CenterColumn => Self::CenterColumn,
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown,
niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp,
niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index),
niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp,
niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft,
niri_ipc::Action::FocusMonitorRight => Self::FocusMonitorRight,
niri_ipc::Action::FocusMonitorDown => Self::FocusMonitorDown,
niri_ipc::Action::FocusMonitorUp => Self::FocusMonitorUp,
niri_ipc::Action::MoveWindowToMonitorLeft => Self::MoveWindowToMonitorLeft,
niri_ipc::Action::MoveWindowToMonitorRight => Self::MoveWindowToMonitorRight,
niri_ipc::Action::MoveWindowToMonitorDown => Self::MoveWindowToMonitorDown,
niri_ipc::Action::MoveWindowToMonitorUp => Self::MoveWindowToMonitorUp,
niri_ipc::Action::MoveColumnToMonitorLeft => Self::MoveColumnToMonitorLeft,
niri_ipc::Action::MoveColumnToMonitorRight => Self::MoveColumnToMonitorRight,
niri_ipc::Action::MoveColumnToMonitorDown => Self::MoveColumnToMonitorDown,
niri_ipc::Action::MoveColumnToMonitorUp => Self::MoveColumnToMonitorUp,
niri_ipc::Action::SetWindowHeight { change } => Self::SetWindowHeight(change),
niri_ipc::Action::SwitchPresetColumnWidth => Self::SwitchPresetColumnWidth,
niri_ipc::Action::MaximizeColumn => Self::MaximizeColumn,
niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change),
niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout),
niri_ipc::Action::ShowHotkeyOverlay => Self::ShowHotkeyOverlay,
niri_ipc::Action::MoveWorkspaceToMonitorLeft => Self::MoveWorkspaceToMonitorLeft,
niri_ipc::Action::MoveWorkspaceToMonitorRight => Self::MoveWorkspaceToMonitorRight,
niri_ipc::Action::MoveWorkspaceToMonitorDown => Self::MoveWorkspaceToMonitorDown,
niri_ipc::Action::MoveWorkspaceToMonitorUp => Self::MoveWorkspaceToMonitorUp,
niri_ipc::Action::ToggleDebugTint => Self::ToggleDebugTint,
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
@ -718,58 +778,6 @@ impl FromStr for Key {
}
}
impl FromStr for SizeChange {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err(miette!("trailing characters after '%' are not allowed"));
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err(miette!("value is missing")),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err(miette!("value is missing")),
}
}
}
}
}
impl FromStr for AccelProfile {
type Err = miette::Error;

View File

@ -8,4 +8,8 @@ edition.workspace = true
repository.workspace = true
[dependencies]
clap = { workspace = true, optional = true }
serde.workspace = true
[features]
clap = ["dep:clap"]

View File

@ -2,6 +2,7 @@
#![warn(missing_docs)]
use std::collections::HashMap;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
@ -13,6 +14,8 @@ pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
pub enum Request {
/// Request information about connected outputs.
Outputs,
/// Perform an action.
Action(Action),
}
/// Response from niri to client.
@ -24,6 +27,192 @@ pub enum Response {
Outputs(HashMap<String, Output>),
}
/// Actions that niri can perform.
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
// variants from niri-config should be present here.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
pub enum Action {
/// Exit niri.
Quit,
/// Power off all monitors via DPMS.
PowerOffMonitors,
/// Spawn a command.
Spawn {
/// Command to spawn.
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
command: Vec<String>,
},
/// Open the screenshot UI.
Screenshot,
/// Screenshot the focused screen.
ScreenshotScreen,
/// Screenshot the focused window.
ScreenshotWindow,
/// Close the focused window.
CloseWindow,
/// Toggle fullscreen on the focused window.
FullscreenWindow,
/// Focus the column to the left.
FocusColumnLeft,
/// Focus the column to the right.
FocusColumnRight,
/// Focus the first column.
FocusColumnFirst,
/// Focus the last column.
FocusColumnLast,
/// Focus the window below.
FocusWindowDown,
/// Focus the window above.
FocusWindowUp,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceDown,
/// Focus the window or the workspace above.
FocusWindowOrWorkspaceUp,
/// Move the focused column to the left.
MoveColumnLeft,
/// Move the focused column to the right.
MoveColumnRight,
/// Move the focused column to the start of the workspace.
MoveColumnToFirst,
/// Move the focused column to the end of the workspace.
MoveColumnToLast,
/// Move the focused window down in a column.
MoveWindowDown,
/// Move the focused window up in a column.
MoveWindowUp,
/// Move the focused window down in a column or to the workspace below.
MoveWindowDownOrToWorkspaceDown,
/// Move the focused window up in a column or to the workspace above.
MoveWindowUpOrToWorkspaceUp,
/// Consume or expel the focused window left.
ConsumeOrExpelWindowLeft,
/// Consume or expel the focused window right.
ConsumeOrExpelWindowRight,
/// Consume the window to the right into the focused column.
ConsumeWindowIntoColumn,
/// Expel the focused window from the column.
ExpelWindowFromColumn,
/// Center the focused column on the screen.
CenterColumn,
/// Focus the workspace below.
FocusWorkspaceDown,
/// Focus the workspace above.
FocusWorkspaceUp,
/// Focus a workspace by index.
FocusWorkspace {
/// Index of the workspace to focus.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused window to the workspace below.
MoveWindowToWorkspaceDown,
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by index.
MoveWindowToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
/// Move the focused column to a workspace by index.
MoveColumnToWorkspace {
/// Index of the target workspace.
#[cfg_attr(feature = "clap", arg())]
index: u8,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
/// Move the focused workspace up.
MoveWorkspaceUp,
/// Focus the monitor to the left.
FocusMonitorLeft,
/// Focus the monitor to the right.
FocusMonitorRight,
/// Focus the monitor below.
FocusMonitorDown,
/// Focus the monitor above.
FocusMonitorUp,
/// Move the focused window to the monitor to the left.
MoveWindowToMonitorLeft,
/// Move the focused window to the monitor to the right.
MoveWindowToMonitorRight,
/// Move the focused window to the monitor below.
MoveWindowToMonitorDown,
/// Move the focused window to the monitor above.
MoveWindowToMonitorUp,
/// Move the focused column to the monitor to the left.
MoveColumnToMonitorLeft,
/// Move the focused column to the monitor to the right.
MoveColumnToMonitorRight,
/// Move the focused column to the monitor below.
MoveColumnToMonitorDown,
/// Move the focused column to the monitor above.
MoveColumnToMonitorUp,
/// Change the height of the focused window.
SetWindowHeight {
/// How to change the height.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between preset column widths.
SwitchPresetColumnWidth,
/// Toggle the maximized state of the focused column.
MaximizeColumn,
/// Change the width of the focused column.
SetColumnWidth {
/// How to change the width.
#[cfg_attr(feature = "clap", arg())]
change: SizeChange,
},
/// Switch between keyboard layouts.
SwitchLayout {
/// Layout to switch to.
#[cfg_attr(feature = "clap", arg())]
layout: LayoutSwitchTarget,
},
/// Show the hotkey overlay.
ShowHotkeyOverlay,
/// Move the focused workspace to the monitor to the left.
MoveWorkspaceToMonitorLeft,
/// Move the focused workspace to the monitor to the right.
MoveWorkspaceToMonitorRight,
/// Move the focused workspace to the monitor below.
MoveWorkspaceToMonitorDown,
/// Move the focused workspace to the monitor above.
MoveWorkspaceToMonitorUp,
/// Toggle a debug tint on windows.
ToggleDebugTint,
}
/// Change in window or column size.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
/// Set the size in logical pixels.
SetFixed(i32),
/// Set the size as a proportion of the working area.
SetProportion(f64),
/// Add or subtract to the current size in logical pixels.
AdjustFixed(i32),
/// Add or subtract to the current size as a proportion of the working area.
AdjustProportion(f64),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum LayoutSwitchTarget {
/// The next configured layout.
Next,
/// The previous configured layout.
Prev,
}
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
@ -53,3 +242,55 @@ pub struct Mode {
/// Refresh rate in millihertz.
pub refresh_rate: u32,
}
impl FromStr for SizeChange {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err("trailing characters after '%' are not allowed");
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err("value is missing"),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value.parse().map_err(|_| "error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err("value is missing"),
}
}
}
}
}
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"next" => Ok(Self::Next),
"prev" => Ok(Self::Prev),
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
}
}
}

View File

@ -251,6 +251,9 @@ binds {
//
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// when running as a winit window.
//
// Most actions that you can bind here can also be invoked programmatically with
// `niri msg action do-something`.
// Mod-Shift-/, which is usually the same as Mod-?,
// shows a list of important hotkeys.

View File

@ -2,6 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use niri_ipc::Action;
use crate::utils::version;
@ -46,4 +47,9 @@ pub enum Sub {
pub enum Msg {
/// List connected outputs.
Outputs,
/// Perform an action.
Action {
#[command(subcommand)]
action: Action,
},
}

View File

@ -1,7 +1,8 @@
use std::any::Any;
use std::collections::HashSet;
use niri_config::{Action, Binds, LayoutAction, Modifiers};
use niri_config::{Action, Binds, Modifiers};
use niri_ipc::LayoutSwitchTarget;
use smithay::backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event,
GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _,
@ -273,6 +274,10 @@ impl State {
return;
}
self.do_action(action);
}
pub fn do_action(&mut self, action: Action) {
if self.niri.is_locked() && !allowed_when_locked(&action) {
return;
}
@ -377,8 +382,8 @@ impl State {
self.niri.seat.get_keyboard().unwrap().with_xkb_state(
self,
|mut state| match action {
LayoutAction::Next => state.cycle_next_layout(),
LayoutAction::Prev => state.cycle_prev_layout(),
LayoutSwitchTarget::Next => state.cycle_next_layout(),
LayoutSwitchTarget::Prev => state.cycle_prev_layout(),
},
);
}

View File

@ -19,8 +19,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let mut stream =
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
let request = match msg {
let request = match &msg {
Msg::Outputs => Request::Outputs,
Msg::Action { action } => Request::Action(action.clone()),
};
let mut buf = serde_json::to_vec(&request).unwrap();
stream
@ -35,6 +36,14 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
.read_to_end(&mut buf)
.context("error reading IPC response")?;
if matches!(msg, Msg::Action { .. }) {
if buf.is_empty() {
return Ok(());
} else {
bail!("unexpected response: expected no response, got {buf:?}");
}
}
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
match msg {
Msg::Outputs => {
@ -100,6 +109,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!();
}
}
Msg::Action { .. } => unreachable!(),
}
Ok(())

View File

@ -22,6 +22,7 @@ pub struct IpcServer {
}
struct ClientCtx {
event_loop: LoopHandle<'static, State>,
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
}
@ -85,6 +86,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
};
let ctx = ClientCtx {
event_loop: state.niri.event_loop.clone(),
ipc_outputs: state.backend.ipc_outputs(),
};
@ -115,6 +117,13 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
Response::Outputs(ipc_outputs)
}
Request::Action(action) => {
let action = niri_config::Action::from(action);
ctx.event_loop.insert_idle(move |state| {
state.do_action(action);
});
return Ok(());
}
};
let buf = serde_json::to_vec(&response).context("error formatting response")?;

View File

@ -34,7 +34,8 @@ use std::mem;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{self, CenterFocusedColumn, Config, SizeChange, Struts};
use niri_config::{self, CenterFocusedColumn, Config, Struts};
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{AsRenderElements, Id};

View File

@ -2,7 +2,7 @@ use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;
use niri_config::SizeChange;
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::utils::{
CropRenderElement, Relocate, RelocateRenderElement,
};

View File

@ -3,7 +3,8 @@ use std::iter::{self, zip};
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, SizeChange, Struts};
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
use niri_ipc::SizeChange;
use smithay::desktop::space::SpaceElement;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;