Metadata-Version: 2.5
Name: ttyscan
Version: 0.0.10
Summary: Queries a terminal emulator for its type, size and capabilities
Project-URL: Homepage, https://github.com/jquast/ttyscan
Author-email: Jeff Quast <contact@jeffquast.com>
License-Expression: MIT
License-File: LICENSE
Keywords: capability,colorterm,console,curses,query,serial,setupterm,ssh,telnet,terminal,terminfo,tty,xtgettcap
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Terminals
Requires-Python: >=3.6
Requires-Dist: dataclasses; python_version < '3.7'
Description-Content-Type: text/x-rst

| |pypi_downloads| |codecov| |windows| |linux| |mac| |bsd|

ttyscan
=======

*ttyscan* queries a terminal emulator for its type, size, capabilities, and identity. Creating a
curses-compatible `terminfo(5)`_ file entry if necessary, and exports any corrected values of
``TERM``, ``TERM_PROGRAM``, ``COLORTERM``, ``LINES``, ``COLUMNS``, and optionally ``TERMCAP``
(`termcap(5)`_) environment variables.

Problem
-------

ncurses_ does not support XTGETTCAP, and so `terminfo(5)`_ files for some terminals must be
deployed to remote systems by system operators in some circumstances.

Curses programs fail to start until those files are deployed or an alternate ``TERM`` is exported.
All kinds of errors and warnings may be raised until this is done, some examples::

       $ tmux attach
       missing or unsuitable terminal: xterm-kitty

       $ irssi
       setupterm() failed for TERM=xterm-kitty: 0
       Can't initialize screen handling.

       $ htop
       Error opening terminal: xterm-kitty.

       $ python -c 'import curses;curses.setupterm()'
       Traceback (most recent call last):
         File "<string>", line 1, in <module>
       _curses.error: setupterm: could not find terminal

Solution
--------

*ttyscan* creates the missing `terminfo(5)` file using the ``XTGETTCAP`` (``DCS +q``) terminal query
protocol when supported, and suggests a new ``TERMINFO`` environment value for export, allowing
legacy calls to curses of `setupterm(3)`_ to succeed::

       $ ttyscan -vft
       ttyscan: probing terminal via XTGETTCAP ...
       ttyscan: size via dual CPR: 28x100
       ttyscan: XTGETTCAP supported, terminal: rio; received 198 caps (11 bool, 5 num, 178 str) in 4ms
       ttyscan: writing /home/jq/.config/ttyscan/terminfo/r/rio
       export COLORTERM=truecolor
       export TERM=rio
       export LINES=28
       export COLUMNS=100
       export TERMINFO=/home/jq/.config/ttyscan/terminfo
       export TERMCAP='rio|XTGETTCAP-discovered terminal::am::ut::hs::km::5i::mi::ms::NP::xn::Co#256::co#80::it#8::li#24::pa#32767::ac=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~::bl=^G::mb=\E[5m::md=\E[1m::bt=\E[Z::vi=\E[?25l::cl=\E[H\E[2J::ve=\E[?12l\E[?25h::cr=\r::cs=\E[%i%p1%d;%p2%dr::le=\b::DO=\E[%p1%dB::do=\n::nd=\E[C::cm=\E[%i%p1%d;%p2%dH::up=\E[A::vs=\E[?12;25h::dC=\E[P::mh=\E[2m::dl=\E[M::ds=\E]2;^G::ec=\E[%p1%dX::cd=\E[J::ce=\E[K::cb=\E[1K::vb=\E[?5h$<100/>\E[?5l::fs=^G::ho=\E[H::ch=\E[%i%p1%dG::ta=\t::st=\EH::al=\E[L::sf=\n::mk=\E[8m::is=\E[!p\E[?3;4l\E[4l\E>::K2=\EOE::kb=^?::kB=\E[Z::kl=\EOD::kd=\EOB::kr=\EOC::ku=\EOA::kD=\E[3~::@7=\EOF::@8=\EOM::k1=\EOP::k0=\E[21~::F1=\E[23~::F2=\E[24~::k2=\EOQ::k3=\EOR::k4=\EOS::k5=\E[15~::k6=\E[17~::k7=\E[18~::k8=\E[19~::k9=\E[20~::kh=\EOH::kI=\E[2~::kH=\E[1;2B::kN=\E[6~::kP=\E[5~::ps=\E[i::pf=\E[4i::po=\E[5i::op=\E[39;49m::rp=%p1%c\E[%p2%{1}%-%db::mr=\E[7m::sr=\EM::ZR=\E[23m::ae=\E(B::te=\E[?1049l\E[23;0;0t::ei=\E[4l::ke=\E[?1l\E>::se=\E[27m::ue=\E[24m::r1=\Ec\E]104^G::r2=\E[!p\E[?3;4l\E[4l\E>::sa=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m::me=\E(B\E[m::ZH=\E[3m::as=\E(0::ti=\E[?1049h\E[22;0;0t::im=\E[4h::ks=\E[?1h\E=::so=\E[7m::us=\E[4m::ct=\E[3g::ts=\E]2;::cv=\E[%i%p1%dd:'

       $ eval `ttyscan`

       $ echo $TERM, $TERMINFO
       rio, /home/jquast/.config/ttyscan/terminfo

       $ file -b $TERMINFO/r/rio
       Compiled terminfo entry "rio"

       $ python -c 'import curses;curses.setupterm()'; echo $?
       0

