Files
gniza4linux/tui/screens/main_menu.py
shuki fec13135ce Add source targets, docs panel, tail-style log viewer, and various improvements
- Add source.sh for remote source backup support
- Add responsive DocsPanel with layout adaptations for narrow screens
- Running tasks log viewer now shows last 100 lines (tail -f style)
- Add incremental backup explanation to README
- Update backup, transfer, schedule, and snaplog modules
- Add MCP config and logo asset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 03:06:39 +02:00

115 lines
4.4 KiB
Python

from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, OptionList
from textual.widgets.option_list import Option
from textual.containers import Horizontal, Vertical
from tui.jobs import job_manager
LOGO = """\
[green]▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓
▓▓
[/green]
GNIZA - Linux Backup Manager
"""
MENU_ITEMS = [
("backup", "Backup"),
("restore", "Restore"),
("running_tasks", "Running Tasks"),
("targets", "Targets"),
("remotes", "Remotes"),
("schedule", "Schedules"),
("snapshots", "Snapshots Browser"),
("logs", "Logs"),
("settings", "Settings"),
("quit", "Quit"),
]
SPINNER_FRAMES = ["", "", "", "", "", "", "", "", "", ""]
class MainMenuScreen(Screen):
BINDINGS = [("q", "quit_app", "Quit")]
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
with Horizontal(id="main-layout"):
yield Static(LOGO, id="logo", markup=True)
menu_items = []
for mid, label in MENU_ITEMS:
menu_items.append(Option(label, id=mid))
if mid == "running_tasks":
menu_items.append(None)
yield OptionList(*menu_items, id="menu-list")
yield Footer()
def on_mount(self) -> None:
self._update_layout()
self.query_one("#menu-list", OptionList).focus()
self._spinner_idx = 0
self._update_running_label()
self._spinner_timer = self.set_interval(1, self._tick_spinner)
def on_resize(self) -> None:
self._update_layout()
def _update_layout(self) -> None:
width = self.app.size.width
logo = self.query_one("#logo")
layout = self.query_one("#main-layout")
logo.display = width >= 48
if width < 100:
layout.styles.layout = "vertical"
layout.styles.align = ("center", "top")
layout.styles.overflow_y = "auto"
else:
layout.styles.layout = "horizontal"
layout.styles.align = ("center", "middle")
layout.styles.overflow_y = "hidden"
def _tick_spinner(self) -> None:
self._spinner_idx = (self._spinner_idx + 1) % len(SPINNER_FRAMES)
self._update_running_label()
def _update_running_label(self) -> None:
count = job_manager.running_count()
menu = self.query_one("#menu-list", OptionList)
# Find the running_tasks option index
for idx in range(menu.option_count):
opt = menu.get_option_at_index(idx)
if opt.id == "running_tasks":
if count > 0:
spinner = SPINNER_FRAMES[self._spinner_idx]
label = f"{spinner} Running Tasks ({count})"
else:
label = "Running Tasks"
menu.replace_option_prompt(opt.id, label)
break
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
option_id = event.option.id
if option_id == "quit":
self.app.exit()
elif option_id:
self.app.push_screen(option_id)
def action_quit_app(self) -> None:
self.app.exit()