mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 11:48:13 +01:00
Collect full CLI interface in cli module
This commit is contained in:
234
src/cli.rs
Normal file
234
src/cli.rs
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
use clap::{Args, ValueEnum};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080";
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[clap(author, version, about)]
|
||||||
|
#[command(name = "asciinema")]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
|
||||||
|
/// asciinema server URL
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub server_url: Option<String>,
|
||||||
|
|
||||||
|
/// Quiet mode, i.e. suppress diagnostic messages
|
||||||
|
#[clap(short, long, global = true)]
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Record a terminal session
|
||||||
|
Rec(Record),
|
||||||
|
|
||||||
|
/// Replay a terminal session
|
||||||
|
Play(Play),
|
||||||
|
|
||||||
|
/// Stream a terminal session
|
||||||
|
Stream(Stream),
|
||||||
|
|
||||||
|
/// Concatenate multiple recordings
|
||||||
|
Cat(Cat),
|
||||||
|
|
||||||
|
/// Convert a recording into another format
|
||||||
|
Convert(Convert),
|
||||||
|
|
||||||
|
/// Upload a recording to an asciinema server
|
||||||
|
Upload(Upload),
|
||||||
|
|
||||||
|
/// Authenticate this CLI with an asciinema server account
|
||||||
|
Auth(Auth),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Record {
|
||||||
|
pub filename: String,
|
||||||
|
|
||||||
|
/// Enable input recording
|
||||||
|
#[arg(long, short = 'I', alias = "stdin")]
|
||||||
|
pub input: bool,
|
||||||
|
|
||||||
|
/// Append to an existing recording file
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub append: bool,
|
||||||
|
|
||||||
|
/// Recording file format [default: asciicast]
|
||||||
|
#[arg(short, long, value_enum)]
|
||||||
|
pub format: Option<Format>,
|
||||||
|
|
||||||
|
#[arg(long, hide = true)]
|
||||||
|
pub raw: bool,
|
||||||
|
|
||||||
|
/// Overwrite target file if it already exists
|
||||||
|
#[arg(long, conflicts_with = "append")]
|
||||||
|
pub overwrite: bool,
|
||||||
|
|
||||||
|
/// Command to record [default: $SHELL]
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub command: Option<String>,
|
||||||
|
|
||||||
|
/// List of env vars to save [default: TERM,SHELL]
|
||||||
|
#[arg(long)]
|
||||||
|
pub env: Option<String>,
|
||||||
|
|
||||||
|
/// Title of the recording
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub title: Option<String>,
|
||||||
|
|
||||||
|
/// Limit idle time to a given number of seconds
|
||||||
|
#[arg(short, long, value_name = "SECS")]
|
||||||
|
pub idle_time_limit: Option<f64>,
|
||||||
|
|
||||||
|
/// Override terminal size for the recorded command
|
||||||
|
#[arg(long, value_name = "COLSxROWS", value_parser = parse_tty_size)]
|
||||||
|
pub tty_size: Option<(Option<u16>, Option<u16>)>,
|
||||||
|
// pub tty_size: Option<TtySizeOverride>,
|
||||||
|
#[arg(long, hide = true)]
|
||||||
|
cols: Option<u16>,
|
||||||
|
|
||||||
|
#[arg(long, hide = true)]
|
||||||
|
rows: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Play {
|
||||||
|
#[arg(value_name = "FILENAME_OR_URL")]
|
||||||
|
pub filename: String,
|
||||||
|
|
||||||
|
/// Limit idle time to a given number of seconds
|
||||||
|
#[arg(short, long, value_name = "SECS")]
|
||||||
|
pub idle_time_limit: Option<f64>,
|
||||||
|
|
||||||
|
/// Set playback speed
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub speed: Option<f64>,
|
||||||
|
|
||||||
|
/// Loop loop loop loop
|
||||||
|
#[arg(short, long, name = "loop")]
|
||||||
|
pub loop_: bool,
|
||||||
|
|
||||||
|
/// Automatically pause on markers
|
||||||
|
#[arg(short = 'm', long)]
|
||||||
|
pub pause_on_markers: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Stream {
|
||||||
|
/// Enable input capture
|
||||||
|
#[arg(long, short = 'I', alias = "stdin")]
|
||||||
|
pub input: bool,
|
||||||
|
|
||||||
|
/// Command to stream [default: $SHELL]
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub command: Option<String>,
|
||||||
|
|
||||||
|
/// Serve the stream with the built-in HTTP server
|
||||||
|
#[arg(short, long, value_name = "IP:PORT", default_missing_value = DEFAULT_LISTEN_ADDR, num_args = 0..=1)]
|
||||||
|
pub serve: Option<SocketAddr>,
|
||||||
|
|
||||||
|
/// Relay the stream via an asciinema server
|
||||||
|
#[arg(short, long, value_name = "STREAM-ID|WS-URL", default_missing_value = "", num_args = 0..=1, value_parser = validate_forward_target)]
|
||||||
|
pub relay: Option<RelayTarget>,
|
||||||
|
|
||||||
|
/// Override terminal size for the session
|
||||||
|
#[arg(long, value_name = "COLSxROWS", value_parser = parse_tty_size)]
|
||||||
|
pub tty_size: Option<(Option<u16>, Option<u16>)>,
|
||||||
|
// pub tty_size: Option<TtySizeOverride>,
|
||||||
|
/// Log file path
|
||||||
|
#[arg(long)]
|
||||||
|
pub log_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Cat {
|
||||||
|
#[arg(required = true)]
|
||||||
|
pub filename: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Convert {
|
||||||
|
#[arg(value_name = "INPUT_FILENAME_OR_URL")]
|
||||||
|
pub input_filename: String,
|
||||||
|
|
||||||
|
pub output_filename: String,
|
||||||
|
|
||||||
|
/// Output file format [default: asciicast]
|
||||||
|
#[arg(short, long, value_enum)]
|
||||||
|
pub format: Option<Format>,
|
||||||
|
|
||||||
|
/// Overwrite target file if it already exists
|
||||||
|
#[arg(long)]
|
||||||
|
pub overwrite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Upload {
|
||||||
|
/// Filename/path of asciicast to upload
|
||||||
|
pub filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct Auth {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
|
||||||
|
pub enum Format {
|
||||||
|
Asciicast,
|
||||||
|
Raw,
|
||||||
|
Txt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RelayTarget {
|
||||||
|
StreamId(String),
|
||||||
|
WsProducerUrl(url::Url),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tty_size(s: &str) -> Result<(Option<u16>, Option<u16>), String> {
|
||||||
|
match s.split_once('x') {
|
||||||
|
Some((cols, "")) => {
|
||||||
|
let cols: u16 = cols.parse().map_err(|e: ParseIntError| e.to_string())?;
|
||||||
|
|
||||||
|
Ok((Some(cols), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(("", rows)) => {
|
||||||
|
let rows: u16 = rows.parse().map_err(|e: ParseIntError| e.to_string())?;
|
||||||
|
|
||||||
|
Ok((None, Some(rows)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((cols, rows)) => {
|
||||||
|
let cols: u16 = cols.parse().map_err(|e: ParseIntError| e.to_string())?;
|
||||||
|
let rows: u16 = rows.parse().map_err(|e: ParseIntError| e.to_string())?;
|
||||||
|
|
||||||
|
Ok((Some(cols), Some(rows)))
|
||||||
|
}
|
||||||
|
|
||||||
|
None => Err(s.to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_forward_target(s: &str) -> Result<RelayTarget, String> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
match url::Url::parse(s) {
|
||||||
|
Ok(url) => {
|
||||||
|
let scheme = url.scheme();
|
||||||
|
|
||||||
|
if scheme == "ws" || scheme == "wss" {
|
||||||
|
Ok(RelayTarget::WsProducerUrl(url))
|
||||||
|
} else {
|
||||||
|
Err("must be a WebSocket URL (ws:// or wss://)".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(url::ParseError::RelativeUrlWithoutBase) => Ok(RelayTarget::StreamId(s.to_owned())),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args;
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Auth {
|
||||||
pub struct Cli {}
|
fn run(self, config: &Config) -> Result<()> {
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self, config: &Config) -> Result<()> {
|
|
||||||
let server_url = config.get_server_url()?;
|
let server_url = config.get_server_url()?;
|
||||||
let server_hostname = server_url.host().unwrap();
|
let server_hostname = server_url.host().unwrap();
|
||||||
let auth_url = api::get_auth_url(config)?;
|
let auth_url = api::get_auth_url(config)?;
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::asciicast;
|
use crate::asciicast;
|
||||||
|
use crate::cli;
|
||||||
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Cat {
|
||||||
pub struct Cli {
|
fn run(self, _config: &Config) -> Result<()> {
|
||||||
#[arg(required = true)]
|
|
||||||
filename: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self) -> Result<()> {
|
|
||||||
let mut writer = asciicast::Writer::new(io::stdout(), 0);
|
let mut writer = asciicast::Writer::new(io::stdout(), 0);
|
||||||
let mut time_offset: u64 = 0;
|
let mut time_offset: u64 = 0;
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
|
|||||||
@@ -1,45 +1,24 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::asciicast::{self, Header};
|
use crate::asciicast::{self, Header};
|
||||||
use crate::encoder;
|
use crate::cli::{self, Format};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::encoder::{self, EncoderExt};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::{Args, ValueEnum};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Convert {
|
||||||
pub struct Cli {
|
fn run(self, _config: &Config) -> Result<()> {
|
||||||
#[arg(value_name = "INPUT_FILENAME_OR_URL")]
|
|
||||||
input_filename: String,
|
|
||||||
|
|
||||||
output_filename: String,
|
|
||||||
|
|
||||||
/// Output file format [default: asciicast]
|
|
||||||
#[arg(short, long, value_enum)]
|
|
||||||
format: Option<Format>,
|
|
||||||
|
|
||||||
/// Overwrite target file if it already exists
|
|
||||||
#[arg(long)]
|
|
||||||
overwrite: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
|
||||||
enum Format {
|
|
||||||
Asciicast,
|
|
||||||
Raw,
|
|
||||||
Txt,
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::encoder::EncoderExt;
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self) -> Result<()> {
|
|
||||||
let path = util::get_local_path(&self.input_filename)?;
|
let path = util::get_local_path(&self.input_filename)?;
|
||||||
let input = asciicast::open_from_path(&*path)?;
|
let input = asciicast::open_from_path(&*path)?;
|
||||||
let mut output = self.get_output(&input.header)?;
|
let mut output = self.get_output(&input.header)?;
|
||||||
|
|
||||||
output.encode(input)
|
output.encode(input)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cli::Convert {
|
||||||
fn get_output(&self, header: &Header) -> Result<Box<dyn encoder::Encoder>> {
|
fn get_output(&self, header: &Header) -> Result<Box<dyn encoder::Encoder>> {
|
||||||
let file = self.open_file()?;
|
let file = self.open_file()?;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ use crate::notifier;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
pub trait Command {
|
||||||
|
fn run(self, config: &Config) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
fn get_notifier(config: &Config) -> Box<dyn notifier::Notifier> {
|
fn get_notifier(config: &Config) -> Box<dyn notifier::Notifier> {
|
||||||
if config.notifications.enabled {
|
if config.notifications.enabled {
|
||||||
notifier::get_notifier(config.notifications.command.clone())
|
notifier::get_notifier(config.notifications.command.clone())
|
||||||
|
|||||||
@@ -1,36 +1,15 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::asciicast;
|
use crate::asciicast;
|
||||||
|
use crate::cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::logger;
|
use crate::logger;
|
||||||
use crate::player::{self, KeyBindings};
|
use crate::player::{self, KeyBindings};
|
||||||
use crate::tty;
|
use crate::tty;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args;
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Play {
|
||||||
pub struct Cli {
|
fn run(self, config: &Config) -> Result<()> {
|
||||||
#[arg(value_name = "FILENAME_OR_URL")]
|
|
||||||
filename: String,
|
|
||||||
|
|
||||||
/// Limit idle time to a given number of seconds
|
|
||||||
#[arg(short, long, value_name = "SECS")]
|
|
||||||
idle_time_limit: Option<f64>,
|
|
||||||
|
|
||||||
/// Set playback speed
|
|
||||||
#[arg(short, long)]
|
|
||||||
speed: Option<f64>,
|
|
||||||
|
|
||||||
/// Loop loop loop loop
|
|
||||||
#[arg(short, long, name = "loop")]
|
|
||||||
loop_: bool,
|
|
||||||
|
|
||||||
/// Automatically pause on markers
|
|
||||||
#[arg(short = 'm', long)]
|
|
||||||
pause_on_markers: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self, config: &Config) -> Result<()> {
|
|
||||||
let speed = self.speed.or(config.cmd_play_speed()).unwrap_or(1.0);
|
let speed = self.speed.or(config.cmd_play_speed()).unwrap_or(1.0);
|
||||||
let idle_time_limit = self.idle_time_limit.or(config.cmd_play_idle_time_limit());
|
let idle_time_limit = self.idle_time_limit.or(config.cmd_play_idle_time_limit());
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::asciicast;
|
use crate::asciicast;
|
||||||
|
use crate::cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::encoder;
|
use crate::encoder;
|
||||||
use crate::locale;
|
use crate::locale;
|
||||||
@@ -7,71 +9,14 @@ use crate::pty;
|
|||||||
use crate::recorder::{self, KeyBindings};
|
use crate::recorder::{self, KeyBindings};
|
||||||
use crate::tty;
|
use crate::tty;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::{Args, ValueEnum};
|
use cli::Format;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Record {
|
||||||
pub struct Cli {
|
fn run(self, config: &Config) -> Result<()> {
|
||||||
filename: String,
|
|
||||||
|
|
||||||
/// Enable input recording
|
|
||||||
#[arg(long, short = 'I', alias = "stdin")]
|
|
||||||
input: bool,
|
|
||||||
|
|
||||||
/// Append to an existing recording file
|
|
||||||
#[arg(short, long)]
|
|
||||||
append: bool,
|
|
||||||
|
|
||||||
/// Recording file format [default: asciicast]
|
|
||||||
#[arg(short, long, value_enum)]
|
|
||||||
format: Option<Format>,
|
|
||||||
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
raw: bool,
|
|
||||||
|
|
||||||
/// Overwrite target file if it already exists
|
|
||||||
#[arg(long, conflicts_with = "append")]
|
|
||||||
overwrite: bool,
|
|
||||||
|
|
||||||
/// Command to record [default: $SHELL]
|
|
||||||
#[arg(short, long)]
|
|
||||||
command: Option<String>,
|
|
||||||
|
|
||||||
/// List of env vars to save [default: TERM,SHELL]
|
|
||||||
#[arg(long)]
|
|
||||||
env: Option<String>,
|
|
||||||
|
|
||||||
/// Title of the recording
|
|
||||||
#[arg(short, long)]
|
|
||||||
title: Option<String>,
|
|
||||||
|
|
||||||
/// Limit idle time to a given number of seconds
|
|
||||||
#[arg(short, long, value_name = "SECS")]
|
|
||||||
idle_time_limit: Option<f64>,
|
|
||||||
|
|
||||||
/// Override terminal size for the recorded command
|
|
||||||
#[arg(long, value_name = "COLSxROWS")]
|
|
||||||
tty_size: Option<pty::WinsizeOverride>,
|
|
||||||
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
cols: Option<u16>,
|
|
||||||
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
rows: Option<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
|
|
||||||
enum Format {
|
|
||||||
Asciicast,
|
|
||||||
Raw,
|
|
||||||
Txt,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self, config: &Config) -> Result<()> {
|
|
||||||
locale::check_utf8_locale()?;
|
locale::check_utf8_locale()?;
|
||||||
|
|
||||||
let format = self.get_format();
|
let format = self.get_format();
|
||||||
@@ -116,7 +61,9 @@ impl Cli {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cli::Record {
|
||||||
fn get_mode(&self) -> Result<(bool, bool)> {
|
fn get_mode(&self) -> Result<(bool, bool)> {
|
||||||
let mut overwrite = self.overwrite;
|
let mut overwrite = self.overwrite;
|
||||||
let mut append = self.append;
|
let mut append = self.append;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::locale;
|
use crate::locale;
|
||||||
use crate::logger;
|
use crate::logger;
|
||||||
@@ -8,80 +10,24 @@ use crate::tty;
|
|||||||
use crate::util;
|
use crate::util;
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use clap::Args;
|
use cli::{RelayTarget, DEFAULT_LISTEN_ADDR};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080";
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
pub struct Cli {
|
|
||||||
/// Enable input capture
|
|
||||||
#[arg(long, short = 'I', alias = "stdin")]
|
|
||||||
input: bool,
|
|
||||||
|
|
||||||
/// Command to stream [default: $SHELL]
|
|
||||||
#[arg(short, long)]
|
|
||||||
command: Option<String>,
|
|
||||||
|
|
||||||
/// Serve the stream with the built-in HTTP server
|
|
||||||
#[clap(short, long, value_name = "IP:PORT", default_missing_value = DEFAULT_LISTEN_ADDR, num_args = 0..=1)]
|
|
||||||
serve: Option<SocketAddr>,
|
|
||||||
|
|
||||||
/// Relay the stream via an asciinema server
|
|
||||||
#[clap(short, long, value_name = "STREAM-ID|WS-URL", default_missing_value = "", num_args = 0..=1, value_parser = validate_forward_target)]
|
|
||||||
relay: Option<RelayTarget>,
|
|
||||||
|
|
||||||
/// Override terminal size for the session
|
|
||||||
#[arg(long, value_name = "COLSxROWS")]
|
|
||||||
tty_size: Option<pty::WinsizeOverride>,
|
|
||||||
|
|
||||||
/// Log file path
|
|
||||||
#[arg(long)]
|
|
||||||
log_file: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum RelayTarget {
|
|
||||||
StreamId(String),
|
|
||||||
WsProducerUrl(url::Url),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Relay {
|
struct Relay {
|
||||||
ws_producer_url: Url,
|
ws_producer_url: Url,
|
||||||
url: Option<Url>,
|
url: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_forward_target(s: &str) -> Result<RelayTarget, String> {
|
impl Command for cli::Stream {
|
||||||
let s = s.trim();
|
fn run(mut self, config: &Config) -> Result<()> {
|
||||||
|
|
||||||
match url::Url::parse(s) {
|
|
||||||
Ok(url) => {
|
|
||||||
let scheme = url.scheme();
|
|
||||||
|
|
||||||
if scheme == "ws" || scheme == "wss" {
|
|
||||||
Ok(RelayTarget::WsProducerUrl(url))
|
|
||||||
} else {
|
|
||||||
Err("must be a WebSocket URL (ws:// or wss://)".to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(url::ParseError::RelativeUrlWithoutBase) => Ok(RelayTarget::StreamId(s.to_owned())),
|
|
||||||
Err(e) => Err(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(mut self, config: &Config) -> Result<()> {
|
|
||||||
locale::check_utf8_locale()?;
|
locale::check_utf8_locale()?;
|
||||||
|
|
||||||
if self.serve.is_none() && self.relay.is_none() {
|
if self.serve.is_none() && self.relay.is_none() {
|
||||||
@@ -157,7 +103,9 @@ impl Cli {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cli::Stream {
|
||||||
fn get_command(&self, config: &Config) -> Option<String> {
|
fn get_command(&self, config: &Config) -> Option<String> {
|
||||||
self.command
|
self.command
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
|
use super::Command;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::asciicast;
|
use crate::asciicast;
|
||||||
|
use crate::cli;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Args;
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
impl Command for cli::Upload {
|
||||||
pub struct Cli {
|
fn run(self, config: &Config) -> Result<()> {
|
||||||
/// Filename/path of asciicast to upload
|
|
||||||
filename: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
|
||||||
pub fn run(self, config: &Config) -> Result<()> {
|
|
||||||
let _ = asciicast::open_from_path(&self.filename)?;
|
let _ = asciicast::open_from_path(&self.filename)?;
|
||||||
let response = api::upload_asciicast(&self.filename, config)?;
|
let response = api::upload_asciicast(&self.filename, config)?;
|
||||||
println!("{}", response.message.unwrap_or(response.url));
|
println!("{}", response.message.unwrap_or(response.url));
|
||||||
|
|||||||
52
src/main.rs
52
src/main.rs
@@ -1,5 +1,6 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod asciicast;
|
mod asciicast;
|
||||||
|
mod cli;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod config;
|
mod config;
|
||||||
mod encoder;
|
mod encoder;
|
||||||
@@ -13,51 +14,12 @@ mod recorder;
|
|||||||
mod streamer;
|
mod streamer;
|
||||||
mod tty;
|
mod tty;
|
||||||
mod util;
|
mod util;
|
||||||
|
use crate::cli::{Cli, Commands};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::Result;
|
use clap::Parser;
|
||||||
use clap::{Parser, Subcommand};
|
use cmd::Command;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
fn main() -> anyhow::Result<()> {
|
||||||
#[clap(author, version, about)]
|
|
||||||
#[command(name = "asciinema")]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Commands,
|
|
||||||
|
|
||||||
/// asciinema server URL
|
|
||||||
#[arg(long, global = true)]
|
|
||||||
server_url: Option<String>,
|
|
||||||
|
|
||||||
/// Quiet mode, i.e. suppress diagnostic messages
|
|
||||||
#[clap(short, long, global = true)]
|
|
||||||
quiet: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
/// Record a terminal session
|
|
||||||
Rec(cmd::rec::Cli),
|
|
||||||
|
|
||||||
/// Replay a terminal session
|
|
||||||
Play(cmd::play::Cli),
|
|
||||||
|
|
||||||
/// Stream a terminal session
|
|
||||||
Stream(cmd::stream::Cli),
|
|
||||||
|
|
||||||
/// Concatenate multiple recordings
|
|
||||||
Cat(cmd::cat::Cli),
|
|
||||||
|
|
||||||
/// Convert a recording into another format
|
|
||||||
Convert(cmd::convert::Cli),
|
|
||||||
|
|
||||||
/// Upload a recording to an asciinema server
|
|
||||||
Upload(cmd::upload::Cli),
|
|
||||||
|
|
||||||
/// Authenticate this CLI with an asciinema server account
|
|
||||||
Auth(cmd::auth::Cli),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = Config::new(cli.server_url.clone())?;
|
let config = Config::new(cli.server_url.clone())?;
|
||||||
|
|
||||||
@@ -69,8 +31,8 @@ fn main() -> Result<()> {
|
|||||||
Commands::Rec(record) => record.run(&config),
|
Commands::Rec(record) => record.run(&config),
|
||||||
Commands::Play(play) => play.run(&config),
|
Commands::Play(play) => play.run(&config),
|
||||||
Commands::Stream(stream) => stream.run(&config),
|
Commands::Stream(stream) => stream.run(&config),
|
||||||
Commands::Cat(cat) => cat.run(),
|
Commands::Cat(cat) => cat.run(&config),
|
||||||
Commands::Convert(convert) => convert.run(),
|
Commands::Convert(convert) => convert.run(&config),
|
||||||
Commands::Upload(upload) => upload.run(&config),
|
Commands::Upload(upload) => upload.run(&config),
|
||||||
Commands::Auth(auth) => auth.run(&config),
|
Commands::Auth(auth) => auth.run(&config),
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/pty.rs
74
src/pty.rs
@@ -15,9 +15,9 @@ use std::io::{self, ErrorKind, Read, Write};
|
|||||||
use std::os::fd::{AsFd, RawFd};
|
use std::os::fd::{AsFd, RawFd};
|
||||||
use std::os::fd::{BorrowedFd, OwnedFd};
|
use std::os::fd::{BorrowedFd, OwnedFd};
|
||||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
|
type TtySizeOverride = (Option<u16>, Option<u16>);
|
||||||
type ExtraEnv = HashMap<String, String>;
|
type ExtraEnv = HashMap<String, String>;
|
||||||
|
|
||||||
pub trait Recorder {
|
pub trait Recorder {
|
||||||
@@ -31,10 +31,10 @@ pub fn exec<S: AsRef<str>, T: Tty + ?Sized, R: Recorder>(
|
|||||||
command: &[S],
|
command: &[S],
|
||||||
extra_env: &ExtraEnv,
|
extra_env: &ExtraEnv,
|
||||||
tty: &mut T,
|
tty: &mut T,
|
||||||
winsize_override: Option<WinsizeOverride>,
|
tty_size_override: Option<TtySizeOverride>,
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
let winsize = get_winsize(&*tty, winsize_override.as_ref());
|
let winsize = get_winsize(&*tty, &tty_size_override);
|
||||||
recorder.start(winsize.into())?;
|
recorder.start(winsize.into())?;
|
||||||
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
|
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ pub fn exec<S: AsRef<str>, T: Tty + ?Sized, R: Recorder>(
|
|||||||
result.master.as_raw_fd(),
|
result.master.as_raw_fd(),
|
||||||
child,
|
child,
|
||||||
tty,
|
tty,
|
||||||
winsize_override,
|
tty_size_override,
|
||||||
recorder,
|
recorder,
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ fn handle_parent<T: Tty + ?Sized, R: Recorder>(
|
|||||||
master_fd: RawFd,
|
master_fd: RawFd,
|
||||||
child: unistd::Pid,
|
child: unistd::Pid,
|
||||||
tty: &mut T,
|
tty: &mut T,
|
||||||
winsize_override: Option<WinsizeOverride>,
|
tty_size_override: Option<TtySizeOverride>,
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
let wait_result = match copy(master_fd, child, tty, winsize_override, recorder) {
|
let wait_result = match copy(master_fd, child, tty, tty_size_override, recorder) {
|
||||||
Ok(Some(status)) => Ok(status),
|
Ok(Some(status)) => Ok(status),
|
||||||
Ok(None) => wait::waitpid(child, None),
|
Ok(None) => wait::waitpid(child, None),
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ fn copy<T: Tty + ?Sized, R: Recorder>(
|
|||||||
master_raw_fd: RawFd,
|
master_raw_fd: RawFd,
|
||||||
child: unistd::Pid,
|
child: unistd::Pid,
|
||||||
tty: &mut T,
|
tty: &mut T,
|
||||||
winsize_override: Option<WinsizeOverride>,
|
tty_size_override: Option<TtySizeOverride>,
|
||||||
recorder: &mut R,
|
recorder: &mut R,
|
||||||
) -> Result<Option<WaitStatus>> {
|
) -> Result<Option<WaitStatus>> {
|
||||||
let mut master = unsafe { fs::File::from_raw_fd(master_raw_fd) };
|
let mut master = unsafe { fs::File::from_raw_fd(master_raw_fd) };
|
||||||
@@ -223,7 +223,7 @@ fn copy<T: Tty + ?Sized, R: Recorder>(
|
|||||||
|
|
||||||
if sigwinch_read {
|
if sigwinch_read {
|
||||||
sigwinch_fd.flush();
|
sigwinch_fd.flush();
|
||||||
let winsize = get_winsize(&*tty, winsize_override.as_ref());
|
let winsize = get_winsize(&*tty, &tty_size_override);
|
||||||
set_pty_size(master_raw_fd, &winsize);
|
set_pty_size(master_raw_fd, &winsize);
|
||||||
recorder.resize(winsize.into());
|
recorder.resize(winsize.into());
|
||||||
}
|
}
|
||||||
@@ -287,61 +287,25 @@ fn handle_child<S: AsRef<str>>(command: &[S], extra_env: &ExtraEnv) -> Result<()
|
|||||||
unsafe { libc::_exit(1) }
|
unsafe { libc::_exit(1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum WinsizeOverride {
|
|
||||||
Full(u16, u16),
|
|
||||||
Cols(u16),
|
|
||||||
Rows(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for WinsizeOverride {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.split_once('x') {
|
|
||||||
Some((cols, "")) => {
|
|
||||||
let cols: u16 = cols.parse()?;
|
|
||||||
|
|
||||||
Ok(WinsizeOverride::Cols(cols))
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(("", rows)) => {
|
|
||||||
let rows: u16 = rows.parse()?;
|
|
||||||
|
|
||||||
Ok(WinsizeOverride::Rows(rows))
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((cols, rows)) => {
|
|
||||||
let cols: u16 = cols.parse()?;
|
|
||||||
let rows: u16 = rows.parse()?;
|
|
||||||
|
|
||||||
Ok(WinsizeOverride::Full(cols, rows))
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
|
||||||
bail!("{s}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_winsize<T: Tty + ?Sized>(
|
fn get_winsize<T: Tty + ?Sized>(
|
||||||
tty: &T,
|
tty: &T,
|
||||||
winsize_override: Option<&WinsizeOverride>,
|
tty_size_override: &Option<TtySizeOverride>,
|
||||||
) -> pty::Winsize {
|
) -> pty::Winsize {
|
||||||
let mut winsize = tty.get_size();
|
let mut winsize = tty.get_size();
|
||||||
|
|
||||||
match winsize_override {
|
match tty_size_override {
|
||||||
Some(WinsizeOverride::Full(cols, rows)) => {
|
Some((None, None)) => (),
|
||||||
|
|
||||||
|
Some((Some(cols), None)) => {
|
||||||
winsize.ws_col = *cols;
|
winsize.ws_col = *cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((None, Some(rows))) => {
|
||||||
winsize.ws_row = *rows;
|
winsize.ws_row = *rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(WinsizeOverride::Cols(cols)) => {
|
Some((Some(cols), Some(rows))) => {
|
||||||
winsize.ws_col = *cols;
|
winsize.ws_col = *cols;
|
||||||
}
|
|
||||||
|
|
||||||
Some(WinsizeOverride::Rows(rows)) => {
|
|
||||||
winsize.ws_row = *rows;
|
winsize.ws_row = *rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +402,7 @@ impl Drop for SignalFd {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::Recorder;
|
use super::Recorder;
|
||||||
use crate::pty::{ExtraEnv, WinsizeOverride};
|
use crate::pty::ExtraEnv;
|
||||||
use crate::tty::{NullTty, TtySize};
|
use crate::tty::{NullTty, TtySize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -558,7 +522,7 @@ sys.stdout.write('bar');
|
|||||||
&["true"],
|
&["true"],
|
||||||
&ExtraEnv::new(),
|
&ExtraEnv::new(),
|
||||||
&mut NullTty::open().unwrap(),
|
&mut NullTty::open().unwrap(),
|
||||||
Some(WinsizeOverride::Full(100, 50)),
|
Some((Some(100), Some(50))),
|
||||||
&mut recorder,
|
&mut recorder,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user