diff options
author | sterni <sternenseemann@systemli.org> | 2021-07-04 15:55:28 +0200 |
---|---|---|
committer | Sören Tempel <soeren+git@soeren-tempel.net> | 2021-07-15 13:28:33 +0200 |
commit | d59393e1efc2d21e4a79ce1698d4d6c1cb924c66 (patch) | |
tree | 7ded5ffbe031de6074b4e20d82e7166b7207793b /tests.py | |
parent | a33f9be15276ebe217fe14fa75f1848533dff4e8 (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.py | 28 |
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() |