Option to override cols/rows of the recorded process

This adds `--cols <n>` / `--rows <n>` options to `rec` command. This disables
autodection of terminal size, and reports fake fixed number of columns/rows to
the recorded process.
This commit is contained in:
Marcin Kulik
2022-02-16 23:01:15 +01:00
parent 41d2476c11
commit fd09df89f6
8 changed files with 83 additions and 29 deletions

View File

@@ -228,6 +228,8 @@ Available options:
to `SHELL,TERM` to `SHELL,TERM`
- `-t, --title=<title>` - Specify the title of the asciicast - `-t, --title=<title>` - Specify the title of the asciicast
- `-i, --idle-time-limit=<sec>` - Limit recorded terminal inactivity to max `<sec>` seconds - `-i, --idle-time-limit=<sec>` - Limit recorded terminal inactivity to max `<sec>` seconds
- `--cols=<n>` - Override terminal columns for recorded process
- `--rows=<n>` - Override terminal rows for recorded process
- `-y, --yes` - Answer "yes" to all prompts (e.g. upload confirmation) - `-y, --yes` - Answer "yes" to all prompts (e.g. upload confirmation)
- `-q, --quiet` - Be quiet, suppress all notices/warnings (implies -y) - `-q, --quiet` - Be quiet, suppress all notices/warnings (implies -y)

View File

