Add logical output info and preferred modes to IPC

This commit is contained in:
Ivan Molodetskikh
2024-03-27 14:54:24 +04:00
parent e276c906bf
commit cf87a185a9
10 changed files with 223 additions and 75 deletions
+47
View File
@@ -248,6 +248,10 @@ pub struct Output {
/// ///
/// `None` if the output is disabled. /// `None` if the output is disabled.
pub current_mode: Option<usize>, pub current_mode: Option<usize>,
/// Logical output information.
///
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
pub logical: Option<LogicalOutput>,
} }
/// Output mode. /// Output mode.
@@ -259,6 +263,49 @@ pub struct Mode {
pub height: u16, pub height: u16,
/// Refresh rate in millihertz. /// Refresh rate in millihertz.
pub refresh_rate: u32, pub refresh_rate: u32,
/// Whether this mode is preferred by the monitor.
pub is_preferred: bool,
}
/// Logical output in the compositor's coordinate space.
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct LogicalOutput {
/// Logical X position.
pub x: i32,
/// Logical Y position.
pub y: i32,
/// Width in logical pixels.
pub width: u32,
/// Height in logical pixels.
pub height: u32,
/// Scale factor.
pub scale: f64,
/// Transform.
pub transform: Transform,
}
/// Output transform, which goes counter-clockwise.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transform {
/// Untransformed.
Normal,
/// Rotated by 90°.
#[serde(rename = "90")]
_90,
/// Rotated by 180°.
#[serde(rename = "180")]
_180,
/// Rotated by 270°.
#[serde(rename = "270")]
_270,
/// Flipped horizontally.
Flipped,
/// Rotated by 90° and flipped horizontally.
Flipped90,
/// Flipped vertically.
Flipped180,
/// Rotated by 270° and flipped horizontally.
Flipped270,
} }
impl FromStr for SizeChange { impl FromStr for SizeChange {
+1 -1
View File
@@ -31,7 +31,7 @@ pub enum RenderResult {
Skipped, Skipped,
} }
pub type IpcOutputMap = HashMap<String, (niri_ipc::Output, Option<Output>)>; pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
impl Backend { impl Backend {
pub fn init(&mut self, niri: &mut Niri) { pub fn init(&mut self, niri: &mut Niri) {
+6 -4
View File
@@ -60,7 +60,7 @@ use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State}; use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::renderer::AsGlesRenderer; use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{shaders, RenderTarget}; use crate::render_helpers::{shaders, RenderTarget};
use crate::utils::get_monotonic_time; use crate::utils::{get_monotonic_time, logical_output};
const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888]; const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888];
@@ -1370,6 +1370,7 @@ impl Tty {
width: m.size().0, width: m.size().0,
height: m.size().1, height: m.size().1,
refresh_rate: Mode::from(*m).refresh as u32, refresh_rate: Mode::from(*m).refresh as u32,
is_preferred: m.mode_type().contains(ModeTypeFlags::PREFERRED),
} }
}) })
.collect(); .collect();
@@ -1384,14 +1385,14 @@ impl Tty {
} }
} }
let output = niri let logical = niri
.global_space .global_space
.outputs() .outputs()
.find(|output| { .find(|output| {
let tty_state: &TtyOutputState = output.user_data().get().unwrap(); let tty_state: &TtyOutputState = output.user_data().get().unwrap();
tty_state.node == *node && tty_state.crtc == crtc tty_state.node == *node && tty_state.crtc == crtc
}) })
.cloned(); .map(logical_output);
let ipc_output = niri_ipc::Output { let ipc_output = niri_ipc::Output {
name: connector_name.clone(), name: connector_name.clone(),
@@ -1400,9 +1401,10 @@ impl Tty {
physical_size, physical_size,
modes, modes,
current_mode, current_mode,
logical,
}; };
ipc_outputs.insert(connector_name, (ipc_output, output)); ipc_outputs.insert(connector_name, ipc_output);
} }
} }
+9 -5
View File
@@ -20,7 +20,7 @@ use smithay::reexports::winit::window::WindowBuilder;
use super::{IpcOutputMap, RenderResult}; use super::{IpcOutputMap, RenderResult};
use crate::niri::{Niri, RedrawState, State}; use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::{shaders, RenderTarget}; use crate::render_helpers::{shaders, RenderTarget};
use crate::utils::get_monotonic_time; use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit { pub struct Winit {
config: Rc<RefCell<Config>>, config: Rc<RefCell<Config>>,
@@ -61,7 +61,6 @@ impl Winit {
let physical_properties = output.physical_properties(); let physical_properties = output.physical_properties();
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([( let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(), "winit".to_owned(),
(
niri_ipc::Output { niri_ipc::Output {
name: output.name(), name: output.name(),
make: physical_properties.make, make: physical_properties.make,
@@ -71,11 +70,11 @@ impl Winit {
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16, width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16, height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
refresh_rate: 60_000, refresh_rate: 60_000,
is_preferred: true,
}], }],
current_mode: Some(0), current_mode: Some(0),
logical: Some(logical_output(&output)),
}, },
Some(output.clone()),
),
)]))); )])));
let damage_tracker = OutputDamageTracker::from_output(&output); let damage_tracker = OutputDamageTracker::from_output(&output);
@@ -96,9 +95,14 @@ impl Winit {
{ {
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap(); let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
let mode = &mut ipc_outputs.get_mut("winit").unwrap().0.modes[0]; let output = ipc_outputs.get_mut("winit").unwrap();
let mode = &mut output.modes[0];
mode.width = size.w.clamp(0, u16::MAX as i32) as u16; mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
mode.height = size.h.clamp(0, u16::MAX as i32) as u16; mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
if let Some(logical) = output.logical.as_mut() {
logical.width = size.w as u32;
logical.height = size.h as u32;
}
state.niri.ipc_outputs_changed = true; state.niri.ipc_outputs_changed = true;
} }
+22 -22
View File
@@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use serde::Serialize; use serde::Serialize;
use smithay::utils::Transform;
use zbus::fdo::RequestNameFlags; use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{self, OwnedValue, Type}; use zbus::zvariant::{self, OwnedValue, Type};
use zbus::{dbus_interface, fdo, SignalContext}; use zbus::{dbus_interface, fdo, SignalContext};
@@ -60,11 +59,8 @@ impl DisplayConfig {
.unwrap() .unwrap()
.iter() .iter()
// Take only enabled outputs. // Take only enabled outputs.
.filter_map(|(c, (ipc, output))| { .filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
ipc.current_mode?; .map(|(c, output)| {
output.as_ref().map(move |output| (c, (ipc, output)))
})
.map(|(c, (ipc, output))| {
// Loosely matches the check in Mutter. // Loosely matches the check in Mutter.
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-")); let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
@@ -84,7 +80,7 @@ impl DisplayConfig {
OwnedValue::from(is_laptop_panel), OwnedValue::from(is_laptop_panel),
); );
let mut modes: Vec<Mode> = ipc let mut modes: Vec<Mode> = output
.modes .modes
.iter() .iter()
.map(|m| { .map(|m| {
@@ -92,6 +88,7 @@ impl DisplayConfig {
width, width,
height, height,
refresh_rate, refresh_rate,
is_preferred,
} = *m; } = *m;
let refresh = refresh_rate as f64 / 1000.; let refresh = refresh_rate as f64 / 1000.;
@@ -102,11 +99,14 @@ impl DisplayConfig {
refresh_rate: refresh, refresh_rate: refresh,
preferred_scale: 1., preferred_scale: 1.,
supported_scales: vec![1., 2., 3.], supported_scales: vec![1., 2., 3.],
properties: HashMap::new(), properties: HashMap::from([(
String::from("is-preferred"),
OwnedValue::from(is_preferred),
)]),
} }
}) })
.collect(); .collect();
modes[ipc.current_mode.unwrap()] modes[output.current_mode.unwrap()]
.properties .properties
.insert(String::from("is-current"), OwnedValue::from(true)); .insert(String::from("is-current"), OwnedValue::from(true));
@@ -116,23 +116,23 @@ impl DisplayConfig {
properties, properties,
}; };
let loc = output.current_location(); let logical = output.logical.as_ref().unwrap();
let transform = match output.current_transform() { let transform = match logical.transform {
Transform::Normal => 0, niri_ipc::Transform::Normal => 0,
Transform::_90 => 1, niri_ipc::Transform::_90 => 1,
Transform::_180 => 2, niri_ipc::Transform::_180 => 2,
Transform::_270 => 3, niri_ipc::Transform::_270 => 3,
Transform::Flipped => 4, niri_ipc::Transform::Flipped => 4,
Transform::Flipped90 => 5, niri_ipc::Transform::Flipped90 => 5,
Transform::Flipped180 => 6, niri_ipc::Transform::Flipped180 => 6,
Transform::Flipped270 => 7, niri_ipc::Transform::Flipped270 => 7,
}; };
let logical_monitor = LogicalMonitor { let logical_monitor = LogicalMonitor {
x: loc.x, x: logical.x,
y: loc.y, y: logical.y,
scale: output.current_scale().fractional_scale(), scale: logical.scale,
transform, transform,
is_primary: false, is_primary: false,
monitors: vec![monitor.names.clone()], monitors: vec![monitor.names.clone()],
+18 -10
View File
@@ -11,7 +11,6 @@ use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
use super::Start; use super::Start;
use crate::backend::IpcOutputMap; use crate::backend::IpcOutputMap;
use crate::utils::output_size;
#[derive(Clone)] #[derive(Clone)]
pub struct ScreenCast { pub struct ScreenCast {
@@ -49,7 +48,8 @@ struct RecordMonitorProperties {
#[derive(Clone)] #[derive(Clone)]
pub struct Stream { pub struct Stream {
output: Output, // FIXME: update on scale changes and whatnot.
output: niri_ipc::Output,
cursor_mode: CursorMode, cursor_mode: CursorMode,
was_started: Arc<AtomicBool>, was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>, to_niri: calloop::channel::Sender<ScreenCastToNiri>,
@@ -58,6 +58,8 @@ pub struct Stream {
#[derive(Debug, SerializeDict, Type, Value)] #[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")] #[zvariant(signature = "dict")]
struct StreamParameters { struct StreamParameters {
/// Position of the stream in logical coordinates.
position: (i32, i32),
/// Size of the stream in logical coordinates. /// Size of the stream in logical coordinates.
size: (i32, i32), size: (i32, i32),
} }
@@ -65,7 +67,7 @@ struct StreamParameters {
pub enum ScreenCastToNiri { pub enum ScreenCastToNiri {
StartCast { StartCast {
session_id: usize, session_id: usize,
output: Output, output: String,
cursor_mode: CursorMode, cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>, signal_ctx: SignalContext<'static>,
}, },
@@ -160,11 +162,14 @@ impl Session {
) -> fdo::Result<OwnedObjectPath> { ) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor"); debug!(connector, ?properties, "record_monitor");
let Some((_, Some(output))) = self.ipc_outputs.lock().unwrap().get(connector).cloned() let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
else {
return Err(fdo::Error::Failed("no such monitor".to_owned())); return Err(fdo::Error::Failed("no such monitor".to_owned()));
}; };
if output.logical.is_none() {
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0); static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!( let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}", "/org/gnome/Mutter/ScreenCast/Stream/u{}",
@@ -174,7 +179,7 @@ impl Session {
let cursor_mode = properties.cursor_mode.unwrap_or_default(); let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output, cursor_mode, self.to_niri.clone()); let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await { match server.at(&path, stream.clone()).await {
Ok(true) => { Ok(true) => {
let iface = server.interface(&path).await.unwrap(); let iface = server.interface(&path).await.unwrap();
@@ -203,8 +208,11 @@ impl Stream {
#[dbus_interface(property)] #[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters { async fn parameters(&self) -> StreamParameters {
let size = output_size(&self.output).into(); let logical = self.output.logical.as_ref().unwrap();
StreamParameters { size } StreamParameters {
position: (logical.x, logical.y),
size: (logical.width as i32, logical.height as i32),
}
} }
} }
@@ -261,7 +269,7 @@ impl Drop for Session {
impl Stream { impl Stream {
pub fn new( pub fn new(
output: Output, output: niri_ipc::Output,
cursor_mode: CursorMode, cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>, to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self { ) -> Self {
@@ -280,7 +288,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast { let msg = ScreenCastToNiri::StartCast {
session_id, session_id,
output: self.output.clone(), output: self.output.name.clone(),
cursor_mode: self.cursor_mode, cursor_mode: self.cursor_mode,
signal_ctx: ctxt, signal_ctx: ctxt,
}; };
+47 -4
View File
@@ -4,7 +4,7 @@ use std::net::Shutdown;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use anyhow::{anyhow, bail, Context}; use anyhow::{anyhow, bail, Context};
use niri_ipc::{Mode, Output, Reply, Request, Response}; use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
use crate::cli::Msg; use crate::cli::Msg;
@@ -66,6 +66,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
physical_size, physical_size,
modes, modes,
current_mode, current_mode,
logical,
} = output; } = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#); println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
@@ -78,9 +79,11 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
width, width,
height, height,
refresh_rate, refresh_rate,
is_preferred,
} = mode; } = mode;
let refresh = refresh_rate as f64 / 1000.; let refresh = refresh_rate as f64 / 1000.;
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz"); let preferred = if is_preferred { " (preferred)" } else { "" };
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
} else { } else {
println!(" Disabled"); println!(" Disabled");
} }
@@ -91,15 +94,55 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!(" Physical size: unknown"); println!(" Physical size: unknown");
} }
if let Some(logical) = logical {
let LogicalOutput {
x,
y,
width,
height,
scale,
transform,
} = logical;
println!(" Logical position: {x}, {y}");
println!(" Logical size: {width}x{height}");
println!(" Scale: {scale}");
let transform = match transform {
niri_ipc::Transform::Normal => "normal",
niri_ipc::Transform::_90 => "90° counter-clockwise",
niri_ipc::Transform::_180 => "180°",
niri_ipc::Transform::_270 => "270° counter-clockwise",
niri_ipc::Transform::Flipped => "flipped horizontally",
niri_ipc::Transform::Flipped90 => {
"90° counter-clockwise, flipped horizontally"
}
niri_ipc::Transform::Flipped180 => "flipped vertically",
niri_ipc::Transform::Flipped270 => {
"270° counter-clockwise, flipped horizontally"
}
};
println!(" Transform: {transform}");
}
println!(" Available modes:"); println!(" Available modes:");
for mode in modes { for (idx, mode) in modes.into_iter().enumerate() {
let Mode { let Mode {
width, width,
height, height,
refresh_rate, refresh_rate,
is_preferred,
} = mode; } = mode;
let refresh = refresh_rate as f64 / 1000.; let refresh = refresh_rate as f64 / 1000.;
println!(" {width}x{height}@{refresh:.3}");
let is_current = Some(idx) == current_mode;
let qualifier = match (is_current, is_preferred) {
(true, true) => " (current, preferred)",
(true, false) => " (current)",
(false, true) => " (preferred)",
(false, false) => "",
};
println!(" {width}x{height}@{refresh:.3}{qualifier}");
} }
println!(); println!();
} }
+1 -5
View File
@@ -125,11 +125,7 @@ fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
let response = match request { let response = match request {
Request::Outputs => { Request::Outputs => {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap(); let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
let ipc_outputs = ipc_outputs
.iter()
.map(|(name, (ipc, _))| (name.clone(), ipc.clone()))
.collect();
Response::Outputs(ipc_outputs) Response::Outputs(ipc_outputs)
} }
Request::Action(action) => { Request::Action(action) => {
+36 -11
View File
@@ -115,7 +115,8 @@ use crate::ui::hotkey_overlay::HotkeyOverlay;
use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement}; use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::spawning::CHILD_ENV; use crate::utils::spawning::CHILD_ENV;
use crate::utils::{ use crate::utils::{
center, center_f64, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8, center, center_f64, get_monotonic_time, logical_output, make_screenshot_path, output_size,
write_png_rgba8,
}; };
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef}; use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef};
use crate::{animation, niri_render_elements}; use crate::{animation, niri_render_elements};
@@ -459,7 +460,7 @@ impl State {
self.refresh_pointer_focus(); self.refresh_pointer_focus();
foreign_toplevel::refresh(self); foreign_toplevel::refresh(self);
self.niri.refresh_window_rules(); self.niri.refresh_window_rules();
self.niri.check_ipc_output_changed(); self.refresh_ipc_outputs();
} }
pub fn move_cursor(&mut self, location: Point<f64, Logical>) { pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
@@ -967,6 +968,28 @@ impl State {
self.niri.queue_redraw_all(); self.niri.queue_redraw_all();
} }
pub fn refresh_ipc_outputs(&mut self) {
if !self.niri.ipc_outputs_changed {
return;
}
self.niri.ipc_outputs_changed = false;
let _span = tracy_client::span!("State::refresh_ipc_outputs");
for (name, ipc_output) in self.backend.ipc_outputs().lock().unwrap().iter_mut() {
let logical = self
.niri
.global_space
.outputs()
.find(|output| output.name() == *name)
.map(logical_output);
ipc_output.logical = logical;
}
#[cfg(feature = "dbus")]
self.niri.on_ipc_outputs_changed();
}
#[cfg(feature = "xdp-gnome-screencast")] #[cfg(feature = "xdp-gnome-screencast")]
pub fn on_screen_cast_msg( pub fn on_screen_cast_msg(
&mut self, &mut self,
@@ -997,6 +1020,17 @@ impl State {
return; return;
}; };
let Some(output) = self
.niri
.global_space
.outputs()
.find(|out| out.name() == output)
.cloned()
else {
warn!("tried to start a screencast on missing output: {output}");
return;
};
match pw.start_cast( match pw.start_cast(
to_niri.clone(), to_niri.clone(),
gbm, gbm,
@@ -3438,15 +3472,6 @@ impl Niri {
}); });
} }
pub fn check_ipc_output_changed(&mut self) {
if self.ipc_outputs_changed {
self.ipc_outputs_changed = false;
#[cfg(feature = "dbus")]
self.on_ipc_outputs_changed();
}
}
#[cfg(feature = "dbus")] #[cfg(feature = "dbus")]
pub fn on_ipc_outputs_changed(&self) { pub fn on_ipc_outputs_changed(&self) {
let _span = tracy_client::span!("Niri::on_ipc_outputs_changed"); let _span = tracy_client::span!("Niri::on_ipc_outputs_changed");
+24 -1
View File
@@ -12,7 +12,7 @@ use git_version::git_version;
use niri_config::Config; use niri_config::Config;
use smithay::output::Output; use smithay::output::Output;
use smithay::reexports::rustix::time::{clock_gettime, ClockId}; use smithay::reexports::rustix::time::{clock_gettime, ClockId};
use smithay::utils::{Logical, Point, Rectangle, Size}; use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
pub mod id; pub mod id;
pub mod spawning; pub mod spawning;
@@ -51,6 +51,29 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
.to_logical(output_scale) .to_logical(output_scale)
} }
pub fn logical_output(output: &Output) -> niri_ipc::LogicalOutput {
let loc = output.current_location();
let size = output_size(output);
let transform = match output.current_transform() {
Transform::Normal => niri_ipc::Transform::Normal,
Transform::_90 => niri_ipc::Transform::_90,
Transform::_180 => niri_ipc::Transform::_180,
Transform::_270 => niri_ipc::Transform::_270,
Transform::Flipped => niri_ipc::Transform::Flipped,
Transform::Flipped90 => niri_ipc::Transform::Flipped90,
Transform::Flipped180 => niri_ipc::Transform::Flipped180,
Transform::Flipped270 => niri_ipc::Transform::Flipped270,
};
niri_ipc::LogicalOutput {
x: loc.x,
y: loc.y,
width: size.w as u32,
height: size.h as u32,
scale: output.current_scale().fractional_scale(),
transform,
}
}
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> { pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
if let Ok(rest) = path.strip_prefix("~") { if let Ok(rest) = path.strip_prefix("~") {
let dirs = UserDirs::new().context("error retrieving home directory")?; let dirs = UserDirs::new().context("error retrieving home directory")?;