Owns the local terminal: raw mode, alternate screen, mouse reporting, input decoding.
A singleton registered under this module's name. setup/1 puts the terminal into
raw mode, switches to the alternate screen, hides the cursor and enables mouse and
bracketed-paste reporting. cleanup/0 undoes all of it and restores the saved
terminal mode; it also runs from terminate/2.
Input is not read until start_input/0 is called, so a caller can drain whatever
the terminal echoed during startup first:
Drafter.Terminal.Driver.setup()
Drafter.Terminal.Driver.drain_pending_input()
Drafter.Terminal.Driver.start_input()Decoded input is cast to the event manager as {:event, event}; the manager then
delivers {:tui_event, event} to its subscribers. Event shapes are the ones
Drafter.Terminal.ANSI produces, plus {:resize, {cols, rows}} emitted on
SIGWINCH.
The event manager is chosen at init/1 by the :event_manager option, defaulting
to Drafter.Event.Manager.
Raw mode is entered through the termios NIF where it loads, and through stty
otherwise. On a non-unix system neither runs and the terminal mode is left alone,
while the rest of setup/1 still happens.
Terminal size is read by TIOCGWINSZ, falling back to Erlang's IO dimensions and
then to tput; 80 by 24 if all of them fail. Setting DRAFTER_TPUT_SIZE to a
non-empty value forces the tput path. On Windows the size comes from PowerShell,
and 80 by 24 if that fails.
Summary
Functions
Returns a specification to start this module under a supervisor.
Turn mouse reporting off, if the terminal is in raw mode and it is currently on.
Discard stdin that has arrived but not been turned into events.
Turn mouse reporting on, if the terminal is in raw mode and it is currently off.
The last known terminal size as {cols, rows}, without re-measuring.
The graphics protocol this terminal answered the startup probe with.
Measure the terminal again and return the new {cols, rows}.
Put the terminal into TUI mode.
Start reading stdin and emitting events.
Start the driver and register it under this module's name.
Write bytes to the terminal.
Write bytes to the controlling terminal and return once they have been handed over.
Types
@type state() :: %Drafter.Terminal.Driver{ alt_screen: boolean(), buffer: Drafter.Terminal.InputBuffer.t(), mouse_enabled: boolean(), probe_replies: term(), probe_result: term(), probe_waiters: term(), probing: term(), raw_mode: boolean(), shell_pid: :ok | {:error, term()} | nil, size: {pos_integer(), pos_integer()}, stdin_reader_pid: pid() | nil, terminal_mode: terminal_mode() }
The driver's own state.
shell_pid holds whatever :shell.start_interactive/1 returned, which is :ok
or an error tuple rather than a pid.
@type terminal_mode() :: :nif | {:stty, binary()} | nil
How raw mode was entered, and so how it is undone.
:nif means the termios NIF holds the saved settings, {:stty, saved} means
stty does and saved is the stty -g string, and nil means raw mode is not
in force.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec cleanup() :: :ok
Restore the terminal to the state it was in before setup/1.
Disables bracketed paste and mouse reporting, shows the cursor, leaves the
alternate screen, restores the saved terminal mode and stops the stdin reader.
Safe to call when setup/1 was never called or already undone.
Mouse reporting is turned off as Drafter.Terminal.ANSI.disable_mouse/1 does
with its defaults, so it clears the any-motion mode that setup/1 enables when
:hover is left at true. Everything except the stdin reader is skipped when
raw mode is not in force. The sequences are written straight to /dev/tty by
write_synchronously/1, so they land even while the driver is shutting down.
Also runs from terminate/2.
@spec disable_mouse(keyword()) :: :ok
Turn mouse reporting off, if the terminal is in raw mode and it is currently on.
opts defaults to [] and is passed to Drafter.Terminal.ANSI.disable_mouse/1,
whose only key is :hover; it must match what enable_mouse/1 was given, or the
mode that was set is not the mode that is cleared. Asynchronous, and silently does
nothing when raw mode is not in force or reporting is already off.
@spec drain_pending_input() :: :ok
Discard stdin that has arrived but not been turned into events.
Drops queued {:stdin, _} messages, flushes the operating system's input queue
and empties the partial-sequence buffer. Nothing is emitted to the event manager.
@spec enable_mouse(keyword()) :: :ok
Turn mouse reporting on, if the terminal is in raw mode and it is currently off.
opts defaults to [] and is passed to Drafter.Terminal.ANSI.enable_mouse/1,
whose only key is :hover. Asynchronous, and silently does nothing when raw mode
is not in force or reporting is already on.
@spec get_size() :: {pos_integer(), pos_integer()}
The last known terminal size as {cols, rows}, without re-measuring.
The graphics protocol this terminal answered the startup probe with.
Returns {:ok, protocol} where protocol is :kitty, :iterm2, :sixel, or
nil for a terminal that answered naming none. Returns :unprobed when there
was no terminal to ask, which is the caller's cue to fall back to guessing from
the environment.
Asking writes to the terminal and reads its answer, so call this after
start_input/0 and after any drain_pending_input/0, which would otherwise
discard the answer. Blocks until the terminal answers or timeout passes, and
never longer: a driver with no terminal replies at once, and one that fails to
reply at all is reported as :unprobed rather than taking the caller down.
@spec refresh_size() :: {pos_integer(), pos_integer()}
Measure the terminal again and return the new {cols, rows}.
Put the terminal into TUI mode.
Enters raw mode, switches to the alternate screen, hides the cursor, clears it,
and enables mouse and bracketed-paste reporting. mouse_opts defaults to []
and is passed to Drafter.Terminal.ANSI.enable_mouse/1, whose only key is
:hover.
Terminal size is measured again on success, and SIGWINCH starts being watched.
On a system where the signal cannot be watched, size is polled instead and a
change is reported the same way.
Returns {:error, reason} if raw mode could not be entered, leaving the terminal
untouched. Input reading does not begin here; call start_input/0.
@spec start_input() :: :ok
Start reading stdin and emitting events.
Idempotent: a second call while a reader is running does nothing.
@spec start_link(keyword()) :: GenServer.on_start()
Start the driver and register it under this module's name.
The name is fixed, so only one driver runs per node and every function in this module addresses it without being told which.
Options:
:event_manager— process name or pid decoded events are cast to, defaultDrafter.Event.Manager
The terminal is measured here but not altered; setup/1 does that.
@spec write(iodata()) :: :ok
Write bytes to the terminal.
Asynchronous. Bytes are discarded unless the terminal is in raw mode.
@spec write_synchronously(iodata()) :: :ok
Write bytes to the controlling terminal and return once they have been handed over.
Opens /dev/tty directly, bypassing the driver process and the Erlang IO server,
so the bytes land even while the driver is shutting down. Falls back to IO.write/1
when /dev/tty cannot be opened. An empty list writes nothing.