2025-03-12 13:15:02 +01:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::fs::{self, OpenOptions};
|
|
|
|
|
use std::io::LineWriter;
|
|
|
|
|
use std::net::TcpListener;
|
2025-05-04 21:41:59 +02:00
|
|
|
use std::path::Path;
|
2025-05-29 13:49:53 +02:00
|
|
|
use std::process::ExitCode;
|
2025-06-05 12:26:48 +02:00
|
|
|
use std::time::{Duration, SystemTime};
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
|
|
|
|
use tokio::runtime::Runtime;
|
|
|
|
|
use tokio::time;
|
|
|
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
|
use tracing::debug;
|
|
|
|
|
use tracing::level_filters::LevelFilter;
|
|
|
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
|
use url::{form_urlencoded, Url};
|
|
|
|
|
|
|
|
|
|
use crate::api;
|
2025-05-08 14:54:13 +02:00
|
|
|
use crate::asciicast::{self, Version};
|
2025-03-12 13:15:02 +01:00
|
|
|
use crate::cli::{self, Format, RelayTarget};
|
|
|
|
|
use crate::config::{self, Config};
|
2025-04-24 11:00:14 +02:00
|
|
|
use crate::encoder::{AsciicastV2Encoder, AsciicastV3Encoder, RawEncoder, TextEncoder};
|
2025-06-05 12:26:48 +02:00
|
|
|
use crate::file_writer::FileWriter;
|
2025-03-12 13:15:02 +01:00
|
|
|
use crate::forwarder;
|
2025-05-11 20:24:24 +02:00
|
|
|
use crate::hash;
|
2025-03-12 13:15:02 +01:00
|
|
|
use crate::locale;
|
|
|
|
|
use crate::notifier::{self, Notifier, NullNotifier};
|
|
|
|
|
use crate::pty;
|
|
|
|
|
use crate::server;
|
2025-06-05 12:26:48 +02:00
|
|
|
use crate::session::{self, KeyBindings, Metadata, Session, TermInfo};
|
2025-04-12 07:58:14 +02:00
|
|
|
use crate::status;
|
2025-03-12 13:15:02 +01:00
|
|
|
use crate::stream::Stream;
|
2025-04-11 16:17:11 +02:00
|
|
|
use crate::tty::{DevTty, FixedSizeTty, NullTty, Tty};
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
impl cli::Session {
|
2025-05-29 13:49:53 +02:00
|
|
|
pub fn run(mut self) -> Result<ExitCode> {
|
2025-03-12 13:15:02 +01:00
|
|
|
locale::check_utf8_locale()?;
|
|
|
|
|
|
2025-05-29 13:16:37 +02:00
|
|
|
let config = Config::new(self.server_url.clone())?;
|
2025-03-12 13:15:02 +01:00
|
|
|
let runtime = Runtime::new()?;
|
2025-05-07 16:42:04 +02:00
|
|
|
let command = self.get_command(&config.recording);
|
|
|
|
|
let keys = get_key_bindings(&config.recording)?;
|
2025-05-29 13:16:37 +02:00
|
|
|
let notifier = notifier::threaded(get_notifier(&config));
|
2025-05-07 16:42:04 +02:00
|
|
|
let record_input = self.rec_input || config.recording.rec_input;
|
2025-06-05 12:26:48 +02:00
|
|
|
let signal_fd = pty::open_signal_fd()?;
|
|
|
|
|
let metadata = self.get_session_metadata(&config.recording)?;
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-05-04 21:41:59 +02:00
|
|
|
let file_writer = self
|
2025-05-03 17:28:10 +02:00
|
|
|
.output_file
|
2025-03-12 13:15:02 +01:00
|
|
|
.as_ref()
|
2025-06-05 12:26:48 +02:00
|
|
|
.map(|path| self.get_file_writer(path, &metadata, notifier.clone()))
|
2025-03-12 13:15:02 +01:00
|
|
|
.transpose()?;
|
|
|
|
|
|
|
|
|
|
let mut listener = self
|
2025-05-06 14:27:46 +02:00
|
|
|
.stream_local
|
2025-03-12 13:15:02 +01:00
|
|
|
.take()
|
|
|
|
|
.map(TcpListener::bind)
|
|
|
|
|
.transpose()
|
|
|
|
|
.context("cannot start listener")?;
|
|
|
|
|
|
|
|
|
|
let mut relay = self
|
2025-05-06 14:27:46 +02:00
|
|
|
.stream_remote
|
2025-03-12 13:15:02 +01:00
|
|
|
.take()
|
2025-06-05 12:26:48 +02:00
|
|
|
.map(|target| get_relay(target, &metadata, &config))
|
2025-03-12 13:15:02 +01:00
|
|
|
.transpose()?;
|
|
|
|
|
|
|
|
|
|
let relay_id = relay.as_ref().map(|r| r.id());
|
|
|
|
|
let parent_session_relay_id = get_parent_session_relay_id();
|
|
|
|
|
|
|
|
|
|
if relay_id.is_some()
|
|
|
|
|
&& parent_session_relay_id.is_some()
|
|
|
|
|
&& relay_id == parent_session_relay_id
|
|
|
|
|
{
|
|
|
|
|
if let Some(Relay { url: Some(url), .. }) = relay {
|
|
|
|
|
bail!("This shell is already being streamed at {url}");
|
|
|
|
|
} else {
|
|
|
|
|
bail!("This shell is already being streamed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if listener.is_some() || relay.is_some() {
|
|
|
|
|
self.init_logging()?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!("asciinema session started");
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
let mut output_count = 0;
|
|
|
|
|
|
2025-05-04 21:41:59 +02:00
|
|
|
if let Some(path) = self.output_file.as_ref() {
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!("Recording to {}", path);
|
2025-06-05 12:26:48 +02:00
|
|
|
output_count += 1;
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(listener) = &listener {
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!(
|
2025-03-12 13:15:02 +01:00
|
|
|
"Live streaming at http://{}",
|
|
|
|
|
listener.local_addr().unwrap()
|
|
|
|
|
);
|
2025-06-05 12:26:48 +02:00
|
|
|
|
|
|
|
|
output_count += 1;
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(Relay { url: Some(url), .. }) = &relay {
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!("Live streaming at {}", url);
|
2025-06-05 12:26:48 +02:00
|
|
|
output_count += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if output_count == 0 {
|
|
|
|
|
status::warning!("No outputs enabled, consider using -o, -l, or -r");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if command.is_none() {
|
|
|
|
|
status::info!("Press <ctrl+d> or type 'exit' to end");
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stream = Stream::new();
|
|
|
|
|
let shutdown_token = CancellationToken::new();
|
2025-06-05 12:26:48 +02:00
|
|
|
let mut outputs: Vec<Box<dyn session::Output>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
if let Some(writer) = file_writer {
|
|
|
|
|
let output = writer.start()?;
|
|
|
|
|
outputs.push(Box::new(output));
|
|
|
|
|
}
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
let server = listener.take().map(|listener| {
|
|
|
|
|
runtime.spawn(server::serve(
|
|
|
|
|
listener,
|
|
|
|
|
stream.subscriber(),
|
|
|
|
|
shutdown_token.clone(),
|
|
|
|
|
))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let forwarder = relay.take().map(|relay| {
|
|
|
|
|
runtime.spawn(forwarder::forward(
|
|
|
|
|
relay.ws_producer_url,
|
|
|
|
|
stream.subscriber(),
|
|
|
|
|
notifier.clone(),
|
|
|
|
|
shutdown_token.clone(),
|
|
|
|
|
))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if server.is_some() || forwarder.is_some() {
|
2025-06-05 12:26:48 +02:00
|
|
|
let output = stream.start(runtime.handle().clone(), &metadata);
|
2025-03-12 13:15:02 +01:00
|
|
|
outputs.push(Box::new(output));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let exec_command = build_exec_command(command.as_ref().cloned());
|
|
|
|
|
let exec_extra_env = build_exec_extra_env(relay_id.as_ref());
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
let exit_status = {
|
2025-04-12 07:48:59 +02:00
|
|
|
let mut tty = self.get_tty(true)?;
|
2025-06-05 12:26:48 +02:00
|
|
|
|
|
|
|
|
let mut session =
|
|
|
|
|
Session::new(outputs, metadata.term.size, record_input, keys, notifier);
|
|
|
|
|
|
|
|
|
|
pty::exec(
|
|
|
|
|
&exec_command,
|
|
|
|
|
&exec_extra_env,
|
|
|
|
|
metadata.term.size,
|
|
|
|
|
&mut tty,
|
|
|
|
|
&mut session,
|
|
|
|
|
signal_fd,
|
|
|
|
|
)?
|
2025-05-29 13:49:53 +02:00
|
|
|
};
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
runtime.block_on(async {
|
|
|
|
|
debug!("session shutting down...");
|
|
|
|
|
shutdown_token.cancel();
|
|
|
|
|
|
|
|
|
|
if let Some(task) = server {
|
|
|
|
|
debug!("waiting for server shutdown...");
|
|
|
|
|
let _ = time::timeout(Duration::from_secs(5), task).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(task) = forwarder {
|
|
|
|
|
debug!("waiting for forwarder shutdown...");
|
|
|
|
|
let _ = time::timeout(Duration::from_secs(5), task).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug!("shutdown complete");
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!("asciinema session ended");
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-05-29 13:49:53 +02:00
|
|
|
if !self.return_ || exit_status == 0 {
|
|
|
|
|
Ok(ExitCode::from(0))
|
|
|
|
|
} else if exit_status > 0 {
|
|
|
|
|
Ok(ExitCode::from(exit_status as u8))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(ExitCode::from(1))
|
|
|
|
|
}
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
fn get_session_metadata(&self, config: &config::Recording) -> Result<Metadata> {
|
|
|
|
|
Ok(Metadata {
|
|
|
|
|
time: SystemTime::now(),
|
|
|
|
|
term: self.get_term_info()?,
|
|
|
|
|
idle_time_limit: self.idle_time_limit.or(config.idle_time_limit),
|
|
|
|
|
command: self.get_command(config),
|
|
|
|
|
title: self.title.clone(),
|
|
|
|
|
env: capture_env(self.rec_env.clone(), config),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_term_info(&self) -> Result<TermInfo> {
|
|
|
|
|
let tty = self.get_tty(false)?;
|
|
|
|
|
|
|
|
|
|
Ok(TermInfo {
|
|
|
|
|
type_: env::var("TERM").ok(),
|
|
|
|
|
version: tty.get_version(),
|
|
|
|
|
size: tty.get_size().into(),
|
|
|
|
|
theme: tty.get_theme(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 13:30:11 +02:00
|
|
|
fn get_file_writer<N: Notifier + 'static>(
|
2025-04-04 18:14:56 +02:00
|
|
|
&self,
|
|
|
|
|
path: &str,
|
2025-06-05 12:26:48 +02:00
|
|
|
metadata: &Metadata,
|
2025-04-09 13:30:11 +02:00
|
|
|
notifier: N,
|
2025-06-05 12:26:48 +02:00
|
|
|
) -> Result<FileWriter> {
|
2025-03-12 13:15:02 +01:00
|
|
|
let mut overwrite = self.overwrite;
|
|
|
|
|
let mut append = self.append;
|
|
|
|
|
let path = Path::new(path);
|
|
|
|
|
|
|
|
|
|
if path.exists() {
|
|
|
|
|
let metadata = fs::metadata(path)?;
|
|
|
|
|
|
|
|
|
|
if metadata.len() == 0 {
|
|
|
|
|
overwrite = true;
|
|
|
|
|
append = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !append && !overwrite {
|
|
|
|
|
bail!("file exists, use --overwrite or --append");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
append = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-01 09:32:40 +02:00
|
|
|
if let Some(dir) = path.parent() {
|
|
|
|
|
let _ = fs::create_dir_all(dir);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:54:13 +02:00
|
|
|
let format = self.output_format.map(Ok).unwrap_or_else(|| {
|
|
|
|
|
if path.extension().is_some_and(|ext| ext == "txt") {
|
|
|
|
|
Ok(Format::Txt)
|
|
|
|
|
} else if append {
|
|
|
|
|
match asciicast::open_from_path(path) {
|
|
|
|
|
Ok(cast) => match cast.version {
|
|
|
|
|
Version::One => bail!("appending to asciicast v1 files is not supported"),
|
|
|
|
|
Version::Two => Ok(Format::AsciicastV2),
|
|
|
|
|
Version::Three => Ok(Format::AsciicastV3),
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
Err(e) => bail!("can't append: {e}"),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Ok(Format::AsciicastV3)
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
|
2025-03-12 13:15:02 +01:00
|
|
|
let file = OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.append(append)
|
|
|
|
|
.create(overwrite)
|
|
|
|
|
.create_new(!overwrite && !append)
|
|
|
|
|
.truncate(overwrite)
|
|
|
|
|
.open(path)?;
|
|
|
|
|
|
2025-04-09 13:30:11 +02:00
|
|
|
let notifier = Box::new(notifier);
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
let file_writer = match format {
|
2025-04-24 11:00:14 +02:00
|
|
|
Format::AsciicastV3 => {
|
2025-03-12 13:15:02 +01:00
|
|
|
let writer = Box::new(LineWriter::new(file));
|
2025-04-24 11:00:14 +02:00
|
|
|
let encoder = Box::new(AsciicastV3Encoder::new(append));
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
FileWriter::new(writer, encoder, notifier, metadata.clone())
|
2025-04-24 11:00:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Format::AsciicastV2 => {
|
2025-05-08 14:54:13 +02:00
|
|
|
let time_offset = if append {
|
|
|
|
|
asciicast::get_duration(path)?
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-24 11:00:14 +02:00
|
|
|
let writer = Box::new(LineWriter::new(file));
|
|
|
|
|
let encoder = Box::new(AsciicastV2Encoder::new(append, time_offset));
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
FileWriter::new(writer, encoder, notifier, metadata.clone())
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Format::Raw => {
|
|
|
|
|
let writer = Box::new(file);
|
2025-06-01 09:43:05 +02:00
|
|
|
let encoder = Box::new(RawEncoder::new());
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
FileWriter::new(writer, encoder, notifier, metadata.clone())
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Format::Txt => {
|
|
|
|
|
let writer = Box::new(file);
|
|
|
|
|
let encoder = Box::new(TextEncoder::new());
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
FileWriter::new(writer, encoder, notifier, metadata.clone())
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
Ok(file_writer)
|
2025-04-11 16:17:11 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-07 16:42:04 +02:00
|
|
|
fn get_command(&self, config: &config::Recording) -> Option<String> {
|
2025-03-12 13:15:02 +01:00
|
|
|
self.command.as_ref().cloned().or(config.command.clone())
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-12 07:48:59 +02:00
|
|
|
fn get_tty(&self, quiet: bool) -> Result<impl Tty> {
|
2025-05-07 17:15:09 +02:00
|
|
|
let (cols, rows) = self.window_size.unwrap_or((None, None));
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
if self.headless {
|
|
|
|
|
Ok(FixedSizeTty::new(NullTty::open()?, cols, rows))
|
|
|
|
|
} else if let Ok(dev_tty) = DevTty::open() {
|
|
|
|
|
Ok(FixedSizeTty::new(dev_tty, cols, rows))
|
|
|
|
|
} else {
|
2025-04-12 07:48:59 +02:00
|
|
|
if !quiet {
|
2025-04-12 07:58:14 +02:00
|
|
|
status::info!("TTY not available, recording in headless mode");
|
2025-04-12 07:48:59 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-12 13:15:02 +01:00
|
|
|
Ok(FixedSizeTty::new(NullTty::open()?, cols, rows))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init_logging(&self) -> Result<()> {
|
|
|
|
|
let log_file = self.log_file.as_ref().cloned();
|
|
|
|
|
|
|
|
|
|
if let Some(path) = &log_file {
|
|
|
|
|
let file = OpenOptions::new()
|
|
|
|
|
.create(true)
|
|
|
|
|
.append(true)
|
|
|
|
|
.open(path)
|
|
|
|
|
.map_err(|e| anyhow!("cannot open log file {}: {}", path.to_string_lossy(), e))?;
|
|
|
|
|
|
|
|
|
|
let filter = EnvFilter::builder()
|
|
|
|
|
.with_default_directive(LevelFilter::INFO.into())
|
|
|
|
|
.from_env_lossy();
|
|
|
|
|
|
|
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_ansi(false)
|
|
|
|
|
.with_env_filter(filter)
|
|
|
|
|
.with_writer(file)
|
|
|
|
|
.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Relay {
|
|
|
|
|
ws_producer_url: Url,
|
|
|
|
|
url: Option<Url>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Relay {
|
|
|
|
|
fn id(&self) -> String {
|
2025-05-11 20:24:24 +02:00
|
|
|
format!("{:x}", hash::fnv1a_128(self.ws_producer_url.as_ref()))
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
fn get_relay(target: RelayTarget, metadata: &Metadata, config: &Config) -> Result<Relay> {
|
2025-03-12 13:15:02 +01:00
|
|
|
match target {
|
|
|
|
|
RelayTarget::StreamId(id) => {
|
|
|
|
|
let stream = api::create_user_stream(id, config)?;
|
2025-06-05 12:26:48 +02:00
|
|
|
let ws_producer_url = build_producer_url(&stream.ws_producer_url, metadata)?;
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
Ok(Relay {
|
|
|
|
|
ws_producer_url,
|
|
|
|
|
url: Some(stream.url.parse()?),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RelayTarget::WsProducerUrl(url) => Ok(Relay {
|
|
|
|
|
ws_producer_url: url,
|
|
|
|
|
url: None,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
fn build_producer_url(url: &str, metadata: &Metadata) -> Result<Url> {
|
2025-03-12 13:15:02 +01:00
|
|
|
let mut url: Url = url.parse()?;
|
2025-04-18 22:29:26 +02:00
|
|
|
let mut params = Vec::new();
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
if let Some(type_) = &metadata.term.type_ {
|
|
|
|
|
params.push(("term[type]".to_string(), type_.clone()));
|
2025-04-18 22:29:26 +02:00
|
|
|
}
|
2025-03-12 13:15:02 +01:00
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
if let Some(version) = &metadata.term.version {
|
|
|
|
|
params.push(("term[version]".to_string(), version.clone()));
|
2025-04-11 16:17:11 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-18 22:29:26 +02:00
|
|
|
if let Ok(shell) = env::var("SHELL") {
|
|
|
|
|
params.push(("shell".to_string(), shell));
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
if let Some(title) = &metadata.title {
|
|
|
|
|
params.push(("title".to_string(), title.clone()));
|
2025-05-29 13:38:51 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-05 12:26:48 +02:00
|
|
|
for (k, v) in &metadata.env {
|
2025-04-04 18:14:56 +02:00
|
|
|
params.push((format!("env[{k}]"), v.to_string()));
|
2025-03-12 13:15:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let params = params.into_iter().filter(|(_k, v)| !v.is_empty());
|
|
|
|
|
|
|
|
|
|
let query = form_urlencoded::Serializer::new(String::new())
|
|
|
|
|
.extend_pairs(params)
|
|
|
|
|
.finish();
|
|
|
|
|
|
|
|
|
|
url.set_query(Some(&query));
|
|
|
|
|
|
|
|
|
|
Ok(url)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 16:42:04 +02:00
|
|
|
fn get_key_bindings(config: &config::Recording) -> Result<KeyBindings> {
|
2025-03-12 13:15:02 +01:00
|
|
|
let mut keys = KeyBindings::default();
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.prefix_key()? {
|
|
|
|
|
keys.prefix = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.pause_key()? {
|
|
|
|
|
keys.pause = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(key) = config.add_marker_key()? {
|
|
|
|
|
keys.add_marker = key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(keys)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 16:42:04 +02:00
|
|
|
fn capture_env(var_names: Option<String>, config: &config::Recording) -> HashMap<String, String> {
|
2025-04-04 18:14:56 +02:00
|
|
|
let var_names = var_names
|
2025-05-03 17:28:10 +02:00
|
|
|
.or(config.rec_env.clone())
|
2025-05-04 21:41:59 +02:00
|
|
|
.unwrap_or(String::from("SHELL"));
|
2025-04-04 18:14:56 +02:00
|
|
|
|
|
|
|
|
let vars = var_names.split(',').collect::<HashSet<_>>();
|
2025-03-12 13:15:02 +01:00
|
|
|
|
|
|
|
|
env::vars()
|
|
|
|
|
.filter(|(k, _v)| vars.contains(&k.as_str()))
|
|
|
|
|
.collect::<HashMap<_, _>>()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_notifier(config: &Config) -> Box<dyn Notifier> {
|
|
|
|
|
if config.notifications.enabled {
|
|
|
|
|
notifier::get_notifier(config.notifications.command.clone())
|
|
|
|
|
} else {
|
|
|
|
|
Box::new(NullNotifier)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_exec_command(command: Option<String>) -> Vec<String> {
|
|
|
|
|
let command = command
|
|
|
|
|
.or(env::var("SHELL").ok())
|
|
|
|
|
.unwrap_or("/bin/sh".to_owned());
|
|
|
|
|
|
|
|
|
|
vec!["/bin/sh".to_owned(), "-c".to_owned(), command]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_exec_extra_env(relay_id: Option<&String>) -> HashMap<String, String> {
|
|
|
|
|
let mut env = HashMap::new();
|
|
|
|
|
|
|
|
|
|
env.insert("ASCIINEMA_REC".to_owned(), "1".to_owned());
|
|
|
|
|
|
|
|
|
|
if let Some(id) = relay_id {
|
|
|
|
|
env.insert("ASCIINEMA_RELAY_ID".to_owned(), id.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_parent_session_relay_id() -> Option<String> {
|
|
|
|
|
env::var("ASCIINEMA_RELAY_ID").ok()
|
|
|
|
|
}
|