v0.1.11: add SVG 'screenshots', support many cards in TUI
v0.1.11: add SVG 'screenshots', support many cards in TUI
This commit is contained in:
commit
7af94e21cb
9 changed files with 155 additions and 102 deletions
|
@ -1,6 +1,6 @@
|
||||||
# amdgpu_stats
|
# amdgpu_stats
|
||||||
|
|
||||||
A simple Python module/TUI for AMD GPU statistics
|
A Python module/TUI for AMD GPU statistics
|
||||||
|
|
||||||
![Screenshot of main screen](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/main.png "Main screen")
|
![Screenshot of main screen](https://raw.githubusercontent.com/joshlay/amdgpu_stats/master/screens/main.png "Main screen")
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ For more information on what the module provides, please see:
|
||||||
- `help('amdgpu_stats.utils')` in your interpreter
|
- `help('amdgpu_stats.utils')` in your interpreter
|
||||||
- [The module source](https://github.com/joshlay/amdgpu_stats/blob/master/src/amdgpu_stats/utils.py)
|
- [The module source](https://github.com/joshlay/amdgpu_stats/blob/master/src/amdgpu_stats/utils.py)
|
||||||
|
|
||||||
Feature requests [are encouraged](https://github.com/joshlay/amdgpu_stats/issues) :)
|
Feature requests [are encouraged](https://github.com/joshlay/amdgpu_stats/issues) 😀
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
Only `Linux` is supported. Information is _completely_ sourced from interfaces in `sysfs`.
|
Only `Linux` is supported. Information is _completely_ sourced from interfaces in `sysfs`.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "amdgpu-stats"
|
name = "amdgpu-stats"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
description = "A simple module/TUI for AMD GPU statistics"
|
description = "A module/TUI for AMD GPU statistics"
|
||||||
authors = ["Josh Lay <pypi@jlay.io>"]
|
authors = ["Josh Lay <pypi@jlay.io>"]
|
||||||
repository = "https://github.com/joshlay/amdgpu_stats"
|
repository = "https://github.com/joshlay/amdgpu_stats"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -14,7 +14,7 @@ textual = ">=0.10"
|
||||||
humanfriendly = ">=10.0"
|
humanfriendly = ">=10.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
amdgpu-stats = "amdgpu_stats.interface:tui"
|
amdgpu-stats = "amdgpu_stats.tui:start"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 22 KiB |
BIN
screens/main.png
BIN
screens/main.png
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 32 KiB |
13
src/amdgpu_stats/__main__.py
Normal file
13
src/amdgpu_stats/__main__.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"""TUI for amdgpu_stats
|
||||||
|
|
||||||
|
This file aims to ensure the TUI only starts in interactive shells"""
|
||||||
|
from .tui import start
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""main function, spawns the TUI for amdgpu_stats"""
|
||||||
|
start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -1,23 +0,0 @@
|
||||||
GPUStatsWidget {
|
|
||||||
layout: grid;
|
|
||||||
grid-size: 3;
|
|
||||||
grid-gutter: 2 12;
|
|
||||||
background: $boost;
|
|
||||||
height: 11;
|
|
||||||
min-width: 50;
|
|
||||||
padding: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */
|
|
||||||
|
|
||||||
.box {
|
|
||||||
height: 100%;
|
|
||||||
/* border: ascii $primary-background;*/
|
|
||||||
border: solid $primary-background;
|
|
||||||
content-align: center middle;
|
|
||||||
padding: 0 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statvalue {
|
|
||||||
dock: right;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
"""Pretty Textual-based stats for AMD GPUs
|
|
||||||
|
|
||||||
TODO: restore argparse / --card, in case detection fails.
|
|
||||||
will require separating the hwmon finding tasks from 'find_card'
|
|
||||||
|
|
||||||
rich markup reference:
|
|
||||||
https://rich.readthedocs.io/en/stable/markup.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from .interface import tui
|
|
||||||
tui()
|
|
38
src/amdgpu_stats/style.css
Normal file
38
src/amdgpu_stats/style.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
Header {
|
||||||
|
background: $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPUStatsWidget {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-gutter: 2 8;
|
||||||
|
box-sizing: content-box;
|
||||||
|
background: $panel;
|
||||||
|
height: 10;
|
||||||
|
min-width: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */
|
||||||
|
|
||||||
|
.widgetheader {
|
||||||
|
background: $panel;
|
||||||
|
border: solid $primary-background;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
/* border: ascii $primary-background;*/
|
||||||
|
content-align: center middle;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Horizontal {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statvalue {
|
||||||
|
dock: right;
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
"""
|
"""
|
||||||
interface.py
|
tui.py
|
||||||
|
|
||||||
This module provides the TUI aspect of 'amdgpu-stats'
|
This file provides the user interface of `amdgpu-stats`
|
||||||
|
|
||||||
|
Can be used as a way to monitor GPU(s) in your terminal, or inform other utilities.
|
||||||
|
|
||||||
Classes:
|
Classes:
|
||||||
- GPUStats: the object for the _Application_, instantiated at runtime
|
- GPUStats: the object for the _Application_, instantiated at runtime
|
||||||
|
@ -12,9 +14,13 @@ Classes:
|
||||||
- LogScreen: Second screen with the logging widget, header, and footer
|
- LogScreen: Second screen with the logging widget, header, and footer
|
||||||
|
|
||||||
Functions:
|
Functions:
|
||||||
- tui: Renders the TUI using the classes above
|
- start: Creates the 'App' and renders the TUI using the classes above
|
||||||
"""
|
"""
|
||||||
|
# disable superfluouos linting
|
||||||
|
# pylint: disable=line-too-long
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from os import path
|
||||||
|
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
|
@ -25,11 +31,8 @@ from textual.widgets import Header, Footer, Static, TextLog, Label
|
||||||
|
|
||||||
from .utils import AMDGPU_CARDS, format_frequency, get_core_stats, get_fan_rpm, get_fan_target, get_power_stats, get_temp_stats # pylint: disable=line-too-long
|
from .utils import AMDGPU_CARDS, format_frequency, get_core_stats, get_fan_rpm, get_fan_target, get_power_stats, get_temp_stats # pylint: disable=line-too-long
|
||||||
|
|
||||||
# globals - card handling / choice for TUI
|
# rich markup reference:
|
||||||
if len(AMDGPU_CARDS) > 0:
|
# https://rich.readthedocs.io/en/stable/markup.html
|
||||||
# default to showing stats for the first detected card
|
|
||||||
CARD = next(iter(AMDGPU_CARDS))
|
|
||||||
hwmon_dir = AMDGPU_CARDS[CARD]
|
|
||||||
|
|
||||||
|
|
||||||
class LogScreen(Screen):
|
class LogScreen(Screen):
|
||||||
|
@ -44,7 +47,7 @@ class LogScreen(Screen):
|
||||||
On first display in this case."""
|
On first display in this case."""
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header()
|
yield Header(show_clock=True)
|
||||||
yield Container(self.text_log)
|
yield Container(self.text_log)
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
@ -56,36 +59,57 @@ class LogScreen(Screen):
|
||||||
class GPUStatsWidget(Static):
|
class GPUStatsWidget(Static):
|
||||||
"""The main stats widget."""
|
"""The main stats widget."""
|
||||||
|
|
||||||
|
def __init__(self, *args, card=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Instance variables
|
||||||
|
self.card = card
|
||||||
|
self.hwmon_dir = AMDGPU_CARDS[self.card]
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Create child widgets."""
|
"""Create child widgets."""
|
||||||
yield ClockDisplay(classes="box")
|
yield ClockDisplay(classes="box", card=self.card, hwmon_dir=self.hwmon_dir)
|
||||||
yield PowerDisplay(classes="box")
|
yield PowerDisplay(classes="box", card=self.card, hwmon_dir=self.hwmon_dir)
|
||||||
yield MiscDisplay(classes="box")
|
yield MiscDisplay(classes="box", card=self.card)
|
||||||
|
_msg = f'''[bold]App:[/] creating stat widgets for [green]{self.card}[/], stats directory: {self.hwmon_dir}'''
|
||||||
|
self.update_log(_msg)
|
||||||
|
|
||||||
|
def update_log(self, message: str) -> None:
|
||||||
|
"""Update the TextLog widget with a new message."""
|
||||||
|
log_screen = AMDGPUStats.SCREENS["logs"]
|
||||||
|
log_screen.text_log.write(message)
|
||||||
|
|
||||||
|
|
||||||
class GPUStats(App):
|
class AMDGPUStats(App):
|
||||||
"""Textual-based tool to show AMDGPU statistics."""
|
"""Textual-based tool to show AMDGPU statistics."""
|
||||||
|
|
||||||
# apply stylesheet
|
# apply stylesheet
|
||||||
CSS_PATH = 'amdgpu_stats.css'
|
CSS_PATH = 'style.css'
|
||||||
|
|
||||||
# initialize log screen
|
# initialize log screen
|
||||||
SCREENS = {"logs": LogScreen()}
|
SCREENS = {"logs": LogScreen()}
|
||||||
|
|
||||||
|
# title the app after the card
|
||||||
|
# TITLE = 'GPUStats - ' + CARD
|
||||||
|
|
||||||
# setup keybinds
|
# setup keybinds
|
||||||
# Binding("l", "push_screen('logs')", "Toggle logs", priority=True),
|
# Binding("l", "push_screen('logs')", "Toggle logs", priority=True),
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("c", "toggle_dark", "Toggle colors", priority=True),
|
Binding("c", "custom_dark", "Colors"),
|
||||||
Binding("l", "toggle_log", "Toggle logs", priority=True),
|
Binding("l", "toggle_log", "Logs"),
|
||||||
Binding("q", "quit_app", "Quit", priority=True)
|
Binding("s", "screenshot_wrapper", "Screenshot"),
|
||||||
|
Binding("q", "quit", "Quit")
|
||||||
]
|
]
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Create child widgets for the app."""
|
"""Create child widgets for the app."""
|
||||||
yield Header()
|
yield Header(show_clock=True)
|
||||||
yield Container(GPUStatsWidget())
|
for card in AMDGPU_CARDS:
|
||||||
|
stat_widget_name = "stats_" + card
|
||||||
|
yield Label(card, expand=True, classes='widgetheader')
|
||||||
|
widget = Container(GPUStatsWidget(card=card, id=stat_widget_name))
|
||||||
|
yield widget
|
||||||
self.update_log("[bold green]App started, logging begin!")
|
self.update_log("[bold green]App started, logging begin!")
|
||||||
self.update_log(f"[bold italic]Information source:[/] {hwmon_dir}")
|
self.update_log(f"[bold]Discovered AMD GPUs:[/] {list(AMDGPU_CARDS)}")
|
||||||
# nice-to-have: account for not storing these in dicts, but resolved in funcs
|
# nice-to-have: account for not storing these in dicts, but resolved in funcs
|
||||||
# for metric, source in SRC_FILES.items():
|
# for metric, source in SRC_FILES.items():
|
||||||
# self.update_log(f'[bold] {metric}:[/] {source}')
|
# self.update_log(f'[bold] {metric}:[/] {source}')
|
||||||
|
@ -93,16 +117,23 @@ class GPUStats(App):
|
||||||
# self.update_log(f'[bold] {metric} temperature:[/] {source}')
|
# self.update_log(f'[bold] {metric} temperature:[/] {source}')
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def action_toggle_dark(self) -> None:
|
async def action_custom_dark(self) -> None:
|
||||||
"""An action to toggle dark mode."""
|
"""An action to toggle dark mode.
|
||||||
|
|
||||||
|
Wraps 'action_toggle_dark' with logging and a refresh"""
|
||||||
self.dark = not self.dark
|
self.dark = not self.dark
|
||||||
self.update_log(f"[bold]Dark side: [italic]{self.dark}")
|
self.update_log(f"[bold]Dark side: [italic]{self.dark}")
|
||||||
|
self.refresh()
|
||||||
|
# self.dark = not self.dark
|
||||||
|
|
||||||
def action_quit_app(self) -> None:
|
def action_screenshot_wrapper(self, screen_dir: str = '/tmp') -> None:
|
||||||
"""An action to quit the program"""
|
"""Action that fires when the user presses 's' for a screenshot"""
|
||||||
message = "Exiting on user request"
|
# construct the screenshot elements + path
|
||||||
self.update_log(f"[bold]{message}")
|
timestamp = datetime.now().isoformat().replace(":", "_")
|
||||||
self.exit(message)
|
screen_name = 'amdgpu_stats_' + timestamp + '.svg'
|
||||||
|
screen_path = path.join(screen_dir, screen_name)
|
||||||
|
self.action_screenshot(path=screen_dir, filename=screen_name)
|
||||||
|
self.update_log(f'[bold]Screenshot taken: [italic]{screen_path}')
|
||||||
|
|
||||||
def action_toggle_log(self) -> None:
|
def action_toggle_log(self) -> None:
|
||||||
"""Toggle between the main screen and the LogScreen."""
|
"""Toggle between the main screen and the LogScreen."""
|
||||||
|
@ -125,7 +156,6 @@ class MiscDisplay(Static):
|
||||||
fan_rpm_target = reactive(0)
|
fan_rpm_target = reactive(0)
|
||||||
# do some dancing to craft the UI; initialize the reactive obj with data
|
# do some dancing to craft the UI; initialize the reactive obj with data
|
||||||
# to get proper labels
|
# to get proper labels
|
||||||
initial_stats = get_temp_stats(CARD)
|
|
||||||
# dynamic object for temperature updates
|
# dynamic object for temperature updates
|
||||||
temp_stats = reactive({})
|
temp_stats = reactive({})
|
||||||
# default to 'not composed', once labels are made - become true
|
# default to 'not composed', once labels are made - become true
|
||||||
|
@ -133,10 +163,12 @@ class MiscDisplay(Static):
|
||||||
# ... and making labels for them
|
# ... and making labels for them
|
||||||
composed = False
|
composed = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, card: str, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.timer_misc = None
|
self.timer_misc = None
|
||||||
self.temp_stats = get_temp_stats(CARD)
|
self.card = card
|
||||||
|
self.initial_stats = get_temp_stats(self.card)
|
||||||
|
self.temp_stats = get_temp_stats(self.card)
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Horizontal(Label("[underline]Temperatures"),
|
yield Horizontal(Label("[underline]Temperatures"),
|
||||||
|
@ -144,14 +176,15 @@ class MiscDisplay(Static):
|
||||||
for temp_node in self.initial_stats:
|
for temp_node in self.initial_stats:
|
||||||
# capitalize the first letter for display
|
# capitalize the first letter for display
|
||||||
caption = temp_node[0].upper() + temp_node[1:]
|
caption = temp_node[0].upper() + temp_node[1:]
|
||||||
yield Horizontal(Label(f' {caption}:',),
|
yield Horizontal(Label(f' {caption}:',),
|
||||||
Label("", id="temp_" + temp_node,
|
Label("", id="temp_" + temp_node, classes="statvalue"))
|
||||||
classes="statvalue"))
|
# padding to split groups
|
||||||
|
yield Horizontal()
|
||||||
yield Horizontal(Label("[underline]Fan RPM"),
|
yield Horizontal(Label("[underline]Fan RPM"),
|
||||||
Label("", classes="statvalue"))
|
Label("", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Current:",),
|
yield Horizontal(Label(" Current:",),
|
||||||
Label("", id="fan_rpm", classes="statvalue"))
|
Label("", id="fan_rpm", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Target:",),
|
yield Horizontal(Label(" Target:",),
|
||||||
Label("", id="fan_rpm_target", classes="statvalue"))
|
Label("", id="fan_rpm_target", classes="statvalue"))
|
||||||
self.composed = True
|
self.composed = True
|
||||||
|
|
||||||
|
@ -163,9 +196,9 @@ class MiscDisplay(Static):
|
||||||
"""Method to update the temp/fan values to current measurements.
|
"""Method to update the temp/fan values to current measurements.
|
||||||
|
|
||||||
Run by a timer created 'on_mount'"""
|
Run by a timer created 'on_mount'"""
|
||||||
self.fan_rpm = get_fan_rpm(CARD)
|
self.fan_rpm = get_fan_rpm(self.card)
|
||||||
self.fan_rpm_target = get_fan_target(CARD)
|
self.fan_rpm_target = get_fan_target(self.card)
|
||||||
self.temp_stats = get_temp_stats(CARD)
|
self.temp_stats = get_temp_stats(self.card)
|
||||||
|
|
||||||
def watch_fan_rpm(self, fan_rpm: int) -> None:
|
def watch_fan_rpm(self, fan_rpm: int) -> None:
|
||||||
"""Called when the 'fan_rpm' reactive attr changes.
|
"""Called when the 'fan_rpm' reactive attr changes.
|
||||||
|
@ -195,25 +228,28 @@ class ClockDisplay(Static):
|
||||||
"""A widget to display GPU power stats."""
|
"""A widget to display GPU power stats."""
|
||||||
core_vals = reactive({"sclk": 0, "mclk": 0, "voltage": 0, "util_pct": 0})
|
core_vals = reactive({"sclk": 0, "mclk": 0, "voltage": 0, "util_pct": 0})
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, card: str, hwmon_dir: str, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.timer_clocks = None
|
self.timer_clocks = None
|
||||||
|
self.card = card
|
||||||
|
self.hwmon_dir = hwmon_dir
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Horizontal(Label("[underline]Clocks"),
|
yield Horizontal(Label("[underline]Performance"),
|
||||||
Label("", classes="statvalue"))
|
Label("", classes="statvalue"))
|
||||||
yield Horizontal(Label(" GPU core:",),
|
yield Horizontal(Label(" Core clock:",),
|
||||||
Label("", id="clk_core_val", classes="statvalue"))
|
Label("", id="clk_core_val", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Memory:"),
|
yield Horizontal(Label(" Memory clock:"),
|
||||||
Label("", id="clk_memory_val", classes="statvalue"))
|
Label("", id="clk_memory_val", classes="statvalue"))
|
||||||
# padding to split groups
|
yield Horizontal(Label(" Utilization:",),
|
||||||
yield Horizontal(Label(""), Label("", classes="statvalue"))
|
|
||||||
yield Horizontal(Label("[underline]Core"),
|
|
||||||
Label("", classes="statvalue"))
|
|
||||||
yield Horizontal(Label(" Utilization:",),
|
|
||||||
Label("", id="util_pct", classes="statvalue"))
|
Label("", id="util_pct", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Voltage:",),
|
yield Horizontal(Label(" Voltage:",),
|
||||||
Label("", id="clk_voltage_val", classes="statvalue"))
|
Label("", id="clk_voltage_val", classes="statvalue"))
|
||||||
|
# padding underneath, don't let them space out vertically
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Event handler called when widget is added to the app."""
|
"""Event handler called when widget is added to the app."""
|
||||||
|
@ -222,7 +258,7 @@ class ClockDisplay(Static):
|
||||||
def update_core_vals(self) -> None:
|
def update_core_vals(self) -> None:
|
||||||
"""Method to update GPU clock values to the current measurements.
|
"""Method to update GPU clock values to the current measurements.
|
||||||
Run by a timer created 'on_mount'"""
|
Run by a timer created 'on_mount'"""
|
||||||
self.core_vals = get_core_stats(CARD)
|
self.core_vals = get_core_stats(self.card)
|
||||||
|
|
||||||
def watch_core_vals(self, core_vals: dict) -> None:
|
def watch_core_vals(self, core_vals: dict) -> None:
|
||||||
"""Called when the clocks attribute changes
|
"""Called when the clocks attribute changes
|
||||||
|
@ -246,25 +282,27 @@ class PowerDisplay(Static):
|
||||||
"capability": 0,
|
"capability": 0,
|
||||||
"default": 0})
|
"default": 0})
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, card: str, hwmon_dir: str, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.timer_watts = None
|
self.timer_watts = None
|
||||||
|
self.card = card
|
||||||
|
self.hwmon_dir = hwmon_dir
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Horizontal(Label("[underline]Power"),
|
yield Horizontal(Label("[underline]Power"),
|
||||||
Label("", classes="statvalue"))
|
Label("", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Usage:",),
|
yield Horizontal(Label(" Usage:",),
|
||||||
Label("", id="pwr_avg_val", classes="statvalue"))
|
Label("", id="pwr_avg_val", classes="statvalue"))
|
||||||
# padding to split groups
|
yield Horizontal(Label(" Set Limit:",),
|
||||||
yield Horizontal(Label(""), Label("", classes="statvalue"))
|
|
||||||
yield Horizontal(Label("[underline]Limits"),
|
|
||||||
Label("", classes="statvalue"))
|
|
||||||
yield Horizontal(Label(" Configured:",),
|
|
||||||
Label("", id="pwr_lim_val", classes="statvalue"))
|
Label("", id="pwr_lim_val", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Default:",),
|
yield Horizontal(Label(" Default Limit:",),
|
||||||
Label("", id="pwr_def_val", classes="statvalue"))
|
Label("", id="pwr_def_val", classes="statvalue"))
|
||||||
yield Horizontal(Label(" Board capability:",),
|
yield Horizontal(Label(" Capability:",),
|
||||||
Label("", id="pwr_cap_val", classes="statvalue"))
|
Label("", id="pwr_cap_val", classes="statvalue"))
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
yield Horizontal()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Event handler called when widget is added to the app."""
|
"""Event handler called when widget is added to the app."""
|
||||||
|
@ -274,7 +312,7 @@ class PowerDisplay(Static):
|
||||||
"""Method to update GPU power values to current measurements.
|
"""Method to update GPU power values to current measurements.
|
||||||
|
|
||||||
Run by a timer created 'on_mount'"""
|
Run by a timer created 'on_mount'"""
|
||||||
self.watts = get_power_stats(CARD)
|
self.watts = get_power_stats(self.card)
|
||||||
|
|
||||||
def watch_watts(self, watts: dict) -> None:
|
def watch_watts(self, watts: dict) -> None:
|
||||||
"""Called when the 'watts' reactive attribute (var) changes.
|
"""Called when the 'watts' reactive attribute (var) changes.
|
||||||
|
@ -290,10 +328,10 @@ class PowerDisplay(Static):
|
||||||
Static).update(f"{watts['capability']}W")
|
Static).update(f"{watts['capability']}W")
|
||||||
|
|
||||||
|
|
||||||
def tui() -> None:
|
def start() -> None:
|
||||||
'''Spawns the textual UI only during CLI invocation / after argparse'''
|
'''Spawns the textual UI only during CLI invocation / after argparse'''
|
||||||
if len(AMDGPU_CARDS) > 0:
|
if len(AMDGPU_CARDS) > 0:
|
||||||
app = GPUStats()
|
app = AMDGPUStats(watch_css=True)
|
||||||
app.run()
|
app.run()
|
||||||
else:
|
else:
|
||||||
sys.exit('Could not find an AMD GPU, exiting.')
|
sys.exit('Could not find an AMD GPU, exiting.')
|
Reference in a new issue