diff --git a/README.md b/README.md index 5cb47e7..d281352 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Key commands inside the shell: | `ftp download `| Download a file via FTP/FTPS. | | `help` | Print the command catalogue. | | `clear` | Clear the terminal and reprint the banner. | -| `htop` | Launch `htop` with the ServerCommander color theme. | +| `htop` | Launch `htop` with the ServerCommander theme (falls back to PowerShell monitor on Windows). | | `exit` | Gracefully shut down ServerCommander. | > **Note:** When prompted for the authentication method during `session add`, enter `password` or `private_key`. Passwords are never stored—if you choose `password` you will be asked for it when connecting. diff --git a/docs/USAGE.md b/docs/USAGE.md index 674e209..6f47d39 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -122,6 +122,8 @@ server-commander htop This opens `htop` in the current terminal session. Press `q` to exit the monitor and return to the ServerCommander console. +> **Windows users:** If `htop` is not installed, ServerCommander automatically runs a PowerShell-based process monitor sorted by CPU usage. Use the paging controls shown in the prompt (Space/PageUp/PageDown) and press `Ctrl+C` to return. + ## Additional Options ### Help diff --git a/src/cmd/htop.go b/src/cmd/htop.go index 6eceae2..e1e057e 100644 --- a/src/cmd/htop.go +++ b/src/cmd/htop.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "servercommander/src/utils" ) @@ -21,9 +22,36 @@ func htopCommand(args []string) error { } if runtime.GOOS == "windows" { - return errors.New("htop is not available on Windows. Please install an alternative process monitor.") + return runWindowsProcessMonitor() } + return runHtopBinary() +} + +func prepareHtopTheme() (string, func(), error) { + if len(htopTheme) == 0 { + return "", nil, nil + } + + dir, err := os.MkdirTemp("", "servercommander-htop") + if err != nil { + return "", nil, fmt.Errorf("failed to create temporary configuration directory: %w", err) + } + + path := filepath.Join(dir, "htoprc") + if err := os.WriteFile(path, htopTheme, 0o600); err != nil { + os.RemoveAll(dir) + return "", nil, fmt.Errorf("failed to prepare htop theme: %w", err) + } + + cleanup := func() { + _ = os.RemoveAll(dir) + } + + return path, cleanup, nil +} + +func runHtopBinary() error { binary, err := exec.LookPath("htop") if err != nil { return fmt.Errorf("htop executable not found in PATH: %w", err) @@ -50,25 +78,62 @@ func htopCommand(args []string) error { return cmd.Run() } -func prepareHtopTheme() (string, func(), error) { - if len(htopTheme) == 0 { - return "", nil, nil +func runWindowsProcessMonitor() error { + // Prefer a native htop installation if available. + if binary, err := exec.LookPath("htop.exe"); err == nil { + return runBinaryWithTheme(binary) + } + if binary, err := exec.LookPath("htop"); err == nil { + return runBinaryWithTheme(binary) } - dir, err := os.MkdirTemp("", "servercommander-htop") - if err != nil { - return "", nil, fmt.Errorf("failed to create temporary configuration directory: %w", err) + // Fall back to PowerShell's process listing with paging. + powershellCandidates := []string{"powershell.exe", "powershell", "pwsh.exe", "pwsh"} + var psBinary string + for _, candidate := range powershellCandidates { + if binary, err := exec.LookPath(candidate); err == nil { + psBinary = binary + break + } + } + if psBinary == "" { + return errors.New("neither htop nor PowerShell were found in PATH. Please install htop or ensure PowerShell is available") } - path := filepath.Join(dir, "htoprc") - if err := os.WriteFile(path, htopTheme, 0o600); err != nil { - os.RemoveAll(dir) - return "", nil, fmt.Errorf("failed to prepare htop theme: %w", err) + fmt.Printf("%sLaunching PowerShell process monitor (htop not found). Use Space/PageUp/PageDown to navigate and press 'q' to exit.%s\n", utils.Green, utils.Reset) + + script := strings.Join([]string{ + "$ErrorActionPreference = 'Stop'", + "Write-Host \"Process list sorted by CPU usage. Press 'q' to exit paging.\"", + "Get-Process | Sort-Object -Property CPU -Descending | Select-Object -Property Id, ProcessName, CPU, PM, WS, StartTime -ErrorAction SilentlyContinue | Format-Table -AutoSize | Out-Host -Paging", + }, "; ") + + cmd := exec.Command(psBinary, "-NoProfile", "-Command", script) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func runBinaryWithTheme(binary string) error { + themePath, cleanup, err := prepareHtopTheme() + if err != nil { + return err + } + if cleanup != nil { + defer cleanup() } - cleanup := func() { - _ = os.RemoveAll(dir) + fmt.Printf("%sLaunching htop with ServerCommander theme. Press 'q' to return to the console.%s\n", utils.Green, utils.Reset) + + cmd := exec.Command(binary) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if themePath != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("HTOPRC=%s", themePath)) } - return path, cleanup, nil + return cmd.Run() }