mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 14:56:39 +00:00
Merge branch 'feature/installer-tui' of github.com:LaurenceJJones/pangolin into LaurenceJJones-feature/installer-tui
This commit is contained in:
@@ -3,8 +3,36 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.39.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
|
golang.org/x/term v0.40.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.40.0 // indirect
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,80 @@
|
|||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
||||||
|
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
287
install/input.go
287
install/input.go
@@ -1,92 +1,235 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"os"
|
||||||
"syscall"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
// 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 != "" {
|
if defaultValue != "" {
|
||||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
||||||
} else {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
}
|
}
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|
||||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
input := huh.NewInput().
|
||||||
fmt.Print(prompt + ": ")
|
Title(title).
|
||||||
input, _ := reader.ReadString('\n')
|
Value(&value)
|
||||||
return strings.TrimSpace(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
// If no default value, this field is required
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
if defaultValue == "" {
|
||||||
fmt.Print(prompt + ": ")
|
input = input.Validate(func(s string) error {
|
||||||
// Read password without echo if we're in a terminal
|
if s == "" {
|
||||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
return fmt.Errorf("this field is required")
|
||||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
}
|
||||||
if err != nil {
|
return nil
|
||||||
return ""
|
})
|
||||||
}
|
|
||||||
input := strings.TrimSpace(string(password))
|
|
||||||
if input == "" {
|
|
||||||
return readPassword(prompt, reader)
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
} else {
|
|
||||||
// Fallback to reading from stdin if not in a terminal
|
|
||||||
return readString(reader, prompt, "")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
err := runField(input)
|
||||||
defaultStr := "no"
|
handleAbort(err)
|
||||||
if defaultValue {
|
|
||||||
defaultStr = "yes"
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
|
||||||
lower := strings.ToLower(input)
|
|
||||||
if lower == "yes" {
|
|
||||||
return true
|
|
||||||
} else if lower == "no" {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
fmt.Println("Please enter 'yes' or 'no'.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
if value == "" {
|
||||||
for {
|
value = defaultValue
|
||||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
|
||||||
lower := strings.ToLower(input)
|
|
||||||
if lower == "yes" {
|
|
||||||
return true
|
|
||||||
} else if lower == "no" {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
fmt.Println("Please enter 'yes' or 'no'.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
||||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
if !isAccessibleMode() {
|
||||||
if input == "" {
|
fmt.Printf("%s: %s\n", prompt, value)
|
||||||
return defaultValue
|
|
||||||
}
|
}
|
||||||
value := defaultValue
|
|
||||||
fmt.Sscanf(input, "%d", &value)
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -82,14 +81,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
var alreadyInstalled = false
|
var alreadyInstalled = false
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
config.DoCrowdsecInstall = false
|
config.DoCrowdsecInstall = false
|
||||||
@@ -117,12 +114,12 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool("Would you like to install and start the containers?", true) {
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool("Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
// try to start docker service but ignore errors
|
// try to start docker service but ignore errors
|
||||||
if err := startDockerService(); err != nil {
|
if err := startDockerService(); err != nil {
|
||||||
@@ -167,7 +164,7 @@ func main() {
|
|||||||
fmt.Println("\n=== MaxMind Database Update ===")
|
fmt.Println("\n=== MaxMind Database Update ===")
|
||||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
if readBool("Would you like to update the MaxMind database to the latest version?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try updating it manually later if needed.")
|
fmt.Println("You can try updating it manually later if needed.")
|
||||||
@@ -175,7 +172,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||||
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
if readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try downloading it manually later if needed.")
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
@@ -192,11 +189,11 @@ func main() {
|
|||||||
if !checkIsCrowdsecInstalledInCompose() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool("Would you like to install CrowdSec?", false) {
|
||||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||||
|
|
||||||
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool("Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -225,8 +222,8 @@ func main() {
|
|||||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
if !readBool(reader, "Are these values correct?", true) {
|
if !readBool("Are these values correct?", true) {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +232,7 @@ func main() {
|
|||||||
if detectedType == Undefined {
|
if detectedType == Undefined {
|
||||||
// If detection fails, prompt the user
|
// If detection fails, prompt the user
|
||||||
fmt.Println("Unable to detect container type from existing installation.")
|
fmt.Println("Unable to detect container type from existing installation.")
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
} else {
|
} else {
|
||||||
config.InstallationContainerType = detectedType
|
config.InstallationContainerType = detectedType
|
||||||
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
||||||
@@ -277,8 +274,8 @@ func main() {
|
|||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func podmanOrDocker() SupportedContainer {
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
chosenContainer := Docker
|
chosenContainer := Docker
|
||||||
if strings.EqualFold(inputContainer, "docker") {
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
@@ -299,7 +296,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
approved := readBool("The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
||||||
if approved {
|
if approved {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
@@ -344,35 +341,35 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
return chosenContainer
|
return chosenContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectUserInput(reader *bufio.Reader) Config {
|
func collectUserInput() Config {
|
||||||
config := Config{}
|
config := Config{}
|
||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
|
|
||||||
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
|
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
|
||||||
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
// Set default dashboard domain after base domain is collected
|
||||||
defaultDashboardDomain := ""
|
defaultDashboardDomain := ""
|
||||||
if config.BaseDomain != "" {
|
if config.BaseDomain != "" {
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
}
|
}
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
config.EnableEmail = readBool("Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString("Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString("Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readPassword("Enter SMTP password")
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
|
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -393,8 +390,8 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
|
|
||||||
fmt.Println("\n=== Advanced Configuration ===")
|
fmt.Println("\n=== Advanced Configuration ===")
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
|
||||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||||
|
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
|
|||||||
51
install/theme.go
Normal file
51
install/theme.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pangolin brand colors (converted from oklch to hex)
|
||||||
|
var (
|
||||||
|
// Primary orange/amber - oklch(0.6717 0.1946 41.93)
|
||||||
|
primaryColor = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"}
|
||||||
|
// Muted foreground
|
||||||
|
mutedColor = lipgloss.AdaptiveColor{Light: "#737373", Dark: "#A3A3A3"}
|
||||||
|
// Success green
|
||||||
|
successColor = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"}
|
||||||
|
// Error red - oklch(0.577 0.245 27.325)
|
||||||
|
errorColor = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"}
|
||||||
|
// Normal text
|
||||||
|
normalFg = lipgloss.AdaptiveColor{Light: "#171717", Dark: "#FAFAFA"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThemePangolin returns a huh theme using Pangolin brand colors
|
||||||
|
func ThemePangolin() *huh.Theme {
|
||||||
|
t := huh.ThemeBase()
|
||||||
|
|
||||||
|
// Focused state styles
|
||||||
|
t.Focused.Base = t.Focused.Base.BorderForeground(primaryColor)
|
||||||
|
t.Focused.Title = t.Focused.Title.Foreground(primaryColor).Bold(true)
|
||||||
|
t.Focused.Description = t.Focused.Description.Foreground(mutedColor)
|
||||||
|
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor)
|
||||||
|
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor)
|
||||||
|
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(primaryColor)
|
||||||
|
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
|
||||||
|
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(primaryColor)
|
||||||
|
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(successColor).SetString("✓ ")
|
||||||
|
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(mutedColor).SetString(" ")
|
||||||
|
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("#FFFFFF")).Background(primaryColor)
|
||||||
|
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#404040"})
|
||||||
|
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primaryColor)
|
||||||
|
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primaryColor)
|
||||||
|
|
||||||
|
// Blurred state inherits from focused but with hidden border
|
||||||
|
t.Blurred = t.Focused
|
||||||
|
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||||
|
t.Blurred.Title = t.Blurred.Title.Foreground(mutedColor).Bold(false)
|
||||||
|
t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(mutedColor)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user