Compare commits

...

4 commits
v0.3.0 ... main

Author SHA1 Message Date
SW
f63599af65 docs: update install instructions for Forgejo 2026-09-10 14:22:04 +02:00
github-actions[bot]
a67c5c8214 chore: refresh formula for v0.3.1 2026-06-17 18:52:15 +00:00
SW
57d3c97ed4 fix(services): scrolling, cursor persistence and visible controls
- Make ServicesTab a VerticalScroll so the service list is scrollable.
- Preserve cursor position / selected unit across periodic refreshes.
- Add visible Start/Stop/Restart buttons.
- Show selected service in the status bar.
- Focus the service table when switching to F3.
- Bump version to 0.3.1.
2026-06-17 20:51:44 +02:00
github-actions[bot]
093960ffe4 chore: refresh formula for v0.3.0 2026-06-17 18:32:40 +00:00
6 changed files with 109 additions and 11 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.2.2.tar.gz" url "https://github.com/janvete/dsmview/archive/refs/tags/v0.3.1.tar.gz"
sha256 "3cd58fdd47fcec0b1aac967435e43b8bf7156b9ff7184152463a87f06dc72439" sha256 "6f0fcda846ee254604b87e06f3eaf20f58cdb148573cc30cb0fc7f31e922d665"
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,11 +2,31 @@
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
``` ```
@ -50,6 +70,7 @@ 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.0" __version__ = "0.3.1"

View file

@ -98,6 +98,8 @@ 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 from textual.containers import Container, Horizontal, VerticalScroll
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,28 +54,63 @@ class ConfirmActionScreen(ModalScreen[bool]):
self.dismiss(event.button.id == "confirm-yes") self.dismiss(event.button.id == "confirm-yes")
class ServicesTab(Container): class ServicesTab(VerticalScroll):
"""Interactive service list with start/stop/restart actions.""" """Interactive service list with visible start/stop/restart controls."""
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 [s]tart [S]top [R]estart", "SERVICES — ↑/↓ select, then Start / Stop / Restart",
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(zebra_stripes=True, show_cursor=True, cursor_type="row") self.table = DataTable(
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)
@ -89,6 +124,23 @@ class ServicesTab(Container):
) )
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")
@ -142,3 +194,26 @@ class ServicesTab(Container):
) )
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.0" version = "0.3.1"
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" }