Compare commits
No commits in common. "31280abff63d3a5e4ea7f0e580b1301ce89c2fa4" and "30b887dc03681fb70edaa20820ae49aac5504118" have entirely different histories.
31280abff6
...
30b887dc03
1626
Cargo.lock
generated
1626
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ name = "servicepoint-cli"
|
|||
description = "A command line interface for the ServicePoint display."
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.80.0"
|
||||
rust-version = "1.78.0"
|
||||
publish = true
|
||||
resolver = "2"
|
||||
readme = "README.md"
|
||||
|
@ -17,5 +17,3 @@ servicepoint = { version = "0.13.0", features = ["protocol_websocket"] }
|
|||
clap = { version = "4.5", features = ["derive"]}
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
scap = "0.0.8"
|
||||
image = "0.25.5"
|
||||
|
|
64
README.md
64
README.md
|
@ -31,75 +31,13 @@ cd servicepoint-cli
|
|||
cargo run -- <args>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage: servicepoint-cli [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
reset-everything [aliases: r]
|
||||
pixels [aliases: p]
|
||||
brightness [aliases: b]
|
||||
stream [aliases: s]
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-d, --destination <DESTINATION> ip:port of the servicepoint display [default: 127.0.0.1:2342]
|
||||
-t, --transport <TRANSPORT> protocol to use for communication with display [default: udp] [possible values: udp, web-socket, fake]
|
||||
-v, --verbose verbose logging
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
### Stream
|
||||
|
||||
```
|
||||
Usage: servicepoint-cli stream <COMMAND>
|
||||
|
||||
Commands:
|
||||
stdin
|
||||
screen
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### Brightness
|
||||
|
||||
```
|
||||
Usage: servicepoint-cli brightness <COMMAND>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Pixels subcommands:
|
||||
```
|
||||
Usage: servicepoint-cli pixels <COMMAND>
|
||||
|
||||
Commands:
|
||||
reset [aliases: r]
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you have ideas on how to improve the code, add features or improve documentation feel free to open a pull request.
|
||||
|
||||
You think you found a bug? Please open an issue.
|
||||
|
||||
Submissions on [Forgejo](https://git.berlin.ccc.de/servicepoint/servicepoint-cli) are preferred, but you can also use [GitHub](https://github.com/kaesaecracker/servicepoint-cli).
|
||||
Submissions on Forgejo are preferred, but you can also use GitHub.
|
||||
|
||||
All creatures welcome.
|
||||
|
||||
|
|
19
flake.nix
19
flake.nix
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
description = "Flake for command line interface of the ServicePoint display.";
|
||||
description = "Flake for servicepoint-cli";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
||||
|
@ -55,25 +55,12 @@
|
|||
};
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
libclang
|
||||
rustPlatform.bindgenHook
|
||||
];
|
||||
strictDeps = true;
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
buildInputs = with pkgs; [
|
||||
xe
|
||||
xz
|
||||
clang
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isLinux (
|
||||
with pkgs;
|
||||
[
|
||||
dbus
|
||||
pipewire
|
||||
libclang
|
||||
]
|
||||
);
|
||||
];
|
||||
};
|
||||
|
||||
default = servicepoint-cli;
|
||||
|
|
19
src/cli.rs
19
src/cli.rs
|
@ -1,5 +1,3 @@
|
|||
use crate::stream_window::StreamScreenOptions;
|
||||
|
||||
#[derive(clap::Parser, std::fmt::Debug)]
|
||||
#[clap(version, arg_required_else_help = true)]
|
||||
pub struct Cli {
|
||||
|
@ -38,11 +36,6 @@ pub enum Mode {
|
|||
#[clap(subcommand)]
|
||||
brightness_command: BrightnessCommand,
|
||||
},
|
||||
#[command(visible_alias = "s")]
|
||||
Stream {
|
||||
#[clap(subcommand)]
|
||||
stream_command: StreamCommand,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, std::fmt::Debug)]
|
||||
|
@ -69,15 +62,3 @@ pub enum Protocol {
|
|||
WebSocket,
|
||||
Fake,
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, std::fmt::Debug)]
|
||||
pub enum StreamCommand {
|
||||
Stdin {
|
||||
#[arg(long, short, default_value_t = false)]
|
||||
slow: bool,
|
||||
},
|
||||
Screen {
|
||||
#[command(flatten)]
|
||||
options: StreamScreenOptions,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use crate::cli::{BrightnessCommand, Mode, PixelCommand, StreamCommand};
|
||||
use crate::stream_stdin::stream_stdin;
|
||||
use crate::stream_window::stream_window;
|
||||
use crate::cli::{BrightnessCommand, Mode, PixelCommand};
|
||||
use log::info;
|
||||
use servicepoint::{Brightness, Command, Connection};
|
||||
|
||||
|
@ -12,10 +10,6 @@ pub fn execute_mode(mode: Mode, connection: Connection) {
|
|||
}
|
||||
Mode::Pixels { pixel_command } => pixels(&connection, pixel_command),
|
||||
Mode::Brightness { brightness_command } => brightness(&connection, brightness_command),
|
||||
Mode::Stream { stream_command } => match stream_command {
|
||||
StreamCommand::Stdin { slow } => stream_stdin(&connection, slow),
|
||||
StreamCommand::Screen { options } => stream_window(&connection, options),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ use servicepoint::Connection;
|
|||
|
||||
mod cli;
|
||||
mod execute;
|
||||
mod stream_stdin;
|
||||
mod stream_window;
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
use log::warn;
|
||||
use servicepoint::*;
|
||||
use std::thread::sleep;
|
||||
|
||||
pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
|
||||
warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet.");
|
||||
let mut app = App {
|
||||
connection,
|
||||
mirror: CharGrid::new(TILE_WIDTH, TILE_HEIGHT),
|
||||
y: 0,
|
||||
slow,
|
||||
};
|
||||
app.run()
|
||||
}
|
||||
|
||||
struct App<'t> {
|
||||
connection: &'t Connection,
|
||||
mirror: CharGrid,
|
||||
y: usize,
|
||||
slow: bool,
|
||||
}
|
||||
|
||||
impl<'t> App<'t> {
|
||||
fn run(&mut self) {
|
||||
self.connection
|
||||
.send(Command::Clear)
|
||||
.expect("couldn't clear screen");
|
||||
let last_y = self.mirror.height() - 1;
|
||||
for line in std::io::stdin().lines() {
|
||||
let line = line.expect("could not read from stdin");
|
||||
|
||||
if self.y <= last_y {
|
||||
self.single_line(&line);
|
||||
self.y += 1;
|
||||
} else {
|
||||
self.shift_rows();
|
||||
Self::line_onto_grid(&mut self.mirror, last_y, &line);
|
||||
self.send_mirror()
|
||||
// we stay on last y
|
||||
}
|
||||
|
||||
if self.slow {
|
||||
sleep(FRAME_PACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shift_rows(&mut self) {
|
||||
let data = self.mirror.data_ref_mut();
|
||||
data.rotate_left(TILE_WIDTH);
|
||||
if let Some(row) = data.last_chunk_mut::<TILE_WIDTH>() {
|
||||
row.fill(' ')
|
||||
}
|
||||
}
|
||||
|
||||
fn line_onto_grid(grid: &mut CharGrid, y: usize, line: &str) {
|
||||
for (x, char) in line.chars().enumerate() {
|
||||
if x < grid.width() {
|
||||
grid.set(x, y, char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_mirror(&self) {
|
||||
self.connection
|
||||
.send(Command::Cp437Data(
|
||||
Origin::ZERO,
|
||||
Cp437Grid::from(&self.mirror),
|
||||
))
|
||||
.expect("couldn't send screen to display");
|
||||
}
|
||||
|
||||
fn single_line(&mut self, line: &str) {
|
||||
let mut line_grid = CharGrid::new(TILE_WIDTH, 1);
|
||||
Self::line_onto_grid(&mut line_grid, 0, line);
|
||||
Self::line_onto_grid(&mut self.mirror, self.y, line);
|
||||
self.connection
|
||||
.send(Command::Utf8Data(Origin::new(0, self.y), line_grid))
|
||||
.expect("couldn't send single line to screen");
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
use image::{
|
||||
imageops::{dither, resize, BiLevel, FilterType},
|
||||
DynamicImage, ImageBuffer, Rgb, Rgba,
|
||||
};
|
||||
use log::{error, warn};
|
||||
use scap::{
|
||||
capturer::{Capturer, Options},
|
||||
frame::convert_bgra_to_rgb,
|
||||
frame::Frame,
|
||||
};
|
||||
use servicepoint::{
|
||||
Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
|
||||
};
|
||||
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() {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||
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);
|
||||
}
|
||||
|
||||
for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) {
|
||||
*dest = src.0[0] > u8::MAX / 2;
|
||||
}
|
||||
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
bitmap.clone(),
|
||||
CompressionCode::Uncompressed,
|
||||
))
|
||||
.expect("failed to send frame to display");
|
||||
}
|
||||
}
|
||||
|
||||
fn start_capture() -> Option<Capturer> {
|
||||
if !scap::is_supported() {
|
||||
error!("platform not supported by scap");
|
||||
return None;
|
||||
}
|
||||
|
||||
if !scap::has_permission() {
|
||||
warn!("requesting screen recording permission");
|
||||
if !scap::request_permission() {
|
||||
error!("screen recording ermission denied");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("failed to create screen capture");
|
||||
capturer.start_capture();
|
||||
Some(capturer)
|
||||
}
|
||||
|
||||
fn frame_to_image(frame: Frame) -> DynamicImage {
|
||||
match frame {
|
||||
Frame::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
||||
Frame::RGBx(frame) => DynamicImage::from(
|
||||
ImageBuffer::<Rgba<_>, _>::from_raw(
|
||||
frame.width as u32,
|
||||
frame.height as u32,
|
||||
frame.data,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
Frame::BGR0(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
||||
Frame::RGB(frame) => DynamicImage::from(
|
||||
ImageBuffer::<Rgb<_>, _>::from_raw(frame.width as u32, frame.height as u32, frame.data)
|
||||
.unwrap(),
|
||||
),
|
||||
Frame::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
||||
Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"),
|
||||
}
|
||||
}
|
||||
|
||||
fn bgrx_to_rgb(width: i32, height: i32, data: Vec<u8>) -> DynamicImage {
|
||||
DynamicImage::from(
|
||||
ImageBuffer::<Rgb<_>, _>::from_raw(width as u32, height as u32, convert_bgra_to_rgb(data))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue