Compare commits
No commits in common. "main" and "v0.3.0" have entirely different histories.
6 changed files with 11 additions and 109 deletions
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.3.1"
|
__version__ = "0.3.0"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue