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() - } -}