@@ -12,6 +12,14 @@ from .commands.record import RecordCommand
from .commands.upload import UploadCommand from .commands.upload import UploadCommand
def positive_int(value: str) -> int:
_value = int(value)
if _value <= 0:
raise argparse.ArgumentTypeError("must be positive")
return _value
def positive_float(value: str) -> float: def positive_float(value: str) -> float:
_value = float(value) _value = float(value)
if _value <= 0.0: if _value <= 0.0:
@@ -120,6 +128,18 @@ For help on a specific command run:
type=positive_float, type=positive_float,
default=maybe_str(cfg.record_idle_time_limit), default=maybe_str(cfg.record_idle_time_limit),
) )
parser_rec.add_argument(
"--cols",
help="override terminal columns for recorded process",
type=positive_int,
default=None,
)
parser_rec.add_argument(
"--rows",
help="override terminal rows for recorded process",
type=positive_int,
default=None,
)
parser_rec.add_argument( parser_rec.add_argument(
"-y", "-y",
"--yes", "--yes",

View File

@@ -21,6 +21,8 @@ class RecordCommand(Command): # pylint: disable=too-many-instance-attributes
self.title = args.title self.title = args.title
self.assume_yes = args.yes or args.quiet self.assume_yes = args.yes or args.quiet
self.idle_time_limit = args.idle_time_limit self.idle_time_limit = args.idle_time_limit
self.cols_override = args.cols
self.rows_override = args.rows
self.append = args.append self.append = args.append
self.overwrite = args.overwrite self.overwrite = args.overwrite
self.raw = args.raw self.raw = args.raw
@@ -109,6 +111,8 @@ class RecordCommand(Command): # pylint: disable=too-many-instance-attributes
writer=self.writer, writer=self.writer,
notifier=self.notifier, notifier=self.notifier,
key_bindings=self.key_bindings, key_bindings=self.key_bindings,
cols_override=self.cols_override,
rows_override=self.rows_override,
) )
except v2.LoadError: except v2.LoadError:
self.print_error( self.print_error(

View File

@@ -8,7 +8,7 @@ import signal
import struct import struct
import termios import termios
import time import time
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from .term import raw from .term import raw
@@ -17,11 +17,14 @@ from .term import raw
def record( def record(
command: Any, command: Any,
writer: Any, writer: Any,
get_tty_size: Callable[[], Tuple[int, int]],
env: Any = None, env: Any = None,
rec_stdin: bool = False, rec_stdin: bool = False,
time_offset: float = 0, time_offset: float = 0,
notifier: Any = None, notifier: Any = None,
key_bindings: Optional[Dict[str, Any]] = None, key_bindings: Optional[Dict[str, Any]] = None,
tty_stdin_fd: int = pty.STDIN_FILENO,
tty_stdout_fd: int = pty.STDOUT_FILENO,
) -> None: ) -> None:
if env is None: if env is None:
env = os.environ env = os.environ
@@ -46,18 +49,15 @@ def record(
# 1. Get the terminal size of the real terminal. # 1. Get the terminal size of the real terminal.
# 2. Set the same size on the pseudoterminal. # 2. Set the same size on the pseudoterminal.
if os.isatty(pty.STDOUT_FILENO):
buf = array.array("h", [0, 0, 0, 0])
fcntl.ioctl(pty.STDOUT_FILENO, termios.TIOCGWINSZ, buf, True)
else:
buf = array.array("h", [24, 80, 0, 0])
cols, rows = get_tty_size()
buf = array.array("h", [rows, cols, 0, 0])
fcntl.ioctl(master_fd, termios.TIOCSWINSZ, buf) fcntl.ioctl(master_fd, termios.TIOCSWINSZ, buf)
def _write_stdout(data: Any) -> None: def _write_stdout(data: Any) -> None:
"""Writes to stdout as if the child process had written the data.""" """Writes to stdout as if the child process had written the data."""
os.write(pty.STDOUT_FILENO, data) os.write(tty_stdout_fd, data)
def _handle_master_read(data: Any) -> None: def _handle_master_read(data: Any) -> None:
"""Handles new data on child process stdout.""" """Handles new data on child process stdout."""
@@ -120,7 +120,7 @@ def record(
when new data arrives. when new data arrives.
""" """
fds = [master_fd, pty.STDIN_FILENO, signal_fd] fds = [master_fd, tty_stdin_fd, signal_fd]
while True: while True:
try: try:
@@ -136,10 +136,10 @@ def record(
else: else:
_handle_master_read(data) _handle_master_read(data)
if pty.STDIN_FILENO in rfds: if tty_stdin_fd in rfds:
data = os.read(pty.STDIN_FILENO, 1024) data = os.read(tty_stdin_fd, 1024)
if not data: if not data:
fds.remove(pty.STDIN_FILENO) fds.remove(tty_stdin_fd)
else: else:
_handle_stdin_read(data) _handle_stdin_read(data)
@@ -188,7 +188,7 @@ def record(
start_time = time.time() - time_offset start_time = time.time() - time_offset
with raw(pty.STDIN_FILENO): with raw(tty_stdin_fd):
try: try:
_copy(pipe_r) _copy(pipe_r)
except (IOError, OSError): except (IOError, OSError):

View File

@@ -3,7 +3,6 @@ import time
from typing import Any, Callable, Dict, Optional, Tuple, Type from typing import Any, Callable, Dict, Optional, Tuple, Type
from . import pty_ as pty # avoid collisions with standard library `pty` from . import pty_ as pty # avoid collisions with standard library `pty`
from . import term
from .asciicast import v2 from .asciicast import v2
from .asciicast.v2 import writer as w2 from .asciicast.v2 import writer as w2
from .async_worker import async_worker from .async_worker import async_worker
@@ -23,6 +22,8 @@ def record( # pylint: disable=too-many-arguments,too-many-locals
record_: Callable[..., None] = pty.record, record_: Callable[..., None] = pty.record,
notifier: Any = None, notifier: Any = None,
key_bindings: Optional[Dict[str, Any]] = None, key_bindings: Optional[Dict[str, Any]] = None,
cols_override: Optional[int] = None,
rows_override: Optional[int] = None,
) -> None: ) -> None:
if command is None: if command is None:
command = os.environ.get("SHELL", "sh") command = os.environ.get("SHELL", "sh")
@@ -38,11 +39,16 @@ def record( # pylint: disable=too-many-arguments,too-many-locals
if capture_env is None: if capture_env is None:
capture_env = ["SHELL", "TERM"] capture_env = ["SHELL", "TERM"]
w, h = term.get_size() tty_stdin_fd = 0
tty_stdout_fd = 1
get_tty_size = _get_tty_size(tty_stdout_fd, cols_override, rows_override)
cols, rows = get_tty_size()
full_metadata: Dict[str, Any] = { full_metadata: Dict[str, Any] = {
"width": w, "width": cols,
"height": h, "height": rows,
"timestamp": int(time.time()), "timestamp": int(time.time()),
} }
@@ -75,11 +81,14 @@ def record( # pylint: disable=too-many-arguments,too-many-locals
record_( record_(
["sh", "-c", command], ["sh", "-c", command],
_writer, _writer,
get_tty_size,
command_env, command_env,
rec_stdin, rec_stdin,
time_offset, time_offset,
_notifier, _notifier,
key_bindings, key_bindings,
tty_stdin_fd=tty_stdin_fd,
tty_stdout_fd=tty_stdout_fd,
) )
@@ -143,3 +152,27 @@ class async_notifier(async_worker):
# we catch *ALL* exceptions here because we don't want failed # we catch *ALL* exceptions here because we don't want failed
# notification to crash the recording session # notification to crash the recording session
pass pass
def _get_tty_size(
fd: int, cols_override: Optional[int], rows_override: Optional[int]
) -> Callable[[], Tuple[int, int]]:
if cols_override is not None and rows_override is not None:
def fixed_size() -> Tuple[int, int]:
return (cols_override, rows_override) # type: ignore
return fixed_size
if not os.isatty(fd):
def fallback_size() -> Tuple[int, int]:
return (cols_override or 80, rows_override or 24)
return fallback_size
def size() -> Tuple[int, int]:
cols, rows = os.get_terminal_size(fd)
return (cols_override or cols, rows_override or rows)
return size

View File

@@ -1,10 +1,9 @@
import os import os
import select import select
import subprocess
import termios as tty # avoid `Module "tty" has no attribute ...` errors import termios as tty # avoid `Module "tty" has no attribute ...` errors
from time import sleep from time import sleep
from tty import setraw from tty import setraw
from typing import IO, Any, List, Optional, Tuple, Union from typing import IO, Any, List, Optional, Union
class raw: class raw:
@@ -33,13 +32,3 @@ def read_blocking(fd: int, timeout: Any) -> bytes:
return os.read(fd, 1024) return os.read(fd, 1024)
return b"" return b""
def get_size() -> Tuple[int, int]:
try:
return os.get_terminal_size()
except: # pylint: disable=bare-except # noqa: E722
return (
int(subprocess.check_output(["tput", "cols"])),
int(subprocess.check_output(["tput", "lines"])),
)

View File

@@ -93,6 +93,12 @@ Available options:
`-i, --idle-time-limit=<sec>` `-i, --idle-time-limit=<sec>`
: Limit recorded terminal inactivity to max `<sec>` seconds : Limit recorded terminal inactivity to max `<sec>` seconds
`--cols=<n>`
: Override terminal columns for recorded process
`--rows=<n>`
: Override terminal rows for recorded process
`-y, --yes` `-y, --yes`
: Answer "yes" to all prompts (e.g. upload confirmation) : Answer "yes" to all prompts (e.g. upload confirmation)

View File

@@ -44,6 +44,6 @@ class TestRecord(Test):
"; sys.stdout.write('bar')" "; sys.stdout.write('bar')"
), ),
] ]
asciinema.pty_.record(command, output) asciinema.pty_.record(command, output, lambda: (80, 24))
assert output.data == [b"foo", b"bar"] assert output.data == [b"foo", b"bar"]