diff --git a/go.mod b/go.mod
index 608a4bf..7e15047 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,3 @@
module servercommander
go 1.23.5
-
-require fyne.io/fyne/v2 v2.4.5
-
-replace fyne.io/fyne/v2 => ./third_party/fyne
diff --git a/src/cmd/clear.go b/src/cmd/clear.go
index 2381224..61452e0 100644
--- a/src/cmd/clear.go
+++ b/src/cmd/clear.go
@@ -6,12 +6,12 @@ import (
"os/exec"
"runtime"
- "servercommander/src/ui"
+ "servercommander/src/console"
)
func clearCommand(args []string) error {
- if ui.ClearConsole() {
- ui.ApplicationBanner()
+ if console.ClearConsole() {
+ console.ApplicationBanner()
return nil
}
@@ -24,7 +24,7 @@ func clearCommand(args []string) error {
return fmt.Errorf("failed to clear the console: %w", err)
}
- ui.ApplicationBanner()
+ console.ApplicationBanner()
return nil
}
diff --git a/src/cmd/exit.go b/src/cmd/exit.go
index b47482b..dd3df51 100644
--- a/src/cmd/exit.go
+++ b/src/cmd/exit.go
@@ -4,12 +4,12 @@ import (
"fmt"
"os"
- "servercommander/src/ui"
+ "servercommander/src/console"
"servercommander/src/utils"
)
func exitCommand(args []string) error {
- ui.GoodbyeBanner()
+ console.GoodbyeBanner()
fmt.Println(utils.Red, "Exiting the program...", utils.Reset)
os.Exit(0)
return nil
diff --git a/src/console/console.go b/src/console/console.go
new file mode 100644
index 0000000..67832af
--- /dev/null
+++ b/src/console/console.go
@@ -0,0 +1,68 @@
+package console
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+
+ "servercommander/src/utils"
+)
+
+// Run starts the interactive console loop using the provided executor to
+// handle individual command lines.
+func Run(executor func(string) error) error {
+ ApplicationBanner()
+
+ scanner := bufio.NewScanner(os.Stdin)
+ for {
+ fmt.Printf("%ssc>%s ", utils.Cyan, utils.Reset)
+
+ if !scanner.Scan() {
+ fmt.Println()
+ return scanner.Err()
+ }
+
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+
+ if err := executor(line); err != nil {
+ fmt.Println(utils.Red, err.Error(), utils.Reset)
+ }
+ }
+}
+
+// ApplicationBanner prints the program banner in the console.
+func ApplicationBanner() {
+ fmt.Println(utils.Cyan, "==============================")
+ fmt.Println(utils.Green, " Server Commander v1.0.1")
+ fmt.Println(utils.Cyan, "==============================", utils.Reset)
+}
+
+// GoodbyeBanner displays a small exit animation before terminating.
+func GoodbyeBanner() {
+ fmt.Println(utils.Yellow, "Goodbye! The program will close in 3 seconds...", utils.Reset)
+ time.Sleep(time.Second)
+
+ for i := 3; i > 0; i-- {
+ fmt.Printf("%sClosing in %d seconds...\r", utils.Cyan, i)
+ time.Sleep(time.Second)
+ }
+
+ time.Sleep(2 * time.Second)
+}
+
+// ClearConsole tries to clear the terminal using ANSI escape sequences. It
+// returns true when handled internally so callers can skip external commands.
+func ClearConsole() bool {
+ if runtime.GOOS == "windows" {
+ return false
+ }
+
+ fmt.Print("\033[H\033[2J")
+ return true
+}
diff --git a/src/main.go b/src/main.go
index b1d4934..d0860ea 100644
--- a/src/main.go
+++ b/src/main.go
@@ -4,11 +4,11 @@ import (
"log"
"servercommander/src/cmd"
- "servercommander/src/ui"
+ "servercommander/src/console"
)
func main() {
- if err := ui.RunStandaloneConsole(cmd.Execute); err != nil {
+ if err := console.Run(cmd.Execute); err != nil {
log.Fatal(err)
}
}
diff --git a/src/ui/applicationBanner.go b/src/ui/applicationBanner.go
deleted file mode 100644
index 94ede4d..0000000
--- a/src/ui/applicationBanner.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package ui
-
-import (
- "fmt"
- "servercommander/src/utils"
-)
-
-func ApplicationBanner() {
- fmt.Println(utils.Cyan, "==============================")
- fmt.Println(utils.Green, " Server Commander v1.0.1")
- fmt.Println(utils.Cyan, "==============================", utils.Reset)
-}
diff --git a/src/ui/console_server.go b/src/ui/console_server.go
deleted file mode 100644
index 6b3df7a..0000000
--- a/src/ui/console_server.go
+++ /dev/null
@@ -1,560 +0,0 @@
-//go:build !desktop
-// +build !desktop
-
-package ui
-
-import (
- "bufio"
- "encoding/json"
- "errors"
- "fmt"
- "net"
- "net/http"
- "os"
- "os/exec"
- "regexp"
- "runtime"
- "strings"
- "sync"
-
- "servercommander/src/utils"
-)
-
-type commandExecutor func(string) error
-
-type eventPayload struct {
- Type string `json:"type"`
- Text string `json:"text,omitempty"`
-}
-
-type ConsoleServer struct {
- executor commandExecutor
- listener net.Listener
- clients map[int]chan eventPayload
- clientsMu sync.Mutex
- history []eventPayload
- historyMu sync.Mutex
- errCh chan error
-
- stdoutPipe *os.File
- stderrPipe *os.File
- origStdout *os.File
- origStderr *os.File
-}
-
-var (
- consoleInstance *ConsoleServer
- consoleMu sync.RWMutex
-)
-
-const (
- controlClearEvent = "clear"
- appendEvent = "append"
-)
-
-var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
-
-func RunStandaloneConsole(executor func(string) error) error {
- server, err := newConsoleServer(executor)
- if err != nil {
- return err
- }
-
- consoleMu.Lock()
- consoleInstance = server
- consoleMu.Unlock()
-
- return server.run()
-}
-
-func ClearConsole() bool {
- consoleMu.RLock()
- server := consoleInstance
- consoleMu.RUnlock()
- if server == nil {
- return false
- }
- server.clear()
- return true
-}
-
-func newConsoleServer(executor commandExecutor) (*ConsoleServer, error) {
- listener, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return nil, fmt.Errorf("failed to allocate listener: %w", err)
- }
-
- server := &ConsoleServer{
- executor: executor,
- listener: listener,
- clients: make(map[int]chan eventPayload),
- errCh: make(chan error, 1),
- }
-
- if err := server.redirectStandardStreams(); err != nil {
- listener.Close()
- return nil, err
- }
-
- return server, nil
-}
-
-func (c *ConsoleServer) run() error {
- mux := http.NewServeMux()
- mux.HandleFunc("/", c.handleIndex)
- mux.HandleFunc("/execute", c.handleExecute)
- mux.HandleFunc("/events", c.handleEvents)
-
- go func() {
- c.errCh <- http.Serve(c.listener, mux)
- }()
-
- url := "http://" + c.listener.Addr().String() + "/"
- if err := openBrowser(url); err != nil {
- fmt.Println(utils.Yellow, "Unable to automatically open the interface. Visit", url, "in your browser.", utils.Reset)
- } else {
- fmt.Println(utils.Green, "ServerCommander interface started at", url, utils.Reset)
- }
-
- ApplicationBanner()
-
- err := <-c.errCh
- if err != nil && !errors.Is(err, http.ErrServerClosed) {
- return err
- }
- return nil
-}
-
-func (c *ConsoleServer) redirectStandardStreams() error {
- stdoutReader, stdoutWriter, err := os.Pipe()
- if err != nil {
- return fmt.Errorf("failed to redirect stdout: %w", err)
- }
-
- stderrReader, stderrWriter, err := os.Pipe()
- if err != nil {
- stdoutReader.Close()
- stdoutWriter.Close()
- return fmt.Errorf("failed to redirect stderr: %w", err)
- }
-
- c.stdoutPipe = stdoutWriter
- c.stderrPipe = stderrWriter
- c.origStdout = os.Stdout
- c.origStderr = os.Stderr
-
- os.Stdout = stdoutWriter
- os.Stderr = stderrWriter
-
- go c.readPipe(stdoutReader, c.origStdout)
- go c.readPipe(stderrReader, c.origStderr)
-
- return nil
-}
-
-func (c *ConsoleServer) readPipe(pipe *os.File, mirror *os.File) {
- reader := bufio.NewReader(pipe)
- buffer := make([]byte, 1024)
- var pending strings.Builder
-
- for {
- n, err := reader.Read(buffer)
- if n > 0 {
- chunk := buffer[:n]
- if mirror != nil {
- _, _ = mirror.Write(chunk)
- }
-
- pending.Write(chunk)
- emit, remainder := splitANSISequences(pending.String())
- if emit != "" {
- c.broadcastAppend(emit)
- }
-
- pending.Reset()
- if remainder != "" {
- pending.WriteString(remainder)
- }
- }
-
- if err != nil {
- if pending.Len() > 0 {
- c.broadcastAppend(pending.String())
- }
- return
- }
- }
-}
-
-func splitANSISequences(input string) (string, string) {
- last := strings.LastIndexByte(input, 0x1b)
- if last == -1 {
- return input, ""
- }
-
- fragment := input[last:]
- if fragment == "" {
- return input, ""
- }
-
- if ansiSequenceComplete(fragment) {
- return input, ""
- }
-
- return input[:last], fragment
-}
-
-func ansiSequenceComplete(fragment string) bool {
- if len(fragment) < 2 {
- return false
- }
-
- if fragment[0] != 0x1b {
- return true
- }
-
- if fragment[1] != '[' {
- return true
- }
-
- for i := 2; i < len(fragment); i++ {
- b := fragment[i]
- if (b >= '0' && b <= '9') || b == ';' {
- continue
- }
- if b >= '@' && b <= '~' {
- return true
- }
- return false
- }
-
- return false
-}
-
-func (c *ConsoleServer) handleIndex(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, consoleHTML)
-}
-
-func (c *ConsoleServer) handleExecute(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- var payload struct {
- Command string `json:"command"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
- http.Error(w, "invalid payload", http.StatusBadRequest)
- return
- }
-
- command := strings.TrimSpace(payload.Command)
- if command == "" {
- w.WriteHeader(http.StatusNoContent)
- return
- }
-
- c.broadcastAppend(">> " + command + "\n")
- go c.executeCommand(command)
- w.WriteHeader(http.StatusAccepted)
-}
-
-func (c *ConsoleServer) executeCommand(command string) {
- if c.executor == nil {
- return
- }
-
- if err := c.executor(command); err != nil {
- fmt.Println(utils.Red, err.Error(), utils.Reset)
- }
-}
-
-func (c *ConsoleServer) handleEvents(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- flusher, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "streaming unsupported", http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", "text/event-stream")
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Connection", "keep-alive")
-
- clientChan := make(chan eventPayload, 16)
- clientID := c.addClient(clientChan)
- defer c.removeClient(clientID)
-
- history := c.snapshotHistory()
- for _, event := range history {
- if err := writeSSE(w, event); err != nil {
- return
- }
- }
- flusher.Flush()
-
- for {
- select {
- case <-r.Context().Done():
- return
- case event := <-clientChan:
- if err := writeSSE(w, event); err != nil {
- return
- }
- flusher.Flush()
- }
- }
-}
-
-func (c *ConsoleServer) addClient(ch chan eventPayload) int {
- c.clientsMu.Lock()
- defer c.clientsMu.Unlock()
-
- id := len(c.clients) + 1
- c.clients[id] = ch
- return id
-}
-
-func (c *ConsoleServer) removeClient(id int) {
- c.clientsMu.Lock()
- defer c.clientsMu.Unlock()
-
- if ch, exists := c.clients[id]; exists {
- close(ch)
- delete(c.clients, id)
- }
-}
-
-func (c *ConsoleServer) snapshotHistory() []eventPayload {
- c.historyMu.Lock()
- defer c.historyMu.Unlock()
-
- snapshot := make([]eventPayload, len(c.history))
- copy(snapshot, c.history)
- return snapshot
-}
-
-func (c *ConsoleServer) broadcastAppend(text string) {
- cleaned := sanitizeText(text)
- if cleaned == "" {
- return
- }
-
- event := eventPayload{Type: appendEvent, Text: cleaned}
-
- c.historyMu.Lock()
- c.history = append(c.history, event)
- c.historyMu.Unlock()
-
- c.dispatch(event)
-}
-
-func (c *ConsoleServer) clear() {
- c.historyMu.Lock()
- c.history = nil
- c.historyMu.Unlock()
-
- c.dispatch(eventPayload{Type: controlClearEvent})
-}
-
-func (c *ConsoleServer) dispatch(event eventPayload) {
- c.clientsMu.Lock()
- defer c.clientsMu.Unlock()
-
- for id, ch := range c.clients {
- select {
- case ch <- event:
- default:
- go func(target chan eventPayload, identifier int) {
- target <- event
- }(ch, id)
- }
- }
-}
-
-func sanitizeText(text string) string {
- if text == "" {
- return ""
- }
-
- cleaned := ansiPattern.ReplaceAllString(text, "")
- cleaned = strings.ReplaceAll(cleaned, "\r\n", "\n")
- cleaned = strings.ReplaceAll(cleaned, "\r", "\n")
- return cleaned
-}
-
-func writeSSE(w http.ResponseWriter, event eventPayload) error {
- data, err := json.Marshal(event)
- if err != nil {
- return err
- }
-
- if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
- return err
- }
- return nil
-}
-
-const consoleHTML = `
-
-
-
-ServerCommander
-
-
-
-
-
-
-
-`
-
-func openBrowser(url string) error {
- var command string
- var args []string
-
- switch runtime.GOOS {
- case "windows":
- command = "rundll32"
- args = []string{"url.dll,FileProtocolHandler", url}
- case "darwin":
- command = "open"
- args = []string{url}
- default:
- command = "xdg-open"
- args = []string{url}
- }
-
- executable, err := exec.LookPath(command)
- if err != nil {
- return err
- }
-
- cmd := exec.Command(executable, args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Stdin = os.Stdin
- return cmd.Start()
-}
diff --git a/src/ui/desktop_console.go b/src/ui/desktop_console.go
deleted file mode 100644
index 0b5e71c..0000000
--- a/src/ui/desktop_console.go
+++ /dev/null
@@ -1,583 +0,0 @@
-//go:build desktop
-// +build desktop
-
-package ui
-
-import (
- "bufio"
- "fmt"
- "io"
- "net/url"
- "os"
- "regexp"
- "strings"
- "sync"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/app"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/layout"
- "fyne.io/fyne/v2/theme"
- "fyne.io/fyne/v2/widget"
-
- "servercommander/src/utils"
-)
-
-type commandExecutor func(string) error
-
-var (
- consoleInstance *DesktopConsole
- consoleMu sync.RWMutex
-)
-
-var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
-
-const (
- portfolioLink = "https://uplytech.com/portfolio"
- websiteLink = "https://uplytech.com"
- commandPlaceholder = "Befehl eingeben und Enter drücken…"
- promptPlaceholder = "Antwort eingeben und Enter drücken…"
-)
-
-// DesktopConsole encapsulates the GUI console experience, piping stdout and
-// stderr into a dedicated window that accepts command execution requests.
-type DesktopConsole struct {
- executor commandExecutor
-
- app fyne.App
- window fyne.Window
-
- output *widget.Label
- scroller *container.Scroll
- input *widget.Entry
-
- stdoutPipe *os.File
- stderrPipe *os.File
- origStdout *os.File
- origStderr *os.File
-
- promptBridge *promptBridge
- promptMu sync.Mutex
- pendingResponse chan string
-
- logMu sync.Mutex
- logBuilder strings.Builder
-
- cleanupOnce sync.Once
-}
-
-// RunStandaloneConsole launches the GUI console and blocks until it closes.
-func RunStandaloneConsole(executor func(string) error) error {
- console := &DesktopConsole{executor: executor}
-
- application := app.New()
- console.app = application
-
- if err := console.redirectStandardStreams(); err != nil {
- return err
- }
-
- console.initializePromptBridge()
-
- consoleMu.Lock()
- consoleInstance = console
- consoleMu.Unlock()
-
- defer func() {
- console.cleanup()
- consoleMu.Lock()
- consoleInstance = nil
- consoleMu.Unlock()
- }()
-
- return console.run()
-}
-
-// ClearConsole removes the accumulated log content from the GUI window.
-func ClearConsole() bool {
- consoleMu.RLock()
- console := consoleInstance
- consoleMu.RUnlock()
- if console == nil {
- return false
- }
-
- console.clear()
- return true
-}
-
-func (c *DesktopConsole) run() error {
- c.window = c.app.NewWindow("ServerCommander")
- c.window.SetMaster()
- c.window.SetFixedSize(true)
- c.window.Resize(fyne.NewSize(960, 600))
- c.window.CenterOnScreen()
-
- c.window.SetCloseIntercept(func() {
- c.cleanup()
- c.window.Close()
- })
-
- c.buildUI()
-
- c.window.Show()
- ApplicationBanner()
- c.app.Run()
- return nil
-}
-
-func (c *DesktopConsole) buildUI() {
- c.output = widget.NewLabel("")
- c.output.TextStyle = fyne.TextStyle{Monospace: true}
- c.output.Wrapping = fyne.TextWrapWord
- c.scroller = container.NewVScroll(c.output)
- c.scroller.SetMinSize(fyne.NewSize(0, 400))
-
- c.input = widget.NewEntry()
- c.input.SetPlaceHolder(commandPlaceholder)
- c.input.OnSubmitted = func(text string) {
- c.handleInput(text)
- }
-
- runButton := widget.NewButton("Ausführen", func() {
- c.handleInput(c.input.Text)
- })
- runButton.Importance = widget.HighImportance
-
- clearButton := widget.NewButton("Leeren", func() {
- c.clear()
- ApplicationBanner()
- })
-
- actions := container.NewHBox(clearButton, layout.NewSpacer(), runButton)
- footer := container.NewBorder(nil, nil, nil, actions, c.input)
-
- header := container.NewHBox(newLogoWidget(c), layout.NewSpacer())
-
- c.window.SetContent(container.NewBorder(header, footer, nil, nil, c.scroller))
-}
-
-func (c *DesktopConsole) executeCommand(command string) {
- if c.executor == nil {
- return
- }
-
- if err := c.executor(command); err != nil {
- fmt.Println(utils.Red, err.Error(), utils.Reset)
- }
-}
-
-func (c *DesktopConsole) handleInput(input string) {
- trimmed := strings.TrimSpace(input)
- c.input.SetText("")
- if trimmed == "" {
- return
- }
-
- if c.deliverPromptResponse(trimmed) {
- c.appendLog(trimmed + "\n")
- return
- }
-
- c.appendLog(">> " + trimmed + "\n")
- go c.executeCommand(trimmed)
-}
-
-func (c *DesktopConsole) redirectStandardStreams() error {
- stdoutReader, stdoutWriter, err := os.Pipe()
- if err != nil {
- return fmt.Errorf("failed to redirect stdout: %w", err)
- }
-
- stderrReader, stderrWriter, err := os.Pipe()
- if err != nil {
- stdoutReader.Close()
- stdoutWriter.Close()
- return fmt.Errorf("failed to redirect stderr: %w", err)
- }
-
- c.stdoutPipe = stdoutWriter
- c.stderrPipe = stderrWriter
- c.origStdout = os.Stdout
- c.origStderr = os.Stderr
-
- os.Stdout = stdoutWriter
- os.Stderr = stderrWriter
-
- go c.readPipe(stdoutReader, c.origStdout)
- go c.readPipe(stderrReader, c.origStderr)
-
- return nil
-}
-
-func (c *DesktopConsole) initializePromptBridge() {
- bridge := newPromptBridge()
- c.promptBridge = bridge
- utils.SetPromptReader(bridge)
-
- go c.watchPromptRequests()
-}
-
-func (c *DesktopConsole) readPipe(pipe *os.File, mirror *os.File) {
- reader := bufio.NewReader(pipe)
- buffer := make([]byte, 1024)
- var pending strings.Builder
-
- for {
- n, err := reader.Read(buffer)
- if n > 0 {
- chunk := buffer[:n]
- if mirror != nil {
- _, _ = mirror.Write(chunk)
- }
-
- pending.Write(chunk)
- emit, remainder := splitANSISequences(pending.String())
- if emit != "" {
- c.appendLog(emit)
- }
-
- pending.Reset()
- if remainder != "" {
- pending.WriteString(remainder)
- }
- }
-
- if err != nil {
- if pending.Len() > 0 {
- c.appendLog(pending.String())
- }
- return
- }
- }
-}
-
-func (c *DesktopConsole) appendLog(text string) {
- cleaned := sanitizeText(text)
- if cleaned == "" {
- return
- }
-
- c.logMu.Lock()
- c.logBuilder.WriteString(cleaned)
- logSnapshot := c.logBuilder.String()
- c.logMu.Unlock()
-
- if c.app == nil {
- return
- }
-
- c.app.QueueUpdate(func() {
- if c.output != nil {
- c.output.SetText(logSnapshot)
- }
- if c.scroller != nil {
- c.scroller.ScrollToBottom()
- }
- })
-}
-
-func (c *DesktopConsole) clear() {
- c.logMu.Lock()
- c.logBuilder.Reset()
- c.logMu.Unlock()
-
- if c.app == nil {
- return
- }
-
- c.app.QueueUpdate(func() {
- if c.output != nil {
- c.output.SetText("")
- }
- if c.scroller != nil {
- c.scroller.ScrollToTop()
- }
- })
-}
-
-func (c *DesktopConsole) cleanup() {
- c.cleanupOnce.Do(func() {
- if c.stdoutPipe != nil {
- c.stdoutPipe.Close()
- }
- if c.stderrPipe != nil {
- c.stderrPipe.Close()
- }
- if c.origStdout != nil {
- os.Stdout = c.origStdout
- }
- if c.origStderr != nil {
- os.Stderr = c.origStderr
- }
- if c.promptBridge != nil {
- c.promptBridge.Close()
- }
- utils.SetPromptReader(os.Stdin)
- c.promptMu.Lock()
- if c.pendingResponse != nil {
- close(c.pendingResponse)
- c.pendingResponse = nil
- }
- c.promptMu.Unlock()
- })
-}
-
-func (c *DesktopConsole) watchPromptRequests() {
- if c.promptBridge == nil {
- return
- }
-
- for responseCh := range c.promptBridge.Requests() {
- c.promptMu.Lock()
- c.pendingResponse = responseCh
- c.promptMu.Unlock()
- c.setPromptPlaceholder(true)
- }
- c.setPromptPlaceholder(false)
-}
-
-func (c *DesktopConsole) deliverPromptResponse(response string) bool {
- c.promptMu.Lock()
- responseCh := c.pendingResponse
- if responseCh != nil {
- c.pendingResponse = nil
- }
- c.promptMu.Unlock()
-
- if responseCh == nil {
- return false
- }
-
- responseCh <- response
- close(responseCh)
- c.setPromptPlaceholder(false)
- return true
-}
-
-func (c *DesktopConsole) setPromptPlaceholder(waiting bool) {
- if c.app == nil {
- return
- }
-
- placeholder := commandPlaceholder
- if waiting {
- placeholder = promptPlaceholder
- }
-
- c.app.QueueUpdate(func() {
- if c.input != nil {
- c.input.SetPlaceHolder(placeholder)
- if waiting {
- c.input.Focus()
- }
- }
- })
-}
-
-func sanitizeText(text string) string {
- if text == "" {
- return ""
- }
-
- cleaned := ansiPattern.ReplaceAllString(text, "")
- cleaned = strings.ReplaceAll(cleaned, "\r\n", "\n")
- cleaned = strings.ReplaceAll(cleaned, "\r", "\n")
- return cleaned
-}
-
-func splitANSISequences(input string) (string, string) {
- last := strings.LastIndexByte(input, 0x1b)
- if last == -1 {
- return input, ""
- }
-
- fragment := input[last:]
- if fragment == "" {
- return input, ""
- }
-
- if ansiSequenceComplete(fragment) {
- return input, ""
- }
-
- return input[:last], fragment
-}
-
-func ansiSequenceComplete(fragment string) bool {
- if len(fragment) < 2 {
- return false
- }
-
- if fragment[0] != 0x1b {
- return true
- }
-
- if fragment[1] != '[' {
- return true
- }
-
- for i := 2; i < len(fragment); i++ {
- b := fragment[i]
- if (b >= '0' && b <= '9') || b == ';' {
- continue
- }
- if b >= '@' && b <= '~' {
- return true
- }
- return false
- }
-
- return false
-}
-
-type logoWidget struct {
- widget.BaseWidget
-
- label *canvas.Text
- parent *DesktopConsole
-}
-
-func newLogoWidget(console *DesktopConsole) *logoWidget {
- widget := &logoWidget{parent: console}
- widget.ExtendBaseWidget(widget)
- return widget
-}
-
-func (l *logoWidget) CreateRenderer() fyne.WidgetRenderer {
- l.label = canvas.NewText("ServerCommander", theme.PrimaryColor())
- l.label.TextStyle = fyne.TextStyle{Bold: true}
- l.label.Alignment = fyne.TextAlignCenter
-
- content := container.NewCenter(l.label)
- return widget.NewSimpleRenderer(content)
-}
-
-func (l *logoWidget) MinSize() fyne.Size {
- if l.label == nil {
- return fyne.NewSize(140, 48)
- }
-
- min := l.label.MinSize()
- return fyne.NewSize(min.Width+32, min.Height+16)
-}
-
-func (l *logoWidget) Tapped(_ *fyne.PointEvent) {}
-
-func (l *logoWidget) TappedSecondary(event *fyne.PointEvent) {
- menu := fyne.NewMenu("",
- fyne.NewMenuItem("Portfolio", func() {
- l.openURL(portfolioLink)
- }),
- fyne.NewMenuItem("UplyTech Website", func() {
- l.openURL(websiteLink)
- }),
- )
-
- if l.parent != nil && l.parent.window != nil {
- pop := widget.NewPopUpMenu(menu, l.parent.window.Canvas())
- pop.ShowAtPosition(event.AbsolutePosition)
- }
-}
-
-func (l *logoWidget) openURL(target string) {
- if l.parent == nil || l.parent.app == nil {
- return
- }
-
- parsed, err := url.Parse(target)
- if err != nil {
- fmt.Println(utils.Red, "Ungültige URL:", target, utils.Reset)
- return
- }
-
- if err := l.parent.app.OpenURL(parsed); err != nil {
- fmt.Println(utils.Red, "Konnte URL nicht öffnen:", target, utils.Reset)
- }
-}
-
-type promptBridge struct {
- requestCh chan chan string
- closeCh chan struct{}
-
- mu sync.Mutex
- buffer []byte
-}
-
-func newPromptBridge() *promptBridge {
- return &promptBridge{
- requestCh: make(chan chan string),
- closeCh: make(chan struct{}),
- }
-}
-
-func (p *promptBridge) Read(dst []byte) (int, error) {
- if len(dst) == 0 {
- return 0, nil
- }
-
- p.mu.Lock()
- if len(p.buffer) > 0 {
- n := copy(dst, p.buffer)
- p.buffer = p.buffer[n:]
- p.mu.Unlock()
- return n, nil
- }
- p.mu.Unlock()
-
- responseCh := make(chan string, 1)
-
- select {
- case <-p.closeCh:
- return 0, io.EOF
- case p.requestCh <- responseCh:
- }
-
- var (
- response string
- ok bool
- )
- select {
- case <-p.closeCh:
- return 0, io.EOF
- case response, ok = <-responseCh:
- }
-
- if !ok {
- return 0, io.EOF
- }
-
- if !strings.HasSuffix(response, "\n") {
- response += "\n"
- }
-
- data := []byte(response)
- n := copy(dst, data)
-
- if n < len(data) {
- p.mu.Lock()
- p.buffer = append(p.buffer, data[n:]...)
- p.mu.Unlock()
- }
-
- return n, nil
-}
-
-func (p *promptBridge) Close() {
- p.mu.Lock()
- select {
- case <-p.closeCh:
- p.mu.Unlock()
- return
- default:
- }
- close(p.closeCh)
- close(p.requestCh)
- p.buffer = nil
- p.mu.Unlock()
-}
-
-func (p *promptBridge) Requests() <-chan chan string {
- return p.requestCh
-}
diff --git a/src/ui/goodbyeBanner.go b/src/ui/goodbyeBanner.go
deleted file mode 100644
index b5c86aa..0000000
--- a/src/ui/goodbyeBanner.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package ui
-
-import (
- "fmt"
- "servercommander/src/utils"
- "time"
-)
-
-func GoodbyeBanner() {
- fmt.Println(utils.Yellow, "Goodbye! The program will close in 3 seconds...", utils.Reset)
- time.Sleep(1 * time.Second)
-
- for i := 3; i > 0; i-- {
- fmt.Printf("%sClosing in %d seconds...\r", utils.Cyan, i)
- time.Sleep(1 * time.Second)
- }
-
- time.Sleep(2 * time.Second)
-}
diff --git a/third_party/fyne/app/app.go b/third_party/fyne/app/app.go
deleted file mode 100644
index 7a69c85..0000000
--- a/third_party/fyne/app/app.go
+++ /dev/null
@@ -1,160 +0,0 @@
-package app
-
-import (
- "fmt"
- "net/url"
- "sync"
-
- "fyne.io/fyne/v2"
-)
-
-// New returns a minimal stub implementation of fyne.App so builds can succeed
-// in environments where the full dependency graph is unavailable.
-func New() fyne.App {
- return &stubApp{}
-}
-
-type stubApp struct {
- mu sync.Mutex
- windows map[*stubWindow]struct{}
- runOnce sync.Once
- runCh chan struct{}
- runClosed bool
-}
-
-func (a *stubApp) NewWindow(title string) fyne.Window {
- win := &stubWindow{title: title, app: a}
-
- a.mu.Lock()
- if a.windows == nil {
- a.windows = make(map[*stubWindow]struct{})
- }
- a.windows[win] = struct{}{}
- a.mu.Unlock()
-
- a.runChannel()
-
- return win
-}
-
-func (a *stubApp) Run() {
- ch := a.runChannel()
-
- shouldClose := false
- a.mu.Lock()
- if len(a.windows) == 0 && !a.runClosed {
- a.runClosed = true
- shouldClose = true
- }
- a.mu.Unlock()
-
- if shouldClose {
- close(ch)
- }
-
- <-ch
-}
-
-func (a *stubApp) runChannel() chan struct{} {
- a.runOnce.Do(func() {
- a.runCh = make(chan struct{})
- })
- return a.runCh
-}
-
-func (a *stubApp) windowClosed(win *stubWindow) {
- var ch chan struct{}
- shouldClose := false
-
- a.mu.Lock()
- if a.windows != nil {
- if _, ok := a.windows[win]; ok {
- delete(a.windows, win)
- if len(a.windows) == 0 && !a.runClosed && a.runCh != nil {
- a.runClosed = true
- ch = a.runCh
- shouldClose = true
- }
- }
- }
- a.mu.Unlock()
-
- if shouldClose {
- close(ch)
- }
-}
-
-func (a *stubApp) QueueUpdate(fn func()) {
- if fn != nil {
- fn()
- }
-}
-
-func (a *stubApp) OpenURL(u *url.URL) error {
- if u == nil {
- return fmt.Errorf("nil url provided")
- }
- // The stub just acknowledges the request.
- return nil
-}
-
-type stubWindow struct {
- app *stubApp
- title string
- size fyne.Size
- content fyne.CanvasObject
- closeIntercept func()
-
- mu sync.Mutex
- closed bool
-}
-
-func (w *stubWindow) SetMaster() {}
-
-func (w *stubWindow) SetFixedSize(bool) {}
-
-func (w *stubWindow) Resize(size fyne.Size) {
- w.size = size
-}
-
-func (w *stubWindow) CenterOnScreen() {}
-
-func (w *stubWindow) SetCloseIntercept(fn func()) {
- w.closeIntercept = fn
-}
-
-func (w *stubWindow) Show() {}
-
-func (w *stubWindow) Close() {
- w.mu.Lock()
- if w.closed {
- w.mu.Unlock()
- return
- }
- w.closed = true
- intercept := w.closeIntercept
- w.closeIntercept = nil
- w.mu.Unlock()
-
- if intercept != nil {
- intercept()
- }
-
- if w.app != nil {
- w.app.windowClosed(w)
- }
-}
-
-func (w *stubWindow) SetContent(obj fyne.CanvasObject) {
- w.content = obj
-}
-
-func (w *stubWindow) Canvas() fyne.Canvas {
- return stubCanvas{}
-}
-
-type stubCanvas struct{}
-
-func (stubCanvas) MinSize() fyne.Size {
- return fyne.Size{}
-}
diff --git a/third_party/fyne/canvas/text.go b/third_party/fyne/canvas/text.go
deleted file mode 100644
index 8d6ca38..0000000
--- a/third_party/fyne/canvas/text.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package canvas
-
-import (
- "image/color"
-
- "fyne.io/fyne/v2"
-)
-
-// Text is a minimal representation of a canvas text element.
-type Text struct {
- Text string
- Color color.Color
- TextSize float32
- TextStyle fyne.TextStyle
- Alignment fyne.TextAlign
-}
-
-// NewText creates a new canvas text instance.
-func NewText(text string, clr color.Color) *Text {
- return &Text{Text: text, Color: clr, TextSize: 12}
-}
-
-// MinSize returns a naive size estimation for the text.
-func (t *Text) MinSize() fyne.Size {
- width := float32(len(t.Text)) * 7
- if width == 0 {
- width = 1
- }
- height := t.TextSize
- if height == 0 {
- height = 12
- }
- return fyne.NewSize(width, height)
-}
-
-// Refresh does nothing in the stub implementation.
-func (t *Text) Refresh() {}
diff --git a/third_party/fyne/container/container.go b/third_party/fyne/container/container.go
deleted file mode 100644
index 82640d7..0000000
--- a/third_party/fyne/container/container.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package container
-
-import "fyne.io/fyne/v2"
-
-// Container is a simple grouping of canvas objects.
-type Container struct {
- Objects []fyne.CanvasObject
- size fyne.Size
-}
-
-// MinSize returns a naive size estimation based on contained objects.
-func (c *Container) MinSize() fyne.Size {
- if c == nil {
- return fyne.Size{}
- }
- if c.size != (fyne.Size{}) {
- return c.size
- }
- var width, height float32
- for _, obj := range c.Objects {
- if obj == nil {
- continue
- }
- sz := obj.MinSize()
- if sz.Width > width {
- width = sz.Width
- }
- height += sz.Height
- }
- return fyne.NewSize(width, height)
-}
-
-// SetMinSize hints the container size.
-func (c *Container) SetMinSize(size fyne.Size) {
- if c != nil {
- c.size = size
- }
-}
-
-// NewVScroll wraps content inside a scroll container.
-func NewVScroll(content fyne.CanvasObject) *Scroll {
- return &Scroll{Content: content}
-}
-
-// NewHBox returns a container laid out horizontally.
-func NewHBox(objects ...fyne.CanvasObject) *Container {
- return &Container{Objects: append([]fyne.CanvasObject(nil), objects...)}
-}
-
-// NewBorder arranges objects around a center element.
-func NewBorder(top, bottom, left, right, center fyne.CanvasObject) *Container {
- objs := []fyne.CanvasObject{center, top, bottom, left, right}
- return &Container{Objects: objs}
-}
-
-// NewCenter centres a single object.
-func NewCenter(object fyne.CanvasObject) *Container {
- return &Container{Objects: []fyne.CanvasObject{object}}
-}
-
-// Scroll represents a vertical scroll container.
-type Scroll struct {
- Content fyne.CanvasObject
- size fyne.Size
-}
-
-// MinSize returns the stored minimum size.
-func (s *Scroll) MinSize() fyne.Size {
- if s == nil {
- return fyne.Size{}
- }
- if s.size != (fyne.Size{}) {
- return s.size
- }
- if s.Content == nil {
- return fyne.Size{}
- }
- return s.Content.MinSize()
-}
-
-// SetMinSize stores a requested minimum size.
-func (s *Scroll) SetMinSize(size fyne.Size) {
- if s != nil {
- s.size = size
- }
-}
-
-// ScrollToBottom is a no-op in the stub implementation.
-func (s *Scroll) ScrollToBottom() {}
-
-// ScrollToTop is a no-op in the stub implementation.
-func (s *Scroll) ScrollToTop() {}
diff --git a/third_party/fyne/go.mod b/third_party/fyne/go.mod
deleted file mode 100644
index b062998..0000000
--- a/third_party/fyne/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module fyne.io/fyne/v2
-
-go 1.20
diff --git a/third_party/fyne/layout/spacer.go b/third_party/fyne/layout/spacer.go
deleted file mode 100644
index 3b309a3..0000000
--- a/third_party/fyne/layout/spacer.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package layout
-
-import "fyne.io/fyne/v2"
-
-type spacer struct{}
-
-func (s *spacer) MinSize() fyne.Size { return fyne.Size{} }
-
-// NewSpacer returns a placeholder object used to align widgets.
-func NewSpacer() fyne.CanvasObject { return &spacer{} }
diff --git a/third_party/fyne/theme/theme.go b/third_party/fyne/theme/theme.go
deleted file mode 100644
index 83353cf..0000000
--- a/third_party/fyne/theme/theme.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package theme
-
-import "image/color"
-
-// ForegroundColor returns a generic foreground colour.
-func ForegroundColor() color.Color {
- return color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff}
-}
-
-// PrimaryColor returns a highlight colour.
-func PrimaryColor() color.Color {
- return color.NRGBA{R: 0x21, G: 0x6c, B: 0xff, A: 0xff}
-}
-
-// BackgroundColor returns a generic background colour.
-func BackgroundColor() color.Color {
- return color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff}
-}
diff --git a/third_party/fyne/types.go b/third_party/fyne/types.go
deleted file mode 100644
index 7cea35e..0000000
--- a/third_party/fyne/types.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package fyne
-
-import "net/url"
-
-// App represents a graphical application instance.
-type App interface {
- NewWindow(title string) Window
- Run()
- QueueUpdate(func())
- OpenURL(*url.URL) error
-}
-
-// Window describes a top-level user interface window.
-type Window interface {
- SetMaster()
- SetFixedSize(bool)
- Resize(Size)
- CenterOnScreen()
- SetCloseIntercept(func())
- Show()
- Close()
- SetContent(CanvasObject)
- Canvas() Canvas
-}
-
-// Canvas represents a drawable surface.
-type Canvas interface{}
-
-// CanvasObject is any drawable UI element.
-type CanvasObject interface {
- MinSize() Size
-}
-
-// Widget is any interactive canvas object that can render itself.
-type Widget interface {
- CanvasObject
- CreateRenderer() WidgetRenderer
-}
-
-// WidgetRenderer draws a widget.
-type WidgetRenderer interface {
- Layout(Size)
- MinSize() Size
- Refresh()
- Destroy()
- Objects() []CanvasObject
-}
-
-// Size represents a width/height pair.
-type Size struct {
- Width float32
- Height float32
-}
-
-// NewSize constructs a Size instance.
-func NewSize(width, height float32) Size {
- return Size{Width: width, Height: height}
-}
-
-// Position represents a point in 2D space.
-type Position struct {
- X float32
- Y float32
-}
-
-// PointEvent describes a pointer interaction event.
-type PointEvent struct {
- AbsolutePosition Position
-}
-
-// TextStyle configures textual rendering options.
-type TextStyle struct {
- Bold bool
- Italic bool
- Monospace bool
-}
-
-// TextWrap controls wrapping behaviour.
-type TextWrap int
-
-const (
- TextWrapOff TextWrap = iota
- TextWrapBreak
- TextWrapWord
-)
-
-// TextAlign controls textual alignment.
-type TextAlign int
-
-const (
- TextAlignLeading TextAlign = iota
- TextAlignCenter
- TextAlignTrailing
-)
-
-// Menu represents a pop-up menu structure.
-type Menu struct {
- Label string
- Items []*MenuItem
-}
-
-// MenuItem is a single selectable menu item.
-type MenuItem struct {
- Label string
- Action func()
-}
-
-// NewMenu constructs a new menu.
-func NewMenu(label string, items ...*MenuItem) *Menu {
- return &Menu{Label: label, Items: items}
-}
-
-// NewMenuItem constructs a new menu item.
-func NewMenuItem(label string, action func()) *MenuItem {
- return &MenuItem{Label: label, Action: action}
-}
-
-// Activate triggers the menu item's action.
-func (m *MenuItem) Activate() {
- if m != nil && m.Action != nil {
- m.Action()
- }
-}
diff --git a/third_party/fyne/widget/popup.go b/third_party/fyne/widget/popup.go
deleted file mode 100644
index b03bf02..0000000
--- a/third_party/fyne/widget/popup.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package widget
-
-import "fyne.io/fyne/v2"
-
-// PopUpMenu is a stub implementation of a pop-up menu.
-type PopUpMenu struct {
- menu *fyne.Menu
- canvas fyne.Canvas
-}
-
-// NewPopUpMenu creates a new pop-up menu instance.
-func NewPopUpMenu(menu *fyne.Menu, canvas fyne.Canvas) *PopUpMenu {
- return &PopUpMenu{menu: menu, canvas: canvas}
-}
-
-// ShowAtPosition has no visible behaviour in the stub implementation.
-func (p *PopUpMenu) ShowAtPosition(fyne.Position) {}
-
-// Dismiss is a no-op for the stub implementation.
-func (p *PopUpMenu) Dismiss() {}
diff --git a/third_party/fyne/widget/widget.go b/third_party/fyne/widget/widget.go
deleted file mode 100644
index 520c764..0000000
--- a/third_party/fyne/widget/widget.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package widget
-
-import "fyne.io/fyne/v2"
-
-// BaseWidget provides helper methods for building custom widgets.
-type BaseWidget struct{}
-
-// ExtendBaseWidget is a no-op for the stub implementation.
-func (b *BaseWidget) ExtendBaseWidget(fyne.Widget) {}
-
-// Refresh is a no-op for the stub implementation.
-func (b *BaseWidget) Refresh() {}
-
-// stubRenderer implements fyne.WidgetRenderer with no behaviour.
-type stubRenderer struct {
- object fyne.CanvasObject
-}
-
-func (r *stubRenderer) Layout(fyne.Size) {}
-func (r *stubRenderer) MinSize() fyne.Size { return r.object.MinSize() }
-func (r *stubRenderer) Refresh() {}
-func (r *stubRenderer) Destroy() {}
-func (r *stubRenderer) Objects() []fyne.CanvasObject { return []fyne.CanvasObject{r.object} }
-
-// NewSimpleRenderer creates a renderer that simply exposes the object.
-func NewSimpleRenderer(obj fyne.CanvasObject) fyne.WidgetRenderer {
- return &stubRenderer{object: obj}
-}
-
-// Label represents simple text output.
-type Label struct {
- BaseWidget
- Text string
- TextStyle fyne.TextStyle
- Wrapping fyne.TextWrap
-}
-
-// NewLabel creates a new label instance.
-func NewLabel(text string) *Label {
- l := &Label{Text: text}
- l.ExtendBaseWidget(l)
- return l
-}
-
-// SetText updates the label contents.
-func (l *Label) SetText(text string) {
- l.Text = text
-}
-
-// MinSize returns a naive size estimation.
-func (l *Label) MinSize() fyne.Size {
- width := float32(len(l.Text)) * 7
- if width == 0 {
- width = 1
- }
- height := float32(14)
- return fyne.NewSize(width, height)
-}
-
-// CreateRenderer returns a minimal renderer for compatibility.
-func (l *Label) CreateRenderer() fyne.WidgetRenderer {
- return &stubRenderer{object: l}
-}
-
-// Entry represents text input.
-type Entry struct {
- BaseWidget
- Text string
- placeholder string
- OnSubmitted func(string)
-}
-
-// NewEntry constructs an empty entry widget.
-func NewEntry() *Entry {
- e := &Entry{}
- e.ExtendBaseWidget(e)
- return e
-}
-
-// SetPlaceHolder stores the placeholder text.
-func (e *Entry) SetPlaceHolder(text string) {
- e.placeholder = text
-}
-
-// SetText updates the entry value.
-func (e *Entry) SetText(text string) {
- e.Text = text
-}
-
-// Focus is a no-op for the stub implementation.
-func (e *Entry) Focus() {}
-
-// MinSize returns a naive entry size.
-func (e *Entry) MinSize() fyne.Size {
- return fyne.NewSize(120, 20)
-}
-
-// CreateRenderer returns a stub renderer.
-func (e *Entry) CreateRenderer() fyne.WidgetRenderer {
- return &stubRenderer{object: e}
-}
-
-// Importance controls button emphasis.
-type Importance int
-
-const (
- LowImportance Importance = iota
- MediumImportance
- HighImportance
-)
-
-// Button is a clickable element.
-type Button struct {
- BaseWidget
- Label string
- Importance Importance
- onTapped func()
-}
-
-// NewButton creates a new button instance.
-func NewButton(label string, tapped func()) *Button {
- b := &Button{Label: label, onTapped: tapped}
- b.ExtendBaseWidget(b)
- return b
-}
-
-// MinSize returns a naive button size.
-func (b *Button) MinSize() fyne.Size {
- width := float32(len(b.Label))*7 + 16
- if width < 40 {
- width = 40
- }
- return fyne.NewSize(width, 24)
-}
-
-// CreateRenderer returns a stub renderer.
-func (b *Button) CreateRenderer() fyne.WidgetRenderer {
- return &stubRenderer{object: b}
-}
-
-// Tapped triggers the button callback.
-func (b *Button) Tapped() {
- if b.onTapped != nil {
- b.onTapped()
- }
-}