dsmview/dsmview/ui/widgets/braille_graph.py
Jan Vetesnik af63fc7b15 feat: initial dsmview - TUI SSH monitor for Synology DSM 7.x
- Python 3.12+ / Textual TUI with btop-style theme and 4 tabs
  (Dashboard, Logs, Services, Disks)
- Paramiko SSH layer with ssh_config support, key fallback,
  keepalive and reconnect; async executor over a thread pool
- Read-only collectors for system, storage, disks, network,
  logs and services; confirm dialog gates service restart/stop/start
- Click CLI: dsmview ssh [-p PORT] [-i KEY] TARGET
- Homebrew formula using virtualenv_install_with_resources
- GitHub Actions release workflow: builds source tarball,
  regenerates Python resource blocks via homebrew-pypi-poet,
  pushes updated formula to janvete/homebrew-dsmview tap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-13 16:12:58 +02:00

56 lines
1.5 KiB
Python

from __future__ import annotations
from collections import deque
from typing import Deque
import plotext as plt
from rich.text import Text
from textual.widget import Widget
class BrailleGraph(Widget):
"""A compact braille-dot graph rendered with plotext.
Stores a rolling history and redraws on `push()`. Sized by Textual,
re-rendered to fit the current widget size.
"""
DEFAULT_CSS = """
BrailleGraph {
height: 100%;
width: 100%;
}
"""
def __init__(self, *, max_points: int = 120, color: str = "red", y_max: float | None = None) -> None:
super().__init__()
self.max_points: int = max_points
self.color: str = color
self.y_max: float | None = y_max
self._history: Deque[float] = deque(maxlen=max_points)
def push(self, value: float) -> None:
self._history.append(float(value))
self.refresh()
def reset(self) -> None:
self._history.clear()
self.refresh()
def render(self) -> Text:
if not self._history:
return Text("")
w = max(10, self.size.width - 2)
h = max(3, self.size.height - 1)
plt.clear_figure()
plt.plotsize(w, h)
plt.theme("clear")
data = list(self._history)
plt.plot(data, marker="braille", color=self.color)
plt.xaxes(False, False)
plt.yaxes(False, False)
plt.frame(False)
if self.y_max is not None:
plt.ylim(0, self.y_max)
rendered = plt.build()
return Text.from_ansi(rendered)