Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,530 changes: 973 additions & 557 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 11 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "PowerSession"
version = "0.1.11"
authors = ["Yuwei B <contact@yba.dev>"]
edition = "2021"
edition = "2024"

license = "MIT"
description = "Asciinema-compatible terminal session recorder for Windows"
Expand All @@ -13,30 +13,29 @@ keywords = ["cli", "asciinema", "terminal", "recorder", "conpty"]
categories = ["command-line-utilities"]

[dependencies]
clap = { version = "3.2.17", features = ["cargo"] }
clap = { version = "4.5", features = ["cargo"] }
log = "0.4"
fern = { version = "0.6", features = ["colored"] }
fern = { version = "0.7", features = ["colored"] }

platform-dirs = "0.3.0"

serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

uuid = { version = "1.12.1", features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
uuid = { version = "1.16.0", features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]}
] }

reqwest = { version = "0.12.4", features = ["blocking", "multipart"] }
reqwest = { version = "0.12", features = ["blocking", "multipart"] }

rustc_version_runtime = "0.3.0"
os_info = "3"
base64 = "0.13.0"
base64 = "0.22"

#[cfg(windows)]
windows = { version = "0.38.0", features=[
"alloc",
windows = { version = "0.61.1", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
Expand All @@ -45,4 +44,4 @@ windows = { version = "0.38.0", features=[
"Win32_System_Pipes",
"Win32_Storage_FileSystem",
"Win32_System_IO",
]}
] }
5 changes: 3 additions & 2 deletions src/commands/api/asciinema.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::ApiService;

use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use log::trace;
use os_info::Version;
use platform_dirs::AppDirs;
Expand All @@ -9,7 +11,6 @@ use std::fs::File;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;

use uuid::Uuid;

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -114,7 +115,7 @@ impl Asciinema {
);

let cred = format!("user:{}", config.install_id);
let cred_b64 = base64::encode_config(cred, base64::STANDARD);
let cred_b64 = BASE64_STANDARD.encode(&cred);
let hdr = format!("Basic {}", cred_b64);
let mut auth_value = header::HeaderValue::from_str(hdr.as_str()).unwrap();
auth_value.set_sensitive(true);
Expand Down
104 changes: 54 additions & 50 deletions src/commands/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,66 +92,70 @@ impl Record {
.write((serde_json::to_string(&header).unwrap() + "\n").as_bytes())
.unwrap();

let (stdin_tx, stdin_rx) = channel::<(Arc<[u8]>, usize)>();
let (stdout_tx, stdout_rx) = channel::<(Arc<[u8]>, usize)>();

thread::spawn(move || loop {
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut buf = [0; 10];
let rv = handle.read(&mut buf);
match rv {
Ok(n) if n > 0 => {
stdin_tx.send((Arc::from(buf), n)).unwrap();
}
_ => {
panic!("pty stdin closed");
let (stdin_tx, stdin_rx) = channel::<(Vec<u8>, usize)>();
let (stdout_tx, stdout_rx) = channel::<(Vec<u8>, usize)>();

thread::spawn(move || {
loop {
let stdin = std::io::stdin();
let mut handle = stdin.lock();
let mut buf = [0; 10];
let rv = handle.read(&mut buf);
match rv {
Ok(n) if n > 0 => {
stdin_tx.send((buf.to_vec(), n)).unwrap();
}
_ => {
panic!("pty stdin closed");
}
}
}
});

let output_writer = self.output_writer.clone();
let filename = self.filename.clone();

thread::spawn(move || loop {
let mut stdout = std::io::stdout();
thread::spawn(move || {
loop {
let mut stdout = std::io::stdout();

let rv = stdout_rx.recv();
match rv {
Ok((buf, len)) => {
if len == 0 {
trace!("stdout received close indicator");
println!("Record finished. Result saved to file {}", filename);
break;
}

let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("check your machine time");

let ts = now.as_secs() as f64 + now.subsec_nanos() as f64 * 1e-9
- record_start_time;
// https://github.com/asciinema/asciinema/blob/5a385765f050e04523c9d74fbf98d5afaa2deff0/asciinema/asciicast/v2.py#L119
let chars = String::from_utf8_lossy(&buf[..len]).to_string();
let data = vec![
LineItem::F64(ts),
LineItem::String("o".to_string()),
LineItem::String(chars),
];
let line = serde_json::to_string(&data).unwrap() + "\n";
output_writer
.lock()
.unwrap()
.write(line.as_bytes())
.unwrap();

stdout.write(&buf[..len]).expect("failed to write stdout");
stdout.flush().expect("failed to flush stdout");
}

let rv = stdout_rx.recv();
match rv {
Ok((buf, len)) => {
if len == 0 {
trace!("stdout received close indicator");
println!("Record finished. Result saved to file {}", filename);
Err(err) => {
error!("reading stdout: {}", err.to_string());
break;
}

let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("check your machine time");

let ts =
now.as_secs() as f64 + now.subsec_nanos() as f64 * 1e-9 - record_start_time;
// https://github.com/asciinema/asciinema/blob/5a385765f050e04523c9d74fbf98d5afaa2deff0/asciinema/asciicast/v2.py#L119
let chars = String::from_utf8_lossy(&buf[..len]).to_string();
let data = vec![
LineItem::F64(ts),
LineItem::String("o".to_string()),
LineItem::String(chars),
];
let line = serde_json::to_string(&data).unwrap() + "\n";
output_writer
.lock()
.unwrap()
.write(line.as_bytes())
.unwrap();

stdout.write(&buf[..len]).expect("failed to write stdout");
stdout.flush().expect("failed to flush stdout");
}

Err(err) => {
error!("reading stdout: {}", err.to_string());
break;
}
}
});
Expand Down
30 changes: 17 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extern crate core;
mod commands;
mod terminal;

use clap::{crate_version, AppSettings, Arg, Command};
use clap::{Arg, Command, crate_version};
use commands::{Asciinema, Auth, Play};
use commands::{Record, Upload};
use fern::colors::ColoredLevelConfig;
Expand All @@ -31,7 +31,6 @@ fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> {
fn main() {
let app = Command::new("PowerSession")
.version(crate_version!())
.setting(AppSettings::DeriveDisplayOrder)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Expand All @@ -46,14 +45,14 @@ fn main() {
.arg(
Arg::new("command")
.help("The command to record, defaults to $SHELL")
.takes_value(true)
.num_args(1)
.short('c')
.long("command"),
)
.arg(
Arg::new("force")
.help("Overwrite if session already exists")
.takes_value(false)
.num_args(0)
.short('f')
.long("force"),
),
Expand Down Expand Up @@ -97,13 +96,13 @@ fn main() {
.default_value("error")
.default_missing_value("trace")
.global(true)
.takes_value(true),
.num_args(1),
);

let m = app.get_matches();

match m.value_of("log-level") {
Some(log_level) => match log_level {
match m.get_one::<String>("log-level") {
Some(log_level) => match log_level.as_str() {
"error" => setup_logger(log::LevelFilter::Error).unwrap(),
"warn" => setup_logger(log::LevelFilter::Warn).unwrap(),
"info" => setup_logger(log::LevelFilter::Info).unwrap(),
Expand All @@ -118,15 +117,20 @@ fn main() {

match m.subcommand() {
Some(("play", play_matches)) => {
let play = Play::new(play_matches.value_of("file").unwrap().to_owned());
let play = Play::new(
play_matches
.get_one::<String>("file")
.expect("record file required")
.to_owned(),
);
play.execute();
}
Some(("rec", rec_matches)) => {
let mut record = Record::new(
rec_matches.value_of("file").unwrap().to_owned(),
rec_matches.get_one::<String>("file").unwrap().to_owned(),
None,
rec_matches.value_of("command").map(Into::into),
rec_matches.is_present("force"),
rec_matches.get_one::<String>("command").map(Into::into),
rec_matches.contains_id("force"),
);
record.execute();
}
Expand All @@ -139,12 +143,12 @@ fn main() {
let api_service = Asciinema::new();
let upload = Upload::new(
Box::new(api_service),
upload_matches.value_of("file").unwrap().to_owned(),
upload_matches.get_one::<String>("file").unwrap().to_owned(),
);
upload.execute();
}
Some(("server", new_server)) => {
let url = &new_server.value_of("url").unwrap().to_owned();
let url = &new_server.get_one::<String>("url").unwrap().to_owned();
let is_url = reqwest::Url::parse(url);
match is_url {
Ok(_) => Asciinema::change_server(url.to_string()),
Expand Down
Loading