diff --git a/internal/pages/edit_server.go b/internal/pages/edit_server.go new file mode 100644 index 0000000..fa2782d --- /dev/null +++ b/internal/pages/edit_server.go @@ -0,0 +1,7 @@ +package pages + +type SelectEditServerPageMsg struct{} + +type EditServerPageMsg struct { + ServerName string +} diff --git a/internal/services/config.go b/internal/services/config.go index 0aadac9..e5370bb 100644 --- a/internal/services/config.go +++ b/internal/services/config.go @@ -69,6 +69,21 @@ func (c *ConfigService) AddServer(name string, s Server) error { return c.flush() } +func (c *ConfigService) EditServer(oldName, newName string, s Server) error { + if !c.HasServer(oldName) { + return fmt.Errorf("server %q not found", oldName) + } + if newName != oldName && c.HasServer(newName) { + return fmt.Errorf("server %q already exists", newName) + } + if newName != oldName { + delete(c.servers, oldName) + } + c.servers[newName] = s + return c.flush() +} + + func (c *ConfigService) RemoveServer(name string) error { if !c.HasServer(name) { return fmt.Errorf("server %q not found", name) diff --git a/internal/tui/form.go b/internal/tui/form.go index 9e788ac..c6d7e16 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -3,6 +3,8 @@ package tui import ( "strings" + "filepass/internal/services" + "charm.land/bubbles/v2/textinput" ) @@ -76,3 +78,32 @@ func (f addServerForm) focusPrev() addServerForm { f.focusField(prev) return f } + +// newEditServerForm builds a pre-filled form for editing an existing server. +// The name field is pre-populated with the server key; all other fields with +// the existing server values. +func newEditServerForm(name string, s services.Server) addServerForm { + mkInput := func(placeholder string, limit int, value string) textinput.Model { + ti := textinput.New() + ti.Prompt = "" + ti.CharLimit = limit + ti.SetWidth(40) + ti.Placeholder = placeholder + ti.SetValue(value) + return ti + } + + port := s.Port + if port == "22" { + port = "" + } + + f := addServerForm{} + f.inputs[fieldName] = mkInput("production-web", 64, name) + f.inputs[fieldHost] = mkInput("192.168.1.1 or example.com", 253, s.Host) + f.inputs[fieldUser] = mkInput("deploy", 64, s.User) + f.inputs[fieldPrivateKey] = mkInput("~/.ssh/id_rsa", 512, s.PrivateKey) + f.inputs[fieldPort] = mkInput("22 (optional)", 5, port) + f.inputs[fieldName].Focus() + return f +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 41cb78c..c3c9027 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -19,6 +19,8 @@ const ( pageSend pageCleanAll pageRemoveServer + pageSelectEditServer + pageEditServer ) type TUIInterface struct { @@ -49,6 +51,8 @@ type TUIInterface struct { FileOpLoading bool FileOpErr error FileOpSuccess string + // edit server page + EditingServer string // original name of server being edited // clean all confirmation page CleanInput textinput.Model CleanOpLoading bool diff --git a/internal/tui/update.go b/internal/tui/update.go index 5ae33bb..5670e8f 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -27,6 +27,12 @@ type serverRemovedMsg struct { servers map[string]services.Server } +type serverEditedMsg struct { + oldName string + newName string + servers map[string]services.Server +} + type clearFlashMsg struct{} type storageFilesMsg struct { @@ -182,6 +188,30 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.FileOpSuccess = "" return m, nil + case pages.SelectEditServerPageMsg: + m.Page = pageSelectEditServer + m.Selected = 0 + return m, nil + + case pages.EditServerPageMsg: + name := msg.ServerName + srv := m.Servers[name] + m.Page = pageEditServer + m.EditingServer = name + m.Form = newEditServerForm(name, srv) + m.FormErr = "" + return m, nil + + case serverEditedMsg: + m.Servers = msg.servers + m.ServerNames = sortedServerNames(msg.servers) + m.NoServers = len(msg.servers) == 0 + m.Page = pageConfig + m.MenuItems = pages.ConfigMenuItems() + m.Selected = 0 + m.FlashMsg = "✓ \"" + msg.newName + "\" updated." + return m, clearFlashAfter(2 * time.Second) + case pages.RemoveServerPageMsg: m.Page = pageRemoveServer m.Selected = 0 @@ -289,6 +319,12 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.Page == pageRemoveServer { return m.updateRemoveServer(msg) } + if m.Page == pageSelectEditServer { + return m.updateSelectEditServer(msg) + } + if m.Page == pageEditServer { + return m.updateAddServer(msg) + } switch msg.String() { case "up", "k": @@ -311,6 +347,8 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, func() tea.Msg { return pages.AddServerPageMsg{} } case "server": return m, func() tea.Msg { return pages.SelectServerPageMsg{} } + case "edit": + return m, func() tea.Msg { return pages.SelectEditServerPageMsg{} } case "remove": return m, func() tea.Msg { return pages.RemoveServerPageMsg{} } // TODO: "edit" @@ -534,6 +572,31 @@ func (m TUIInterface) updateRemoveServer(msg tea.KeyPressMsg) (tea.Model, tea.Cm return m, nil } +func (m TUIInterface) updateSelectEditServer(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + last := len(m.ServerNames) - 1 + switch msg.String() { + case "up", "k": + if m.Selected > 0 { + m.Selected-- + } + case "down", "j": + if m.Selected < last { + m.Selected++ + } + case "enter": + if m.Selected >= 0 && m.Selected < len(m.ServerNames) { + name := m.ServerNames[m.Selected] + return m, func() tea.Msg { return pages.EditServerPageMsg{ServerName: name} } + } + case "ctrl+c": + m.Quitting = true + return m, tea.Quit + case "esc": + return m, func() tea.Msg { return pages.ConfigPageMsg{} } + } + return m, nil +} + func (m TUIInterface) updateCleanAll(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.CleanOpLoading { if msg.String() == "ctrl+c" { @@ -655,6 +718,9 @@ func (m TUIInterface) updateAddServerPaste(text string) (tea.Model, tea.Cmd) { } func (m TUIInterface) submitAddServer() (tea.Model, tea.Cmd) { + if m.Page == pageEditServer { + return m.submitEditServer() + } f := m.Form name := strings.TrimSpace(f.inputs[fieldName].Value()) @@ -683,3 +749,34 @@ func (m TUIInterface) submitAddServer() (tea.Model, tea.Cmd) { return serverAddedMsg{name: name, servers: m.Services.Config.Servers()} } } + +func (m TUIInterface) submitEditServer() (tea.Model, tea.Cmd) { + f := m.Form + newName := strings.TrimSpace(f.inputs[fieldName].Value()) + + if !f.canSave() { + return m, nil + } + + if newName != m.EditingServer && m.Services.Config.HasServer(newName) { + m.FormErr = "✗ \"" + newName + "\" already exists." + return m, nil + } + + s := services.Server{ + Host: strings.TrimSpace(f.inputs[fieldHost].Value()), + User: strings.TrimSpace(f.inputs[fieldUser].Value()), + PrivateKey: strings.TrimSpace(f.inputs[fieldPrivateKey].Value()), + Port: strings.TrimSpace(f.inputs[fieldPort].Value()), + } + + if err := m.Services.Config.EditServer(m.EditingServer, newName, s); err != nil { + m.FormErr = "✗ " + err.Error() + return m, nil + } + + oldName := m.EditingServer + return m, func() tea.Msg { + return serverEditedMsg{oldName: oldName, newName: newName, servers: m.Services.Config.Servers()} + } +} diff --git a/internal/tui/view.go b/internal/tui/view.go index 243d4b5..fae2228 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -25,6 +25,10 @@ func (m TUIInterface) subtitle() string { return "Configuration" case pageAddServer: return "Add Server" + case pageSelectEditServer: + return "Edit Server" + case pageEditServer: + return "Edit — " + m.EditingServer case pageSelectServer: return "Select Server" case pageServerActions: @@ -61,8 +65,10 @@ func (m TUIInterface) View() tea.View { var body string switch m.Page { - case pageAddServer: + case pageAddServer, pageEditServer: body = m.viewAddServer() + case pageSelectEditServer: + body = m.viewSelectEditServer() case pageSelectServer: body = m.viewSelectServer() case pageServerActions: @@ -90,7 +96,7 @@ func (m TUIInterface) View() tea.View { var footerStr string switch m.Page { - case pageAddServer: + case pageAddServer, pageEditServer: footerStr = footerHint("tab/↑↓", "navigate") + footerSep() + footerHint("enter", "confirm") + @@ -118,6 +124,12 @@ func (m TUIInterface) View() tea.View { footerHint("enter", "confirm") + footerSep() + footerHint("esc", "back") + case pageSelectEditServer: + footerStr = footerHint("↑↓", "navigate") + + footerSep() + + footerHint("enter", "edit") + + footerSep() + + footerHint("esc", "back") case pageRemoveServer: footerStr = footerHint("↑↓", "navigate") + footerSep() + @@ -272,6 +284,17 @@ func (m TUIInterface) viewSend() string { return lipgloss.JoinVertical(lipgloss.Left, crumb, queryLine, list) } +func (m TUIInterface) viewSelectEditServer() string { + if len(m.ServerNames) == 0 { + return styles.StatusWarnStyle.Render("⚠ No servers configured.") + } + var rows []string + for i, name := range m.ServerNames { + rows = append(rows, styles.ServerRowStyle(i == m.Selected, name)) + } + return lipgloss.JoinVertical(lipgloss.Left, rows...) +} + func (m TUIInterface) viewRemoveServer() string { if len(m.ServerNames) == 0 { return styles.StatusWarnStyle.Render("⚠ No servers configured.")