350 lines
11 KiB
Python
350 lines
11 KiB
Python
"""Feature demo: colors, bold/italic, CJK, emoji, background colors — cycles through 5 panels."""
|
|
|
|
import math
|
|
import time
|
|
import termstr
|
|
import textgrid
|
|
from cwcwidth import wcswidth
|
|
|
|
ROWS = 24
|
|
COLS = 80
|
|
BLOCKS = " ░▒▓█"
|
|
|
|
|
|
def center(text):
|
|
"""Center text in COLS columns."""
|
|
w = wcswidth(text)
|
|
if w >= COLS:
|
|
return text
|
|
left = (COLS - w) // 2
|
|
return " " * left + text + " " * (COLS - w - left)
|
|
|
|
|
|
def pad(text):
|
|
"""Left-align and pad text to display width COLS."""
|
|
w = wcswidth(text)
|
|
if w >= COLS:
|
|
return text
|
|
return text + " " * (COLS - w)
|
|
|
|
|
|
def rainbow(hue):
|
|
r = int(255 * (0.5 + 0.5 * math.cos(hue * math.tau)))
|
|
g = int(255 * (0.5 + 0.5 * math.cos(hue * math.tau - 2.094)))
|
|
b = int(255 * (0.5 + 0.5 * math.cos(hue * math.tau - 4.188)))
|
|
return r, g, b
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Panel 1 — Colors
|
|
# ---------------------------------------------------------------------------
|
|
def panel_colors(phase):
|
|
rows = []
|
|
|
|
# header
|
|
rows.append(termstr.TermStr(
|
|
center("Colors: RGB and xterm 256"),
|
|
termstr.CharStyle(fg_color=(120, 120, 120)),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# 2 rows of RGB rainbow
|
|
for _ in range(2):
|
|
s = termstr.CharStyle(fg_color=(0, 0, 0))
|
|
ts = termstr.TermStr(
|
|
"".join(BLOCKS[int((math.sin(x / COLS * math.pi) + 1) * 2)] for x in range(COLS)),
|
|
s,
|
|
)
|
|
rows.append(ts)
|
|
|
|
# 2 rows of xterm 256 gradient
|
|
for offset in (16, 80):
|
|
text = "".join(BLOCKS[(offset + x) % 5] for x in range(COLS))
|
|
styles = tuple(
|
|
termstr.CharStyle(fg_color=(offset + x) % 256) for x in range(COLS)
|
|
)
|
|
rows.append(termstr.TermStr(text, styles))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# ripple (plain str to show default rendering still works)
|
|
for y in range(ROWS - len(rows)):
|
|
line = []
|
|
for x in range(COLS):
|
|
dx = x - COLS / 2
|
|
dy = (y - 2) - ROWS / 2 + 6
|
|
v = math.sin(math.sqrt(dx * dx + dy * dy) * 0.3 - phase)
|
|
v = max(-1, min(1, v))
|
|
idx = int((v + 1) * 0.5 * (len(BLOCKS) - 1))
|
|
line.append(BLOCKS[idx])
|
|
rows.append("".join(line))
|
|
|
|
return rows
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Panel 2 — Font Styles
|
|
# ---------------------------------------------------------------------------
|
|
def panel_font_styles():
|
|
rows = []
|
|
|
|
rows.append(termstr.TermStr(
|
|
center("Font Styles: Bold · Italic · Bold+Italic"),
|
|
termstr.CharStyle(fg_color=(120, 120, 120)),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
demos = [
|
|
("Regular", lambda s: s),
|
|
("Bold", lambda s: s.bold()),
|
|
("Italic", lambda s: s.italic()),
|
|
("Underline", lambda s: s.underline()),
|
|
("Bold+Italic", lambda s: s.bold().italic()),
|
|
("Bold+Underline", lambda s: s.bold().underline()),
|
|
("Italic+Underline", lambda s: s.italic().underline()),
|
|
("Bold+Ita+Under", lambda s: s.bold().italic().underline()),
|
|
]
|
|
for label, mod in demos:
|
|
text = f" {label:14s} The quick brown fox jumps over the lazy dog"
|
|
text = pad(text)
|
|
styles = tuple(
|
|
mod(termstr.CharStyle(fg_color=(200, 200, 200)))
|
|
for _ in range(COLS)
|
|
)
|
|
rows.append(termstr.TermStr(text, styles))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# per-character alternation (no wide chars)
|
|
text = " MIXED: Bold | Italic | Regular | Bold+Italic | Repeat..."
|
|
text = pad(text)
|
|
styles = []
|
|
for i in range(COLS):
|
|
s = termstr.CharStyle(fg_color=rainbow(i / COLS))
|
|
if i % 4 == 0:
|
|
s = s.bold()
|
|
elif i % 4 == 1:
|
|
s = s.italic()
|
|
elif i % 4 == 3:
|
|
s = s.bold().italic()
|
|
styles.append(s)
|
|
rows.append(termstr.TermStr(text, tuple(styles)))
|
|
|
|
while len(rows) < ROWS:
|
|
rows.append(" " * COLS)
|
|
return rows
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Panel 3 — Wide Characters
|
|
# ---------------------------------------------------------------------------
|
|
def panel_wide_chars():
|
|
rows = []
|
|
|
|
rows.append(termstr.TermStr(
|
|
center("Wide Characters: CJK & Emoji (2 columns each)"),
|
|
termstr.CharStyle(fg_color=(120, 120, 120)),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# CJK rows — uniform colors, single CharStyle (broadcast)
|
|
for text, fg in [
|
|
(" CJK: 你好世界 你好世界 你好世界 你好世界", (0, 200, 255)),
|
|
(" 你好世界 你好世界 你好世界 你好世界", (100, 220, 255)),
|
|
(" 你好世界 你好世界 你好世界 你好世界", (180, 240, 255)),
|
|
]:
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(fg_color=fg)))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Emoji rows
|
|
for text, fg in [
|
|
(" Emoji: 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡❤️💎", (255, 200, 0)),
|
|
(" 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡❤️💎", (255, 220, 80)),
|
|
(" 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡❤️💎", (255, 240, 120)),
|
|
]:
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(fg_color=fg)))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Mixed row — use plain str (default colors)
|
|
rows.append(pad("hello你好🍕world你好🍔!!!"))
|
|
|
|
rows.append(" " * COLS)
|
|
rows.append(termstr.TermStr(
|
|
center("(Zero-width padding chars are invisible; wide chars occupy 2 cells)"),
|
|
termstr.CharStyle(fg_color=(100, 100, 100)),
|
|
))
|
|
|
|
while len(rows) < ROWS:
|
|
rows.append(" " * COLS)
|
|
return rows
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Panel 4 — Background Colors
|
|
# ---------------------------------------------------------------------------
|
|
def panel_bg_colors():
|
|
rows = []
|
|
|
|
rows.append(termstr.TermStr(
|
|
center("Background Colors: text & emoji on colored backgrounds"),
|
|
termstr.CharStyle(fg_color=(120, 120, 120)),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Highlighted text rows
|
|
labels = [" Highlight", " Selection", " Warning ", " Success "]
|
|
bgs = [(60, 60, 100), (50, 50, 150), (100, 30, 30), (30, 80, 30)]
|
|
for label, bg in zip(labels, bgs):
|
|
text = f"{label}: Lorem ipsum dolor sit amet, consectetur adipiscing elit "
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(bg_color=bg)))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Underline with background (full row)
|
|
rows.append(termstr.TermStr(
|
|
pad(" Underlined text on colored background — highlight + underline "),
|
|
termstr.CharStyle(bg_color=(50, 30, 60)).underline(),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Emoji with background — tests bg + color emoji blending
|
|
rows.append(termstr.TermStr(
|
|
pad(" Emoji on colored backgrounds — 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡ "),
|
|
termstr.CharStyle(bg_color=(40, 40, 80)),
|
|
))
|
|
rows.append(termstr.TermStr(
|
|
pad(" Brighter bg — 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡ "),
|
|
termstr.CharStyle(bg_color=(80, 60, 40)),
|
|
))
|
|
rows.append(termstr.TermStr(
|
|
pad(" Green bg — 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡ "),
|
|
termstr.CharStyle(bg_color=(20, 80, 40)),
|
|
))
|
|
|
|
rows.append(" " * COLS)
|
|
|
|
# Alternating row highlights (like terminal selection)
|
|
rows.append(termstr.TermStr(
|
|
center("Alternating row backgrounds"),
|
|
termstr.CharStyle(fg_color=(100, 100, 100)),
|
|
))
|
|
for i in range(6):
|
|
bg = (30, 35 + i * 8, 50 + i * 12) if i % 2 == 0 else (0, 0, 0)
|
|
icon = "🍕" if i % 2 == 0 else " "
|
|
text = f" Row {i+1}: {icon} Alternating background colors in a list view "
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(bg_color=bg)))
|
|
|
|
while len(rows) < ROWS:
|
|
rows.append(" " * COLS)
|
|
return rows
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Panel 5 — Everything Together
|
|
# ---------------------------------------------------------------------------
|
|
def panel_mixed(phase):
|
|
rows = []
|
|
|
|
rows.append(termstr.TermStr(
|
|
center("All Features Combined"),
|
|
termstr.CharStyle(fg_color=(120, 120, 120)),
|
|
))
|
|
|
|
for y in range(ROWS - 1):
|
|
# Top half: per-char styles with ASCII chars only
|
|
if y < (ROWS - 1) // 2:
|
|
row_chars = []
|
|
row_styles = []
|
|
for x in range(COLS):
|
|
p = math.sin(x * 0.2 + y * 0.3 - phase)
|
|
ch = BLOCKS[int((p + 1) * 2)]
|
|
s = termstr.CharStyle(fg_color=rainbow(x / COLS + y / ROWS))
|
|
if y % 2 == 0:
|
|
s = s.bold()
|
|
elif y % 3 == 0:
|
|
s = s.italic()
|
|
row_chars.append(ch)
|
|
row_styles.append(s)
|
|
rows.append(termstr.TermStr("".join(row_chars), tuple(row_styles)))
|
|
# Bottom half: three bands with mixed features
|
|
else:
|
|
band = y - (ROWS - 1) // 2
|
|
if band % 3 == 0:
|
|
# xterm gradient
|
|
styles = tuple(
|
|
termstr.CharStyle(fg_color=(x + band * 10) % 256)
|
|
for x in range(COLS)
|
|
)
|
|
text = "".join(BLOCKS[(x + band) % 5] for x in range(COLS))
|
|
rows.append(termstr.TermStr(text, styles))
|
|
elif band % 3 == 1:
|
|
text = "CJK: 你好世界你好世界你好世界你好世界"
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(fg_color=(0, 200, 255))))
|
|
else:
|
|
text = "Emoji 🍕🍔🌭🎉🔥⭐💎🚀❤️🌟🎵💡⚡"
|
|
text = pad(text)
|
|
rows.append(termstr.TermStr(text, termstr.CharStyle(fg_color=(255, 200, 0))))
|
|
|
|
return rows
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
_closed = False
|
|
|
|
def _on_close():
|
|
global _closed
|
|
_closed = True
|
|
|
|
def main():
|
|
tg = textgrid.TextGrid(
|
|
ROWS, COLS,
|
|
"TextGrid Feature Demo",
|
|
font_path="/usr/local/share/fonts/truetype/sarasa/SarasaTermSC-Regular.ttf",
|
|
color_font_path="/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf",
|
|
on_close=_on_close,
|
|
)
|
|
phase = 0.0
|
|
try:
|
|
while not _closed:
|
|
tg.update(panel_colors(phase))
|
|
if _closed: break
|
|
time.sleep(4)
|
|
if _closed: break
|
|
tg.update(panel_font_styles())
|
|
if _closed: break
|
|
time.sleep(4)
|
|
if _closed: break
|
|
tg.update(panel_wide_chars())
|
|
if _closed: break
|
|
time.sleep(4)
|
|
if _closed: break
|
|
tg.update(panel_bg_colors())
|
|
if _closed: break
|
|
time.sleep(4)
|
|
if _closed: break
|
|
tg.update(panel_mixed(phase))
|
|
if _closed: break
|
|
time.sleep(4)
|
|
phase += 0.25
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
tg.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|