about summary refs log tree commit diff
path: root/tests.py
diff options
context:
space:
mode:
authorsterni <sternenseemann@systemli.org>2021-07-04 15:55:28 +0200
committerSören Tempel <soeren+git@soeren-tempel.net>2021-07-15 13:28:33 +0200
commitd59393e1efc2d21e4a79ce1698d4d6c1cb924c66 (patch)
tree7ded5ffbe031de6074b4e20d82e7166b7207793b /tests.py
parenta33f9be15276ebe217fe14fa75f1848533dff4e8 (diff)
Implement a reasonable subset of SGR escape sequences
SGR (Select Graphical Representation) escape sequences are CSI escape
sequences ending in the final byte 'm'. They are described in the
ECMA-48 standard, but we also support a compatible extension which is
commonly used nowadays, namely extended colors (256 colors and true 24
bit colors) which are specified in ITU-T Rec. T.416.

SGR sequences are probably the most commonly used escape sequences and
a lot of CLI tools now feel much more familiar in saneterm. The
implemented SGR sequences for example allow:

* to change the current text's foreground and background color in the
  three commonly used color modes 8/16 colors, 256 colors and 24 bit
  true color.

* to change the current text's appearance: italic, bold, underline,
  strikethrough and more are supported.

The current implementation uses a new TextStyle object which is added
to pty.Parser's state to track the inherintly stateful changes in text
appearance described by SGR escape sequences.

When the TextStyle object changes, a TEXT_STYLE event is emitted and
the a Gtk.TextTag is created from the TextStyle and registered in the
widget's TextBuffer.

For the most part this is quite straightforward, just two areas
deserve more attention:

* The extended colors (256 colors and 24 bit true color) are a bit
  more complicated to parse which is handled by parse_extended_color().
  This function doesn't fully support everything the recommendation
  mandates. Especially true color will need more real world testing,
  e. g. lolcat(1) a heavy user of true color doesn't even emit true
  color escape sequences conforming to the standard.

* Color handling in general contributes to most of the complexity:

  * There are three ways to specify colors via SGR escape sequences
    we support which all need to be converted to Gdk.RGBA objects.
    This is handled by saneterm.color.Color. True color is trivial,
    for 256 colors we implement the conversion instead of generating a
    lookup table (like XTerm does). For the 8 basic colors in their
    normal and bright variants, we use hard coded list of X11 color
    names for now. This probably should become configurable in the
    future.

  * Many implementation use the intensity escape sequences to
    influence color vibrance: SGR 2 is interpreted as dim wrt
    to colors and SGR 1 not only makes the text bold but also
    chooses brighter colors. So far we interpret SGR 1, 2 and 22
    only in terms of font weight. EMCA-48 permits both. Changing the
    color intensity as well increases complexity and has little
    benefit, so this should probably be kept this way.

  * Instead we implement the 90-97 and 100-107 non-standard bright
    color SGR escape sequences.

The current implementation is, however, not without issues:

* Tracking the text style state in the parser is probably a layer
  violation — pty.Parser should instead translate the escape sequences
  into events and the state tracking done in saneterm.terminal.

* Performance is poor if a lot of escape sequences are in the input.
  This is due to two reasons: a) insert_data is called with little
  chunks of text which decreases performance and b) a lot of anonymous
  Gtk TextTags are created which hurts performance a lot. We should
  investigate a way to deduplicate the created TextTags (by using
  names?) and possibly decouple the application of tags from the
  insertion of text itself.
Diffstat (limited to 'tests.py')
-rw-r--r--tests.py28
1 files changed, 28 insertions, 0 deletions
diff --git a/tests.py b/tests.py
index 34db339..aacdcdc 100644
--- a/tests.py
+++ b/tests.py
@@ -1,8 +1,11 @@
 import copy
 import unittest
 
+from saneterm.color import Color, ColorType
 from saneterm.pty import PositionedIterator
 
+from gi.repository import Gdk
+
 TEST_STRING = 'foo;bar'
 
 class TestPositionedIterator(unittest.TestCase):
@@ -92,5 +95,30 @@ class TestPositionedIterator(unittest.TestCase):
         # using take does not consume the next element!
         self.assertEqual(it1.pos, length - 1)
 
+class TestColor(unittest.TestCase):
+    def test_256_colors(self):
+        """
+        Check divmod based RGB value calculation against
+        256 color table generation as implemented in
+        XTerm's 256colres.pl.
+        """
+        def channel_val(c):
+            return (c * 40 + 55 if c > 0 else 0) / 255
+
+        for r in range(6):
+            for g in range(6):
+                for b in range(6):
+                    n = 16 + (r * 36) + (g * 6) + b
+
+                    expected = Gdk.RGBA(*map(channel_val, (r, g, b)))
+                    col = Color(ColorType.NUMBERED_256, n).to_gdk()
+
+                    self.assertTrue(
+                        expected.equal(col),
+                        'Color {}: expected: {}; got: {}'.format(
+                            n, expected.to_string(), col.to_string()
+                        )
+                    )
+
 if __name__ == '__main__':
     unittest.main()