display live RAM usage

This commit is contained in:
rambros 2026-03-12 21:38:50 +05:30
parent 975369c784
commit 6a42cdf008
5 changed files with 67 additions and 2 deletions

View file

@ -7,6 +7,7 @@ from textual.app import ComposeResult
from textual.screen import Screen from textual.screen import Screen
from textual.containers import Container, Vertical, VerticalScroll, Horizontal from textual.containers import Container, Vertical, VerticalScroll, Horizontal
from textual.widgets import Button, Label, Rule, Tree from textual.widgets import Button, Label, Rule, Tree
from src.ui.widgets import RamDisplay
from textual import work from textual import work
from rich.text import Text from rich.text import Text
from rich.style import Style from rich.style import Style
@ -211,6 +212,7 @@ class BackupStatsScreen(Screen[None]):
with Horizontal(id="bs_actions"): with Horizontal(id="bs_actions"):
yield Button("Back", id="btn_back", variant="default") yield Button("Back", id="btn_back", variant="default")
yield RamDisplay()
def on_mount(self) -> None: def on_mount(self) -> None:
self.stats_tree = self.query_one("#stats_tree", Tree) self.stats_tree = self.query_one("#stats_tree", Tree)

View file

@ -9,13 +9,14 @@ from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical, VerticalScroll from textual.containers import Container, Horizontal, Vertical, VerticalScroll
from textual.widgets import ( from textual.widgets import (
Header, Footer, Button, Label, Input, ListItem, Header, Footer, Button, Label, Input, ListItem,
ListView, Rule, RadioButton, RadioSet, Select, ListView, Rule, RadioButton, RadioSet, Select, Static,
) )
from textual.screen import Screen, ModalScreen from textual.screen import Screen, ModalScreen
from src.core.configuration import ( from src.core.configuration import (
get_available_configs, create_new_config, load_config, save_config, get_available_configs, create_new_config, load_config, save_config,
) )
from src.ui.widgets import RamDisplay
# ────────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────────
@ -100,6 +101,7 @@ class ConfigSelectionScreen(Screen):
yield Button("New Config", id="btn_new_config", variant="success", tooltip="Create a new configuration folder") yield Button("New Config", id="btn_new_config", variant="success", tooltip="Create a new configuration folder")
yield Button("Exit", id="btn_exit", variant="error") yield Button("Exit", id="btn_exit", variant="error")
yield Footer() yield Footer()
yield RamDisplay()
def on_mount(self) -> None: def on_mount(self) -> None:
self.refresh_configs() self.refresh_configs()
@ -298,6 +300,7 @@ class ConfigScreen(Screen):
yield Button("Save Configuration", variant="success", id="btn_save", tooltip="Save all changes to config.yaml") yield Button("Save Configuration", variant="success", id="btn_save", tooltip="Save all changes to config.yaml")
yield Button("Back", id="btn_back") yield Button("Back", id="btn_back")
yield Footer() yield Footer()
yield RamDisplay()
def on_mount(self) -> None: def on_mount(self) -> None:
self._toggle_target_section() self._toggle_target_section()
@ -488,6 +491,16 @@ class ReaperApp(App):
"config_selection": ConfigSelectionScreen, "config_selection": ConfigSelectionScreen,
} }
DEFAULT_CSS = """
RamDisplay {
dock: bottom;
width: 30;
height: 1;
margin-left: 2;
color: green;
}
"""
def on_mount(self) -> None: def on_mount(self) -> None:
self.push_screen("config_selection") self.push_screen("config_selection")
self.theme = "dracula" self.theme = "dracula"

View file

