Merge pull request #10710 from cli/kw/first-pass-accessible-prompter
Introduce accessible prompter for screen readers (preview)
This commit is contained in:
commit
dec71dd9e1
5 changed files with 775 additions and 8 deletions
14
go.mod
14
go.mod
|
|
@ -7,9 +7,11 @@ toolchain go1.23.5
|
|||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
|
||||
github.com/cli/go-gh/v2 v2.12.0
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
|
||||
|
|
@ -29,6 +31,7 @@ require (
|
|||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/henvic/httpretty v0.1.4
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
|
||||
github.com/in-toto/attestation v1.1.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
|
|
@ -64,12 +67,17 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.2 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.4 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // 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/cli/browser v1.3.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.4 // indirect
|
||||
|
|
@ -82,6 +90,8 @@ require (
|
|||
github.com/docker/cli v27.5.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
|
|
@ -117,12 +127,16 @@ require (
|
|||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
|
|
|
|||
29
go.sum
29
go.sum
|
|
@ -52,6 +52,8 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u
|
|||
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
|
|
@ -94,22 +96,32 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
|||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
|
||||
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
|
||||
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
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/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g=
|
||||
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s=
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 h1:uOnMxWghHfEYm2DPMeIHHAEirV/TduBVC9ZRXGcX9Q8=
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5/go.mod h1:xl27E/xNaX3WwdkqpvBwjJcGWhupkU52CWLC5hReBTw=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
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/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
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/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
|
|
@ -158,6 +170,10 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
|
|||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
|
|
@ -335,6 +351,8 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
|
|||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
|
|
@ -351,10 +369,16 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
|
|||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
|
|
@ -542,6 +566,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
|||
493
internal/prompter/accessible_prompter_test.go
Normal file
493
internal/prompter/accessible_prompter_test.go
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
//go:build !windows
|
||||
|
||||
package prompter_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Netflix/go-expect"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/creack/pty"
|
||||
"github.com/hinshun/vt10x"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The following tests are broadly testing the accessible prompter, and NOT asserting
|
||||
// on the prompter's complete and exact output strings.
|
||||
//
|
||||
// These tests generally operate with this logic:
|
||||
// - Wait for a particular substring (a portion of the prompt) to appear
|
||||
// - Send input
|
||||
// - Wait for another substring to appear or for control to return to the test
|
||||
// - Assert that the input value was returned from the prompter function
|
||||
|
||||
// In the future, expanding these tests to assert on the exact prompt strings
|
||||
// would help build confidence in `huh` upgrades, but for now these tests
|
||||
// are sufficient to ensure that the accessible prompter behaves roughly as expected
|
||||
// but doesn't mandate that prompts always look exactly the same.
|
||||
func TestAccessiblePrompter(t *testing.T) {
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Choose:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select option 1
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, selectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select options 1 and 2
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValue, err := p.MultiSelect("Select a number", []string{}, []string{"1", "2", "3"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("Input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
dummyText := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Enter some characters")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter a number
|
||||
_, err = console.SendLine(dummyText)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.Input("Enter some characters", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dummyText, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Input - blank input returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Enter some characters")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dummyDefaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Password", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
dummyPassword := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Enter password")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter a number
|
||||
_, err = console.SendLine(dummyPassword)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
passwordValue, err := p.Password("Enter password")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyPassword, passwordValue)
|
||||
})
|
||||
|
||||
t.Run("Confirm", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Are you sure")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm
|
||||
_, err = console.SendLine("y")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
confirmValue, err := p.Confirm("Are you sure", false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("Confirm - blank input returns default", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Are you sure")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
confirmValue, err := p.Confirm("Are you sure", false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("AuthToken", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
dummyAuthToken := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Paste your authentication token:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter some dummy auth token
|
||||
_, err = console.SendLine(dummyAuthToken)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
authValue, err := p.AuthToken()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyAuthToken, authValue)
|
||||
})
|
||||
|
||||
t.Run("AuthToken - blank input returns error", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
dummyAuthTokenForAfterFailure := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Paste your authentication token:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect an error message
|
||||
_, err = console.ExpectString("token is required")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now enter some dummy auth token to return control back to the test
|
||||
_, err = console.SendLine(dummyAuthTokenForAfterFailure)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
authValue, err := p.AuthToken()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyAuthTokenForAfterFailure, authValue)
|
||||
})
|
||||
|
||||
t.Run("ConfirmDeletion", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
requiredValue := "test"
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm
|
||||
_, err = console.SendLine(requiredValue)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// An err indicates that the confirmation text sent did not match
|
||||
err := p.ConfirmDeletion(requiredValue)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
requiredValue := "test"
|
||||
badInputValue := "garbage"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString(fmt.Sprintf("Type %q to confirm deletion", requiredValue))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm with bad input
|
||||
_, err = console.SendLine(badInputValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect an error message and loop back to the prompt
|
||||
_, err = console.ExpectString(fmt.Sprintf("You entered: %q", badInputValue))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm with the correct input to return control back to the test
|
||||
_, err = console.SendLine(requiredValue)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// An err indicates that the confirmation text sent did not match
|
||||
err := p.ConfirmDeletion(requiredValue)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("InputHostname", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
hostname := "example.com"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Hostname:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter the hostname
|
||||
_, err = console.SendLine(hostname)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.InputHostname()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, hostname, inputValue)
|
||||
})
|
||||
|
||||
t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("How to edit?")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter 2, to select "skip"
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.MarkdownEditor("How to edit?", "", true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", inputValue)
|
||||
})
|
||||
|
||||
t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
defaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("How to edit?")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter number 2 to select "skip". This shouldn't be allowed.
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect a notice to enter something valid since blank is disallowed.
|
||||
_, err = console.ExpectString("invalid input. please try again")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send a 1 to select to open the editor. This will immediately exit
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.MarkdownEditor("How to edit?", defaultValue, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, defaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("How to edit?")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter number 2 to select "skip". This shouldn't be allowed.
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect a notice to enter something valid since blank is disallowed.
|
||||
_, err = console.ExpectString("invalid input. please try again")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send a 1 to select to open the editor since skip is invalid and
|
||||
// we need to return control back to the test.
|
||||
_, err = console.SendLine("1")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.MarkdownEditor("How to edit?", "", false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", inputValue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSurveyPrompter(t *testing.T) {
|
||||
// This not a comprehensive test of the survey prompter, but it does
|
||||
// demonstrate that the survey prompter is used when the
|
||||
// accessible prompter is disabled.
|
||||
t.Run("Select uses survey prompter when accessible prompter is disabled", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestSurveyPrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send a newline to select the first option
|
||||
// Note: This would not work with the accessible prompter
|
||||
// because it would requires sending a 1 to select the first option.
|
||||
// So it proves we are seeing a survey prompter.
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", "", []string{"1", "2", "3"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, selectValue)
|
||||
})
|
||||
}
|
||||
|
||||
func newTestVirtualTerminal(t *testing.T) *expect.Console {
|
||||
t.Helper()
|
||||
|
||||
// Create a PTY and hook up a virtual terminal emulator
|
||||
ptm, pts, err := pty.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
term := vt10x.New(vt10x.WithWriter(pts))
|
||||
|
||||
// Create a console via Expect that allows scripting against the terminal
|
||||
consoleOpts := []expect.ConsoleOpt{
|
||||
expect.WithStdin(ptm),
|
||||
expect.WithStdout(term),
|
||||
expect.WithCloser(ptm, pts),
|
||||
failOnExpectError(t),
|
||||
failOnSendError(t),
|
||||
expect.WithDefaultTimeout(time.Second),
|
||||
}
|
||||
|
||||
console, err := expect.NewConsole(consoleOpts...)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { testCloser(t, console) })
|
||||
|
||||
return console
|
||||
}
|
||||
|
||||
func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
|
||||
// `echo`` is chose as the editor command because it immediately returns
|
||||
// a success exit code, returns an empty string, doesn't require any user input,
|
||||
// and since this file is only built on Linux, it is near guaranteed to be available.
|
||||
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
|
||||
}
|
||||
|
||||
func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("GH_ACCESSIBLE_PROMPTER", "false")
|
||||
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
|
||||
}
|
||||
|
||||
// failOnExpectError adds an observer that will fail the test in a standardised way
|
||||
// if any expectation on the command output fails, without requiring an explicit
|
||||
// assertion.
|
||||
//
|
||||
// Use WithRelaxedIO to disable this behaviour.
|
||||
func failOnExpectError(t *testing.T) expect.ConsoleOpt {
|
||||
t.Helper()
|
||||
return expect.WithExpectObserver(
|
||||
func(matchers []expect.Matcher, buf string, err error) {
|
||||
t.Helper()
|
||||
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(matchers) == 0 {
|
||||
t.Fatalf("Error occurred while matching %q: %s\n", buf, err)
|
||||
}
|
||||
|
||||
var criteria []string
|
||||
for _, matcher := range matchers {
|
||||
criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria()))
|
||||
}
|
||||
t.Fatalf("Failed to find [%s] in %q: %s\n", strings.Join(criteria, ", "), buf, err)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// failOnSendError adds an observer that will fail the test in a standardised way
|
||||
// if any sending of input fails, without requiring an explicit assertion.
|
||||
//
|
||||
// Use WithRelaxedIO to disable this behaviour.
|
||||
func failOnSendError(t *testing.T) expect.ConsoleOpt {
|
||||
t.Helper()
|
||||
return expect.WithSendObserver(
|
||||
func(msg string, n int, err error) {
|
||||
t.Helper()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send %q: %s\n", msg, err)
|
||||
}
|
||||
if len(msg) != n {
|
||||
t.Fatalf("Only sent %d of %d bytes for %q\n", n, len(msg), msg)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// testCloser is a helper to fail the test if a Closer fails to close.
|
||||
func testCloser(t *testing.T, closer io.Closer) {
|
||||
t.Helper()
|
||||
if err := closer.Close(); err != nil {
|
||||
t.Errorf("Close failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,12 @@ package prompter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
|
||||
|
|
@ -13,20 +16,46 @@ import (
|
|||
//go:generate moq -rm -out prompter_mock.go . Prompter
|
||||
type Prompter interface {
|
||||
// generic prompts from go-gh
|
||||
Select(string, string, []string) (int, error)
|
||||
|
||||
// Select prompts the user to select an option from a list of options.
|
||||
Select(prompt string, defaultValue string, options []string) (int, error)
|
||||
// MultiSelect prompts the user to select one or more options from a list of options.
|
||||
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
|
||||
Input(string, string) (string, error)
|
||||
Password(string) (string, error)
|
||||
Confirm(string, bool) (bool, error)
|
||||
// Input prompts the user to enter a string value.
|
||||
Input(prompt string, defaultValue string) (string, error)
|
||||
// Password prompts the user to enter a password.
|
||||
Password(prompt string) (string, error)
|
||||
// Confirm prompts the user to confirm an action.
|
||||
Confirm(prompt string, defaultValue bool) (bool, error)
|
||||
|
||||
// gh specific prompts
|
||||
|
||||
// AuthToken prompts the user to enter an authentication token.
|
||||
AuthToken() (string, error)
|
||||
ConfirmDeletion(string) error
|
||||
// ConfirmDeletion prompts the user to confirm deletion of a resource by
|
||||
// typing the requiredValue.
|
||||
ConfirmDeletion(requiredValue string) error
|
||||
// InputHostname prompts the user to enter a hostname.
|
||||
InputHostname() (string, error)
|
||||
MarkdownEditor(string, string, bool) (string, error)
|
||||
// MarkdownEditor prompts the user to edit a markdown document in an editor.
|
||||
// If blankAllowed is true, the user can skip the editor and an empty string
|
||||
// will be returned.
|
||||
MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
|
||||
}
|
||||
|
||||
func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter {
|
||||
accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
|
||||
falseyValues := []string{"false", "0", "no", ""}
|
||||
|
||||
if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) {
|
||||
return &accessiblePrompter{
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
editorCmd: editorCmd,
|
||||
}
|
||||
}
|
||||
|
||||
return &surveyPrompter{
|
||||
prompter: ghPrompter.New(stdin, stdout, stderr),
|
||||
stdin: stdin,
|
||||
|
|
@ -36,6 +65,209 @@ func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWr
|
|||
}
|
||||
}
|
||||
|
||||
type accessiblePrompter struct {
|
||||
stdin ghPrompter.FileReader
|
||||
stdout ghPrompter.FileWriter
|
||||
stderr ghPrompter.FileWriter
|
||||
editorCmd string
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form {
|
||||
return huh.NewForm(groups...).
|
||||
WithTheme(huh.ThemeBase16()).
|
||||
WithAccessible(true).
|
||||
WithInput(p.stdin).
|
||||
WithOutput(p.stdout)
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) {
|
||||
var result int
|
||||
formOptions := []huh.Option[int]{}
|
||||
for i, o := range options {
|
||||
formOptions = append(formOptions, huh.NewOption(o, i))
|
||||
}
|
||||
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[int]().
|
||||
Title(prompt).
|
||||
Value(&result).
|
||||
Options(formOptions...),
|
||||
),
|
||||
)
|
||||
|
||||
err := form.Run()
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
var result []int
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewMultiSelect[int]().
|
||||
Title(prompt).
|
||||
Value(&result).
|
||||
Limit(len(options)).
|
||||
Options(formOptions...),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
result := defaultValue
|
||||
prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue)
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
err := form.Run()
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Password(prompt string) (string, error) {
|
||||
var result string
|
||||
// EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode.
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
result := defaultValue
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
)
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) AuthToken() (string, error) {
|
||||
var result string
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Paste your authentication token:").
|
||||
// Note: if this validation fails, the prompt loops.
|
||||
Validate(func(input string) error {
|
||||
if input == "" {
|
||||
return fmt.Errorf("token is required")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Value(&result),
|
||||
// This doesn't have any effect in accessible mode.
|
||||
// EchoMode(huh.EchoModePassword),
|
||||
),
|
||||
)
|
||||
|
||||
err := form.Run()
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) ConfirmDeletion(requiredValue string) error {
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(fmt.Sprintf("Type %q to confirm deletion", requiredValue)).
|
||||
Validate(func(input string) error {
|
||||
if input != requiredValue {
|
||||
return fmt.Errorf("You entered: %q", input)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) InputHostname() (string, error) {
|
||||
var result string
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Hostname:").
|
||||
Validate(ghinstance.HostnameValidator).
|
||||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
err := form.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
var result string
|
||||
skipOption := "skip"
|
||||
launchOption := "launch"
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption(fmt.Sprintf("Launch %s", surveyext.EditorName(p.editorCmd)), launchOption),
|
||||
}
|
||||
if blankAllowed {
|
||||
options = append(options, huh.NewOption("Skip", skipOption))
|
||||
}
|
||||
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title(prompt).
|
||||
Options(options...).
|
||||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result == skipOption {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// launchOption was selected
|
||||
text, err := surveyext.Edit(p.editorCmd, "*.md", defaultValue, p.stdin, p.stdout, p.stderr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
||||
type surveyPrompter struct {
|
||||
prompter *ghPrompter.Prompter
|
||||
stdin ghPrompter.FileReader
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ var HelpTopics = []helpTopic{
|
|||
%[1]sGH_MDWIDTH%[1]s: default maximum width for markdown render wrapping. The max width of lines
|
||||
wrapped on the terminal will be taken as the lesser of the terminal width, this value, or 120 if
|
||||
not specified. This value is used, for example, with %[1]spr view%[1]s subcommand.
|
||||
|
||||
%[1]sGH_ACCESSIBLE_PROMPTER%[1]s (preview): set to a truthy value to enable prompts that are
|
||||
more compatible with speech synthesis and braille screen readers.
|
||||
`, "`"),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue