palette_test.py

  1#!/usr/bin/env python
  2#
  3# Urwid Palette Test.  Showing off highcolor support
  4#    Copyright (C) 2004-2009  Ian Ward
  5#
  6#    This library is free software; you can redistribute it and/or
  7#    modify it under the terms of the GNU Lesser General Public
  8#    License as published by the Free Software Foundation; either
  9#    version 2.1 of the License, or (at your option) any later version.
 10#
 11#    This library is distributed in the hope that it will be useful,
 12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 14#    Lesser General Public License for more details.
 15#
 16#    You should have received a copy of the GNU Lesser General Public
 17#    License along with this library; if not, write to the Free Software
 18#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19#
 20# Urwid web site: https://urwid.org/
 21
 22"""Palette test.
 23
 24Shows the available foreground and background settings
 25in monochrome, 16 color, 88 color, 256 color, and 24-bit (true) color modes.
 26"""
 27
 28from __future__ import annotations
 29
 30import re
 31import typing
 32
 33import urwid
 34
 35_Widget = typing.TypeVar("_Widget", bound=urwid.AbstractWidget)
 36
 37if typing.TYPE_CHECKING:
 38    from collections.abc import Callable
 39    from typing import Literal
 40
 41CHART_TRUE = """
 42#e50000#e51000#e52000#e53000#e54000#e55000#e56000#e57000#e58000#e59000\
 43#e5a000#e5b100#e5c100#e5d100#e5e100#d9e500#c9e500#b9e500#a9e500#99e500\
 44#89e500#79e500#68e500#58e500#48e500#38e500#28e500#18e500#08e500#00e507\
 45#00e517#00e527#00e538#00e548#00e558#00e568#00e578#00e588#00e598#00e5a8\
 46#00e5b8#00e5c8#00e5d8#00e1e5#00d1e5#00c1e5#00b1e5#00a1e5#0091e5#0081e5\
 47#0071e5#0061e5#0051e5#0040e5#0030e5#0020e5#0010e5#0000e5#0f00e5#1f00e5\
 48#2f00e5#3f00e5#4f00e5#5f00e5#7000e5#8000e5#9000e5#a000e5#b000e5#c000e5\
 49#d000e5#e000e5#e500da#e500ca#e500b9#e500a9#e50099#e50089
 50#da0000#da0f00#da1e00#da2e00#da3d00#da4c00#da5c00#da6b00#da7a00#da8a00\
 51#da9900#daa800#dab800#dac700#dad600#cfda00#c0da00#b0da00#a1da00#92da00\
 52#82da00#73da00#64da00#54da00#45da00#35da00#26da00#17da00#07da00#00da07\
 53#00da16#00da26#00da35#00da44#00da54#00da63#00da72#00da82#00da91#00daa0\
 54#00dab0#00dabf#00dace#00d7da#00c8da#00b8da#00a9da#0099da#008ada#007bda\
 55#006bda#005cda#004dda#003dda#002eda#001fda#000fda#0000da#0e00da#1e00da\
 56#2d00da#3c00da#4c00da#5b00da#6a00da#7a00da#8900da#9800da#a800da#b700da\
 57#c600da#d600da#da00cf#da00c0#da00b1#da00a1#da0092#da0083
 58#d00000#d00e00#d01d00#d02b00#d03a00#d04800#d05700#d06600#d07400#d08300\
 59#d09100#d0a000#d0af00#d0bd00#d0cc00#c5d000#b6d000#a8d000#99d000#8ad000\
 60#7cd000#6dd000#5fd000#50d000#41d000#33d000#24d000#16d000#07d000#00d007\
 61#00d015#00d024#00d032#00d041#00d04f#00d05e#00d06d#00d07b#00d08a#00d098\
 62#00d0a7#00d0b6#00d0c4#00ccd0#00bed0#00afd0#00a1d0#0092d0#0083d0#0075d0\
 63#0066d0#0058d0#0049d0#003ad0#002cd0#001dd0#000fd0#0000d0#0e00d0#1c00d0\
 64#2b00d0#3900d0#4800d0#5600d0#6500d0#7400d0#8200d0#9100d0#9f00d0#ae00d0\
 65#bd00d0#cb00d0#d000c5#d000b7#d000a8#d00099#d0008b#d0007c
 66#c50000#c50d00#c51b00#c52900#c53700#c54500#c55300#c56000#c56e00#c57c00\
 67#c58a00#c59800#c5a600#c5b300#c5c100#bbc500#adc500#9fc500#91c500#83c500\
 68#75c500#68c500#5ac500#4cc500#3ec500#30c500#22c500#15c500#07c500#00c506\
 69#00c514#00c522#00c530#00c53e#00c54b#00c559#00c567#00c575#00c583#00c591\
 70#00c59e#00c5ac#00c5ba#00c2c5#00b4c5#00a6c5#0098c5#008ac5#007dc5#006fc5\
 71#0061c5#0053c5#0045c5#0037c5#002ac5#001cc5#000ec5#0000c5#0d00c5#1b00c5\
 72#2800c5#3600c5#4400c5#5200c5#6000c5#6e00c5#7c00c5#8900c5#9700c5#a500c5\
 73#b300c5#c100c5#c500bb#c500ad#c5009f#c50092#c50084#c50076
 74#ba0000#ba0d00#ba1a00#ba2700#ba3400#ba4100#ba4e00#ba5b00#ba6800#ba7500\
 75#ba8200#ba8f00#ba9c00#baaa00#bab700#b0ba00#a3ba00#96ba00#89ba00#7cba00\
 76#6fba00#62ba00#55ba00#48ba00#3bba00#2eba00#20ba00#13ba00#06ba00#00ba06\
 77#00ba13#00ba20#00ba2d#00ba3a#00ba47#00ba54#00ba61#00ba6e#00ba7c#00ba89\
 78#00ba96#00baa3#00bab0#00b7ba#00aaba#009dba#0090ba#0083ba#0076ba#0069ba\
 79#005cba#004eba#0041ba#0034ba#0027ba#001aba#000dba#0000ba#0c00ba#1900ba\
 80#2600ba#3300ba#4000ba#4e00ba#5b00ba#6800ba#7500ba#8200ba#8f00ba#9c00ba\
 81#a900ba#b600ba#ba00b1#ba00a4#ba0097#ba008a#ba007d#ba006f
 82#af0000#af0c00#af1800#af2400#af3100#af3d00#af4900#af5600#af6200#af6e00\
 83#af7b00#af8700#af9300#afa000#afac00#a6af00#9aaf00#8eaf00#81af00#75af00\
 84#69af00#5caf00#50af00#44af00#37af00#2baf00#1faf00#12af00#06af00#00af05\
 85#00af12#00af1e#00af2a#00af37#00af43#00af4f#00af5c#00af68#00af74#00af81\
 86#00af8d#00af99#00afa6#00adaf#00a0af#0094af#0088af#007baf#006faf#0063af\
 87#0056af#004aaf#003eaf#0031af#0025af#0019af#000caf#0000af#0b00af#1800af\
 88#2400af#3000af#3d00af#4900af#5500af#6200af#6e00af#7a00af#8700af#9300af\
 89#9f00af#ac00af#af00a7#af009a#af008e#af0082#af0075#af0069
 90#a50000#a50b00#a51700#a52200#a52e00#a53900#a54500#a55100#a55c00#a56800\
 91#a57300#a57f00#a58a00#a59600#a5a200#9ca500#90a500#85a500#79a500#6ea500\
 92#62a500#57a500#4ba500#3fa500#34a500#28a500#1da500#11a500#06a500#00a505\
 93#00a511#00a51c#00a528#00a533#00a53f#00a54b#00a556#00a562#00a56d#00a579\
 94#00a584#00a590#00a59c#00a2a5#0096a5#008ba5#007fa5#0074a5#0068a5#005da5\
 95#0051a5#0045a5#003aa5#002ea5#0023a5#0017a5#000ca5#0000a5#0b00a5#1600a5\
 96#2200a5#2d00a5#3900a5#4500a5#5000a5#5c00a5#6700a5#7300a5#7e00a5#8a00a5\
 97#9600a5#a100a5#a5009c#a50091#a50085#a5007a#a5006e#a50063
 98#9a0000#9a0a00#9a1500#9a2000#9a2b00#9a3600#9a4000#9a4b00#9a5600#9a6100\
 99#9a6c00#9a7700#9a8100#9a8c00#9a9700#929a00#879a00#7c9a00#719a00#679a00\
100#5c9a00#519a00#469a00#3b9a00#309a00#269a00#1b9a00#109a00#059a00#009a05\
101#009a10#009a1a#009a25#009a30#009a3b#009a46#009a50#009a5b#009a66#009a71\
102#009a7c#009a87#009a91#00979a#008d9a#00829a#00779a#006c9a#00619a#00569a\
103#004c9a#00419a#00369a#002b9a#00209a#00169a#000b9a#00009a#0a009a#15009a\
104#20009a#2a009a#35009a#40009a#4b009a#56009a#61009a#6b009a#76009a#81009a\
105#8c009a#97009a#9a0092#9a0087#9a007d#9a0072#9a0067#9a005c
106#8f0000#8f0a00#8f1400#8f1e00#8f2800#8f3200#8f3c00#8f4600#8f5000#8f5a00\
107#8f6400#8f6e00#8f7800#8f8200#8f8c00#888f00#7e8f00#748f00#698f00#5f8f00\
108#558f00#4b8f00#418f00#378f00#2d8f00#238f00#198f00#0f8f00#058f00#008f04\
109#008f0e#008f18#008f23#008f2d#008f37#008f41#008f4b#008f55#008f5f#008f69\
110#008f73#008f7d#008f87#008d8f#00838f#00798f#006f8f#00658f#005b8f#00508f\
111#00468f#003c8f#00328f#00288f#001e8f#00148f#000a8f#00008f#09008f#13008f\
112#1d008f#27008f#31008f#3c008f#46008f#50008f#5a008f#64008f#6e008f#78008f\
113#82008f#8c008f#8f0088#8f007e#8f0074#8f006a#8f0060#8f0056
114#840000#840900#841200#841b00#842500#842e00#843700#844100#844a00#845300\
115#845d00#846600#846f00#847900#848200#7d8400#748400#6b8400#628400#588400\
116#4f8400#468400#3c8400#338400#2a8400#208400#178400#0e8400#048400#008404\
117#00840d#008417#008420#008429#008433#00843c#008445#00844f#008458#008461\
118#00846a#008474#00847d#008284#007984#007084#006684#005d84#005484#004a84\
119#004184#003884#002e84#002584#001c84#001284#000984#000084#080084#120084\
120#1b0084#240084#2e0084#370084#400084#4a0084#530084#5c0084#660084#6f0084\
121#780084#820084#84007e#840074#84006b#840062#840059#84004f
122#7a0000#7a0800#7a1100#7a1900#7a2200#7a2a00#7a3300#7a3b00#7a4400#7a4d00\
123#7a5500#7a5e00#7a6600#7a6f00#7a7700#737a00#6b7a00#627a00#5a7a00#517a00\
124#487a00#407a00#377a00#2f7a00#267a00#1e7a00#157a00#0d7a00#047a00#007a04\
125#007a0c#007a15#007a1d#007a26#007a2e#007a37#007a40#007a48#007a51#007a59\
126#007a62#007a6a#007a73#00787a#006f7a#00677a#005e7a#00557a#004d7a#00447a\
127#003c7a#00337a#002b7a#00227a#001a7a#00117a#00087a#00007a#08007a#10007a\
128#19007a#21007a#2a007a#33007a#3b007a#44007a#4c007a#55007a#5d007a#66007a\
129#6f007a#77007a#7a0074#7a006b#7a0062#7a005a#7a0051#7a0049
130#6f0000#6f0700#6f0f00#6f1700#6f1f00#6f2700#6f2e00#6f3600#6f3e00#6f4600\
131#6f4e00#6f5500#6f5d00#6f6500#6f6d00#696f00#616f00#596f00#526f00#4a6f00\
132#426f00#3a6f00#326f00#2b6f00#236f00#1b6f00#136f00#0b6f00#046f00#006f03\
133#006f0b#006f13#006f1b#006f23#006f2a#006f32#006f3a#006f42#006f4a#006f51\
134#006f59#006f61#006f69#006d6f#00656f#005e6f#00566f#004e6f#00466f#003e6f\
135#00366f#002f6f#00276f#001f6f#00176f#000f6f#00086f#00006f#07006f#0f006f\
136#17006f#1e006f#26006f#2e006f#36006f#3e006f#46006f#4d006f#55006f#5d006f\
137#65006f#6d006f#6f0069#6f0062#6f005a#6f0052#6f004a#6f0042
138#640000#640700#640e00#641500#641c00#642300#642a00#643100#643800#643f00\
139#644600#644d00#645400#645b00#646200#5f6400#586400#516400#4a6400#436400\
140#3c6400#356400#2e6400#266400#1f6400#186400#116400#0a6400#036400#006403\
141#00640a#006411#006418#00641f#006426#00642d#006434#00643b#006442#006449\
142#006451#006458#00645f#006364#005c64#005464#004d64#004664#003f64#003864\
143#003164#002a64#002364#001c64#001564#000e64#000764#000064#060064#0d0064\
144#140064#1b0064#230064#2a0064#310064#380064#3f0064#460064#4d0064#540064\
145#5b0064#620064#64005f#640058#640051#64004a#640043#64003c
146#590000#590600#590c00#591200#591900#591f00#592500#592c00#593200#593800\
147#593f00#594500#594b00#595100#595800#555900#4e5900#485900#425900#3c5900\
148#355900#2f5900#295900#225900#1c5900#165900#0f5900#095900#035900#005903\
149#005909#00590f#005915#00591c#005922#005928#00592f#005935#00593b#005942\
150#005948#00594e#005955#005859#005259#004b59#004559#003f59#003859#003259\
151#002c59#002659#001f59#001959#001359#000c59#000659#000059#060059#0c0059\
152#120059#180059#1f0059#250059#2b0059#320059#380059#3e0059#450059#4b0059\
153#510059#580059#590055#59004f#590048#590042#59003c#590035
154#4f0000#4f0500#4f0b00#4f1000#4f1600#4f1b00#4f2100#4f2600#4f2c00#4f3100\
155#4f3700#4f3d00#4f4200#4f4800#4f4d00#4b4f00#454f00#3f4f00#3a4f00#344f00\
156#2f4f00#294f00#244f00#1e4f00#194f00#134f00#0d4f00#084f00#024f00#004f02\
157#004f08#004f0d#004f13#004f18#004f1e#004f23#004f29#004f2f#004f34#004f3a\
158#004f3f#004f45#004f4a#004d4f#00484f#00424f#003d4f#00374f#00324f#002c4f\
159#00274f#00214f#001b4f#00164f#00104f#000b4f#00054f#00004f#05004f#0a004f\
160#10004f#16004f#1b004f#21004f#26004f#2c004f#31004f#37004f#3c004f#42004f\
161#47004f#4d004f#4f004b#4f0045#4f0040#4f003a#4f0035#4f002f
162#440000#440400#440900#440e00#441300#441800#441c00#442100#442600#442b00\
163#443000#443400#443900#443e00#444300#404400#3c4400#374400#324400#2d4400\
164#284400#244400#1f4400#1a4400#154400#104400#0c4400#074400#024400#004402\
165#004407#00440b#004410#004415#00441a#00441f#004423#004428#00442d#004432\
166#004437#00443b#004440#004344#003e44#003944#003444#003044#002b44#002644\
167#002144#001c44#001844#001344#000e44#000944#000444#000044#040044#090044\
168#0e0044#130044#170044#1c0044#210044#260044#2b0044#2f0044#340044#390044\
169#3e0044#430044#440041#44003c#440037#440032#44002d#440029
170#390000#390400#390800#390c00#391000#391400#391800#391c00#392000#392400\
171#392800#392c00#393000#393400#393800#363900#323900#2e3900#2a3900#263900\
172#223900#1e3900#1a3900#163900#123900#0e3900#0a3900#063900#023900#003901\
173#003905#00390a#00390e#003912#003916#00391a#00391e#003922#003926#00392a\
174#00392e#003932#003936#003839#003439#003039#002c39#002839#002439#002039\
175#001c39#001839#001439#001039#000c39#000839#000439#000039#030039#070039\
176#0b0039#100039#140039#180039#1c0039#200039#240039#280039#2c0039#300039\
177#340039#380039#390036#390032#39002e#39002a#390026#390022
178#2e0000#2e0300#2e0600#2e0900#2e0d00#2e1000#2e1300#2e1700#2e1a00#2e1d00\
179#2e2000#2e2400#2e2700#2e2a00#2e2e00#2c2e00#292e00#252e00#222e00#1f2e00\
180#1c2e00#182e00#152e00#122e00#0e2e00#0b2e00#082e00#052e00#012e00#002e01\
181#002e04#002e08#002e0b#002e0e#002e12#002e15#002e18#002e1b#002e1f#002e22\
182#002e25#002e29#002e2c#002e2e#002a2e#00272e#00242e#00212e#001d2e#001a2e\
183#00172e#00132e#00102e#000d2e#000a2e#00062e#00032e#00002e#03002e#06002e\
184#09002e#0d002e#10002e#13002e#16002e#1a002e#1d002e#20002e#24002e#27002e\
185#2a002e#2d002e#2e002c#2e0029#2e0026#2e0022#2e001f#2e001c
186#240000#240200#240500#240700#240a00#240c00#240f00#241100#241400#241600\
187#241900#241b00#241e00#242100#242300#222400#1f2400#1d2400#1a2400#182400\
188#152400#132400#102400#0e2400#0b2400#082400#062400#032400#012400#002401\
189#002403#002406#002408#00240b#00240d#002410#002413#002415#002418#00241a\
190#00241d#00241f#002422#002324#002124#001e24#001c24#001924#001624#001424\
191#001124#000f24#000c24#000a24#000724#000524#000224#000024#020024#040024\
192#070024#0a0024#0c0024#0f0024#110024#140024#160024#190024#1b0024#1e0024\
193#200024#230024#240022#24001f#24001d#24001a#240018#240015
194#190000#190100#190300#190500#190700#190800#190a00#190c00#190e00#191000\
195#191100#191300#191500#191700#191900#181900#161900#141900#121900#111900\
196#0f1900#0d1900#0b1900#091900#081900#061900#041900#021900#001900#001900\
197#001902#001904#001906#001908#001909#00190b#00190d#00190f#001910#001912\
198#001914#001916#001918#001919#001719#001519#001319#001119#001019#000e19\
199#000c19#000a19#000919#000719#000519#000319#000119#000019#010019#030019\
200#050019#070019#080019#0a0019#0c0019#0e0019#100019#110019#130019#150019\
201#170019#180019#190018#190016#190014#190012#190011#19000f
202"""
203# raise Exception(CHART_TRUE)
204
205CHART_256 = """
206brown__   dark_red_   dark_magenta_   dark_blue_   dark_cyan_   dark_green_
207yellow_   light_red   light_magenta   light_blue   light_cyan   light_green
208
209              #00f#06f#08f#0af#0df#0ff        black_______    dark_gray___
210            #60f#00d#06d#08d#0ad#0dd#0fd        light_gray__    white_______
211          #80f#60d#00a#06a#08a#0aa#0da#0fa
212        #a0f#80d#60a#008#068#088#0a8#0d8#0f8
213      #d0f#a0d#80d#608#006#066#086#0a6#0d6#0f6
214    #f0f#d0d#a0a#808#606#000#060#080#0a0#0d0#0f0#0f6#0f8#0fa#0fd#0ff
215      #f0d#d0a#a08#806#600#660#680#6a0#6d0#6f0#6f6#6f8#6fa#6fd#6ff#0df
216        #f0a#d08#a06#800#860#880#8a0#8d0#8f0#8f6#8f8#8fa#8fd#8ff#6df#0af
217          #f08#d06#a00#a60#a80#aa0#ad0#af0#af6#af8#afa#afd#aff#8df#6af#08f
218            #f06#d00#d60#d80#da0#dd0#df0#df6#df8#dfa#dfd#dff#adf#8af#68f#06f
219              #f00#f60#f80#fa0#fd0#ff0#ff6#ff8#ffa#ffd#fff#ddf#aaf#88f#66f#00f
220                                    #fd0#fd6#fd8#fda#fdd#fdf#daf#a8f#86f#60f
221      #66d#68d#6ad#6dd                #fa0#fa6#fa8#faa#fad#faf#d8f#a6f#80f
222    #86d#66a#68a#6aa#6da                #f80#f86#f88#f8a#f8d#f8f#d6f#a0f
223  #a6d#86a#668#688#6a8#6d8                #f60#f66#f68#f6a#f6d#f6f#d0f
224#d6d#a6a#868#666#686#6a6#6d6#6d8#6da#6dd    #f00#f06#f08#f0a#f0d#f0f
225  #d6a#a68#866#886#8a6#8d6#8d8#8da#8dd#6ad
226    #d68#a66#a86#aa6#ad6#ad8#ada#add#8ad#68d
227      #d66#d86#da6#dd6#dd8#dda#ddd#aad#88d#66d        g78_g82_g85_g89_g93_g100
228                    #da6#da8#daa#dad#a8d#86d        g52_g58_g62_g66_g70_g74_
229      #88a#8aa        #d86#d88#d8a#d8d#a6d        g27_g31_g35_g38_g42_g46_g50_
230    #a8a#888#8a8#8aa    #d66#d68#d6a#d6d        g0__g3__g7__g11_g15_g19_g23_
231      #a88#aa8#aaa#88a
232            #a88#a8a
233"""
234
235CHART_88 = """
236brown__   dark_red_   dark_magenta_   dark_blue_   dark_cyan_   dark_green_
237yellow_   light_red   light_magenta   light_blue   light_cyan   light_green
238
239      #00f#08f#0cf#0ff            black_______    dark_gray___
240    #80f#00c#08c#0cc#0fc            light_gray__    white_______
241  #c0f#80c#008#088#0c8#0f8
242#f0f#c0c#808#000#080#0c0#0f0#0f8#0fc#0ff            #88c#8cc
243  #f0c#c08#800#880#8c0#8f0#8f8#8fc#8ff#0cf        #c8c#888#8c8#8cc
244    #f08#c00#c80#cc0#cf0#cf8#cfc#cff#8cf#08f        #c88#cc8#ccc#88c
245      #f00#f80#fc0#ff0#ff8#ffc#fff#ccf#88f#00f            #c88#c8c
246                    #fc0#fc8#fcc#fcf#c8f#80f
247                      #f80#f88#f8c#f8f#c0f        g62_g74_g82_g89_g100
248                        #f00#f08#f0c#f0f        g0__g19_g35_g46_g52
249"""
250
251CHART_16 = """
252brown__   dark_red_   dark_magenta_   dark_blue_   dark_cyan_   dark_green_
253yellow_   light_red   light_magenta   light_blue   light_cyan   light_green
254
255black_______    dark_gray___    light_gray__    white_______
256"""
257
258ATTR_RE = re.compile(r"(?P<whitespace>[ \n]*)(?P<entry>(?:#[0-9A-Fa-f]{6})|(?:#[0-9A-Fa-f]{3})|(?:[^ \n]+))")
259LONG_ATTR = 7
260SHORT_ATTR = 4  # length of short high-colour descriptions which may
261# be packed one after the next
262
263
264def parse_chart(
265    chart: str,
266    convert: Callable[[str], tuple[urwid.AttrSpec, str] | None],
267) -> list[str | tuple[urwid.AttrSpec, str]]:
268    """Convert string chart into text markup with the correct attributes.
269
270    :param chart: palette chart as a string
271    :param convert: function that converts a single palette entry to an (attr, text) tuple, or ``None`` if no
272                     match is found
273    :returns: text markup for the chart
274    """
275    out = []
276    for match in ATTR_RE.finditer(chart):
277        if match.group("whitespace"):
278            out.append(match.group("whitespace"))
279        entry = match.group("entry")
280        entry = entry.replace("_", " ")
281        while entry:
282            if chart == CHART_TRUE and len(entry) == LONG_ATTR:
283                attrtext = convert(entry[:LONG_ATTR])
284                elen = LONG_ATTR
285            else:
286                elen = SHORT_ATTR
287                attrtext = convert(entry[:SHORT_ATTR])
288            # try the first four characters
289            if attrtext:
290                entry = entry[elen:].strip()
291            else:  # try the whole thing
292                attrtext = convert(entry.strip())
293                assert attrtext, f"Invalid palette entry: {entry!r}"  # noqa: S101  # "assert" is OK for examples
294                elen = len(entry)
295                entry = ""
296            attr, text = attrtext
297            if chart == CHART_TRUE:
298                out.append((attr, "▄"))
299            else:
300                out.append((attr, text.ljust(elen)))
301    return out
302
303
304def foreground_chart(
305    chart: str,
306    background: str,
307    colors: Literal[1, 16, 88, 256, 16777216],
308) -> list[str | tuple[urwid.AttrSpec, str]]:
309    """Create text markup for a foreground colour chart.
310
311    :param chart: palette chart as string
312    :param background: colour to use for background of chart
313    :param colors: number of colors
314    :returns: text markup for the chart
315    """
316
317    def convert_foreground(entry: str) -> tuple[urwid.AttrSpec, str] | None:
318        try:
319            attr = urwid.AttrSpec(entry, background, colors)
320        except urwid.AttrSpecError:
321            return None
322        return attr, entry
323
324    return parse_chart(chart, convert_foreground)
325
326
327def background_chart(
328    chart: str,
329    foreground: str,
330    colors: Literal[1, 16, 88, 256, 16777216],
331) -> list[str | tuple[urwid.AttrSpec, str]]:
332    """Create text markup for a background colour chart.
333
334    Remaps 8 <= colour < 16 to high-colour versions in the hopes of greater compatibility.
335
336    :param chart: palette chart as string
337    :param foreground: colour to use for foreground of chart
338    :param colors: number of colors
339    :returns: text markup for the chart
340    """
341
342    def convert_background(entry: str) -> tuple[urwid.AttrSpec, str] | None:
343        try:
344            attr = urwid.AttrSpec(foreground, entry, colors)
345        except urwid.AttrSpecError:
346            return None
347        # fix 8 <= colour < 16
348        if colors > 16 and attr.background_basic and attr.background_number >= 8:
349            # use high-colour with same number
350            entry = f"h{attr.background_number:d}"
351            attr = urwid.AttrSpec(foreground, entry, colors)
352        return attr, entry
353
354    return parse_chart(chart, convert_background)
355
356
357def main() -> None:
358    """Run the palette test example program."""
359    palette = [
360        ("header", "black,underline", "light gray", "standout,underline", "black,underline", "#88a"),
361        ("panel", "light gray", "dark blue", "", "#ffd", "#00a"),
362        ("focus", "light gray", "dark cyan", "standout", "#ff8", "#806"),
363    ]
364
365    screen = urwid.display.raw.Screen()
366    screen.register_palette(palette)
367
368    lb: urwid.SimpleListWalker[urwid.widget.AbstractFlowWidget] = urwid.SimpleListWalker([])
369    chart_offset = 0  # offset of chart in lb list
370
371    mode_radio_buttons: list[urwid.RadioButton] = []
372    chart_radio_buttons: list[urwid.RadioButton] = []
373
374    def fcs(widget: _Widget) -> urwid.AttrMap[_Widget]:
375        # wrap widgets that can take focus
376        return urwid.AttrMap(widget, None, "focus")
377
378    def set_mode(colors: Literal[1, 16, 88, 256, 16777216], is_foreground_chart: bool) -> None:
379        # set terminal mode and redraw chart
380        screen.set_terminal_properties(colors)
381        screen.reset_default_terminal_palette()
382
383        chart_fn = (background_chart, foreground_chart)[is_foreground_chart]
384        if colors == 1:
385            lb[chart_offset] = urwid.Divider()
386        else:
387            chart: str = {16: CHART_16, 88: CHART_88, 256: CHART_256, 2**24: CHART_TRUE}[colors]
388            txt = chart_fn(chart, "default", colors)
389            lb[chart_offset] = urwid.Text(txt, wrap=urwid.CLIP)
390
391    def on_mode_change(colors: Literal[1, 16, 88, 256, 16777216], rb: urwid.RadioButton, state: bool) -> None:
392        # if this radio button is checked
393        if state:
394            is_foreground_chart = chart_radio_buttons[0].state
395            set_mode(colors, typing.cast("bool", is_foreground_chart))
396
397    def mode_rb(text: str, colors: int, state: bool = False) -> urwid.AttrMap[urwid.RadioButton]:
398        # mode radio buttons
399        rb = urwid.RadioButton(mode_radio_buttons, text, state)
400        urwid.connect_signal(rb, "change", on_mode_change, user_args=(colors,))
401        return fcs(rb)
402
403    def on_chart_change(rb: urwid.RadioButton, state: bool) -> None:
404        # handle foreground check box state change
405        set_mode(screen.colors, state)
406
407    def click_exit(button: urwid.Button) -> typing.NoReturn:
408        raise urwid.ExitMainLoop
409
410    # select_navigation lets TAB / SHIFT TAB move through the mode buttons, then the chart buttons and Exit
411    lb.extend(
412        [
413            urwid.AttrMap(urwid.Text("Urwid Palette Test"), "header"),
414            urwid.AttrMap(
415                urwid.Columns(
416                    [
417                        urwid.Pile(
418                            [
419                                mode_rb("Monochrome", 1),
420                                mode_rb("16-Color", 16, True),
421                                mode_rb("88-Color", 88),
422                                mode_rb("256-Color", 256),
423                                mode_rb("24-bit Color", 2**24),
424                            ],
425                            select_navigation=True,
426                        ),
427                        urwid.Pile(
428                            [
429                                fcs(urwid.RadioButton(chart_radio_buttons, "Foreground Colors", True, on_chart_change)),
430                                fcs(urwid.RadioButton(chart_radio_buttons, "Background Colors")),
431                                urwid.Divider(),
432                                fcs(urwid.Button("Exit", click_exit)),
433                            ],
434                            select_navigation=True,
435                        ),
436                    ],
437                    select_navigation=True,
438                ),
439                "panel",
440            ),
441        ]
442    )
443
444    chart_offset = len(lb)
445    lb.extend([urwid.Divider()])  # placeholder for the chart
446
447    set_mode(16, True)  # displays the chart
448
449    def unhandled_input(key: str | tuple[str, int, int, int]) -> None:
450        if key in {"Q", "q", "esc"}:
451            raise urwid.ExitMainLoop
452
453    urwid.MainLoop(urwid.ListBox(lb, select_navigation=True), screen=screen, unhandled_input=unhandled_input).run()
454
455
456if __name__ == "__main__":
457    main()