aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs64
-rw-r--r--niri-ipc/src/lib.rs153
-rw-r--r--src/backend/tty.rs2
-rw-r--r--src/cli.rs17
-rw-r--r--src/ipc/client.rs9
-rw-r--r--src/ipc/server.rs6
-rw-r--r--src/niri.rs161
7 files changed, 311 insertions, 101 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 39784c4e..4aa1edad 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -11,7 +11,7 @@ use bitflags::bitflags;
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
-use niri_ipc::{LayoutSwitchTarget, SizeChange, Transform};
+use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform};
use regex::Regex;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@@ -250,7 +250,7 @@ pub struct Output {
#[knuffel(child)]
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
- pub mode: Option<Mode>,
+ pub mode: Option<ConfiguredMode>,
#[knuffel(child)]
pub variable_refresh_rate: bool,
}
@@ -277,13 +277,6 @@ pub struct Position {
pub y: i32,
}
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Mode {
- pub width: u16,
- pub height: u16,
- pub refresh: Option<f64>,
-}
-
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Layout {
#[knuffel(child, default)]
@@ -1971,41 +1964,6 @@ where
}
}
-impl FromStr for Mode {
- type Err = miette::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let Some((width, rest)) = s.split_once('x') else {
- return Err(miette!("no 'x' separator found"));
- };
-
- let (height, refresh) = match rest.split_once('@') {
- Some((height, refresh)) => (height, Some(refresh)),
- None => (rest, None),
- };
-
- let width = width
- .parse()
- .into_diagnostic()
- .context("error parsing width")?;
- let height = height
- .parse()
- .into_diagnostic()
- .context("error parsing height")?;
- let refresh = refresh
- .map(str::parse)
- .transpose()
- .into_diagnostic()
- .context("error parsing refresh rate")?;
-
- Ok(Self {
- width,
- height,
- refresh,
- })
- }
-}
-
impl FromStr for Key {
type Err = miette::Error;
@@ -2336,7 +2294,7 @@ mod tests {
scale: 2.,
transform: Transform::Flipped90,
position: Some(Position { x: 10, y: 20 }),
- mode: Some(Mode {
+ mode: Some(ConfiguredMode {
width: 1920,
height: 1080,
refresh: Some(144.),
@@ -2574,8 +2532,8 @@ mod tests {
#[test]
fn parse_mode() {
assert_eq!(
- "2560x1600@165.004".parse::<Mode>().unwrap(),
- Mode {
+ "2560x1600@165.004".parse::<ConfiguredMode>().unwrap(),
+ ConfiguredMode {
width: 2560,
height: 1600,
refresh: Some(165.004),
@@ -2583,18 +2541,18 @@ mod tests {
);
assert_eq!(
- "1920x1080".parse::<Mode>().unwrap(),
- Mode {
+ "1920x1080".parse::<ConfiguredMode>().unwrap(),
+ ConfiguredMode {
width: 1920,
height: 1080,
refresh: None,
},
);
- assert!("1920".parse::<Mode>().is_err());
- assert!("1920x".parse::<Mode>().is_err());
- assert!("1920x1080@".parse::<Mode>().is_err());
- assert!("1920x1080@60Hz".parse::<Mode>().is_err());
+ assert!("1920".parse::<ConfiguredMode>().is_err());
+ assert!("1920x".parse::<ConfiguredMode>().is_err());
+ assert!("1920x1080@".parse::<ConfiguredMode>().is_err());
+ assert!("1920x1080@60Hz".parse::<ConfiguredMode>().is_err());
}
#[test]
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 74d04aa0..c721ac9b 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -20,6 +20,17 @@ pub enum Request {
FocusedWindow,
/// Perform an action.
Action(Action),
+ /// Change output configuration temporarily.
+ ///
+ /// The configuration is changed temporarily and not saved into the config file. If the output
+ /// configuration subsequently changes in the config file, these temporary changes will be
+ /// forgotten.
+ Output {
+ /// Output name.
+ output: String,
+ /// Configuration to apply.
+ action: OutputAction,
+ },
/// Respond with an error (for testing error handling).
ReturnError,
}
@@ -245,6 +256,103 @@ pub enum LayoutSwitchTarget {
Prev,
}
+/// Output actions that niri can perform.
+// Variants in this enum should match the spelling of the ones in niri-config. Most thigs from
+// niri-config should be present here.
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[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 OutputAction {
+ /// Turn off the output.
+ Off,
+ /// Turn on the output.
+ On,
+ /// Set the output mode.
+ Mode {
+ /// Mode to set, or "auto" for automatic selection.
+ ///
+ /// Run `niri msg outputs` to see the avaliable modes.
+ #[cfg_attr(feature = "clap", arg())]
+ mode: ModeToSet,
+ },
+ /// Set the output scale.
+ Scale {
+ /// Scale factor to set.
+ #[cfg_attr(feature = "clap", arg())]
+ scale: f64,
+ },
+ /// Set the output transform.
+ Transform {
+ /// Transform to set, counter-clockwise.
+ #[cfg_attr(feature = "clap", arg())]
+ transform: Transform,
+ },
+ /// Set the output position.
+ Position {
+ /// Position to set, or "auto" for automatic selection.
+ #[cfg_attr(feature = "clap", command(subcommand))]
+ position: PositionToSet,
+ },
+ /// Toggle variable refresh rate.
+ Vrr {
+ /// Whether to enable variable refresh rate.
+ #[cfg_attr(
+ feature = "clap",
+ arg(
+ value_name = "ON|OFF",
+ action = clap::ArgAction::Set,
+ value_parser = clap::builder::BoolishValueParser::new(),
+ ),
+ )]
+ enable: bool,
+ },
+}
+
+/// Output mode to set.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+pub enum ModeToSet {
+ /// Niri will pick the mode automatically.
+ Automatic,
+ /// Specific mode.
+ Specific(ConfiguredMode),
+}
+
+/// Output mode as set in the config file.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+pub struct ConfiguredMode {
+ /// Width in physical pixels.
+ pub width: u16,
+ /// Height in physical pixels.
+ pub height: u16,
+ /// Refresh rate.
+ pub refresh: Option<f64>,
+}
+
+/// Output position to set.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
+#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
+#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
+pub enum PositionToSet {
+ /// Position the output automatically.
+ #[cfg_attr(feature = "clap", command(name = "auto"))]
+ Automatic,
+ /// Set a specific position.
+ #[cfg_attr(feature = "clap", command(name = "set"))]
+ Specific(ConfiguredPosition),
+}
+
+/// Output position as set in the config file.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "clap", derive(clap::Args))]
+pub struct ConfiguredPosition {
+ /// Logical X position.
+ pub x: i32,
+ /// Logical Y position.
+ pub y: i32,
+}
+
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
@@ -304,6 +412,7 @@ pub struct LogicalOutput {
/// Output transform, which goes counter-clockwise.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum Transform {
/// Untransformed.
Normal,
@@ -319,10 +428,13 @@ pub enum Transform {
/// Flipped horizontally.
Flipped,
/// Rotated by 90° and flipped horizontally.
+ #[cfg_attr(feature = "clap", value(name("flipped-90")))]
Flipped90,
/// Flipped vertically.
+ #[cfg_attr(feature = "clap", value(name("flipped-180")))]
Flipped180,
/// Rotated by 270° and flipped horizontally.
+ #[cfg_attr(feature = "clap", value(name("flipped-270")))]
Flipped270,
}
@@ -407,3 +519,44 @@ impl FromStr for Transform {
}
}
}
+
+impl FromStr for ModeToSet {
+ type Err = &'static str;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.eq_ignore_ascii_case("auto") {
+ return Ok(Self::Automatic);
+ }
+
+ let mode = s.parse()?;
+ Ok(Self::Specific(mode))
+ }
+}
+
+impl FromStr for ConfiguredMode {
+ type Err = &'static str;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let Some((width, rest)) = s.split_once('x') else {
+ return Err("no 'x' separator found");
+ };
+
+ let (height, refresh) = match rest.split_once('@') {
+ Some((height, refresh)) => (height, Some(refresh)),
+ None => (rest, None),
+ };
+
+ let width = width.parse().map_err(|_| "error parsing width")?;
+ let height = height.parse().map_err(|_| "error parsing height")?;
+ let refresh = refresh
+ .map(str::parse)
+ .transpose()
+ .map_err(|_| "error parsing refresh rate")?;
+
+ Ok(Self {
+ width,
+ height,
+ refresh,
+ })
+ }
+}
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index 327a5c97..4e7055ee 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -2111,7 +2111,7 @@ fn queue_estimated_vblank_timer(
fn pick_mode(
connector: &connector::Info,
- target: Option<niri_config::Mode>,
+ target: Option<niri_ipc::ConfiguredMode>,
) -> Option<(control::Mode, bool)> {
let mut mode = None;
let mut fallback = false;
diff --git a/src/cli.rs b/src/cli.rs
index 78f9fc0e..65e2fd14 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -2,7 +2,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
-use niri_ipc::Action;
+use niri_ipc::{Action, OutputAction};
use crate::utils::version;
@@ -63,6 +63,21 @@ pub enum Msg {
#[command(subcommand)]
action: Action,
},
+ /// Change output configuration temporarily.
+ ///
+ /// The configuration is changed temporarily and not saved into the config file. If the output
+ /// configuration subsequently changes in the config file, these temporary changes will be
+ /// forgotten.
+ Output {
+ /// Output name.
+ ///
+ /// Run `niri msg outputs` to see the output names.
+ #[arg()]
+ output: String,
+ /// Configuration to apply.
+ #[command(subcommand)]
+ action: OutputAction,
+ },
/// Request an error from the running niri instance.
RequestError,
}
diff --git a/src/ipc/client.rs b/src/ipc/client.rs
index 1704adfb..3aa5eb22 100644
--- a/src/ipc/client.rs
+++ b/src/ipc/client.rs
@@ -11,6 +11,10 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::Outputs => Request::Outputs,
Msg::FocusedWindow => Request::FocusedWindow,
Msg::Action { action } => Request::Action(action.clone()),
+ Msg::Output { output, action } => Request::Output {
+ output: output.clone(),
+ action: action.clone(),
+ },
Msg::RequestError => Request::ReturnError,
};
@@ -237,6 +241,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
bail!("unexpected response: expected Handled, got {response:?}");
};
}
+ Msg::Output { .. } => {
+ let Response::Handled = response else {
+ bail!("unexpected response: expected Handled, got {response:?}");
+ };
+ }
}
Ok(())
diff --git a/src/ipc/server.rs b/src/ipc/server.rs
index 5e18c16a..59f929d8 100644
--- a/src/ipc/server.rs
+++ b/src/ipc/server.rs
@@ -169,6 +169,12 @@ fn process(ctx: &ClientCtx, request: Request) -> Reply {
});
Response::Handled
}
+ Request::Output { output, action } => {
+ ctx.event_loop.insert_idle(move |state| {
+ state.apply_transient_output_config(&output, action);
+ });
+ Response::Handled
+ }
};
Ok(response)
diff --git a/src/niri.rs b/src/niri.rs
index b1e2d917..b6bfb84d 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -138,6 +138,13 @@ const FRAME_CALLBACK_THROTTLE: Option<Duration> = Some(Duration::from_millis(995
pub struct Niri {
pub config: Rc<RefCell<Config>>,
+ /// Output config from the config file.
+ ///
+ /// This does not include transient output config changes done via IPC. It is only used when
+ /// reloading the config from disk to determine if the output configuration should be reloaded
+ /// (and transient changes dropped).
+ pub config_file_output_config: Vec<niri_config::Output>,
+
pub event_loop: LoopHandle<'static, State>,
pub scheduler: Scheduler<()>,
pub stop_signal: LoopSignal,
@@ -858,6 +865,7 @@ impl State {
let mut reload_xkb = None;
let mut libinput_config_changed = false;
let mut output_config_changed = false;
+ let mut preserved_output_config = None;
let mut window_rules_changed = false;
let mut debug_config_changed = false;
let mut shaders_changed = false;
@@ -894,8 +902,15 @@ impl State {
libinput_config_changed = true;
}
- if config.outputs != old_config.outputs {
+ if config.outputs != self.niri.config_file_output_config {
output_config_changed = true;
+ self.niri
+ .config_file_output_config
+ .clone_from(&config.outputs);
+ } else {
+ // Output config did not change from the last disk load, so we need to preserve the
+ // transient changes.
+ preserved_output_config = Some(mem::take(&mut old_config.outputs));
}
if config.binds != old_config.binds {
@@ -926,6 +941,10 @@ impl State {
*old_config = config;
+ if let Some(outputs) = preserved_output_config {
+ old_config.outputs = outputs;
+ }
+
// Release the borrow.
drop(old_config);
@@ -945,51 +964,7 @@ impl State {
}
if output_config_changed {
- let mut resized_outputs = vec![];
- for output in self.niri.global_space.outputs() {
- let name = output.name();
- let config = self.niri.config.borrow_mut();
- let config = config.outputs.iter().find(|o| o.name == name);
-
- let scale = config.map(|c| c.scale).unwrap_or_else(|| {
- let size_mm = output.physical_properties().size;
- let resolution = output.current_mode().unwrap().size;
- guess_monitor_scale(size_mm, resolution)
- });
- let scale = scale.clamp(1., 10.).ceil() as i32;
-
- let mut transform = config
- .map(|c| ipc_transform_to_smithay(c.transform))
- .unwrap_or(Transform::Normal);
- // FIXME: fix winit damage on other transforms.
- if name == "winit" {
- transform = Transform::Flipped180;
- }
-
- if output.current_scale().integer_scale() != scale
- || output.current_transform() != transform
- {
- output.change_current_state(
- None,
- Some(transform),
- Some(output::Scale::Integer(scale)),
- None,
- );
- self.niri.ipc_outputs_changed = true;
- resized_outputs.push(output.clone());
- }
- }
- for output in resized_outputs {
- self.niri.output_resized(&output);
- }
-
- self.backend.on_output_config_changed(&mut self.niri);
-
- self.niri.reposition_outputs(None);
-
- if let Some(touch) = self.niri.seat.get_touch() {
- touch.cancel(self);
- }
+ self.reload_output_config();
}
if debug_config_changed {
@@ -1032,6 +1007,98 @@ impl State {
self.niri.queue_redraw_all();
}
+ fn reload_output_config(&mut self) {
+ let mut resized_outputs = vec![];
+ for output in self.niri.global_space.outputs() {
+ let name = output.name();
+ let config = self.niri.config.borrow_mut();
+ let config = config.outputs.iter().find(|o| o.name == name);
+
+ let scale = config.map(|c| c.scale).unwrap_or_else(|| {
+ let size_mm = output.physical_properties().size;
+ let resolution = output.current_mode().unwrap().size;
+ guess_monitor_scale(size_mm, resolution)
+ });
+ let scale = scale.clamp(1., 10.).ceil() as i32;
+
+ let mut transform = config
+ .map(|c| ipc_transform_to_smithay(c.transform))
+ .unwrap_or(Transform::Normal);
+ // FIXME: fix winit damage on other transforms.
+ if name == "winit" {
+ transform = Transform::Flipped180;
+ }
+
+ if output.current_scale().integer_scale() != scale
+ || output.current_transform() != transform
+ {
+ output.change_current_state(
+ None,
+ Some(transform),
+ Some(output::Scale::Integer(scale)),
+ None,
+ );
+ self.niri.ipc_outputs_changed = true;
+ resized_outputs.push(output.clone());
+ }
+ }
+ for output in resized_outputs {
+ self.niri.output_resized(&output);
+ }
+
+ self.backend.on_output_config_changed(&mut self.niri);
+
+ self.niri.reposition_outputs(None);
+
+ if let Some(touch) = self.niri.seat.get_touch() {
+ touch.cancel(self);
+ }
+ }
+
+ pub fn apply_transient_output_config(&mut self, name: &str, action: niri_ipc::OutputAction) {
+ {
+ let mut config = self.niri.config.borrow_mut();
+ let config = if let Some(config) = config.outputs.iter_mut().find(|o| o.name == name) {
+ config
+ } else {
+ config.outputs.push(niri_config::Output {
+ name: String::from(name),
+ ..Default::default()
+ });
+ config.outputs.last_mut().unwrap()
+ };
+
+ match action {
+ niri_ipc::OutputAction::Off => config.off = true,
+ niri_ipc::OutputAction::On => config.off = false,
+ niri_ipc::OutputAction::Mode { mode } => {
+ config.mode = match mode {
+ niri_ipc::ModeToSet::Automatic => None,
+ niri_ipc::ModeToSet::Specific(mode) => Some(mode),
+ }
+ }
+ niri_ipc::OutputAction::Scale { scale } => config.scale = scale,
+ niri_ipc::OutputAction::Transform { transform } => config.transform = transform,
+ niri_ipc::OutputAction::Position { position } => {
+ config.position = match position {
+ niri_ipc::PositionToSet::Automatic => None,
+ niri_ipc::PositionToSet::Specific(position) => {
+ Some(niri_config::Position {
+ x: position.x,
+ y: position.y,
+ })
+ }
+ }
+ }
+ niri_ipc::OutputAction::Vrr { enable } => {
+ config.variable_refresh_rate = enable;
+ }
+ }
+ }
+
+ self.reload_output_config();
+ }
+
pub fn refresh_ipc_outputs(&mut self) {
if !self.niri.ipc_outputs_changed {
return;
@@ -1175,6 +1242,7 @@ impl Niri {
let display_handle = display.handle();
let config_ = config.borrow();
+ let config_file_output_config = config_.outputs.clone();
let layout = Layout::new(&config_);
@@ -1353,6 +1421,7 @@ impl Niri {
drop(config_);
Self {
config,
+ config_file_output_config,
event_loop,
scheduler,