Drafter.Terminal.Driver (drafter v0.3.2)

Copy Markdown View Source

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

Types

The driver's own state.

How raw mode was entered, and so how it is undone.

Functions

Returns a specification to start this module under a supervisor.

Restore the terminal to the state it was in before setup/1.

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

state()

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

terminal_mode()

@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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

cleanup()

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

disable_mouse(opts \\ [])

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

drain_pending_input()

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

enable_mouse(opts \\ [])

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

get_size()

@spec get_size() :: {pos_integer(), pos_integer()}

The last known terminal size as {cols, rows}, without re-measuring.

probe(timeout \\ 250)

@spec probe(timeout()) :: {:ok, atom() | nil} | :unprobed

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.

refresh_size()

@spec refresh_size() :: {pos_integer(), pos_integer()}

Measure the terminal again and return the new {cols, rows}.

setup(mouse_opts \\ [])

@spec setup(keyword()) :: :ok | {:error, term()}

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.

start_input()

@spec start_input() :: :ok

Start reading stdin and emitting events.

Idempotent: a second call while a reader is running does nothing.

start_link(opts \\ [])

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

The terminal is measured here but not altered; setup/1 does that.

write(data)

@spec write(iodata()) :: :ok

Write bytes to the terminal.

Asynchronous. Bytes are discarded unless the terminal is in raw mode.

write_synchronously(iodata)

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