From b770607893d1654daba5803d30e6b5d930215b80 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Wed, 12 Feb 2025 22:02:17 +0100 Subject: [PATCH] add invert, more help text, ... choose if pointer is visible to make dithering more stable --- README.md | 44 ++++++++++++++++++++++----------- src/cli.rs | 59 +++++++++++++++++++++++++++++++++++--------- src/execute.rs | 23 ++++++++++++++--- src/stream_window.rs | 58 +++++++++++++++++++++---------------------- 4 files changed, 125 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 1a8f88b..439cf6d 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,29 @@ Options: Usage: servicepoint-cli stream Commands: - stdin - screen + stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` + screen Stream the default source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. help Print this message or the help of the given subcommand(s) +``` + +#### Screen + +``` +Usage: servicepoint-cli stream screen [OPTIONS] Options: + -n, --no-dither Disable dithering + -p, --pointer Show mouse pointer in video feed + -h, --help Print help +``` + +#### Stdin + +``` +Usage: servicepoint-cli stream stdin [OPTIONS] + +Options: + -s, --slow -h, --help Print help ``` @@ -71,26 +89,22 @@ Options: Usage: servicepoint-cli brightness Commands: - reset [aliases: r] - set [aliases: s] - min - max - help Print this message or the help of the given subcommand(s) - -Options: - -h, --help Print help + max Reset brightness to the default (max) level [aliases: r, reset] + set Set one brightness for the whole screen [aliases: s] + min Set brightness to lowest possible level. + help Print this message or the help of the given subcommand(s) ``` ### Pixels + ``` Usage: servicepoint-cli pixels Commands: - reset [aliases: r] - help Print this message or the help of the given subcommand(s) - -Options: - -h, --help Print help + off Reset all pixels to the default (off) state [aliases: r, reset] + invert Invert the state of all pixels [aliases: i] + on Set all pixels to the on state + help Print this message or the help of the given subcommand(s) ``` ## Contributing diff --git a/src/cli.rs b/src/cli.rs index afa61f1..fa420d6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,9 @@ -use crate::stream_window::StreamScreenOptions; - #[derive(clap::Parser, std::fmt::Debug)] -#[clap(version, arg_required_else_help = true)] +#[clap( + version, + arg_required_else_help = true, + about = "A command line interface for the ServicePoint display." +)] pub struct Cli { #[arg( short, @@ -26,7 +28,7 @@ pub struct Cli { #[derive(clap::Parser, std::fmt::Debug)] pub enum Mode { - #[command(visible_alias = "r")] + #[command(visible_alias = "r", about = "Reset both pixels and brightness")] ResetEverything, #[command(visible_alias = "p")] Pixels { @@ -42,25 +44,40 @@ pub enum Mode { Stream { #[clap(subcommand)] stream_command: StreamCommand, - } + }, } #[derive(clap::Parser, std::fmt::Debug)] +#[clap(about = "Commands for manipulating pixels")] pub enum PixelCommand { - #[command(visible_alias = "r")] - Reset, + #[command( + visible_alias = "r", + visible_alias = "reset", + about = "Reset all pixels to the default (off) state" + )] + Off, + #[command(visible_alias = "i", about = "Invert the state of all pixels")] + Invert, + #[command(about = "Set all pixels to the on state")] + On, } #[derive(clap::Parser, std::fmt::Debug)] +#[clap(about = "Commands for manipulating the brightness")] pub enum BrightnessCommand { - #[command(visible_alias = "r")] - Reset, - #[command(visible_alias = "s")] + #[command( + visible_alias = "r", + visible_alias = "reset", + about = "Reset brightness to the default (max) level" + )] + Max, + #[command(visible_alias = "s", about = "Set one brightness for the whole screen")] Set { + #[arg()] brightness: u8, }, + #[command(about = "Set brightness to lowest possible level.")] Min, - Max, } #[derive(clap::ValueEnum, Clone, Debug)] @@ -71,13 +88,33 @@ pub enum Protocol { } #[derive(clap::Parser, std::fmt::Debug)] +#[clap(about = "Continuously send data to the display")] pub enum StreamCommand { + #[clap( + about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" + )] Stdin { #[arg(long, short, default_value_t = false)] slow: bool, }, + #[clap(about = "Stream the default source to the display. \ + On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen.")] Screen { #[command(flatten)] options: StreamScreenOptions, }, } + +#[derive(clap::Parser, std::fmt::Debug, Clone)] +pub struct StreamScreenOptions { + #[arg(long, short, default_value_t = false, help = "Disable dithering")] + pub no_dither: bool, + + #[arg( + long, + short, + default_value_t = false, + help = "Show mouse pointer in video feed" + )] + pub pointer: bool, +} diff --git a/src/execute.rs b/src/execute.rs index 74f7db4..9859d3f 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -2,7 +2,7 @@ use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand}; use crate::stream_stdin::stream_stdin; use crate::stream_window::stream_window; use log::info; -use servicepoint::{Brightness, Command, Connection}; +use servicepoint::{BitVec, Brightness, Command, CompressionCode, Connection, PIXEL_COUNT}; pub fn execute_mode(mode: Mode, connection: Connection) { match mode { @@ -21,15 +21,30 @@ pub fn execute_mode(mode: Mode, connection: Connection) { fn pixels(connection: &Connection, pixel_command: PixelCommand) { match pixel_command { - PixelCommand::Reset => pixels_reset(connection), + PixelCommand::Off => pixels_reset(connection), + PixelCommand::Invert => pixels_invert(connection), + PixelCommand::On => pixels_on(connection) } } +fn pixels_on(connection: &Connection) { + let mask = BitVec::repeat(true, PIXEL_COUNT); + connection + .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) + .expect("could not send command") +} + +fn pixels_invert(connection: &Connection) { + let mask = BitVec::repeat(true, PIXEL_COUNT); + connection + .send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) + .expect("could not send command") +} + fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { match brightness_command { - BrightnessCommand::Reset => brightness_reset(connection), + BrightnessCommand::Max => brightness_reset(connection), BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), - BrightnessCommand::Max => brightness_set(connection, Brightness::MAX), BrightnessCommand::Set { brightness } => { brightness_set(connection, Brightness::saturating_from(brightness)) } diff --git a/src/stream_window.rs b/src/stream_window.rs index a528281..e3a626a 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,8 +1,9 @@ +use crate::cli::StreamScreenOptions; use image::{ imageops::{dither, resize, BiLevel, FilterType}, - DynamicImage, ImageBuffer, Rgb, Rgba, + DynamicImage, ImageBuffer, Luma, Rgb, Rgba, }; -use log::{error, warn}; +use log::{error, info, warn}; use scap::{ capturer::{Capturer, Options}, frame::convert_bgra_to_rgb, @@ -13,34 +14,19 @@ use servicepoint::{ }; use std::time::Duration; -#[derive(clap::Parser, std::fmt::Debug, Clone)] -pub struct StreamScreenOptions { - #[arg(long, short, default_value_t = false)] - pub no_dither: bool, -} - pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { - let capturer = match start_capture() { + info!("Starting capture with options: {:?}", options); + warn!("this implementation does not drop any frames - set a lower fps or disable dithering if your computer cannot keep up."); + + let capturer = match start_capture(&options) { Some(value) => value, None => return, }; let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); + info!("now starting to stream images"); loop { - let frame = capturer.get_next_frame().expect("failed to capture frame"); - let frame = frame_to_image(frame); - let frame = frame.grayscale().to_luma8(); - let mut frame = resize( - &frame, - PIXEL_WIDTH as u32, - PIXEL_HEIGHT as u32, - FilterType::Nearest, - ); - - if !options.no_dither { - dither(&mut frame, &BiLevel); - } - + let frame = get_next_frame(&capturer, options.no_dither); for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { *dest = src.0[0] > u8::MAX / 2; } @@ -55,7 +41,24 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { } } -fn start_capture() -> Option { +fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer, Vec> { + let frame = capturer.get_next_frame().expect("failed to capture frame"); + let frame = frame_to_image(frame); + let frame = frame.grayscale().to_luma8(); + let mut frame = resize( + &frame, + PIXEL_WIDTH as u32, + PIXEL_HEIGHT as u32, + FilterType::Nearest, + ); + + if !no_dither { + dither(&mut frame, &BiLevel); + } + frame +} + +fn start_capture(options: &StreamScreenOptions) -> Option { if !scap::is_supported() { error!("platform not supported by scap"); return None; @@ -71,11 +74,8 @@ fn start_capture() -> Option { let mut capturer = Capturer::build(Options { fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, - target: None, - show_cursor: true, - show_highlight: true, - excluded_targets: None, - output_type: scap::frame::FrameType::BGR0, + show_cursor: options.pointer, + output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion ..Default::default() }) .expect("failed to create screen capture");