mirror of
https://github.com/YaLTeR/niri.git
synced 2024-10-27 04:07:59 +03:00
Compare commits
18 Commits
2a905e7eee
...
a4e044caae
Author | SHA1 | Date | |
---|---|---|---|
|
a4e044caae | ||
|
3e385d5c48 | ||
|
4bfa58cdba | ||
|
816533bd31 | ||
|
574c449418 | ||
|
1af47499af | ||
|
0f8c4939d7 | ||
|
2b8443b548 | ||
|
4fc02f22eb | ||
|
aef479c37a | ||
|
a870412e08 | ||
|
b78da7c060 | ||
|
afc6958add | ||
|
5d98a58ec0 | ||
|
5310794b0c | ||
|
ca5292f21f | ||
|
e666a0e724 | ||
|
d8ad6f2d56 |
@ -11,7 +11,7 @@ use bitflags::bitflags;
|
||||
use knuffel::errors::DecodeError;
|
||||
use knuffel::Decode as _;
|
||||
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
|
||||
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform};
|
||||
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
|
||||
use regex::Regex;
|
||||
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
|
||||
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
|
||||
@ -52,6 +52,8 @@ pub struct Config {
|
||||
pub binds: Binds,
|
||||
#[knuffel(child, default)]
|
||||
pub debug: DebugConfig,
|
||||
#[knuffel(children(name = "workspace"))]
|
||||
pub workspaces: Vec<Workspace>,
|
||||
}
|
||||
|
||||
// FIXME: Add other devices.
|
||||
@ -687,6 +689,17 @@ pub struct EnvironmentVariable {
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Workspace {
|
||||
#[knuffel(argument)]
|
||||
pub name: WorkspaceName,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub open_on_output: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceName(pub String);
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct WindowRule {
|
||||
#[knuffel(children(name = "match"))]
|
||||
@ -700,6 +713,8 @@ pub struct WindowRule {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub open_on_output: Option<String>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub open_on_workspace: Option<String>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub open_maximized: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub open_fullscreen: Option<bool>,
|
||||
@ -884,14 +899,14 @@ pub enum Action {
|
||||
CenterColumn,
|
||||
FocusWorkspaceDown,
|
||||
FocusWorkspaceUp,
|
||||
FocusWorkspace(#[knuffel(argument)] u8),
|
||||
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
|
||||
FocusWorkspacePrevious,
|
||||
MoveWindowToWorkspaceDown,
|
||||
MoveWindowToWorkspaceUp,
|
||||
MoveWindowToWorkspace(#[knuffel(argument)] u8),
|
||||
MoveWindowToWorkspace(#[knuffel(argument)] WorkspaceReference),
|
||||
MoveColumnToWorkspaceDown,
|
||||
MoveColumnToWorkspaceUp,
|
||||
MoveColumnToWorkspace(#[knuffel(argument)] u8),
|
||||
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
|
||||
MoveWorkspaceDown,
|
||||
MoveWorkspaceUp,
|
||||
FocusMonitorLeft,
|
||||
@ -956,14 +971,20 @@ impl From<niri_ipc::Action> for Action {
|
||||
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::FocusWorkspace { reference } => {
|
||||
Self::FocusWorkspace(WorkspaceReference::from(reference))
|
||||
}
|
||||
niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious,
|
||||
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
|
||||
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
|
||||
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
|
||||
niri_ipc::Action::MoveWindowToWorkspace { reference } => {
|
||||
Self::MoveWindowToWorkspace(WorkspaceReference::from(reference))
|
||||
}
|
||||
niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown,
|
||||
niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp,
|
||||
niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index),
|
||||
niri_ipc::Action::MoveColumnToWorkspace { reference } => {
|
||||
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference))
|
||||
}
|
||||
niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown,
|
||||
niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp,
|
||||
niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft,
|
||||
@ -996,6 +1017,59 @@ impl From<niri_ipc::Action> for Action {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum WorkspaceReference {
|
||||
Index(u8),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl From<WorkspaceReferenceArg> for WorkspaceReference {
|
||||
fn from(reference: WorkspaceReferenceArg) -> WorkspaceReference {
|
||||
match reference {
|
||||
WorkspaceReferenceArg::Index(i) => Self::Index(i),
|
||||
WorkspaceReferenceArg::Name(n) => Self::Name(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceReference {
|
||||
fn type_check(
|
||||
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) {
|
||||
if let Some(type_name) = &type_name {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
type_name,
|
||||
"type name",
|
||||
"no type name expected for this node",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_decode(
|
||||
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) -> Result<WorkspaceReference, DecodeError<S>> {
|
||||
match &**val {
|
||||
knuffel::ast::Literal::String(ref s) => Ok(WorkspaceReference::Name(s.clone().into())),
|
||||
knuffel::ast::Literal::Int(ref value) => match value.try_into() {
|
||||
Ok(v) => Ok(WorkspaceReference::Index(v)),
|
||||
Err(e) => {
|
||||
ctx.emit_error(DecodeError::conversion(val, e));
|
||||
Ok(WorkspaceReference::Index(0))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
ctx.emit_error(DecodeError::unsupported(
|
||||
val,
|
||||
"Unsupported value, only numbers and strings are recognized",
|
||||
));
|
||||
Ok(WorkspaceReference::Index(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||
pub struct DebugConfig {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
@ -1403,6 +1477,54 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceName {
|
||||
fn type_check(
|
||||
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) {
|
||||
if let Some(type_name) = &type_name {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
type_name,
|
||||
"type name",
|
||||
"no type name expected for this node",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_decode(
|
||||
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) -> Result<WorkspaceName, DecodeError<S>> {
|
||||
#[derive(Debug)]
|
||||
struct WorkspaceNameSet(HashSet<String>);
|
||||
match &**val {
|
||||
knuffel::ast::Literal::String(ref s) => {
|
||||
let mut name_set: HashSet<String> = match ctx.get::<WorkspaceNameSet>() {
|
||||
Some(h) => h.0.clone(),
|
||||
None => HashSet::new(),
|
||||
};
|
||||
if !name_set.insert(s.clone().to_string()) {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&val,
|
||||
"named workspace",
|
||||
format!("duplicate named workspace: {}", s),
|
||||
));
|
||||
return Ok(Self(String::new()));
|
||||
}
|
||||
ctx.set(WorkspaceNameSet(name_set));
|
||||
Ok(Self(s.clone().into()))
|
||||
}
|
||||
_ => {
|
||||
ctx.emit_error(DecodeError::unsupported(
|
||||
val,
|
||||
"workspace names must be strings",
|
||||
));
|
||||
Ok(Self(String::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> knuffel::Decode<S> for WindowOpenAnim
|
||||
where
|
||||
S: knuffel::traits::ErrorSpan,
|
||||
@ -2261,6 +2383,7 @@ mod tests {
|
||||
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+Shift+1 { focus-workspace "workspace-1"; }
|
||||
Mod+Shift+E { quit skip-confirmation=true; }
|
||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||
}
|
||||
@ -2268,6 +2391,12 @@ mod tests {
|
||||
debug {
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
}
|
||||
|
||||
workspace "workspace-1" {
|
||||
open-on-output "eDP-1"
|
||||
}
|
||||
workspace "workspace-2"
|
||||
workspace "workspace-3"
|
||||
"##,
|
||||
Config {
|
||||
input: Input {
|
||||
@ -2469,6 +2598,20 @@ mod tests {
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
workspaces: vec![
|
||||
Workspace {
|
||||
name: WorkspaceName("workspace-1".to_string()),
|
||||
open_on_output: Some("eDP-1".to_string()),
|
||||
},
|
||||
Workspace {
|
||||
name: WorkspaceName("workspace-2".to_string()),
|
||||
open_on_output: None,
|
||||
},
|
||||
Workspace {
|
||||
name: WorkspaceName("workspace-3".to_string()),
|
||||
open_on_output: None,
|
||||
},
|
||||
],
|
||||
binds: Binds(vec![
|
||||
Bind {
|
||||
key: Key {
|
||||
@ -2520,7 +2663,16 @@ mod tests {
|
||||
trigger: Trigger::Keysym(Keysym::_1),
|
||||
modifiers: Modifiers::COMPOSITOR,
|
||||
},
|
||||
action: Action::FocusWorkspace(1),
|
||||
action: Action::FocusWorkspace(WorkspaceReference::Index(1)),
|
||||
cooldown: None,
|
||||
allow_when_locked: false,
|
||||
},
|
||||
Bind {
|
||||
key: Key {
|
||||
trigger: Trigger::Keysym(Keysym::_1),
|
||||
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
|
||||
},
|
||||
action: Action::FocusWorkspace(WorkspaceReference::Name("workspace-1".to_string())),
|
||||
cooldown: None,
|
||||
allow_when_locked: false,
|
||||
},
|
||||
|
@ -146,11 +146,11 @@ pub enum Action {
|
||||
FocusWorkspaceDown,
|
||||
/// Focus the workspace above.
|
||||
FocusWorkspaceUp,
|
||||
/// Focus a workspace by index.
|
||||
/// Focus a workspace by reference (index or name).
|
||||
FocusWorkspace {
|
||||
/// Index of the workspace to focus.
|
||||
/// Reference (index or name) of the workspace to focus.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// Focus the previous workspace.
|
||||
FocusWorkspacePrevious,
|
||||
@ -158,21 +158,21 @@ pub enum Action {
|
||||
MoveWindowToWorkspaceDown,
|
||||
/// Move the focused window to the workspace above.
|
||||
MoveWindowToWorkspaceUp,
|
||||
/// Move the focused window to a workspace by index.
|
||||
/// Move the focused window to a workspace by reference (index or name).
|
||||
MoveWindowToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
/// Reference (index or name) of the workspace to move the window to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// 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.
|
||||
/// Move the focused column to a workspace by reference (index or name).
|
||||
MoveColumnToWorkspace {
|
||||
/// Index of the target workspace.
|
||||
/// Reference (index or name) of the workspace to move the column to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// Move the focused workspace down.
|
||||
MoveWorkspaceDown,
|
||||
@ -257,6 +257,15 @@ pub enum SizeChange {
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
/// Workspace reference (index or name) to operate on.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkspaceReferenceArg {
|
||||
/// Index of the workspace.
|
||||
Index(u8),
|
||||
/// Name of the workspace.
|
||||
Name(String),
|
||||
}
|
||||
|
||||
/// Layout to switch to.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LayoutSwitchTarget {
|
||||
@ -475,6 +484,24 @@ pub enum OutputConfigChanged {
|
||||
OutputWasMissing,
|
||||
}
|
||||
|
||||
impl FromStr for WorkspaceReferenceArg {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let reference = if let Ok(index) = s.parse::<i32>() {
|
||||
if let Ok(idx) = u8::try_from(index) {
|
||||
Self::Index(idx)
|
||||
} else {
|
||||
return Err("workspace indexes must be between 0 and 255");
|
||||
}
|
||||
} else {
|
||||
Self::Name(s.to_string())
|
||||
};
|
||||
|
||||
Ok(reference)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SizeChange {
|
||||
type Err = &'static str;
|
||||
|
||||
|
@ -119,22 +119,23 @@ impl CompositorHandler for State {
|
||||
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
|
||||
let (rules, width, is_full_width, output) =
|
||||
let (rules, width, is_full_width, output, workspace_name) =
|
||||
if let InitialConfigureState::Configured {
|
||||
rules,
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
} = state
|
||||
{
|
||||
// Check that the output is still connected.
|
||||
let output =
|
||||
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||
|
||||
(rules, width, is_full_width, output)
|
||||
(rules, width, is_full_width, output, workspace_name)
|
||||
} else {
|
||||
error!("window map must happen after initial configure");
|
||||
(ResolvedWindowRules::empty(), None, false, None)
|
||||
(ResolvedWindowRules::empty(), None, false, None, None)
|
||||
};
|
||||
|
||||
let parent = toplevel
|
||||
@ -160,6 +161,16 @@ impl CompositorHandler for State {
|
||||
self.niri
|
||||
.layout
|
||||
.add_window_right_of(&p, mapped, width, is_full_width)
|
||||
} else if let (Some(workspace_name), Some(output)) = (&workspace_name, &output)
|
||||
{
|
||||
self.niri.layout.add_window_to_named_workspace(
|
||||
output,
|
||||
workspace_name,
|
||||
mapped,
|
||||
width,
|
||||
is_full_width,
|
||||
);
|
||||
Some(output)
|
||||
} else if let Some(output) = &output {
|
||||
self.niri
|
||||
.layout
|
||||
|
@ -15,6 +15,7 @@ use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
@ -188,6 +189,10 @@ impl SelectionHandler for State {
|
||||
|
||||
let buf = user_data.clone();
|
||||
thread::spawn(move || {
|
||||
// Clear O_NONBLOCK, otherwise io::copy() will stop halfway.
|
||||
if let Err(err) = fcntl_setfl(&fd, OFlags::empty()) {
|
||||
warn!("error clearing flags on selection target fd: {err:?}");
|
||||
}
|
||||
if let Err(err) = File::from(fd).write_all(&buf) {
|
||||
warn!("error writing selection: {err:?}");
|
||||
}
|
||||
|
@ -369,38 +369,52 @@ impl XdgShellHandler for State {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name,
|
||||
} => {
|
||||
// Figure out the monitor following a similar logic to initial configure.
|
||||
// FIXME: deduplicate.
|
||||
let mon = output
|
||||
.as_ref()
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, false))
|
||||
// If not, check if we have a parent with a monitor.
|
||||
.or_else(|| {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
// If not, fall back to the active monitor.
|
||||
.or_else(|| {
|
||||
self.niri
|
||||
.layout
|
||||
.active_monitor_ref()
|
||||
.map(|mon| (mon, false))
|
||||
});
|
||||
let mon = workspace_name
|
||||
.as_deref()
|
||||
.and_then(|name| self.niri.layout.monitor_for_workspace(name))
|
||||
.map(|mon| (mon, false));
|
||||
|
||||
let mon = mon.or_else(|| {
|
||||
output
|
||||
.as_ref()
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, false))
|
||||
// If not, check if we have a parent with a monitor.
|
||||
.or_else(|| {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| {
|
||||
self.niri.layout.find_window_and_output(&parent)
|
||||
})
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
// If not, fall back to the active monitor.
|
||||
.or_else(|| {
|
||||
self.niri
|
||||
.layout
|
||||
.active_monitor_ref()
|
||||
.map(|mon| (mon, false))
|
||||
})
|
||||
});
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = mon
|
||||
.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace());
|
||||
let ws = workspace_name
|
||||
.as_deref()
|
||||
.and_then(|name| mon.map(|mon| mon.workspace_ref(name)))
|
||||
.unwrap_or_else(|| {
|
||||
mon.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace())
|
||||
});
|
||||
|
||||
if let Some(ws) = ws {
|
||||
toplevel.with_pending_state(|state| {
|
||||
@ -577,12 +591,20 @@ impl State {
|
||||
return;
|
||||
};
|
||||
|
||||
// Pick the target monitor. First, check if we had an output set in the window rules.
|
||||
// Pick the target monitor. First, check if we had a workspace set in the window rules.
|
||||
let mon = rules
|
||||
.open_on_output
|
||||
.open_on_workspace
|
||||
.as_deref()
|
||||
.and_then(|name| self.niri.output_by_name.get(name))
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o));
|
||||
.and_then(|name| self.niri.layout.monitor_for_workspace(name));
|
||||
|
||||
// If not, check if we had an output set in the window rules.
|
||||
let mon = mon.or_else(|| {
|
||||
rules
|
||||
.open_on_output
|
||||
.as_deref()
|
||||
.and_then(|name| self.niri.output_by_name.get(name))
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
});
|
||||
|
||||
// If not, check if the window requested one for fullscreen.
|
||||
let mon = mon.or_else(|| {
|
||||
@ -622,9 +644,14 @@ impl State {
|
||||
let is_full_width = rules.open_maximized.unwrap_or(false);
|
||||
|
||||
// Tell the surface the preferred size and bounds for its likely output.
|
||||
let ws = mon
|
||||
.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace());
|
||||
let ws = rules
|
||||
.open_on_workspace
|
||||
.as_deref()
|
||||
.and_then(|name| mon.map(|mon| mon.workspace_ref(name)))
|
||||
.unwrap_or_else(|| {
|
||||
mon.map(|mon| mon.active_workspace_ref())
|
||||
.or_else(|| self.niri.layout.active_workspace())
|
||||
});
|
||||
|
||||
if let Some(ws) = ws {
|
||||
// Set a fullscreen state based on window request and window rule.
|
||||
@ -663,6 +690,7 @@ impl State {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name: ws.and_then(|w| w.name.clone()),
|
||||
};
|
||||
|
||||
toplevel.send_configure();
|
||||
|
@ -587,12 +587,22 @@ impl State {
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::MoveWindowToWorkspace(idx) => {
|
||||
let idx = idx.saturating_sub(1) as usize;
|
||||
self.niri.layout.move_to_workspace(idx);
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
Action::MoveWindowToWorkspace(reference) => {
|
||||
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
|
||||
{
|
||||
if let Some(output) = output {
|
||||
self.niri.layout.move_to_workspace_on_output(&output, index);
|
||||
if !self.maybe_warp_cursor_to_focus_centered() {
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
} else {
|
||||
self.niri.layout.move_to_workspace(index);
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
Action::MoveColumnToWorkspaceDown => {
|
||||
self.niri.layout.move_column_to_workspace_down();
|
||||
@ -606,12 +616,24 @@ impl State {
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::MoveColumnToWorkspace(idx) => {
|
||||
let idx = idx.saturating_sub(1) as usize;
|
||||
self.niri.layout.move_column_to_workspace(idx);
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
Action::MoveColumnToWorkspace(reference) => {
|
||||
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
|
||||
{
|
||||
if let Some(output) = output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_column_to_workspace_on_output(&output, index);
|
||||
if !self.maybe_warp_cursor_to_focus_centered() {
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
} else {
|
||||
self.niri.layout.move_column_to_workspace(index);
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
Action::FocusWorkspaceDown => {
|
||||
self.niri.layout.switch_workspace_down();
|
||||
@ -625,19 +647,28 @@ impl State {
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::FocusWorkspace(idx) => {
|
||||
let idx = idx.saturating_sub(1) as usize;
|
||||
Action::FocusWorkspace(reference) => {
|
||||
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
|
||||
{
|
||||
if let Some(output) = output {
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.niri.layout.switch_workspace(index);
|
||||
if !self.maybe_warp_cursor_to_focus_centered() {
|
||||
self.move_cursor_to_output(&output);
|
||||
}
|
||||
} else {
|
||||
let config = &self.niri.config;
|
||||
if config.borrow().input.workspace_auto_back_and_forth {
|
||||
self.niri.layout.switch_workspace_auto_back_and_forth(index);
|
||||
} else {
|
||||
self.niri.layout.switch_workspace(index);
|
||||
}
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
let config = &self.niri.config;
|
||||
if config.borrow().input.workspace_auto_back_and_forth {
|
||||
self.niri.layout.switch_workspace_auto_back_and_forth(idx);
|
||||
} else {
|
||||
self.niri.layout.switch_workspace(idx);
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
// FIXME: granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::FocusWorkspacePrevious => {
|
||||
self.niri.layout.switch_workspace_previous();
|
||||
|
@ -34,7 +34,7 @@ use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, Config, Struts};
|
||||
use niri_config::{CenterFocusedColumn, Config, Struts, Workspace as WorkspaceConfig};
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
@ -278,7 +278,7 @@ impl Options {
|
||||
|
||||
impl<W: LayoutElement> Layout<W> {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
Self::with_options(Options::from_config(config))
|
||||
Self::with_options_and_workspaces(&config, Options::from_config(config))
|
||||
}
|
||||
|
||||
pub fn with_options(options: Options) -> Self {
|
||||
@ -288,6 +288,20 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
fn with_options_and_workspaces(config: &Config, options: Options) -> Self {
|
||||
let opts = Rc::new(options);
|
||||
|
||||
let workspaces = config.workspaces
|
||||
.iter()
|
||||
.map(|ws| Workspace::new_with_config_no_outputs(Some(ws.clone()), opts.clone()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
monitor_set: MonitorSet::NoOutputs { workspaces },
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_output(&mut self, output: Output) {
|
||||
let id = OutputId::new(&output);
|
||||
|
||||
@ -317,7 +331,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
// The user could've closed a window while remaining on this workspace, on
|
||||
// another monitor. However, we will add an empty workspace in the end
|
||||
// instead.
|
||||
if ws.has_windows() {
|
||||
if ws.has_windows() || ws.name.is_some() {
|
||||
workspaces.push(ws);
|
||||
}
|
||||
|
||||
@ -462,6 +476,56 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new window to the layout on a specific output & workspace.
|
||||
pub fn add_window_to_named_workspace(
|
||||
&mut self,
|
||||
output: &Output,
|
||||
workspace_name: &str,
|
||||
window: W,
|
||||
width: Option<ColumnWidth>,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
|
||||
if let ColumnWidth::Fixed(w) = &mut width {
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
if !border_config.off {
|
||||
*w += border_config.width as i32 * 2;
|
||||
}
|
||||
}
|
||||
|
||||
let MonitorSet::Normal {
|
||||
monitors,
|
||||
active_monitor_idx,
|
||||
..
|
||||
} = &mut self.monitor_set
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
let (mon_idx, mon) = monitors
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find(|(_, mon)| mon.output == *output)
|
||||
.unwrap();
|
||||
|
||||
let ws_idx = &mon
|
||||
.find_workspace_index(workspace_name)
|
||||
.unwrap();
|
||||
|
||||
// Don't steal focus from an active fullscreen window.
|
||||
let mut activate = true;
|
||||
let ws = &mon.workspaces[*ws_idx];
|
||||
if mon_idx == *active_monitor_idx
|
||||
&& !ws.columns.is_empty()
|
||||
&& ws.columns[ws.active_column_idx].is_fullscreen
|
||||
{
|
||||
activate = false;
|
||||
}
|
||||
|
||||
mon.add_window(*ws_idx, window, activate, width, is_full_width);
|
||||
}
|
||||
|
||||
pub fn add_column_by_idx(
|
||||
&mut self,
|
||||
monitor_idx: usize,
|
||||
@ -648,6 +712,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
&& idx != mon.active_workspace_idx
|
||||
&& idx != mon.workspaces.len() - 1
|
||||
&& mon.workspace_switch.is_none()
|
||||
&& mon.workspaces[idx].name.is_none()
|
||||
{
|
||||
mon.workspaces.remove(idx);
|
||||
|
||||
@ -667,7 +732,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
rv = Some(ws.remove_window(window));
|
||||
|
||||
// Clean up empty workspaces.
|
||||
if !ws.has_windows() {
|
||||
if !ws.has_windows() && workspaces[idx].name.is_none() {
|
||||
workspaces.remove(idx);
|
||||
}
|
||||
|
||||
@ -717,6 +782,35 @@ impl<W: LayoutElement> Layout<W> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_workspace_by_name(&self, workspace_name: &str) -> Option<(usize, &Workspace<W>)> {
|
||||
if let MonitorSet::Normal { ref monitors, .. } = &self.monitor_set {
|
||||
for mon in monitors {
|
||||
if let Some((index, workspace)) = mon
|
||||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, w)| w.name.as_deref() == Some(workspace_name))
|
||||
{
|
||||
return Some((index, workspace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn unname_workspace(&mut self, workspace_name: &str) {
|
||||
if let MonitorSet::Normal {
|
||||
ref mut monitors, ..
|
||||
} = &mut self.monitor_set
|
||||
{
|
||||
for mon in monitors {
|
||||
mon.unname_workspace(workspace_name);
|
||||
mon.clean_up_workspaces();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_window_and_output_mut(
|
||||
&mut self,
|
||||
wl_surface: &WlSurface,
|
||||
@ -969,6 +1063,19 @@ impl<W: LayoutElement> Layout<W> {
|
||||
monitors.iter().find(|monitor| &monitor.output == output)
|
||||
}
|
||||
|
||||
pub fn monitor_for_workspace(&self, workspace_name: &str) -> Option<&Monitor<W>> {
|
||||
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
|
||||
return None;
|
||||
};
|
||||
|
||||
monitors.iter().find(|monitor| {
|
||||
monitor
|
||||
.workspaces
|
||||
.iter()
|
||||
.any(|ws| ws.name.as_deref() == Some(workspace_name))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn outputs(&self) -> impl Iterator<Item = &Output> + '_ {
|
||||
let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
|
||||
&monitors[..]
|
||||
@ -1126,6 +1233,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
monitor.move_to_workspace(idx);
|
||||
}
|
||||
|
||||
pub fn move_to_workspace_on_output(&mut self, output: &Output, idx: usize) {
|
||||
self.move_to_output(&output);
|
||||
self.focus_output(&output);
|
||||
self.move_to_workspace(idx);
|
||||
}
|
||||
|
||||
pub fn move_column_to_workspace_up(&mut self) {
|
||||
let Some(monitor) = self.active_monitor() else {
|
||||
return;
|
||||
@ -1147,6 +1260,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
monitor.move_column_to_workspace(idx);
|
||||
}
|
||||
|
||||
pub fn move_column_to_workspace_on_output(&mut self, output: &Output, idx: usize) {
|
||||
self.move_to_output(&output);
|
||||
self.focus_output(&output);
|
||||
self.move_column_to_workspace(idx);
|
||||
}
|
||||
|
||||
pub fn switch_workspace_up(&mut self) {
|
||||
let Some(monitor) = self.active_monitor() else {
|
||||
return;
|
||||
@ -1256,6 +1375,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
use crate::layout::monitor::WorkspaceSwitch;
|
||||
|
||||
let mut seen_workspace_id = HashSet::new();
|
||||
let mut seen_workspace_name = HashSet::new();
|
||||
|
||||
let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set {
|
||||
MonitorSet::Normal {
|
||||
@ -1280,6 +1400,13 @@ impl<W: LayoutElement> Layout<W> {
|
||||
"workspace id must be unique"
|
||||
);
|
||||
|
||||
if let Some(name) = &workspace.name {
|
||||
assert!(
|
||||
seen_workspace_name.insert(name),
|
||||
"workspace name must be unique"
|
||||
);
|
||||
}
|
||||
|
||||
workspace.verify_invariants();
|
||||
}
|
||||
|
||||
@ -1342,6 +1469,11 @@ impl<W: LayoutElement> Layout<W> {
|
||||
"monitor must have an empty workspace in the end"
|
||||
);
|
||||
|
||||
assert!(
|
||||
monitor.workspaces.last().unwrap().name.is_none(),
|
||||
"monitor must have an unnamed workspace in the end"
|
||||
);
|
||||
|
||||
// If there's no workspace switch in progress, there can't be any non-last non-active
|
||||
// empty workspaces.
|
||||
if monitor.workspace_switch.is_none() {
|
||||
@ -1369,6 +1501,13 @@ impl<W: LayoutElement> Layout<W> {
|
||||
"workspace id must be unique"
|
||||
);
|
||||
|
||||
if let Some(name) = &workspace.name {
|
||||
assert!(
|
||||
seen_workspace_name.insert(name),
|
||||
"workspace name must be unique"
|
||||
);
|
||||
}
|
||||
|
||||
workspace.verify_invariants();
|
||||
}
|
||||
}
|
||||
@ -1429,6 +1568,29 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_workspace_for_output(
|
||||
&mut self,
|
||||
workspace_config: WorkspaceConfig,
|
||||
output: Option<Output>,
|
||||
config: &Config,
|
||||
) {
|
||||
let options = Rc::new(Options::from_config(config));
|
||||
|
||||
let output = output.or(self.active_output().cloned());
|
||||
|
||||
let ws = if let Some(output) = output {
|
||||
Workspace::new_with_config(output, Some(workspace_config), options)
|
||||
} else {
|
||||
Workspace::new_with_config_no_outputs(Some(workspace_config.clone()), options)
|
||||
};
|
||||
|
||||
let Some(mon) = self.active_monitor() else {
|
||||
return;
|
||||
};
|
||||
|
||||
mon.workspaces.push(ws);
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &Config) {
|
||||
let options = Rc::new(Options::from_config(config));
|
||||
|
||||
|
@ -103,6 +103,18 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
&self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
|
||||
pub fn workspace_ref(&self, workspace_name: &str) -> Option<&Workspace<W>> {
|
||||
self.workspaces
|
||||
.iter()
|
||||
.find(|w| w.name.as_deref() == Some(workspace_name))
|
||||
}
|
||||
|
||||
pub fn find_workspace_index(&self, workspace_name: &str) -> Option<usize> {
|
||||
self.workspaces
|
||||
.iter()
|
||||
.position(|w| w.name.as_deref() == Some(workspace_name))
|
||||
}
|
||||
|
||||
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
||||
&mut self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
@ -204,7 +216,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !self.workspaces[idx].has_windows() {
|
||||
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
|
||||
self.workspaces.remove(idx);
|
||||
if self.active_workspace_idx > idx {
|
||||
self.active_workspace_idx -= 1;
|
||||
@ -213,6 +225,17 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unname_workspace(&mut self, workspace_name: &str) {
|
||||
for idx in 0..self.workspaces.len() - 1 {
|
||||
if let Some(name) = &self.workspaces[idx].name {
|
||||
if name == workspace_name {
|
||||
self.workspaces[idx].unname();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
self.active_workspace().move_left();
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::iter::{self, zip};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
|
||||
use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig};
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::{layer_map_for_output, Window};
|
||||
@ -94,6 +94,9 @@ pub struct Workspace<W: LayoutElement> {
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
|
||||
/// Optional name of this workspace.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Unique ID of this workspace.
|
||||
id: WorkspaceId,
|
||||
}
|
||||
@ -313,9 +316,23 @@ impl TileData {
|
||||
|
||||
impl<W: LayoutElement> Workspace<W> {
|
||||
pub fn new(output: Output, options: Rc<Options>) -> Self {
|
||||
Self::new_with_config(output, None, options)
|
||||
}
|
||||
|
||||
pub fn new_with_config(
|
||||
output: Output,
|
||||
config: Option<WorkspaceConfig>,
|
||||
options: Rc<Options>,
|
||||
) -> Self {
|
||||
let original_output = config
|
||||
.clone()
|
||||
.and_then(|c| c.open_on_output.clone())
|
||||
.and_then(|c| Some(OutputId(c)))
|
||||
.unwrap_or(OutputId::new(&output));
|
||||
|
||||
let working_area = compute_working_area(&output, options.struts);
|
||||
Self {
|
||||
original_output: OutputId::new(&output),
|
||||
original_output,
|
||||
view_size: output_size(&output),
|
||||
working_area,
|
||||
output: Some(output),
|
||||
@ -329,14 +346,24 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
view_offset_before_fullscreen: None,
|
||||
closing_windows: vec![],
|
||||
options,
|
||||
name: config.and_then(|c| Some(c.name.clone().0)),
|
||||
id: WorkspaceId::next(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_outputs(options: Rc<Options>) -> Self {
|
||||
pub fn new_with_config_no_outputs(
|
||||
config: Option<WorkspaceConfig>,
|
||||
options: Rc<Options>,
|
||||
) -> Self {
|
||||
let original_output = OutputId(
|
||||
config
|
||||
.clone()
|
||||
.and_then(|c| c.open_on_output)
|
||||
.unwrap_or(String::new()),
|
||||
);
|
||||
Self {
|
||||
output: None,
|
||||
original_output: OutputId(String::new()),
|
||||
original_output,
|
||||
view_size: Size::from((1280, 720)),
|
||||
working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)),
|
||||
columns: vec![],
|
||||
@ -349,14 +376,23 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
view_offset_before_fullscreen: None,
|
||||
closing_windows: vec![],
|
||||
options,
|
||||
name: config.and_then(|c| Some(c.name.0)),
|
||||
id: WorkspaceId::next(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_outputs(options: Rc<Options>) -> Self {
|
||||
Self::new_with_config_no_outputs(None, options)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WorkspaceId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn unname(&mut self) {
|
||||
self.name = None;
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
|
||||
anim.set_current_time(current_time);
|
||||
@ -435,6 +471,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
.map(Tile::window_mut)
|
||||
}
|
||||
|
||||
pub fn get_output_name(&self) -> Option<String> {
|
||||
self.output.as_ref().and_then(|o| Some(o.name()))
|
||||
}
|
||||
|
||||
pub fn set_output(&mut self, output: Option<Output>) {
|
||||
if self.output == output {
|
||||
return;
|
||||
|
71
src/niri.rs
71
src/niri.rs
@ -11,7 +11,7 @@ use std::{env, mem, thread};
|
||||
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
|
||||
use anyhow::{ensure, Context};
|
||||
use calloop::futures::Scheduler;
|
||||
use niri_config::{Config, Key, Modifiers, PreviewRender, TrackLayout};
|
||||
use niri_config::{Config, Key, Modifiers, PreviewRender, TrackLayout, WorkspaceReference};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||
@ -857,6 +857,35 @@ impl State {
|
||||
|
||||
self.niri.config_error_notification.hide();
|
||||
|
||||
// Create new named workspaces
|
||||
for ws in &config.workspaces {
|
||||
if self
|
||||
.niri
|
||||
.layout
|
||||
.find_workspace_by_name(&ws.name.0)
|
||||
.is_none()
|
||||
{
|
||||
let output = ws
|
||||
.open_on_output
|
||||
.clone()
|
||||
.and_then(|name| self.niri.output_by_name(Some(&name)));
|
||||
self.niri
|
||||
.layout
|
||||
.new_workspace_for_output(ws.clone(), output, &config);
|
||||
}
|
||||
}
|
||||
|
||||
// Find & orphan removed named workspaces
|
||||
let mut removed_workspaces: Vec<String> = vec![];
|
||||
for ws in &self.niri.config.borrow().workspaces {
|
||||
if !config.workspaces.iter().any(|w| w.name == ws.name) {
|
||||
removed_workspaces.push(ws.name.0.clone());
|
||||
}
|
||||
}
|
||||
for name in removed_workspaces {
|
||||
self.niri.layout.unname_workspace(&name);
|
||||
}
|
||||
|
||||
self.niri.layout.update_config(&config);
|
||||
|
||||
let slowdown = if config.animations.off {
|
||||
@ -2087,6 +2116,46 @@ impl Niri {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn output_by_name(&self, name: Option<&str>) -> Option<Output> {
|
||||
let Some(name) = name else {
|
||||
return None;
|
||||
};
|
||||
self.global_space
|
||||
.outputs()
|
||||
.find(|output| output.name().eq_ignore_ascii_case(&name))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn find_output_and_workspace_index(
|
||||
&self,
|
||||
workspace_reference: WorkspaceReference,
|
||||
) -> Option<(Option<Output>, usize)> {
|
||||
let workspace_name = match workspace_reference {
|
||||
WorkspaceReference::Index(index) => {
|
||||
return Some((None, index.saturating_sub(1) as usize));
|
||||
}
|
||||
WorkspaceReference::Name(name) => name,
|
||||
};
|
||||
|
||||
let (target_workspace_index, target_workspace) =
|
||||
self.layout.find_workspace_by_name(&workspace_name)?;
|
||||
let Some(active_workspace) = self.layout.active_workspace() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if target_workspace.get_output_name() == active_workspace.get_output_name() {
|
||||
return Some((None, target_workspace_index));
|
||||
}
|
||||
|
||||
let Some(target_output) =
|
||||
self.output_by_name(target_workspace.get_output_name().as_deref())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((Some(target_output), target_workspace_index))
|
||||
}
|
||||
|
||||
pub fn output_down(&self) -> Option<Output> {
|
||||
let active = self.layout.active_output()?;
|
||||
let active_geo = self.global_space.output_geometry(active).unwrap();
|
||||
|
@ -33,6 +33,9 @@ pub struct ResolvedWindowRules {
|
||||
/// Output to open this window on.
|
||||
pub open_on_output: Option<String>,
|
||||
|
||||
/// Workspace to open this window on.
|
||||
pub open_on_workspace: Option<String>,
|
||||
|
||||
/// Whether the window should open full-width.
|
||||
pub open_maximized: Option<bool>,
|
||||
|
||||
@ -99,6 +102,7 @@ impl ResolvedWindowRules {
|
||||
Self {
|
||||
default_width: None,
|
||||
open_on_output: None,
|
||||
open_on_workspace: None,
|
||||
open_maximized: None,
|
||||
open_fullscreen: None,
|
||||
min_width: None,
|
||||
@ -151,6 +155,7 @@ impl ResolvedWindowRules {
|
||||
}
|
||||
|
||||
let mut open_on_output = None;
|
||||
let mut open_on_workspace = None;
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m| window_matches(window, &role, m);
|
||||
@ -175,6 +180,10 @@ impl ResolvedWindowRules {
|
||||
open_on_output = Some(x);
|
||||
}
|
||||
|
||||
if let Some(x) = rule.open_on_workspace.as_deref() {
|
||||
open_on_workspace = Some(x);
|
||||
}
|
||||
|
||||
if let Some(x) = rule.open_maximized {
|
||||
resolved.open_maximized = Some(x);
|
||||
}
|
||||
@ -217,6 +226,7 @@ impl ResolvedWindowRules {
|
||||
}
|
||||
|
||||
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
|
||||
resolved.open_on_workspace = open_on_workspace.map(|x| x.to_owned());
|
||||
});
|
||||
|
||||
resolved
|
||||
|
@ -42,6 +42,9 @@ pub enum InitialConfigureState {
|
||||
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
|
||||
/// should fetch the parent's current output again upon mapping.
|
||||
output: Option<Output>,
|
||||
|
||||
/// Workspace to open this window on.
|
||||
workspace_name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user