Archived
1
1
Fork 0

abandon screens, move to tabbed content

This commit is contained in:
Josh Lay 2023-05-02 19:18:21 -05:00
parent 22bbabc034
commit 5cd18b00e8
Signed by: jlay
GPG key ID: B265E45CACAD108A
2 changed files with 125 additions and 95 deletions

View file

@ -1,17 +1,42 @@
Header {
background: $panel;
}
/* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */
GPUStatsWidget {
box-sizing: content-box;
/* box-sizing: content-box;*/
background: $panel;
height: 100%;
width: 100%;
min-width: 50;
/* min-width: 50;*/
}
.logs {
height: 1fr;
}
/* for colors, see: https://textual.textualize.io/guide/design/#theme-reference */
DataTable {
.stat_table {
width: 100%;
}
Container {
height: 1fr;
}
TabbedContent {
/* box-sizing: content-box;*/
height: 1fr;
}
Notification {
dock: bottom;
layer: notification;
width: auto;
margin: 2 4;
padding: 1 2;
background: $background;
color: $text;
height: auto;
}
Header {
dock: top;
content-align: center middle;
background: $panel;
}

View file

@ -7,11 +7,7 @@ Can be used as a way to monitor GPU(s) in your terminal, or inform other utiliti
Classes:
- GPUStats: the object for the _Application_, instantiated at runtime
- GPUStatsWidget: the primary container for the three stat widgets:
- MiscDisplay
- ClockDisplay
- PowerDisplay
- LogScreen: Second screen with the logging widget, header, and footer
- GPUStatsWidget: the primary container for the tabbed content; stats table / logs
Functions:
- start: Creates the 'App' and renders the TUI using the classes above
@ -20,60 +16,55 @@ Functions:
# pylint: disable=line-too-long
import sys
from datetime import datetime
from os import path
from rich.text import Text
from textual.binding import Binding
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, TextLog, DataTable
from .utils import AMDGPU_CARDS, get_fan_rpm, get_power_stats, get_temp_stat, get_clock, get_gpu_usage, get_voltage
# pylint: disable=line-too-long
from textual.widgets import (
Header, Footer, Static, TextLog, DataTable, TabbedContent
)
from .utils import (
AMDGPU_CARDS,
get_fan_rpm,
get_power_stats,
get_temp_stat,
get_clock,
get_gpu_usage,
get_voltage
)
# rich markup reference:
# https://rich.readthedocs.io/en/stable/markup.html
class LogScreen(Screen):
"""Creates a screen for the logging widget"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.text_log = TextLog(highlight=True, markup=True, name='log_gpu')
class Notification(Static):
def on_mount(self) -> None:
"""Event handler called when widget is first added
On first display in this case."""
self.set_timer(3, self.remove)
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield self.text_log
yield Footer()
# def on_key(self, event: events.Key) -> None:
# """Log/show key presses when the log window is open"""
# self.text_log.write(event)
def on_click(self) -> None:
self.remove()
class GPUStatsWidget(Static):
"""The main stats widget."""
columns = ["card",
"core clock",
"memory clock",
"utilization",
"voltage",
"power usage",
"set limit",
"default limit",
"capability",
"fan rpm",
"edge temp",
"junction temp",
"memory temp"]
columns = ["Card",
"Core clock",
"Memory clock",
"Utilization",
"Voltage",
"Power",
"[italic]Limit",
"[italic]Default",
"[italic]Capability",
"Fan RPM",
"Edge temp",
"Junction temp",
"Memory temp"]
timer_stats = None
text_log = None
stats_table = None
table = None
table_needs_init = True
data = {}
@ -82,60 +73,73 @@ class GPUStatsWidget(Static):
super().__init__(*args, **kwargs)
# Instance variables
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, name='stats_table', classes='stat_table')
async def on_mount(self) -> None:
'''Fires when stats widget first shown'''
self.table = self.query_one(DataTable)
# construct the table columns
for column in self.columns:
self.table.add_column(label=column, key=column)
# self.table.add_columns(*self.columns)
# mark the table as needing initialization (with rows)
self.table_needs_init = True
# do a one-off stat collection, populate table before the interval
if self.table_needs_init:
self.get_stats()
# stand up the stat-collecting interval, once per second
self.timer_stats = self.set_interval(1, self.get_stats)
def compose(self) -> ComposeResult:
"""Create child widgets."""
stats_table = DataTable(zebra_stripes=True, show_cursor=False, name='stats_table')
yield stats_table
# Add the TabbedContent widget
with TabbedContent("Stats", "Logs"):
yield self.stats_table
yield self.text_log
self.update_log("[bold green]App started, logging begin!")
self.update_log(f"[bold]Discovered AMD GPUs: [/]{list(AMDGPU_CARDS)}")
self.update_log('[bold]App: [/]created stats table')
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)
self.text_log.write(message)
def get_stats(self):
'''Function to fetch stats / update the table'''
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'
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 usage": f'{power_stats["average"]}W',
"set limit": f'{power_stats["limit"]}W',
"default limit": f'{power_stats["default"]}W',
"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"}
"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"}
# handle the table data appopriately
# if needs populated anew or updated
if self.table_needs_init:
# Add rows for the first time
# Adding styled and justified `Text` objects instead of plain strings.
# Adding right-justified `Text` objects instead of plain strings
styled_row = [
Text(str(cell), style="italic", justify="right") for cell in self.data.values()
Text(str(cell), style="normal", justify="right") for cell in self.data.values()
]
self.table.add_row(*styled_row, key=card)
hwmon_dir = AMDGPU_CARDS[card]
self.update_log(f'[bold]stats:[/] added row for [bold green]{card}[/], info dir: {hwmon_dir}')
self.update_log(f"[bold]Table: [/]added row for '{card}', info dir: '{hwmon_dir}'")
else:
# Update existing rows
# Update existing rows, retaining styling/justification
for column, value in self.data.items():
styled_cell = Text(str(value), style="italic", justify="right")
styled_cell = Text(str(value), style="normal", justify="right")
self.table.update_cell(card, column, styled_cell)
if self.table_needs_init:
# if this is the first time updating the table, mark it initialized
@ -149,9 +153,6 @@ class AMDGPUStats(App):
# apply stylesheet
CSS_PATH = 'style.css'
# initialize log screen
SCREENS = {"logs": LogScreen()}
# title the app after the card
# TITLE = 'GPUStats - ' + CARD
@ -163,51 +164,55 @@ class AMDGPUStats(App):
Binding("s", "custom_screenshot", "Screenshot"),
Binding("q", "quit", "Quit")
]
stats_widget = GPUStatsWidget(cards=AMDGPU_CARDS,
name="stats_widget")
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Header(show_clock=True)
stats_widget = Container(GPUStatsWidget(cards=AMDGPU_CARDS,
name="stats_widget"))
yield stats_widget
self.update_log("[bold green]App started, logging begin!")
self.update_log(f"[bold]Discovered AMD GPUs:[/] {list(AMDGPU_CARDS)}")
yield Footer()
yield Container(self.stats_widget)
# nice-to-have: account for not storing these in dicts, but resolved in funcs
# for metric, source in SRC_FILES.items():
# self.update_log(f'[bold] {metric}:[/] {source}')
# for metric, source in TEMP_FILES.items():
# self.update_log(f'[bold] {metric} temperature:[/] {source}')
yield Footer()
async def action_custom_dark(self) -> None:
def action_custom_dark(self) -> None:
"""An action to toggle dark mode.
Wraps 'action_toggle_dark' with logging and a refresh"""
self.dark = not self.dark
self.update_log(f"[bold]Dark side: [italic]{self.dark}")
self.refresh()
self.app.dark = not self.app.dark
self.update_log(f"[bold]Dark side: [italic]{self.app.dark}")
# self.refresh()
# self.dark = not self.dark
def action_custom_screenshot(self, screen_dir: str = '/tmp') -> None:
"""Action that fires when the user presses 's' for a screenshot"""
# construct the screenshot elements + path
# construct the screenshot elements: name (w/ ISO timestamp) + path
timestamp = datetime.now().isoformat().replace(":", "_")
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}')
# take the screenshot, recording the path for logging/notification
outpath = self.save_screenshot(path=screen_dir, filename=screen_name)
# construct the log/notification message, then show it
message = Text.assemble("Screenshot saved to ", (f"'{outpath}'", "bold"))
self.screen.mount(Notification(message))
self.update_log(message)
def action_custom_log(self) -> None:
"""Toggle between the main screen and the LogScreen."""
if isinstance(self.screen, LogScreen):
self.pop_screen()
active = self.query_one(TabbedContent).active
# if the second tab (logs), go to first
if active == "tab-2":
self.query_one(TabbedContent).active = 'tab-1'
else:
self.push_screen("logs")
# otherwise, go to logs
self.query_one(TabbedContent).active = 'tab-2'
def update_log(self, message: str) -> None:
"""Update the TextLog widget with a new message."""
log_screen = self.SCREENS["logs"]
log_screen.text_log.write(message)
log = self.stats_widget.text_log
log.write(message)
def start() -> None: