148 lines
4.3 KiB

# Copyright (C) 2020 by Coffee (
# you may redistribute and/or modify this file under the terms of
# the GNU Affero General Public License, version 3 or later. The full
# text of this license can be found in the file called LICENSE, which
# should have been distributed along with this file. If not, try the
# following sources:
# SPDX-License-Identifier: AGPL-3.0-or-later
import collections
import os
import re
import sys
import termios
import time
Dimensions = collections.namedtuple("Dimensions", "width height")
class RawTerminal:
def __enter__(self):
self.saved_attrs = termios.tcgetattr(0)
self.saved_blocking = os.get_blocking(0)
attrs = termios.tcgetattr(0)
attrs[3] &= ~termios.ICANON
attrs[3] &= ~termios.ECHO
termios.tcsetattr(0, termios.TCSADRAIN, attrs)
os.set_blocking(0, False)
return self
def __exit__(self, exc_type, exc_value, traceback):
termios.tcsetattr(0, termios.TCSADRAIN, self.saved_attrs)
os.set_blocking(0, self.saved_blocking)
def _interrogate(question, stopseq, answerpattern, timeout=5):
r = ""
c = ""
noinput = 0
capture = False
while True:
c =
if c == "":
noinput += 1
if noinput > timeout: return
noinput = 0
if not capture:
if c != "\033": continue
capture = True
r = r + c
if r.endswith(stopseq): break
if len(r) >= 65535: return
if isinstance(answerpattern, str):
return, r)
def _has_swapped_size_bug(self):
# gnome-terminal responds with width;height when
# interrogated with the <ESC>[14t sequence. The correct
# order is height;width, as per the spec and as correctly
# implemented in both xterm and mlterm.
# This method detects when a libvte-based terminal
# (such as gnome-terminal) is in use, and offers its
# best guess as to whether the bug is present.
# That is to say, we currently assume all libvte-based
# terminals have the bug.
# Reference for detection:
m = self.get_DA3()
if not m: return False
if != "7E565445": return False # Hex "~VTE"
return True
#m = self.get_DA2()
#if not m: return True # Can't get version - assume bug is present
#if < 1000000: return True
def get_size_pixels(self):
m = self._interrogate("\033[14t", "t", "^\033\\[4;([0-9]+);([0-9]+)t$")
if m:
if self._has_swapped_size_bug():
# Broken libvte terminal emulator
return Dimensions(int(, int(
return Dimensions(int(, int(
def get_size_characters(self):
m = self._interrogate("\033[18t", "t", "^\033\\[8;([0-9]+);([0-9]+)t$")
if m: return Dimensions(int(, int(
def get_character_cell_size(self):
m = self._interrogate("\033[16t", "t", "^\033\\[6;([0-9]+);([0-9]+)t$")
if m: return Dimensions(int(, int(
# Well, that didn't work. Let's try the indirect route,
# and divide canvas size in pixels by canvas size in
# characters.
d_pixels = self.get_size_pixels()
if not d_pixels: return
d_chars = self.get_size_characters()
if not d_chars: return
w = d_pixels.width / d_chars.width
h = d_pixels.height / d_chars.height
if w == int(w) and h == int(h): return Dimensions(int(w), int(h))
def get_DA2(self):
return self._interrogate("\033[>c", "c", "^\033\\[>([0-9]*);([0-9]*);([0-9]*)c$")
def get_DA3(self):
return self._interrogate("\033[=c", "\033\\", "^\033P!\\|([0-9A-F]{8})\033\\\\$")
def show_value(text, format, obj):
fmo = "(not detected)"
if obj: fmo=format.format(obj)
if __name__ == '__main__':
with RawTerminal() as terminal:
show_value("Your terminal size in pixels [WxH]: {}",
"{0.width}x{0.height}", terminal.get_size_pixels())
show_value("Your terminal size in characters [WxH]: {}",
"{0.width}x{0.height}", terminal.get_size_characters())
show_value("Your character cell size in pixels [WxH]: {}",
"{0.width}x{0.height}", terminal.get_character_cell_size())