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

View File

@@ -1,24 +1,15 @@
settings: settings:
theme: neon-green theme: neon-green
terminal: terminal:
term: xterm-256color term: xterm-256color
enable_kitty_fix: true enable_kitty_fix: true
servers: servers:
- name: Unraid - name: Unraid
host: 10.0.0.15 host: 10.0.0.15
user: root user: root
port: 22 port: 22
group: Homelab group: Homelab
auth: password auth: password
password_id: unraid-root key: ""
kitty_fix: true password_id: unraid-root
kitty_fix: true
- name: Test Server
host: 10.0.0.23
user: administrator
port: 22
group: Homelab
auth: password
password_id: test-server-admin
kitty_fix: true

17
go.mod
View File

@@ -5,22 +5,27 @@ go 1.26.2
require ( require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.2 // indirect github.com/99designs/keyring v1.2.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v1.0.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mtibben/percent v0.2.1 // indirect github.com/mtibben/percent v0.2.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect

22
go.sum
View File

@@ -2,20 +2,38 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -31,12 +49,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=

View File

@@ -17,4 +17,13 @@ func LoadConfig(path string) (models.AppConfig, error) {
err = yaml.Unmarshal(data, &cfg) err = yaml.Unmarshal(data, &cfg)
return cfg, err return cfg, err
}
func SaveConfig(path string, cfg models.AppConfig) error {
data, err := yaml.Marshal(&cfg)
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
} }

369
main.go
View File

@@ -13,15 +13,19 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/textinput"
"golang.org/x/term" "golang.org/x/term"
) )
type viewMode string type viewMode string
const ( const (
ViewServers viewMode = "servers" ViewServers viewMode = "servers"
ViewSettings viewMode = "settings" ViewSettings viewMode = "settings"
ViewHelp viewMode = "help" ViewHelp viewMode = "help"
ViewAddServer viewMode = "add_server"
ViewEditServer viewMode = "edit_server"
ViewDeleteConfirm viewMode = "delete_confirm"
) )
type model struct { type model struct {
@@ -32,6 +36,10 @@ type model struct {
width int width int
height int height int
err error err error
addInputs []textinput.Model
addFocus int
editIndex int
} }
var ( var (
@@ -85,18 +93,103 @@ func initialModel() model {
return model{err: err} return model{err: err}
} }
return model{ m := model{
cfg: cfg, cfg: cfg,
servers: cfg.Servers, servers: cfg.Servers,
selected: 0, selected: 0,
view: ViewServers, view: ViewServers,
} }
m.initAddInputs()
return m
} }
func (m model) Init() tea.Cmd { func (m model) Init() tea.Cmd {
return nil 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) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.err != nil { if m.err != nil {
return m, nil return m, nil
@@ -108,6 +201,72 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height m.height = msg.Height
case tea.KeyMsg: 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() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
@@ -122,6 +281,26 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "tab": case "tab":
m.view = nextView(m.view) 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": case "s":
m.view = ViewSettings m.view = ViewSettings
@@ -182,6 +361,12 @@ func (m model) View() string {
right = m.renderSettingsContent() right = m.renderSettingsContent()
case ViewHelp: case ViewHelp:
right = m.renderHelpContent() right = m.renderHelpContent()
case ViewAddServer:
right = m.renderAddServerContent()
case ViewEditServer:
right = m.renderEditServerContent()
case ViewDeleteConfirm:
right = m.renderDeleteConfirmContent()
default: default:
right = m.renderServerList() right = m.renderServerList()
} }
@@ -193,7 +378,7 @@ func (m model) View() string {
right, 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( content := lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
@@ -233,6 +418,7 @@ func (m model) renderNavigation() string {
view viewMode view viewMode
}{ }{
{"󰒋", "Server", ViewServers}, {"󰒋", "Server", ViewServers},
{"󰐕", "Add Server", ViewAddServer},
{"󰢹", "Settings", ViewSettings}, {"󰢹", "Settings", ViewSettings},
{"󰋖", "Help", ViewHelp}, {"󰋖", "Help", ViewHelp},
} }
@@ -448,6 +634,179 @@ func max(a, b int) int {
return b 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() { func main() {
p := tea.NewProgram( p := tea.NewProgram(
initialModel(), initialModel(),