commit 321f4a5bad28a04331f7ca15e6c74ff7f9875f75 Author: Pepe44DEV Date: Sun May 3 01:13:59 2026 +0200 Innitial Commit +added Base Project Version 0.0.1 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..f74894e --- /dev/null +++ b/config.yaml @@ -0,0 +1,24 @@ +settings: + 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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0833d6f --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module pdev-ssh + +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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // 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/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/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/mtibben/percent v0.2.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e445fe9 --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +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/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/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/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/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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +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= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +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/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/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= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..61ebaf0 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,20 @@ +package config + +import ( + "os" + + "pdev-ssh/internal/models" + "gopkg.in/yaml.v3" +) + +func LoadConfig(path string) (models.AppConfig, error) { + var cfg models.AppConfig + + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + + err = yaml.Unmarshal(data, &cfg) + return cfg, err +} \ No newline at end of file diff --git a/internal/models/server.go b/internal/models/server.go new file mode 100644 index 0000000..e55516c --- /dev/null +++ b/internal/models/server.go @@ -0,0 +1,28 @@ +package models + +type TerminalSettings struct { + Term string `yaml:"term"` + EnableKittyFix bool `yaml:"enable_kitty_fix"` +} + +type Settings struct { + Theme string `yaml:"theme"` + Terminal TerminalSettings `yaml:"terminal"` +} + +type Server struct { + Name string `yaml:"name"` + Host string `yaml:"host"` + User string `yaml:"user"` + Port int `yaml:"port"` + Group string `yaml:"group"` + Auth string `yaml:"auth"` + Key string `yaml:"key"` + PasswordID string `yaml:"password_id"` + KittyFix bool `yaml:"kitty_fix"` +} + +type AppConfig struct { + Settings Settings `yaml:"settings"` + Servers []Server `yaml:"servers"` +} \ No newline at end of file diff --git a/internal/secret/keyring.go b/internal/secret/keyring.go new file mode 100644 index 0000000..eeb7f48 --- /dev/null +++ b/internal/secret/keyring.go @@ -0,0 +1,45 @@ +package secret + +import ( + "fmt" + + "github.com/99designs/keyring" +) + +const serviceName = "pdev-ssh" + +func OpenKeyring() (keyring.Keyring, error) { + return keyring.Open(keyring.Config{ + ServiceName: serviceName, + }) +} + +func GetPassword(id string) (string, error) { + kr, err := OpenKeyring() + if err != nil { + return "", err + } + + item, err := kr.Get(id) + if err != nil { + return "", err + } + + return string(item.Data), nil +} + +func SavePassword(id string, password string) error { + if id == "" { + return fmt.Errorf("password_id fehlt") + } + + kr, err := OpenKeyring() + if err != nil { + return err + } + + return kr.Set(keyring.Item{ + Key: id, + Data: []byte(password), + }) +} \ No newline at end of file diff --git a/internal/sshclient/ssh.go b/internal/sshclient/ssh.go new file mode 100644 index 0000000..94cb95d --- /dev/null +++ b/internal/sshclient/ssh.go @@ -0,0 +1,54 @@ +package sshclient + +import ( + "fmt" + "os" + + "pdev-ssh/internal/models" + + "golang.org/x/crypto/ssh" +) + +func ConnectWithPassword(server models.Server, password string) error { + config := &ssh.ClientConfig{ + User: server.User, + Auth: []ssh.AuthMethod{ + ssh.Password(password), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + addr := fmt.Sprintf("%s:%d", server.Host, server.Port) + + client, err := ssh.Dial("tcp", addr, config) + if err != nil { + return err + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return err + } + defer session.Close() + + session.Stdout = os.Stdout + session.Stderr = os.Stderr + session.Stdin = os.Stdin + + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + if err := session.RequestPty("xterm-256color", 40, 120, modes); err != nil { + return err + } + + if err := session.Shell(); err != nil { + return err + } + + return session.Wait() +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..2ccfc1c --- /dev/null +++ b/main.go @@ -0,0 +1,461 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "pdev-ssh/internal/config" + "pdev-ssh/internal/models" + "pdev-ssh/internal/secret" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +type viewMode string + +const ( + ViewServers viewMode = "servers" + ViewSettings viewMode = "settings" + ViewHelp viewMode = "help" +) + +type model struct { + cfg models.AppConfig + servers []models.Server + selected int + view viewMode + width int + height int + err error +} + +var ( + green = lipgloss.Color("#00ff99") + cyan = lipgloss.Color("#33ccff") + gray = lipgloss.Color("#777777") + text = lipgloss.Color("#d7ffe9") + dimText = lipgloss.Color("#8aa99b") + panelBg = lipgloss.Color("#07110d") + border = lipgloss.Color("#00aa66") + warn = lipgloss.Color("#ffaa00") + + baseStyle = lipgloss.NewStyle(). + Foreground(text) + + headerStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(green). + Border(lipgloss.RoundedBorder()). + BorderForeground(border). + Padding(1, 2) + + panelStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(border). + Padding(1, 2) + + selectedStyle = lipgloss.NewStyle(). + Foreground(green). + Bold(true) + + normalStyle = lipgloss.NewStyle(). + Foreground(text) + + mutedStyle = lipgloss.NewStyle(). + Foreground(gray) + + helpStyle = lipgloss.NewStyle(). + Foreground(dimText) + + badgeStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#001a10")). + Background(green). + Bold(true). + Padding(0, 1) +) + +func initialModel() model { + cfg, err := config.LoadConfig("config.yaml") + if err != nil { + return model{err: err} + } + + return model{ + cfg: cfg, + servers: cfg.Servers, + selected: 0, + view: ViewServers, + } +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.err != nil { + return m, nil + } + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + + case "q": + if m.view != ViewServers { + m.view = ViewServers + return m, nil + } + return m, tea.Quit + + case "tab": + m.view = nextView(m.view) + + case "s": + m.view = ViewSettings + + case "h": + m.view = ViewHelp + + case "up", "k": + if m.view == ViewServers && m.selected > 0 { + m.selected-- + } + + case "down", "j": + if m.view == ViewServers && m.selected < len(m.servers)-1 { + m.selected++ + } + + case "enter": + if m.view == ViewServers && len(m.servers) > 0 { + server := m.servers[m.selected] + return m, connectSSH(server, m.cfg.Settings) + } + } + } + + return m, nil +} + +func nextView(v viewMode) viewMode { + switch v { + case ViewServers: + return ViewSettings + case ViewSettings: + return ViewHelp + default: + return ViewServers + } +} + +func (m model) View() string { + if m.err != nil { + return "Fehler: " + m.err.Error() + } + + if m.width == 0 { + m.width = 100 + } + if m.height == 0 { + m.height = 30 + } + + header := m.renderHeader() + + var right string + switch m.view { + case ViewServers: + right = m.renderServerList() + case ViewSettings: + right = m.renderSettingsContent() + case ViewHelp: + right = m.renderHelpContent() + default: + right = m.renderServerList() + } + + body := lipgloss.JoinHorizontal( + lipgloss.Top, + m.renderNavigation(), + " ", + right, + ) + + footer := helpStyle.Render("↑/↓ Auswahl Enter Verbinden Tab Ansicht wechseln s Settings h Hilfe q Zurück/Beenden") + + content := lipgloss.JoinVertical( + lipgloss.Left, + header, + body, + footer, + ) + + return baseStyle.Render(content) +} + +func (m model) renderHeader() string { + title := "󰣀 PDEV SSH Manager" + subtitle := fmt.Sprintf( + "Homelab SSH Control Center • Server: %d • Theme: %s • TERM: %s", + len(m.servers), + m.cfg.Settings.Theme, + m.cfg.Settings.Terminal.Term, + ) + + return headerStyle. + Width(max(m.width-6, 60)). + Render(title + "\n" + mutedStyle.Render(subtitle)) +} + +/*func (m model) renderServerView() string { + left := m.renderNavigation() + right := m.renderServerList() + + return lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right) +}*/ + +func (m model) renderNavigation() string { + items := []struct { + icon string + name string + view viewMode + }{ + {"󰒋", "Server", ViewServers}, + {"󰢹", "Settings", ViewSettings}, + {"󰋖", "Help", ViewHelp}, + } + + var lines []string + lines = append(lines, badgeStyle.Render(" NAVIGATION ")) + lines = append(lines, "") + + for _, item := range items { + line := fmt.Sprintf("%s %s", item.icon, item.name) + if m.view == item.view { + lines = append(lines, selectedStyle.Render("> "+line)) + } else { + lines = append(lines, normalStyle.Render(" "+line)) + } + } + + return panelStyle. + Width(24). + Height(max(m.height-9, 12)). + Render(strings.Join(lines, "\n")) +} + +func (m model) renderServerList() string { + var lines []string + lines = append(lines, badgeStyle.Render(" SERVERS ")) + lines = append(lines, "") + + for i, server := range m.servers { + authIcon := "󰌾" + if server.Auth == "key" { + authIcon = "󰌆" + } + + kitty := "" + if server.KittyFix { + kitty = " 󰄛 kitty" + } + + line := fmt.Sprintf( + "%s %s %s@%s:%d [%s]%s", + authIcon, + server.Name, + server.User, + server.Host, + server.Port, + server.Group, + kitty, + ) + + if i == m.selected { + lines = append(lines, selectedStyle.Render("> "+line)) + } else { + lines = append(lines, normalStyle.Render(" "+line)) + } + } + + if len(m.servers) == 0 { + lines = append(lines, mutedStyle.Render("Keine Server in config.yaml gefunden.")) + } + + width := max(m.width-36, 50) + + return panelStyle. + Width(width). + Height(max(m.height-9, 12)). + Render(strings.Join(lines, "\n")) +} + +func (m model) renderSettingsContent() string { + lines := []string{ + badgeStyle.Render(" SETTINGS "), + "", + selectedStyle.Render("Terminal"), + fmt.Sprintf(" Kitty Fix global: %v", m.cfg.Settings.Terminal.EnableKittyFix), + fmt.Sprintf(" TERM Override: %s", m.cfg.Settings.Terminal.Term), + "", + selectedStyle.Render("Hinweis"), + " Wenn Kitty Fix aktiv ist, startet SSH mit TERM=xterm-256color.", + " Das verhindert auf Ubuntu/Debian oft:", + " 'Error opening terminal: xterm-kitty'", + "", + mutedStyle.Render("Später bauen wir hier Toggle-Optionen mit Enter ein."), + } + + return panelStyle. + Width(max(m.width-6, 70)). + Height(max(m.height-9, 14)). + Render(strings.Join(lines, "\n")) +} + +func (m model) renderHelpContent() string { + lines := []string{ + badgeStyle.Render(" HELP "), + "", + "Enter Ausgewählten Server verbinden", + "↑/↓ / j/k Server auswählen", + "Tab Ansicht wechseln", + "s Settings öffnen", + "h Hilfe öffnen", + "q Zurück oder Beenden", + "", + selectedStyle.Render("Kitty Fix"), + "Wenn du per Kitty auf Ubuntu SSH nutzt und nano/clear Fehler machen:", + "TERM=xterm-256color hilft meistens.", + } + + return panelStyle. + Width(max(m.width-6, 70)). + Height(max(m.height-9, 14)). + Render(strings.Join(lines, "\n")) +} + +func connectSSH(server models.Server, settings models.Settings) tea.Cmd { + switch server.Auth { + case "key": + args := []string{ + "-p", strconv.Itoa(server.Port), + } + + if server.Key != "" { + args = append(args, "-i", expandHome(server.Key)) + } + + args = append(args, server.User+"@"+server.Host) + + cmd := exec.Command("ssh", args...) + cmd.Env = buildSSHEnv(server, settings) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return tea.ExecProcess(cmd, func(err error) tea.Msg { + return err + }) + + case "password": + password, err := secret.GetPassword(server.PasswordID) + + if err != nil { + fmt.Printf("\nPasswort für %s@%s: ", server.User, server.Host) + + pw, inputErr := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + + if inputErr != nil { + return func() tea.Msg { + return inputErr + } + } + + password = string(pw) + + saveErr := secret.SavePassword(server.PasswordID, password) + if saveErr != nil { + fmt.Println("Fehler beim Speichern:", saveErr) + } else { + fmt.Println("Passwort gespeichert.") + } + } + + args := []string{ + "-e", + "ssh", + "-p", strconv.Itoa(server.Port), + server.User + "@" + server.Host, + } + + cmd := exec.Command("sshpass", args...) + cmd.Env = append(buildSSHEnv(server, settings), "SSHPASS="+password) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return tea.ExecProcess(cmd, func(err error) tea.Msg { + return err + }) + + default: + return func() tea.Msg { + return fmt.Errorf("unbekannte auth methode: %s", server.Auth) + } + } +} + +func buildSSHEnv(server models.Server, settings models.Settings) []string { + env := os.Environ() + + if settings.Terminal.EnableKittyFix && server.KittyFix { + termValue := settings.Terminal.Term + if termValue == "" { + termValue = "xterm-256color" + } + + env = append(env, "TERM="+termValue) + } + + return env +} + +func expandHome(path string) string { + if len(path) >= 2 && path[:2] == "~/" { + home, _ := os.UserHomeDir() + return home + path[1:] + } + return path +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func main() { + p := tea.NewProgram( + initialModel(), + tea.WithAltScreen(), + ) + + if _, err := p.Run(); err != nil { + fmt.Println("Fehler:", err) + os.Exit(1) + } +} \ No newline at end of file