From 6a42cdf008ec5a930003ca01dc3469723e786059 Mon Sep 17 00:00:00 2001 From: rambros Date: Thu, 12 Mar 2026 21:38:50 +0530 Subject: [PATCH] display live RAM usage --- src/ui/backup_stats.py | 2 ++ src/ui/main_app.py | 15 ++++++++++++++- src/ui/modals.py | 12 +++++++++++- src/ui/mode_screen.py | 3 +++ src/ui/widgets.py | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/ui/widgets.py diff --git a/src/ui/backup_stats.py b/src/ui/backup_stats.py index 71fe48e..69624b4 100644 --- a/src/ui/backup_stats.py +++ b/src/ui/backup_stats.py @@ -7,6 +7,7 @@ from textual.app import ComposeResult from textual.screen import Screen from textual.containers import Container, Vertical, VerticalScroll, Horizontal from textual.widgets import Button, Label, Rule, Tree +from src.ui.widgets import RamDisplay from textual import work from rich.text import Text from rich.style import Style @@ -211,6 +212,7 @@ class BackupStatsScreen(Screen[None]): with Horizontal(id="bs_actions"): yield Button("Back", id="btn_back", variant="default") + yield RamDisplay() def on_mount(self) -> None: self.stats_tree = self.query_one("#stats_tree", Tree) diff --git a/src/ui/main_app.py b/src/ui/main_app.py index 9cd8513..369766e 100644 --- a/src/ui/main_app.py +++ b/src/ui/main_app.py @@ -9,13 +9,14 @@ from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, Vertical, VerticalScroll from textual.widgets import ( Header, Footer, Button, Label, Input, ListItem, - ListView, Rule, RadioButton, RadioSet, Select, + ListView, Rule, RadioButton, RadioSet, Select, Static, ) from textual.screen import Screen, ModalScreen from src.core.configuration import ( 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("Exit", id="btn_exit", variant="error") yield Footer() + yield RamDisplay() def on_mount(self) -> None: 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("Back", id="btn_back") yield Footer() + yield RamDisplay() def on_mount(self) -> None: self._toggle_target_section() @@ -488,6 +491,16 @@ class ReaperApp(App): "config_selection": ConfigSelectionScreen, } + DEFAULT_CSS = """ + RamDisplay { + dock: bottom; + width: 30; + height: 1; + margin-left: 2; + color: green; + } + """ + def on_mount(self) -> None: self.push_screen("config_selection") self.theme = "dracula" diff --git a/src/ui/modals.py b/src/ui/modals.py index 44ba413..8455ea3 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -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.option_list import Option from textual.screen import ModalScreen, Screen +from src.ui.widgets import RamDisplay import time @@ -113,6 +114,7 @@ class ProgressScreen(Screen[None]): with Horizontal(classes="action_row", id="prog_actions_cancel"): yield Button("Back", id="btn_cancel", variant="default", tooltip="Stop current operation") yield Footer() + yield RamDisplay() def __init__(self, log_level: str = "INFO", *args, **kwargs): super().__init__(*args, **kwargs) @@ -419,6 +421,7 @@ class SubMenuModal(ModalScreen[str]): yield Button(label, id=btn_id, variant=variant) yield Rule(id="footer_rule") yield Button("Cancel", id="btn_cancel_sub") + yield RamDisplay() def on_button_pressed(self, event: Button.Pressed): if event.button.id == "btn_cancel_sub": @@ -475,6 +478,7 @@ class OptionSelectModal(ModalScreen[list[str]]): with Horizontal(id="opt_buttons"): 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 RamDisplay() def on_button_pressed(self, event: Button.Pressed): 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("Back", id="btn_pick_back", tooltip="Cancel selection") yield Footer() + yield RamDisplay() def on_mount(self) -> None: """Set initial highlights after mounting.""" @@ -805,7 +810,8 @@ class ChannelSelectScreen(Screen[dict]): 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 Footer() - + yield RamDisplay() + def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "btn_all": for cb in self.query(RadioButton): @@ -874,6 +880,7 @@ class MessageIDInputModal(ModalScreen[int | None]): 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("Back", variant="warning", id="btn_cancel_msg_id", tooltip="Cancel and go back") + yield RamDisplay() def on_input_changed(self, event: Input.Changed) -> None: if event.input.id == "input_msg_id": @@ -970,6 +977,7 @@ class ChannelNameInputModal(ModalScreen[str | None]): with Horizontal(id="chan_name_buttons"): yield Button("Create", variant="success", id="btn_create_chan") yield Button("Back", id="btn_cancel_chan_name") + yield RamDisplay() def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "btn_cancel_chan_name": @@ -1028,6 +1036,7 @@ class ChannelIDInputModal(ModalScreen[dict | None]): with Horizontal(id="chan_id_buttons"): yield Button("Verify", variant="primary", id="btn_verify_chan", disabled=True) yield Button("Back", variant="warning", id="btn_cancel_chan_id") + yield RamDisplay() def on_input_changed(self, event: Input.Changed) -> None: if event.input.id == "input_chan_id": @@ -1076,3 +1085,4 @@ class ChannelIDInputModal(ModalScreen[dict | None]): btn.variant = "success" 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]") + yield RamDisplay() diff --git a/src/ui/mode_screen.py b/src/ui/mode_screen.py index 26c8852..589115b 100644 --- a/src/ui/mode_screen.py +++ b/src/ui/mode_screen.py @@ -15,6 +15,7 @@ from textual.screen import Screen from src.core.configuration import load_config from src.ui.shuttle_ops import OperationPane +from src.ui.widgets import RamDisplay, Footnote class ModeScreen(Screen): @@ -138,6 +139,8 @@ class ModeScreen(Screen): yield Button("Exit", id="btn_exit", variant="error") yield Footer() + yield Footnote() + yield RamDisplay() def on_button_pressed(self, event: Button.Pressed) -> None: bid = event.button.id diff --git a/src/ui/widgets.py b/src/ui/widgets.py new file mode 100644 index 0000000..cec7bfd --- /dev/null +++ b/src/ui/widgets.py @@ -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]")