CLI Arguments
-------------

::

    usage: ttyscan [-h] [-v] [-f] [-t] [--version]

    Export terminal capabilities discovered via XTGETTCAP

    options:
      -h, --help     show this help message and exit
      -v, --verbose  Print diagnostic information to stderr
      -f, --force    Force export of all values even if unchanged
      -t, --termcap  Also export TERMCAP value
      --version      show program's version number and exit

*ttyscan* saves a `terminfo(5)`_ file to $XDG_CONFIG_HOME/ttyscan/terminfo or
~/.config/ttyscan/terminfo, and does not re-query or re-create it when it already exists from
previous executions, unless ``--force`` is used.

Typical total execution time for remote shells is under 200ms.

Installation
------------

.. code-block:: shell

   pip install ttyscan

*ttyscan* requires Python3.6 or newer.

ttyscan.py_ is a **stand-alone python file**, it does not require pip to install, you can copy this
file from source and execute it directly, eg. ``python ~/bin/ttyscan.py``

Motive
------

Naturally, transferring your ``$HOME/.terminfo`` folder to a remote machine is the best solution.

However, enterprise systems, bastion hosts, cloud systems, web consoles, hypervisors, radio, and
serial ports can be challenging places or protocol layers through which to deploy terminfo files.

Some workarounds include exporting a generally-compatible ``TERM=xterm-256color`` or ``xterm`` with
sometimes minor corruption of screen output, missed interpretation of keyboard input such as
backspace or delete, or a small reduction of features such as italic or underlined text or number of
colors.

ttyscan.py_ serves as a demonstration: that a full terminal capability database *can* be transferred
using XTGETTCAP_ for many modern terminals. Further, by use of data collected by ucs-detect_,
terminals that do not respond to XTVERSION or ENQ inquires can be identified by their unique
fingerprints of DA1_ response and their unicode terminal width "quirks".

My hope is that ncurses_ and `setupterm(3)`_ could someday negotiate with full XTGETTCAP_ support at
some point in the future, so that remote deployment of terminfo files are not necessary, and that
this utility would not be used or required!

Scope
-----

At time of this writing (November 2026), the ucs-detect_ dataset of 42 terminals shows three
categories of support for ``XTGETTCAP``:

- **Full** ``XTGETTCAP`` capability support: AbsoluteTelnet/SSH, Bobcat, contour_, foot_, ghostty_,
  iTerm2, kitty_, rio_, WezTerm_, and XTerm.NET transmit their complete terminfo boolean, numeric,
  and string capabilities via XTGETTCAP_.

  *ttyscan* produces terminfo files for only these terminals.  *ttyscan* may also discover a
  preferred ``TERM`` from ``TN``, and ``COLORTERM=truecolor`` from ``RGB``.

- **Partial** ``XTGETTCAP`` capability support: GNOME Terminal, mlterm, TeraTerm, Terminator, XFCE
  Terminal, and XTerm_ report only ``TN``, ``Co``, and ``RGB``.

  *ttyscan* may only discover preferred ``TERM`` from ``TN``, and ``COLORTERM=truecolor`` from
  ``RGB``.

  XTerm_ supports only keyboard sequences in addition to ``TN``, ``Co``, and ``RGB``. It does not
  report *all* capabilities, and so a `terminfo(5)`_ entry cannot be built. However,
  ``TERM=xterm-256color`` and ``TERM=xterm`` are the most ubiquitous terminal name, you should be
  just fine.

