| 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"} |
When to Use
Use this skill when:
- Adding new installation steps
- Modifying existing tool installations
- Working on backup/restore functionality
- Implementing non-interactive mode support
- Adding new OS/platform support
Critical Patterns
Pattern 1: InstallStep Structure
All steps follow this structure in model.go:
type InstallStep struct {
ID string
Name string
Description string
Status StepStatus
Progress float64
Error error
Interactive bool
}
Pattern 2: Step Registration in SetupInstallSteps
Steps MUST be registered in SetupInstallSteps() in model.go:
func (m *Model) SetupInstallSteps() {
m.Steps = []InstallStep{}
if m.Choices.SomeChoice {
m.Steps = append(m.Steps, InstallStep{
ID: "newstep",
Name: "Install Something",
Description: "Description here",
Status: StatusPending,
Interactive: false,
})
}
}
Pattern 3: Step Execution in executeStep
All step logic goes in installer.go:
func executeStep(stepID string, m *Model) error {
switch stepID {
case "newstep":
return stepNewStep(m)
default:
return fmt.Errorf("unknown step: %s", stepID)
}
}
func stepNewStep(m *Model) error {
stepID := "newstep"
SendLog(stepID, "Starting installation...")
if system.CommandExists("newtool") {
SendLog(stepID, "Already installed, skipping...")
return nil
}
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
}
Pattern 4: Interactive Steps (sudo/password required)
Mark step as Interactive and use runInteractiveStep:
m.Steps = append(m.Steps, InstallStep{
ID: "interactive_step",
Name: "Configure System",
Description: "Requires password",
Status: StatusPending,
Interactive: true,
})
if step.Interactive {
return runInteractiveStep(step.ID, &m)
}
Decision Tree
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
Code Examples
Example 1: OS-Specific Installation
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:
result = system.RunBrewWithLogs("install tool", nil, logFunc(stepID))
}
if result.Error != nil {
return wrapStepError("tool", "Install Tool",
"Failed to install tool",
result.Error)
}
}
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)
}
}
Example 2: Error Wrapping Pattern
func wrapStepError(stepID, stepName, description string, cause error) error {
return &StepError{
StepID: stepID,
StepName: stepName,
Description: description,
Cause: cause,
}
}
if result.Error != nil {
return wrapStepError("terminal", "Install Alacritty",
"Failed to install Alacritty. Check your internet connection.",
result.Error)
}
Example 3: Config Patching
func stepInstallShell(m *Model) error {
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
}
Logging Pattern
Always use SendLog for step progress:
SendLog(stepID, "Starting...")
SendLog(stepID, "Downloading...")
SendLog(stepID, " ā file.txt")
SendLog(stepID, "ā Step completed")
Commands
cd installer && go build ./cmd/gentleman-installer
./gentleman-installer --help
./gentleman-installer --non-interactive --shell=fish
GENTLEMAN_VERBOSE=1 ./gentleman-installer --non-interactive
Resources
- Steps: See
installer/internal/tui/installer.go for step implementations
- Model: See
installer/internal/tui/model.go for SetupInstallSteps
- System: See
installer/internal/system/exec.go for command execution
- Non-interactive: See
installer/internal/tui/non_interactive.go for CLI mode