feat: valw window
This commit is contained in:
1 parent
7513fc8090
commit
5034cdc54f
2 files changed
+94
-6
No files matched your search
+24
-6
@@ -1,6 +1,3 @@
|
|||||||
// Window-mode helpers land before main uses them; Task 2 removes this.
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
mod capture;
|
mod capture;
|
||||||
mod config;
|
mod config;
|
||||||
mod detach;
|
mod detach;
|
||||||
@@ -20,6 +17,7 @@ mod selection;
|
|||||||
mod stack;
|
mod stack;
|
||||||
mod thumbnail;
|
mod thumbnail;
|
||||||
mod wayland;
|
mod wayland;
|
||||||
|
mod window;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -59,6 +57,11 @@ enum Command {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
common: Common,
|
common: Common,
|
||||||
},
|
},
|
||||||
|
/// Click a window to capture it (Cmd+Shift+4, then Space).
|
||||||
|
Window {
|
||||||
|
#[command(flatten)]
|
||||||
|
common: Common,
|
||||||
|
},
|
||||||
/// Report what the compositor and system support.
|
/// Report what the compositor and system support.
|
||||||
Doctor,
|
Doctor,
|
||||||
/// Internal: the process that shows preview thumbnails.
|
/// Internal: the process that shows preview thumbnails.
|
||||||
@@ -91,9 +94,14 @@ fn parse_delay(s: &str) -> Result<Duration, String> {
|
|||||||
.map_err(|_| format!("must be a non-negative number of seconds: {s}"))
|
.map_err(|_| format!("must be a non-negative number of seconds: {s}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (image, output name for the file name) pairs, which one goes to the
|
||||||
|
/// clipboard, and the output the preview appears on.
|
||||||
|
type Shots = (Vec<(RgbaImage, Option<String>)>, usize, String);
|
||||||
|
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Screen { all: bool },
|
Screen { all: bool },
|
||||||
Region,
|
Region,
|
||||||
|
Window,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -126,6 +134,7 @@ fn run(cli: Cli) -> Result<()> {
|
|||||||
Command::PreviewHost => host::run(),
|
Command::PreviewHost => host::run(),
|
||||||
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
||||||
Command::Region { common } => capture(Mode::Region, common),
|
Command::Region { common } => capture(Mode::Region, common),
|
||||||
|
Command::Window { common } => capture(Mode::Window, common),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,9 +154,7 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
// Thumbnails from earlier captures must not end up in this one.
|
// Thumbnails from earlier captures must not end up in this one.
|
||||||
let mut hide = ipc::HideGuard::new(&ipc::socket_path());
|
let mut hide = ipc::HideGuard::new(&ipc::socket_path());
|
||||||
|
|
||||||
// (image, output name for the file name), which one goes to the
|
let (shots, clip, source): Shots = match mode {
|
||||||
// clipboard, and the output the preview appears on.
|
|
||||||
let (shots, clip, source): (Vec<(RgbaImage, Option<String>)>, usize, String) = match mode {
|
|
||||||
Mode::Screen { all } => {
|
Mode::Screen { all } => {
|
||||||
let focused = focused_output(&outputs);
|
let focused = focused_output(&outputs);
|
||||||
let source = outputs[focused].geom.name.clone();
|
let source = outputs[focused].geom.name.clone();
|
||||||
@@ -169,6 +176,7 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
let source = frames[i].output.name.clone();
|
let source = frames[i].output.name.clone();
|
||||||
(vec![(frames[i].to_rgba(r), None)], 0, source)
|
(vec![(frames[i].to_rgba(r), None)], 0, source)
|
||||||
}
|
}
|
||||||
|
Mode::Window => window_shot(&outputs, cursor)?,
|
||||||
};
|
};
|
||||||
drop(wl);
|
drop(wl);
|
||||||
|
|
||||||
@@ -195,6 +203,14 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A window shot, shaped like the other modes' results. The preview goes to
|
||||||
|
/// the window's output, or niri's focused one if that is unknown.
|
||||||
|
fn window_shot(outputs: &[wayland::Output], cursor: bool) -> Result<Shots> {
|
||||||
|
let (image, output) = window::capture(cursor)?;
|
||||||
|
let source = output.unwrap_or_else(|| outputs[focused_output(outputs)].geom.name.clone());
|
||||||
|
Ok((vec![(image, None)], 0, source))
|
||||||
|
}
|
||||||
|
|
||||||
/// Index of niri's focused output, or 0 if niri can't tell us.
|
/// Index of niri's focused output, or 0 if niri can't tell us.
|
||||||
fn focused_output(outputs: &[wayland::Output]) -> usize {
|
fn focused_output(outputs: &[wayland::Output]) -> usize {
|
||||||
let name = match niri::focused_output() {
|
let name = match niri::focused_output() {
|
||||||
@@ -284,6 +300,8 @@ mod tests {
|
|||||||
assert!(!parses(&["region", "--clipboard-only", "-o", "a.png"]));
|
assert!(!parses(&["region", "--clipboard-only", "-o", "a.png"]));
|
||||||
assert!(parses(&["region", "--no-preview"]));
|
assert!(parses(&["region", "--no-preview"]));
|
||||||
assert!(parses(&["screen", "--all", "--no-preview"]));
|
assert!(parses(&["screen", "--all", "--no-preview"]));
|
||||||
|
assert!(parses(&["window", "--cursor", "--delay", "2"]));
|
||||||
|
assert!(!parses(&["window", "--clipboard-only", "-o", "a.png"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
//! Window mode: niri picks the window and renders it; valw reads the result.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
|
use crate::error::Cancelled;
|
||||||
|
use crate::niri;
|
||||||
|
|
||||||
|
/// How long niri gets to render and write the window.
|
||||||
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Lets the user click a window. Returns its pixels and the output it is
|
||||||
|
/// on, if niri knows. Esc during the pick is `Cancelled`.
|
||||||
|
pub fn capture(cursor: bool) -> Result<(RgbaImage, Option<String>)> {
|
||||||
|
let window = niri::pick_window()?.ok_or(Cancelled)?;
|
||||||
|
tracing::info!("picked window {} ({:?})", window.id, window.app_id);
|
||||||
|
let tmp = TempFile(temp_path());
|
||||||
|
niri::screenshot_window(window.id, &tmp.0, cursor, TIMEOUT)?;
|
||||||
|
let image = crate::thumbnail::load(&tmp.0)?;
|
||||||
|
let workspaces = niri::workspaces().unwrap_or_else(|e| {
|
||||||
|
tracing::warn!("no workspace list: {e:#}");
|
||||||
|
Vec::new()
|
||||||
|
});
|
||||||
|
Ok((image, niri::output_of(window.workspace_id, &workspaces)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where niri writes the window: next to the capture lock, one per process.
|
||||||
|
fn temp_path() -> PathBuf {
|
||||||
|
crate::lock::default_path().with_file_name(format!("valw-window-{}.png", std::process::id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the temporary screenshot however `capture` ends.
|
||||||
|
struct TempFile(PathBuf);
|
||||||
|
|
||||||
|
impl Drop for TempFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_file(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temp_file_is_removed_on_drop() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("valw-window-test-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("shot.png");
|
||||||
|
std::fs::write(&path, b"png").unwrap();
|
||||||
|
drop(TempFile(path.clone()));
|
||||||
|
assert!(!path.exists());
|
||||||
|
// A file niri never wrote is fine too.
|
||||||
|
drop(TempFile(dir.join("missing.png")));
|
||||||
|
std::fs::remove_dir(&dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temp_path_is_absolute_and_per_process() {
|
||||||
|
let p = temp_path();
|
||||||
|
assert!(p.is_absolute(), "niri rejects relative paths: {p:?}");
|
||||||
|
assert!(
|
||||||
|
p.to_string_lossy()
|
||||||
|
.contains(&std::process::id().to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user