@ -7,6 +7,7 @@ from textual.containers import Horizontal, Vertical, VerticalScroll, Container,
from textual.widgets import Button, Label, Input, ProgressBar, RichLog, Rule, RadioButton, LoadingIndicator, Header, Footer, RadioSet, OptionList from textual.widgets import Button, Label, Input, ProgressBar, RichLog, Rule, RadioButton, LoadingIndicator, Header, Footer, RadioSet, OptionList
from textual.widgets.option_list import Option from textual.widgets.option_list import Option
from textual.screen import ModalScreen, Screen from textual.screen import ModalScreen, Screen
from src.ui.widgets import RamDisplay
import time import time
@ -113,6 +114,7 @@ class ProgressScreen(Screen[None]):
with Horizontal(classes="action_row", id="prog_actions_cancel"): with Horizontal(classes="action_row", id="prog_actions_cancel"):
yield Button("Back", id="btn_cancel", variant="default", tooltip="Stop current operation") yield Button("Back", id="btn_cancel", variant="default", tooltip="Stop current operation")
yield Footer() yield Footer()
yield RamDisplay()
def __init__(self, log_level: str = "INFO", *args, **kwargs): def __init__(self, log_level: str = "INFO", *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -419,6 +421,7 @@ class SubMenuModal(ModalScreen[str]):
yield Button(label, id=btn_id, variant=variant) yield Button(label, id=btn_id, variant=variant)
yield Rule(id="footer_rule") yield Rule(id="footer_rule")
yield Button("Cancel", id="btn_cancel_sub") yield Button("Cancel", id="btn_cancel_sub")
yield RamDisplay()
def on_button_pressed(self, event: Button.Pressed): def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "btn_cancel_sub": if event.button.id == "btn_cancel_sub":
@ -475,6 +478,7 @@ class OptionSelectModal(ModalScreen[list[str]]):
with Horizontal(id="opt_buttons"): with Horizontal(id="opt_buttons"):
yield Button("Proceed", variant="success", id="btn_opt_ok", tooltip="Proceed with the selected options") yield Button("Proceed", variant="success", id="btn_opt_ok", tooltip="Proceed with the selected options")
yield Button("Back", id="btn_opt_back", tooltip="Return to the previous menu") yield Button("Back", id="btn_opt_back", tooltip="Return to the previous menu")
yield RamDisplay()
def on_button_pressed(self, event: Button.Pressed): def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "btn_opt_back": if event.button.id == "btn_opt_back":
@ -618,6 +622,7 @@ class ChannelPickerScreen(Screen[tuple]):
yield Button("Select", variant="success", id="btn_pick_ok", tooltip="Confirm selection and start migration") yield Button("Select", variant="success", id="btn_pick_ok", tooltip="Confirm selection and start migration")
yield Button("Back", id="btn_pick_back", tooltip="Cancel selection") yield Button("Back", id="btn_pick_back", tooltip="Cancel selection")
yield Footer() yield Footer()
yield RamDisplay()
def on_mount(self) -> None: def on_mount(self) -> None:
"""Set initial highlights after mounting.""" """Set initial highlights after mounting."""
@ -805,6 +810,7 @@ class ChannelSelectScreen(Screen[dict]):
yield Button("Backup", variant="success", id="btn_backup", tooltip="Start backing up selected channels") yield Button("Backup", variant="success", id="btn_backup", tooltip="Start backing up selected channels")
yield Button("Back", id="btn_cancel_chan", tooltip="Cancel and go back") yield Button("Back", id="btn_cancel_chan", tooltip="Cancel and go back")
yield Footer() yield Footer()
yield RamDisplay()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn_all": if event.button.id == "btn_all":
@ -874,6 +880,7 @@ class MessageIDInputModal(ModalScreen[int | None]):
with Horizontal(id="msg_id_buttons"): with Horizontal(id="msg_id_buttons"):
yield Button("Verify", variant="primary", id="btn_verify_start", disabled=True, tooltip="Check if this message ID exists in the channel") yield Button("Verify", variant="primary", id="btn_verify_start", disabled=True, tooltip="Check if this message ID exists in the channel")
yield Button("Back", variant="warning", id="btn_cancel_msg_id", tooltip="Cancel and go back") yield Button("Back", variant="warning", id="btn_cancel_msg_id", tooltip="Cancel and go back")
yield RamDisplay()
def on_input_changed(self, event: Input.Changed) -> None: def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "input_msg_id": if event.input.id == "input_msg_id":
@ -970,6 +977,7 @@ class ChannelNameInputModal(ModalScreen[str | None]):
with Horizontal(id="chan_name_buttons"): with Horizontal(id="chan_name_buttons"):
yield Button("Create", variant="success", id="btn_create_chan") yield Button("Create", variant="success", id="btn_create_chan")
yield Button("Back", id="btn_cancel_chan_name") yield Button("Back", id="btn_cancel_chan_name")
yield RamDisplay()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn_cancel_chan_name": if event.button.id == "btn_cancel_chan_name":
@ -1028,6 +1036,7 @@ class ChannelIDInputModal(ModalScreen[dict | None]):
with Horizontal(id="chan_id_buttons"): with Horizontal(id="chan_id_buttons"):
yield Button("Verify", variant="primary", id="btn_verify_chan", disabled=True) yield Button("Verify", variant="primary", id="btn_verify_chan", disabled=True)
yield Button("Back", variant="warning", id="btn_cancel_chan_id") yield Button("Back", variant="warning", id="btn_cancel_chan_id")
yield RamDisplay()
def on_input_changed(self, event: Input.Changed) -> None: def on_input_changed(self, event: Input.Changed) -> None:
if event.input.id == "input_chan_id": if event.input.id == "input_chan_id":
@ -1076,3 +1085,4 @@ class ChannelIDInputModal(ModalScreen[dict | None]):
btn.variant = "success" btn.variant = "success"
else: else:
preview.update(f"[bold red]No channel found with ID: {chan_id_str}[/bold red]\n[dim]Make sure the ID belongs to a channel in the target community.[/dim]") preview.update(f"[bold red]No channel found with ID: {chan_id_str}[/bold red]\n[dim]Make sure the ID belongs to a channel in the target community.[/dim]")
yield RamDisplay()

View file

@ -15,6 +15,7 @@ from textual.screen import Screen
from src.core.configuration import load_config from src.core.configuration import load_config
from src.ui.shuttle_ops import OperationPane from src.ui.shuttle_ops import OperationPane
from src.ui.widgets import RamDisplay, Footnote
class ModeScreen(Screen): class ModeScreen(Screen):
@ -138,6 +139,8 @@ class ModeScreen(Screen):
yield Button("Exit", id="btn_exit", variant="error") yield Button("Exit", id="btn_exit", variant="error")
yield Footer() yield Footer()
yield Footnote()
yield RamDisplay()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
bid = event.button.id bid = event.button.id

37
src/ui/widgets.py Normal file
View file

@ -0,0 +1,37 @@
from textual.widgets import Static
import logging
logger = logging.getLogger(__name__)
class RamDisplay(Static):
"""Widget to display current RAM usage."""
def on_mount(self) -> None:
self.update_ram()
self.set_interval(1.0, self.update_ram)
def update_ram(self) -> None:
"""Fetch and display RAM usage (RSS)."""
try:
# RSS is in KB in /proc/self/status on Linux
with open("/proc/self/status", "r") as f:
for line in f:
if line.startswith("VmRSS:"):
rss_kb = int(line.split()[1])
break
else:
rss_kb = 0
if rss_kb > 1024 * 1024:
usage = f"{rss_kb / (1024 * 1024):.2f} GB"
else:
usage = f"{rss_kb / 1024:.2f} MB"
self.update(f" RAM: [yellow]{usage}[/yellow]")
except Exception:
self.update(" RAM: [bold red]N/A[/bold red]")
class Footnote(Static):
"""Widget to display branding text at the bottom right."""
def on_mount(self) -> None:
self.update("made by [bold]RamBros[/bold]")