-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
gh-138122: Integrate live profiler TUI with _colorize theming system #142360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b024e50
b1f1a93
5ccaa25
2e42a81
5bd5b75
214a339
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,19 @@ class ANSIColors: | |
| ColorCodes = set() | ||
| NoColors = ANSIColors() | ||
|
|
||
|
|
||
| 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 | ||
|
|
||
| for attr, code in ANSIColors.__dict__.items(): | ||
| if not attr.startswith("__"): | ||
| ColorCodes.add(code) | ||
|
|
@@ -223,6 +236,119 @@ class Unittest(ThemeSection): | |
| reset: str = ANSIColors.RESET | ||
|
|
||
|
|
||
| @dataclass(frozen=True, kw_only=True) | ||
| class LiveProfiler(ThemeSection): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please slot this and |
||
| """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 | ||
| sorted_header_fg: int = CursesColors.BLACK | ||
| sorted_header_bg: int = CursesColors.YELLOW | ||
|
|
||
| # 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: str = "dark" | ||
|
|
||
|
|
||
| LiveProfilerLight = LiveProfiler( | ||
| # Header colors | ||
| title_fg=CursesColors.BLUE, | ||
| title_bg=CursesColors.DEFAULT, | ||
|
|
||
| # Status display colors | ||
| pid_fg=CursesColors.BLUE, | ||
| uptime_fg=CursesColors.GREEN, | ||
| time_fg=CursesColors.YELLOW, | ||
| 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.YELLOW, | ||
| gc_fg=CursesColors.MAGENTA, | ||
|
|
||
| # Function display colors | ||
| func_total_fg=CursesColors.BLUE, | ||
| func_exec_fg=CursesColors.GREEN, | ||
| func_stack_fg=CursesColors.YELLOW, | ||
| func_shown_fg=CursesColors.MAGENTA, | ||
|
|
||
| # Table header colors | ||
| sorted_header_fg=CursesColors.WHITE, | ||
| sorted_header_bg=CursesColors.BLUE, | ||
|
|
||
| # Data row colors | ||
| samples_fg=CursesColors.BLUE, | ||
| file_fg=CursesColors.GREEN, | ||
| func_fg=CursesColors.MAGENTA, | ||
|
|
||
| # 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. | ||
|
|
@@ -232,6 +358,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) | ||
|
|
@@ -241,6 +368,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, | ||
|
|
@@ -253,6 +381,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, | ||
|
|
@@ -269,6 +398,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(), | ||
|
|
@@ -338,6 +468,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( | ||
| *, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -525,79 +525,57 @@ 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 | ||||||||
| ) | ||||||||
| theme = _colorize.get_theme(force_color=True) | ||||||||
| profiler_theme = theme.live_profiler | ||||||||
|
Comment on lines
+535
to
+536
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe combine and just use
Suggested change
|
||||||||
| 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 on lines
+539
to
+541
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use constants for 4+ below, should we use constants for 1-3? |
||||||||
|
|
||||||||
| header_bg = 2 if profiler_theme.background_style == "dark" else 4 | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use colour constants instead of hardcoded 2 and 4? |
||||||||
| self.display.init_color_pair(COLOR_PAIR_HEADER_BG, 0, 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_GREEN, COLOR_GREEN, COLOR_BLACK | ||||||||
| ) | ||||||||
| self.display.init_color_pair( | ||||||||
| COLOR_PAIR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK | ||||||||
| ) | ||||||||
| self.display.init_color_pair( | ||||||||
| COLOR_PAIR_RED, COLOR_RED, COLOR_BLACK | ||||||||
| ) | ||||||||
| 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, | ||||||||
| "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, | ||||||||
| "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 | ||||||||
| return { | ||||||||
| "header": A_REVERSE | A_BOLD, | ||||||||
| "cyan": A_BOLD, | ||||||||
|
|
@@ -610,7 +588,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, | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| The Tachyon profiler's live TUI now integrates with the experimental | ||
| :mod:`!_colorize` theming system, allowing users to customize colors via | ||
| :func:`!_colorize.set_theme`. A :class:`!LiveProfilerLight` theme is provided | ||
| for light terminal backgrounds. Patch by Pablo Galindo. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather have this one after the for-loop as it's not tied to ANSI colors.