Added Add Host Functionality

+added function to add server from tui
+added function to eddit and delete server from tui
This commit is contained in:
2026-05-03 01:38:16 +02:00
parent 321f4a5bad
commit d2b61046c6
5 changed files with 419 additions and 33 deletions

369
main.go
View File

@@ -13,15 +13,19 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/textinput"
"golang.org/x/term"
)
type viewMode string
const (
ViewServers viewMode = "servers"
ViewSettings viewMode = "settings"
ViewHelp viewMode = "help"
ViewServers viewMode = "servers"
ViewSettings viewMode = "settings"
ViewHelp viewMode = "help"
ViewAddServer viewMode = "add_server"
ViewEditServer viewMode = "edit_server"
ViewDeleteConfirm viewMode = "delete_confirm"
)
type model struct {
@@ -32,6 +36,10 @@ type model struct {
width int
height int
err error
addInputs []textinput.Model
addFocus int
editIndex int
}
var (
@@ -85,18 +93,103 @@ func initialModel() model {
return model{err: err}
}
return model{
m := model{
cfg: cfg,
servers: cfg.Servers,
selected: 0,
view: ViewServers,
}
m.initAddInputs()
return m
}
func (m model) Init() tea.Cmd {
return nil
}
func (m *model) initAddInputs() {
labels := []string{
"Name",
"Host",
"User",
"Port",
"Group",
"Auth (key/password)",
"Key Path",
"Password ID",
}
m.addInputs = make([]textinput.Model, len(labels))
for i, label := range labels {
input := textinput.New()
input.Placeholder = label
input.CharLimit = 120
input.Width = 40
switch label {
case "Port":
input.SetValue("22")
case "Auth (key/password)":
input.SetValue("key")
case "Group":
input.SetValue("Homelab")
}
if i == 0 {
input.Focus()
}
m.addInputs[i] = input
}
}
func (m *model) saveNewServer() error {
port, err := strconv.Atoi(m.addInputs[3].Value())
if err != nil {
port = 22
}
server := models.Server{
Name: m.addInputs[0].Value(),
Host: m.addInputs[1].Value(),
User: m.addInputs[2].Value(),
Port: port,
Group: m.addInputs[4].Value(),
Auth: m.addInputs[5].Value(),
Key: m.addInputs[6].Value(),
PasswordID: m.addInputs[7].Value(),
KittyFix: true,
}
if server.Name == "" || server.Host == "" || server.User == "" {
return fmt.Errorf("name, host und user sind pflichtfelder")
}
if server.Auth == "" {
server.Auth = "key"
}
if server.Auth == "password" && server.PasswordID == "" {
server.PasswordID = strings.ToLower(server.Name) + "-" + server.User
}
m.cfg.Servers = append(m.cfg.Servers, server)
m.servers = m.cfg.Servers
err = config.SaveConfig("config.yaml", m.cfg)
if err != nil {
return err
}
m.selected = len(m.servers) - 1
m.initAddInputs()
m.view = ViewServers
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.err != nil {
return m, nil
@@ -108,6 +201,72 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
case tea.KeyMsg:
if m.view == ViewAddServer || m.view == ViewEditServer {
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "esc", "q":
m.view = ViewServers
return m, nil
case "tab", "down":
m.addInputs[m.addFocus].Blur()
m.addFocus = (m.addFocus + 1) % len(m.addInputs)
m.addInputs[m.addFocus].Focus()
return m, nil
case "shift+tab", "up":
m.addInputs[m.addFocus].Blur()
m.addFocus--
if m.addFocus < 0 {
m.addFocus = len(m.addInputs) - 1
}
m.addInputs[m.addFocus].Focus()
return m, nil
case "enter":
if m.addFocus < len(m.addInputs)-1 {
m.addInputs[m.addFocus].Blur()
m.addFocus++
m.addInputs[m.addFocus].Focus()
return m, nil
}
var err error
if m.view == ViewAddServer {
err = m.saveNewServer()
} else {
err = m.saveEditedServer()
}
if err != nil {
m.err = err
}
return m, nil
}
var cmd tea.Cmd
m.addInputs[m.addFocus], cmd = m.addInputs[m.addFocus].Update(msg)
return m, cmd
}
if m.view == ViewDeleteConfirm {
switch msg.String() {
case "y", "Y", "j":
err := m.deleteSelectedServer()
if err != nil {
m.err = err
}
return m, nil
case "n", "N", "esc", "q":
m.view = ViewServers
return m, nil
}
}
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
@@ -122,6 +281,26 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "tab":
m.view = nextView(m.view)
case "a":
m.view = ViewAddServer
m.addFocus = 0
m.initAddInputs()
return m, nil
case "e":
if m.view == ViewServers && len(m.servers) > 0 {
m.editIndex = m.selected
m.view = ViewEditServer
m.loadServerIntoForm(m.servers[m.selected])
}
return m, nil
case "d":
if m.view == ViewServers && len(m.servers) > 0 {
m.view = ViewDeleteConfirm
}
return m, nil
case "s":
m.view = ViewSettings
@@ -182,6 +361,12 @@ func (m model) View() string {
right = m.renderSettingsContent()
case ViewHelp:
right = m.renderHelpContent()
case ViewAddServer:
right = m.renderAddServerContent()
case ViewEditServer:
right = m.renderEditServerContent()
case ViewDeleteConfirm:
right = m.renderDeleteConfirmContent()
default:
right = m.renderServerList()
}
@@ -193,7 +378,7 @@ func (m model) View() string {
right,
)
footer := helpStyle.Render("↑/↓ Auswahl Enter Verbinden Tab Ansicht wechseln s Settings h Hilfe q Zurück/Beenden")
footer := helpStyle.Render("↑/↓ Auswahl Enter Verbinden a Hinzufügen e Editieren d Löschen Tab Ansicht q Zurück/Beenden")
content := lipgloss.JoinVertical(
lipgloss.Left,
@@ -233,6 +418,7 @@ func (m model) renderNavigation() string {
view viewMode
}{
{"󰒋", "Server", ViewServers},
{"󰐕", "Add Server", ViewAddServer},
{"󰢹", "Settings", ViewSettings},
{"󰋖", "Help", ViewHelp},
}
@@ -448,6 +634,179 @@ func max(a, b int) int {
return b
}
func (m *model) loadServerIntoForm(server models.Server) {
m.initAddInputs()
m.addInputs[0].SetValue(server.Name)
m.addInputs[1].SetValue(server.Host)
m.addInputs[2].SetValue(server.User)
m.addInputs[3].SetValue(strconv.Itoa(server.Port))
m.addInputs[4].SetValue(server.Group)
m.addInputs[5].SetValue(server.Auth)
m.addInputs[6].SetValue(server.Key)
m.addInputs[7].SetValue(server.PasswordID)
m.addFocus = 0
m.addInputs[0].Focus()
}
func (m *model) saveEditedServer() error {
if m.editIndex < 0 || m.editIndex >= len(m.servers) {
return fmt.Errorf("ungültiger server index")
}
port, err := strconv.Atoi(m.addInputs[3].Value())
if err != nil {
port = 22
}
server := models.Server{
Name: m.addInputs[0].Value(),
Host: m.addInputs[1].Value(),
User: m.addInputs[2].Value(),
Port: port,
Group: m.addInputs[4].Value(),
Auth: m.addInputs[5].Value(),
Key: m.addInputs[6].Value(),
PasswordID: m.addInputs[7].Value(),
KittyFix: true,
}
if server.Name == "" || server.Host == "" || server.User == "" {
return fmt.Errorf("name, host und user sind pflichtfelder")
}
if server.Auth == "" {
server.Auth = "key"
}
if server.Auth == "password" && server.PasswordID == "" {
server.PasswordID = strings.ToLower(server.Name) + "-" + server.User
}
m.cfg.Servers[m.editIndex] = server
m.servers = m.cfg.Servers
err = config.SaveConfig("config.yaml", m.cfg)
if err != nil {
return err
}
m.selected = m.editIndex
m.view = ViewServers
m.initAddInputs()
return nil
}
func (m *model) deleteSelectedServer() error {
if m.selected < 0 || m.selected >= len(m.servers) {
return fmt.Errorf("kein gültiger server ausgewählt")
}
m.cfg.Servers = append(
m.cfg.Servers[:m.selected],
m.cfg.Servers[m.selected+1:]...,
)
m.servers = m.cfg.Servers
if m.selected >= len(m.servers) && m.selected > 0 {
m.selected--
}
err := config.SaveConfig("config.yaml", m.cfg)
if err != nil {
return err
}
m.view = ViewServers
return nil
}
func (m model) renderEditServerContent() string {
return m.renderServerFormContent(" EDIT SERVER ", "Server bearbeiten")
}
func (m model) renderAddServerContent() string {
return m.renderServerFormContent(" ADD SERVER ", "Neuen SSH-Server hinzufügen")
}
func (m model) renderServerFormContent(badge string, title string) string {
lines := []string{
badgeStyle.Render(badge),
"",
title,
"",
}
labels := []string{
"Name",
"Host",
"User",
"Port",
"Group",
"Auth",
"Key Path",
"Password ID",
}
for i, input := range m.addInputs {
label := labels[i]
if i == m.addFocus {
lines = append(lines, selectedStyle.Render("> "+label))
} else {
lines = append(lines, normalStyle.Render(" "+label))
}
lines = append(lines, " "+input.View())
lines = append(lines, "")
}
lines = append(lines, mutedStyle.Render("Enter: nächstes Feld / speichern Tab: weiter Esc/q: abbrechen"))
lines = append(lines, mutedStyle.Render("Auth: key oder password"))
return panelStyle.
Width(max(m.width-36, 60)).
Height(max(m.height-9, 18)).
Render(strings.Join(lines, "\n"))
}
func (m model) renderDeleteConfirmContent() string {
if len(m.servers) == 0 {
return panelStyle.Render("Kein Server zum Löschen vorhanden.")
}
server := m.servers[m.selected]
lines := []string{
badgeStyle.Render(" DELETE SERVER "),
"",
warnStyle().Render("Diesen Server wirklich löschen?"),
"",
fmt.Sprintf("Name: %s", server.Name),
fmt.Sprintf("Host: %s@%s:%d", server.User, server.Host, server.Port),
fmt.Sprintf("Group: %s", server.Group),
"",
selectedStyle.Render("y / j = löschen"),
mutedStyle.Render("n / q / Esc = abbrechen"),
}
return panelStyle.
Width(max(m.width-36, 60)).
Height(max(m.height-9, 14)).
Render(strings.Join(lines, "\n"))
}
func warnStyle() lipgloss.Style {
return lipgloss.NewStyle().
Foreground(warn).
Bold(true)
}
func main() {
p := tea.NewProgram(
initialModel(),