From d2b61046c603d16f75fb8afba88c481f0b1f3c40 Mon Sep 17 00:00:00 2001 From: Pepe44DEV Date: Sun, 3 May 2026 01:38:16 +0200 Subject: [PATCH] Added Add Host Functionality +added function to add server from tui +added function to eddit and delete server from tui --- config.yaml | 35 ++-- go.mod | 17 +- go.sum | 22 +++ internal/config/config.go | 9 + main.go | 369 +++++++++++++++++++++++++++++++++++++- 5 files changed, 419 insertions(+), 33 deletions(-) diff --git a/config.yaml b/config.yaml index f74894e..93897b3 100644 --- a/config.yaml +++ b/config.yaml @@ -1,24 +1,15 @@ settings: - theme: neon-green - terminal: - term: xterm-256color - enable_kitty_fix: true - + theme: neon-green + terminal: + term: xterm-256color + enable_kitty_fix: true servers: - - name: Unraid - host: 10.0.0.15 - user: root - port: 22 - group: Homelab - auth: password - 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 \ No newline at end of file + - name: Unraid + host: 10.0.0.15 + user: root + port: 22 + group: Homelab + auth: password + key: "" + password_id: unraid-root + kitty_fix: true diff --git a/go.mod b/go.mod index 0833d6f..68a500f 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,27 @@ go 1.26.2 require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // 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/charmbracelet/bubbles v1.0.0 // 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/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // 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/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // 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-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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect diff --git a/go.sum b/go.sum index e445fe9..7f938dd 100644 --- a/go.sum +++ b/go.sum @@ -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/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= 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/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/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/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/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 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.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/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/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/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 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/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.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/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/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 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.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/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= diff --git a/internal/config/config.go b/internal/config/config.go index 61ebaf0..01877f3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,4 +17,13 @@ func LoadConfig(path string) (models.AppConfig, error) { err = yaml.Unmarshal(data, &cfg) 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) } \ No newline at end of file diff --git a/main.go b/main.go index 2ccfc1c..92e0af0 100644 --- a/main.go +++ b/main.go @@ -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(),