2024-01-23 11:52:28 +01:00
|
|
|
use crate::asciicast;
|
2024-01-12 12:18:04 +01:00
|
|
|
use crate::config::Config;
|
2024-01-25 16:01:14 +01:00
|
|
|
use crate::encoder;
|
2023-12-22 11:20:51 +01:00
|
|
|
use crate::locale;
|
2024-01-19 16:05:38 +01:00
|
|
|
use crate::logger;
|
2023-12-22 11:20:51 +01:00
|
|
|
use crate::pty;
|
2024-01-12 12:18:04 +01:00
|
|
|
use crate::recorder::{self, KeyBindings};
|
2024-01-05 14:53:01 +01:00
|
|
|
use crate::tty;
|
2024-01-18 10:45:23 +01:00
|
|
|
use anyhow::{bail, Result};
|
2024-01-17 21:29:06 +01:00
|
|
|
use clap::{Args, ValueEnum};
|
2023-12-22 11:20:51 +01:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
2023-12-22 11:45:50 +01:00
|
|
|
#[derive(Debug, Args)]
|
|
|
|
|
pub struct Cli {
|
2023-12-22 11:20:51 +01:00
|
|
|
filename: String,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
|
|
|
|
/// Enable input recording
|
2024-01-17 21:02:47 +01:00
|
|
|
#[arg(long, short = 'I', alias = "stdin")]
|
2024-01-17 18:58:01 +01:00
|
|
|
input: bool,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
2024-01-24 21:50:34 +01:00
|
|
|
/// Append to an existing recording file
|
2024-01-17 21:09:52 +01:00
|
|
|
#[arg(short, long)]
|
2023-12-22 11:45:50 +01:00
|
|
|
append: bool,
|
|
|
|
|
|
2024-01-24 21:33:12 +01:00
|
|
|
/// Recording file format [default: asciicast]
|
|
|
|
|
#[arg(short, long, value_enum)]
|
|
|
|
|
format: Option<Format>,
|
2024-01-17 21:29:06 +01:00
|
|
|
|
|
|
|
|
#[arg(long, hide = true)]
|
2023-12-22 11:20:51 +01:00
|
|
|
raw: bool,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
|
|
|
|
/// Overwrite target file if it already exists
|
|
|
|
|
#[arg(long, conflicts_with = "append")]
|
|
|
|
|
overwrite: bool,
|
|
|
|
|
|
|
|
|
|
/// Command to record [default: $SHELL]
|
|
|
|
|
#[arg(short, long)]
|
2023-12-22 11:20:51 +01:00
|
|
|
command: Option<String>,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
2024-01-19 10:18:26 +01:00
|
|
|
/// List of env vars to save [default: TERM,SHELL]
|
2024-01-19 16:36:15 +01:00
|
|
|
#[arg(long)]
|
2024-01-19 10:18:26 +01:00
|
|
|
env: Option<String>,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
|
|
|
|
/// Title of the recording
|
|
|
|
|
#[arg(short, long)]
|
2023-12-22 11:20:51 +01:00
|
|
|
title: Option<String>,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
2024-01-03 22:36:13 +01:00
|
|
|
/// Limit idle time to a given number of seconds
|
2023-12-22 11:45:50 +01:00
|
|
|
#[arg(short, long, value_name = "SECS")]
|
2024-01-02 20:53:43 +01:00
|
|
|
idle_time_limit: Option<f64>,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
2024-01-18 10:45:23 +01:00
|
|
|
/// Override terminal size for the recorded command
|
2024-02-01 11:43:01 +01:00
|
|
|
#[arg(long, value_name = "COLSxROWS")]
|
|
|
|
|
tty_size: Option<pty::WinsizeOverride>,
|
2024-01-18 10:45:23 +01:00
|
|
|
|
|
|
|
|
#[arg(long, hide = true)]
|
2023-12-22 11:20:51 +01:00
|
|
|
cols: Option<u16>,
|
2023-12-22 11:45:50 +01:00
|
|
|
|
2024-01-18 10:45:23 +01:00
|
|
|
#[arg(long, hide = true)]
|
2023-12-22 11:20:51 +01:00
|
|
|
rows: Option<u16>,
|
2023-12-22 11:45:50 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-18 10:45:23 +01:00
|
|
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
2024-01-17 21:29:06 +01:00
|
|
|
enum Format {
|
|
|
|
|
Asciicast,
|
|
|
|
|
Raw,
|
2024-01-24 21:19:10 +01:00
|
|
|
Txt,
|
2024-01-17 21:29:06 +01:00
|
|
|
}
|
|
|
|
|
|
2023-12-22 11:45:50 +01:00
|
|
|
impl Cli {
|
2024-01-12 12:18:04 +01:00
|
|
|
pub fn run(self, config: &Config) -> Result<()> {
|
2023-12-22 11:45:50 +01:00
|
|
|
locale::check_utf8_locale()?;
|
2023-12-22 11:20:51 +01:00
|
|
|
|
2024-01-18 15:40:59 +01:00
|
|
|
let (append, overwrite) = self.get_mode()?;
|
|
|
|
|
let file = self.open_file(append, overwrite)?;
|
2024-01-18 14:23:35 +01:00
|
|
|
let command = self.get_command(config);
|
2024-01-23 11:52:28 +01:00
|
|
|
let output = self.get_output(file, append, config)?;
|
2024-01-12 12:18:04 +01:00
|
|
|
let keys = get_key_bindings(config)?;
|
2024-02-01 10:24:04 +01:00
|
|
|
let notifier = super::get_notifier(config);
|
2024-01-23 11:52:28 +01:00
|
|
|
let record_input = self.input || config.cmd_rec_input();
|
2024-02-01 10:24:04 +01:00
|
|
|
let exec_command = super::build_exec_command(command.as_ref().cloned());
|
|
|
|
|
let exec_extra_env = super::build_exec_extra_env();
|
2023-12-22 11:20:51 +01:00
|
|
|
|
2024-01-19 16:05:38 +01:00
|
|
|
logger::info!("Recording session started, writing to {}", self.filename);
|
2024-01-19 16:35:08 +01:00
|
|
|
|
|
|
|
|
if command.is_none() {
|
|
|
|
|
logger::info!("Press <ctrl+d> or type 'exit' to end");
|
|
|
|
|
}
|
2023-12-22 12:11:50 +01:00
|
|
|
|
2024-01-10 15:53:47 +01:00
|
|
|
{
|
|
|
|
|
let mut tty: Box<dyn tty::Tty> = if let Ok(dev_tty) = tty::DevTty::open() {
|
|
|
|
|
Box::new(dev_tty)
|
|
|
|
|
} else {
|
2024-01-19 16:05:38 +01:00
|
|
|
logger::info!("TTY not available, recording in headless mode");
|
2024-01-10 15:53:47 +01:00
|
|
|
Box::new(tty::NullTty::open()?)
|
|
|
|
|
};
|
2024-01-05 14:53:01 +01:00
|
|
|
|
2024-01-24 21:19:10 +01:00
|
|
|
let mut recorder = recorder::Recorder::new(output, record_input, keys, notifier);
|
|
|
|
|
|
2024-01-10 15:53:47 +01:00
|
|
|
pty::exec(
|
|
|
|
|
&exec_command,
|
|
|
|
|
&exec_extra_env,
|
|
|
|
|
&mut *tty,
|
2024-02-01 11:43:01 +01:00
|
|
|
self.tty_size,
|
2024-01-10 15:53:47 +01:00
|
|
|
&mut recorder,
|
|
|
|
|
)?;
|
|
|
|
|
}
|
2023-12-22 11:20:51 +01:00
|
|
|
|
2024-01-19 16:05:38 +01:00
|
|
|
logger::info!("Recording session ended");
|
2023-12-22 12:11:50 +01:00
|
|
|
|
2023-12-22 11:45:50 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-01-18 10:45:23 +01:00
|
|
|
|
2024-01-18 15:40:59 +01:00
|
|
|
fn get_mode(&self) -> Result<(bool, bool)> {
|
|
|
|
|
let mut overwrite = self.overwrite;
|
|
|
|
|
let mut append = self.append;
|
|
|
|
|
let path = Path::new(&self.filename);
|
|
|
|
|
|
|
|
|
|
if path.exists() {
|
|
|
|
|
let metadata = fs::metadata(path)?;
|
|
|
|
|
|
|
|
|
|
if metadata.len() == 0 {
|
|
|
|
|
overwrite = true;
|
|
|
|
|
append = false;
|
|
|
|
|
}
|
2024-01-19 10:34:06 +01:00
|
|
|
|
|
|
|
|
if !append && !overwrite {
|
|
|
|
|
bail!("file exists, use --overwrite or --append");
|
|
|
|
|
}
|
2024-01-18 15:40:59 +01:00
|
|
|
} else {
|
|
|
|
|
append = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((append, overwrite))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_file(&self, append: bool, overwrite: bool) -> Result<fs::File> {
|
|
|
|
|
let file = fs::OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.append(append)
|
|
|
|
|
.create(overwrite)
|
|
|
|
|
.create_new(!overwrite && !append)
|
|
|
|
|
.truncate(overwrite)
|
|
|
|
|
.open(&self.filename)?;
|
|
|
|
|
|
|
|
|
|
Ok(file)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 11:52:28 +01:00
|
|
|
fn get_output(
|
2024-01-18 15:25:40 +01:00
|
|
|
&self,
|
|
|
|
|
file: fs::File,
|
|
|
|
|
append: bool,
|
2024-01-23 11:52:28 +01:00
|
|
|
config: &Config,
|
|
|
|
|
) -> Result<Box<dyn recorder::Output + Send>> {
|
2024-01-24 21:33:12 +01:00
|
|
|
let format = self.format.unwrap_or_else(|| {
|
|
|
|
|
if self.raw {
|
|
|
|
|
Format::Raw
|
|
|
|
|
} else if self.filename.to_lowercase().ends_with(".txt") {
|
|
|
|
|
Format::Txt
|
|
|
|
|
} else {
|
|
|
|
|
Format::Asciicast
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-01-18 15:25:40 +01:00
|
|
|
|
|
|
|
|
match format {
|
|
|
|
|
Format::Asciicast => {
|
|
|
|
|
let time_offset = if append {
|
|
|
|
|
asciicast::get_duration(&self.filename)?
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-23 11:52:28 +01:00
|
|
|
let metadata = self.build_asciicast_metadata(config);
|
|
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
Ok(Box::new(encoder::AsciicastEncoder::new(
|
2024-01-23 11:52:28 +01:00
|
|
|
file,
|
|
|
|
|
append,
|
|
|
|
|
time_offset,
|
|
|
|
|
metadata,
|
|
|
|
|
)))
|
2024-01-18 15:25:40 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
Format::Raw => Ok(Box::new(encoder::RawEncoder::new(file, append))),
|
2024-01-28 18:23:07 +01:00
|
|
|
Format::Txt => Ok(Box::new(encoder::TextEncoder::new(file))),
|
2024-01-18 15:25:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-18 14:23:35 +01:00
|
|
|
fn get_command(&self, config: &Config) -> Option<String> {
|
|
|
|
|
self.command.as_ref().cloned().or(config.cmd_rec_command())
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
fn build_asciicast_metadata(&self, config: &Config) -> encoder::Metadata {
|
2024-01-19 09:45:32 +01:00
|
|
|
let idle_time_limit = self.idle_time_limit.or(config.cmd_rec_idle_time_limit());
|
2024-01-23 11:52:28 +01:00
|
|
|
let command = self.get_command(config);
|
2024-01-19 09:45:32 +01:00
|
|
|
|
2024-01-19 10:18:26 +01:00
|
|
|
let env = self
|
|
|
|
|
.env
|
|
|
|
|
.as_ref()
|
|
|
|
|
.cloned()
|
|
|
|
|
.or(config.cmd_rec_env())
|
|
|
|
|
.unwrap_or(String::from("TERM,SHELL"));
|
|
|
|
|
|
2024-01-25 16:01:14 +01:00
|
|
|
encoder::Metadata {
|
2024-01-19 09:45:32 +01:00
|
|
|
idle_time_limit,
|
2024-01-18 14:27:44 +01:00
|
|
|
command,
|
|
|
|
|
title: self.title.clone(),
|
2024-01-25 16:01:14 +01:00
|
|
|
env: Some(capture_env(&env)),
|
2024-01-18 14:27:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 11:20:51 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 12:18:04 +01:00
|
|
|
fn get_key_bindings(config: &Config) -> Result<KeyBindings> {
|
|
|
|
|
let mut keys = KeyBindings::default();
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.cmd_rec_prefix_key()? {
|
|
|
|
|
keys.prefix = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.cmd_rec_pause_key()? {
|
|
|
|
|
keys.pause = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.cmd_rec_add_marker_key()? {
|
|
|
|
|
keys.add_marker = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(keys)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-22 11:20:51 +01:00
|
|
|
fn capture_env(vars: &str) -> HashMap<String, String> {
|
|
|
|
|
let vars = vars.split(',').collect::<HashSet<_>>();
|
|
|
|
|
|
|
|
|
|
env::vars()
|
|
|
|
|
.filter(|(k, _v)| vars.contains(&k.as_str()))
|
|
|
|
|
.collect::<HashMap<_, _>>()
|
|
|
|
|
}
|