Changed Project Name to Pulsegate
+added README.MD
This commit is contained in:
233
README.md
Normal file
233
README.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# PulseGate
|
||||
|
||||
**PulseGate** ist ein neon-inspiriertes SSH Control Center als TUI-Anwendung für dein Terminal.
|
||||
Die App verwaltet gespeicherte Server, unterstützt Key- und Passwort-Login, Quick Commands, Statuschecks und Kitty/TERM-Fixes für saubere SSH-Sessions unter Hyprland/Kitty.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Server aus `config.yaml` laden
|
||||
- Server hinzufügen, bearbeiten und löschen
|
||||
- SSH-Verbindung per Key oder Passwort
|
||||
- Passwortspeicherung über den Linux-Keyring
|
||||
- Quick Commands pro ausgewähltem Server ausführen
|
||||
- Command-Output in der TUI anzeigen
|
||||
- SSH-Port-Statuscheck mit Refresh
|
||||
- Kitty-Fix über `TERM=xterm-256color`
|
||||
- Vollbild-TUI mit Navigation, Settings und Help-View
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### Arch / CachyOS
|
||||
|
||||
```bash
|
||||
sudo pacman -S go git sshpass gnome-keyring libsecret seahorse
|
||||
```
|
||||
|
||||
Optional, aber empfohlen:
|
||||
|
||||
```bash
|
||||
sudo pacman -S openssh
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Repository klonen oder Projektordner öffnen:
|
||||
|
||||
```bash
|
||||
cd ~/Projects/pulsegate
|
||||
```
|
||||
|
||||
Go-Abhängigkeiten installieren:
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
Falls du die Dependencies manuell installieren möchtest:
|
||||
|
||||
```bash
|
||||
go get github.com/charmbracelet/bubbletea
|
||||
go get github.com/charmbracelet/lipgloss
|
||||
go get github.com/charmbracelet/bubbles/textinput
|
||||
go get gopkg.in/yaml.v3
|
||||
go get github.com/99designs/keyring
|
||||
go get golang.org/x/term
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Lege im Projektordner eine `config.yaml` an:
|
||||
|
||||
```yaml
|
||||
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: Webhost
|
||||
host: 10.0.0.23
|
||||
user: administrator
|
||||
port: 22
|
||||
group: Homelab
|
||||
auth: key
|
||||
key: ~/.ssh/id_ed25519
|
||||
kitty_fix: true
|
||||
|
||||
quick_commands:
|
||||
- name: Docker PS
|
||||
command: docker ps
|
||||
|
||||
- name: Disk Usage
|
||||
command: df -h
|
||||
|
||||
- name: RAM Usage
|
||||
command: free -h
|
||||
|
||||
- name: Uptime
|
||||
command: uptime
|
||||
```
|
||||
|
||||
## Starten
|
||||
|
||||
Während der Entwicklung:
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
Als Binary bauen:
|
||||
|
||||
```bash
|
||||
go build -o pulsegate
|
||||
./pulsegate
|
||||
```
|
||||
|
||||
Optional global installieren:
|
||||
|
||||
```bash
|
||||
sudo cp pulsegate /usr/local/bin/
|
||||
pulsegate
|
||||
```
|
||||
|
||||
## Bedienung
|
||||
|
||||
| Taste | Funktion |
|
||||
|---|---|
|
||||
| `↑` / `↓` oder `k` / `j` | Auswahl bewegen |
|
||||
| `Enter` | Server verbinden oder Command ausführen |
|
||||
| `a` | Server hinzufügen |
|
||||
| `e` | Server bearbeiten |
|
||||
| `d` | Server löschen |
|
||||
| `c` | Quick Commands öffnen |
|
||||
| `r` | SSH-Port-Status aktualisieren |
|
||||
| `Tab` | Ansicht wechseln |
|
||||
| `s` | Settings öffnen |
|
||||
| `h` | Hilfe öffnen |
|
||||
| `q` | Zurück oder beenden |
|
||||
|
||||
## Passwort-Login
|
||||
|
||||
PulseGate speichert Passwörter nicht in der `config.yaml`, sondern über den Linux-Keyring.
|
||||
Beim ersten Verbindungsaufbau mit `auth: password` fragt PulseGate nach dem Passwort und speichert es unter der angegebenen `password_id`.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```yaml
|
||||
auth: password
|
||||
password_id: unraid-root
|
||||
```
|
||||
|
||||
Zum Anzeigen oder Löschen gespeicherter Passwörter kannst du Seahorse verwenden:
|
||||
|
||||
```bash
|
||||
seahorse
|
||||
```
|
||||
|
||||
Dort nach `pulsegate` oder der jeweiligen `password_id` suchen.
|
||||
|
||||
## SSH-Key-Login
|
||||
|
||||
Beispielkonfiguration:
|
||||
|
||||
```yaml
|
||||
auth: key
|
||||
key: ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Public Key auf den Server kopieren:
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server-ip
|
||||
```
|
||||
|
||||
## Kitty / TERM Fix
|
||||
|
||||
Wenn du über Kitty per SSH auf Server gehst, kann es zu Fehlern kommen wie:
|
||||
|
||||
```text
|
||||
Error opening terminal: xterm-kitty
|
||||
```
|
||||
|
||||
PulseGate kann für solche Server automatisch setzen:
|
||||
|
||||
```bash
|
||||
TERM=xterm-256color
|
||||
```
|
||||
|
||||
Aktivierung global:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
terminal:
|
||||
term: xterm-256color
|
||||
enable_kitty_fix: true
|
||||
```
|
||||
|
||||
Aktivierung pro Server:
|
||||
|
||||
```yaml
|
||||
kitty_fix: true
|
||||
```
|
||||
|
||||
## Screenshot austauschen
|
||||
|
||||
Der enthaltene Screenshot ist ein Beispielbild.
|
||||
Einen echten Screenshot kannst du unter Hyprland z. B. mit `grim` erstellen:
|
||||
|
||||
```bash
|
||||
grim -g "$(slurp)" pulsegate_screenshot.png
|
||||
```
|
||||
|
||||
Oder mit einem anderen Screenshot-Tool deiner Wahl.
|
||||
Lege die Datei anschließend neben die README:
|
||||
|
||||
```text
|
||||
README.md
|
||||
pulsegate_screenshot.png
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
Mögliche nächste Features:
|
||||
|
||||
- Suche und Filter mit `/`
|
||||
- Scrollbarer Command-Output
|
||||
- Servergruppen / Tags
|
||||
- Import aus `~/.ssh/config`
|
||||
- Passwort ändern/löschen direkt in der TUI
|
||||
- Themes wie `neon-green`, `neon-cyan`, `orange-authentik`
|
||||
- Health Checks mit SSH-Login-Test
|
||||
- Quick Commands pro Servergruppe
|
||||
@@ -3,7 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
|
||||
"pdev-ssh/internal/models"
|
||||
"pulsegate/internal/models"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/99designs/keyring"
|
||||
)
|
||||
|
||||
const serviceName = "pdev-ssh"
|
||||
const serviceName = "pulsegate"
|
||||
|
||||
func OpenKeyring() (keyring.Keyring, error) {
|
||||
return keyring.Open(keyring.Config{
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"pdev-ssh/internal/models"
|
||||
"pulsegate/internal/models"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
103
main.go
103
main.go
@@ -6,10 +6,12 @@ import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"pdev-ssh/internal/config"
|
||||
"pdev-ssh/internal/models"
|
||||
"pdev-ssh/internal/secret"
|
||||
"pulsegate/internal/config"
|
||||
"pulsegate/internal/models"
|
||||
"pulsegate/internal/secret"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -46,6 +48,7 @@ type model struct {
|
||||
commandOutput string
|
||||
commandTitle string
|
||||
commandError string
|
||||
serverStatus map[int]string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -104,6 +107,7 @@ func initialModel() model {
|
||||
servers: cfg.Servers,
|
||||
selected: 0,
|
||||
view: ViewServers,
|
||||
serverStatus: make(map[int]string),
|
||||
}
|
||||
|
||||
m.initAddInputs()
|
||||
@@ -111,7 +115,7 @@ func initialModel() model {
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
return checkAllServerStatus(m.servers)
|
||||
}
|
||||
|
||||
func (m *model) initAddInputs() {
|
||||
@@ -202,6 +206,11 @@ type quickCommandResultMsg struct {
|
||||
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
|
||||
@@ -219,6 +228,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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
|
||||
@@ -248,6 +265,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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()
|
||||
@@ -437,7 +457,7 @@ func (m model) View() string {
|
||||
right,
|
||||
)
|
||||
|
||||
footer := helpStyle.Render("↑/↓ Auswahl Enter Verbinden/Ausführen a Hinzufügen e Editieren d Löschen c Commands Tab Ansicht q Zurück/Beenden")
|
||||
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,
|
||||
@@ -450,7 +470,7 @@ func (m model) View() string {
|
||||
}
|
||||
|
||||
func (m model) renderHeader() string {
|
||||
title := " PDEV SSH Manager"
|
||||
title := " PulseGate"
|
||||
subtitle := fmt.Sprintf(
|
||||
"Homelab SSH Control Center • Server: %d • Theme: %s • TERM: %s",
|
||||
len(m.servers),
|
||||
@@ -509,6 +529,8 @@ func (m model) renderServerList() string {
|
||||
|
||||
for i, server := range m.servers {
|
||||
authIcon := ""
|
||||
status := m.serverStatus[i]
|
||||
statusRendered := statusStyle(status).Render(statusIcon(status))
|
||||
if server.Auth == "key" {
|
||||
authIcon = ""
|
||||
}
|
||||
@@ -518,8 +540,17 @@ func (m model) renderServerList() string {
|
||||
kitty = " kitty"
|
||||
}
|
||||
|
||||
statusText := ""
|
||||
if status == "online" {
|
||||
statusText = " ssh"
|
||||
} else if status == "offline" {
|
||||
statusText = " off"
|
||||
}
|
||||
|
||||
line := fmt.Sprintf(
|
||||
"%s %s %s@%s:%d [%s]%s",
|
||||
"%s%s %s %s %s@%s:%d [%s]%s",
|
||||
statusRendered,
|
||||
statusText,
|
||||
authIcon,
|
||||
server.Name,
|
||||
server.User,
|
||||
@@ -966,6 +997,64 @@ func runQuickCommand(server models.Server, settings models.Settings, quick model
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
|
||||
BIN
pulsegate_screenshot.png
Normal file
BIN
pulsegate_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Reference in New Issue
Block a user