Collect full CLI interface in cli module

This commit is contained in:
Marcin Kulik
2024-04-29 16:44:18 +02:00
parent bc8c4012ef
commit b692339d50
11 changed files with 303 additions and 297 deletions

234
src/cli.rs Normal file
View 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()),
}
}

View File

@@ -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)?;

View File

@@ -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;

View File

@@ -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()?;

View 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())

View File

@@ -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());

View File

@@ -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;

View File

@@ -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()

View File

@@ -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));

View File

@@ -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),
} }

View File

@@ -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();