feat: initial commit for ipv64 DNS TUI client
- 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
This commit is contained in:
1 parent
fc1f055b11
commit
d15c927f55
11 files changed
+603
-1
No files matched your search
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/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 = ""
|
||||
Reference in new issue
Block a user