Compare commits

..

No commits in common. "main" and "v0.3.0" have entirely different histories.
main ... v0.3.0

6 changed files with 11 additions and 109 deletions

View file

@ -3,8 +3,8 @@ class Dsmview < Formula
desc "TUI SSH monitor for Synology DSM 7.x" desc "TUI SSH monitor for Synology DSM 7.x"
homepage "https://github.com/janvete/dsmview" homepage "https://github.com/janvete/dsmview"
url "https://github.com/janvete/dsmview/archive/refs/tags/v0.3.1.tar.gz" url "https://github.com/janvete/dsmview/archive/refs/tags/v0.2.2.tar.gz"
sha256 "6f0fcda846ee254604b87e06f3eaf20f58cdb148573cc30cb0fc7f31e922d665" sha256 "3cd58fdd47fcec0b1aac967435e43b8bf7156b9ff7184152463a87f06dc72439"
license "MIT" license "MIT"
head "https://github.com/janvete/dsmview.git", branch: "main" head "https://github.com/janvete/dsmview.git", branch: "main"

View file

@ -2,31 +2,11 @@
TUI SSH monitor for Synology DSM 7.x — read-only dashboard inspired by `btop`. TUI SSH monitor for Synology DSM 7.x — read-only dashboard inspired by `btop`.
## Installation ```
brew tap janvete/tools
### macOS (Homebrew)
```bash
brew tap janvete/tools https://git.janvete.com/seth/homebrew-tools.git
brew install dsmview brew install dsmview
``` ```
### Debian / Ubuntu (APT)
```bash
echo "deb [trusted=yes] https://git.janvete.com/seth/apt/raw/branch/main ./" | sudo tee /etc/apt/sources.list.d/janvete.list
sudo apt update
sudo apt install dsmview
```
### From source
```bash
git clone ssh://git@git-ssh.janvete.com:554/seth/dsmview.git
cd dsmview
pip install .
```
## Usage ## Usage
``` ```
@ -70,7 +50,6 @@ Services tab:
s start s start
S stop S stop
R restart R restart
(or use the Start / Stop / Restart buttons)
``` ```
## Requirements ## Requirements

View file

@ -1 +1 @@
__version__ = "0.3.1" __version__ = "0.3.0"

View file

@ -98,8 +98,6 @@ class DsmviewApp(App):
def action_switch_tab(self, tab_id: str) -> None: def action_switch_tab(self, tab_id: str) -> None:
self.query_one(TabbedContent).active = tab_id self.query_one(TabbedContent).active = tab_id
if tab_id == "services":
self.services_tab.focus_table()
def action_filter(self, name: str) -> None: def action_filter(self, name: str) -> None:
self.query_one(TabbedContent).active = "logs" self.query_one(TabbedContent).active = "logs"

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Container, Horizontal, VerticalScroll from textual.containers import Container, Horizontal
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.widgets import Button, DataTable, Static from textual.widgets import Button, DataTable, Static
@ -54,63 +54,28 @@ class ConfirmActionScreen(ModalScreen[bool]):
self.dismiss(event.button.id == "confirm-yes") self.dismiss(event.button.id == "confirm-yes")
class ServicesTab(VerticalScroll): class ServicesTab(Container):
"""Interactive service list with visible start/stop/restart controls.""" """Interactive service list with start/stop/restart actions."""
DEFAULT_CSS = """
ServicesTab {
padding: 0 1;
}
ServicesTab #services-table {
height: 1fr;
min-height: 10;
}
ServicesTab #services-buttons {
height: auto;
margin-top: 1;
}
"""
def __init__(self, executor: Executor) -> None: def __init__(self, executor: Executor) -> None:
super().__init__() super().__init__()
self.executor = executor self.executor = executor
self._services: list[ServiceInfo] = [] self._services: list[ServiceInfo] = []
self._last_cursor_row: int = 0
self._last_selected_unit: str = ""
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Container(classes="panel"): with Container(classes="panel"):
yield Static( yield Static(
"SERVICES — ↑/↓ select, then Start / Stop / Restart", "SERVICES — ↑/↓ select [s]tart [S]top [R]estart",
classes="panel-title", classes="panel-title",
id="services-header", id="services-header",
) )
self.status_bar = Static("loading...", id="services-status") self.status_bar = Static("loading...", id="services-status")
yield self.status_bar yield self.status_bar
self.table = DataTable( self.table = DataTable(zebra_stripes=True, show_cursor=True, cursor_type="row")
zebra_stripes=True,
show_cursor=True,
cursor_type="row",
id="services-table",
)
self.table.add_columns("SERVICE", "STATUS", "DESCRIPTION") self.table.add_columns("SERVICE", "STATUS", "DESCRIPTION")
yield self.table yield self.table
with Horizontal(id="services-buttons"):
yield Button("Start", id="btn-start", variant="success")
yield Button("Stop", id="btn-stop", variant="error")
yield Button("Restart", id="btn-restart", variant="warning")
def update(self, services: list[ServiceInfo]) -> None: def update(self, services: list[ServiceInfo]) -> None:
# Remember what was selected so we can restore the cursor after the
# periodic refresh re-renders the table.
if self.table.row_count:
coord = self.table.cursor_coordinate
self._last_cursor_row = coord.row
if 0 <= coord.row < len(self._services):
self._last_selected_unit = self._services[coord.row].unit
else:
self._last_selected_unit = ""
self._services = services self._services = services
self.table.clear() self.table.clear()
running = sum(1 for s in services if s.running) running = sum(1 for s in services if s.running)
@ -124,23 +89,6 @@ class ServicesTab(VerticalScroll):
) )
self.table.add_row(s.name, status, s.description) self.table.add_row(s.name, status, s.description)
# Restore cursor: prefer the previously selected unit, otherwise the
# previously selected row index.
new_row = -1
if self._last_selected_unit:
for i, s in enumerate(services):
if s.unit == self._last_selected_unit:
new_row = i
break
if new_row < 0 and 0 <= self._last_cursor_row < len(services):
new_row = self._last_cursor_row
if new_row >= 0:
self.table.move_cursor(row=new_row)
self._update_action_status()
def focus_table(self) -> None:
self.table.focus()
def action_start_service(self) -> None: def action_start_service(self) -> None:
self._run_action("start") self._run_action("start")
@ -194,26 +142,3 @@ class ServicesTab(VerticalScroll):
) )
self.app.run_worker(run(), group="service-action", exclusive=False) self.app.run_worker(run(), group="service-action", exclusive=False)
def _update_action_status(self) -> None:
info = self._selected_service()
name = info.name if info else "—"
running = sum(1 for s in self._services if s.running)
self.status_bar.update(
f"[dim]{running}/{len(self._services)} running[/] │ "
f"selected: [bold]{name}[/] │ Start Stop Restart"
)
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
if event.data_table.id == "services-table":
self._update_action_status()
def on_button_pressed(self, event: Button.Pressed) -> None:
mapping = {
"btn-start": "start",
"btn-stop": "stop",
"btn-restart": "restart",
}
action = mapping.get(event.button.id or "")
if action:
self._run_action(action)

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "dsmview" name = "dsmview"
version = "0.3.1" version = "0.3.0"
description = "TUI SSH monitor for Synology DSM 7.x" description = "TUI SSH monitor for Synology DSM 7.x"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }