Skip to content
141 changes: 141 additions & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ class ANSIColors:
ColorCodes = set()
NoColors = ANSIColors()


class CursesColors:
Comment thread
pablogsal marked this conversation as resolved.
"""Curses color constants for terminal UI theming."""
BLACK = 0
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4
MAGENTA = 5
CYAN = 6
WHITE = 7
DEFAULT = -1

for attr, code in ANSIColors.__dict__.items():
if not attr.startswith("__"):
ColorCodes.add(code)
Expand Down Expand Up @@ -223,6 +236,127 @@ class Unittest(ThemeSection):
reset: str = ANSIColors.RESET


@dataclass(frozen=True, kw_only=True)
class LiveProfiler(ThemeSection):
Comment thread
pablogsal marked this conversation as resolved.
"""Theme section for the live profiling TUI (Tachyon profiler).

Colors use CursesColors constants (BLACK, RED, GREEN, YELLOW,
BLUE, MAGENTA, CYAN, WHITE, DEFAULT).
"""
# Header colors
title_fg: int = CursesColors.CYAN
title_bg: int = CursesColors.DEFAULT

# Status display colors
pid_fg: int = CursesColors.CYAN
uptime_fg: int = CursesColors.GREEN
time_fg: int = CursesColors.YELLOW
interval_fg: int = CursesColors.MAGENTA

# Thread view colors
thread_all_fg: int = CursesColors.GREEN
thread_single_fg: int = CursesColors.MAGENTA

# Progress bar colors
bar_good_fg: int = CursesColors.GREEN
bar_bad_fg: int = CursesColors.RED

# Stats colors
on_gil_fg: int = CursesColors.GREEN
off_gil_fg: int = CursesColors.RED
waiting_gil_fg: int = CursesColors.YELLOW
gc_fg: int = CursesColors.MAGENTA

# Function display colors
func_total_fg: int = CursesColors.CYAN
func_exec_fg: int = CursesColors.GREEN
func_stack_fg: int = CursesColors.YELLOW
func_shown_fg: int = CursesColors.MAGENTA

# Table header colors (for sorted column highlight)
sorted_header_fg: int = CursesColors.BLACK
sorted_header_bg: int = CursesColors.CYAN

# Normal header colors (non-sorted columns)
normal_header_fg: int = CursesColors.WHITE
normal_header_bg: int = CursesColors.BLUE

# Data row colors
samples_fg: int = CursesColors.CYAN
file_fg: int = CursesColors.GREEN
func_fg: int = CursesColors.YELLOW

# Trend indicator colors
trend_up_fg: int = CursesColors.GREEN
trend_down_fg: int = CursesColors.RED

# Medal colors for top functions
medal_gold_fg: int = CursesColors.RED
medal_silver_fg: int = CursesColors.YELLOW
medal_bronze_fg: int = CursesColors.GREEN

# Background style: 'dark' or 'light'
Comment thread
pablogsal marked this conversation as resolved.
background_style: str = "dark"


LiveProfilerLight = LiveProfiler(
# Header colors
title_fg=CursesColors.BLUE,
Comment thread
pablogsal marked this conversation as resolved.
Outdated
title_bg=CursesColors.DEFAULT,

# Status display colors
pid_fg=CursesColors.BLUE,
uptime_fg=CursesColors.GREEN,
time_fg=CursesColors.RED, # Use red instead of yellow for better visibility on light bg
interval_fg=CursesColors.MAGENTA,

# Thread view colors
thread_all_fg=CursesColors.GREEN,
thread_single_fg=CursesColors.MAGENTA,

# Progress bar colors
bar_good_fg=CursesColors.GREEN,
bar_bad_fg=CursesColors.RED,

# Stats colors
on_gil_fg=CursesColors.GREEN,
off_gil_fg=CursesColors.RED,
waiting_gil_fg=CursesColors.RED, # Use red instead of yellow for visibility
gc_fg=CursesColors.MAGENTA,

# Function display colors
func_total_fg=CursesColors.BLUE,
func_exec_fg=CursesColors.GREEN,
func_stack_fg=CursesColors.RED, # Use red instead of yellow
func_shown_fg=CursesColors.MAGENTA,

# Table header colors (for sorted column highlight)
sorted_header_fg=CursesColors.WHITE,
sorted_header_bg=CursesColors.MAGENTA,

# Normal header colors (non-sorted columns)
normal_header_fg=CursesColors.WHITE,
normal_header_bg=CursesColors.BLUE,

# Data row colors
samples_fg=CursesColors.BLUE,
file_fg=CursesColors.GREEN,
func_fg=CursesColors.MAGENTA, # Use magenta instead of yellow

# Trend indicator colors
trend_up_fg=CursesColors.GREEN,
trend_down_fg=CursesColors.RED,

# Medal colors for top functions
medal_gold_fg=CursesColors.RED,
medal_silver_fg=CursesColors.BLUE,
medal_bronze_fg=CursesColors.GREEN,

# Background style
background_style="light",
)


