Files
PulseGate/main.go
2026-05-03 04:02:00 +02:00

1157 lines
24 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"net"
"time"
"pulsegate/internal/config"
"pulsegate/internal/models"
"pulsegate/internal/secret"
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"
ViewAddServer viewMode = "add_server"
ViewEditServer viewMode = "edit_server"
ViewDeleteConfirm viewMode = "delete_confirm"
ViewCommands viewMode = "commands"
ViewCommandOutput viewMode = "command_output"
)
type model struct {
cfg models.AppConfig
servers []models.Server
selected int
view viewMode
width int
height int
err error
addInputs []textinput.Model
addFocus int
editIndex int
commandSelected int
commandOutput string
commandTitle string
commandError string
serverStatus map[int]string
}
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 {
configPath := getConfigPath()
if err := ensureConfigExists(configPath); err != nil {
return model{err: err}
}
cfg, err := config.LoadConfig(configPath)
if err != nil {
return model{err: err}
}
m := model{
cfg: cfg,
servers: cfg.Servers,
selected: 0,
view: ViewServers,
serverStatus: make(map[int]string),
}
m.initAddInputs()
return m
}
func (m model) Init() tea.Cmd {
return checkAllServerStatus(m.servers)
}
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(getConfigPath(), m.cfg)
if err != nil {
return err
}
m.selected = len(m.servers) - 1
m.initAddInputs()
m.view = ViewServers
return nil
}
type quickCommandResultMsg struct {
Title string
Output string
Error string
}
type serverStatusMsg struct {
Index int
Status string
}
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 quickCommandResultMsg:
m.commandTitle = msg.Title
m.commandOutput = msg.Output
m.commandError = msg.Error
m.view = ViewCommandOutput
return m, nil
case serverStatusMsg:
if m.serverStatus == nil {
m.serverStatus = make(map[int]string)
}
m.serverStatus[msg.Index] = msg.Status
return m, nil
case tea.KeyMsg:
// Command Output offen lassen, bis du ihn wegdrückst
if m.view == ViewCommandOutput {
switch msg.String() {
case "q", "esc", "enter":
m.view = ViewCommands
return m, nil
case "ctrl+c":
return m, tea.Quit
}
}
// Add/Edit Formular
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 "r":
return m, checkAllServerStatus(m.servers)
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
}
// Delete Confirm
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
}
}
// Normale Navigation
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 "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 "c":
if len(m.servers) > 0 {
m.view = ViewCommands
m.commandSelected = 0
}
return m, nil
case "s":
m.view = ViewSettings
return m, nil
case "h":
m.view = ViewHelp
return m, nil
case "up", "k":
if m.view == ViewServers && m.selected > 0 {
m.selected--
}
if m.view == ViewCommands && m.commandSelected > 0 {
m.commandSelected--
}
case "down", "j":
if m.view == ViewServers && m.selected < len(m.servers)-1 {
m.selected++
}
if m.view == ViewCommands && m.commandSelected < len(m.cfg.QuickCommands)-1 {
m.commandSelected++
}
case "enter":
if m.view == ViewServers && len(m.servers) > 0 {
server := m.servers[m.selected]
return m, connectSSH(server, m.cfg.Settings)
}
if m.view == ViewCommands && len(m.servers) > 0 && len(m.cfg.QuickCommands) > 0 {
server := m.servers[m.selected]
quick := m.cfg.QuickCommands[m.commandSelected]
return m, runQuickCommand(server, m.cfg.Settings, quick)
}
}
}
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()
case ViewAddServer:
right = m.renderAddServerContent()
case ViewEditServer:
right = m.renderEditServerContent()
case ViewDeleteConfirm:
right = m.renderDeleteConfirmContent()
case ViewCommands:
right = m.renderCommandsContent()
case ViewCommandOutput:
right = m.renderCommandOutputContent()
default:
right = m.renderServerList()
}
body := lipgloss.JoinHorizontal(
lipgloss.Top,
m.renderNavigation(),
" ",
right,
)
footer := helpStyle.Render("↑/↓ Auswahl Enter Verbinden a Hinzufügen e Editieren d Löschen c Commands r Status refresh Tab Ansicht q Zurück/Beenden")
content := lipgloss.JoinVertical(
lipgloss.Left,
header,
body,
footer,
)
return baseStyle.Render(content)
}
func (m model) renderHeader() string {
title := "󰣀 PulseGate"
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},
{"󰐕", "Add Server", ViewAddServer},
{"󰢹", "Settings", ViewSettings},
{"󰋖", "Help", ViewHelp},
{"󰘳", "Commands", ViewCommands},
}
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 := "󰌾"
status := m.serverStatus[i]
statusRendered := statusStyle(status).Render(statusIcon(status))
if server.Auth == "key" {
authIcon = "󰌆"
}
kitty := ""
if server.KittyFix {
kitty = " 󰄛 kitty"
}
statusText := ""
if status == "online" {
statusText = " ssh"
} else if status == "offline" {
statusText = " off"
}
line := fmt.Sprintf(
"%s%s %s %s %s@%s:%d [%s]%s",
statusRendered,
statusText,
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 (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(getConfigPath(), 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(getConfigPath(), 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) renderCommandOutputContent() string {
lines := []string{
badgeStyle.Render(" COMMAND OUTPUT "),
"",
selectedStyle.Render(m.commandTitle),
"",
}
if m.commandError != "" {
lines = append(lines, warnStyle().Render("Fehler: "+m.commandError))
lines = append(lines, "")
}
output := strings.TrimSpace(m.commandOutput)
if output == "" {
output = "Keine Ausgabe."
}
lines = append(lines, output)
lines = append(lines, "")
lines = append(lines, mutedStyle.Render("q / Esc / Enter: zurück zu Quick Commands"))
return panelStyle.
Width(max(m.width-36, 60)).
Height(max(m.height-9, 14)).
Render(strings.Join(lines, "\n"))
}
func (m model) renderCommandsContent() string {
lines := []string{
badgeStyle.Render(" QUICK COMMANDS "),
"",
}
if len(m.servers) == 0 {
lines = append(lines, mutedStyle.Render("Kein Server ausgewählt."))
} else {
server := m.servers[m.selected]
lines = append(lines, fmt.Sprintf("Server: %s %s@%s:%d", server.Name, server.User, server.Host, server.Port))
lines = append(lines, "")
}
if len(m.cfg.QuickCommands) == 0 {
lines = append(lines, mutedStyle.Render("Keine quick_commands in config.yaml gefunden."))
} else {
for i, cmd := range m.cfg.QuickCommands {
line := fmt.Sprintf("%s → %s", cmd.Name, cmd.Command)
if i == m.commandSelected {
lines = append(lines, selectedStyle.Render("> "+line))
} else {
lines = append(lines, normalStyle.Render(" "+line))
}
}
}
lines = append(lines, "")
lines = append(lines, mutedStyle.Render("Enter: Command ausführen ↑/↓ Auswahl q: zurück"))
return panelStyle.
Width(max(m.width-36, 60)).
Height(max(m.height-9, 14)).
Render(strings.Join(lines, "\n"))
}
func runQuickCommand(server models.Server, settings models.Settings, quick models.QuickCommand) tea.Cmd {
return func() tea.Msg {
title := fmt.Sprintf("%s auf %s", quick.Name, server.Name)
args := []string{
"-p", strconv.Itoa(server.Port),
}
if settings.Terminal.EnableKittyFix && server.KittyFix {
args = append(args, "-t")
}
var cmd *exec.Cmd
if server.Auth == "key" {
if server.Key != "" {
args = append(args, "-i", expandHome(server.Key))
}
args = append(args, server.User+"@"+server.Host, quick.Command)
cmd = exec.Command("ssh", args...)
cmd.Env = buildSSHEnv(server, settings)
} else if server.Auth == "password" {
password, err := secret.GetPassword(server.PasswordID)
if err != nil {
return quickCommandResultMsg{
Title: title,
Output: "",
Error: fmt.Sprintf("kein passwort gespeichert für %s", server.PasswordID),
}
}
sshArgs := []string{
"-e",
"ssh",
"-p", strconv.Itoa(server.Port),
server.User + "@" + server.Host,
quick.Command,
}
cmd = exec.Command("sshpass", sshArgs...)
cmd.Env = append(buildSSHEnv(server, settings), "SSHPASS="+password)
} else {
return quickCommandResultMsg{
Title: title,
Output: "",
Error: fmt.Sprintf("unbekannte auth methode: %s", server.Auth),
}
}
output, err := cmd.CombinedOutput()
errorText := ""
if err != nil {
errorText = err.Error()
}
return quickCommandResultMsg{
Title: title,
Output: string(output),
Error: errorText,
}
}
}
func checkAllServerStatus(servers []models.Server) tea.Cmd {
cmds := make([]tea.Cmd, 0, len(servers))
for i, server := range servers {
cmds = append(cmds, checkServerStatus(i, server))
}
return tea.Batch(cmds...)
}
func checkServerStatus(index int, server models.Server) tea.Cmd {
return func() tea.Msg {
port := server.Port
if port == 0 {
port = 22
}
addr := fmt.Sprintf("%s:%d", server.Host, port)
conn, err := net.DialTimeout("tcp", addr, 1500*time.Millisecond)
if err != nil {
return serverStatusMsg{
Index: index,
Status: "offline",
}
}
_ = conn.Close()
return serverStatusMsg{
Index: index,
Status: "online",
}
}
}
func statusIcon(status string) string {
switch status {
case "online":
return "●"
case "offline":
return "○"
default:
return "◌"
}
}
func statusStyle(status string) lipgloss.Style {
switch status {
case "online":
return lipgloss.NewStyle().Foreground(green).Bold(true)
case "offline":
return lipgloss.NewStyle().Foreground(gray)
default:
return lipgloss.NewStyle().Foreground(dimText)
}
}
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 getConfigPath() string {
configDir, err := os.UserConfigDir()
if err != nil {
home, _ := os.UserHomeDir()
return home + "/.config/pulsegate/config.yaml"
}
return configDir + "/pulsegate/config.yaml"
}
func ensureConfigExists(path string) error {
if _, err := os.Stat(path); err == nil {
return nil
}
dir := strings.TrimSuffix(path, "/config.yaml")
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
example := `settings:
theme: neon-green
terminal:
term: xterm-256color
enable_kitty_fix: true
servers:
- name: Example Server
host: 10.0.0.10
user: root
port: 22
group: Homelab
auth: key
key: ~/.ssh/id_ed25519
kitty_fix: true
quick_commands:
- name: Disk Usage
command: df -h
- name: RAM Usage
command: free -h
- name: Uptime
command: uptime
`
return os.WriteFile(path, []byte(example), 0600)
}
func main() {
p := tea.NewProgram(
initialModel(),
tea.WithAltScreen(),
)
if _, err := p.Run(); err != nil {
fmt.Println("Fehler:", err)
os.Exit(1)
}
}