- Implement modular Textual TUI (main, app, widgets, config, api_client) - Integrate IPv64.net REST API client using httpx - Support record listing, creation, in-place edit, and deletion - Add flexible token resolution (.env, env export, interactive prompt) - Add bilingual README (DE/EN) and project configuration
108 lines
4.2 KiB
Python
Executable File
108 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""IPv64 DNS TUI Client
|
|
Author: Pascal Bouquet <pascal@ptbos.de>
|
|
License: GNU General Public License v3.0 (GPL-3.0)
|
|
Year: 2026
|
|
"""
|
|
|
|
from rich.markup import escape
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Horizontal, Vertical
|
|
from textual.widgets import Button, Input, Label, Select, Static
|
|
|
|
|
|
class RecordForm(Static):
|
|
"""Eingabe- und Bearbeitungsformular für DNS-Records."""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.edit_record_id: str | None = None
|
|
|
|
def compose(self) -> ComposeResult:
|
|
with Vertical(id="form-pane"):
|
|
yield Label("Record hinzufügen:", id="form-title")
|
|
with Horizontal(classes="form-row"):
|
|
yield Label("Subdomain:", classes="form-label")
|
|
yield Input(placeholder="z. B. www oder mx01", id="in-subdomain")
|
|
yield Label("Typ:", classes="form-label")
|
|
yield Select(
|
|
[
|
|
("A", "A"),
|
|
("AAAA", "AAAA"),
|
|
("CNAME", "CNAME"),
|
|
("TXT", "TXT"),
|
|
("MX", "MX"),
|
|
("NS", "NS"),
|
|
("SRV", "SRV"),
|
|
],
|
|
value="A",
|
|
id="sel-type",
|
|
)
|
|
with Horizontal(classes="form-row"):
|
|
yield Label("Content/IP:", classes="form-label")
|
|
yield Input(placeholder="192.0.2.1 oder Ziel", id="in-content")
|
|
yield Label("Prio:", classes="form-label")
|
|
yield Input(placeholder="10 (nur MX/SRV)", id="in-prio")
|
|
yield Button("Erstellen", id="btn-submit", variant="success")
|
|
yield Button(
|
|
"Abbrechen",
|
|
id="btn-cancel",
|
|
variant="default",
|
|
classes="hidden",
|
|
)
|
|
|
|
def set_edit_mode(
|
|
self, record_id: str, subdomain: str, typ: str, content: str, prio: str
|
|
) -> None:
|
|
"""Schaltet das Formular in den Bearbeitungsmodus."""
|
|
self.edit_record_id = record_id
|
|
|
|
# Runde Klammern und escape() verhindern den Rich-Markup-Error
|
|
clean_id = escape(str(record_id))
|
|
self.query_one("#form-title", Label).update(
|
|
f"Record bearbeiten (ID: {clean_id}):"
|
|
)
|
|
|
|
# Subdomain bereinigen: @ oder leer wird zu leerem Input
|
|
self.query_one("#in-subdomain", Input).value = "" if subdomain in ("@", "") else subdomain
|
|
|
|
# Select-Wert für Typ setzen, falls in Optionen vorhanden
|
|
sel_type = self.query_one("#sel-type", Select)
|
|
if typ in [opt[1] for opt in sel_type._options]:
|
|
sel_type.value = typ
|
|
|
|
self.query_one("#in-content", Input).value = "" if content == "-" else content
|
|
self.query_one("#in-prio", Input).value = "" if prio in ("-", "None") else prio
|
|
|
|
btn_submit = self.query_one("#btn-submit", Button)
|
|
btn_submit.label = "Speichern"
|
|
btn_submit.variant = "warning"
|
|
|
|
self.query_one("#btn-cancel", Button).remove_class("hidden")
|
|
|
|
def reset_mode(self) -> None:
|
|
"""Setzt das Formular wieder auf 'Neuer Record' zurück."""
|
|
self.edit_record_id = None
|
|
self.query_one("#form-title", Label).update("Record hinzufügen:")
|
|
self.clear()
|
|
|
|
btn_submit = self.query_one("#btn-submit", Button)
|
|
btn_submit.label = "Erstellen"
|
|
btn_submit.variant = "success"
|
|
|
|
self.query_one("#btn-cancel", Button).add_class("hidden")
|
|
|
|
def get_data(self) -> dict:
|
|
return {
|
|
"record_id": self.edit_record_id,
|
|
"subdomain": self.query_one("#in-subdomain", Input).value.strip(),
|
|
"type": str(self.query_one("#sel-type", Select).value),
|
|
"content": self.query_one("#in-content", Input).value.strip(),
|
|
"prio": self.query_one("#in-prio", Input).value.strip(),
|
|
}
|
|
|
|
def clear(self) -> None:
|
|
self.query_one("#in-subdomain", Input).value = ""
|
|
self.query_one("#in-content", Input).value = ""
|
|
self.query_one("#in-prio", Input).value = ""
|