@dataclass(frozen=True, kw_only=True)
class Theme:
"""A suite of themes for all sections of Python.
Expand All @@ -232,6 +366,7 @@ class Theme:
"""
argparse: Argparse = field(default_factory=Argparse)
difflib: Difflib = field(default_factory=Difflib)
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
syntax: Syntax = field(default_factory=Syntax)
traceback: Traceback = field(default_factory=Traceback)
unittest: Unittest = field(default_factory=Unittest)
Expand All @@ -241,6 +376,7 @@ def copy_with(
*,
argparse: Argparse | None = None,
difflib: Difflib | None = None,
live_profiler: LiveProfiler | None = None,
syntax: Syntax | None = None,
traceback: Traceback | None = None,
unittest: Unittest | None = None,
Expand All @@ -253,6 +389,7 @@ def copy_with(
return type(self)(
argparse=argparse or self.argparse,
difflib=difflib or self.difflib,
live_profiler=live_profiler or self.live_profiler,
syntax=syntax or self.syntax,
traceback=traceback or self.traceback,
unittest=unittest or self.unittest,
Expand All @@ -269,6 +406,7 @@ def no_colors(cls) -> Self:
return cls(
argparse=Argparse.no_colors(),
difflib=Difflib.no_colors(),
live_profiler=LiveProfiler.no_colors(),
syntax=Syntax.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),
Expand Down Expand Up @@ -338,6 +476,9 @@ def _safe_getenv(k: str, fallback: str | None = None) -> str | None:
default_theme = Theme()
theme_no_color = default_theme.no_colors()

# Convenience theme with light profiler colors (for white/light terminal backgrounds)
light_profiler_theme = default_theme.copy_with(live_profiler=LiveProfilerLight)


def get_theme(
*,
Expand Down
92 changes: 37 additions & 55 deletions Lib/profiling/sampling/live_collector/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,79 +525,62 @@ def _cycle_sort(self, reverse=False):

def _setup_colors(self):
"""Set up color pairs and return color attributes."""

A_BOLD = self.display.get_attr("A_BOLD")
A_REVERSE = self.display.get_attr("A_REVERSE")
A_UNDERLINE = self.display.get_attr("A_UNDERLINE")
A_NORMAL = self.display.get_attr("A_NORMAL")

# Check both curses color support and _colorize.can_colorize()
if self.display.has_colors() and self._can_colorize:
with contextlib.suppress(Exception):
# Color constants (using curses values for compatibility)
COLOR_CYAN = 6
COLOR_GREEN = 2
COLOR_YELLOW = 3
COLOR_BLACK = 0
COLOR_MAGENTA = 5
COLOR_RED = 1

# Initialize all color pairs used throughout the UI
self.display.init_color_pair(
1, COLOR_CYAN, -1
) # Data colors for stats rows
self.display.init_color_pair(2, COLOR_GREEN, -1)
self.display.init_color_pair(3, COLOR_YELLOW, -1)
self.display.init_color_pair(
COLOR_PAIR_HEADER_BG, COLOR_BLACK, COLOR_GREEN
)
self.display.init_color_pair(
COLOR_PAIR_CYAN, COLOR_CYAN, COLOR_BLACK
)
self.display.init_color_pair(
COLOR_PAIR_YELLOW, COLOR_YELLOW, COLOR_BLACK
)
self.display.init_color_pair(
COLOR_PAIR_GREEN, COLOR_GREEN, COLOR_BLACK
)
self.display.init_color_pair(
COLOR_PAIR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK
)
theme = _colorize.get_theme(force_color=True)
profiler_theme = theme.live_profiler
Comment thread
pablogsal marked this conversation as resolved.
Outdated
default_bg = -1

self.display.init_color_pair(1, profiler_theme.samples_fg, default_bg)
self.display.init_color_pair(2, profiler_theme.file_fg, default_bg)
self.display.init_color_pair(3, profiler_theme.func_fg, default_bg)
Comment thread
pablogsal marked this conversation as resolved.
Outdated

# Normal header background color pair
self.display.init_color_pair(
COLOR_PAIR_RED, COLOR_RED, COLOR_BLACK
COLOR_PAIR_HEADER_BG,
profiler_theme.normal_header_fg,
profiler_theme.normal_header_bg,
)

self.display.init_color_pair(COLOR_PAIR_CYAN, profiler_theme.pid_fg, default_bg)
self.display.init_color_pair(COLOR_PAIR_YELLOW, profiler_theme.time_fg, default_bg)
self.display.init_color_pair(COLOR_PAIR_GREEN, profiler_theme.uptime_fg, default_bg)
self.display.init_color_pair(COLOR_PAIR_MAGENTA, profiler_theme.interval_fg, default_bg)
self.display.init_color_pair(COLOR_PAIR_RED, profiler_theme.off_gil_fg, default_bg)
self.display.init_color_pair(
COLOR_PAIR_SORTED_HEADER, COLOR_BLACK, COLOR_YELLOW
COLOR_PAIR_SORTED_HEADER,
profiler_theme.sorted_header_fg,
profiler_theme.sorted_header_bg,
)

TREND_UP_PAIR = 11
TREND_DOWN_PAIR = 12
self.display.init_color_pair(TREND_UP_PAIR, profiler_theme.trend_up_fg, default_bg)
self.display.init_color_pair(TREND_DOWN_PAIR, profiler_theme.trend_down_fg, default_bg)

return {
"header": self.display.get_color_pair(COLOR_PAIR_HEADER_BG)
| A_BOLD,
"cyan": self.display.get_color_pair(COLOR_PAIR_CYAN)
| A_BOLD,
"yellow": self.display.get_color_pair(COLOR_PAIR_YELLOW)
| A_BOLD,
"green": self.display.get_color_pair(COLOR_PAIR_GREEN)
| A_BOLD,
"magenta": self.display.get_color_pair(COLOR_PAIR_MAGENTA)
| A_BOLD,
"red": self.display.get_color_pair(COLOR_PAIR_RED)
| A_BOLD,
"sorted_header": self.display.get_color_pair(
COLOR_PAIR_SORTED_HEADER
)
| A_BOLD,
"normal_header": A_REVERSE | A_BOLD,
"header": self.display.get_color_pair(COLOR_PAIR_HEADER_BG) | A_BOLD,
"cyan": self.display.get_color_pair(COLOR_PAIR_CYAN) | A_BOLD,
"yellow": self.display.get_color_pair(COLOR_PAIR_YELLOW) | A_BOLD,
"green": self.display.get_color_pair(COLOR_PAIR_GREEN) | A_BOLD,
"magenta": self.display.get_color_pair(COLOR_PAIR_MAGENTA) | A_BOLD,
"red": self.display.get_color_pair(COLOR_PAIR_RED) | A_BOLD,
"sorted_header": self.display.get_color_pair(COLOR_PAIR_SORTED_HEADER) | A_BOLD,
"normal_header": self.display.get_color_pair(COLOR_PAIR_HEADER_BG) | A_BOLD,
"color_samples": self.display.get_color_pair(1),
"color_file": self.display.get_color_pair(2),
"color_func": self.display.get_color_pair(3),
# Trend colors (stock-like indicators)
"trend_up": self.display.get_color_pair(COLOR_PAIR_GREEN) | A_BOLD,
"trend_down": self.display.get_color_pair(COLOR_PAIR_RED) | A_BOLD,
"trend_up": self.display.get_color_pair(TREND_UP_PAIR) | A_BOLD,
"trend_down": self.display.get_color_pair(TREND_DOWN_PAIR) | A_BOLD,
"trend_stable": A_NORMAL,
}

# Fallback to non-color attributes
# Fallback for no-color mode
return {
"header": A_REVERSE | A_BOLD,
"cyan": A_BOLD,
Expand All @@ -610,7 +593,6 @@ def _setup_colors(self):
"color_samples": A_NORMAL,
"color_file": A_NORMAL,
"color_func": A_NORMAL,
# Trend colors (fallback to bold/normal for monochrome)
"trend_up": A_BOLD,
"trend_down": A_BOLD,
"trend_stable": A_NORMAL,
Expand Down
8 changes: 6 additions & 2 deletions Lib/profiling/sampling/live_collector/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ def get_dimensions(self):
return self.stdscr.getmaxyx()

def clear(self):
self.stdscr.clear()
# Use erase() instead of clear() to avoid flickering
# clear() forces a complete screen redraw, erase() just clears the buffer
self.stdscr.erase()

def refresh(self):
self.stdscr.refresh()

def redraw(self):
self.stdscr.redrawwin()
# Use noutrefresh + doupdate for smoother updates
self.stdscr.noutrefresh()
curses.doupdate()

def add_str(self, line, col, text, attr=0):
try:
Expand Down
Loading