mirror of
https://github.com/YaLTeR/niri.git
synced 2024-09-19 16:27:23 +03:00
Implement niri msg output
This commit is contained in:
parent
2dff674470
commit
65b9c74f62
@ -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]
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
17
src/cli.rs
17
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,
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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)
|
||||
|
139
src/niri.rs
139
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,6 +964,50 @@ impl State {
|
||||
}
|
||||
|
||||
if output_config_changed {
|
||||
self.reload_output_config();
|
||||
}
|
||||
|
||||
if debug_config_changed {
|
||||
self.backend.on_debug_config_changed();
|
||||
}
|
||||
|
||||
if window_rules_changed {
|
||||
let _span = tracy_client::span!("recompute window rules");
|
||||
|
||||
let window_rules = &self.niri.config.borrow().window_rules;
|
||||
|
||||
for unmapped in self.niri.unmapped_windows.values_mut() {
|
||||
let new_rules =
|
||||
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
|
||||
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
|
||||
*rules = new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
let mut windows = vec![];
|
||||
self.niri.layout.with_windows_mut(|mapped, _| {
|
||||
if mapped.recompute_window_rules(window_rules) {
|
||||
windows.push(mapped.window.clone());
|
||||
}
|
||||
});
|
||||
for win in windows {
|
||||
self.niri.layout.update_window(&win);
|
||||
}
|
||||
}
|
||||
|
||||
if shaders_changed {
|
||||
self.niri.layout.update_shaders();
|
||||
}
|
||||
|
||||
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
|
||||
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
|
||||
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
|
||||
// clients will use the new xdg-decoration setting.
|
||||
|
||||
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();
|
||||
@ -992,44 +1055,48 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if debug_config_changed {
|
||||
self.backend.on_debug_config_changed();
|
||||
}
|
||||
|
||||
if window_rules_changed {
|
||||
let _span = tracy_client::span!("recompute window rules");
|
||||
|
||||
let window_rules = &self.niri.config.borrow().window_rules;
|
||||
|
||||
for unmapped in self.niri.unmapped_windows.values_mut() {
|
||||
let new_rules =
|
||||
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
|
||||
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
|
||||
*rules = new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
let mut windows = vec![];
|
||||
self.niri.layout.with_windows_mut(|mapped, _| {
|
||||
if mapped.recompute_window_rules(window_rules) {
|
||||
windows.push(mapped.window.clone());
|
||||
}
|
||||
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()
|
||||
});
|
||||
for win in windows {
|
||||
self.niri.layout.update_window(&win);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shaders_changed {
|
||||
self.niri.layout.update_shaders();
|
||||
}
|
||||
|
||||
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
|
||||
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
|
||||
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
|
||||
// clients will use the new xdg-decoration setting.
|
||||
|
||||
self.niri.queue_redraw_all();
|
||||
self.reload_output_config();
|
||||
}
|
||||
|
||||
pub fn refresh_ipc_outputs(&mut self) {
|
||||
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user