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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"pdev-ssh/internal/models"
|
"pulsegate/internal/models"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/99designs/keyring"
|
"github.com/99designs/keyring"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceName = "pdev-ssh"
|
const serviceName = "pulsegate"
|
||||||
|
|
||||||
func OpenKeyring() (keyring.Keyring, error) {
|
func OpenKeyring() (keyring.Keyring, error) {
|
||||||
return keyring.Open(keyring.Config{
|
return keyring.Open(keyring.Config{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"pdev-ssh/internal/models"
|
"pulsegate/internal/models"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|||||||
103
main.go
103
main.go
@@ -6,10 +6,12 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"pdev-ssh/internal/config"
|
"pulsegate/internal/config"
|
||||||
"pdev-ssh/internal/models"
|
"pulsegate/internal/models"
|
||||||
"pdev-ssh/internal/secret"
|
"pulsegate/internal/secret"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -46,6 +48,7 @@ type model struct {
|
|||||||
commandOutput string
|
commandOutput string
|
||||||
commandTitle string
|
commandTitle string
|
||||||
commandError string
|
commandError string
|
||||||
|
serverStatus map[int]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -104,6 +107,7 @@ func initialModel() model {
|
|||||||
servers: cfg.Servers,
|
servers: cfg.Servers,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
view: ViewServers,
|
view: ViewServers,
|
||||||
|
serverStatus: make(map[int]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.initAddInputs()
|
m.initAddInputs()
|
||||||
@@ -111,7 +115,7 @@ func initialModel() model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
return nil
|
return checkAllServerStatus(m.servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) initAddInputs() {
|
func (m *model) initAddInputs() {
|
||||||
@@ -202,6 +206,11 @@ type quickCommandResultMsg struct {
|
|||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serverStatusMsg struct {
|
||||||
|
Index int
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if m.err != nil {
|
if m.err != nil {
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -219,6 +228,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.view = ViewCommandOutput
|
m.view = ViewCommandOutput
|
||||||
return m, nil
|
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:
|
case tea.KeyMsg:
|
||||||
|
|
||||||
// Command Output offen lassen, bis du ihn wegdrückst
|
// 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.addFocus = (m.addFocus + 1) % len(m.addInputs)
|
||||||
m.addInputs[m.addFocus].Focus()
|
m.addInputs[m.addFocus].Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case "r":
|
||||||
|
return m, checkAllServerStatus(m.servers)
|
||||||
|
|
||||||
case "shift+tab", "up":
|
case "shift+tab", "up":
|
||||||
m.addInputs[m.addFocus].Blur()
|
m.addInputs[m.addFocus].Blur()
|
||||||
@@ -437,7 +457,7 @@ func (m model) View() string {
|
|||||||
right,
|
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(
|
content := lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
@@ -450,7 +470,7 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) renderHeader() string {
|
func (m model) renderHeader() string {
|
||||||
title := " PDEV SSH Manager"
|
title := " PulseGate"
|
||||||
subtitle := fmt.Sprintf(
|
subtitle := fmt.Sprintf(
|
||||||
"Homelab SSH Control Center • Server: %d • Theme: %s • TERM: %s",
|
"Homelab SSH Control Center • Server: %d • Theme: %s • TERM: %s",
|
||||||
len(m.servers),
|
len(m.servers),
|
||||||
@@ -509,6 +529,8 @@ func (m model) renderServerList() string {
|
|||||||
|
|
||||||
for i, server := range m.servers {
|
for i, server := range m.servers {
|
||||||
authIcon := ""
|
authIcon := ""
|
||||||
|
status := m.serverStatus[i]
|
||||||
|
statusRendered := statusStyle(status).Render(statusIcon(status))
|
||||||
if server.Auth == "key" {
|
if server.Auth == "key" {
|
||||||
authIcon = ""
|
authIcon = ""
|
||||||
}
|
}
|
||||||
@@ -518,8 +540,17 @@ func (m model) renderServerList() string {
|
|||||||
kitty = " kitty"
|
kitty = " kitty"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusText := ""
|
||||||
|
if status == "online" {
|
||||||
|
statusText = " ssh"
|
||||||
|
} else if status == "offline" {
|
||||||
|
statusText = " off"
|
||||||
|
}
|
||||||
|
|
||||||
line := fmt.Sprintf(
|
line := fmt.Sprintf(
|
||||||
"%s %s %s@%s:%d [%s]%s",
|
"%s%s %s %s %s@%s:%d [%s]%s",
|
||||||
|
statusRendered,
|
||||||
|
statusText,
|
||||||
authIcon,
|
authIcon,
|
||||||
server.Name,
|
server.Name,
|
||||||
server.User,
|
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 {
|
func (m model) renderDeleteConfirmContent() string {
|
||||||
if len(m.servers) == 0 {
|
if len(m.servers) == 0 {
|
||||||
return panelStyle.Render("Kein Server zum Löschen vorhanden.")
|
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