Project Initialisation

added Project files
This commit is contained in:
2026-05-03 21:26:10 +02:00
commit dcafc1e7c1
12 changed files with 2029 additions and 0 deletions

254
cmd/pulsegate-gui/main.go Normal file
View File

@@ -0,0 +1,254 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"pulsegate-gui/internal/config"
"pulsegate-gui/internal/models"
)
type apiServer struct {
configPath string
}
func main() {
configPath := config.DefaultPath()
if len(os.Args) > 1 {
configPath = os.Args[1]
}
if err := config.EnsureExists(configPath); err != nil {
log.Fatal(err)
}
app := apiServer{configPath: configPath}
mux := http.NewServeMux()
mux.HandleFunc("GET /api/config", app.getConfig)
mux.HandleFunc("GET /api/capabilities", app.capabilities)
mux.HandleFunc("PUT /api/config", app.putConfig)
mux.HandleFunc("POST /api/ssh-command/", app.sshCommand)
mux.HandleFunc("POST /api/connect/", app.connectSSH)
mux.Handle("/", http.FileServer(http.Dir(webDir())))
addr := "127.0.0.1:8090"
log.Printf("PulseGate GUI läuft auf http://%s", addr)
log.Printf("Config: %s", configPath)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func (a apiServer) getConfig(w http.ResponseWriter, r *http.Request) {
cfg, err := config.Load(a.configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, cfg)
}
func (a apiServer) putConfig(w http.ResponseWriter, r *http.Request) {
var cfg models.AppConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if err := config.Save(a.configPath, cfg); err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, cfg)
}
func (a apiServer) capabilities(w http.ResponseWriter, r *http.Request) {
terminal, ok := detectTerminal()
writeJSON(w, http.StatusOK, map[string]any{
"terminal_available": ok,
"terminal": terminal,
})
}
func (a apiServer) sshCommand(w http.ResponseWriter, r *http.Request) {
cfg, err := config.Load(a.configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
server, err := serverFromRequest(r, cfg, "/api/ssh-command/")
if err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{
"command": strings.Join(sshArgs(server), " "),
})
}
func (a apiServer) connectSSH(w http.ResponseWriter, r *http.Request) {
cfg, err := config.Load(a.configPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
server, err := serverFromRequest(r, cfg, "/api/connect/")
if err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
command, args, err := terminalCommand(server)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
cmd := exec.Command(command, args...)
cmd.Env = buildSSHEnv(server, cfg.Settings)
if err := cmd.Start(); err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{
"status": "started",
"command": strings.Join(sshArgs(server), " "),
})
}
func serverFromRequest(r *http.Request, cfg models.AppConfig, prefix string) (models.Server, error) {
rawIndex := strings.TrimPrefix(r.URL.Path, prefix)
index, err := strconv.Atoi(rawIndex)
if err != nil || index < 0 || index >= len(cfg.Servers) {
return models.Server{}, fmt.Errorf("ungültiger server index")
}
return cfg.Servers[index], nil
}
func sshArgs(server models.Server) []string {
args := []string{"ssh", "-p", strconv.Itoa(server.Port)}
if server.Key != "" && server.Auth == "key" {
args = append(args, "-i", expandHome(server.Key))
}
args = append(args, server.User+"@"+server.Host)
return args
}
func terminalCommand(server models.Server) (string, []string, error) {
ssh := strings.Join(quoteArgs(sshArgs(server)), " ")
title := "PulseGate - " + server.Name
holdCommand := ssh + "; printf '\\nSSH beendet. Enter zum Schließen...'; read _"
candidates := []struct {
name string
args []string
}{
{"kitty", []string{"--title", title, "sh", "-lc", holdCommand}},
{"konsole", []string{"--new-tab", "-p", "tabtitle=" + title, "-e", "sh", "-lc", holdCommand}},
{"gnome-terminal", []string{"--title", title, "--", "sh", "-lc", holdCommand}},
{"xfce4-terminal", []string{"--title", title, "--command", "sh -lc " + shellQuote(holdCommand)}},
{"alacritty", []string{"--title", title, "-e", "sh", "-lc", holdCommand}},
{"xterm", []string{"-T", title, "-e", "sh", "-lc", holdCommand}},
}
for _, candidate := range candidates {
if path, err := exec.LookPath(candidate.name); err == nil {
return path, candidate.args, nil
}
}
return "", nil, fmt.Errorf("kein unterstützter Terminal-Emulator gefunden")
}
func detectTerminal() (string, bool) {
for _, name := range []string{"kitty", "konsole", "gnome-terminal", "xfce4-terminal", "alacritty", "xterm"} {
if path, err := exec.LookPath(name); err == nil {
return path, true
}
}
return "", false
}
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 strings.HasPrefix(path, "~/") {
home, _ := os.UserHomeDir()
return filepath.Join(home, strings.TrimPrefix(path, "~/"))
}
return path
}
func quoteArgs(args []string) []string {
quoted := make([]string, len(args))
for i, arg := range args {
quoted[i] = shellQuote(arg)
}
return quoted
}
func shellQuote(value string) string {
if value == "" {
return "''"
}
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, err error) {
writeJSON(w, status, map[string]string{"error": err.Error()})
}
func webDir() string {
candidates := []string{
"web",
filepath.Join("..", "..", "web"),
}
for _, candidate := range candidates {
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
return candidate
}
}
return "web"
}