add invert, more help text, ...

choose if pointer is visible to make dithering more stable
This commit is contained in:
Vinzenz Schroeter 2025-02-12 22:02:17 +01:00
parent 83baf7b419
commit b770607893
4 changed files with 125 additions and 59 deletions

View file

@ -57,11 +57,29 @@ Options:
Usage: servicepoint-cli stream <COMMAND> Usage: servicepoint-cli stream <COMMAND>
Commands: Commands:
stdin stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
screen 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) help Print this message or the help of the given subcommand(s)
```
#### Screen
```
Usage: servicepoint-cli stream screen [OPTIONS]
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 -h, --help Print help
``` ```
@ -71,26 +89,22 @@ Options:
Usage: servicepoint-cli brightness <COMMAND> Usage: servicepoint-cli brightness <COMMAND>
Commands: Commands:
reset [aliases: r] max Reset brightness to the default (max) level [aliases: r, reset]
set [aliases: s] set Set one brightness for the whole screen [aliases: s]
min min Set brightness to lowest possible level.
max
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
``` ```
### Pixels ### Pixels
``` ```
Usage: servicepoint-cli pixels <COMMAND> Usage: servicepoint-cli pixels <COMMAND>
Commands: Commands:
reset [aliases: r] 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) help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
``` ```
## Contributing ## Contributing

View file

@ -1,7 +1,9 @@
use crate::stream_window::StreamScreenOptions;
#[derive(clap::Parser, std::fmt::Debug)] #[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 { pub struct Cli {
#[arg( #[arg(
short, short,
@ -26,7 +28,7 @@ pub struct Cli {
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
pub enum Mode { pub enum Mode {
#[command(visible_alias = "r")] #[command(visible_alias = "r", about = "Reset both pixels and brightness")]
ResetEverything, ResetEverything,
#[command(visible_alias = "p")] #[command(visible_alias = "p")]
Pixels { Pixels {
@ -42,25 +44,40 @@ pub enum Mode {
Stream { Stream {
#[clap(subcommand)] #[clap(subcommand)]
stream_command: StreamCommand, stream_command: StreamCommand,
} },
} }
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
#[clap(about = "Commands for manipulating pixels")]
pub enum PixelCommand { pub enum PixelCommand {
#[command(visible_alias = "r")] #[command(
Reset, 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)] #[derive(clap::Parser, std::fmt::Debug)]
#[clap(about = "Commands for manipulating the brightness")]
pub enum BrightnessCommand { pub enum BrightnessCommand {
#[command(visible_alias = "r")] #[command(
Reset, visible_alias = "r",
#[command(visible_alias = "s")] 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 { Set {
#[arg()]
brightness: u8, brightness: u8,
}, },
#[command(about = "Set brightness to lowest possible level.")]
Min, Min,
Max,
} }
#[derive(clap::ValueEnum, Clone, Debug)] #[derive(clap::ValueEnum, Clone, Debug)]
@ -71,13 +88,33 @@ pub enum Protocol {
} }
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
#[clap(about = "Continuously send data to the display")]
pub enum StreamCommand { pub enum StreamCommand {
#[clap(
about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`"
)]
Stdin { Stdin {
#[arg(long, short, default_value_t = false)] #[arg(long, short, default_value_t = false)]
slow: bool, 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 { Screen {
#[command(flatten)] #[command(flatten)]
options: StreamScreenOptions, 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,
}

View file

@ -2,7 +2,7 @@ use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand};
use crate::stream_stdin::stream_stdin; use crate::stream_stdin::stream_stdin;
use crate::stream_window::stream_window; use crate::stream_window::stream_window;
use log::info; 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) { pub fn execute_mode(mode: Mode, connection: Connection) {
match mode { match mode {
@ -21,15 +21,30 @@ pub fn execute_mode(mode: Mode, connection: Connection) {
fn pixels(connection: &Connection, pixel_command: PixelCommand) { fn pixels(connection: &Connection, pixel_command: PixelCommand) {
match pixel_command { 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) { fn brightness(connection: &Connection, brightness_command: BrightnessCommand) {
match brightness_command { match brightness_command {
BrightnessCommand::Reset => brightness_reset(connection), BrightnessCommand::Max => brightness_reset(connection),
BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), BrightnessCommand::Min => brightness_set(connection, Brightness::MIN),
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
BrightnessCommand::Set { brightness } => { BrightnessCommand::Set { brightness } => {
brightness_set(connection, Brightness::saturating_from(brightness)) brightness_set(connection, Brightness::saturating_from(brightness))
} }

View file

@ -1,8 +1,9 @@
use crate::cli::StreamScreenOptions;
use image::{ use image::{
imageops::{dither, resize, BiLevel, FilterType}, 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::{ use scap::{
capturer::{Capturer, Options}, capturer::{Capturer, Options},
frame::convert_bgra_to_rgb, frame::convert_bgra_to_rgb,
@ -13,34 +14,19 @@ use servicepoint::{
}; };
use std::time::Duration; 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) { 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, Some(value) => value,
None => return, None => return,
}; };
let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
info!("now starting to stream images");
loop { loop {
let frame = capturer.get_next_frame().expect("failed to capture frame"); let frame = get_next_frame(&capturer, options.no_dither);
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);
}
for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) {
*dest = src.0[0] > u8::MAX / 2; *dest = src.0[0] > u8::MAX / 2;
} }
@ -55,7 +41,24 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) {
} }
} }
fn start_capture() -> Option<Capturer> { fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer<Luma<u8>, Vec<u8>> {
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<Capturer> {
if !scap::is_supported() { if !scap::is_supported() {
error!("platform not supported by scap"); error!("platform not supported by scap");
return None; return None;
@ -71,11 +74,8 @@ fn start_capture() -> Option<Capturer> {
let mut capturer = Capturer::build(Options { let mut capturer = Capturer::build(Options {
fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32, fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32,
target: None, show_cursor: options.pointer,
show_cursor: true, output_type: scap::frame::FrameType::BGR0, // this is more like a suggestion
show_highlight: true,
excluded_targets: None,
output_type: scap::frame::FrameType::BGR0,
..Default::default() ..Default::default()
}) })
.expect("failed to create screen capture"); .expect("failed to create screen capture");