- **No Support**: alacritty (refuses: `alacritty/vte#98`_), Extraterm, Hyper, Konsole (requested:
  `KDE#507017`_), libvterm, linux fbdev, mintty, pterm, QTerminal, rxvt-unicode, screen, securecrt,
  st, SyncTERM, Tabby, Terminal.exe (planned: `microsoft/terminal#17735`_), terminology, tmux
  (passthrough: `tmux/tmux#3755`_), VS Code (xterm.js, proposal: `xtermjs/xterm.js#4107`_), Warp,
  weston-terminal, Zellij, and zutty, QTerminal (leaky sequence `fixed upstream
  <https://github.com/lxqt/qtermwidget/pull/644>`_).

- **Non-Compliant**: ConEmu, cool-retro-term, and Apple Terminal: When a terminal emulator does not
  support XTGETTCAP, it **should** silently consume VT100 Mode DCS queries without answer.  However,
  these terminals display XTGETTCAP query output, eg.
  ``+q544e+q524742+q636f6c6f7273+q626c696e6b+..`` is visibly written to screen: cool-retro-term,
  ConEmu.exe, and Apple's Terminal.app all fail to parse DCS queries in this way.

Architecture
------------

*ttyscan* uses the following,

- ``XTGETTCAP`` field ``TN`` is used to correct ``TERM`` when unmatched.
- ``XTGETTCAP`` field ``RGB`` is used to correct ``COLORTERM`` when unmatched.
- all capability strings, keyboard and screen, when provided by ``XTGETTCAP``.
- DEC Private Mode 2048 (In-Band Resize) to determine ``COLUMNS`` and ``LINES``
- Or failing that, using Cursor Position Report like XTerm's `resize.c
  <https://github.com/joejulian/xterm/blob/master/resize.c>`_
- DECRQSS_ (Request Setting Selection) to probe truecolor support when ``RGB`` is unavailable,
  by setting and querying a 24-bit background color and exporting ``COLORTERM`` if successful
- DA1_ response is received and Unicode widths are probed to identify ``TERM_PROGRAM``.

Every query uses a following cursor position report (CPR) sequence to test for negative response
(no support), so the timeout elapses for terminal automation (such as tcl/expect pexpect) that acts
as a terminal but is not one. The default timeout is set by environment value,
``TTYSCAN_QUERY_TIMEOUT=10.0`` (float) in seconds.

Full vs. Partial XTGETTCAP
~~~~~~~~~~~~~~~~~~~~~~~~~~

The difference of *Full* and *Partial* ``XTGETTCAP`` support is best described by foot_:

  ``XTGETTCAP`` is an escape sequence initially introduced by XTerm_, and also implemented (and extended,
  to some degree) by Kitty.

  Applications using this feature do not need to use the classic, file-based, terminfo definition.

  XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys)
  capabilities, and three custom capabilities:

  ``TN`` - terminal name
  ``Co`` - number of colors (alias for the colors capability)
  ``RGB`` - number of bits per color channel (different semantics from the RGB capability in file-based
  terminfo definitions!).

  Kitty has extended this, and also supports querying all integer and string capabilities.

  Foot supports this, and extends it even further, to also include boolean capabilities.
  This means foot's entire terminfo can be queried via ``XTGETTCAP``.

Forwarding of Environment Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Further, all of ``TERM``, ``COLORTERM``, ``LINES``, or ``COLUMNS`` may not be transmitted by all
software or protocols, some examples:

- ssh does not forward ``COLORTERM`` unless configured using ``SendEnv`` in `ssh_config(5)`_ and
  ``AcceptEnv`` in `sshd_config(5)`_.
- serial does not forward any; ``TERM`` is defined by host `agetty(8)`_ configuration, for example.
- rlogin can forward all but ``COLORTERM``.
- telnet can forward all, but IAC NAWS and NEW-ENVIRON capability varies by software.
- websocket cannot forward any (without customized subprotocol), often used in web browsers.

*ttyscan* detects when these variables are unset or do not match values and re-exports the corrected
values when they differ.

