Add contextual Help panel to all TUI screens (F1 toggle)
Split each screen into two columns: existing controls on the left, a DocsPanel with Rich-markup documentation on the right (30% width). Press F1 to toggle the help panel on/off from any screen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
from textual.css.query import NoMatches
|
||||||
|
|
||||||
from tui.config import has_remotes, has_targets
|
from tui.config import has_remotes, has_targets
|
||||||
from tui.screens.main_menu import MainMenuScreen
|
from tui.screens.main_menu import MainMenuScreen
|
||||||
@@ -23,6 +24,7 @@ class GnizaApp(App):
|
|||||||
|
|
||||||
TITLE = "GNIZA - Linux Backup Manager"
|
TITLE = "GNIZA - Linux Backup Manager"
|
||||||
CSS_PATH = "gniza.tcss"
|
CSS_PATH = "gniza.tcss"
|
||||||
|
BINDINGS = [("f1", "toggle_docs", "Help")]
|
||||||
|
|
||||||
SCREENS = {
|
SCREENS = {
|
||||||
"main": MainMenuScreen,
|
"main": MainMenuScreen,
|
||||||
@@ -60,6 +62,13 @@ class GnizaApp(App):
|
|||||||
else:
|
else:
|
||||||
self.notify(f"{job.label} failed (exit code {message.return_code})", severity="error")
|
self.notify(f"{job.label} failed (exit code {message.return_code})", severity="error")
|
||||||
|
|
||||||
|
def action_toggle_docs(self) -> None:
|
||||||
|
try:
|
||||||
|
panel = self.screen.query_one("#docs-panel")
|
||||||
|
panel.display = not panel.display
|
||||||
|
except NoMatches:
|
||||||
|
pass
|
||||||
|
|
||||||
async def action_quit(self) -> None:
|
async def action_quit(self) -> None:
|
||||||
if job_manager.running_count() > 0:
|
if job_manager.running_count() > 0:
|
||||||
from tui.widgets import ConfirmDialog
|
from tui.widgets import ConfirmDialog
|
||||||
|
|||||||
328
tui/docs.py
Normal file
328
tui/docs.py
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
SCREEN_DOCS = {
|
||||||
|
"backup-screen": (
|
||||||
|
"[bold]Backup Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Run backups of configured targets to remote destinations.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Target[/bold] - The backup target to run. Each target\n"
|
||||||
|
" defines which folders and databases to back up.\n"
|
||||||
|
"\n"
|
||||||
|
" [bold]Remote[/bold] - Where to send the backup. Choose a\n"
|
||||||
|
" specific remote or 'Default (all)' to back up to\n"
|
||||||
|
" every configured remote.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Run Backup[/bold] - Back up the selected target to\n"
|
||||||
|
" the selected remote.\n"
|
||||||
|
" [bold]Backup All[/bold] - Back up all enabled targets to\n"
|
||||||
|
" all their configured remotes.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - Backups run in the background. You can monitor\n"
|
||||||
|
" progress on the Running Tasks screen.\n"
|
||||||
|
" - Each backup creates a timestamped snapshot on\n"
|
||||||
|
" the remote, so you can restore any point in time.\n"
|
||||||
|
" - Use retention cleanup to remove old snapshots."
|
||||||
|
),
|
||||||
|
"restore-screen": (
|
||||||
|
"[bold]Restore Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Restore files and databases from a remote snapshot.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Target[/bold] - The backup target to restore.\n"
|
||||||
|
" [bold]Remote[/bold] - The remote that holds the snapshot.\n"
|
||||||
|
" [bold]Snapshot[/bold] - The specific point-in-time snapshot\n"
|
||||||
|
" to restore from. Loaded after selecting target and\n"
|
||||||
|
" remote.\n"
|
||||||
|
" [bold]Restore location[/bold] - Choose 'In-place' to\n"
|
||||||
|
" overwrite original files, or 'Custom directory' to\n"
|
||||||
|
" restore to a different path.\n"
|
||||||
|
" [bold]Restore MySQL[/bold] - Toggle whether to restore\n"
|
||||||
|
" MySQL databases (only shown if the target has MySQL\n"
|
||||||
|
" backup enabled).\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Warning:[/bold]\n"
|
||||||
|
" In-place restore will overwrite existing files.\n"
|
||||||
|
" Consider restoring to a custom directory first to\n"
|
||||||
|
" verify the contents."
|
||||||
|
),
|
||||||
|
"targets-screen": (
|
||||||
|
"[bold]Targets Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"List and manage backup targets. A target defines what\n"
|
||||||
|
"to back up: folders, file patterns, and optionally\n"
|
||||||
|
"MySQL databases.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Table columns:[/bold]\n"
|
||||||
|
" [bold]Name[/bold] - Unique target identifier.\n"
|
||||||
|
" [bold]Folders[/bold] - Comma-separated paths to back up.\n"
|
||||||
|
" [bold]Enabled[/bold] - Whether the target is active.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Add[/bold] - Create a new target.\n"
|
||||||
|
" [bold]Edit[/bold] - Modify the selected target.\n"
|
||||||
|
" [bold]Delete[/bold] - Remove the selected target.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - You need at least one target and one remote\n"
|
||||||
|
" before you can run backups.\n"
|
||||||
|
" - Disable a target to skip it during 'Backup All'\n"
|
||||||
|
" without deleting it."
|
||||||
|
),
|
||||||
|
"target-edit": (
|
||||||
|
"[bold]Target Editor[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Add or edit a backup target configuration.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Name[/bold] - Unique identifier (letters, digits,\n"
|
||||||
|
" dash, underscore; max 32 chars).\n"
|
||||||
|
" [bold]Folders[/bold] - Comma-separated paths to back up.\n"
|
||||||
|
" Use Browse to pick folders interactively.\n"
|
||||||
|
" [bold]Include/Exclude[/bold] - Glob patterns to filter\n"
|
||||||
|
" files (e.g. *.conf, *.log).\n"
|
||||||
|
" [bold]Remote override[/bold] - Force this target to a\n"
|
||||||
|
" specific remote instead of the default.\n"
|
||||||
|
" [bold]Retention override[/bold] - Custom snapshot count.\n"
|
||||||
|
" [bold]Pre/Post hooks[/bold] - Shell commands to run\n"
|
||||||
|
" before/after the backup.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]MySQL section:[/bold]\n"
|
||||||
|
" Enable MySQL to dump databases alongside files.\n"
|
||||||
|
" Choose 'All databases' or select specific ones.\n"
|
||||||
|
" Leave user/password empty for socket auth."
|
||||||
|
),
|
||||||
|
"remotes-screen": (
|
||||||
|
"[bold]Remotes Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"List and manage remote backup destinations.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Table columns:[/bold]\n"
|
||||||
|
" [bold]Name[/bold] - Unique remote identifier.\n"
|
||||||
|
" [bold]Type[/bold] - Connection type (SSH, Local, S3,\n"
|
||||||
|
" Google Drive).\n"
|
||||||
|
" [bold]Host/Path[/bold] - Connection details.\n"
|
||||||
|
" [bold]Disk[/bold] - Available space (loaded in background).\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Add[/bold] - Create a new remote.\n"
|
||||||
|
" [bold]Edit[/bold] - Modify the selected remote.\n"
|
||||||
|
" [bold]Test[/bold] - Verify connectivity to the remote.\n"
|
||||||
|
" [bold]Delete[/bold] - Remove the selected remote.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - Always test a new remote before running backups.\n"
|
||||||
|
" - The Disk column shows used/total space and may\n"
|
||||||
|
" take a moment to load for SSH remotes."
|
||||||
|
),
|
||||||
|
"remote-edit": (
|
||||||
|
"[bold]Remote Editor[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Add or edit a remote backup destination.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Types:[/bold]\n"
|
||||||
|
" [bold]SSH[/bold] - Remote server via SSH/rsync.\n"
|
||||||
|
" [bold]Local[/bold] - Local directory or mounted drive.\n"
|
||||||
|
" [bold]S3[/bold] - Amazon S3 or compatible storage.\n"
|
||||||
|
" [bold]Google Drive[/bold] - Google Drive via service\n"
|
||||||
|
" account.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]SSH fields:[/bold]\n"
|
||||||
|
" Host, port, user, and either SSH key or password\n"
|
||||||
|
" authentication.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Common fields:[/bold]\n"
|
||||||
|
" [bold]Base path[/bold] - Root directory for backups on\n"
|
||||||
|
" the remote.\n"
|
||||||
|
" [bold]Bandwidth limit[/bold] - Throttle transfer speed\n"
|
||||||
|
" in KB/s (0 = unlimited).\n"
|
||||||
|
" [bold]Retention count[/bold] - Max snapshots to keep.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - For SSH, ensure the remote user has write access\n"
|
||||||
|
" to the base path.\n"
|
||||||
|
" - Use key-based auth for unattended backups."
|
||||||
|
),
|
||||||
|
"snapshots-screen": (
|
||||||
|
"[bold]Snapshots Browser[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Browse snapshots stored on remote destinations.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Target[/bold] - The backup target.\n"
|
||||||
|
" [bold]Remote[/bold] - The remote holding snapshots.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Load Snapshots[/bold] - Fetch the list of\n"
|
||||||
|
" available snapshots from the remote.\n"
|
||||||
|
" [bold]Browse Files[/bold] - View the file tree inside\n"
|
||||||
|
" the selected snapshot.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - Each snapshot is a timestamped directory on the\n"
|
||||||
|
" remote containing the backed-up files.\n"
|
||||||
|
" - Loading may take a moment for SSH remotes.\n"
|
||||||
|
" - Use the Restore screen to actually restore files\n"
|
||||||
|
" from a snapshot."
|
||||||
|
),
|
||||||
|
"retention-screen": (
|
||||||
|
"[bold]Retention Cleanup[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Remove old snapshots based on retention count.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Target[/bold] - Run cleanup for a specific target.\n"
|
||||||
|
" [bold]Default retention count[/bold] - Number of\n"
|
||||||
|
" snapshots to keep per target/remote pair.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Run Cleanup[/bold] - Clean old snapshots for the\n"
|
||||||
|
" selected target.\n"
|
||||||
|
" [bold]Cleanup All[/bold] - Clean old snapshots for all\n"
|
||||||
|
" targets.\n"
|
||||||
|
" [bold]Save[/bold] - Save the default retention count.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]How it works:[/bold]\n"
|
||||||
|
" Retention keeps the N most recent snapshots and\n"
|
||||||
|
" deletes the rest. Per-target or per-remote overrides\n"
|
||||||
|
" take priority over the default count.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Warning:[/bold]\n"
|
||||||
|
" Deleted snapshots cannot be recovered."
|
||||||
|
),
|
||||||
|
"schedule-screen": (
|
||||||
|
"[bold]Schedules Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Manage scheduled backup jobs via cron.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Table columns:[/bold]\n"
|
||||||
|
" [bold]Name[/bold] - Schedule identifier.\n"
|
||||||
|
" [bold]Active[/bold] - Whether the schedule is enabled.\n"
|
||||||
|
" [bold]Type[/bold] - Frequency (hourly/daily/weekly/\n"
|
||||||
|
" monthly/custom).\n"
|
||||||
|
" [bold]Time[/bold] - When the backup runs.\n"
|
||||||
|
" [bold]Last/Next Run[/bold] - Timing information.\n"
|
||||||
|
" [bold]Targets/Remotes[/bold] - Scope of the schedule.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]Toggle Active[/bold] - Enable/disable a schedule.\n"
|
||||||
|
" [bold]Show crontab[/bold] - View the installed crontab.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - The cron daemon must be running for schedules to\n"
|
||||||
|
" execute. You will be warned if it is not running.\n"
|
||||||
|
" - Changes are automatically synced to crontab."
|
||||||
|
),
|
||||||
|
"schedule-edit": (
|
||||||
|
"[bold]Schedule Editor[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Add or edit a scheduled backup.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Schedule types:[/bold]\n"
|
||||||
|
" [bold]Hourly[/bold] - Run every N hours.\n"
|
||||||
|
" [bold]Daily[/bold] - Run at a set time on selected days.\n"
|
||||||
|
" [bold]Weekly[/bold] - Run once a week on a chosen day.\n"
|
||||||
|
" [bold]Monthly[/bold] - Run once a month on a chosen day.\n"
|
||||||
|
" [bold]Custom cron[/bold] - Use a raw 5-field cron\n"
|
||||||
|
" expression for full control.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Fields:[/bold]\n"
|
||||||
|
" [bold]Time[/bold] - The time of day to run (HH:MM).\n"
|
||||||
|
" [bold]Targets[/bold] - Which targets to back up. Leave\n"
|
||||||
|
" empty to back up all targets.\n"
|
||||||
|
" [bold]Remotes[/bold] - Which remotes to use. Leave empty\n"
|
||||||
|
" to use all remotes.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - Daily schedules let you pick specific days of\n"
|
||||||
|
" the week.\n"
|
||||||
|
" - Custom cron: minute hour day month weekday"
|
||||||
|
),
|
||||||
|
"logs-screen": (
|
||||||
|
"[bold]Logs Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"View backup operation logs.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Table columns:[/bold]\n"
|
||||||
|
" [bold]Status[/bold] - Outcome (Success, Failed,\n"
|
||||||
|
" Interrupted, Empty).\n"
|
||||||
|
" [bold]Date/Time[/bold] - When the operation ran.\n"
|
||||||
|
" [bold]Size[/bold] - Log file size.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]View[/bold] - Display the selected log file\n"
|
||||||
|
" contents in the viewer below.\n"
|
||||||
|
" [bold]Status[/bold] - Show a summary of recent backup\n"
|
||||||
|
" activity.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Status detection:[/bold]\n"
|
||||||
|
" - [green]Success[/green] - 'Backup completed' found, no errors.\n"
|
||||||
|
" - [red]Failed[/red] - ERROR or FATAL entries found.\n"
|
||||||
|
" - [yellow]Interrupted[/yellow] - Log exists but backup did\n"
|
||||||
|
" not complete.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - The 20 most recent logs are shown.\n"
|
||||||
|
" - Check logs after scheduled backups to verify\n"
|
||||||
|
" they ran successfully."
|
||||||
|
),
|
||||||
|
"settings-screen": (
|
||||||
|
"[bold]Settings Screen[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Global application settings.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]General:[/bold]\n"
|
||||||
|
" [bold]Log Level[/bold] - Verbosity of log output.\n"
|
||||||
|
" [bold]Log Retention[/bold] - Days to keep log files.\n"
|
||||||
|
" [bold]Retention Count[/bold] - Default number of\n"
|
||||||
|
" snapshots to keep.\n"
|
||||||
|
" [bold]Bandwidth Limit[/bold] - Default transfer speed\n"
|
||||||
|
" limit in KB/s.\n"
|
||||||
|
" [bold]Disk Threshold[/bold] - Warn when remote disk\n"
|
||||||
|
" usage exceeds this percentage.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Email Notifications:[/bold]\n"
|
||||||
|
" Configure SMTP to receive email alerts on backup\n"
|
||||||
|
" success or failure.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]SSH:[/bold]\n"
|
||||||
|
" [bold]Timeout[/bold] - Seconds before SSH gives up.\n"
|
||||||
|
" [bold]Retries[/bold] - Number of retry attempts.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Web Dashboard:[/bold]\n"
|
||||||
|
" Port, host, and API key for the web interface.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - Per-target and per-remote settings override\n"
|
||||||
|
" these defaults."
|
||||||
|
),
|
||||||
|
"running-tasks-screen": (
|
||||||
|
"[bold]Running Tasks[/bold]\n"
|
||||||
|
"\n"
|
||||||
|
"Monitor running backup and restore jobs.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Table columns:[/bold]\n"
|
||||||
|
" [bold]Status[/bold] - Current state (running, ok, failed,\n"
|
||||||
|
" unknown).\n"
|
||||||
|
" [bold]Job[/bold] - Description of the operation.\n"
|
||||||
|
" [bold]Started[/bold] - When the job began.\n"
|
||||||
|
" [bold]Duration[/bold] - Elapsed time.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Buttons:[/bold]\n"
|
||||||
|
" [bold]View Log[/bold] - Show live log output and\n"
|
||||||
|
" progress bar for the selected job.\n"
|
||||||
|
" [bold]Kill Job[/bold] - Terminate the selected running\n"
|
||||||
|
" job.\n"
|
||||||
|
" [bold]Clear Finished[/bold] - Remove completed jobs\n"
|
||||||
|
" from the list.\n"
|
||||||
|
"\n"
|
||||||
|
"[bold]Tips:[/bold]\n"
|
||||||
|
" - The table refreshes every second.\n"
|
||||||
|
" - The progress bar shows rsync transfer progress\n"
|
||||||
|
" when viewing a running job's log.\n"
|
||||||
|
" - Jobs continue in the background even if you\n"
|
||||||
|
" navigate away from this screen."
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -33,6 +33,10 @@ Screen {
|
|||||||
margin: 1 2 0 2;
|
margin: 1 2 0 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screen-with-docs {
|
||||||
|
height: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
/* Data tables */
|
/* Data tables */
|
||||||
DataTable {
|
DataTable {
|
||||||
height: 12;
|
height: 12;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from textual.widgets import Header, Footer, Static, Button, Select
|
|||||||
from textual.containers import Vertical, Horizontal
|
from textual.containers import Vertical, Horizontal
|
||||||
from tui.config import list_conf_dir, has_targets, has_remotes
|
from tui.config import list_conf_dir, has_targets, has_remotes
|
||||||
from tui.jobs import job_manager
|
from tui.jobs import job_manager
|
||||||
from tui.widgets import ConfirmDialog
|
from tui.widgets import ConfirmDialog, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class BackupScreen(Screen):
|
class BackupScreen(Screen):
|
||||||
@@ -15,6 +15,7 @@ class BackupScreen(Screen):
|
|||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
targets = list_conf_dir("targets.d")
|
targets = list_conf_dir("targets.d")
|
||||||
remotes = list_conf_dir("remotes.d")
|
remotes = list_conf_dir("remotes.d")
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="backup-screen"):
|
with Vertical(id="backup-screen"):
|
||||||
yield Static("Backup", id="screen-title")
|
yield Static("Backup", id="screen-title")
|
||||||
if not targets:
|
if not targets:
|
||||||
@@ -37,6 +38,7 @@ class BackupScreen(Screen):
|
|||||||
yield Button("Run Backup", variant="primary", id="btn-backup")
|
yield Button("Run Backup", variant="primary", id="btn-backup")
|
||||||
yield Button("Backup All", variant="warning", id="btn-backup-all")
|
yield Button("Backup All", variant="warning", id="btn-backup-all")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("backup-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from textual.widgets import Header, Footer, Static, Button, DataTable, RichLog
|
|||||||
from textual.containers import Vertical, Horizontal
|
from textual.containers import Vertical, Horizontal
|
||||||
|
|
||||||
from tui.config import LOG_DIR
|
from tui.config import LOG_DIR
|
||||||
|
from tui.widgets import DocsPanel
|
||||||
|
|
||||||
|
|
||||||
_LOG_NAME_RE = re.compile(r"gniza-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})\.log")
|
_LOG_NAME_RE = re.compile(r"gniza-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})\.log")
|
||||||
@@ -47,6 +48,7 @@ class LogsScreen(Screen):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="logs-screen"):
|
with Vertical(id="logs-screen"):
|
||||||
yield Static("Logs", id="screen-title")
|
yield Static("Logs", id="screen-title")
|
||||||
yield DataTable(id="logs-table")
|
yield DataTable(id="logs-table")
|
||||||
@@ -55,6 +57,7 @@ class LogsScreen(Screen):
|
|||||||
yield Button("Status", id="btn-status")
|
yield Button("Status", id="btn-status")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
yield RichLog(id="log-viewer", wrap=True, highlight=True)
|
yield RichLog(id="log-viewer", wrap=True, highlight=True)
|
||||||
|
yield DocsPanel.for_screen("logs-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from textual.containers import Vertical, Horizontal
|
|||||||
|
|
||||||
from tui.config import parse_conf, write_conf, CONFIG_DIR
|
from tui.config import parse_conf, write_conf, CONFIG_DIR
|
||||||
from tui.models import Remote
|
from tui.models import Remote
|
||||||
from tui.widgets import FilePicker
|
from tui.widgets import FilePicker, DocsPanel
|
||||||
|
|
||||||
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ class RemoteEditScreen(Screen):
|
|||||||
data = parse_conf(CONFIG_DIR / "remotes.d" / f"{self._edit_name}.conf")
|
data = parse_conf(CONFIG_DIR / "remotes.d" / f"{self._edit_name}.conf")
|
||||||
remote = Remote.from_conf(self._edit_name, data)
|
remote = Remote.from_conf(self._edit_name, data)
|
||||||
|
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="remote-edit"):
|
with Vertical(id="remote-edit"):
|
||||||
yield Static(title, id="screen-title")
|
yield Static(title, id="screen-title")
|
||||||
if self._is_new:
|
if self._is_new:
|
||||||
@@ -88,6 +89,7 @@ class RemoteEditScreen(Screen):
|
|||||||
with Horizontal(id="re-buttons"):
|
with Horizontal(id="re-buttons"):
|
||||||
yield Button("Save", variant="primary", id="btn-save")
|
yield Button("Save", variant="primary", id="btn-save")
|
||||||
yield Button("Cancel", id="btn-cancel")
|
yield Button("Cancel", id="btn-cancel")
|
||||||
|
yield DocsPanel.for_screen("remote-edit")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from textual import work
|
|||||||
|
|
||||||
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
||||||
from tui.backend import run_cli
|
from tui.backend import run_cli
|
||||||
from tui.widgets import ConfirmDialog, OperationLog
|
from tui.widgets import ConfirmDialog, OperationLog, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class RemotesScreen(Screen):
|
class RemotesScreen(Screen):
|
||||||
@@ -15,6 +15,7 @@ class RemotesScreen(Screen):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="remotes-screen"):
|
with Vertical(id="remotes-screen"):
|
||||||
yield Static("Remotes", id="screen-title")
|
yield Static("Remotes", id="screen-title")
|
||||||
yield DataTable(id="remotes-table")
|
yield DataTable(id="remotes-table")
|
||||||
@@ -24,6 +25,7 @@ class RemotesScreen(Screen):
|
|||||||
yield Button("Test", variant="warning", id="btn-test")
|
yield Button("Test", variant="warning", id="btn-test")
|
||||||
yield Button("Delete", variant="error", id="btn-delete")
|
yield Button("Delete", variant="error", id="btn-delete")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("remotes-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from textual import work, on
|
|||||||
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
||||||
from tui.backend import run_cli
|
from tui.backend import run_cli
|
||||||
from tui.jobs import job_manager
|
from tui.jobs import job_manager
|
||||||
from tui.widgets import ConfirmDialog, FolderPicker
|
from tui.widgets import ConfirmDialog, FolderPicker, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class RestoreScreen(Screen):
|
class RestoreScreen(Screen):
|
||||||
@@ -18,6 +18,7 @@ class RestoreScreen(Screen):
|
|||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
targets = list_conf_dir("targets.d")
|
targets = list_conf_dir("targets.d")
|
||||||
remotes = list_conf_dir("remotes.d")
|
remotes = list_conf_dir("remotes.d")
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="restore-screen"):
|
with Vertical(id="restore-screen"):
|
||||||
yield Static("Restore", id="screen-title")
|
yield Static("Restore", id="screen-title")
|
||||||
if not targets or not remotes:
|
if not targets or not remotes:
|
||||||
@@ -42,6 +43,7 @@ class RestoreScreen(Screen):
|
|||||||
with Horizontal(id="restore-buttons"):
|
with Horizontal(id="restore-buttons"):
|
||||||
yield Button("Restore", variant="primary", id="btn-restore")
|
yield Button("Restore", variant="primary", id="btn-restore")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("restore-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from textual import work
|
|||||||
|
|
||||||
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR
|
||||||
from tui.backend import stream_cli
|
from tui.backend import stream_cli
|
||||||
from tui.widgets import ConfirmDialog, OperationLog
|
from tui.widgets import ConfirmDialog, OperationLog, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class RetentionScreen(Screen):
|
class RetentionScreen(Screen):
|
||||||
@@ -18,6 +18,7 @@ class RetentionScreen(Screen):
|
|||||||
targets = list_conf_dir("targets.d")
|
targets = list_conf_dir("targets.d")
|
||||||
conf = parse_conf(CONFIG_DIR / "gniza.conf")
|
conf = parse_conf(CONFIG_DIR / "gniza.conf")
|
||||||
current_count = conf.get("RETENTION_COUNT", "30")
|
current_count = conf.get("RETENTION_COUNT", "30")
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="retention-screen"):
|
with Vertical(id="retention-screen"):
|
||||||
yield Static("Retention Cleanup", id="screen-title")
|
yield Static("Retention Cleanup", id="screen-title")
|
||||||
if not targets:
|
if not targets:
|
||||||
@@ -38,6 +39,7 @@ class RetentionScreen(Screen):
|
|||||||
yield Input(value=current_count, id="ret-count", placeholder="30")
|
yield Input(value=current_count, id="ret-count", placeholder="30")
|
||||||
yield Button("Save", id="btn-save-count")
|
yield Button("Save", id="btn-save-count")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("retention-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from textual.containers import Vertical, Horizontal
|
|||||||
from textual.timer import Timer
|
from textual.timer import Timer
|
||||||
|
|
||||||
from tui.jobs import job_manager
|
from tui.jobs import job_manager
|
||||||
from tui.widgets import ConfirmDialog
|
from tui.widgets import ConfirmDialog, DocsPanel
|
||||||
|
|
||||||
_PROGRESS_RE = re.compile(r"(\d+)%")
|
_PROGRESS_RE = re.compile(r"(\d+)%")
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ class RunningTasksScreen(Screen):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="running-tasks-screen"):
|
with Vertical(id="running-tasks-screen"):
|
||||||
yield Static("Running Tasks", id="screen-title")
|
yield Static("Running Tasks", id="screen-title")
|
||||||
yield DataTable(id="rt-table")
|
yield DataTable(id="rt-table")
|
||||||
@@ -31,6 +32,7 @@ class RunningTasksScreen(Screen):
|
|||||||
yield Static("", id="rt-progress-label")
|
yield Static("", id="rt-progress-label")
|
||||||
yield ProgressBar(id="rt-progress", total=100, show_eta=False)
|
yield ProgressBar(id="rt-progress", total=100, show_eta=False)
|
||||||
yield RichLog(id="rt-log-viewer", wrap=True, highlight=True)
|
yield RichLog(id="rt-log-viewer", wrap=True, highlight=True)
|
||||||
|
yield DocsPanel.for_screen("running-tasks-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from textual import work
|
|||||||
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR, LOG_DIR
|
from tui.config import list_conf_dir, parse_conf, update_conf_key, CONFIG_DIR, LOG_DIR
|
||||||
from tui.models import Schedule
|
from tui.models import Schedule
|
||||||
from tui.backend import run_cli
|
from tui.backend import run_cli
|
||||||
from tui.widgets import ConfirmDialog, OperationLog
|
from tui.widgets import ConfirmDialog, OperationLog, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class ScheduleScreen(Screen):
|
class ScheduleScreen(Screen):
|
||||||
@@ -18,6 +18,7 @@ class ScheduleScreen(Screen):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="schedule-screen"):
|
with Vertical(id="schedule-screen"):
|
||||||
yield Static("Schedules", id="screen-title")
|
yield Static("Schedules", id="screen-title")
|
||||||
yield DataTable(id="sched-table")
|
yield DataTable(id="sched-table")
|
||||||
@@ -28,6 +29,7 @@ class ScheduleScreen(Screen):
|
|||||||
yield Button("Delete", variant="error", id="btn-delete")
|
yield Button("Delete", variant="error", id="btn-delete")
|
||||||
yield Button("Show crontab", id="btn-show")
|
yield Button("Show crontab", id="btn-show")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("schedule-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
@@ -169,6 +171,36 @@ class ScheduleScreen(Screen):
|
|||||||
rc, stdout, stderr = await run_cli("schedule", "remove")
|
rc, stdout, stderr = await run_cli("schedule", "remove")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
self.notify(f"Crontab sync failed: {stderr or stdout}", severity="error")
|
self.notify(f"Crontab sync failed: {stderr or stdout}", severity="error")
|
||||||
|
# Warn if cron daemon is not running
|
||||||
|
if has_active and not await self._is_cron_running():
|
||||||
|
self.notify(
|
||||||
|
"Cron daemon is not running — schedules won't execute. "
|
||||||
|
"Start it with: sudo systemctl start cron",
|
||||||
|
severity="warning",
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _is_cron_running() -> bool:
|
||||||
|
"""Check if the cron daemon is active."""
|
||||||
|
import asyncio
|
||||||
|
for svc in ("cron", "crond"):
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"systemctl", "is-active", svc,
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
await proc.wait()
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return True
|
||||||
|
# Fallback: check for a running process
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"pgrep", "-x", "cron",
|
||||||
|
stdout=asyncio.subprocess.DEVNULL,
|
||||||
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
await proc.wait()
|
||||||
|
return proc.returncode == 0
|
||||||
|
|
||||||
def _delete_schedule(self, name: str) -> None:
|
def _delete_schedule(self, name: str) -> None:
|
||||||
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
conf = CONFIG_DIR / "schedules.d" / f"{name}.conf"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from textual.containers import Vertical, Horizontal
|
|||||||
|
|
||||||
from tui.config import list_conf_dir, parse_conf, write_conf, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, write_conf, CONFIG_DIR
|
||||||
from tui.models import Schedule
|
from tui.models import Schedule
|
||||||
|
from tui.widgets import DocsPanel
|
||||||
|
|
||||||
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ class ScheduleEditScreen(Screen):
|
|||||||
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{self._edit_name}.conf")
|
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{self._edit_name}.conf")
|
||||||
sched = Schedule.from_conf(self._edit_name, data)
|
sched = Schedule.from_conf(self._edit_name, data)
|
||||||
|
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="schedule-edit"):
|
with Vertical(id="schedule-edit"):
|
||||||
yield Static(title, id="screen-title")
|
yield Static(title, id="screen-title")
|
||||||
if self._is_new:
|
if self._is_new:
|
||||||
@@ -115,6 +117,7 @@ class ScheduleEditScreen(Screen):
|
|||||||
with Horizontal(id="sched-edit-buttons"):
|
with Horizontal(id="sched-edit-buttons"):
|
||||||
yield Button("Save", variant="primary", id="btn-save")
|
yield Button("Save", variant="primary", id="btn-save")
|
||||||
yield Button("Cancel", id="btn-cancel")
|
yield Button("Cancel", id="btn-cancel")
|
||||||
|
yield DocsPanel.for_screen("schedule-edit")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def _build_target_choices(self) -> list[tuple[str, str]]:
|
def _build_target_choices(self) -> list[tuple[str, str]]:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from textual.containers import Vertical, Horizontal
|
|||||||
|
|
||||||
from tui.config import parse_conf, write_conf, CONFIG_DIR
|
from tui.config import parse_conf, write_conf, CONFIG_DIR
|
||||||
from tui.models import AppSettings
|
from tui.models import AppSettings
|
||||||
|
from tui.widgets import DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class SettingsScreen(Screen):
|
class SettingsScreen(Screen):
|
||||||
@@ -15,6 +16,7 @@ class SettingsScreen(Screen):
|
|||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
conf = parse_conf(CONFIG_DIR / "gniza.conf")
|
conf = parse_conf(CONFIG_DIR / "gniza.conf")
|
||||||
settings = AppSettings.from_conf(conf)
|
settings = AppSettings.from_conf(conf)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="settings-screen"):
|
with Vertical(id="settings-screen"):
|
||||||
yield Static("Settings", id="screen-title")
|
yield Static("Settings", id="screen-title")
|
||||||
yield Static("Log Level:")
|
yield Static("Log Level:")
|
||||||
@@ -71,6 +73,7 @@ class SettingsScreen(Screen):
|
|||||||
with Horizontal(id="set-buttons"):
|
with Horizontal(id="set-buttons"):
|
||||||
yield Button("Save", variant="primary", id="btn-save")
|
yield Button("Save", variant="primary", id="btn-save")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("settings-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from textual import work
|
|||||||
|
|
||||||
from tui.config import list_conf_dir
|
from tui.config import list_conf_dir
|
||||||
from tui.backend import run_cli
|
from tui.backend import run_cli
|
||||||
from tui.widgets import SnapshotBrowser
|
from tui.widgets import SnapshotBrowser, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
def _format_snapshot_ts(ts: str) -> str:
|
def _format_snapshot_ts(ts: str) -> str:
|
||||||
@@ -29,6 +29,7 @@ class SnapshotsScreen(Screen):
|
|||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
targets = list_conf_dir("targets.d")
|
targets = list_conf_dir("targets.d")
|
||||||
remotes = list_conf_dir("remotes.d")
|
remotes = list_conf_dir("remotes.d")
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="snapshots-screen"):
|
with Vertical(id="snapshots-screen"):
|
||||||
yield Static("Snapshots Browser", id="screen-title")
|
yield Static("Snapshots Browser", id="screen-title")
|
||||||
if not targets or not remotes:
|
if not targets or not remotes:
|
||||||
@@ -43,6 +44,7 @@ class SnapshotsScreen(Screen):
|
|||||||
with Horizontal(id="snapshots-buttons"):
|
with Horizontal(id="snapshots-buttons"):
|
||||||
yield Button("Browse Files", id="btn-browse")
|
yield Button("Browse Files", id="btn-browse")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("snapshots-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from textual.containers import Vertical, Horizontal
|
|||||||
|
|
||||||
from tui.config import parse_conf, write_conf, CONFIG_DIR, list_conf_dir
|
from tui.config import parse_conf, write_conf, CONFIG_DIR, list_conf_dir
|
||||||
from tui.models import Target
|
from tui.models import Target
|
||||||
from tui.widgets import FolderPicker
|
from tui.widgets import FolderPicker, DocsPanel
|
||||||
|
|
||||||
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
_NAME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]{0,31}$')
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ class TargetEditScreen(Screen):
|
|||||||
data = parse_conf(CONFIG_DIR / "targets.d" / f"{self._edit_name}.conf")
|
data = parse_conf(CONFIG_DIR / "targets.d" / f"{self._edit_name}.conf")
|
||||||
target = Target.from_conf(self._edit_name, data)
|
target = Target.from_conf(self._edit_name, data)
|
||||||
|
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="target-edit"):
|
with Vertical(id="target-edit"):
|
||||||
yield Static(title, id="screen-title")
|
yield Static(title, id="screen-title")
|
||||||
if self._is_new:
|
if self._is_new:
|
||||||
@@ -85,6 +86,7 @@ class TargetEditScreen(Screen):
|
|||||||
with Horizontal(id="te-buttons"):
|
with Horizontal(id="te-buttons"):
|
||||||
yield Button("Save", variant="primary", id="btn-save")
|
yield Button("Save", variant="primary", id="btn-save")
|
||||||
yield Button("Cancel", id="btn-cancel")
|
yield Button("Cancel", id="btn-cancel")
|
||||||
|
yield DocsPanel.for_screen("target-edit")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from textual.widgets import Header, Footer, Static, Button, DataTable
|
|||||||
from textual.containers import Vertical, Horizontal
|
from textual.containers import Vertical, Horizontal
|
||||||
|
|
||||||
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
from tui.config import list_conf_dir, parse_conf, CONFIG_DIR
|
||||||
from tui.widgets import ConfirmDialog
|
from tui.widgets import ConfirmDialog, DocsPanel
|
||||||
|
|
||||||
|
|
||||||
class TargetsScreen(Screen):
|
class TargetsScreen(Screen):
|
||||||
@@ -13,6 +13,7 @@ class TargetsScreen(Screen):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
|
with Horizontal(classes="screen-with-docs"):
|
||||||
with Vertical(id="targets-screen"):
|
with Vertical(id="targets-screen"):
|
||||||
yield Static("Targets", id="screen-title")
|
yield Static("Targets", id="screen-title")
|
||||||
yield DataTable(id="targets-table")
|
yield DataTable(id="targets-table")
|
||||||
@@ -21,6 +22,7 @@ class TargetsScreen(Screen):
|
|||||||
yield Button("Edit", id="btn-edit")
|
yield Button("Edit", id="btn-edit")
|
||||||
yield Button("Delete", variant="error", id="btn-delete")
|
yield Button("Delete", variant="error", id="btn-delete")
|
||||||
yield Button("Back", id="btn-back")
|
yield Button("Back", id="btn-back")
|
||||||
|
yield DocsPanel.for_screen("targets-screen")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ from tui.widgets.file_picker import FilePicker
|
|||||||
from tui.widgets.confirm_dialog import ConfirmDialog
|
from tui.widgets.confirm_dialog import ConfirmDialog
|
||||||
from tui.widgets.operation_log import OperationLog
|
from tui.widgets.operation_log import OperationLog
|
||||||
from tui.widgets.snapshot_browser import SnapshotBrowser
|
from tui.widgets.snapshot_browser import SnapshotBrowser
|
||||||
|
from tui.widgets.docs_panel import DocsPanel
|
||||||
|
|||||||
28
tui/widgets/docs_panel.py
Normal file
28
tui/widgets/docs_panel.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from textual.containers import VerticalScroll
|
||||||
|
from textual.widgets import Static
|
||||||
|
from tui.docs import SCREEN_DOCS
|
||||||
|
|
||||||
|
|
||||||
|
class DocsPanel(VerticalScroll):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
DocsPanel {
|
||||||
|
width: 30%;
|
||||||
|
min-width: 30;
|
||||||
|
border-left: solid $accent;
|
||||||
|
padding: 1 2;
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, content: str, **kwargs):
|
||||||
|
super().__init__(id="docs-panel", **kwargs)
|
||||||
|
self._content = content
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Static("[bold underline]Help[/]", id="docs-title")
|
||||||
|
yield Static(self._content, id="docs-body")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_screen(cls, screen_id: str) -> "DocsPanel":
|
||||||
|
text = SCREEN_DOCS.get(screen_id, "No documentation available for this screen.")
|
||||||
|
return cls(content=text)
|
||||||
Reference in New Issue
Block a user