From 1f79d59f54fd84ed58a5b27725c0bb24c684becf Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 5 May 2023 17:42:36 -0500 Subject: [PATCH 1/5] allow "textual run"/python -m, rework init/main/tui --- src/amdgpu_stats/__init__.py | 25 ++++++++ src/amdgpu_stats/__main__.py | 13 ++-- src/amdgpu_stats/tui.py | 112 ++++++++++++++++++----------------- 3 files changed, 89 insertions(+), 61 deletions(-) diff --git a/src/amdgpu_stats/__init__.py b/src/amdgpu_stats/__init__.py index e69de29..38920e4 100644 --- a/src/amdgpu_stats/__init__.py +++ b/src/amdgpu_stats/__init__.py @@ -0,0 +1,25 @@ +"""__init__.py for amdgpu-stats""" + +import sys +from .tui import app +from .utils import ( + AMDGPU_CARDS, +) + + +def check_for_cards() -> bool: + """Used by '__main__' and 'textual_run', they should exit w/ a message if no cards + + Returns: + bool: If any AMD cards found or not""" + if len(AMDGPU_CARDS) > 0: + return True + return False + + +def textual_run() -> None: + if check_for_cards(): + AMDGPUStats = app(watch_css=True) + AMDGPUStats.run() + else: + sys.exit('Could not find an AMD GPU, exiting.') diff --git a/src/amdgpu_stats/__main__.py b/src/amdgpu_stats/__main__.py index cde54a6..63de9b5 100644 --- a/src/amdgpu_stats/__main__.py +++ b/src/amdgpu_stats/__main__.py @@ -1,13 +1,12 @@ """TUI for amdgpu_stats -This file aims to ensure the TUI only starts in interactive shells""" -from .tui import start +This file aims to ensure the TUI only starts in interactive shells +import/use 'amdgpu_stats.utils' to access functions for metrics""" -def main(): - """main function, spawns the TUI for amdgpu_stats""" - start() - +from . import textual_run if __name__ == "__main__": - main() + textual_run() +else: + pass diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index d8830a7..3854833 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -14,8 +14,8 @@ Functions: """ # disable superfluouos linting # pylint: disable=line-too-long -import sys from datetime import datetime +from typing import Optional from rich.text import Text from textual.binding import Binding @@ -61,39 +61,62 @@ class Notification(Static): class GPUStatsWidget(Static): """The main stats widget.""" - # define the columns for the stats table; used as keys during update - columns = ["Card", - "Core clock", - "Memory clock", - "Utilization", - "Voltage", - "Power", - "[italic]Limit", - "[italic]Default", - "[italic]Capability", - "Fan RPM", - "Edge temp", - "Junction temp", - "Memory temp"] - # initialize empty/default instance vars + def get_column_data_mapping(self, card: Optional[str] = None) -> dict: + '''Returns a dictionary of stats + + Columns are derived from keys, and values provide measurements + *Measurements require `card`*''' + if card is None: + return { + "Card": "", + "Core clock": "", + "Memory clock": "", + "Utilization": "", + "Voltage": "", + "Power": "", + "[italic]Limit": "", + "[italic]Default": "", + "[italic]Capability": "", + "Fan RPM": "", + "Edge temp": "", + "Junction temp": "", + "Memory temp": "" + } + return { + "Card": card, + "Core clock": get_clock('core', card=card, format_freq=True), + "Memory clock": get_clock('memory', card=card, format_freq=True), + "Utilization": f'{get_gpu_usage(card=card)}%', + "Voltage": f'{get_voltage(card=card)}V', + "Power": f'{get_power_stats(card=card)["average"]}W', + "[italic]Limit": f'{get_power_stats(card=card)["limit"]}W', + "[italic]Default": f'{get_power_stats(card=card)["default"]}W', + "[italic]Capability": f'{get_power_stats(card=card)["capability"]}W', + "Fan RPM": f'{get_fan_rpm(card=card)}', + "Edge temp": f"{get_temp_stat(name='edge', card=card)}C", + "Junction temp": f"{get_temp_stat(name='junction', card=card)}C", + "Memory temp": f"{get_temp_stat(name='mem', card=card)}C" + } + + # initialize empty/default instance vars and objects data = {} stats_table = None - text_log = None tabbed_container = None - table = None + text_log = None + timer_stats = None # mark the table as needing initialization (with rows) table_needs_init = True - timer_stats = None def __init__(self, *args, cards=None, **kwargs): super().__init__(*args, **kwargs) + self.cards = cards self.text_log = TextLog(highlight=True, markup=True, name='log_gpu', classes='logs') self.stats_table = DataTable(zebra_stripes=True, - show_cursor=False, + show_cursor=True, name='stats_table', classes='stat_table') self.tabbed_container = TabbedContent() @@ -101,12 +124,13 @@ class GPUStatsWidget(Static): async def on_mount(self) -> None: '''Fires when stats widget 'mounted', behaves like on first showing''' # construct the table columns - for column in self.columns: + columns = list(self.get_column_data_mapping(None).keys()) + for column in columns: self.stats_table.add_column(label=column, key=column) # do a one-off stat collection, populate table before the interval self.get_stats() - # stand up the stat-collecting interval, once per second - self.timer_stats = self.set_interval(1, self.get_stats) + # stand up the stat-collecting interval, twice per second + self.timer_stats = self.set_interval(0.5, self.get_stats) def compose(self) -> ComposeResult: """Create child widgets.""" @@ -127,25 +151,11 @@ class GPUStatsWidget(Static): def get_stats(self): '''Function to fetch stats / update the table for each AMD GPU found''' for card in self.cards: - power_stats = get_power_stats(card=card) # annoyingly, must retain the styling used w/ the cols above # otherwise stats won't update # noticed when fiddling 'style' below between new/update 'Text' # should store their IDs on creation and map those instead - self.data = { - "Card": card, - "Core clock": get_clock('core', card=card, format_freq=True), - "Memory clock": get_clock('memory', card=card, format_freq=True), - "Utilization": f'{get_gpu_usage(card=card)}%', - "Voltage": f'{get_voltage(card=card)}V', - "Power": f'{power_stats["average"]}W', - "[italic]Limit": f'{power_stats["limit"]}W', - "[italic]Default": f'{power_stats["default"]}W', - "[italic]Capability": f'{power_stats["capability"]}W', - "Fan RPM": f'{get_fan_rpm(card=card)}', - "Edge temp": f"{get_temp_stat(name='edge', card=card)}C", - "Junction temp": f"{get_temp_stat(name='junction', card=card)}C", - "Memory temp": f"{get_temp_stat(name='mem', card=card)}C"} + self.data = self.get_column_data_mapping(card) # handle the table data appopriately # if needs populated anew or updated if self.table_needs_init: @@ -167,10 +177,11 @@ class GPUStatsWidget(Static): self.table_needs_init = False -class AMDGPUStats(App): +class app(App): """Textual-based tool to show AMDGPU statistics.""" - # apply stylesheet + # apply stylesheet; this is watched/dynamically reloaded + # can be edited (in installation dir) and seen live CSS_PATH = 'style.css' # set the title - same as the class, but with spaces @@ -179,23 +190,25 @@ class AMDGPUStats(App): # setup keybinds BINDINGS = [ Binding("c", "custom_dark", "Colors"), - Binding("l", "custom_log", "Logs"), + Binding("t", "custom_tab", "Tab switch"), Binding("s", "custom_screenshot", "Screenshot"), Binding("q", "quit", "Quit") ] + + # create an instance of the stats widget with all cards stats_widget = GPUStatsWidget(cards=AMDGPU_CARDS, name="stats_widget") def compose(self) -> ComposeResult: """Create child widgets for the app.""" yield Header(show_clock=True) - yield Footer() yield Container(self.stats_widget) + yield Footer() def action_custom_dark(self) -> None: """An action to toggle dark mode. - Wraps 'action_toggle_dark' with logging and a refresh""" + Wraps 'action_toggle_dark' with our logging""" self.app.dark = not self.app.dark self.update_log(f"[bold]Dark side: [italic]{self.app.dark}") @@ -212,8 +225,8 @@ class AMDGPUStats(App): self.screen.mount(Notification(message)) self.update_log(message) - def action_custom_log(self) -> None: - """Toggle between the main screen and the LogScreen.""" + def action_custom_tab(self) -> None: + """Toggle between the 'Stats' and 'Logs' tabs""" if self.stats_widget.tabbed_container.active == "tab_stats": self.stats_widget.tabbed_container.active = 'tab_logs' else: @@ -222,12 +235,3 @@ class AMDGPUStats(App): def update_log(self, message: str) -> None: """Update the TextLog widget with a new message.""" self.stats_widget.text_log.write(message) - - -def start() -> None: - '''Spawns the textual UI only during CLI invocation / after argparse''' - if len(AMDGPU_CARDS) > 0: - app = AMDGPUStats(watch_css=True) - app.run() - else: - sys.exit('Could not find an AMD GPU, exiting.') From b4b45d971cdad38c38888afce96260ef6b87ead7 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 5 May 2023 17:43:21 -0500 Subject: [PATCH 2/5] improve footer styling, bump version --- pyproject.toml | 4 ++-- src/amdgpu_stats/style.css | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dd820d0..6774c56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "amdgpu-stats" -version = "0.1.14" +version = "0.1.15" description = "A module/TUI for AMD GPU statistics" authors = ["Josh Lay "] repository = "https://github.com/joshlay/amdgpu_stats" @@ -14,7 +14,7 @@ textual = ">=0.16.0" humanfriendly = ">=10.0" [tool.poetry.scripts] -amdgpu-stats = "amdgpu_stats.tui:start" +amdgpu-stats = "amdgpu_stats:textual_run" [build-system] requires = ["poetry-core"] diff --git a/src/amdgpu_stats/style.css b/src/amdgpu_stats/style.css index 84efbb8..861d6fa 100644 --- a/src/amdgpu_stats/style.css +++ b/src/amdgpu_stats/style.css @@ -40,3 +40,23 @@ Header { content-align: center middle; background: $panel; } + +Footer { + background: #073b61; + dock: bottom; + height: 1; +} + +.footer--highlight { + background: #81a1c1; + color: #434c5e; +} + +.footer--highlight-key { + background: #88c0d0; + color: #434c5e; +} + +.footer--key { + background: #004578; +} From d34a824d77b5e03d10d62cfc055981244823285b Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 5 May 2023 18:49:47 -0500 Subject: [PATCH 3/5] add missing docstring to init --- src/amdgpu_stats/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/amdgpu_stats/__init__.py b/src/amdgpu_stats/__init__.py index 38920e4..1e19243 100644 --- a/src/amdgpu_stats/__init__.py +++ b/src/amdgpu_stats/__init__.py @@ -18,6 +18,7 @@ def check_for_cards() -> bool: def textual_run() -> None: + """runs the AMD GPU Stats TUI; called only when in an interactive shell""" if check_for_cards(): AMDGPUStats = app(watch_css=True) AMDGPUStats.run() From bea5893d687a96ac28a45e42f4ceb59d0ce70fb5 Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 5 May 2023 18:52:45 -0500 Subject: [PATCH 4/5] init: fix snake case linting --- src/amdgpu_stats/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amdgpu_stats/__init__.py b/src/amdgpu_stats/__init__.py index 1e19243..723e81f 100644 --- a/src/amdgpu_stats/__init__.py +++ b/src/amdgpu_stats/__init__.py @@ -20,7 +20,7 @@ def check_for_cards() -> bool: def textual_run() -> None: """runs the AMD GPU Stats TUI; called only when in an interactive shell""" if check_for_cards(): - AMDGPUStats = app(watch_css=True) - AMDGPUStats.run() + amdgpu_stats_app = app(watch_css=True) + amdgpu_stats_app.run() else: sys.exit('Could not find an AMD GPU, exiting.') From 40a7fa2b07c5365102d8534186c1efe24c558e3b Mon Sep 17 00:00:00 2001 From: Josh Lay Date: Fri, 5 May 2023 18:54:16 -0500 Subject: [PATCH 5/5] tui: -pascalcase linting for name required by Textual --- src/amdgpu_stats/tui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/amdgpu_stats/tui.py b/src/amdgpu_stats/tui.py index 3854833..c28da82 100644 --- a/src/amdgpu_stats/tui.py +++ b/src/amdgpu_stats/tui.py @@ -177,7 +177,7 @@ class GPUStatsWidget(Static): self.table_needs_init = False -class app(App): +class app(App): # pylint: disable=invalid-name """Textual-based tool to show AMDGPU statistics.""" # apply stylesheet; this is watched/dynamically reloaded