Terminal Identification
~~~~~~~~~~~~~~~~~~~~~~~

When ``TERM_PROGRAM`` is unset (or ``--force`` is used), *ttyscan* attempts to identify it:

- using ``XTVERSION`` and reply (``CSI > q``, answered ``DCS > | name version ST``)
- using ``ENQ`` (``0x05``) and takes the printable answerback

If both are silent it sends DA1_ (``CSI c``), which all terminals reply with a service class and a
set of extension numbers such as ``CSI ? 62 ; 4 ; 9 ; 22 c``. When that DA1 response is not unique,
*ttyscan* draws complex grapheme clusters (complex unicode strings), narrowing the unique "quirks"
of a terminal's unicode measurements by Cursor Position Response until one remains.

For example, Apple's Terminal.app may be remotely identified even though it does not answer
``XTVERSION`` or ``ENQ``, although it does export ``TERM_PROGRAM``, ssh does not forward it.
Further, its DA1_ reply is not unique, and so it is identified only by its unicode "quirks".

These "quiz tables" are generated automatically from data collected by ucs-detect_, by
command ``tox -e update``.  At time of this writing (November 2026) this allows identifying the
following terminals that otherwise do not identify themselves:

    alacritty, ConEmu, cool-retro-term, Extraterm, Hyper, libvterm, QTerminal, securecrt, st, Tabby,
    TeraTerm, weston-terminal, WindowsTerminal, zutty.

.. _`agetty(8)`: https://linux.die.net/man/8/agetty
.. _`alacritty/vte#98`: https://github.com/alacritty/vte/issues/98
.. _contour: https://github.com/contour-terminal/contour
.. _DECRQSS: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Device-Control-functions:DCS-plus-q-Pt-ST.F95
.. _DA1: https://vt100.net/docs/vt510-rm/DA1.html
.. _foot: https://codeberg.org/dnkl/foot#xtgettcap
.. _ghostty: https://mitchellh.com/writing/ghostty-devlog-004
.. _`KDE#507017`: https://bugs.kde.org/show_bug.cgi?id=507017
.. _kitty: https://sw.kovidgoyal.net/kitty/kittens/query_terminal/
.. _`microsoft/terminal#17735`: https://github.com/microsoft/terminal/issues/17735
.. _ncurses: https://invisible-island.net/ncurses/
.. _rio: https://rioterm.com/changelog#0220
.. _`setupterm(3)`: https://linux.die.net/man/3/setupterm
.. _`ssh_config(5)`: https://linux.die.net/man/5/ssh_config
.. _`sshd_config(5)`: https://linux.die.net/man/5/sshd_config
.. _`termcap(5)`: https://linux.die.net/man/5/termcap
.. _`terminfo(5)`: https://linux.die.net/man/5/terminfo
.. _`tmux/tmux#3755`: https://github.com/tmux/tmux/issues/3755
.. _`ttyscan.py`: https://github.com/jquast/ttyscan/blob/master/ttyscan.py
.. _ucs-detect: https://ucs-detect.readthedocs.io/results.html#terminal-capabilities
.. _wezterm: https://wezterm.org/
.. _XTerm: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Device-Control-functions:DCS-plus-q-Pt-ST.F95
.. _`xtermjs/xterm.js#4107`: https://github.com/xtermjs/xterm.js/issues/4107
.. _XTGETTCAP: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Device-Control-functions:DCS-plus-q-Pt-ST.F95
.. |pypi_downloads| image:: https://img.shields.io/pypi/dm/ttyscan.svg?logo=pypi
    :alt: Downloads
    :target: https://pypi.org/project/ttyscan/
.. |codecov| image:: https://codecov.io/gh/jquast/ttyscan/branch/master/graph/badge.svg
    :alt: codecov.io Code Coverage
    :target: https://codecov.io/gh/jquast/ttyscan/
.. |linux| image:: https://img.shields.io/badge/Linux-yes-success?logo=linux
    :alt: Linux supported
.. |windows| image:: https://img.shields.io/badge/Windows-yes-success?logo=windows
    :alt: Windows supported
.. |mac| image:: https://img.shields.io/badge/MacOS-yes-success?logo=apple
    :alt: MacOS supported
.. |bsd| image:: https://img.shields.io/badge/BSD-yes-success?logo=freebsd
    :alt: BSD supported
