# types
if False:
- from typing import IO, Self, ClassVar
+ from typing import IO, Literal, Self, ClassVar
_theme: Theme
setattr(NoColors, attr, "")
+class CursesColors:
+ """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
+
+
#
# Experimental theming support (see gh-133346)
#
reset: str = ANSIColors.RESET
+@dataclass(frozen=True, kw_only=True)
+class LiveProfiler(ThemeSection):
+ """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) - use reverse video style
+ normal_header_fg: int = CursesColors.BLACK
+ normal_header_bg: int = CursesColors.WHITE
+
+ # 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'
+ background_style: Literal["dark", "light"] = "dark"
+
+
+LiveProfilerLight = LiveProfiler(
+ # Header colors
+ title_fg=CursesColors.BLUE, # Blue is more readable than cyan on light bg
+
+ # Status display colors - darker colors for light backgrounds
+ pid_fg=CursesColors.BLUE,
+ uptime_fg=CursesColors.BLACK,
+ time_fg=CursesColors.BLACK,
+ interval_fg=CursesColors.BLUE,
+
+ # Thread view colors
+ thread_all_fg=CursesColors.BLACK,
+ thread_single_fg=CursesColors.BLUE,
+
+ # Stats colors
+ waiting_gil_fg=CursesColors.RED,
+ gc_fg=CursesColors.BLUE,
+
+ # Function display colors
+ func_total_fg=CursesColors.BLUE,
+ func_exec_fg=CursesColors.BLACK,
+ func_stack_fg=CursesColors.BLACK,
+ func_shown_fg=CursesColors.BLUE,
+
+ # Table header colors (for sorted column highlight)
+ sorted_header_fg=CursesColors.WHITE,
+ sorted_header_bg=CursesColors.BLUE,
+
+ # Normal header colors (non-sorted columns)
+ normal_header_fg=CursesColors.WHITE,
+ normal_header_bg=CursesColors.BLACK,
+
+ # Data row colors - use dark colors readable on white
+ samples_fg=CursesColors.BLACK,
+ file_fg=CursesColors.BLACK,
+ func_fg=CursesColors.BLUE, # Blue is more readable than magenta on light bg
+
+ # Medal colors for top functions
+ medal_silver_fg=CursesColors.BLUE,
+
+ # Background style
+ background_style="light",
+)
+
+
@dataclass(frozen=True, kw_only=True)
class Syntax(ThemeSection):
prompt: str = ANSIColors.BOLD_MAGENTA
"""
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)
*,
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,
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,
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(),
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(
*,
FINISHED_BANNER_EXTRA_LINES,
DEFAULT_SORT_BY,
DEFAULT_DISPLAY_LIMIT,
+ COLOR_PAIR_SAMPLES,
+ COLOR_PAIR_FILE,
+ COLOR_PAIR_FUNC,
COLOR_PAIR_HEADER_BG,
COLOR_PAIR_CYAN,
COLOR_PAIR_YELLOW,
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).live_profiler
+ default_bg = -1
+
+ self.display.init_color_pair(COLOR_PAIR_SAMPLES, theme.samples_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_FILE, theme.file_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_FUNC, theme.func_fg, default_bg)
+
+ # Normal header background color pair
self.display.init_color_pair(
- COLOR_PAIR_RED, COLOR_RED, COLOR_BLACK
+ COLOR_PAIR_HEADER_BG,
+ theme.normal_header_fg,
+ theme.normal_header_bg,
)
+
+ self.display.init_color_pair(COLOR_PAIR_CYAN, theme.pid_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_YELLOW, theme.time_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_GREEN, theme.uptime_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_MAGENTA, theme.interval_fg, default_bg)
+ self.display.init_color_pair(COLOR_PAIR_RED, theme.off_gil_fg, default_bg)
self.display.init_color_pair(
- COLOR_PAIR_SORTED_HEADER, COLOR_BLACK, COLOR_YELLOW
+ COLOR_PAIR_SORTED_HEADER,
+ theme.sorted_header_fg,
+ theme.sorted_header_bg,
)
+ TREND_UP_PAIR = 11
+ TREND_DOWN_PAIR = 12
+ self.display.init_color_pair(TREND_UP_PAIR, theme.trend_up_fg, default_bg)
+ self.display.init_color_pair(TREND_DOWN_PAIR, 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,
- "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,
+ "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(COLOR_PAIR_SAMPLES),
+ "color_file": self.display.get_color_pair(COLOR_PAIR_FILE),
+ "color_func": self.display.get_color_pair(COLOR_PAIR_FUNC),
+ "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,
"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,
OPCODE_PANEL_HEIGHT = 12 # Height reserved for opcode statistics panel
# Color pair IDs
+COLOR_PAIR_SAMPLES = 1
+COLOR_PAIR_FILE = 2
+COLOR_PAIR_FUNC = 3
COLOR_PAIR_HEADER_BG = 4
COLOR_PAIR_CYAN = 5
COLOR_PAIR_YELLOW = 6
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:
def draw_column_headers(self, line, width):
"""Draw column headers with sort indicators."""
- col = 0
-
# Determine which columns to show based on width
show_sample_pct = width >= WIDTH_THRESHOLD_SAMPLE_PCT
show_tottime = width >= WIDTH_THRESHOLD_TOTTIME
"cumtime": 4,
}.get(self.collector.sort_by, -1)
+ # Build the full header line first, then draw it
+ # This avoids gaps between columns when using reverse video
+ header_parts = []
+ col = 0
+
# Column 0: nsamples
- attr = sorted_header if sort_col == 0 else normal_header
- text = f"{'▼nsamples' if sort_col == 0 else 'nsamples':>13}"
- self.add_str(line, col, text, attr)
+ text = f"{'▼nsamples' if sort_col == 0 else 'nsamples':>13} "
+ header_parts.append((col, text, sorted_header if sort_col == 0 else normal_header))
col += 15
# Column 1: sample %
if show_sample_pct:
- attr = sorted_header if sort_col == 1 else normal_header
- text = f"{'▼%' if sort_col == 1 else '%':>5}"
- self.add_str(line, col, text, attr)
+ text = f"{'▼%' if sort_col == 1 else '%':>5} "
+ header_parts.append((col, text, sorted_header if sort_col == 1 else normal_header))
col += 7
# Column 2: tottime
if show_tottime:
- attr = sorted_header if sort_col == 2 else normal_header
- text = f"{'▼tottime' if sort_col == 2 else 'tottime':>10}"
- self.add_str(line, col, text, attr)
+ text = f"{'▼tottime' if sort_col == 2 else 'tottime':>10} "
+ header_parts.append((col, text, sorted_header if sort_col == 2 else normal_header))
col += 12
# Column 3: cumul %
if show_cumul_pct:
- attr = sorted_header if sort_col == 3 else normal_header
- text = f"{'▼%' if sort_col == 3 else '%':>5}"
- self.add_str(line, col, text, attr)
+ text = f"{'▼%' if sort_col == 3 else '%':>5} "
+ header_parts.append((col, text, sorted_header if sort_col == 3 else normal_header))
col += 7
# Column 4: cumtime
if show_cumtime:
- attr = sorted_header if sort_col == 4 else normal_header
- text = f"{'▼cumtime' if sort_col == 4 else 'cumtime':>10}"
- self.add_str(line, col, text, attr)
+ text = f"{'▼cumtime' if sort_col == 4 else 'cumtime':>10} "
+ header_parts.append((col, text, sorted_header if sort_col == 4 else normal_header))
col += 12
# Remaining headers
MAX_FUNC_NAME_WIDTH,
max(MIN_FUNC_NAME_WIDTH, remaining_space // 2),
)
- self.add_str(
- line, col, f"{'function':<{func_width}}", normal_header
- )
+ text = f"{'function':<{func_width}} "
+ header_parts.append((col, text, normal_header))
col += func_width + 2
if col < width - 10:
- self.add_str(line, col, "file:line", normal_header)
+ file_text = "file:line"
+ padding = width - col - len(file_text)
+ text = file_text + " " * max(0, padding)
+ header_parts.append((col, text, normal_header))
+
+ # Draw full-width background first
+ self.add_str(line, 0, " " * (width - 1), normal_header)
+
+ # Draw each header part on top
+ for col_pos, text, attr in header_parts:
+ self.add_str(line, col_pos, text.rstrip(), attr)
return (
line + 1,
column_flags
)
- # Get color attributes from the colors dict (already initialized)
- color_samples = self.colors.get("color_samples", curses.A_NORMAL)
+ # Get color attributes
color_file = self.colors.get("color_file", curses.A_NORMAL)
color_func = self.colors.get("color_func", curses.A_NORMAL)
# Check if this row is selected
is_selected = show_opcodes and row_idx == selected_row
- # Helper function to get trend color for a specific column
+ # Helper function to get trend color
def get_trend_color(column_name):
if is_selected:
return A_REVERSE | A_BOLD
trend = trends.get(column_name, "stable")
- if trend_tracker is not None:
+ if trend_tracker is not None and trend_tracker.enabled:
return trend_tracker.get_color(trend)
return curses.A_NORMAL
--- /dev/null
+The Tachyon profiler's live TUI now integrates with the experimental
+:mod:`!_colorize` theming system. Users can customize colors via
+:func:`!_colorize.set_theme` (experimental API, subject to change).
+A :class:`!LiveProfilerLight` theme is provided for light terminal backgrounds.
+Patch by Pablo Galindo.