mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 19:23:41 +00:00
Instead of relying on stdin and stdout by default, using the huh package from charmbracelet allows us to handle user input more gracefully such as y/n instead of typing 'yes' or 'no'. If a user makes a mistake whilst typing in any text fields they cannot use left or right to edit a single character when using huh it can. This adds a dependancy and may increase the size of installer but overall improves user experience.
236 lines
4.6 KiB
Go
236 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/charmbracelet/huh"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// pangolinTheme is the custom theme using brand colors
|
|
var pangolinTheme = ThemePangolin()
|
|
|
|
// isAccessibleMode checks if we should use accessible mode (simple prompts)
|
|
// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set
|
|
func isAccessibleMode() bool {
|
|
// Check if stdin is not a terminal (piped input, CI, etc.)
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return true
|
|
}
|
|
// Check for dumb terminal
|
|
if os.Getenv("TERM") == "dumb" {
|
|
return true
|
|
}
|
|
// Check for explicit accessible mode request
|
|
if os.Getenv("ACCESSIBLE") != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so
|
|
func handleAbort(err error) {
|
|
if err != nil && errors.Is(err, huh.ErrUserAborted) {
|
|
fmt.Println("\nInstallation cancelled.")
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// runField runs a single field with the Pangolin theme, handling accessible mode
|
|
func runField(field huh.Field) error {
|
|
if isAccessibleMode() {
|
|
return field.RunAccessible(os.Stdout, os.Stdin)
|
|
}
|
|
form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme)
|
|
return form.Run()
|
|
}
|
|
|
|
func readString(prompt string, defaultValue string) string {
|
|
var value string
|
|
|
|
title := prompt
|
|
if defaultValue != "" {
|
|
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
|
}
|
|
|
|
input := huh.NewInput().
|
|
Title(title).
|
|
Value(&value)
|
|
|
|
// If no default value, this field is required
|
|
if defaultValue == "" {
|
|
input = input.Validate(func(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("this field is required")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
err := runField(input)
|
|
handleAbort(err)
|
|
|
|
if value == "" {
|
|
value = defaultValue
|
|
}
|
|
|
|
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %s\n", prompt, value)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func readStringNoDefault(prompt string) string {
|
|
var value string
|
|
|
|
for {
|
|
input := huh.NewInput().
|
|
Title(prompt).
|
|
Value(&value).
|
|
Validate(func(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("this field is required")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
err := runField(input)
|
|
handleAbort(err)
|
|
|
|
if value != "" {
|
|
// Print the answer so it remains visible in terminal history
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %s\n", prompt, value)
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
|
|
func readPassword(prompt string) string {
|
|
var value string
|
|
|
|
for {
|
|
input := huh.NewInput().
|
|
Title(prompt).
|
|
Value(&value).
|
|
EchoMode(huh.EchoModePassword).
|
|
Validate(func(s string) error {
|
|
if s == "" {
|
|
return fmt.Errorf("password is required")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
err := runField(input)
|
|
handleAbort(err)
|
|
|
|
if value != "" {
|
|
// Print confirmation without revealing the password
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %s\n", prompt, "********")
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
|
|
func readBool(prompt string, defaultValue bool) bool {
|
|
var value = defaultValue
|
|
|
|
confirm := huh.NewConfirm().
|
|
Title(prompt).
|
|
Value(&value).
|
|
Affirmative("Yes").
|
|
Negative("No")
|
|
|
|
err := runField(confirm)
|
|
handleAbort(err)
|
|
|
|
// Print the answer so it remains visible in terminal history
|
|
if !isAccessibleMode() {
|
|
answer := "No"
|
|
if value {
|
|
answer = "Yes"
|
|
}
|
|
fmt.Printf("%s: %s\n", prompt, answer)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func readBoolNoDefault(prompt string) bool {
|
|
var value bool
|
|
|
|
confirm := huh.NewConfirm().
|
|
Title(prompt).
|
|
Value(&value).
|
|
Affirmative("Yes").
|
|
Negative("No")
|
|
|
|
err := runField(confirm)
|
|
handleAbort(err)
|
|
|
|
// Print the answer so it remains visible in terminal history
|
|
if !isAccessibleMode() {
|
|
answer := "No"
|
|
if value {
|
|
answer = "Yes"
|
|
}
|
|
fmt.Printf("%s: %s\n", prompt, answer)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func readInt(prompt string, defaultValue int) int {
|
|
var value string
|
|
|
|
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
|
|
|
|
input := huh.NewInput().
|
|
Title(title).
|
|
Value(&value).
|
|
Validate(func(s string) error {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
_, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return fmt.Errorf("please enter a valid number")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
err := runField(input)
|
|
handleAbort(err)
|
|
|
|
if value == "" {
|
|
// Print the answer so it remains visible in terminal history
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
result, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// Print the answer so it remains visible in terminal history
|
|
if !isAccessibleMode() {
|
|
fmt.Printf("%s: %d\n", prompt, result)
|
|
}
|
|
|
|
return result
|
|
}
|