Add entry editing functionality with auto-save; enhance input validation and navigation
This commit is contained in:
parent
d477328bea
commit
5a117fb624
1 changed files with 244 additions and 3 deletions
|
@ -6,10 +6,12 @@ This module contains the main application class and entry point function.
|
|||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widgets import Header, Footer, Static, DataTable
|
||||
from textual.widgets import Header, Footer, Static, DataTable, Input, Checkbox, Label
|
||||
from textual.binding import Binding
|
||||
from textual.reactive import reactive
|
||||
from rich.text import Text
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from .core.parser import HostsParser
|
||||
from .core.models import HostsFile
|
||||
|
@ -89,6 +91,29 @@ class HostsManagerApp(App):
|
|||
}
|
||||
|
||||
/* DataTable row styling - colors are now handled via Rich Text objects */
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#entry-edit-form {
|
||||
height: auto;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
#entry-edit-form Label {
|
||||
margin-bottom: 1;
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#entry-edit-form Input {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#entry-edit-form Checkbox {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
|
@ -99,10 +124,14 @@ class HostsManagerApp(App):
|
|||
Binding("n", "sort_by_hostname", "Sort by Hostname"),
|
||||
Binding("c", "config", "Config"),
|
||||
Binding("ctrl+e", "toggle_edit_mode", "Edit Mode"),
|
||||
Binding("e", "edit_entry", "Edit Entry", show=False),
|
||||
Binding("space", "toggle_entry", "Toggle Entry", show=False),
|
||||
Binding("ctrl+s", "save_file", "Save", show=False),
|
||||
Binding("shift+up", "move_entry_up", "Move Up", show=False),
|
||||
Binding("shift+down", "move_entry_down", "Move Down", show=False),
|
||||
Binding("escape", "exit_edit_entry", "Exit Edit", show=False),
|
||||
Binding("tab", "next_field", "Next Field", show=False),
|
||||
Binding("shift+tab", "prev_field", "Prev Field", show=False),
|
||||
("ctrl+c", "quit", "Quit"),
|
||||
]
|
||||
|
||||
|
@ -110,6 +139,7 @@ class HostsManagerApp(App):
|
|||
hosts_file: reactive[HostsFile] = reactive(HostsFile())
|
||||
selected_entry_index: reactive[int] = reactive(0)
|
||||
edit_mode: reactive[bool] = reactive(False)
|
||||
entry_edit_mode: reactive[bool] = reactive(False)
|
||||
sort_column: reactive[str] = reactive("") # "ip" or "hostname"
|
||||
sort_ascending: reactive[bool] = reactive(True)
|
||||
|
||||
|
@ -137,6 +167,15 @@ class HostsManagerApp(App):
|
|||
right_pane.border_title = "Entry Details"
|
||||
with right_pane:
|
||||
yield Static("", id="entry-details")
|
||||
with Vertical(id="entry-edit-form", classes="hidden"):
|
||||
yield Label("IP Address:")
|
||||
yield Input(id="ip-input", placeholder="Enter IP address")
|
||||
yield Label("Hostname:")
|
||||
yield Input(id="hostname-input", placeholder="Enter hostname")
|
||||
yield Label("Comment:")
|
||||
yield Input(id="comment-input", placeholder="Enter comment (optional)")
|
||||
yield Label("Active:")
|
||||
yield Checkbox(id="active-checkbox", value=True)
|
||||
yield right_pane
|
||||
|
||||
yield Static("", classes="status-bar", id="status")
|
||||
|
@ -322,7 +361,19 @@ class HostsManagerApp(App):
|
|||
|
||||
def update_entry_details(self) -> None:
|
||||
"""Update the right pane with selected entry details."""
|
||||
if self.entry_edit_mode:
|
||||
self.update_edit_form()
|
||||
else:
|
||||
self.update_details_display()
|
||||
|
||||
def update_details_display(self) -> None:
|
||||
"""Update the static details display."""
|
||||
details_widget = self.query_one("#entry-details", Static)
|
||||
edit_form = self.query_one("#entry-edit-form")
|
||||
|
||||
# Show details, hide edit form
|
||||
details_widget.remove_class("hidden")
|
||||
edit_form.add_class("hidden")
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
details_widget.update("No entries loaded")
|
||||
|
@ -374,6 +425,31 @@ class HostsManagerApp(App):
|
|||
|
||||
details_widget.update("\n".join(details_lines))
|
||||
|
||||
def update_edit_form(self) -> None:
|
||||
"""Update the edit form with current entry values."""
|
||||
details_widget = self.query_one("#entry-details", Static)
|
||||
edit_form = self.query_one("#entry-edit-form")
|
||||
|
||||
# Hide details, show edit form
|
||||
details_widget.add_class("hidden")
|
||||
edit_form.remove_class("hidden")
|
||||
|
||||
if not self.hosts_file.entries or self.selected_entry_index >= len(self.hosts_file.entries):
|
||||
return
|
||||
|
||||
entry = self.hosts_file.entries[self.selected_entry_index]
|
||||
|
||||
# Update form fields with current entry values
|
||||
ip_input = self.query_one("#ip-input", Input)
|
||||
hostname_input = self.query_one("#hostname-input", Input)
|
||||
comment_input = self.query_one("#comment-input", Input)
|
||||
active_checkbox = self.query_one("#active-checkbox", Checkbox)
|
||||
|
||||
ip_input.value = entry.ip_address
|
||||
hostname_input.value = ', '.join(entry.hostnames)
|
||||
comment_input.value = entry.comment or ""
|
||||
active_checkbox.value = entry.is_active
|
||||
|
||||
def update_status(self, message: str = "") -> None:
|
||||
"""Update the status bar."""
|
||||
status_widget = self.query_one("#status", Static)
|
||||
|
@ -437,7 +513,7 @@ class HostsManagerApp(App):
|
|||
def action_help(self) -> None:
|
||||
"""Show help information."""
|
||||
# For now, just update the status with help info
|
||||
self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config")
|
||||
self.update_status("Help: ↑/↓ Navigate, r Reload, q Quit, h Help, c Config, e Edit")
|
||||
|
||||
def action_config(self) -> None:
|
||||
"""Show configuration modal."""
|
||||
|
@ -449,7 +525,6 @@ class HostsManagerApp(App):
|
|||
|
||||
self.push_screen(ConfigModal(self.config), handle_config_result)
|
||||
|
||||
|
||||
def action_sort_by_ip(self) -> None:
|
||||
"""Sort entries by IP address, toggle ascending/descending."""
|
||||
# Toggle sort direction if already sorting by IP
|
||||
|
@ -512,6 +587,168 @@ class HostsManagerApp(App):
|
|||
else:
|
||||
self.update_status(f"Error entering edit mode: {message}")
|
||||
|
||||
def action_edit_entry(self) -> None:
|
||||
"""Enter edit mode for the selected entry."""
|
||||
if not self.edit_mode:
|
||||
self.update_status("❌ Cannot edit entry: Application is in read-only mode. Press 'Ctrl+E' to enable edit mode.")
|
||||
return
|
||||
|
||||
if not self.hosts_file.entries:
|
||||
self.update_status("No entries to edit")
|
||||
return
|
||||
|
||||
if self.selected_entry_index >= len(self.hosts_file.entries):
|
||||
self.update_status("Invalid entry selected")
|
||||
return
|
||||
|
||||
entry = self.hosts_file.entries[self.selected_entry_index]
|
||||
if entry.is_default_entry():
|
||||
self.update_status("❌ Cannot edit system default entry")
|
||||
return
|
||||
|
||||
self.entry_edit_mode = True
|
||||
self.update_entry_details()
|
||||
|
||||
# Focus on the IP address input field
|
||||
ip_input = self.query_one("#ip-input", Input)
|
||||
ip_input.focus()
|
||||
|
||||
self.update_status("Editing entry - Use Tab/Shift+Tab to navigate, ESC to exit")
|
||||
|
||||
def action_exit_edit_entry(self) -> None:
|
||||
"""Exit entry edit mode and return focus to the entries table."""
|
||||
if self.entry_edit_mode:
|
||||
self.entry_edit_mode = False
|
||||
self.update_entry_details()
|
||||
|
||||
# Return focus to the entries table
|
||||
table = self.query_one("#entries-table", DataTable)
|
||||
table.focus()
|
||||
|
||||
self.update_status("Exited entry edit mode")
|
||||
|
||||
def action_next_field(self) -> None:
|
||||
"""Move to the next field in edit mode."""
|
||||
if not self.entry_edit_mode:
|
||||
return
|
||||
|
||||
# Get all input fields in order
|
||||
fields = [
|
||||
self.query_one("#ip-input", Input),
|
||||
self.query_one("#hostname-input", Input),
|
||||
self.query_one("#comment-input", Input),
|
||||
self.query_one("#active-checkbox", Checkbox)
|
||||
]
|
||||
|
||||
# Find currently focused field and move to next
|
||||
for i, field in enumerate(fields):
|
||||
if field.has_focus:
|
||||
next_field = fields[(i + 1) % len(fields)]
|
||||
next_field.focus()
|
||||
break
|
||||
|
||||
def action_prev_field(self) -> None:
|
||||
"""Move to the previous field in edit mode."""
|
||||
if not self.entry_edit_mode:
|
||||
return
|
||||
|
||||
# Get all input fields in order
|
||||
fields = [
|
||||
self.query_one("#ip-input", Input),
|
||||
self.query_one("#hostname-input", Input),
|
||||
self.query_one("#comment-input", Input),
|
||||
self.query_one("#active-checkbox", Checkbox)
|
||||
]
|
||||
|
||||
# Find currently focused field and move to previous
|
||||
for i, field in enumerate(fields):
|
||||
if field.has_focus:
|
||||
prev_field = fields[(i - 1) % len(fields)]
|
||||
prev_field.focus()
|
||||
break
|
||||
|
||||
def on_key(self, event) -> None:
|
||||
"""Handle key events to override default tab behavior in edit mode."""
|
||||
if self.entry_edit_mode and event.key == "tab":
|
||||
# Prevent default tab behavior and use our custom navigation
|
||||
event.prevent_default()
|
||||
self.action_next_field()
|
||||
elif self.entry_edit_mode and event.key == "shift+tab":
|
||||
# Prevent default shift+tab behavior and use our custom navigation
|
||||
event.prevent_default()
|
||||
self.action_prev_field()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle input field changes and auto-save."""
|
||||
if not self.entry_edit_mode or not self.edit_mode:
|
||||
return
|
||||
|
||||
if event.input.id in ["ip-input", "hostname-input", "comment-input"]:
|
||||
self.save_entry_changes()
|
||||
|
||||
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
||||
"""Handle checkbox changes and auto-save."""
|
||||
if not self.entry_edit_mode or not self.edit_mode:
|
||||
return
|
||||
|
||||
if event.checkbox.id == "active-checkbox":
|
||||
self.save_entry_changes()
|
||||
|
||||
def save_entry_changes(self) -> None:
|
||||
"""Save the current entry changes."""
|
||||
if not self.hosts_file.entries or self.selected_entry_index >= len(self.hosts_file.entries):
|
||||
return
|
||||
|
||||
entry = self.hosts_file.entries[self.selected_entry_index]
|
||||
|
||||
# Get values from form fields
|
||||
ip_input = self.query_one("#ip-input", Input)
|
||||
hostname_input = self.query_one("#hostname-input", Input)
|
||||
comment_input = self.query_one("#comment-input", Input)
|
||||
active_checkbox = self.query_one("#active-checkbox", Checkbox)
|
||||
|
||||
# Validate IP address
|
||||
try:
|
||||
ipaddress.ip_address(ip_input.value.strip())
|
||||
except ValueError:
|
||||
self.update_status("❌ Invalid IP address")
|
||||
return
|
||||
|
||||
# Validate hostname(s)
|
||||
hostnames = [h.strip() for h in hostname_input.value.split(',') if h.strip()]
|
||||
if not hostnames:
|
||||
self.update_status("❌ At least one hostname is required")
|
||||
return
|
||||
|
||||
hostname_pattern = re.compile(
|
||||
r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
|
||||
)
|
||||
|
||||
for hostname in hostnames:
|
||||
if not hostname_pattern.match(hostname):
|
||||
self.update_status(f"❌ Invalid hostname: {hostname}")
|
||||
return
|
||||
|
||||
# Update the entry
|
||||
entry.ip_address = ip_input.value.strip()
|
||||
entry.hostnames = hostnames
|
||||
entry.comment = comment_input.value.strip() or None
|
||||
entry.is_active = active_checkbox.value
|
||||
|
||||
# Save to file
|
||||
success, message = self.manager.save_hosts_file(self.hosts_file)
|
||||
if success:
|
||||
# Update the table display
|
||||
self.populate_entries_table()
|
||||
# Restore cursor position
|
||||
table = self.query_one("#entries-table", DataTable)
|
||||
display_index = self.actual_index_to_display_index(self.selected_entry_index)
|
||||
if table.row_count > 0 and display_index < table.row_count:
|
||||
table.move_cursor(row=display_index)
|
||||
self.update_status("Entry saved successfully")
|
||||
else:
|
||||
self.update_status(f"❌ Error saving entry: {message}")
|
||||
|
||||
def action_toggle_entry(self) -> None:
|
||||
"""Toggle the active state of the selected entry."""
|
||||
if not self.edit_mode:
|
||||
|
@ -616,6 +853,10 @@ class HostsManagerApp(App):
|
|||
|
||||
def action_quit(self) -> None:
|
||||
"""Quit the application."""
|
||||
# If in entry edit mode, exit it first
|
||||
if self.entry_edit_mode:
|
||||
self.action_exit_edit_entry()
|
||||
|
||||
# If in edit mode, exit it first
|
||||
if self.edit_mode:
|
||||
self.manager.exit_edit_mode()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue