en un clic
gentleman-installer
// Installation step patterns for Gentleman.Dots TUI installer. Trigger: When editing installer.go, adding installation steps, or modifying the installation flow.
// Installation step patterns for Gentleman.Dots TUI installer. Trigger: When editing installer.go, adding installation steps, or modifying the installation flow.
Bubbletea TUI patterns for Gentleman.Dots installer. Trigger: When editing Go files in installer/internal/tui/, working on TUI screens, or adding new UI features.
Docker-based E2E testing patterns for Gentleman.Dots installer. Trigger: When editing files in installer/e2e/, writing E2E tests, or adding platform support.
System detection and command execution patterns for Gentleman.Dots. Trigger: When editing files in installer/internal/system/, adding OS support, or modifying command execution.
Vim Trainer RPG system patterns for Gentleman.Dots. Trigger: When editing files in installer/internal/tui/trainer/, adding exercises, modules, or game mechanics.
Go testing patterns for Gentleman.Dots, including Bubbletea TUI testing. Trigger: When writing Go tests, using teatest, or adding test coverage.
| name | gentleman-installer |
| description | Installation step patterns for Gentleman.Dots TUI installer. Trigger: When editing installer.go, adding installation steps, or modifying the installation flow. |
| license | Apache-2.0 |
| metadata | {"author":"gentleman-programming","version":"1.0"} |
Use this skill when:
All steps follow this structure in model.go:
type InstallStep struct {
ID string // Unique identifier: "terminal", "shell", etc.
Name string // Display name: "Install Fish"
Description string // Short description
Status StepStatus // Pending, Running, Done, Failed, Skipped
Progress float64 // 0.0 - 1.0
Error error // Error if failed
Interactive bool // Needs terminal control (sudo, chsh)
}
Steps MUST be registered in SetupInstallSteps() in model.go:
func (m *Model) SetupInstallSteps() {
m.Steps = []InstallStep{}
// Conditional step based on user choice
if m.Choices.SomeChoice {
m.Steps = append(m.Steps, InstallStep{
ID: "newstep",
Name: "Install Something",
Description: "Description here",
Status: StatusPending,
Interactive: false, // true if needs sudo/password
})
}
}
All step logic goes in installer.go:
func executeStep(stepID string, m *Model) error {
switch stepID {
case "newstep":
return stepNewStep(m)
// ... other cases
default:
return fmt.Errorf("unknown step: %s", stepID)
}
}
func stepNewStep(m *Model) error {
stepID := "newstep"
SendLog(stepID, "Starting installation...")
// Check if already installed
if system.CommandExists("newtool") {
SendLog(stepID, "Already installed, skipping...")
return nil
}
// Install based on OS
var result *system.ExecResult
if m.SystemInfo.IsTermux {
result = system.RunPkgInstall("newtool", nil, func(line string) {
SendLog(stepID, line)
})
} else {
result = system.RunBrewWithLogs("install newtool", nil, func(line string) {
SendLog(stepID, line)
})
}
if result.Error != nil {
return wrapStepError("newstep", "Install NewTool",
"Failed to install NewTool",
result.Error)
}
SendLog(stepID, "✓ NewTool installed")
return nil
}
Mark step as Interactive and use runInteractiveStep:
// In SetupInstallSteps:
m.Steps = append(m.Steps, InstallStep{
ID: "interactive_step",
Name: "Configure System",
Description: "Requires password",
Status: StatusPending,
Interactive: true, // KEY: marks as interactive
})
// In runNextStep (update.go):
if step.Interactive {
return runInteractiveStep(step.ID, &m)
}
Adding new tool installation?
├── Add step to SetupInstallSteps() with conditions
├── Add case in executeStep() switch
├── Create step{Name}() function in installer.go
├── Handle all OS variants (Mac, Linux, Arch, Debian, Termux)
├── Use SendLog() for progress updates
└── Return wrapStepError() on failure
Step needs password/sudo?
├── Set Interactive: true in InstallStep
├── Use system.RunSudo() or system.RunSudoWithLogs()
└── Use tea.ExecProcess for full terminal control
Step should be conditional?
├── Check m.Choices.{option} before appending
├── Check m.SystemInfo for OS-specific logic
└── Use StatusSkipped if conditions not met
func stepInstallTool(m *Model) error {
stepID := "tool"
if !system.CommandExists("tool") {
SendLog(stepID, "Installing tool...")
var result *system.ExecResult
switch {
case m.SystemInfo.IsTermux:
result = system.RunPkgInstall("tool", nil, logFunc(stepID))
case m.SystemInfo.OS == system.OSArch:
result = system.RunSudoWithLogs("pacman -S --noconfirm tool", nil, logFunc(stepID))
case m.SystemInfo.OS == system.OSMac:
result = system.RunBrewWithLogs("install tool", nil, logFunc(stepID))
default: // Debian/Ubuntu
result = system.RunBrewWithLogs("install tool", nil, logFunc(stepID))
}
if result.Error != nil {
return wrapStepError("tool", "Install Tool",
"Failed to install tool",
result.Error)
}
}
// Copy configuration
SendLog(stepID, "Copying configuration...")
homeDir := os.Getenv("HOME")
if err := system.CopyDir(filepath.Join("Gentleman.Dots", "ToolConfig/*"),
filepath.Join(homeDir, ".config/tool/")); err != nil {
return wrapStepError("tool", "Install Tool",
"Failed to copy configuration",
err)
}
SendLog(stepID, "✓ Tool configured")
return nil
}
func logFunc(stepID string) func(string) {
return func(line string) {
SendLog(stepID, line)
}
}
func wrapStepError(stepID, stepName, description string, cause error) error {
return &StepError{
StepID: stepID,
StepName: stepName,
Description: description,
Cause: cause,
}
}
// Usage:
if result.Error != nil {
return wrapStepError("terminal", "Install Alacritty",
"Failed to install Alacritty. Check your internet connection.",
result.Error)
}
// Patch config based on user choices
func stepInstallShell(m *Model) error {
// ... install shell ...
// Patch config for window manager choice
configPath := filepath.Join(homeDir, ".config/fish/config.fish")
if err := system.PatchFishForWM(configPath, m.Choices.WindowMgr, m.Choices.InstallNvim); err != nil {
return wrapStepError("shell", "Install Fish",
"Failed to configure window manager in shell",
err)
}
return nil
}
Always use SendLog for step progress:
SendLog(stepID, "Starting...") // Start
SendLog(stepID, "Downloading...") // Progress
SendLog(stepID, " → file.txt") // Sub-item
SendLog(stepID, "✓ Step completed") // Success
cd installer && go build ./cmd/gentleman-installer # Build
./gentleman-installer --help # Show help
./gentleman-installer --non-interactive --shell=fish # Non-interactive
GENTLEMAN_VERBOSE=1 ./gentleman-installer --non-interactive # Verbose logs
installer/internal/tui/installer.go for step implementationsinstaller/internal/tui/model.go for SetupInstallStepsinstaller/internal/system/exec.go for command executioninstaller/internal/tui/non_interactive.go for CLI mode