Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Key commands inside the shell:
| `ftp download <alias> <remote> <local>`| 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.
Expand Down
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 79 additions & 14 deletions src/cmd/htop.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"

"servercommander/src/utils"
)
Expand All @@ -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)
Expand All @@ -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()
}
Loading