diff --git a/examples/prompter/main.go b/examples/prompter/main.go new file mode 100644 index 0000000..0354ab4 --- /dev/null +++ b/examples/prompter/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/cli/go-gh/v2/pkg/prompter" +) + +func main() { + p := prompter.New(os.Stdin, os.Stdout, os.Stderr) + + // Demonstrating single-option select / dropdown prompts + cuisines := []string{"Italian", "Greek", "Indian", "Japanese", "American"} + favorite, err := p.Select("Favorite cuisine?", "Italian", cuisines) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Favorite cuisine: %s\n", cuisines[favorite]) + + // Demonstrating multi-option select / dropdown prompts + favorites, err := p.MultiSelect("Favorite cuisines?", []string{}, cuisines) + if err != nil { + log.Fatal(err) + } + for _, f := range favorites { + fmt.Printf("Favorite cuisine: %s\n", cuisines[f]) + } + + // Demonstrating text input prompts + text, err := p.Input("Favorite meal?", "Breakfast") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Favorite meal: %s\n", text) + + // Demonstrating password input prompts + safeword, err := p.Password("Safe word?") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Safe word: %s\n", safeword) + + // Demonstrating confirmation prompts + confirmation, err := p.Confirm("Are you sure?", false) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Confirmation: %t\n", confirmation) +} diff --git a/go.mod b/go.mod index b5e74ca..969276b 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc v1.0.0 + github.com/charmbracelet/bubbletea v0.26.3 github.com/charmbracelet/glamour v0.7.0 - github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c + github.com/charmbracelet/huh v0.4.2 + github.com/charmbracelet/lipgloss v0.11.0 github.com/cli/browser v1.3.0 github.com/cli/safeexec v1.0.0 github.com/cli/shurcooL-graphql v0.0.4 @@ -18,20 +20,29 @@ require ( github.com/muesli/termenv v0.15.2 github.com/stretchr/testify v1.7.0 github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e - golang.org/x/sys v0.19.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/sys v0.20.0 + golang.org/x/term v0.20.0 + golang.org/x/text v0.15.0 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/alecthomas/chroma/v2 v2.8.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/bubbles v0.18.0 // indirect + github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect + github.com/charmbracelet/x/input v0.1.1 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect @@ -40,13 +51,18 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // 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.15 // indirect github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect golang.org/x/net v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 81011e4..6faa6c8 100644 --- a/go.sum +++ b/go.sum @@ -10,29 +10,54 @@ github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1 github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= +github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= -github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY= -github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= -github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc= -github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= +github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM= +github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0= +github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= +github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= +github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I= +github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= +github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= +github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= +github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= +github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -65,6 +90,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -74,6 +101,10 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= @@ -94,6 +125,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +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= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= @@ -102,6 +135,8 @@ github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GA github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -110,27 +145,30 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/prompter/prompter.go b/pkg/prompter/prompter.go index 1cc1849..e8c6bc0 100644 --- a/pkg/prompter/prompter.go +++ b/pkg/prompter/prompter.go @@ -3,19 +3,21 @@ package prompter import ( - "fmt" "io" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/cli/go-gh/v2/pkg/text" + "os" ) // Prompter provides methods for prompting the user. type Prompter struct { - stdin FileReader - stdout FileWriter - stderr FileWriter + pClient PrompterClient +} + +type PrompterClient interface { + Select(prompt, defaultValue string, options []string) (int, error) + MultiSelect(prompt string, defaultValues, options []string) ([]int, error) + Input(prompt, defaultValue string) (string, error) + Password(prompt string) (string, error) + Confirm(prompt string, defaultValue bool) (bool, error) } // FileWriter provides a minimal writable interface for stdout and stderr. @@ -32,103 +34,40 @@ type FileReader interface { // New instantiates a new Prompter. func New(stdin FileReader, stdout FileWriter, stderr FileWriter) *Prompter { + // TODO: Enhance logic to look at configuration for prompter type. + prompterType := os.Getenv("GH_PROMPTER") + if prompterType == "accessible" { + return &Prompter{ + pClient: NewAccessiblePrompter(stdin, stdout, stderr), + } + } + return &Prompter{ - stdin: stdin, - stdout: stdout, - stderr: stderr, + pClient: NewLegacyPrompter(stdin, stdout, stderr), } } // Select prompts the user to select an option from a list of options. func (p *Prompter) Select(prompt, defaultValue string, options []string) (int, error) { - var result int - q := &survey.Select{ - Message: prompt, - Options: options, - PageSize: 20, - Filter: latinMatchingFilter, - } - if defaultValue != "" { - for _, o := range options { - if o == defaultValue { - q.Default = defaultValue - break - } - } - } - err := p.ask(q, &result) - return result, err + return p.pClient.Select(prompt, defaultValue, options) } // MultiSelect prompts the user to select multiple options from a list of options. func (p *Prompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { - var result []int - q := &survey.MultiSelect{ - Message: prompt, - Options: options, - PageSize: 20, - Filter: latinMatchingFilter, - } - if len(defaultValues) > 0 { - validatedDefault := []string{} - for _, x := range defaultValues { - for _, y := range options { - if x == y { - validatedDefault = append(validatedDefault, x) - } - } - } - q.Default = validatedDefault - } - err := p.ask(q, &result) - return result, err + return p.pClient.MultiSelect(prompt, defaultValues, options) } // Input prompts the user to input a single-line string. func (p *Prompter) Input(prompt, defaultValue string) (string, error) { - var result string - err := p.ask(&survey.Input{ - Message: prompt, - Default: defaultValue, - }, &result) - return result, err + return p.pClient.Input(prompt, defaultValue) } // Password prompts the user to input a single-line string without echoing the input. func (p *Prompter) Password(prompt string) (string, error) { - var result string - err := p.ask(&survey.Password{ - Message: prompt, - }, &result) - return result, err + return p.pClient.Password(prompt) } // Confirm prompts the user to confirm a yes/no question. func (p *Prompter) Confirm(prompt string, defaultValue bool) (bool, error) { - var result bool - err := p.ask(&survey.Confirm{ - Message: prompt, - Default: defaultValue, - }, &result) - return result, err -} - -func (p *Prompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { - opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) - err := survey.AskOne(q, response, opts...) - if err == nil { - return nil - } - return fmt.Errorf("could not prompt: %w", err) -} - -// latinMatchingFilter returns whether the value matches the input filter. -// The strings are compared normalized in case. -// The filter's diactritics are kept as-is, but the value's are normalized, -// so that a missing diactritic in the filter still returns a result. -func latinMatchingFilter(filter, value string, index int) bool { - filter = strings.ToLower(filter) - value = strings.ToLower(value) - // include this option if it matches. - return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter) + return p.pClient.Confirm(prompt, defaultValue) } diff --git a/pkg/prompter/prompter_accessible.go b/pkg/prompter/prompter_accessible.go new file mode 100644 index 0000000..21afc92 --- /dev/null +++ b/pkg/prompter/prompter_accessible.go @@ -0,0 +1,127 @@ +package prompter + +import ( + "io" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" +) + +type AccessiblePrompter struct { + stdin io.Reader + stdout io.Writer + stderr io.Writer +} + +func NewAccessiblePrompter(stdin io.Reader, stdout io.Writer, stderr io.Writer) *AccessiblePrompter { + return &AccessiblePrompter{ + stdin: stdin, + stdout: stdout, + stderr: stderr, + } +} + +func (p *AccessiblePrompter) newForm(groups ...*huh.Group) *huh.Form { + return huh.NewForm(groups...). + WithTheme(huh.ThemeBase16()). + WithAccessible(os.Getenv("ACCESSIBLE") != ""). + WithProgramOptions(tea.WithOutput(p.stdout), tea.WithInput(p.stdin)) +} + +// Select prompts the user to select an option from a list of options. +func (p *AccessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) { + var result int + formOptions := []huh.Option[int]{} + for i, o := range options { + formOptions = append(formOptions, huh.NewOption(o, i)) + + if o == defaultValue { + result = i + } + } + + form := p.newForm( + huh.NewGroup( + huh.NewSelect[int](). + Title(prompt). + Value(&result). + Options(formOptions...), + ), + ) + + err := form.Run() + return result, err +} + +// MultiSelect prompts the user to select multiple options from a list of options. +func (p *AccessiblePrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + var result []int + formOptions := []huh.Option[int]{} + for i, o := range options { + formOptions = append(formOptions, huh.NewOption(o, i)) + + for _, d := range defaultValues { + if d == o { + result = append(result, i) + } + } + } + + form := p.newForm( + huh.NewGroup( + huh.NewMultiSelect[int](). + Title(prompt). + Value(&result). + Options(formOptions...), + ), + ) + + err := form.Run() + return result, err +} + +// Input prompts the user to input a single-line string. +func (p *AccessiblePrompter) Input(prompt, defaultValue string) (string, error) { + result := defaultValue + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title(prompt). + Value(&result), + ), + ) + + err := form.Run() + return result, err +} + +// Password prompts the user to input a single-line string without echoing the input. +func (p *AccessiblePrompter) Password(prompt string) (string, error) { + var result string + form := p.newForm( + huh.NewGroup( + huh.NewInput(). + Title(prompt). + Value(&result). + EchoMode(huh.EchoModePassword), + ), + ) + + err := form.Run() + return result, err +} + +// Confirm prompts the user to confirm a yes/no question. +func (p *AccessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) { + result := defaultValue + form := p.newForm( + huh.NewGroup( + huh.NewConfirm(). + Title(prompt). + Value(&result), + ), + ) + err := form.Run() + return result, err +} diff --git a/pkg/prompter/prompter_legacy.go b/pkg/prompter/prompter_legacy.go new file mode 100644 index 0000000..bfab02a --- /dev/null +++ b/pkg/prompter/prompter_legacy.go @@ -0,0 +1,117 @@ +package prompter + +import ( + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/go-gh/v2/pkg/text" +) + +type LegacyPrompter struct { + stdin FileReader + stdout FileWriter + stderr FileWriter +} + +func NewLegacyPrompter(stdin FileReader, stdout FileWriter, stderr FileWriter) *LegacyPrompter { + return &LegacyPrompter{ + stdin: stdin, + stdout: stdout, + stderr: stderr, + } +} + +// Select prompts the user to select an option from a list of options. +func (p *LegacyPrompter) Select(prompt, defaultValue string, options []string) (int, error) { + var result int + q := &survey.Select{ + Message: prompt, + Options: options, + PageSize: 20, + Filter: latinMatchingFilter, + } + if defaultValue != "" { + for _, o := range options { + if o == defaultValue { + q.Default = defaultValue + break + } + } + } + err := p.ask(q, &result) + return result, err +} + +// MultiSelect prompts the user to select multiple options from a list of options. +func (p *LegacyPrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + var result []int + q := &survey.MultiSelect{ + Message: prompt, + Options: options, + PageSize: 20, + Filter: latinMatchingFilter, + } + if len(defaultValues) > 0 { + validatedDefault := []string{} + for _, x := range defaultValues { + for _, y := range options { + if x == y { + validatedDefault = append(validatedDefault, x) + } + } + } + q.Default = validatedDefault + } + err := p.ask(q, &result) + return result, err +} + +// Input prompts the user to input a single-line string. +func (p *LegacyPrompter) Input(prompt, defaultValue string) (string, error) { + var result string + err := p.ask(&survey.Input{ + Message: prompt, + Default: defaultValue, + }, &result) + return result, err +} + +// Password prompts the user to input a single-line string without echoing the input. +func (p *LegacyPrompter) Password(prompt string) (string, error) { + var result string + err := p.ask(&survey.Password{ + Message: prompt, + }, &result) + return result, err +} + +// Confirm prompts the user to confirm a yes/no question. +func (p *LegacyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { + var result bool + err := p.ask(&survey.Confirm{ + Message: prompt, + Default: defaultValue, + }, &result) + return result, err +} + +func (p *LegacyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { + opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) + err := survey.AskOne(q, response, opts...) + if err == nil { + return nil + } + return fmt.Errorf("could not prompt: %w", err) +} + +// latinMatchingFilter returns whether the value matches the input filter. +// The strings are compared normalized in case. +// The filter's diactritics are kept as-is, but the value's are normalized, +// so that a missing diactritic in the filter still returns a result. +func latinMatchingFilter(filter, value string, index int) bool { + filter = strings.ToLower(filter) + value = strings.ToLower(value) + // include this option if it matches. + return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter) +}