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

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