Innitial Commit

+added Base Project Version 0.0.1
This commit is contained in:
2026-05-03 01:13:59 +02:00
commit 321f4a5bad
8 changed files with 745 additions and 0 deletions

24
config.yaml Normal file
View File

@@ -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

35
go.mod Normal file
View File

@@ -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
)

78
go.sum Normal file
View File

@@ -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=

20
internal/config/config.go Normal file
View File

@@ -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
}

28
internal/models/server.go Normal file
View File

@@ -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"`
}

View File

@@ -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),
})
}

54
internal/sshclient/ssh.go Normal file
View File

@@ -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()
}

461
main.go Normal file
View File

@@ -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)
}
}