Use Survey For Interactions With User (#186)
fixes Use Survey For Interactions With User Add Vendor "github.com/AlecAivazis/survey/v2" Co-authored-by: Norwin Roosen <git@nroo.de> Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/186 Reviewed-by: techknowlogick <techknowlogick@gitea.io> Reviewed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
parent
e23f56e81c
commit
7ac3ffcc1b
1
go.mod
1
go.mod
|
@ -5,6 +5,7 @@ go 1.12
|
||||||
require (
|
require (
|
||||||
code.gitea.io/gitea-vet v0.2.0
|
code.gitea.io/gitea-vet v0.2.0
|
||||||
code.gitea.io/sdk/gitea v0.13.0
|
code.gitea.io/sdk/gitea v0.13.0
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.1.1
|
||||||
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1
|
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1
|
||||||
github.com/charmbracelet/glamour v0.2.0
|
github.com/charmbracelet/glamour v0.2.0
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
|
|
19
go.sum
19
go.sum
|
@ -2,7 +2,11 @@ code.gitea.io/gitea-vet v0.2.0 h1:xkUePzbHI8e0qp4Aly4GBSd0+6cqEMVTrdZq57fPozo=
|
||||||
code.gitea.io/gitea-vet v0.2.0/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
code.gitea.io/gitea-vet v0.2.0/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||||
code.gitea.io/sdk/gitea v0.13.0 h1:iHognp8ZMhMFLooUUNZFpm8IHaC9qoHJDvAE5vTm5aw=
|
code.gitea.io/sdk/gitea v0.13.0 h1:iHognp8ZMhMFLooUUNZFpm8IHaC9qoHJDvAE5vTm5aw=
|
||||||
code.gitea.io/sdk/gitea v0.13.0/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
|
code.gitea.io/sdk/gitea v0.13.0/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI=
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
||||||
|
@ -56,29 +60,40 @@ github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+d
|
||||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||||
|
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
@ -110,6 +125,7 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
@ -126,6 +142,7 @@ github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc=
|
||||||
github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
@ -150,7 +167,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||||
|
|
|
@ -9,18 +9,20 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/tea/modules/config"
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateLogin create an login interactive
|
// CreateLogin create an login interactive
|
||||||
func CreateLogin() error {
|
func CreateLogin() error {
|
||||||
var stdin, name, token, user, passwd, sshKey, giteaURL string
|
var name, token, user, passwd, sshKey, giteaURL string
|
||||||
var insecure = false
|
var insecure = false
|
||||||
|
|
||||||
fmt.Print("URL of Gitea instance: ")
|
promptI := &survey.Input{Message: "URL of Gitea instance: "}
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
if err := survey.AskOne(promptI, &giteaURL, survey.WithValidator(survey.Required)); err != nil {
|
||||||
stdin = ""
|
return err
|
||||||
}
|
}
|
||||||
giteaURL = strings.TrimSpace(stdin)
|
giteaURL = strings.TrimSuffix(strings.TrimSpace(giteaURL), "/")
|
||||||
if len(giteaURL) == 0 {
|
if len(giteaURL) == 0 {
|
||||||
fmt.Println("URL is required!")
|
fmt.Println("URL is required!")
|
||||||
return nil
|
return nil
|
||||||
|
@ -31,54 +33,58 @@ func CreateLogin() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Name of new Login [" + name + "]: ")
|
promptI = &survey.Input{Message: "Name of new Login [" + name + "]: "}
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
if err := survey.AskOne(promptI, &name); err != nil {
|
||||||
stdin = ""
|
return err
|
||||||
}
|
|
||||||
if len(strings.TrimSpace(stdin)) != 0 {
|
|
||||||
name = strings.TrimSpace(stdin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Do you have a token [Yes/no]: ")
|
var hasToken bool
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
promptYN := &survey.Confirm{
|
||||||
stdin = ""
|
Message: "Do you have an access token?",
|
||||||
|
Default: false,
|
||||||
|
}
|
||||||
|
if err = survey.AskOne(promptYN, &hasToken); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if len(stdin) != 0 && strings.ToLower(stdin[:1]) == "n" {
|
|
||||||
fmt.Print("Username: ")
|
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
|
||||||
stdin = ""
|
|
||||||
}
|
|
||||||
user = strings.TrimSpace(stdin)
|
|
||||||
|
|
||||||
fmt.Print("Password: ")
|
if hasToken {
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
promptI = &survey.Input{Message: "Token: "}
|
||||||
stdin = ""
|
if err := survey.AskOne(promptI, &token, survey.WithValidator(survey.Required)); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
passwd = strings.TrimSpace(stdin)
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Print("Token: ")
|
promptI = &survey.Input{Message: "Username: "}
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
if err = survey.AskOne(promptI, &user, survey.WithValidator(survey.Required)); err != nil {
|
||||||
stdin = ""
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
promptPW := &survey.Password{Message: "Password: "}
|
||||||
|
if err = survey.AskOne(promptPW, &passwd, survey.WithValidator(survey.Required)); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
token = strings.TrimSpace(stdin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Set Optional settings [yes/No]: ")
|
var optSettings bool
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
promptYN = &survey.Confirm{
|
||||||
stdin = ""
|
Message: "Set Optional settings: ",
|
||||||
|
Default: false,
|
||||||
}
|
}
|
||||||
if len(stdin) != 0 && strings.ToLower(stdin[:1]) == "y" {
|
if err = survey.AskOne(promptYN, &optSettings); err != nil {
|
||||||
fmt.Print("SSH Key Path: ")
|
return err
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
}
|
||||||
stdin = ""
|
if optSettings {
|
||||||
|
promptI = &survey.Input{Message: "SSH Key Path: "}
|
||||||
|
if err := survey.AskOne(promptI, &sshKey); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
sshKey = strings.TrimSpace(stdin)
|
|
||||||
|
|
||||||
fmt.Print("Allow Insecure connections [yes/No]: ")
|
promptYN = &survey.Confirm{
|
||||||
if _, err := fmt.Scanln(&stdin); err != nil {
|
Message: "Allow Insecure connections: ",
|
||||||
stdin = ""
|
Default: false,
|
||||||
|
}
|
||||||
|
if err = survey.AskOne(promptYN, &insecure); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
insecure = len(stdin) != 0 && strings.ToLower(stdin[:1]) == "y"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.AddLogin(name, token, user, passwd, sshKey, giteaURL, insecure)
|
return config.AddLogin(name, token, user, passwd, sshKey, giteaURL, insecure)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.12
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- linux-ppc64le
|
||||||
|
- osx
|
||||||
|
- windows
|
||||||
|
|
||||||
|
go_import_path: github.com/AlecAivazis/survey/v2
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get github.com/alecaivazis/run
|
||||||
|
|
||||||
|
install:
|
||||||
|
- run install-deps
|
||||||
|
|
||||||
|
script:
|
||||||
|
- run tests
|
||||||
|
# - run autoplay-tests
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Contributing to Survey
|
||||||
|
|
||||||
|
🎉🎉 First off, thanks for the interest in contributing to `survey`! 🎉🎉
|
||||||
|
|
||||||
|
The following is a set of guidelines to follow when contributing to this package. These are not hard rules, please use common sense and feel free to propose changes to this document in a pull request.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Code of Conduct](#code-of-conduct)
|
||||||
|
1. [Getting Help](#getting-help)
|
||||||
|
1. [Filing a Bug Report](#how-to-file-a-bug-report)
|
||||||
|
1. [Suggesting an API change](#suggesting-an-api-change)
|
||||||
|
1. [Submitting a Contribution](#submitting-a-contribution)
|
||||||
|
1. [Writing and Running Tests](#writing-and-running-tests)
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
This project and its contibutors are expected to uphold the [Go Community Code of Conduct](https://golang.org/conduct). By participating, you are expected to follow these guidelines.
|
||||||
|
|
||||||
|
## Getting help
|
||||||
|
|
||||||
|
Feel free to [open up an issue](https://github.com/AlecAivazis/survey/v2/issues/new) on GitHub when asking a question so others will be able to find it. Please remember to tag the issue with the `Question` label so the maintainers can get to your question as soon as possible. If the question is urgent, feel free to reach out to `@AlecAivazis` directly in the gophers slack channel.
|
||||||
|
|
||||||
|
## How to file a bug report
|
||||||
|
|
||||||
|
Bugs are tracked using the Github Issue tracker. When filing a bug, please remember to label the issue as a `Bug` and answer/provide the following:
|
||||||
|
|
||||||
|
1. What operating system and terminal are you using?
|
||||||
|
1. An example that showcases the bug.
|
||||||
|
1. What did you expect to see?
|
||||||
|
1. What did you see instead?
|
||||||
|
|
||||||
|
## Suggesting an API change
|
||||||
|
|
||||||
|
If you have an idea, I'm more than happy to discuss it. Please open an issue and we can work through it. In order to maintain some sense of stability, additions to the top-level API are taken just as seriously as changes that break it. Adding stuff is much easier than removing it.
|
||||||
|
|
||||||
|
## Submitting a contribution
|
||||||
|
|
||||||
|
In order to maintain stability, most features get fully integrated in more than one PR. This allows for more opportunity to think through each API change without amassing large amounts of tech debt and API changes at once. If your feature can be broken into separate chunks, it will be able to be reviewed much quicker. For example, if the PR that implemented the `Validate` field was submitted in a PR separately from one that included `survey.Required`, it would be able to get merge without having to decide how many different `Validators` we want to provide as part of `survey`'s API.
|
||||||
|
|
||||||
|
When submitting a contribution,
|
||||||
|
|
||||||
|
- Provide a description of the feature or change
|
||||||
|
- Reference the ticket addressed by the PR if there is one
|
||||||
|
- Following community standards, add comments for all exported members so that all necessary information is available on godocs
|
||||||
|
- Remember to update the project README.md with changes to the high-level API
|
||||||
|
- Include both positive and negative unit tests (when applicable)
|
||||||
|
- Contributions with visual ramifications or interaction changes should be accompanied with the appropriate `go-expect` tests. For more information on writing these tests, see [Writing and Running Tests](#writing-and-running-tests)
|
||||||
|
|
||||||
|
## Writing and running tests
|
||||||
|
|
||||||
|
When submitting features, please add as many units tests as necessary to test both positive and negative cases.
|
||||||
|
|
||||||
|
Integration tests for survey uses [go-expect](https://github.com/Netflix/go-expect) to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY, you need a way to interpret terminal / ANSI escape sequences for things like `CursorLocation`. The stdin/stdout handled by `go-expect` is also multiplexed to a [virtual terminal](https://github.com/hinshun/vt10x).
|
||||||
|
|
||||||
|
For example, you can extend the tests for Input by specifying the following test case:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
"Test Input prompt interaction", // Name of the test.
|
||||||
|
&Input{ // An implementation of the survey.Prompt interface.
|
||||||
|
Message: "What is your name?",
|
||||||
|
},
|
||||||
|
func(c *expect.Console) { // An expect procedure. You can expect strings / regexps and
|
||||||
|
c.ExpectString("What is your name?") // write back strings / bytes to its psuedoterminal for survey.
|
||||||
|
c.SendLine("Johnny Appleseed")
|
||||||
|
c.ExpectEOF() // Nothing is read from the tty without an expect, and once an
|
||||||
|
// expectation is met, no further bytes are read. End your
|
||||||
|
// procedure with `c.ExpectEOF()` to read until survey finishes.
|
||||||
|
},
|
||||||
|
"Johnny Appleseed", // The expected result.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to write your own `go-expect` test from scratch, you'll need to instantiate a virtual terminal,
|
||||||
|
multiplex it into an `*expect.Console`, and hook up its tty with survey's optional stdio. Please see `go-expect`
|
||||||
|
[documentation](https://godoc.org/github.com/Netflix/go-expect) for more detail.
|
|
@ -0,0 +1,78 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/Netflix/go-expect"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "c93bf25de8e869da25cf26bcd2932b36141f61ae"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/davecgh/go-spew"
|
||||||
|
packages = ["spew"]
|
||||||
|
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/hinshun/vt10x"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "1954e646417484a2a687ea344edade2c2b6523c8"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kballard/go-shellquote"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "95032a82bc518f77982ea72343cc1ade730072f0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/kr/pty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
|
||||||
|
version = "v1.1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||||
|
version = "v0.0.9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mgutz/ansi"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pmezard/go-difflib"
|
||||||
|
packages = ["difflib"]
|
||||||
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
packages = [
|
||||||
|
"assert",
|
||||||
|
"require"
|
||||||
|
]
|
||||||
|
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||||
|
version = "v1.2.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "371508ebad4798adc38a118f858b5c17a65b58594203548f9feb74cb781dd907"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
#
|
||||||
|
# [prune]
|
||||||
|
# non-go = false
|
||||||
|
# go-tests = true
|
||||||
|
# unused-packages = true
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/Netflix/go-expect"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/hinshun/vt10x"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
version = "0.0.3"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mgutz/ansi"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/stretchr/testify"
|
||||||
|
version = "1.2.1"
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
unused-packages = true
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kballard/go-shellquote"
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Alec Aivazis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,454 @@
|
||||||
|
# Survey
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/survey)
|
||||||
|
[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://pkg.go.dev/github.com/AlecAivazis/survey/v2)
|
||||||
|
|
||||||
|
A library for building interactive prompts.
|
||||||
|
|
||||||
|
<img width="550" src="https://thumbs.gfycat.com/VillainousGraciousKouprey-size_restricted.gif"/>
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the questions to ask
|
||||||
|
var qs = []*survey.Question{
|
||||||
|
{
|
||||||
|
Name: "name",
|
||||||
|
Prompt: &survey.Input{Message: "What is your name?"},
|
||||||
|
Validate: survey.Required,
|
||||||
|
Transform: survey.Title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "color",
|
||||||
|
Prompt: &survey.Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"red", "blue", "green"},
|
||||||
|
Default: "red",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "age",
|
||||||
|
Prompt: &survey.Input{Message: "How old are you?"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// the answers will be written to this struct
|
||||||
|
answers := struct {
|
||||||
|
Name string // survey will match the question and field names
|
||||||
|
FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
|
||||||
|
Age int // if the types don't match, survey will convert it
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// perform the questions
|
||||||
|
err := survey.Ask(qs, &answers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Examples](#examples)
|
||||||
|
1. [Running the Prompts](#running-the-prompts)
|
||||||
|
1. [Prompts](#prompts)
|
||||||
|
1. [Input](#input)
|
||||||
|
1. [Multiline](#multiline)
|
||||||
|
1. [Password](#password)
|
||||||
|
1. [Confirm](#confirm)
|
||||||
|
1. [Select](#select)
|
||||||
|
1. [MultiSelect](#multiselect)
|
||||||
|
1. [Editor](#editor)
|
||||||
|
1. [Filtering Options](#filtering-options)
|
||||||
|
1. [Validation](#validation)
|
||||||
|
1. [Built-in Validators](#built-in-validators)
|
||||||
|
1. [Help Text](#help-text)
|
||||||
|
1. [Changing the input rune](#changing-the-input-rune)
|
||||||
|
1. [Changing the Icons ](#changing-the-icons)
|
||||||
|
1. [Custom Types](#custom-types)
|
||||||
|
1. [Testing](#testing)
|
||||||
|
1. [FAQ](#faq)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Examples can be found in the `examples/` directory. Run them
|
||||||
|
to see basic behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/AlecAivazis/survey/v2
|
||||||
|
|
||||||
|
cd $GOPATH/src/github.com/AlecAivazis/survey
|
||||||
|
|
||||||
|
go run examples/simple.go
|
||||||
|
go run examples/validation.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Prompts
|
||||||
|
|
||||||
|
There are two primary ways to execute prompts and start collecting information from your users: `Ask` and
|
||||||
|
`AskOne`. The primary difference is whether you are interested in collecting a single piece of information
|
||||||
|
or if you have a list of questions to ask whose answers should be collected in a single struct.
|
||||||
|
For most basic usecases, `Ask` should be enough. However, for surveys with complicated branching logic,
|
||||||
|
we recommend that you break out your questions into multiple calls to both of these functions to fit your needs.
|
||||||
|
|
||||||
|
### Configuring the Prompts
|
||||||
|
|
||||||
|
Most prompts take fine-grained configuration through fields on the structs you instantiate. It is also
|
||||||
|
possible to change survey's default behaviors by passing `AskOpts` to either `Ask` or `AskOne`. Examples
|
||||||
|
in this document will do both interchangeably:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
prompt := &Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"red", "blue", "green"},
|
||||||
|
// can pass a validator directly
|
||||||
|
Validate: survey.Required,
|
||||||
|
}
|
||||||
|
|
||||||
|
// or define a default for the single call to `AskOne`
|
||||||
|
// the answer will get written to the color variable
|
||||||
|
survey.AskOne(prompt, &color, survey.WithValidator(survey.Required))
|
||||||
|
|
||||||
|
// or define a default for every entry in a list of questions
|
||||||
|
// the answer will get copied into the matching field of the struct as shown above
|
||||||
|
survey.Ask(questions, &answers, survey.WithValidator(survey.Required))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompts
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/LankyBlindAmericanpainthorse-size_restricted.gif" width="400px"/>
|
||||||
|
|
||||||
|
```golang
|
||||||
|
name := ""
|
||||||
|
prompt := &survey.Input{
|
||||||
|
Message: "ping",
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiline
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/ImperfectShimmeringBeagle-size_restricted.gif" width="400px"/>
|
||||||
|
|
||||||
|
```golang
|
||||||
|
text := ""
|
||||||
|
prompt := &survey.Multiline{
|
||||||
|
Message: "ping",
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/CompassionateSevereHypacrosaurus-size_restricted.gif" width="400px" />
|
||||||
|
|
||||||
|
```golang
|
||||||
|
password := ""
|
||||||
|
prompt := &survey.Password{
|
||||||
|
Message: "Please type your password",
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &password)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confirm
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/UnkemptCarefulGermanpinscher-size_restricted.gif" width="400px"/>
|
||||||
|
|
||||||
|
```golang
|
||||||
|
name := false
|
||||||
|
prompt := &survey.Confirm{
|
||||||
|
Message: "Do you like pie?",
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/GrimFilthyAmazonparrot-size_restricted.gif" width="450px"/>
|
||||||
|
|
||||||
|
```golang
|
||||||
|
color := ""
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"red", "blue", "green"},
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &color)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields and values that come from a `Select` prompt can be one of two different things. If you pass an `int`
|
||||||
|
the field will have the value of the selected index. If you instead pass a string, the string value selected
|
||||||
|
will be written to the field.
|
||||||
|
|
||||||
|
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.
|
||||||
|
|
||||||
|
By default, the select prompt is limited to showing 7 options at a time
|
||||||
|
and will paginate lists of options longer than that. This can be changed a number of ways:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
// as a field on a single select
|
||||||
|
prompt := &survey.MultiSelect{..., PageSize: 10}
|
||||||
|
|
||||||
|
// or as an option to Ask or AskOne
|
||||||
|
survey.AskOne(prompt, &days, survey.WithPageSize(10))
|
||||||
|
```
|
||||||
|
|
||||||
|
### MultiSelect
|
||||||
|
|
||||||
|
![Example](img/multi-select-all-none.gif)
|
||||||
|
|
||||||
|
```golang
|
||||||
|
days := []string{}
|
||||||
|
prompt := &survey.MultiSelect{
|
||||||
|
Message: "What days do you prefer:",
|
||||||
|
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &days)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields and values that come from a `MultiSelect` prompt can be one of two different things. If you pass an `int`
|
||||||
|
the field will have a slice of the selected indices. If you instead pass a string, a slice of the string values
|
||||||
|
selected will be written to the field.
|
||||||
|
|
||||||
|
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.
|
||||||
|
|
||||||
|
By default, the MultiSelect prompt is limited to showing 7 options at a time
|
||||||
|
and will paginate lists of options longer than that. This can be changed a number of ways:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
// as a field on a single select
|
||||||
|
prompt := &survey.MultiSelect{..., PageSize: 10}
|
||||||
|
|
||||||
|
// or as an option to Ask or AskOne
|
||||||
|
survey.AskOne(prompt, &days, survey.WithPageSize(10))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Editor
|
||||||
|
|
||||||
|
Launches the user's preferred editor (defined by the \$VISUAL or \$EDITOR environment variables) on a
|
||||||
|
temporary file. Once the user exits their editor, the contents of the temporary file are read in as
|
||||||
|
the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.
|
||||||
|
|
||||||
|
You can also specify a [pattern](https://golang.org/pkg/io/ioutil/#TempFile) for the name of the temporary file. This
|
||||||
|
can be useful for ensuring syntax highlighting matches your usecase.
|
||||||
|
|
||||||
|
```golang
|
||||||
|
prompt := &survey.Editor{
|
||||||
|
Message: "Shell code snippet",
|
||||||
|
FileName: "*.sh",
|
||||||
|
}
|
||||||
|
|
||||||
|
survey.AskOne(prompt, &content)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering Options
|
||||||
|
|
||||||
|
By default, the user can filter for options in Select and MultiSelects by typing while the prompt
|
||||||
|
is active. This will filter out all options that don't contain the typed string anywhere in their name, ignoring case.
|
||||||
|
|
||||||
|
A custom filter function can also be provided to change this behavior:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
func myFilter(filterValue string, optValue string, optIndex int) bool {
|
||||||
|
// only include the option if it includes the filter and has length greater than 5
|
||||||
|
return strings.Contains(optValue, filterValue) && len(optValue) >= 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure it for a specific prompt
|
||||||
|
&Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"red", "blue", "green"},
|
||||||
|
Filter: myFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
// or define a default for all of the questions
|
||||||
|
survey.AskOne(prompt, &color, survey.WithFilter(myFilter))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keeping the filter active
|
||||||
|
|
||||||
|
By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone.
|
||||||
|
|
||||||
|
However the user can prevent this from happening and keep the filter active for multiple selections in a e.g. MultiSelect:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
// configure it for a specific prompt
|
||||||
|
&Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"light-green", "green", "dark-green", "red"},
|
||||||
|
KeepFilter: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// or define a default for all of the questions
|
||||||
|
survey.AskOne(prompt, &color, survey.WithKeepFilter(true))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Validating individual responses for a particular question can be done by defining a
|
||||||
|
`Validate` field on the `survey.Question` to be validated. This function takes an
|
||||||
|
`interface{}` type and returns an error to show to the user, prompting them for another
|
||||||
|
response. Like usual, validators can be provided directly to the prompt or with `survey.WithValidator`:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
q := &survey.Question{
|
||||||
|
Prompt: &survey.Input{Message: "Hello world validation"},
|
||||||
|
Validate: func (val interface{}) error {
|
||||||
|
// since we are validating an Input, the assertion will always succeed
|
||||||
|
if str, ok := val.(string) ; !ok || len(str) > 10 {
|
||||||
|
return errors.New("This response cannot be longer than 10 characters.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
color := ""
|
||||||
|
prompt := &survey.Input{ Message: "Whats your name?" }
|
||||||
|
|
||||||
|
// you can pass multiple validators here and survey will make sure each one passes
|
||||||
|
survey.AskOne(prompt, &color, survey.WithValidator(survey.Required))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in Validators
|
||||||
|
|
||||||
|
`survey` comes prepackaged with a few validators to fit common situations. Currently these
|
||||||
|
validators include:
|
||||||
|
|
||||||
|
| name | valid types | description | notes |
|
||||||
|
| ------------ | ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| Required | any | Rejects zero values of the response type | Boolean values pass straight through since the zero value (false) is a valid response |
|
||||||
|
| MinLength(n) | string | Enforces that a response is at least the given length | |
|
||||||
|
| MaxLength(n) | string | Enforces that a response is no longer than the given length | |
|
||||||
|
|
||||||
|
## Help Text
|
||||||
|
|
||||||
|
All of the prompts have a `Help` field which can be defined to provide more information to your users:
|
||||||
|
|
||||||
|
<img src="https://thumbs.gfycat.com/CloudyRemorsefulFossa-size_restricted.gif" width="400px" style="margin-top: 8px"/>
|
||||||
|
|
||||||
|
```golang
|
||||||
|
&survey.Input{
|
||||||
|
Message: "What is your phone number:",
|
||||||
|
Help: "Phone number should include the area code",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing the input rune
|
||||||
|
|
||||||
|
In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey
|
||||||
|
looks for with `WithHelpInput`:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import (
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
number := ""
|
||||||
|
prompt := &survey.Input{
|
||||||
|
Message: "If you have this need, please give me a reasonable message.",
|
||||||
|
Help: "I couldn't come up with one.",
|
||||||
|
}
|
||||||
|
|
||||||
|
survey.AskOne(prompt, &number, survey.WithHelpInput('^'))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changing the Icons
|
||||||
|
|
||||||
|
Changing the icons and their color/format can be done by passing the `WithIcons` option. The format
|
||||||
|
follows the patterns outlined [here](https://github.com/mgutz/ansi#style-format). For example:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import (
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
number := ""
|
||||||
|
prompt := &survey.Input{
|
||||||
|
Message: "If you have this need, please give me a reasonable message.",
|
||||||
|
Help: "I couldn't come up with one.",
|
||||||
|
}
|
||||||
|
|
||||||
|
survey.AskOne(prompt, &number, survey.WithIcons(func(icons *survey.IconSet) {
|
||||||
|
// you can set any icons
|
||||||
|
icons.Question.Text = "⁇"
|
||||||
|
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
|
||||||
|
icons.Question.Format = "yellow+hb"
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
The icons and their default text and format are summarized below:
|
||||||
|
|
||||||
|
| name | text | format | description |
|
||||||
|
| -------------- | ---- | ---------- | ------------------------------------------------------------- |
|
||||||
|
| Error | X | red | Before an error |
|
||||||
|
| Help | i | cyan | Before help text |
|
||||||
|
| Question | ? | green+hb | Before the message of a prompt |
|
||||||
|
| SelectFocus | > | green | Marks the current focus in `Select` and `MultiSelect` prompts |
|
||||||
|
| UnmarkedOption | [ ] | default+hb | Marks an unselected option in a `MultiSelect` prompt |
|
||||||
|
| MarkedOption | [x] | cyan+b | Marks a chosen selection in a `MultiSelect` prompt |
|
||||||
|
|
||||||
|
## Custom Types
|
||||||
|
|
||||||
|
survey will assign prompt answers to your custom types if they implement this interface:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
type Settable interface {
|
||||||
|
WriteAnswer(field string, value interface{}) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example how to use them:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
type MyValue struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
func (my *MyValue) WriteAnswer(name string, value interface{}) error {
|
||||||
|
my.value = value.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
myval := MyValue{}
|
||||||
|
survey.AskOne(
|
||||||
|
&survey.Input{
|
||||||
|
Message: "Enter something:",
|
||||||
|
},
|
||||||
|
&myval
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
You can test your program's interactive prompts using [go-expect](https://github.com/Netflix/go-expect). The library
|
||||||
|
can be used to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY,
|
||||||
|
if you are manipulating the cursor or using `survey`, you will need a way to interpret terminal / ANSI escape sequences
|
||||||
|
for things like `CursorLocation`. `vt10x.NewVT10XConsole` will create a `go-expect` console that also multiplexes
|
||||||
|
stdio to an in-memory [virtual terminal](https://github.com/hinshun/vt10x).
|
||||||
|
|
||||||
|
For some examples, you can see any of the tests in this repo.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why isn't sending a SIGINT (aka. CTRL-C) signal working?
|
||||||
|
|
||||||
|
When you send an interrupt signal to the process, it only interrupts the current prompt instead of the entire process. This manifests in a `github.com/AlecAivazis/survey/v2/terminal.InterruptErr` being returned from `Ask` and `AskOne`. If you want to stop the process, handle the returned error in your code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := survey.AskOne(prompt, &myVar)
|
||||||
|
if err == terminal.InterruptErr {
|
||||||
|
fmt.Println("interrupted")
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
} else if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,19 @@
|
||||||
|
task "install-deps" {
|
||||||
|
description = "Install all of package dependencies"
|
||||||
|
pipeline = [
|
||||||
|
"go get {{.files}}",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
task "tests" {
|
||||||
|
description = "Run the test suite"
|
||||||
|
command = "go test {{.files}}"
|
||||||
|
environment {
|
||||||
|
GOFLAGS = "-mod=vendor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variables {
|
||||||
|
files = "$(go list -v ./... | grep -iEv \"tests|examples\")"
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Confirm is a regular text input that accept yes/no answers. Response type is a bool.
|
||||||
|
type Confirm struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Default bool
|
||||||
|
Help string
|
||||||
|
}
|
||||||
|
|
||||||
|
// data available to the templates when processing
|
||||||
|
type ConfirmTemplateData struct {
|
||||||
|
Confirm
|
||||||
|
Answer string
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
var ConfirmQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||||
|
{{- if .Answer}}
|
||||||
|
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||||
|
{{- else }}
|
||||||
|
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
|
||||||
|
{{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
// the regex for answers
|
||||||
|
var (
|
||||||
|
yesRx = regexp.MustCompile("^(?i:y(?:es)?)$")
|
||||||
|
noRx = regexp.MustCompile("^(?i:n(?:o)?)$")
|
||||||
|
)
|
||||||
|
|
||||||
|
func yesNo(t bool) string {
|
||||||
|
if t {
|
||||||
|
return "Yes"
|
||||||
|
}
|
||||||
|
return "No"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) {
|
||||||
|
cursor := c.NewCursor()
|
||||||
|
rr := c.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
// start waiting for input
|
||||||
|
for {
|
||||||
|
line, err := rr.ReadLine(0)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// move back up a line to compensate for the \n echoed from terminal
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
val := string(line)
|
||||||
|
|
||||||
|
// get the answer that matches the
|
||||||
|
var answer bool
|
||||||
|
switch {
|
||||||
|
case yesRx.Match([]byte(val)):
|
||||||
|
answer = true
|
||||||
|
case noRx.Match([]byte(val)):
|
||||||
|
answer = false
|
||||||
|
case val == "":
|
||||||
|
answer = c.Default
|
||||||
|
case val == config.HelpInput && c.Help != "":
|
||||||
|
err := c.Render(
|
||||||
|
ConfirmQuestionTemplate,
|
||||||
|
ConfirmTemplateData{
|
||||||
|
Confirm: *c,
|
||||||
|
ShowHelp: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// use the default value and bubble up
|
||||||
|
return c.Default, err
|
||||||
|
}
|
||||||
|
showHelp = true
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
// we didnt get a valid answer, so print error and prompt again
|
||||||
|
if err := c.Error(config, fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
|
||||||
|
return c.Default, err
|
||||||
|
}
|
||||||
|
err := c.Render(
|
||||||
|
ConfirmQuestionTemplate,
|
||||||
|
ConfirmTemplateData{
|
||||||
|
Confirm: *c,
|
||||||
|
ShowHelp: showHelp,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// use the default value and bubble up
|
||||||
|
return c.Default, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
// should not get here
|
||||||
|
return c.Default, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prompt prompts the user with a simple text field and expects a reply followed
|
||||||
|
by a carriage return.
|
||||||
|
|
||||||
|
likesPie := false
|
||||||
|
prompt := &survey.Confirm{ Message: "What is your name?" }
|
||||||
|
survey.AskOne(prompt, &likesPie)
|
||||||
|
*/
|
||||||
|
func (c *Confirm) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// render the question template
|
||||||
|
err := c.Render(
|
||||||
|
ConfirmQuestionTemplate,
|
||||||
|
ConfirmTemplateData{
|
||||||
|
Confirm: *c,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get input and return
|
||||||
|
return c.getBool(false, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup overwrite the line with the finalized formatted version
|
||||||
|
func (c *Confirm) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
// if the value was previously true
|
||||||
|
ans := yesNo(val.(bool))
|
||||||
|
|
||||||
|
// render the template
|
||||||
|
return c.Render(
|
||||||
|
ConfirmQuestionTemplate,
|
||||||
|
ConfirmTemplateData{
|
||||||
|
Confirm: *c,
|
||||||
|
Answer: ans,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/mgutz/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisableColor can be used to make testing reliable
|
||||||
|
var DisableColor = false
|
||||||
|
|
||||||
|
var TemplateFuncsWithColor = map[string]interface{}{
|
||||||
|
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
"color": ansi.ColorCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
var TemplateFuncsNoColor = map[string]interface{}{
|
||||||
|
// Templates without Color formatting. For layout/ testing.
|
||||||
|
"color": func(color string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//RunTemplate returns two formatted strings given a template and
|
||||||
|
//the data it requires. The first string returned is generated for
|
||||||
|
//user-facing output and may or may not contain ANSI escape codes
|
||||||
|
//for colored output. The second string does not contain escape codes
|
||||||
|
//and can be used by the renderer for layout purposes.
|
||||||
|
func RunTemplate(tmpl string, data interface{}) (string, string, error) {
|
||||||
|
tPair, err := getTemplatePair(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
userBuf := bytes.NewBufferString("")
|
||||||
|
err = tPair[0].Execute(userBuf, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
layoutBuf := bytes.NewBufferString("")
|
||||||
|
err = tPair[1].Execute(layoutBuf, data)
|
||||||
|
if err != nil {
|
||||||
|
return userBuf.String(), "", err
|
||||||
|
}
|
||||||
|
return userBuf.String(), layoutBuf.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
memoizedGetTemplate = map[string][2]*template.Template{}
|
||||||
|
|
||||||
|
memoMutex = &sync.RWMutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
//getTemplatePair returns a pair of compiled templates where the
|
||||||
|
//first template is generated for user-facing output and the
|
||||||
|
//second is generated for use by the renderer. The second
|
||||||
|
//template does not contain any color escape codes, whereas
|
||||||
|
//the first template may or may not depending on DisableColor.
|
||||||
|
func getTemplatePair(tmpl string) ([2]*template.Template, error) {
|
||||||
|
memoMutex.RLock()
|
||||||
|
if t, ok := memoizedGetTemplate[tmpl]; ok {
|
||||||
|
memoMutex.RUnlock()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
memoMutex.RUnlock()
|
||||||
|
|
||||||
|
templatePair := [2]*template.Template{nil, nil}
|
||||||
|
|
||||||
|
templateNoColor, err := template.New("prompt").Funcs(TemplateFuncsNoColor).Parse(tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return [2]*template.Template{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
templatePair[1] = templateNoColor
|
||||||
|
|
||||||
|
if DisableColor {
|
||||||
|
templatePair[0] = templatePair[1]
|
||||||
|
} else {
|
||||||
|
templateWithColor, err := template.New("prompt").Funcs(TemplateFuncsWithColor).Parse(tmpl)
|
||||||
|
templatePair[0] = templateWithColor
|
||||||
|
if err != nil {
|
||||||
|
return [2]*template.Template{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memoMutex.Lock()
|
||||||
|
memoizedGetTemplate[tmpl] = templatePair
|
||||||
|
memoMutex.Unlock()
|
||||||
|
return templatePair, nil
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the tag used to denote the name of the question
|
||||||
|
const tagName = "survey"
|
||||||
|
|
||||||
|
// Settable allow for configuration when assigning answers
|
||||||
|
type Settable interface {
|
||||||
|
WriteAnswer(field string, value interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionAnswer is the return type of Selects/MultiSelects that lets the appropriate information
|
||||||
|
// get copied to the user's struct
|
||||||
|
type OptionAnswer struct {
|
||||||
|
Value string
|
||||||
|
Index int
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionAnswerList(incoming []string) []OptionAnswer {
|
||||||
|
list := []OptionAnswer{}
|
||||||
|
for i, opt := range incoming {
|
||||||
|
list = append(list, OptionAnswer{Value: opt, Index: i})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
|
||||||
|
// if the field is a custom type
|
||||||
|
if s, ok := t.(Settable); ok {
|
||||||
|
// use the interface method
|
||||||
|
return s.WriteAnswer(name, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the target to write to
|
||||||
|
target := reflect.ValueOf(t)
|
||||||
|
// the value to write from
|
||||||
|
value := reflect.ValueOf(v)
|
||||||
|
|
||||||
|
// make sure we are writing to a pointer
|
||||||
|
if target.Kind() != reflect.Ptr {
|
||||||
|
return errors.New("you must pass a pointer as the target of a Write operation")
|
||||||
|
}
|
||||||
|
// the object "inside" of the target pointer
|
||||||
|
elem := target.Elem()
|
||||||
|
|
||||||
|
// handle the special types
|
||||||
|
switch elem.Kind() {
|
||||||
|
// if we are writing to a struct
|
||||||
|
case reflect.Struct:
|
||||||
|
// if we are writing to an option answer than we want to treat
|
||||||
|
// it like a single thing and not a place to deposit answers
|
||||||
|
if elem.Type().Name() == "OptionAnswer" {
|
||||||
|
// copy the value over to the normal struct
|
||||||
|
return copy(elem, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the name of the field that matches the string we were given
|
||||||
|
fieldIndex, err := findFieldIndex(elem, name)
|
||||||
|
// if something went wrong
|
||||||
|
if err != nil {
|
||||||
|
// bubble up
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field := elem.Field(fieldIndex)
|
||||||
|
// handle references to the Settable interface aswell
|
||||||
|
if s, ok := field.Interface().(Settable); ok {
|
||||||
|
// use the interface method
|
||||||
|
return s.WriteAnswer(name, v)
|
||||||
|
}
|
||||||
|
if field.CanAddr() {
|
||||||
|
if s, ok := field.Addr().Interface().(Settable); ok {
|
||||||
|
// use the interface method
|
||||||
|
return s.WriteAnswer(name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the value over to the normal struct
|
||||||
|
return copy(field, value)
|
||||||
|
case reflect.Map:
|
||||||
|
mapType := reflect.TypeOf(t).Elem()
|
||||||
|
if mapType.Key().Kind() != reflect.String {
|
||||||
|
return errors.New("answer maps key must be of type string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy only string value/index value to map if,
|
||||||
|
// map is not of type interface and is 'OptionAnswer'
|
||||||
|
if value.Type().Name() == "OptionAnswer" {
|
||||||
|
if kval := mapType.Elem().Kind(); kval == reflect.String {
|
||||||
|
mt := *t.(*map[string]string)
|
||||||
|
mt[name] = value.FieldByName("Value").String()
|
||||||
|
return nil
|
||||||
|
} else if kval == reflect.Int {
|
||||||
|
mt := *t.(*map[string]int)
|
||||||
|
mt[name] = int(value.FieldByName("Index").Int())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapType.Elem().Kind() != reflect.Interface {
|
||||||
|
return errors.New("answer maps must be of type map[string]interface")
|
||||||
|
}
|
||||||
|
mt := *t.(*map[string]interface{})
|
||||||
|
mt[name] = value.Interface()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// otherwise just copy the value to the target
|
||||||
|
return copy(elem, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type errFieldNotMatch struct {
|
||||||
|
questionName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err errFieldNotMatch) Error() string {
|
||||||
|
return fmt.Sprintf("could not find field matching %v", err.questionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err errFieldNotMatch) Is(target error) bool { // implements the dynamic errors.Is interface.
|
||||||
|
if target != nil {
|
||||||
|
if name, ok := IsFieldNotMatch(target); ok {
|
||||||
|
// if have a filled questionName then perform "deeper" comparison.
|
||||||
|
return name == "" || err.questionName == "" || name == err.questionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFieldNotMatch reports whether an "err" is caused by a non matching field.
|
||||||
|
// It returns the Question.Name that couldn't be matched with a destination field.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// err := survey.Ask(qs, &v);
|
||||||
|
// if err != nil {
|
||||||
|
// if name, ok := core.IsFieldNotMatch(err); ok {
|
||||||
|
// [...name is the not matched question name]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
func IsFieldNotMatch(err error) (string, bool) {
|
||||||
|
if err != nil {
|
||||||
|
if v, ok := err.(errFieldNotMatch); ok {
|
||||||
|
return v.questionName, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
|
||||||
|
// two fields with same name that only differ by casing.
|
||||||
|
func findFieldIndex(s reflect.Value, name string) (int, error) {
|
||||||
|
// the type of the value
|
||||||
|
sType := s.Type()
|
||||||
|
|
||||||
|
// first look for matching tags so we can overwrite matching field names
|
||||||
|
for i := 0; i < sType.NumField(); i++ {
|
||||||
|
// the field we are current scanning
|
||||||
|
field := sType.Field(i)
|
||||||
|
|
||||||
|
// the value of the survey tag
|
||||||
|
tag := field.Tag.Get(tagName)
|
||||||
|
// if the tag matches the name we are looking for
|
||||||
|
if tag != "" && tag == name {
|
||||||
|
// then we found our index
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then look for matching names
|
||||||
|
for i := 0; i < sType.NumField(); i++ {
|
||||||
|
// the field we are current scanning
|
||||||
|
field := sType.Field(i)
|
||||||
|
|
||||||
|
// if the name of the field matches what we're looking for
|
||||||
|
if strings.ToLower(field.Name) == strings.ToLower(name) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we didn't find the field
|
||||||
|
return -1, errFieldNotMatch{name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isList returns true if the element is something we can Len()
|
||||||
|
func isList(v reflect.Value) bool {
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write takes a value and copies it to the target
|
||||||
|
func copy(t reflect.Value, v reflect.Value) (err error) {
|
||||||
|
// if something ends up panicing we need to catch it in a deferred func
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// if we paniced with an error
|
||||||
|
if _, ok := r.(error); ok {
|
||||||
|
// cast the result to an error object
|
||||||
|
err = r.(error)
|
||||||
|
} else if _, ok := r.(string); ok {
|
||||||
|
// otherwise we could have paniced with a string so wrap it in an error
|
||||||
|
err = errors.New(r.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// if we are copying from a string result to something else
|
||||||
|
if v.Kind() == reflect.String && v.Type() != t.Type() {
|
||||||
|
var castVal interface{}
|
||||||
|
var casterr error
|
||||||
|
vString := v.Interface().(string)
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
castVal, casterr = strconv.ParseBool(vString)
|
||||||
|
case reflect.Int:
|
||||||
|
castVal, casterr = strconv.Atoi(vString)
|
||||||
|
case reflect.Int8:
|
||||||
|
var val64 int64
|
||||||
|
val64, casterr = strconv.ParseInt(vString, 10, 8)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = int8(val64)
|
||||||
|
}
|
||||||
|
case reflect.Int16:
|
||||||
|
var val64 int64
|
||||||
|
val64, casterr = strconv.ParseInt(vString, 10, 16)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = int16(val64)
|
||||||
|
}
|
||||||
|
case reflect.Int32:
|
||||||
|
var val64 int64
|
||||||
|
val64, casterr = strconv.ParseInt(vString, 10, 32)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = int32(val64)
|
||||||
|
}
|
||||||
|
case reflect.Int64:
|
||||||
|
if t.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||||
|
castVal, casterr = time.ParseDuration(vString)
|
||||||
|
} else {
|
||||||
|
castVal, casterr = strconv.ParseInt(vString, 10, 64)
|
||||||
|
}
|
||||||
|
case reflect.Uint:
|
||||||
|
var val64 uint64
|
||||||
|
val64, casterr = strconv.ParseUint(vString, 10, 8)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = uint(val64)
|
||||||
|
}
|
||||||
|
case reflect.Uint8:
|
||||||
|
var val64 uint64
|
||||||
|
val64, casterr = strconv.ParseUint(vString, 10, 8)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = uint8(val64)
|
||||||
|
}
|
||||||
|
case reflect.Uint16:
|
||||||
|
var val64 uint64
|
||||||
|
val64, casterr = strconv.ParseUint(vString, 10, 16)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = uint16(val64)
|
||||||
|
}
|
||||||
|
case reflect.Uint32:
|
||||||
|
var val64 uint64
|
||||||
|
val64, casterr = strconv.ParseUint(vString, 10, 32)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = uint32(val64)
|
||||||
|
}
|
||||||
|
case reflect.Uint64:
|
||||||
|
castVal, casterr = strconv.ParseUint(vString, 10, 64)
|
||||||
|
case reflect.Float32:
|
||||||
|
var val64 float64
|
||||||
|
val64, casterr = strconv.ParseFloat(vString, 32)
|
||||||
|
if casterr == nil {
|
||||||
|
castVal = float32(val64)
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
castVal, casterr = strconv.ParseFloat(vString, 64)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unable to convert from string to type %s", t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
if casterr != nil {
|
||||||
|
return casterr
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Set(reflect.ValueOf(castVal))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are copying from an OptionAnswer to something
|
||||||
|
if v.Type().Name() == "OptionAnswer" {
|
||||||
|
// copying an option answer to a string
|
||||||
|
if t.Kind() == reflect.String {
|
||||||
|
// copies the Value field of the struct
|
||||||
|
t.Set(reflect.ValueOf(v.FieldByName("Value").Interface()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// copying an option answer to an int
|
||||||
|
if t.Kind() == reflect.Int {
|
||||||
|
// copies the Index field of the struct
|
||||||
|
t.Set(reflect.ValueOf(v.FieldByName("Index").Interface()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// copying an OptionAnswer to an OptionAnswer
|
||||||
|
if t.Type().Name() == "OptionAnswer" {
|
||||||
|
t.Set(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're copying an option answer to an incorrect type
|
||||||
|
return fmt.Errorf("Unable to convert from OptionAnswer to type %s", t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are copying from one slice or array to another
|
||||||
|
if isList(v) && isList(t) {
|
||||||
|
// loop over every item in the desired value
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
// write to the target given its kind
|
||||||
|
switch t.Kind() {
|
||||||
|
// if its a slice
|
||||||
|
case reflect.Slice:
|
||||||
|
// an object of the correct type
|
||||||
|
obj := reflect.Indirect(reflect.New(t.Type().Elem()))
|
||||||
|
|
||||||
|
// write the appropriate value to the obj and catch any errors
|
||||||
|
if err := copy(obj, v.Index(i)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// just append the value to the end
|
||||||
|
t.Set(reflect.Append(t, obj))
|
||||||
|
// otherwise it could be an array
|
||||||
|
case reflect.Array:
|
||||||
|
// set the index to the appropriate value
|
||||||
|
copy(t.Slice(i, i+1).Index(0), v.Index(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// set the value to the target
|
||||||
|
t.Set(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
shellquote "github.com/kballard/go-shellquote"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Editor launches an instance of the users preferred editor on a temporary file.
|
||||||
|
The editor to use is determined by reading the $VISUAL or $EDITOR environment
|
||||||
|
variables. If neither of those are present, notepad (on Windows) or vim
|
||||||
|
(others) is used.
|
||||||
|
The launch of the editor is triggered by the enter key. Since the response may
|
||||||
|
be long, it will not be echoed as Input does, instead, it print <Received>.
|
||||||
|
Response type is a string.
|
||||||
|
|
||||||
|
message := ""
|
||||||
|
prompt := &survey.Editor{ Message: "What is your commit message?" }
|
||||||
|
survey.AskOne(prompt, &message)
|
||||||
|
*/
|
||||||
|
type Editor struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Default string
|
||||||
|
Help string
|
||||||
|
Editor string
|
||||||
|
HideDefault bool
|
||||||
|
AppendDefault bool
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// data available to the templates when processing
|
||||||
|
type EditorTemplateData struct {
|
||||||
|
Editor
|
||||||
|
Answer string
|
||||||
|
ShowAnswer bool
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
var EditorQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||||
|
{{- if .ShowAnswer}}
|
||||||
|
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||||
|
{{- else }}
|
||||||
|
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
|
||||||
|
{{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||||
|
{{- color "cyan"}}[Enter to launch editor] {{color "reset"}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
var (
|
||||||
|
bom = []byte{0xef, 0xbb, 0xbf}
|
||||||
|
editor = "vim"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
editor = "notepad"
|
||||||
|
}
|
||||||
|
if v := os.Getenv("VISUAL"); v != "" {
|
||||||
|
editor = v
|
||||||
|
} else if e := os.Getenv("EDITOR"); e != "" {
|
||||||
|
editor = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) {
|
||||||
|
initialValue := invalid.(string)
|
||||||
|
return e.prompt(initialValue, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
initialValue := ""
|
||||||
|
if e.Default != "" && e.AppendDefault {
|
||||||
|
initialValue = e.Default
|
||||||
|
}
|
||||||
|
return e.prompt(initialValue, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{}, error) {
|
||||||
|
// render the template
|
||||||
|
err := e.Render(
|
||||||
|
EditorQuestionTemplate,
|
||||||
|
EditorTemplateData{
|
||||||
|
Editor: *e,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start reading runes from the standard in
|
||||||
|
rr := e.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
cursor := e.NewCursor()
|
||||||
|
cursor.Hide()
|
||||||
|
defer cursor.Show()
|
||||||
|
|
||||||
|
for {
|
||||||
|
r, _, err := rr.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if r == '\r' || r == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == terminal.KeyInterrupt {
|
||||||
|
return "", terminal.InterruptErr
|
||||||
|
}
|
||||||
|
if r == terminal.KeyEndTransmission {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if string(r) == config.HelpInput && e.Help != "" {
|
||||||
|
err = e.Render(
|
||||||
|
EditorQuestionTemplate,
|
||||||
|
EditorTemplateData{
|
||||||
|
Editor: *e,
|
||||||
|
ShowHelp: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the temp file
|
||||||
|
pattern := e.FileName
|
||||||
|
if pattern == "" {
|
||||||
|
pattern = "survey*.txt"
|
||||||
|
}
|
||||||
|
f, err := ioutil.TempFile("", pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
// write utf8 BOM header
|
||||||
|
// The reason why we do this is because notepad.exe on Windows determines the
|
||||||
|
// encoding of an "empty" text file by the locale, for example, GBK in China,
|
||||||
|
// while golang string only handles utf8 well. However, a text file with utf8
|
||||||
|
// BOM header is not considered "empty" on Windows, and the encoding will then
|
||||||
|
// be determined utf8 by notepad.exe, instead of GBK or other encodings.
|
||||||
|
if _, err := f.Write(bom); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write initial value
|
||||||
|
if _, err := f.WriteString(initialValue); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the fd to prevent the editor unable to save file
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check is input editor exist
|
||||||
|
if e.Editor != "" {
|
||||||
|
editor = e.Editor
|
||||||
|
}
|
||||||
|
|
||||||
|
stdio := e.Stdio()
|
||||||
|
|
||||||
|
args, err := shellquote.Split(editor)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
args = append(args, f.Name())
|
||||||
|
|
||||||
|
// open the editor
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = stdio.In
|
||||||
|
cmd.Stdout = stdio.Out
|
||||||
|
cmd.Stderr = stdio.Err
|
||||||
|
cursor.Show()
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw is a BOM-unstripped UTF8 byte slice
|
||||||
|
raw, err := ioutil.ReadFile(f.Name())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip BOM header
|
||||||
|
text := string(bytes.TrimPrefix(raw, bom))
|
||||||
|
|
||||||
|
// check length, return default value on empty
|
||||||
|
if len(text) == 0 && !e.AppendDefault {
|
||||||
|
return e.Default, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
return e.Render(
|
||||||
|
EditorQuestionTemplate,
|
||||||
|
EditorTemplateData{
|
||||||
|
Editor: *e,
|
||||||
|
Answer: "<Received>",
|
||||||
|
ShowAnswer: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package survey
|
|
@ -0,0 +1,19 @@
|
||||||
|
module github.com/AlecAivazis/survey/v2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
|
github.com/kr/pty v1.1.4 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.8
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.2.1
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
|
||||||
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect
|
||||||
|
golang.org/x/text v0.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.13
|
|
@ -0,0 +1,31 @@
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||||
|
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/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||||
|
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=
|
||||||
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
@ -0,0 +1,110 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
/*
|
||||||
|
Input is a regular text input that prints each character the user types on the screen
|
||||||
|
and accepts the input with the enter key. Response type is a string.
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
prompt := &survey.Input{ Message: "What is your name?" }
|
||||||
|
survey.AskOne(prompt, &name)
|
||||||
|
*/
|
||||||
|
type Input struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Default string
|
||||||
|
Help string
|
||||||
|
}
|
||||||
|
|
||||||
|
// data available to the templates when processing
|
||||||
|
type InputTemplateData struct {
|
||||||
|
Input
|
||||||
|
Answer string
|
||||||
|
ShowAnswer bool
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
var InputQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||||
|
{{- if .ShowAnswer}}
|
||||||
|
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||||
|
{{- else }}
|
||||||
|
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ print .Config.HelpInput }} for help]{{color "reset"}} {{end}}
|
||||||
|
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// render the template
|
||||||
|
err := i.Render(
|
||||||
|
InputQuestionTemplate,
|
||||||
|
InputTemplateData{
|
||||||
|
Input: *i,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start reading runes from the standard in
|
||||||
|
rr := i.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
cursor := i.NewCursor()
|
||||||
|
|
||||||
|
line := []rune{}
|
||||||
|
// get the next line
|
||||||
|
for {
|
||||||
|
line, err = rr.ReadLine(0)
|
||||||
|
if err != nil {
|
||||||
|
return string(line), err
|
||||||
|
}
|
||||||
|
// terminal will echo the \n so we need to jump back up one row
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
|
||||||
|
if string(line) == config.HelpInput && i.Help != "" {
|
||||||
|
err = i.Render(
|
||||||
|
InputQuestionTemplate,
|
||||||
|
InputTemplateData{
|
||||||
|
Input: *i,
|
||||||
|
ShowHelp: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the line is empty
|
||||||
|
if line == nil || len(line) == 0 {
|
||||||
|
// use the default value
|
||||||
|
return i.Default, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStr := string(line)
|
||||||
|
|
||||||
|
i.AppendRenderedText(lineStr)
|
||||||
|
|
||||||
|
// we're done
|
||||||
|
return lineStr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
return i.Render(
|
||||||
|
InputQuestionTemplate,
|
||||||
|
InputTemplateData{
|
||||||
|
Input: *i,
|
||||||
|
Answer: val.(string),
|
||||||
|
ShowAnswer: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Multiline struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Default string
|
||||||
|
Help string
|
||||||
|
}
|
||||||
|
|
||||||
|
// data available to the templates when processing
|
||||||
|
type MultilineTemplateData struct {
|
||||||
|
Multiline
|
||||||
|
Answer string
|
||||||
|
ShowAnswer bool
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
var MultilineQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||||
|
{{- if .ShowAnswer}}
|
||||||
|
{{- "\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}}
|
||||||
|
{{- if .Answer }}{{ "\n" }}{{ end }}
|
||||||
|
{{- else }}
|
||||||
|
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||||
|
{{- color "cyan"}}[Enter 2 empty lines to finish]{{color "reset"}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// render the template
|
||||||
|
err := i.Render(
|
||||||
|
MultilineQuestionTemplate,
|
||||||
|
MultilineTemplateData{
|
||||||
|
Multiline: *i,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start reading runes from the standard in
|
||||||
|
rr := i.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
cursor := i.NewCursor()
|
||||||
|
|
||||||
|
multiline := make([]string, 0)
|
||||||
|
|
||||||
|
emptyOnce := false
|
||||||
|
// get the next line
|
||||||
|
for {
|
||||||
|
line := []rune{}
|
||||||
|
line, err = rr.ReadLine(0)
|
||||||
|
if err != nil {
|
||||||
|
return string(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(line) == "" {
|
||||||
|
if emptyOnce {
|
||||||
|
numLines := len(multiline) + 2
|
||||||
|
cursor.PreviousLine(numLines)
|
||||||
|
for j := 0; j < numLines; j++ {
|
||||||
|
terminal.EraseLine(i.Stdio().Out, terminal.ERASE_LINE_ALL)
|
||||||
|
cursor.NextLine(1)
|
||||||
|
}
|
||||||
|
cursor.PreviousLine(numLines)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
emptyOnce = true
|
||||||
|
} else {
|
||||||
|
emptyOnce = false
|
||||||
|
}
|
||||||
|
multiline = append(multiline, string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
val := strings.Join(multiline, "\n")
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
|
||||||
|
// if the line is empty
|
||||||
|
if len(val) == 0 {
|
||||||
|
// use the default value
|
||||||
|
return i.Default, err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.AppendRenderedText(val)
|
||||||
|
return val, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Multiline) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
return i.Render(
|
||||||
|
MultilineQuestionTemplate,
|
||||||
|
MultilineTemplateData{
|
||||||
|
Multiline: *i,
|
||||||
|
Answer: val.(string),
|
||||||
|
ShowAnswer: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,327 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
MultiSelect is a prompt that presents a list of various options to the user
|
||||||
|
for them to select using the arrow keys and enter. Response type is a slice of strings.
|
||||||
|
|
||||||
|
days := []string{}
|
||||||
|
prompt := &survey.MultiSelect{
|
||||||
|
Message: "What days do you prefer:",
|
||||||
|
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &days)
|
||||||
|
*/
|
||||||
|
type MultiSelect struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Options []string
|
||||||
|
Default interface{}
|
||||||
|
Help string
|
||||||
|
PageSize int
|
||||||
|
VimMode bool
|
||||||
|
FilterMessage string
|
||||||
|
Filter func(filter string, value string, index int) bool
|
||||||
|
filter string
|
||||||
|
selectedIndex int
|
||||||
|
checked map[int]bool
|
||||||
|
showingHelp bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// data available to the templates when processing
|
||||||
|
type MultiSelectTemplateData struct {
|
||||||
|
MultiSelect
|
||||||
|
Answer string
|
||||||
|
ShowAnswer bool
|
||||||
|
Checked map[int]bool
|
||||||
|
SelectedIndex int
|
||||||
|
ShowHelp bool
|
||||||
|
PageEntries []core.OptionAnswer
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var MultiSelectQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
|
||||||
|
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
|
||||||
|
{{- else }}
|
||||||
|
{{- " "}}{{- color "cyan"}}[Use arrows to move, space to select, <right> to all, <left> to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
|
||||||
|
{{- "\n"}}
|
||||||
|
{{- range $ix, $option := .PageEntries}}
|
||||||
|
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
|
||||||
|
{{- if index $.Checked $option.Index }}{{color $.Config.Icons.MarkedOption.Format }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color $.Config.Icons.UnmarkedOption.Format }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
|
||||||
|
{{- color "reset"}}
|
||||||
|
{{- " "}}{{$option.Value}}{{"\n"}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
// OnChange is called on every keypress.
|
||||||
|
func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
|
||||||
|
options := m.filterOptions(config)
|
||||||
|
oldFilter := m.filter
|
||||||
|
|
||||||
|
if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') {
|
||||||
|
// if we are at the top of the list
|
||||||
|
if m.selectedIndex == 0 {
|
||||||
|
// go to the bottom
|
||||||
|
m.selectedIndex = len(options) - 1
|
||||||
|
} else {
|
||||||
|
// decrement the selected index
|
||||||
|
m.selectedIndex--
|
||||||
|
}
|
||||||
|
} else if key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
|
||||||
|
// if we are at the bottom of the list
|
||||||
|
if m.selectedIndex == len(options)-1 {
|
||||||
|
// start at the top
|
||||||
|
m.selectedIndex = 0
|
||||||
|
} else {
|
||||||
|
// increment the selected index
|
||||||
|
m.selectedIndex++
|
||||||
|
}
|
||||||
|
// if the user pressed down and there is room to move
|
||||||
|
} else if key == terminal.KeySpace {
|
||||||
|
// the option they have selected
|
||||||
|
if m.selectedIndex < len(options) {
|
||||||
|
selectedOpt := options[m.selectedIndex]
|
||||||
|
|
||||||
|
// if we haven't seen this index before
|
||||||
|
if old, ok := m.checked[selectedOpt.Index]; !ok {
|
||||||
|
// set the value to true
|
||||||
|
m.checked[selectedOpt.Index] = true
|
||||||
|
} else {
|
||||||
|
// otherwise just invert the current value
|
||||||
|
m.checked[selectedOpt.Index] = !old
|
||||||
|
}
|
||||||
|
if !config.KeepFilter {
|
||||||
|
m.filter = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only show the help message if we have one to show
|
||||||
|
} else if string(key) == config.HelpInput && m.Help != "" {
|
||||||
|
m.showingHelp = true
|
||||||
|
} else if key == terminal.KeyEscape {
|
||||||
|
m.VimMode = !m.VimMode
|
||||||
|
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
|
||||||
|
m.filter = ""
|
||||||
|
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
|
||||||
|
if m.filter != "" {
|
||||||
|
m.filter = m.filter[0 : len(m.filter)-1]
|
||||||
|
}
|
||||||
|
} else if key >= terminal.KeySpace {
|
||||||
|
m.filter += string(key)
|
||||||
|
m.VimMode = false
|
||||||
|
} else if key == terminal.KeyArrowRight {
|
||||||
|
for _, v := range options {
|
||||||
|
m.checked[v.Index] = true
|
||||||
|
}
|
||||||
|
if !config.KeepFilter {
|
||||||
|
m.filter = ""
|
||||||
|
}
|
||||||
|
} else if key == terminal.KeyArrowLeft {
|
||||||
|
for _, v := range options {
|
||||||
|
m.checked[v.Index] = false
|
||||||
|
}
|
||||||
|
if !config.KeepFilter {
|
||||||
|
m.filter = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.FilterMessage = ""
|
||||||
|
if m.filter != "" {
|
||||||
|
m.FilterMessage = " " + m.filter
|
||||||
|
}
|
||||||
|
if oldFilter != m.filter {
|
||||||
|
// filter changed
|
||||||
|
options = m.filterOptions(config)
|
||||||
|
if len(options) > 0 && len(options) <= m.selectedIndex {
|
||||||
|
m.selectedIndex = len(options) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// paginate the options
|
||||||
|
// figure out the page size
|
||||||
|
pageSize := m.PageSize
|
||||||
|
// if we dont have a specific one
|
||||||
|
if pageSize == 0 {
|
||||||
|
// grab the global value
|
||||||
|
pageSize = config.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO if we have started filtering and were looking at the end of a list
|
||||||
|
// and we have modified the filter then we should move the page back!
|
||||||
|
opts, idx := paginate(pageSize, options, m.selectedIndex)
|
||||||
|
|
||||||
|
// render the options
|
||||||
|
m.Render(
|
||||||
|
MultiSelectQuestionTemplate,
|
||||||
|
MultiSelectTemplateData{
|
||||||
|
MultiSelect: *m,
|
||||||
|
SelectedIndex: idx,
|
||||||
|
Checked: m.checked,
|
||||||
|
ShowHelp: m.showingHelp,
|
||||||
|
PageEntries: opts,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
|
||||||
|
// the filtered list
|
||||||
|
answers := []core.OptionAnswer{}
|
||||||
|
|
||||||
|
// if there is no filter applied
|
||||||
|
if m.filter == "" {
|
||||||
|
// return all of the options
|
||||||
|
return core.OptionAnswerList(m.Options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the filter to apply
|
||||||
|
filter := m.Filter
|
||||||
|
if filter == nil {
|
||||||
|
filter = config.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply the filter to each option
|
||||||
|
for i, opt := range m.Options {
|
||||||
|
// i the filter says to include the option
|
||||||
|
if filter(m.filter, opt, i) {
|
||||||
|
answers = append(answers, core.OptionAnswer{
|
||||||
|
Index: i,
|
||||||
|
Value: opt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done here
|
||||||
|
return answers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// compute the default state
|
||||||
|
m.checked = make(map[int]bool)
|
||||||
|
// if there is a default
|
||||||
|
if m.Default != nil {
|
||||||
|
// if the default is string values
|
||||||
|
if defaultValues, ok := m.Default.([]string); ok {
|
||||||
|
for _, dflt := range defaultValues {
|
||||||
|
for i, opt := range m.Options {
|
||||||
|
// if the option corresponds to the default
|
||||||
|
if opt == dflt {
|
||||||
|
// we found our initial value
|
||||||
|
m.checked[i] = true
|
||||||
|
// stop looking
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the default value is index values
|
||||||
|
} else if defaultIndices, ok := m.Default.([]int); ok {
|
||||||
|
// go over every index we need to enable by default
|
||||||
|
for _, idx := range defaultIndices {
|
||||||
|
// and enable it
|
||||||
|
m.checked[idx] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no options to render
|
||||||
|
if len(m.Options) == 0 {
|
||||||
|
// we failed
|
||||||
|
return "", errors.New("please provide options to select from")
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out the page size
|
||||||
|
pageSize := m.PageSize
|
||||||
|
// if we dont have a specific one
|
||||||
|
if pageSize == 0 {
|
||||||
|
// grab the global value
|
||||||
|
pageSize = config.PageSize
|
||||||
|
}
|
||||||
|
// paginate the options
|
||||||
|
// build up a list of option answers
|
||||||
|
opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)
|
||||||
|
|
||||||
|
cursor := m.NewCursor()
|
||||||
|
cursor.Hide() // hide the cursor
|
||||||
|
defer cursor.Show() // show the cursor when we're done
|
||||||
|
|
||||||
|
// ask the question
|
||||||
|
err := m.Render(
|
||||||
|
MultiSelectQuestionTemplate,
|
||||||
|
MultiSelectTemplateData{
|
||||||
|
MultiSelect: *m,
|
||||||
|
SelectedIndex: idx,
|
||||||
|
Checked: m.checked,
|
||||||
|
PageEntries: opts,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := m.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
// start waiting for input
|
||||||
|
for {
|
||||||
|
r, _, _ := rr.ReadRune()
|
||||||
|
if r == '\r' || r == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == terminal.KeyInterrupt {
|
||||||
|
return "", terminal.InterruptErr
|
||||||
|
}
|
||||||
|
if r == terminal.KeyEndTransmission {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
m.OnChange(r, config)
|
||||||
|
}
|
||||||
|
m.filter = ""
|
||||||
|
m.FilterMessage = ""
|
||||||
|
|
||||||
|
answers := []core.OptionAnswer{}
|
||||||
|
for i, option := range m.Options {
|
||||||
|
if val, ok := m.checked[i]; ok && val {
|
||||||
|
answers = append(answers, core.OptionAnswer{Value: option, Index: i})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes the options section, and renders the ask like a normal question.
|
||||||
|
func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
// the answer to show
|
||||||
|
answer := ""
|
||||||
|
for _, ans := range val.([]core.OptionAnswer) {
|
||||||
|
answer = fmt.Sprintf("%s, %s", answer, ans.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we answered anything
|
||||||
|
if len(answer) > 2 {
|
||||||
|
// remove the precending commas
|
||||||
|
answer = answer[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute the output summary template with the answer
|
||||||
|
return m.Render(
|
||||||
|
MultiSelectQuestionTemplate,
|
||||||
|
MultiSelectTemplateData{
|
||||||
|
MultiSelect: *m,
|
||||||
|
SelectedIndex: m.selectedIndex,
|
||||||
|
Checked: m.checked,
|
||||||
|
Answer: answer,
|
||||||
|
ShowAnswer: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Password is like a normal Input but the text shows up as *'s and there is no default. Response
|
||||||
|
type is a string.
|
||||||
|
|
||||||
|
password := ""
|
||||||
|
prompt := &survey.Password{ Message: "Please type your password" }
|
||||||
|
survey.AskOne(prompt, &password)
|
||||||
|
*/
|
||||||
|
type Password struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Help string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PasswordTemplateData struct {
|
||||||
|
Password
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordQuestionTemplate is a template with color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||||
|
var PasswordQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||||
|
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}`
|
||||||
|
|
||||||
|
func (p *Password) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// render the question template
|
||||||
|
userOut, _, err := core.RunTemplate(
|
||||||
|
PasswordQuestionTemplate,
|
||||||
|
PasswordTemplateData{
|
||||||
|
Password: *p,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := p.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
// no help msg? Just return any response
|
||||||
|
if p.Help == "" {
|
||||||
|
line, err := rr.ReadLine('*')
|
||||||
|
return string(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := p.NewCursor()
|
||||||
|
|
||||||
|
line := []rune{}
|
||||||
|
// process answers looking for help prompt answer
|
||||||
|
for {
|
||||||
|
line, err = rr.ReadLine('*')
|
||||||
|
if err != nil {
|
||||||
|
return string(line), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(line) == config.HelpInput {
|
||||||
|
// terminal will echo the \n so we need to jump back up one row
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
|
||||||
|
err = p.Render(
|
||||||
|
PasswordQuestionTemplate,
|
||||||
|
PasswordTemplateData{
|
||||||
|
Password: *p,
|
||||||
|
ShowHelp: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStr := string(line)
|
||||||
|
p.AppendRenderedText(strings.Repeat("*", len(lineStr)))
|
||||||
|
return lineStr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup hides the string with a fixed number of characters.
|
||||||
|
func (prompt *Password) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
goterm "golang.org/x/crypto/ssh/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Renderer struct {
|
||||||
|
stdio terminal.Stdio
|
||||||
|
renderedErrors bytes.Buffer
|
||||||
|
renderedText bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorTemplateData struct {
|
||||||
|
Error error
|
||||||
|
Icon Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (r *Renderer) WithStdio(stdio terminal.Stdio) {
|
||||||
|
r.stdio = stdio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) Stdio() terminal.Stdio {
|
||||||
|
return r.stdio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) NewRuneReader() *terminal.RuneReader {
|
||||||
|
return terminal.NewRuneReader(r.stdio)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) NewCursor() *terminal.Cursor {
|
||||||
|
return &terminal.Cursor{
|
||||||
|
In: r.stdio.In,
|
||||||
|
Out: r.stdio.Out,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) Error(config *PromptConfig, invalid error) error {
|
||||||
|
// cleanup the currently rendered errors
|
||||||
|
r.resetPrompt(r.countLines(r.renderedErrors))
|
||||||
|
r.renderedErrors.Reset()
|
||||||
|
|
||||||
|
// cleanup the rest of the prompt
|
||||||
|
r.resetPrompt(r.countLines(r.renderedText))
|
||||||
|
r.renderedText.Reset()
|
||||||
|
|
||||||
|
userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{
|
||||||
|
Error: invalid,
|
||||||
|
Icon: config.Icons.Error,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the message to the user
|
||||||
|
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
|
||||||
|
|
||||||
|
// add the printed text to the rendered error buffer so we can cleanup later
|
||||||
|
r.appendRenderedError(layoutOut)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) Render(tmpl string, data interface{}) error {
|
||||||
|
// cleanup the currently rendered text
|
||||||
|
lineCount := r.countLines(r.renderedText)
|
||||||
|
r.resetPrompt(lineCount)
|
||||||
|
r.renderedText.Reset()
|
||||||
|
|
||||||
|
// render the template summarizing the current state
|
||||||
|
userOut, layoutOut, err := core.RunTemplate(tmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// print the summary
|
||||||
|
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
|
||||||
|
|
||||||
|
// add the printed text to the rendered text buffer so we can cleanup later
|
||||||
|
r.AppendRenderedText(layoutOut)
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendRenderedError appends text to the renderer's error buffer
|
||||||
|
// which is used to track what has been printed. It is not exported
|
||||||
|
// as errors should only be displayed via Error(config, error).
|
||||||
|
func (r *Renderer) appendRenderedError(text string) {
|
||||||
|
r.renderedErrors.WriteString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendRenderedText appends text to the renderer's text buffer
|
||||||
|
// which is used to track of what has been printed. The buffer is used
|
||||||
|
// to calculate how many lines to erase before updating the prompt.
|
||||||
|
func (r *Renderer) AppendRenderedText(text string) {
|
||||||
|
r.renderedText.WriteString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) resetPrompt(lines int) {
|
||||||
|
// clean out current line in case tmpl didnt end in newline
|
||||||
|
cursor := r.NewCursor()
|
||||||
|
cursor.HorizontalAbsolute(0)
|
||||||
|
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
||||||
|
// clean up what we left behind last time
|
||||||
|
for i := 0; i < lines; i++ {
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) termWidth() (int, error) {
|
||||||
|
fd := int(r.stdio.Out.Fd())
|
||||||
|
termWidth, _, err := goterm.GetSize(fd)
|
||||||
|
return termWidth, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// countLines will return the count of `\n` with the addition of any
|
||||||
|
// lines that have wrapped due to narrow terminal width
|
||||||
|
func (r *Renderer) countLines(buf bytes.Buffer) int {
|
||||||
|
w, err := r.termWidth()
|
||||||
|
if err != nil || w == 0 {
|
||||||
|
// if we got an error due to terminal.GetSize not being supported
|
||||||
|
// on current platform then just assume a very wide terminal
|
||||||
|
w = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
bufBytes := buf.Bytes()
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
curr := 0
|
||||||
|
delim := -1
|
||||||
|
for curr < len(bufBytes) {
|
||||||
|
// read until the next newline or the end of the string
|
||||||
|
relDelim := bytes.IndexRune(bufBytes[curr:], '\n')
|
||||||
|
if relDelim != -1 {
|
||||||
|
count += 1 // new line found, add it to the count
|
||||||
|
delim = curr + relDelim
|
||||||
|
} else {
|
||||||
|
delim = len(bufBytes) // no new line found, read rest of text
|
||||||
|
}
|
||||||
|
|
||||||
|
// account for word wrapping
|
||||||
|
count += int(utf8.RuneCount(bufBytes[curr:delim]) / w)
|
||||||
|
curr = delim + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
|
@ -0,0 +1,328 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Select is a prompt that presents a list of various options to the user
|
||||||
|
for them to select using the arrow keys and enter. Response type is a string.
|
||||||
|
|
||||||
|
color := ""
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: "Choose a color:",
|
||||||
|
Options: []string{"red", "blue", "green"},
|
||||||
|
}
|
||||||
|
survey.AskOne(prompt, &color)
|
||||||
|
*/
|
||||||
|
type Select struct {
|
||||||
|
Renderer
|
||||||
|
Message string
|
||||||
|
Options []string
|
||||||
|
Default interface{}
|
||||||
|
Help string
|
||||||
|
PageSize int
|
||||||
|
VimMode bool
|
||||||
|
FilterMessage string
|
||||||
|
Filter func(filter string, value string, index int) bool
|
||||||
|
filter string
|
||||||
|
selectedIndex int
|
||||||
|
useDefault bool
|
||||||
|
showingHelp bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectTemplateData is the data available to the templates when processing
|
||||||
|
type SelectTemplateData struct {
|
||||||
|
Select
|
||||||
|
PageEntries []core.OptionAnswer
|
||||||
|
SelectedIndex int
|
||||||
|
Answer string
|
||||||
|
ShowAnswer bool
|
||||||
|
ShowHelp bool
|
||||||
|
Config *PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var SelectQuestionTemplate = `
|
||||||
|
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||||
|
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||||
|
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
|
||||||
|
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
|
||||||
|
{{- else}}
|
||||||
|
{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
|
||||||
|
{{- "\n"}}
|
||||||
|
{{- range $ix, $choice := .PageEntries}}
|
||||||
|
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
|
||||||
|
{{- $choice.Value}}
|
||||||
|
{{- color "reset"}}{{"\n"}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}`
|
||||||
|
|
||||||
|
// OnChange is called on every keypress.
|
||||||
|
func (s *Select) OnChange(key rune, config *PromptConfig) bool {
|
||||||
|
options := s.filterOptions(config)
|
||||||
|
oldFilter := s.filter
|
||||||
|
|
||||||
|
// if the user pressed the enter key and the index is a valid option
|
||||||
|
if key == terminal.KeyEnter || key == '\n' {
|
||||||
|
// if the selected index is a valid option
|
||||||
|
if len(options) > 0 && s.selectedIndex < len(options) {
|
||||||
|
|
||||||
|
// we're done (stop prompting the user)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're not done (keep prompting)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// if the user pressed the up arrow or 'k' to emulate vim
|
||||||
|
} else if key == terminal.KeyArrowUp || (s.VimMode && key == 'k') && len(options) > 0 {
|
||||||
|
s.useDefault = false
|
||||||
|
|
||||||
|
// if we are at the top of the list
|
||||||
|
if s.selectedIndex == 0 {
|
||||||
|
// start from the button
|
||||||
|
s.selectedIndex = len(options) - 1
|
||||||
|
} else {
|
||||||
|
// otherwise we are not at the top of the list so decrement the selected index
|
||||||
|
s.selectedIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the user pressed down or 'j' to emulate vim
|
||||||
|
} else if key == terminal.KeyArrowDown || (s.VimMode && key == 'j') && len(options) > 0 {
|
||||||
|
s.useDefault = false
|
||||||
|
// if we are at the bottom of the list
|
||||||
|
if s.selectedIndex == len(options)-1 {
|
||||||
|
// start from the top
|
||||||
|
s.selectedIndex = 0
|
||||||
|
} else {
|
||||||
|
// increment the selected index
|
||||||
|
s.selectedIndex++
|
||||||
|
}
|
||||||
|
// only show the help message if we have one
|
||||||
|
} else if string(key) == config.HelpInput && s.Help != "" {
|
||||||
|
s.showingHelp = true
|
||||||
|
// if the user wants to toggle vim mode on/off
|
||||||
|
} else if key == terminal.KeyEscape {
|
||||||
|
s.VimMode = !s.VimMode
|
||||||
|
// if the user hits any of the keys that clear the filter
|
||||||
|
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
|
||||||
|
s.filter = ""
|
||||||
|
// if the user is deleting a character in the filter
|
||||||
|
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
|
||||||
|
// if there is content in the filter to delete
|
||||||
|
if s.filter != "" {
|
||||||
|
// subtract a line from the current filter
|
||||||
|
s.filter = s.filter[0 : len(s.filter)-1]
|
||||||
|
// we removed the last value in the filter
|
||||||
|
}
|
||||||
|
} else if key >= terminal.KeySpace {
|
||||||
|
s.filter += string(key)
|
||||||
|
// make sure vim mode is disabled
|
||||||
|
s.VimMode = false
|
||||||
|
// make sure that we use the current value in the filtered list
|
||||||
|
s.useDefault = false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.FilterMessage = ""
|
||||||
|
if s.filter != "" {
|
||||||
|
s.FilterMessage = " " + s.filter
|
||||||
|
}
|
||||||
|
if oldFilter != s.filter {
|
||||||
|
// filter changed
|
||||||
|
options = s.filterOptions(config)
|
||||||
|
if len(options) > 0 && len(options) <= s.selectedIndex {
|
||||||
|
s.selectedIndex = len(options) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out the options and index to render
|
||||||
|
// figure out the page size
|
||||||
|
pageSize := s.PageSize
|
||||||
|
// if we dont have a specific one
|
||||||
|
if pageSize == 0 {
|
||||||
|
// grab the global value
|
||||||
|
pageSize = config.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO if we have started filtering and were looking at the end of a list
|
||||||
|
// and we have modified the filter then we should move the page back!
|
||||||
|
opts, idx := paginate(pageSize, options, s.selectedIndex)
|
||||||
|
|
||||||
|
// render the options
|
||||||
|
s.Render(
|
||||||
|
SelectQuestionTemplate,
|
||||||
|
SelectTemplateData{
|
||||||
|
Select: *s,
|
||||||
|
SelectedIndex: idx,
|
||||||
|
ShowHelp: s.showingHelp,
|
||||||
|
PageEntries: opts,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// keep prompting
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) filterOptions(config *PromptConfig) []core.OptionAnswer {
|
||||||
|
// the filtered list
|
||||||
|
answers := []core.OptionAnswer{}
|
||||||
|
|
||||||
|
// if there is no filter applied
|
||||||
|
if s.filter == "" {
|
||||||
|
return core.OptionAnswerList(s.Options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the filter to apply
|
||||||
|
filter := s.Filter
|
||||||
|
if filter == nil {
|
||||||
|
filter = config.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
for i, opt := range s.Options {
|
||||||
|
// i the filter says to include the option
|
||||||
|
if filter(s.filter, opt, i) {
|
||||||
|
answers = append(answers, core.OptionAnswer{
|
||||||
|
Index: i,
|
||||||
|
Value: opt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the list of answers
|
||||||
|
return answers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
|
||||||
|
// if there are no options to render
|
||||||
|
if len(s.Options) == 0 {
|
||||||
|
// we failed
|
||||||
|
return "", errors.New("please provide options to select from")
|
||||||
|
}
|
||||||
|
|
||||||
|
// start off with the first option selected
|
||||||
|
sel := 0
|
||||||
|
// if there is a default
|
||||||
|
if s.Default != "" {
|
||||||
|
// find the choice
|
||||||
|
for i, opt := range s.Options {
|
||||||
|
// if the option corresponds to the default
|
||||||
|
if opt == s.Default {
|
||||||
|
// we found our initial value
|
||||||
|
sel = i
|
||||||
|
// stop looking
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// save the selected index
|
||||||
|
s.selectedIndex = sel
|
||||||
|
|
||||||
|
// figure out the page size
|
||||||
|
pageSize := s.PageSize
|
||||||
|
// if we dont have a specific one
|
||||||
|
if pageSize == 0 {
|
||||||
|
// grab the global value
|
||||||
|
pageSize = config.PageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out the options and index to render
|
||||||
|
opts, idx := paginate(pageSize, core.OptionAnswerList(s.Options), sel)
|
||||||
|
|
||||||
|
// ask the question
|
||||||
|
err := s.Render(
|
||||||
|
SelectQuestionTemplate,
|
||||||
|
SelectTemplateData{
|
||||||
|
Select: *s,
|
||||||
|
PageEntries: opts,
|
||||||
|
SelectedIndex: idx,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// by default, use the default value
|
||||||
|
s.useDefault = true
|
||||||
|
|
||||||
|
rr := s.NewRuneReader()
|
||||||
|
rr.SetTermMode()
|
||||||
|
defer rr.RestoreTermMode()
|
||||||
|
|
||||||
|
cursor := s.NewCursor()
|
||||||
|
cursor.Hide() // hide the cursor
|
||||||
|
defer cursor.Show() // show the cursor when we're done
|
||||||
|
|
||||||
|
// start waiting for input
|
||||||
|
for {
|
||||||
|
r, _, err := rr.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if r == terminal.KeyInterrupt {
|
||||||
|
return "", terminal.InterruptErr
|
||||||
|
}
|
||||||
|
if r == terminal.KeyEndTransmission {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if s.OnChange(r, config) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options := s.filterOptions(config)
|
||||||
|
s.filter = ""
|
||||||
|
s.FilterMessage = ""
|
||||||
|
|
||||||
|
// the index to report
|
||||||
|
var val string
|
||||||
|
// if we are supposed to use the default value
|
||||||
|
if s.useDefault || s.selectedIndex >= len(options) {
|
||||||
|
// if there is a default value
|
||||||
|
if s.Default != nil {
|
||||||
|
// if the default is a string
|
||||||
|
if defaultString, ok := s.Default.(string); ok {
|
||||||
|
// use the default value
|
||||||
|
val = defaultString
|
||||||
|
// the default value could also be an interpret which is interpretted as the index
|
||||||
|
} else if defaultIndex, ok := s.Default.(int); ok {
|
||||||
|
val = s.Options[defaultIndex]
|
||||||
|
} else {
|
||||||
|
return val, errors.New("default value of select must be an int or string")
|
||||||
|
}
|
||||||
|
} else if len(options) > 0 {
|
||||||
|
// there is no default value so use the first
|
||||||
|
val = options[0].Value
|
||||||
|
}
|
||||||
|
// otherwise the selected index points to the value
|
||||||
|
} else if s.selectedIndex < len(options) {
|
||||||
|
// the
|
||||||
|
val = options[s.selectedIndex].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// now that we have the value lets go hunt down the right index to return
|
||||||
|
idx = -1
|
||||||
|
for i, optionValue := range s.Options {
|
||||||
|
if optionValue == val {
|
||||||
|
idx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return core.OptionAnswer{Value: val, Index: idx}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) Cleanup(config *PromptConfig, val interface{}) error {
|
||||||
|
return s.Render(
|
||||||
|
SelectQuestionTemplate,
|
||||||
|
SelectTemplateData{
|
||||||
|
Select: *s,
|
||||||
|
Answer: val.(core.OptionAnswer).Value,
|
||||||
|
ShowAnswer: true,
|
||||||
|
Config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,398 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/core"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultAskOptions is the default options on ask, using the OS stdio.
|
||||||
|
func defaultAskOptions() *AskOptions {
|
||||||
|
return &AskOptions{
|
||||||
|
Stdio: terminal.Stdio{
|
||||||
|
In: os.Stdin,
|
||||||
|
Out: os.Stdout,
|
||||||
|
Err: os.Stderr,
|
||||||
|
},
|
||||||
|
PromptConfig: PromptConfig{
|
||||||
|
PageSize: 7,
|
||||||
|
HelpInput: "?",
|
||||||
|
Icons: IconSet{
|
||||||
|
Error: Icon{
|
||||||
|
Text: "X",
|
||||||
|
Format: "red",
|
||||||
|
},
|
||||||
|
Help: Icon{
|
||||||
|
Text: "?",
|
||||||
|
Format: "cyan",
|
||||||
|
},
|
||||||
|
Question: Icon{
|
||||||
|
Text: "?",
|
||||||
|
Format: "green+hb",
|
||||||
|
},
|
||||||
|
MarkedOption: Icon{
|
||||||
|
Text: "[x]",
|
||||||
|
Format: "green",
|
||||||
|
},
|
||||||
|
UnmarkedOption: Icon{
|
||||||
|
Text: "[ ]",
|
||||||
|
Format: "default+hb",
|
||||||
|
},
|
||||||
|
SelectFocus: Icon{
|
||||||
|
Text: ">",
|
||||||
|
Format: "cyan+b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filter: func(filter string, value string, index int) (include bool) {
|
||||||
|
filter = strings.ToLower(filter)
|
||||||
|
|
||||||
|
// include this option if it matches
|
||||||
|
return strings.Contains(strings.ToLower(value), filter)
|
||||||
|
},
|
||||||
|
KeepFilter: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func defaultPromptConfig() *PromptConfig {
|
||||||
|
return &defaultAskOptions().PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultIcons() *IconSet {
|
||||||
|
return &defaultPromptConfig().Icons
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionAnswer is an ergonomic alias for core.OptionAnswer
|
||||||
|
type OptionAnswer = core.OptionAnswer
|
||||||
|
|
||||||
|
// Icon holds the text and format to show for a particular icon
|
||||||
|
type Icon struct {
|
||||||
|
Text string
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IconSet holds the icons to use for various prompts
|
||||||
|
type IconSet struct {
|
||||||
|
HelpInput Icon
|
||||||
|
Error Icon
|
||||||
|
Help Icon
|
||||||
|
Question Icon
|
||||||
|
MarkedOption Icon
|
||||||
|
UnmarkedOption Icon
|
||||||
|
SelectFocus Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator is a function passed to a Question after a user has provided a response.
|
||||||
|
// If the function returns an error, then the user will be prompted again for another
|
||||||
|
// response.
|
||||||
|
type Validator func(ans interface{}) error
|
||||||
|
|
||||||
|
// Transformer is a function passed to a Question after a user has provided a response.
|
||||||
|
// The function can be used to implement a custom logic that will result to return
|
||||||
|
// a different representation of the given answer.
|
||||||
|
//
|
||||||
|
// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more.
|
||||||
|
type Transformer func(ans interface{}) (newAns interface{})
|
||||||
|
|
||||||
|
// Question is the core data structure for a survey questionnaire.
|
||||||
|
type Question struct {
|
||||||
|
Name string
|
||||||
|
Prompt Prompt
|
||||||
|
Validate Validator
|
||||||
|
Transform Transformer
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptConfig holds the global configuration for a prompt
|
||||||
|
type PromptConfig struct {
|
||||||
|
PageSize int
|
||||||
|
Icons IconSet
|
||||||
|
HelpInput string
|
||||||
|
Filter func(filter string, option string, index int) bool
|
||||||
|
KeepFilter bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt is the primary interface for the objects that can take user input
|
||||||
|
// and return a response.
|
||||||
|
type Prompt interface {
|
||||||
|
Prompt(config *PromptConfig) (interface{}, error)
|
||||||
|
Cleanup(*PromptConfig, interface{}) error
|
||||||
|
Error(*PromptConfig, error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptAgainer Interface for Prompts that support prompting again after invalid input
|
||||||
|
type PromptAgainer interface {
|
||||||
|
PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AskOpt allows setting optional ask options.
|
||||||
|
type AskOpt func(options *AskOptions) error
|
||||||
|
|
||||||
|
// AskOptions provides additional options on ask.
|
||||||
|
type AskOptions struct {
|
||||||
|
Stdio terminal.Stdio
|
||||||
|
Validators []Validator
|
||||||
|
PromptConfig PromptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStdio specifies the standard input, output and error files survey
|
||||||
|
// interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr.
|
||||||
|
func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
options.Stdio.In = in
|
||||||
|
options.Stdio.Out = out
|
||||||
|
options.Stdio.Err = err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFilter specifies the default filter to use when asking questions.
|
||||||
|
func WithFilter(filter func(filter string, value string, index int) (include bool)) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// save the filter internally
|
||||||
|
options.PromptConfig.Filter = filter
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeepFilter sets the if the filter is kept after selections
|
||||||
|
func WithKeepFilter(KeepFilter bool) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// set the page size
|
||||||
|
options.PromptConfig.KeepFilter = KeepFilter
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValidator specifies a validator to use while prompting the user
|
||||||
|
func WithValidator(v Validator) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// add the provided validator to the list
|
||||||
|
options.Validators = append(options.Validators, v)
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wantsStdio interface {
|
||||||
|
WithStdio(terminal.Stdio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPageSize sets the default page size used by prompts
|
||||||
|
func WithPageSize(pageSize int) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// set the page size
|
||||||
|
options.PromptConfig.PageSize = pageSize
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHelpInput changes the character that prompts look for to give the user helpful information.
|
||||||
|
func WithHelpInput(r rune) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// set the input character
|
||||||
|
options.PromptConfig.HelpInput = string(r)
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIcons sets the icons that will be used when prompting the user
|
||||||
|
func WithIcons(setIcons func(*IconSet)) AskOpt {
|
||||||
|
return func(options *AskOptions) error {
|
||||||
|
// update the default icons with whatever the user says
|
||||||
|
setIcons(&options.PromptConfig.Icons)
|
||||||
|
|
||||||
|
// nothing went wrong
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
AskOne performs the prompt for a single prompt and asks for validation if required.
|
||||||
|
Response types should be something that can be casted from the response type designated
|
||||||
|
in the documentation. For example:
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
prompt := &survey.Input{
|
||||||
|
Message: "name",
|
||||||
|
}
|
||||||
|
|
||||||
|
survey.AskOne(prompt, &name)
|
||||||
|
|
||||||
|
*/
|
||||||
|
func AskOne(p Prompt, response interface{}, opts ...AskOpt) error {
|
||||||
|
err := Ask([]*Question{{Prompt: p}}, response, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ask performs the prompt loop, asking for validation when appropriate. The response
|
||||||
|
type can be one of two options. If a struct is passed, the answer will be written to
|
||||||
|
the field whose name matches the Name field on the corresponding question. Field types
|
||||||
|
should be something that can be casted from the response type designated in the
|
||||||
|
documentation. Note, a survey tag can also be used to identify a Otherwise, a
|
||||||
|
map[string]interface{} can be passed, responses will be written to the key with the
|
||||||
|
matching name. For example:
|
||||||
|
|
||||||
|
qs := []*survey.Question{
|
||||||
|
{
|
||||||
|
Name: "name",
|
||||||
|
Prompt: &survey.Input{Message: "What is your name?"},
|
||||||
|
Validate: survey.Required,
|
||||||
|
Transform: survey.Title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
answers := struct{ Name string }{}
|
||||||
|
|
||||||
|
|
||||||
|
err := survey.Ask(qs, &answers)
|
||||||
|
*/
|
||||||
|
func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
|
||||||
|
// build up the configuration options
|
||||||
|
options := defaultAskOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := opt(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we weren't passed a place to record the answers
|
||||||
|
if response == nil {
|
||||||
|
// we can't go any further
|
||||||
|
return errors.New("cannot call Ask() with a nil reference to record the answers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// go over every question
|
||||||
|
for _, q := range qs {
|
||||||
|
// If Prompt implements controllable stdio, pass in specified stdio.
|
||||||
|
if p, ok := q.Prompt.(wantsStdio); ok {
|
||||||
|
p.WithStdio(options.Stdio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab the user input and save it
|
||||||
|
ans, err := q.Prompt.Prompt(&options.PromptConfig)
|
||||||
|
// if there was a problem
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build up a list of validators that we have to apply to this question
|
||||||
|
validators := []Validator{}
|
||||||
|
|
||||||
|
// make sure to include the question specific one
|
||||||
|
if q.Validate != nil {
|
||||||
|
validators = append(validators, q.Validate)
|
||||||
|
}
|
||||||
|
// add any "global" validators
|
||||||
|
for _, validator := range options.Validators {
|
||||||
|
validators = append(validators, validator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply every validator to thte response
|
||||||
|
for _, validator := range validators {
|
||||||
|
// wait for a valid response
|
||||||
|
for invalid := validator(ans); invalid != nil; invalid = validator(ans) {
|
||||||
|
err := q.Prompt.Error(&options.PromptConfig, invalid)
|
||||||
|
// if there was a problem
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask for more input
|
||||||
|
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok {
|
||||||
|
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, invalid)
|
||||||
|
} else {
|
||||||
|
ans, err = q.Prompt.Prompt(&options.PromptConfig)
|
||||||
|
}
|
||||||
|
// if there was a problem
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.Transform != nil {
|
||||||
|
// check if we have a transformer available, if so
|
||||||
|
// then try to acquire the new representation of the
|
||||||
|
// answer, if the resulting answer is not nil.
|
||||||
|
if newAns := q.Transform(ans); newAns != nil {
|
||||||
|
ans = newAns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell the prompt to cleanup with the validated value
|
||||||
|
q.Prompt.Cleanup(&options.PromptConfig, ans)
|
||||||
|
|
||||||
|
// if something went wrong
|
||||||
|
if err != nil {
|
||||||
|
// stop listening
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add it to the map
|
||||||
|
err = core.WriteAnswer(response, q.Name, ans)
|
||||||
|
// if something went wrong
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the response
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// paginate returns a single page of choices given the page size, the total list of
|
||||||
|
// possible choices, and the current selected index in the total list.
|
||||||
|
func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.OptionAnswer, int) {
|
||||||
|
var start, end, cursor int
|
||||||
|
|
||||||
|
if len(choices) < pageSize {
|
||||||
|
// if we dont have enough options to fill a page
|
||||||
|
start = 0
|
||||||
|
end = len(choices)
|
||||||
|
cursor = sel
|
||||||
|
|
||||||
|
} else if sel < pageSize/2 {
|
||||||
|
// if we are in the first half page
|
||||||
|
start = 0
|
||||||
|
end = pageSize
|
||||||
|
cursor = sel
|
||||||
|
|
||||||
|
} else if len(choices)-sel-1 < pageSize/2 {
|
||||||
|
// if we are in the last half page
|
||||||
|
start = len(choices) - pageSize
|
||||||
|
end = len(choices)
|
||||||
|
cursor = sel - start
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// somewhere in the middle
|
||||||
|
above := pageSize / 2
|
||||||
|
below := pageSize - above
|
||||||
|
|
||||||
|
cursor = pageSize / 2
|
||||||
|
start = sel - above
|
||||||
|
end = sel + below
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the subset we care about and the index
|
||||||
|
return choices[start:end], cursor
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2014 Takashi Kokubun
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# survey/terminal
|
||||||
|
|
||||||
|
This package started as a copy of [kokuban/go-ansi](http://github.com/k0kubun/go-ansi) but has since been modified to fit survey's specific needs.
|
22
vendor/github.com/AlecAivazis/survey/v2/terminal/buffered_reader.go
generated
vendored
Normal file
22
vendor/github.com/AlecAivazis/survey/v2/terminal/buffered_reader.go
generated
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BufferedReader struct {
|
||||||
|
In io.Reader
|
||||||
|
Buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *BufferedReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := br.Buffer.Read(p)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return n, err
|
||||||
|
} else if err == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return br.In.Read(p[n:])
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var COORDINATE_SYSTEM_BEGIN Short = 1
|
||||||
|
|
||||||
|
var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`)
|
||||||
|
|
||||||
|
type Cursor struct {
|
||||||
|
In FileReader
|
||||||
|
Out FileWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up moves the cursor n cells to up.
|
||||||
|
func (c *Cursor) Up(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dA", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down moves the cursor n cells to down.
|
||||||
|
func (c *Cursor) Down(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dB", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward moves the cursor n cells to right.
|
||||||
|
func (c *Cursor) Forward(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dC", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back moves the cursor n cells to left.
|
||||||
|
func (c *Cursor) Back(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dD", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextLine moves cursor to beginning of the line n lines down.
|
||||||
|
func (c *Cursor) NextLine(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dE", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviousLine moves cursor to beginning of the line n lines up.
|
||||||
|
func (c *Cursor) PreviousLine(n int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dF", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalAbsolute moves cursor horizontally to x.
|
||||||
|
func (c *Cursor) HorizontalAbsolute(x int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%dG", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show shows the cursor.
|
||||||
|
func (c *Cursor) Show() {
|
||||||
|
fmt.Fprint(c.Out, "\x1b[?25h")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide hide the cursor.
|
||||||
|
func (c *Cursor) Hide() {
|
||||||
|
fmt.Fprint(c.Out, "\x1b[?25l")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move moves the cursor to a specific x,y location.
|
||||||
|
func (c *Cursor) Move(x int, y int) {
|
||||||
|
fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the current position
|
||||||
|
func (c *Cursor) Save() {
|
||||||
|
fmt.Fprint(c.Out, "\x1b7")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the saved position of the cursor
|
||||||
|
func (c *Cursor) Restore() {
|
||||||
|
fmt.Fprint(c.Out, "\x1b8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// for comparability purposes between windows
|
||||||
|
// in unix we need to print out a new line on some terminals
|
||||||
|
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) {
|
||||||
|
if cur.Y == terminalSize.Y {
|
||||||
|
fmt.Fprintln(c.Out)
|
||||||
|
}
|
||||||
|
c.NextLine(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the current location of the cursor in the terminal
|
||||||
|
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
|
||||||
|
// ANSI escape sequence for DSR - Device Status Report
|
||||||
|
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
|
||||||
|
fmt.Fprint(c.Out, "\x1b[6n")
|
||||||
|
|
||||||
|
// There may be input in Stdin prior to CursorLocation so make sure we don't
|
||||||
|
// drop those bytes.
|
||||||
|
var loc []int
|
||||||
|
var match string
|
||||||
|
for loc == nil {
|
||||||
|
// Reports the cursor position (CPR) to the application as (as though typed at
|
||||||
|
// the keyboard) ESC[n;mR, where n is the row and m is the column.
|
||||||
|
reader := bufio.NewReader(c.In)
|
||||||
|
text, err := reader.ReadSlice(byte('R'))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
loc = dsrPattern.FindStringIndex(string(text))
|
||||||
|
if loc == nil {
|
||||||
|
// After reading slice to byte 'R', the bufio Reader may have read more
|
||||||
|
// bytes into its internal buffer which will be discarded on next ReadSlice.
|
||||||
|
// We create a temporary buffer to read the remaining buffered slice and
|
||||||
|
// write them to output buffer.
|
||||||
|
buffered := make([]byte, reader.Buffered())
|
||||||
|
_, err = io.ReadFull(reader, buffered)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stdin contains R that doesn't match DSR, so pass the bytes along to
|
||||||
|
// output buffer.
|
||||||
|
buf.Write(text)
|
||||||
|
buf.Write(buffered)
|
||||||
|
} else {
|
||||||
|
// Write the non-matching leading bytes to output buffer.
|
||||||
|
buf.Write(text[:loc[0]])
|
||||||
|
|
||||||
|
// Save the matching bytes to extract the row and column of the cursor.
|
||||||
|
match = string(text[loc[0]:loc[1]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := dsrPattern.FindStringSubmatch(string(match))
|
||||||
|
if len(matches) != 3 {
|
||||||
|
return nil, fmt.Errorf("incorrect number of matches: %d", len(matches))
|
||||||
|
}
|
||||||
|
|
||||||
|
col, err := strconv.Atoi(matches[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err := strconv.Atoi(matches[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Coord{Short(col), Short(row)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
|
||||||
|
return cur.X == size.X
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cur Coord) CursorIsAtLineBegin() bool {
|
||||||
|
return cur.X == COORDINATE_SYSTEM_BEGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the height and width of the terminal.
|
||||||
|
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
|
||||||
|
// the general approach here is to move the cursor to the very bottom
|
||||||
|
// of the terminal, ask for the current location and then move the
|
||||||
|
// cursor back where we started
|
||||||
|
|
||||||
|
// hide the cursor (so it doesn't blink when getting the size of the terminal)
|
||||||
|
c.Hide()
|
||||||
|
// save the current location of the cursor
|
||||||
|
c.Save()
|
||||||
|
|
||||||
|
// move the cursor to the very bottom of the terminal
|
||||||
|
c.Move(999, 999)
|
||||||
|
|
||||||
|
// ask for the current location
|
||||||
|
bottom, err := c.Location(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// move back where we began
|
||||||
|
c.Restore()
|
||||||
|
|
||||||
|
// show the cursor
|
||||||
|
c.Show()
|
||||||
|
// since the bottom was calculated in the lower right corner, it
|
||||||
|
// is the dimensions we are looking for
|
||||||
|
return bottom, nil
|
||||||
|
}
|
138
vendor/github.com/AlecAivazis/survey/v2/terminal/cursor_windows.go
generated
vendored
Normal file
138
vendor/github.com/AlecAivazis/survey/v2/terminal/cursor_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var COORDINATE_SYSTEM_BEGIN Short = 0
|
||||||
|
|
||||||
|
// shared variable to save the cursor location from CursorSave()
|
||||||
|
var cursorLoc Coord
|
||||||
|
|
||||||
|
type Cursor struct {
|
||||||
|
In FileReader
|
||||||
|
Out FileWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Up(n int) {
|
||||||
|
c.cursorMove(0, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Down(n int) {
|
||||||
|
c.cursorMove(0, -1*n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Forward(n int) {
|
||||||
|
c.cursorMove(n, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Back(n int) {
|
||||||
|
c.cursorMove(-1*n, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the cursor location
|
||||||
|
func (c *Cursor) Save() {
|
||||||
|
cursorLoc, _ = c.Location(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Restore() {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
// restore it to the original position
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
|
||||||
|
return cur.X == size.X
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cur Coord) CursorIsAtLineBegin() bool {
|
||||||
|
return cur.X == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) cursorMove(x int, y int) {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
var cursor Coord
|
||||||
|
cursor.X = csbi.cursorPosition.X + Short(x)
|
||||||
|
cursor.Y = csbi.cursorPosition.Y + Short(y)
|
||||||
|
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) NextLine(n int) {
|
||||||
|
c.Up(n)
|
||||||
|
c.HorizontalAbsolute(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) PreviousLine(n int) {
|
||||||
|
c.Down(n)
|
||||||
|
c.HorizontalAbsolute(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for comparability purposes between windows
|
||||||
|
// in windows we don't have to print out a new line
|
||||||
|
func (c *Cursor) MoveNextLine(cur Coord, terminalSize *Coord) {
|
||||||
|
c.NextLine(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) HorizontalAbsolute(x int) {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
var cursor Coord
|
||||||
|
cursor.X = Short(x)
|
||||||
|
cursor.Y = csbi.cursorPosition.Y
|
||||||
|
|
||||||
|
if csbi.size.X < cursor.X {
|
||||||
|
cursor.X = csbi.size.X
|
||||||
|
}
|
||||||
|
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Show() {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var cci consoleCursorInfo
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||||
|
cci.visible = 1
|
||||||
|
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Hide() {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var cci consoleCursorInfo
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||||
|
cci.visible = 0
|
||||||
|
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Location(buf *bytes.Buffer) (Coord, error) {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
return csbi.cursorPosition, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
|
||||||
|
handle := syscall.Handle(c.Out.Fd())
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
// windows' coordinate system begins at (0, 0)
|
||||||
|
csbi.size.X--
|
||||||
|
csbi.size.Y--
|
||||||
|
return &csbi.size, nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
type EraseLineMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ERASE_LINE_END EraseLineMode = iota
|
||||||
|
ERASE_LINE_START
|
||||||
|
ERASE_LINE_ALL
|
||||||
|
)
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EraseLine(out FileWriter, mode EraseLineMode) {
|
||||||
|
fmt.Fprintf(out, "\x1b[%dK", mode)
|
||||||
|
}
|
27
vendor/github.com/AlecAivazis/survey/v2/terminal/display_windows.go
generated
vendored
Normal file
27
vendor/github.com/AlecAivazis/survey/v2/terminal/display_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EraseLine(out FileWriter, mode EraseLineMode) {
|
||||||
|
handle := syscall.Handle(out.Fd())
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
var w uint32
|
||||||
|
var x Short
|
||||||
|
cursor := csbi.cursorPosition
|
||||||
|
switch mode {
|
||||||
|
case ERASE_LINE_END:
|
||||||
|
x = csbi.size.X
|
||||||
|
case ERASE_LINE_START:
|
||||||
|
x = 0
|
||||||
|
case ERASE_LINE_ALL:
|
||||||
|
cursor.X = 0
|
||||||
|
x = csbi.size.X
|
||||||
|
}
|
||||||
|
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
InterruptErr = errors.New("interrupt")
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAnsiStdout returns special stdout, which converts escape sequences to Windows API calls
|
||||||
|
// on Windows environment.
|
||||||
|
func NewAnsiStdout(out FileWriter) io.Writer {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnsiStderr returns special stderr, which converts escape sequences to Windows API calls
|
||||||
|
// on Windows environment.
|
||||||
|
func NewAnsiStderr(out FileWriter) io.Writer {
|
||||||
|
return out
|
||||||
|
}
|
227
vendor/github.com/AlecAivazis/survey/v2/terminal/output_windows.go
generated
vendored
Normal file
227
vendor/github.com/AlecAivazis/survey/v2/terminal/output_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cursorFunctions = map[rune]func(c *Cursor) func(int){
|
||||||
|
'A': func(c *Cursor) func(int) { return c.Up },
|
||||||
|
'B': func(c *Cursor) func(int) { return c.Down },
|
||||||
|
'C': func(c *Cursor) func(int) { return c.Forward },
|
||||||
|
'D': func(c *Cursor) func(int) { return c.Back },
|
||||||
|
'E': func(c *Cursor) func(int) { return c.NextLine },
|
||||||
|
'F': func(c *Cursor) func(int) { return c.PreviousLine },
|
||||||
|
'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
foregroundBlue = 0x1
|
||||||
|
foregroundGreen = 0x2
|
||||||
|
foregroundRed = 0x4
|
||||||
|
foregroundIntensity = 0x8
|
||||||
|
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
|
||||||
|
backgroundBlue = 0x10
|
||||||
|
backgroundGreen = 0x20
|
||||||
|
backgroundRed = 0x40
|
||||||
|
backgroundIntensity = 0x80
|
||||||
|
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
out FileWriter
|
||||||
|
handle syscall.Handle
|
||||||
|
orgAttr word
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnsiStdout(out FileWriter) io.Writer {
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
if !isatty.IsTerminal(out.Fd()) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
handle := syscall.Handle(out.Fd())
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnsiStderr(out FileWriter) io.Writer {
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
if !isatty.IsTerminal(out.Fd()) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
handle := syscall.Handle(out.Fd())
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Write(data []byte) (n int, err error) {
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
|
||||||
|
for {
|
||||||
|
ch, size, err := r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
|
||||||
|
switch ch {
|
||||||
|
case '\x1b':
|
||||||
|
size, err = w.handleEscape(r)
|
||||||
|
n += size
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprint(w.out, string(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
|
||||||
|
buf := make([]byte, 0, 10)
|
||||||
|
buf = append(buf, "\x1b"...)
|
||||||
|
|
||||||
|
// Check '[' continues after \x1b
|
||||||
|
ch, size, err := r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(w.out, string(buf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
if ch != '[' {
|
||||||
|
fmt.Fprint(w.out, string(buf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse escape code
|
||||||
|
var code rune
|
||||||
|
argBuf := make([]byte, 0, 10)
|
||||||
|
for {
|
||||||
|
ch, size, err = r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(w.out, string(buf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
|
||||||
|
code = ch
|
||||||
|
break
|
||||||
|
}
|
||||||
|
argBuf = append(argBuf, string(ch)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.applyEscapeCode(buf, string(argBuf), code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
|
||||||
|
c := &Cursor{Out: w.out}
|
||||||
|
|
||||||
|
switch arg + string(code) {
|
||||||
|
case "?25h":
|
||||||
|
c.Show()
|
||||||
|
return
|
||||||
|
case "?25l":
|
||||||
|
c.Hide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, ok := cursorFunctions[code]; ok {
|
||||||
|
if n, err := strconv.Atoi(arg); err == nil {
|
||||||
|
f(c)(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case 'm':
|
||||||
|
w.applySelectGraphicRendition(arg)
|
||||||
|
default:
|
||||||
|
buf = append(buf, string(code)...)
|
||||||
|
fmt.Fprint(w.out, string(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original implementation: https://github.com/mattn/go-colorable
|
||||||
|
func (w *Writer) applySelectGraphicRendition(arg string) {
|
||||||
|
if arg == "" {
|
||||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
attr := csbi.attributes
|
||||||
|
|
||||||
|
for _, param := range strings.Split(arg, ";") {
|
||||||
|
n, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case n == 0 || n == 100:
|
||||||
|
attr = w.orgAttr
|
||||||
|
case 1 <= n && n <= 5:
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
case 30 <= n && n <= 37:
|
||||||
|
attr = (attr & backgroundMask)
|
||||||
|
if (n-30)&1 != 0 {
|
||||||
|
attr |= foregroundRed
|
||||||
|
}
|
||||||
|
if (n-30)&2 != 0 {
|
||||||
|
attr |= foregroundGreen
|
||||||
|
}
|
||||||
|
if (n-30)&4 != 0 {
|
||||||
|
attr |= foregroundBlue
|
||||||
|
}
|
||||||
|
case 40 <= n && n <= 47:
|
||||||
|
attr = (attr & foregroundMask)
|
||||||
|
if (n-40)&1 != 0 {
|
||||||
|
attr |= backgroundRed
|
||||||
|
}
|
||||||
|
if (n-40)&2 != 0 {
|
||||||
|
attr |= backgroundGreen
|
||||||
|
}
|
||||||
|
if (n-40)&4 != 0 {
|
||||||
|
attr |= backgroundBlue
|
||||||
|
}
|
||||||
|
case 90 <= n && n <= 97:
|
||||||
|
attr = (attr & backgroundMask)
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
if (n-90)&1 != 0 {
|
||||||
|
attr |= foregroundRed
|
||||||
|
}
|
||||||
|
if (n-90)&2 != 0 {
|
||||||
|
attr |= foregroundGreen
|
||||||
|
}
|
||||||
|
if (n-90)&4 != 0 {
|
||||||
|
attr |= foregroundBlue
|
||||||
|
}
|
||||||
|
case 100 <= n && n <= 107:
|
||||||
|
attr = (attr & foregroundMask)
|
||||||
|
attr |= backgroundIntensity
|
||||||
|
if (n-100)&1 != 0 {
|
||||||
|
attr |= backgroundRed
|
||||||
|
}
|
||||||
|
if (n-100)&2 != 0 {
|
||||||
|
attr |= backgroundGreen
|
||||||
|
}
|
||||||
|
if (n-100)&4 != 0 {
|
||||||
|
attr |= backgroundBlue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/text/width"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RuneReader struct {
|
||||||
|
stdio Stdio
|
||||||
|
cursor *Cursor
|
||||||
|
state runeReaderState
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRuneReader(stdio Stdio) *RuneReader {
|
||||||
|
return &RuneReader{
|
||||||
|
stdio: stdio,
|
||||||
|
state: newRuneReaderState(stdio.In),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) printChar(char rune, mask rune) {
|
||||||
|
// if we don't need to mask the input
|
||||||
|
if mask == 0 {
|
||||||
|
// just print the character the user pressed
|
||||||
|
fmt.Fprintf(rr.stdio.Out, "%c", char)
|
||||||
|
} else {
|
||||||
|
// otherwise print the mask we were given
|
||||||
|
fmt.Fprintf(rr.stdio.Out, "%c", mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
|
||||||
|
line := []rune{}
|
||||||
|
// we only care about horizontal displacements from the origin so start counting at 0
|
||||||
|
index := 0
|
||||||
|
|
||||||
|
cursor := &Cursor{
|
||||||
|
In: rr.stdio.In,
|
||||||
|
Out: rr.stdio.Out,
|
||||||
|
}
|
||||||
|
|
||||||
|
// we get the terminal width and height (if resized after this point the property might become invalid)
|
||||||
|
terminalSize, _ := cursor.Size(rr.Buffer())
|
||||||
|
// we set the current location of the cursor once
|
||||||
|
cursorCurrent, _ := cursor.Location(rr.Buffer())
|
||||||
|
|
||||||
|
for {
|
||||||
|
// wait for some input
|
||||||
|
r, _, err := rr.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
// increment cursor location
|
||||||
|
cursorCurrent.X++
|
||||||
|
|
||||||
|
// if the user pressed enter or some other newline/termination like ctrl+d
|
||||||
|
if r == '\r' || r == '\n' || r == KeyEndTransmission {
|
||||||
|
// delete what's printed out on the console screen (cleanup)
|
||||||
|
for index > 0 {
|
||||||
|
if cursorCurrent.CursorIsAtLineBegin() {
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
cursor.Forward(int(terminalSize.X))
|
||||||
|
cursorCurrent.X = terminalSize.X
|
||||||
|
cursorCurrent.Y--
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cursor.Back(1)
|
||||||
|
cursorCurrent.X--
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
|
// move the cursor the a new line
|
||||||
|
cursor.MoveNextLine(cursorCurrent, terminalSize)
|
||||||
|
|
||||||
|
// we're done processing the input
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
// if the user interrupts (ie with ctrl+c)
|
||||||
|
if r == KeyInterrupt {
|
||||||
|
// go to the beginning of the next line
|
||||||
|
fmt.Fprint(rr.stdio.Out, "\r\n")
|
||||||
|
|
||||||
|
// we're done processing the input, and treat interrupt like an error
|
||||||
|
return line, InterruptErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow for backspace/delete editing of inputs
|
||||||
|
if r == KeyBackspace || r == KeyDelete {
|
||||||
|
// and we're not at the beginning of the line
|
||||||
|
if index > 0 && len(line) > 0 {
|
||||||
|
// if we are at the end of the word
|
||||||
|
if index == len(line) {
|
||||||
|
// just remove the last letter from the internal representation
|
||||||
|
// also count the number of cells the rune before the cursor occupied
|
||||||
|
cells := runeWidth(line[len(line)-1])
|
||||||
|
line = line[:len(line)-1]
|
||||||
|
// go back one
|
||||||
|
if cursorCurrent.X == 1 {
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
cursor.Forward(int(terminalSize.X))
|
||||||
|
} else {
|
||||||
|
cursor.Back(cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the rest of the line
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
} else {
|
||||||
|
// we need to remove a character from the middle of the word
|
||||||
|
|
||||||
|
cells := runeWidth(line[index-1])
|
||||||
|
|
||||||
|
// remove the current index from the list
|
||||||
|
line = append(line[:index-1], line[index:]...)
|
||||||
|
|
||||||
|
// save the current position of the cursor, as we have to move the cursor one back to erase the current symbol
|
||||||
|
// and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore
|
||||||
|
// the cursor to its previous location.
|
||||||
|
cursor.Save()
|
||||||
|
|
||||||
|
// clear the rest of the line
|
||||||
|
cursor.Back(cells)
|
||||||
|
|
||||||
|
// print what comes after
|
||||||
|
for _, char := range line[index-1:] {
|
||||||
|
//Erase symbols which are left over from older print
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
// print characters to the new line appropriately
|
||||||
|
rr.printChar(char, mask)
|
||||||
|
|
||||||
|
}
|
||||||
|
// erase what's left over from last print
|
||||||
|
if cursorCurrent.Y < terminalSize.Y {
|
||||||
|
cursor.NextLine(1)
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
}
|
||||||
|
// restore cursor
|
||||||
|
cursor.Restore()
|
||||||
|
if cursorCurrent.CursorIsAtLineBegin() {
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
cursor.Forward(int(terminalSize.X))
|
||||||
|
} else {
|
||||||
|
cursor.Back(cells)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrement the index
|
||||||
|
index--
|
||||||
|
} else {
|
||||||
|
// otherwise the user pressed backspace while at the beginning of the line
|
||||||
|
soundBell(rr.stdio.Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done processing this key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the left arrow is pressed
|
||||||
|
if r == KeyArrowLeft {
|
||||||
|
// if we have space to the left
|
||||||
|
if index > 0 {
|
||||||
|
//move the cursor to the prev line if necessary
|
||||||
|
if cursorCurrent.CursorIsAtLineBegin() {
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
cursor.Forward(int(terminalSize.X))
|
||||||
|
} else {
|
||||||
|
cursor.Back(runeWidth(line[index-1]))
|
||||||
|
}
|
||||||
|
//decrement the index
|
||||||
|
index--
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// otherwise we are at the beginning of where we started reading lines
|
||||||
|
// sound the bell
|
||||||
|
soundBell(rr.stdio.Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done processing this key press
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the right arrow is pressed
|
||||||
|
if r == KeyArrowRight {
|
||||||
|
// if we have space to the right
|
||||||
|
if index < len(line) {
|
||||||
|
// move the cursor to the next line if necessary
|
||||||
|
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||||
|
cursor.NextLine(1)
|
||||||
|
} else {
|
||||||
|
cursor.Forward(runeWidth(line[index]))
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// otherwise we are at the end of the word and can't go past
|
||||||
|
// sound the bell
|
||||||
|
soundBell(rr.stdio.Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done processing this key press
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// the user pressed one of the special keys
|
||||||
|
if r == SpecialKeyHome {
|
||||||
|
for index > 0 {
|
||||||
|
if cursorCurrent.CursorIsAtLineBegin() {
|
||||||
|
cursor.PreviousLine(1)
|
||||||
|
cursor.Forward(int(terminalSize.X))
|
||||||
|
cursorCurrent.X = terminalSize.X
|
||||||
|
cursorCurrent.Y--
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cursor.Back(runeWidth(line[index-1]))
|
||||||
|
cursorCurrent.X--
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
// user pressed end
|
||||||
|
} else if r == SpecialKeyEnd {
|
||||||
|
for index != len(line) {
|
||||||
|
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||||
|
cursor.NextLine(1)
|
||||||
|
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
|
||||||
|
cursorCurrent.Y++
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cursor.Forward(runeWidth(line[index]))
|
||||||
|
cursorCurrent.X++
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
// user pressed forward delete key
|
||||||
|
} else if r == SpecialKeyDelete {
|
||||||
|
// if index at the end of the line nothing to delete
|
||||||
|
if index != len(line) {
|
||||||
|
// save the current position of the cursor, as we have to erase the current symbol
|
||||||
|
// and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore
|
||||||
|
// the cursor to its previous location.
|
||||||
|
cursor.Save()
|
||||||
|
// remove the symbol after the cursor
|
||||||
|
line = append(line[:index], line[index+1:]...)
|
||||||
|
// print the updated line
|
||||||
|
for _, char := range line[index:] {
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
// print out the character
|
||||||
|
rr.printChar(char, mask)
|
||||||
|
}
|
||||||
|
// erase what's left on last line
|
||||||
|
if cursorCurrent.Y < terminalSize.Y {
|
||||||
|
cursor.NextLine(1)
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
}
|
||||||
|
// restore cursor
|
||||||
|
cursor.Restore()
|
||||||
|
if len(line) == 0 || index == len(line) {
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the letter is another escape sequence
|
||||||
|
if unicode.IsControl(r) || r == IgnoreKey {
|
||||||
|
// ignore it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// the user pressed a regular key
|
||||||
|
|
||||||
|
// if we are at the end of the line
|
||||||
|
if index == len(line) {
|
||||||
|
// just append the character at the end of the line
|
||||||
|
line = append(line, r)
|
||||||
|
// save the location of the cursor
|
||||||
|
index++
|
||||||
|
// print out the character
|
||||||
|
rr.printChar(r, mask)
|
||||||
|
} else {
|
||||||
|
// we are in the middle of the word so we need to insert the character the user pressed
|
||||||
|
line = append(line[:index], append([]rune{r}, line[index:]...)...)
|
||||||
|
// save the current position of the cursor, as we have to move the cursor back to erase the current symbol
|
||||||
|
// and then move for each symbol in line[index:] to print it out, afterwards we want to restore
|
||||||
|
// cursor's location to its previous one.
|
||||||
|
cursor.Save()
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
// remove the symbol after the cursor
|
||||||
|
// print the updated line
|
||||||
|
for _, char := range line[index:] {
|
||||||
|
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||||
|
// print out the character
|
||||||
|
rr.printChar(char, mask)
|
||||||
|
cursorCurrent.X++
|
||||||
|
}
|
||||||
|
// if we are at the last line, we want to visually insert a new line and append to it.
|
||||||
|
if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
|
||||||
|
// add a new line to the terminal
|
||||||
|
fmt.Fprintln(rr.stdio.Out)
|
||||||
|
// restore the position of the cursor horizontally
|
||||||
|
cursor.Restore()
|
||||||
|
// restore the position of the cursor vertically
|
||||||
|
cursor.Up(1)
|
||||||
|
} else {
|
||||||
|
// restore cursor
|
||||||
|
cursor.Restore()
|
||||||
|
}
|
||||||
|
// check if cursor needs to move to next line
|
||||||
|
cursorCurrent, _ = cursor.Location(rr.Buffer())
|
||||||
|
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||||
|
cursor.NextLine(1)
|
||||||
|
} else {
|
||||||
|
cursor.Forward(runeWidth(r))
|
||||||
|
}
|
||||||
|
// increment the index
|
||||||
|
index++
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runeWidth(r rune) int {
|
||||||
|
switch width.LookupRune(r).Kind() {
|
||||||
|
case width.EastAsianWide, width.EastAsianFullwidth:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
13
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_bsd.go
generated
vendored
Normal file
13
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_bsd.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd netbsd openbsd
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
const ioctlWriteTermios = syscall.TIOCSETA
|
13
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_linux.go
generated
vendored
Normal file
13
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_linux.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
// +build linux,!ppc64le
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
// These constants are declared here, rather than importing
|
||||||
|
// them from the syscall package as some syscall packages, even
|
||||||
|
// on linux, for example gccgo, do not declare them.
|
||||||
|
const ioctlReadTermios = 0x5401 // syscall.TCGETS
|
||||||
|
const ioctlWriteTermios = 0x5402 // syscall.TCSETS
|
111
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_posix.go
generated
vendored
Normal file
111
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_posix.go
generated
vendored
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
// The terminal mode manipulation code is derived heavily from:
|
||||||
|
// https://github.com/golang/crypto/blob/master/ssh/terminal/util.go:
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runeReaderState struct {
|
||||||
|
term syscall.Termios
|
||||||
|
reader *bufio.Reader
|
||||||
|
buf *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuneReaderState(input FileReader) runeReaderState {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
return runeReaderState{
|
||||||
|
reader: bufio.NewReader(&BufferedReader{
|
||||||
|
In: input,
|
||||||
|
Buffer: buf,
|
||||||
|
}),
|
||||||
|
buf: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) Buffer() *bytes.Buffer {
|
||||||
|
return rr.state.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// For reading runes we just want to disable echo.
|
||||||
|
func (rr *RuneReader) SetTermMode() error {
|
||||||
|
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := rr.state.term
|
||||||
|
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG
|
||||||
|
|
||||||
|
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) RestoreTermMode() error {
|
||||||
|
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) ReadRune() (rune, int, error) {
|
||||||
|
r, size, err := rr.state.reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return r, size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse ^[ sequences to look for arrow keys
|
||||||
|
if r == '\033' {
|
||||||
|
if rr.state.reader.Buffered() == 0 {
|
||||||
|
// no more characters so must be `Esc` key
|
||||||
|
return KeyEscape, 1, nil
|
||||||
|
}
|
||||||
|
r, size, err = rr.state.reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return r, size, err
|
||||||
|
}
|
||||||
|
if r != '[' {
|
||||||
|
return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r})
|
||||||
|
}
|
||||||
|
r, size, err = rr.state.reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return r, size, err
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case 'D':
|
||||||
|
return KeyArrowLeft, 1, nil
|
||||||
|
case 'C':
|
||||||
|
return KeyArrowRight, 1, nil
|
||||||
|
case 'A':
|
||||||
|
return KeyArrowUp, 1, nil
|
||||||
|
case 'B':
|
||||||
|
return KeyArrowDown, 1, nil
|
||||||
|
case 'H': // Home button
|
||||||
|
return SpecialKeyHome, 1, nil
|
||||||
|
case 'F': // End button
|
||||||
|
return SpecialKeyEnd, 1, nil
|
||||||
|
case '3': // Delete Button
|
||||||
|
// discard the following '~' key from buffer
|
||||||
|
rr.state.reader.Discard(1)
|
||||||
|
return SpecialKeyDelete, 1, nil
|
||||||
|
default:
|
||||||
|
// discard the following '~' key from buffer
|
||||||
|
rr.state.reader.Discard(1)
|
||||||
|
return IgnoreKey, 1, nil
|
||||||
|
}
|
||||||
|
return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r})
|
||||||
|
}
|
||||||
|
return r, size, err
|
||||||
|
}
|
7
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_ppc64le.go
generated
vendored
Normal file
7
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_ppc64le.go
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// +build ppc64le,linux
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
// Used syscall numbers from https://github.com/golang/go/blob/master/src/syscall/ztypes_linux_ppc64le.go
|
||||||
|
const ioctlReadTermios = 0x402c7413 // syscall.TCGETS
|
||||||
|
const ioctlWriteTermios = 0x802c7414 // syscall.TCSETS
|
142
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_windows.go
generated
vendored
Normal file
142
vendor/github.com/AlecAivazis/survey/v2/terminal/runereader_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dll = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
setConsoleMode = dll.NewProc("SetConsoleMode")
|
||||||
|
getConsoleMode = dll.NewProc("GetConsoleMode")
|
||||||
|
readConsoleInput = dll.NewProc("ReadConsoleInputW")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EVENT_KEY = 0x0001
|
||||||
|
|
||||||
|
// key codes for arrow keys
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||||
|
VK_DELETE = 0x2E
|
||||||
|
VK_END = 0x23
|
||||||
|
VK_HOME = 0x24
|
||||||
|
VK_LEFT = 0x25
|
||||||
|
VK_UP = 0x26
|
||||||
|
VK_RIGHT = 0x27
|
||||||
|
VK_DOWN = 0x28
|
||||||
|
|
||||||
|
RIGHT_CTRL_PRESSED = 0x0004
|
||||||
|
LEFT_CTRL_PRESSED = 0x0008
|
||||||
|
|
||||||
|
ENABLE_ECHO_INPUT uint32 = 0x0004
|
||||||
|
ENABLE_LINE_INPUT uint32 = 0x0002
|
||||||
|
ENABLE_PROCESSED_INPUT uint32 = 0x0001
|
||||||
|
)
|
||||||
|
|
||||||
|
type inputRecord struct {
|
||||||
|
eventType uint16
|
||||||
|
padding uint16
|
||||||
|
event [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyEventRecord struct {
|
||||||
|
bKeyDown int32
|
||||||
|
wRepeatCount uint16
|
||||||
|
wVirtualKeyCode uint16
|
||||||
|
wVirtualScanCode uint16
|
||||||
|
unicodeChar uint16
|
||||||
|
wdControlKeyState uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type runeReaderState struct {
|
||||||
|
term uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuneReaderState(input FileReader) runeReaderState {
|
||||||
|
return runeReaderState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) Buffer() *bytes.Buffer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) SetTermMode() error {
|
||||||
|
r, _, err := getConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(unsafe.Pointer(&rr.state.term)))
|
||||||
|
// windows return 0 on error
|
||||||
|
if r == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := rr.state.term
|
||||||
|
newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
|
||||||
|
r, _, err = setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState))
|
||||||
|
// windows return 0 on error
|
||||||
|
if r == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) RestoreTermMode() error {
|
||||||
|
r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(rr.state.term))
|
||||||
|
// windows return 0 on error
|
||||||
|
if r == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RuneReader) ReadRune() (rune, int, error) {
|
||||||
|
ir := &inputRecord{}
|
||||||
|
bytesRead := 0
|
||||||
|
for {
|
||||||
|
rv, _, e := readConsoleInput.Call(rr.stdio.In.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead)))
|
||||||
|
// windows returns non-zero to indicate success
|
||||||
|
if rv == 0 && e != nil {
|
||||||
|
return 0, 0, e
|
||||||
|
}
|
||||||
|
|
||||||
|
if ir.eventType != EVENT_KEY {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// the event data is really a c struct union, so here we have to do an usafe
|
||||||
|
// cast to put the data into the keyEventRecord (since we have already verified
|
||||||
|
// above that this event does correspond to a key event
|
||||||
|
key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0]))
|
||||||
|
// we only care about key down events
|
||||||
|
if key.bKeyDown == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' {
|
||||||
|
return KeyInterrupt, bytesRead, nil
|
||||||
|
}
|
||||||
|
// not a normal character so look up the input sequence from the
|
||||||
|
// virtual key code mappings (VK_*)
|
||||||
|
if key.unicodeChar == 0 {
|
||||||
|
switch key.wVirtualKeyCode {
|
||||||
|
case VK_DOWN:
|
||||||
|
return KeyArrowDown, bytesRead, nil
|
||||||
|
case VK_LEFT:
|
||||||
|
return KeyArrowLeft, bytesRead, nil
|
||||||
|
case VK_RIGHT:
|
||||||
|
return KeyArrowRight, bytesRead, nil
|
||||||
|
case VK_UP:
|
||||||
|
return KeyArrowUp, bytesRead, nil
|
||||||
|
case VK_DELETE:
|
||||||
|
return SpecialKeyDelete, bytesRead, nil
|
||||||
|
case VK_HOME:
|
||||||
|
return SpecialKeyHome, bytesRead, nil
|
||||||
|
case VK_END:
|
||||||
|
return SpecialKeyEnd, bytesRead, nil
|
||||||
|
default:
|
||||||
|
// not a virtual key that we care about so just continue on to
|
||||||
|
// the next input key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := rune(key.unicodeChar)
|
||||||
|
return r, bytesRead, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyArrowLeft = '\x02'
|
||||||
|
KeyArrowRight = '\x06'
|
||||||
|
KeyArrowUp = '\x10'
|
||||||
|
KeyArrowDown = '\x0e'
|
||||||
|
KeySpace = ' '
|
||||||
|
KeyEnter = '\r'
|
||||||
|
KeyBackspace = '\b'
|
||||||
|
KeyDelete = '\x7f'
|
||||||
|
KeyInterrupt = '\x03'
|
||||||
|
KeyEndTransmission = '\x04'
|
||||||
|
KeyEscape = '\x1b'
|
||||||
|
KeyDeleteWord = '\x17' // Ctrl+W
|
||||||
|
KeyDeleteLine = '\x18' // Ctrl+X
|
||||||
|
SpecialKeyHome = '\x01'
|
||||||
|
SpecialKeyEnd = '\x11'
|
||||||
|
SpecialKeyDelete = '\x12'
|
||||||
|
IgnoreKey = '\000'
|
||||||
|
)
|
||||||
|
|
||||||
|
func soundBell(out io.Writer) {
|
||||||
|
fmt.Fprint(out, "\a")
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stdio is the standard input/output the terminal reads/writes with.
|
||||||
|
type Stdio struct {
|
||||||
|
In FileReader
|
||||||
|
Out FileWriter
|
||||||
|
Err io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileWriter provides a minimal interface for Stdin.
|
||||||
|
type FileWriter interface {
|
||||||
|
io.Writer
|
||||||
|
Fd() uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileReader provides a minimal interface for Stdout.
|
||||||
|
type FileReader interface {
|
||||||
|
io.Reader
|
||||||
|
Fd() uintptr
|
||||||
|
}
|
39
vendor/github.com/AlecAivazis/survey/v2/terminal/syscall_windows.go
generated
vendored
Normal file
39
vendor/github.com/AlecAivazis/survey/v2/terminal/syscall_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
|
||||||
|
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||||
|
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||||
|
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
|
||||||
|
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
|
||||||
|
)
|
||||||
|
|
||||||
|
type wchar uint16
|
||||||
|
type dword uint32
|
||||||
|
type word uint16
|
||||||
|
|
||||||
|
type smallRect struct {
|
||||||
|
left Short
|
||||||
|
top Short
|
||||||
|
right Short
|
||||||
|
bottom Short
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleScreenBufferInfo struct {
|
||||||
|
size Coord
|
||||||
|
cursorPosition Coord
|
||||||
|
attributes word
|
||||||
|
window smallRect
|
||||||
|
maximumWindowSize Coord
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleCursorInfo struct {
|
||||||
|
size dword
|
||||||
|
visible int32
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
type Short int16
|
||||||
|
|
||||||
|
type Coord struct {
|
||||||
|
X Short
|
||||||
|
Y Short
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransformString returns a `Transformer` based on the "f"
|
||||||
|
// function which accepts a string representation of the answer
|
||||||
|
// and returns a new one, transformed, answer.
|
||||||
|
// Take for example the functions inside the std `strings` package,
|
||||||
|
// they can be converted to a compatible `Transformer` by using this function,
|
||||||
|
// i.e: `TransformString(strings.Title)`, `TransformString(strings.ToUpper)`.
|
||||||
|
//
|
||||||
|
// Note that `TransformString` is just a helper, `Transformer` can be used
|
||||||
|
// to transform any type of answer.
|
||||||
|
func TransformString(f func(s string) string) Transformer {
|
||||||
|
return func(ans interface{}) interface{} {
|
||||||
|
// if the answer value passed in is the zero value of the appropriate type
|
||||||
|
if isZero(reflect.ValueOf(ans)) {
|
||||||
|
// skip this `Transformer` by returning a nil value.
|
||||||
|
// The original answer will be not affected,
|
||||||
|
// see survey.go#L125.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// "ans" is never nil here, so we don't have to check that
|
||||||
|
// see survey.go#L97 for more.
|
||||||
|
// Make sure that the the answer's value was a typeof string.
|
||||||
|
s, ok := ans.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLower is a `Transformer`.
|
||||||
|
// It receives an answer value
|
||||||
|
// and returns a copy of the "ans"
|
||||||
|
// with all Unicode letters mapped to their lower case.
|
||||||
|
//
|
||||||
|
// Note that if "ans" is not a string then it will
|
||||||
|
// return a nil value, meaning that the above answer
|
||||||
|
// will not be affected by this call at all.
|
||||||
|
func ToLower(ans interface{}) interface{} {
|
||||||
|
transformer := TransformString(strings.ToLower)
|
||||||
|
return transformer(ans)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title is a `Transformer`.
|
||||||
|
// It receives an answer value
|
||||||
|
// and returns a copy of the "ans"
|
||||||
|
// with all Unicode letters that begin words
|
||||||
|
// mapped to their title case.
|
||||||
|
//
|
||||||
|
// Note that if "ans" is not a string then it will
|
||||||
|
// return a nil value, meaning that the above answer
|
||||||
|
// will not be affected by this call at all.
|
||||||
|
func Title(ans interface{}) interface{} {
|
||||||
|
transformer := TransformString(strings.Title)
|
||||||
|
return transformer(ans)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeTransformers is a variadic function used to create one transformer from many.
|
||||||
|
func ComposeTransformers(transformers ...Transformer) Transformer {
|
||||||
|
// return a transformer that calls each one sequentially
|
||||||
|
return func(ans interface{}) interface{} {
|
||||||
|
// execute each transformer
|
||||||
|
for _, t := range transformers {
|
||||||
|
ans = t(ans)
|
||||||
|
}
|
||||||
|
return ans
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package survey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Required does not allow an empty value
|
||||||
|
func Required(val interface{}) error {
|
||||||
|
// the reflect value of the result
|
||||||
|
value := reflect.ValueOf(val)
|
||||||
|
|
||||||
|
// if the value passed in is the zero value of the appropriate type
|
||||||
|
if isZero(value) && value.Kind() != reflect.Bool {
|
||||||
|
return errors.New("Value is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxLength requires that the string is no longer than the specified value
|
||||||
|
func MaxLength(length int) Validator {
|
||||||
|
// return a validator that checks the length of the string
|
||||||
|
return func(val interface{}) error {
|
||||||
|
if str, ok := val.(string); ok {
|
||||||
|
// if the string is longer than the given value
|
||||||
|
if len([]rune(str)) > length {
|
||||||
|
// yell loudly
|
||||||
|
return fmt.Errorf("value is too long. Max length is %v", length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise we cannot convert the value into a string and cannot enforce length
|
||||||
|
return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// the input is fine
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinLength requires that the string is longer or equal in length to the specified value
|
||||||
|
func MinLength(length int) Validator {
|
||||||
|
// return a validator that checks the length of the string
|
||||||
|
return func(val interface{}) error {
|
||||||
|
if str, ok := val.(string); ok {
|
||||||
|
// if the string is shorter than the given value
|
||||||
|
if len([]rune(str)) < length {
|
||||||
|
// yell loudly
|
||||||
|
return fmt.Errorf("value is too short. Min length is %v", length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise we cannot convert the value into a string and cannot enforce length
|
||||||
|
return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// the input is fine
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeValidators is a variadic function used to create one validator from many.
|
||||||
|
func ComposeValidators(validators ...Validator) Validator {
|
||||||
|
// return a validator that calls each one sequentially
|
||||||
|
return func(val interface{}) error {
|
||||||
|
// execute each validator
|
||||||
|
for _, validator := range validators {
|
||||||
|
// if the answer's value is not valid
|
||||||
|
if err := validator(val); err != nil {
|
||||||
|
// return the error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we passed all validators, the answer is valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isZero returns true if the passed value is the zero object
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Slice, reflect.Map:
|
||||||
|
return v.Len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the types directly with more general coverage
|
||||||
|
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (C) 2014 Kevin Ballard
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||||
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,36 @@
|
||||||
|
PACKAGE
|
||||||
|
|
||||||
|
package shellquote
|
||||||
|
import "github.com/kballard/go-shellquote"
|
||||||
|
|
||||||
|
Shellquote provides utilities for joining/splitting strings using sh's
|
||||||
|
word-splitting rules.
|
||||||
|
|
||||||
|
VARIABLES
|
||||||
|
|
||||||
|
var (
|
||||||
|
UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
|
||||||
|
UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
|
||||||
|
UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTIONS
|
||||||
|
|
||||||
|
func Join(args ...string) string
|
||||||
|
Join quotes each argument and joins them with a space. If passed to
|
||||||
|
/bin/sh, the resulting string will be split back into the original
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
func Split(input string) (words []string, err error)
|
||||||
|
Split splits a string according to /bin/sh's word-splitting rules. It
|
||||||
|
supports backslash-escapes, single-quotes, and double-quotes. Notably it
|
||||||
|
does not support the $'' style of quoting. It also doesn't attempt to
|
||||||
|
perform any other sort of expansion, including brace expansion, shell
|
||||||
|
expansion, or pathname expansion.
|
||||||
|
|
||||||
|
If the given input has an unterminated quoted string or ends in a
|
||||||
|
backslash-escape, one of UnterminatedSingleQuoteError,
|
||||||
|
UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Shellquote provides utilities for joining/splitting strings using sh's
|
||||||
|
// word-splitting rules.
|
||||||
|
package shellquote
|
|
@ -0,0 +1,102 @@
|
||||||
|
package shellquote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Join quotes each argument and joins them with a space.
|
||||||
|
// If passed to /bin/sh, the resulting string will be split back into the
|
||||||
|
// original arguments.
|
||||||
|
func Join(args ...string) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for i, arg := range args {
|
||||||
|
if i != 0 {
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
|
quote(arg, &buf)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
specialChars = "\\'\"`${[|&;<>()*?!"
|
||||||
|
extraSpecialChars = " \t\n"
|
||||||
|
prefixChars = "~"
|
||||||
|
)
|
||||||
|
|
||||||
|
func quote(word string, buf *bytes.Buffer) {
|
||||||
|
// We want to try to produce a "nice" output. As such, we will
|
||||||
|
// backslash-escape most characters, but if we encounter a space, or if we
|
||||||
|
// encounter an extra-special char (which doesn't work with
|
||||||
|
// backslash-escaping) we switch over to quoting the whole word. We do this
|
||||||
|
// with a space because it's typically easier for people to read multi-word
|
||||||
|
// arguments when quoted with a space rather than with ugly backslashes
|
||||||
|
// everywhere.
|
||||||
|
origLen := buf.Len()
|
||||||
|
|
||||||
|
if len(word) == 0 {
|
||||||
|
// oops, no content
|
||||||
|
buf.WriteString("''")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cur, prev := word, word
|
||||||
|
atStart := true
|
||||||
|
for len(cur) > 0 {
|
||||||
|
c, l := utf8.DecodeRuneInString(cur)
|
||||||
|
cur = cur[l:]
|
||||||
|
if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) {
|
||||||
|
// copy the non-special chars up to this point
|
||||||
|
if len(cur) < len(prev) {
|
||||||
|
buf.WriteString(prev[0 : len(prev)-len(cur)-l])
|
||||||
|
}
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteRune(c)
|
||||||
|
prev = cur
|
||||||
|
} else if strings.ContainsRune(extraSpecialChars, c) {
|
||||||
|
// start over in quote mode
|
||||||
|
buf.Truncate(origLen)
|
||||||
|
goto quote
|
||||||
|
}
|
||||||
|
atStart = false
|
||||||
|
}
|
||||||
|
if len(prev) > 0 {
|
||||||
|
buf.WriteString(prev)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
quote:
|
||||||
|
// quote mode
|
||||||
|
// Use single-quotes, but if we find a single-quote in the word, we need
|
||||||
|
// to terminate the string, emit an escaped quote, and start the string up
|
||||||
|
// again
|
||||||
|
inQuote := false
|
||||||
|
for len(word) > 0 {
|
||||||
|
i := strings.IndexRune(word, '\'')
|
||||||
|
if i == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
if !inQuote {
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
inQuote = true
|
||||||
|
}
|
||||||
|
buf.WriteString(word[0:i])
|
||||||
|
}
|
||||||
|
word = word[i+1:]
|
||||||
|
if inQuote {
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
inQuote = false
|
||||||
|
}
|
||||||
|
buf.WriteString("\\'")
|
||||||
|
}
|
||||||
|
if len(word) > 0 {
|
||||||
|
if !inQuote {
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
}
|
||||||
|
buf.WriteString(word)
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package shellquote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
|
||||||
|
UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
|
||||||
|
UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
splitChars = " \n\t"
|
||||||
|
singleChar = '\''
|
||||||
|
doubleChar = '"'
|
||||||
|
escapeChar = '\\'
|
||||||
|
doubleEscapeChars = "$`\"\n\\"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split splits a string according to /bin/sh's word-splitting rules. It
|
||||||
|
// supports backslash-escapes, single-quotes, and double-quotes. Notably it does
|
||||||
|
// not support the $'' style of quoting. It also doesn't attempt to perform any
|
||||||
|
// other sort of expansion, including brace expansion, shell expansion, or
|
||||||
|
// pathname expansion.
|
||||||
|
//
|
||||||
|
// If the given input has an unterminated quoted string or ends in a
|
||||||
|
// backslash-escape, one of UnterminatedSingleQuoteError,
|
||||||
|
// UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
|
||||||
|
func Split(input string) (words []string, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
words = make([]string, 0)
|
||||||
|
|
||||||
|
for len(input) > 0 {
|
||||||
|
// skip any splitChars at the start
|
||||||
|
c, l := utf8.DecodeRuneInString(input)
|
||||||
|
if strings.ContainsRune(splitChars, c) {
|
||||||
|
input = input[l:]
|
||||||
|
continue
|
||||||
|
} else if c == escapeChar {
|
||||||
|
// Look ahead for escaped newline so we can skip over it
|
||||||
|
next := input[l:]
|
||||||
|
if len(next) == 0 {
|
||||||
|
err = UnterminatedEscapeError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c2, l2 := utf8.DecodeRuneInString(next)
|
||||||
|
if c2 == '\n' {
|
||||||
|
input = next[l2:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var word string
|
||||||
|
word, input, err = splitWord(input, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
raw:
|
||||||
|
{
|
||||||
|
cur := input
|
||||||
|
for len(cur) > 0 {
|
||||||
|
c, l := utf8.DecodeRuneInString(cur)
|
||||||
|
cur = cur[l:]
|
||||||
|
if c == singleChar {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||||
|
input = cur
|
||||||
|
goto single
|
||||||
|
} else if c == doubleChar {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||||
|
input = cur
|
||||||
|
goto double
|
||||||
|
} else if c == escapeChar {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||||
|
input = cur
|
||||||
|
goto escape
|
||||||
|
} else if strings.ContainsRune(splitChars, c) {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||||
|
return buf.String(), cur, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(input) > 0 {
|
||||||
|
buf.WriteString(input)
|
||||||
|
input = ""
|
||||||
|
}
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
|
||||||
|
escape:
|
||||||
|
{
|
||||||
|
if len(input) == 0 {
|
||||||
|
return "", "", UnterminatedEscapeError
|
||||||
|
}
|
||||||
|
c, l := utf8.DecodeRuneInString(input)
|
||||||
|
if c == '\n' {
|
||||||
|
// a backslash-escaped newline is elided from the output entirely
|
||||||
|
} else {
|
||||||
|
buf.WriteString(input[:l])
|
||||||
|
}
|
||||||
|
input = input[l:]
|
||||||
|
}
|
||||||
|
goto raw
|
||||||
|
|
||||||
|
single:
|
||||||
|
{
|
||||||
|
i := strings.IndexRune(input, singleChar)
|
||||||
|
if i == -1 {
|
||||||
|
return "", "", UnterminatedSingleQuoteError
|
||||||
|
}
|
||||||
|
buf.WriteString(input[0:i])
|
||||||
|
input = input[i+1:]
|
||||||
|
goto raw
|
||||||
|
}
|
||||||
|
|
||||||
|
double:
|
||||||
|
{
|
||||||
|
cur := input
|
||||||
|
for len(cur) > 0 {
|
||||||
|
c, l := utf8.DecodeRuneInString(cur)
|
||||||
|
cur = cur[l:]
|
||||||
|
if c == doubleChar {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l])
|
||||||
|
input = cur
|
||||||
|
goto raw
|
||||||
|
} else if c == escapeChar {
|
||||||
|
// bash only supports certain escapes in double-quoted strings
|
||||||
|
c2, l2 := utf8.DecodeRuneInString(cur)
|
||||||
|
cur = cur[l2:]
|
||||||
|
if strings.ContainsRune(doubleEscapeChars, c2) {
|
||||||
|
buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
|
||||||
|
if c2 == '\n' {
|
||||||
|
// newline is special, skip the backslash entirely
|
||||||
|
} else {
|
||||||
|
buf.WriteRune(c2)
|
||||||
|
}
|
||||||
|
input = cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", UnterminatedDoubleQuoteError
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
return buf.String(), input, nil
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
go:
|
||||||
|
- 1.13.x
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get -t -v ./...
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ./go.test.sh
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,48 @@
|
||||||
|
# go-colorable
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable)
|
||||||
|
[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
|
||||||
|
|
||||||
|
Colorable writer for windows.
|
||||||
|
|
||||||
|
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.)
|
||||||
|
This package is possible to handle escape sequence for ansi color on windows.
|
||||||
|
|
||||||
|
## Too Bad!
|
||||||
|
|
||||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png)
|
||||||
|
|
||||||
|
|
||||||
|
## So Good!
|
||||||
|
|
||||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||||
|
logrus.SetOutput(colorable.NewColorableStdout())
|
||||||
|
|
||||||
|
logrus.Info("succeeded")
|
||||||
|
logrus.Warn("not correct")
|
||||||
|
logrus.Error("something error")
|
||||||
|
logrus.Fatal("panic")
|
||||||
|
```
|
||||||
|
|
||||||
|
You can compile above code on non-windows OSs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/mattn/go-colorable
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
# Author
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto (a.k.a mattn)
|
|
@ -0,0 +1,37 @@
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewColorable returns new instance of Writer which handles escape sequence.
|
||||||
|
func NewColorable(file *os.File) io.Writer {
|
||||||
|
if file == nil {
|
||||||
|
panic("nil passed instead of *os.File to NewColorable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout.
|
||||||
|
func NewColorableStdout() io.Writer {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr.
|
||||||
|
func NewColorableStderr() io.Writer {
|
||||||
|
return os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableColorsStdout enable colors if possible.
|
||||||
|
func EnableColorsStdout(enabled *bool) func() {
|
||||||
|
if enabled != nil {
|
||||||
|
*enabled = true
|
||||||
|
}
|
||||||
|
return func() {}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// +build !windows
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewColorable returns new instance of Writer which handles escape sequence.
|
||||||
|
func NewColorable(file *os.File) io.Writer {
|
||||||
|
if file == nil {
|
||||||
|
panic("nil passed instead of *os.File to NewColorable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout.
|
||||||
|
func NewColorableStdout() io.Writer {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr.
|
||||||
|
func NewColorableStderr() io.Writer {
|
||||||
|
return os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableColorsStdout enable colors if possible.
|
||||||
|
func EnableColorsStdout(enabled *bool) func() {
|
||||||
|
if enabled != nil {
|
||||||
|
*enabled = true
|
||||||
|
}
|
||||||
|
return func() {}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
module github.com/mattn/go-colorable
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-isatty v0.0.12
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.13
|
|
@ -0,0 +1,5 @@
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
echo "" > coverage.txt
|
||||||
|
|
||||||
|
for d in $(go list ./... | grep -v vendor); do
|
||||||
|
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||||
|
if [ -f profile.out ]; then
|
||||||
|
cat profile.out >> coverage.txt
|
||||||
|
rm profile.out
|
||||||
|
fi
|
||||||
|
done
|
|
@ -0,0 +1,55 @@
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NonColorable holds writer but removes escape sequence.
|
||||||
|
type NonColorable struct {
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNonColorable returns new instance of Writer which removes escape sequence from Writer.
|
||||||
|
func NewNonColorable(w io.Writer) io.Writer {
|
||||||
|
return &NonColorable{out: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes data on console
|
||||||
|
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
||||||
|
er := bytes.NewReader(data)
|
||||||
|
var bw [1]byte
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
c1, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if c1 != 0x1b {
|
||||||
|
bw[0] = c1
|
||||||
|
w.out.Write(bw[:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c2, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if c2 != 0x5b {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for {
|
||||||
|
c, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.Write([]byte(string(c)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(data), nil
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
*.test
|
|
@ -0,0 +1,9 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2013 Mario L. Gutierrez
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
# ansi
|
||||||
|
|
||||||
|
Package ansi is a small, fast library to create ANSI colored strings and codes.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Get it
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get -u github.com/mgutz/ansi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/mgutz/ansi"
|
||||||
|
|
||||||
|
// colorize a string, SLOW
|
||||||
|
msg := ansi.Color("foo", "red+b:white")
|
||||||
|
|
||||||
|
// create a FAST closure function to avoid computation of ANSI code
|
||||||
|
phosphorize := ansi.ColorFunc("green+h:black")
|
||||||
|
msg = phosphorize("Bring back the 80s!")
|
||||||
|
msg2 := phospohorize("Look, I'm a CRT!")
|
||||||
|
|
||||||
|
// cache escape codes and build strings manually
|
||||||
|
lime := ansi.ColorCode("green+h:black")
|
||||||
|
reset := ansi.ColorCode("reset")
|
||||||
|
|
||||||
|
fmt.Println(lime, "Bring back the 80s!", reset)
|
||||||
|
```
|
||||||
|
|
||||||
|
Other examples
|
||||||
|
|
||||||
|
```go
|
||||||
|
Color(s, "red") // red
|
||||||
|
Color(s, "red+b") // red bold
|
||||||
|
Color(s, "red+B") // red blinking
|
||||||
|
Color(s, "red+u") // red underline
|
||||||
|
Color(s, "red+bh") // red bold bright
|
||||||
|
Color(s, "red:white") // red on white
|
||||||
|
Color(s, "red+b:white+h") // red bold on white bright
|
||||||
|
Color(s, "red+B:white+h") // red blink on white bright
|
||||||
|
Color(s, "off") // turn off ansi codes
|
||||||
|
```
|
||||||
|
|
||||||
|
To view color combinations, from project directory in terminal.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Style format
|
||||||
|
|
||||||
|
```go
|
||||||
|
"foregroundColor+attributes:backgroundColor+attributes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Colors
|
||||||
|
|
||||||
|
* black
|
||||||
|
* red
|
||||||
|
* green
|
||||||
|
* yellow
|
||||||
|
* blue
|
||||||
|
* magenta
|
||||||
|
* cyan
|
||||||
|
* white
|
||||||
|
* 0...255 (256 colors)
|
||||||
|
|
||||||
|
Foreground Attributes
|
||||||
|
|
||||||
|
* B = Blink
|
||||||
|
* b = bold
|
||||||
|
* h = high intensity (bright)
|
||||||
|
* i = inverse
|
||||||
|
* s = strikethrough
|
||||||
|
* u = underline
|
||||||
|
|
||||||
|
Background Attributes
|
||||||
|
|
||||||
|
* h = high intensity (bright)
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
* ansi.Reset
|
||||||
|
* ansi.DefaultBG
|
||||||
|
* ansi.DefaultFG
|
||||||
|
* ansi.Black
|
||||||
|
* ansi.Red
|
||||||
|
* ansi.Green
|
||||||
|
* ansi.Yellow
|
||||||
|
* ansi.Blue
|
||||||
|
* ansi.Magenta
|
||||||
|
* ansi.Cyan
|
||||||
|
* ansi.White
|
||||||
|
* ansi.LightBlack
|
||||||
|
* ansi.LightRed
|
||||||
|
* ansi.LightGreen
|
||||||
|
* ansi.LightYellow
|
||||||
|
* ansi.LightBlue
|
||||||
|
* ansi.LightMagenta
|
||||||
|
* ansi.LightCyan
|
||||||
|
* ansi.LightWhite
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Wikipedia ANSI escape codes [Colors](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
|
||||||
|
|
||||||
|
General [tips and formatting](http://misc.flogisoft.com/bash/tip_colors_and_formatting)
|
||||||
|
|
||||||
|
What about support on Windows? Use [colorable by mattn](https://github.com/mattn/go-colorable).
|
||||||
|
Ansi and colorable are used by [logxi](https://github.com/mgutz/logxi) to support logging in
|
||||||
|
color on Windows.
|
||||||
|
|
||||||
|
## MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2013 Mario Gutierrez mario@mgutz.com
|
||||||
|
|
||||||
|
See the file LICENSE for copying permission.
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
black = iota
|
||||||
|
red
|
||||||
|
green
|
||||||
|
yellow
|
||||||
|
blue
|
||||||
|
magenta
|
||||||
|
cyan
|
||||||
|
white
|
||||||
|
defaultt = 9
|
||||||
|
|
||||||
|
normalIntensityFG = 30
|
||||||
|
highIntensityFG = 90
|
||||||
|
normalIntensityBG = 40
|
||||||
|
highIntensityBG = 100
|
||||||
|
|
||||||
|
start = "\033["
|
||||||
|
bold = "1;"
|
||||||
|
blink = "5;"
|
||||||
|
underline = "4;"
|
||||||
|
inverse = "7;"
|
||||||
|
strikethrough = "9;"
|
||||||
|
|
||||||
|
// Reset is the ANSI reset escape sequence
|
||||||
|
Reset = "\033[0m"
|
||||||
|
// DefaultBG is the default background
|
||||||
|
DefaultBG = "\033[49m"
|
||||||
|
// DefaultFG is the default foreground
|
||||||
|
DefaultFG = "\033[39m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Black FG
|
||||||
|
var Black string
|
||||||
|
|
||||||
|
// Red FG
|
||||||
|
var Red string
|
||||||
|
|
||||||
|
// Green FG
|
||||||
|
var Green string
|
||||||
|
|
||||||
|
// Yellow FG
|
||||||
|
var Yellow string
|
||||||
|
|
||||||
|
// Blue FG
|
||||||
|
var Blue string
|
||||||
|
|
||||||
|
// Magenta FG
|
||||||
|
var Magenta string
|
||||||
|
|
||||||
|
// Cyan FG
|
||||||
|
var Cyan string
|
||||||
|
|
||||||
|
// White FG
|
||||||
|
var White string
|
||||||
|
|
||||||
|
// LightBlack FG
|
||||||
|
var LightBlack string
|
||||||
|
|
||||||
|
// LightRed FG
|
||||||
|
var LightRed string
|
||||||
|
|
||||||
|
// LightGreen FG
|
||||||
|
var LightGreen string
|
||||||
|
|
||||||
|
// LightYellow FG
|
||||||
|
var LightYellow string
|
||||||
|
|
||||||
|
// LightBlue FG
|
||||||
|
var LightBlue string
|
||||||
|
|
||||||
|
// LightMagenta FG
|
||||||
|
var LightMagenta string
|
||||||
|
|
||||||
|
// LightCyan FG
|
||||||
|
var LightCyan string
|
||||||
|
|
||||||
|
// LightWhite FG
|
||||||
|
var LightWhite string
|
||||||
|
|
||||||
|
var (
|
||||||
|
plain = false
|
||||||
|
// Colors maps common color names to their ANSI color code.
|
||||||
|
Colors = map[string]int{
|
||||||
|
"black": black,
|
||||||
|
"red": red,
|
||||||
|
"green": green,
|
||||||
|
"yellow": yellow,
|
||||||
|
"blue": blue,
|
||||||
|
"magenta": magenta,
|
||||||
|
"cyan": cyan,
|
||||||
|
"white": white,
|
||||||
|
"default": defaultt,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
Colors[strconv.Itoa(i)] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
Black = ColorCode("black")
|
||||||
|
Red = ColorCode("red")
|
||||||
|
Green = ColorCode("green")
|
||||||
|
Yellow = ColorCode("yellow")
|
||||||
|
Blue = ColorCode("blue")
|
||||||
|
Magenta = ColorCode("magenta")
|
||||||
|
Cyan = ColorCode("cyan")
|
||||||
|
White = ColorCode("white")
|
||||||
|
LightBlack = ColorCode("black+h")
|
||||||
|
LightRed = ColorCode("red+h")
|
||||||
|
LightGreen = ColorCode("green+h")
|
||||||
|
LightYellow = ColorCode("yellow+h")
|
||||||
|
LightBlue = ColorCode("blue+h")
|
||||||
|
LightMagenta = ColorCode("magenta+h")
|
||||||
|
LightCyan = ColorCode("cyan+h")
|
||||||
|
LightWhite = ColorCode("white+h")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorCode returns the ANSI color color code for style.
|
||||||
|
func ColorCode(style string) string {
|
||||||
|
return colorCode(style).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the ANSI color code for a style.
|
||||||
|
func colorCode(style string) *bytes.Buffer {
|
||||||
|
buf := bytes.NewBufferString("")
|
||||||
|
if plain || style == "" {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
if style == "reset" {
|
||||||
|
buf.WriteString(Reset)
|
||||||
|
return buf
|
||||||
|
} else if style == "off" {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
foregroundBackground := strings.Split(style, ":")
|
||||||
|
foreground := strings.Split(foregroundBackground[0], "+")
|
||||||
|
fgKey := foreground[0]
|
||||||
|
fg := Colors[fgKey]
|
||||||
|
fgStyle := ""
|
||||||
|
if len(foreground) > 1 {
|
||||||
|
fgStyle = foreground[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
bg, bgStyle := "", ""
|
||||||
|
|
||||||
|
if len(foregroundBackground) > 1 {
|
||||||
|
background := strings.Split(foregroundBackground[1], "+")
|
||||||
|
bg = background[0]
|
||||||
|
if len(background) > 1 {
|
||||||
|
bgStyle = background[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(start)
|
||||||
|
base := normalIntensityFG
|
||||||
|
if len(fgStyle) > 0 {
|
||||||
|
if strings.Contains(fgStyle, "b") {
|
||||||
|
buf.WriteString(bold)
|
||||||
|
}
|
||||||
|
if strings.Contains(fgStyle, "B") {
|
||||||
|
buf.WriteString(blink)
|
||||||
|
}
|
||||||
|
if strings.Contains(fgStyle, "u") {
|
||||||
|
buf.WriteString(underline)
|
||||||
|
}
|
||||||
|
if strings.Contains(fgStyle, "i") {
|
||||||
|
buf.WriteString(inverse)
|
||||||
|
}
|
||||||
|
if strings.Contains(fgStyle, "s") {
|
||||||
|
buf.WriteString(strikethrough)
|
||||||
|
}
|
||||||
|
if strings.Contains(fgStyle, "h") {
|
||||||
|
base = highIntensityFG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if 256-color
|
||||||
|
n, err := strconv.Atoi(fgKey)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Fprintf(buf, "38;5;%d;", n)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%d;", base+fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
base = normalIntensityBG
|
||||||
|
if len(bg) > 0 {
|
||||||
|
if strings.Contains(bgStyle, "h") {
|
||||||
|
base = highIntensityBG
|
||||||
|
}
|
||||||
|
// if 256-color
|
||||||
|
n, err := strconv.Atoi(bg)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Fprintf(buf, "48;5;%d;", n)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%d;", base+Colors[bg])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove last ";"
|
||||||
|
buf.Truncate(buf.Len() - 1)
|
||||||
|
buf.WriteRune('m')
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color colors a string based on the ANSI color code for style.
|
||||||
|
func Color(s, style string) string {
|
||||||
|
if plain || len(style) < 1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
buf := colorCode(style)
|
||||||
|
buf.WriteString(s)
|
||||||
|
buf.WriteString(Reset)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorFunc creates a closure to avoid computation ANSI color code.
|
||||||
|
func ColorFunc(style string) func(string) string {
|
||||||
|
if style == "" {
|
||||||
|
return func(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color := ColorCode(style)
|
||||||
|
return func(s string) string {
|
||||||
|
if plain || s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
buf := bytes.NewBufferString(color)
|
||||||
|
buf.WriteString(s)
|
||||||
|
buf.WriteString(Reset)
|
||||||
|
result := buf.String()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableColors disables ANSI color codes. The default is false (colors are on).
|
||||||
|
func DisableColors(disable bool) {
|
||||||
|
plain = disable
|
||||||
|
if plain {
|
||||||
|
Black = ""
|
||||||
|
Red = ""
|
||||||
|
Green = ""
|
||||||
|
Yellow = ""
|
||||||
|
Blue = ""
|
||||||
|
Magenta = ""
|
||||||
|
Cyan = ""
|
||||||
|
White = ""
|
||||||
|
LightBlack = ""
|
||||||
|
LightRed = ""
|
||||||
|
LightGreen = ""
|
||||||
|
LightYellow = ""
|
||||||
|
LightBlue = ""
|
||||||
|
LightMagenta = ""
|
||||||
|
LightCyan = ""
|
||||||
|
LightWhite = ""
|
||||||
|
} else {
|
||||||
|
Black = ColorCode("black")
|
||||||
|
Red = ColorCode("red")
|
||||||
|
Green = ColorCode("green")
|
||||||
|
Yellow = ColorCode("yellow")
|
||||||
|
Blue = ColorCode("blue")
|
||||||
|
Magenta = ColorCode("magenta")
|
||||||
|
Cyan = ColorCode("cyan")
|
||||||
|
White = ColorCode("white")
|
||||||
|
LightBlack = ColorCode("black+h")
|
||||||
|
LightRed = ColorCode("red+h")
|
||||||
|
LightGreen = ColorCode("green+h")
|
||||||
|
LightYellow = ColorCode("yellow+h")
|
||||||
|
LightBlue = ColorCode("blue+h")
|
||||||
|
LightMagenta = ColorCode("magenta+h")
|
||||||
|
LightCyan = ColorCode("cyan+h")
|
||||||
|
LightWhite = ColorCode("white+h")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
Package ansi is a small, fast library to create ANSI colored strings and codes.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
|
||||||
|
# this installs the color viewer and the package
|
||||||
|
go get -u github.com/mgutz/ansi/cmd/ansi-mgutz
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
// colorize a string, SLOW
|
||||||
|
msg := ansi.Color("foo", "red+b:white")
|
||||||
|
|
||||||
|
// create a closure to avoid recalculating ANSI code compilation
|
||||||
|
phosphorize := ansi.ColorFunc("green+h:black")
|
||||||
|
msg = phosphorize("Bring back the 80s!")
|
||||||
|
msg2 := phospohorize("Look, I'm a CRT!")
|
||||||
|
|
||||||
|
// cache escape codes and build strings manually
|
||||||
|
lime := ansi.ColorCode("green+h:black")
|
||||||
|
reset := ansi.ColorCode("reset")
|
||||||
|
|
||||||
|
fmt.Println(lime, "Bring back the 80s!", reset)
|
||||||
|
|
||||||
|
Other examples
|
||||||
|
|
||||||
|
Color(s, "red") // red
|
||||||
|
Color(s, "red+b") // red bold
|
||||||
|
Color(s, "red+B") // red blinking
|
||||||
|
Color(s, "red+u") // red underline
|
||||||
|
Color(s, "red+bh") // red bold bright
|
||||||
|
Color(s, "red:white") // red on white
|
||||||
|
Color(s, "red+b:white+h") // red bold on white bright
|
||||||
|
Color(s, "red+B:white+h") // red blink on white bright
|
||||||
|
|
||||||
|
To view color combinations, from terminal
|
||||||
|
|
||||||
|
ansi-mgutz
|
||||||
|
|
||||||
|
Style format
|
||||||
|
|
||||||
|
"foregroundColor+attributes:backgroundColor+attributes"
|
||||||
|
|
||||||
|
Colors
|
||||||
|
|
||||||
|
black
|
||||||
|
red
|
||||||
|
green
|
||||||
|
yellow
|
||||||
|
blue
|
||||||
|
magenta
|
||||||
|
cyan
|
||||||
|
white
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
|
||||||
|
b = bold foreground
|
||||||
|
B = Blink foreground
|
||||||
|
u = underline foreground
|
||||||
|
h = high intensity (bright) foreground, background
|
||||||
|
i = inverse
|
||||||
|
|
||||||
|
Wikipedia ANSI escape codes [Colors](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
|
||||||
|
*/
|
||||||
|
package ansi
|
|
@ -0,0 +1,57 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
colorable "github.com/mattn/go-colorable"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintStyles prints all style combinations to the terminal.
|
||||||
|
func PrintStyles() {
|
||||||
|
// for compatibility with Windows, not needed for *nix
|
||||||
|
stdout := colorable.NewColorableStdout()
|
||||||
|
|
||||||
|
bgColors := []string{
|
||||||
|
"",
|
||||||
|
":black",
|
||||||
|
":red",
|
||||||
|
":green",
|
||||||
|
":yellow",
|
||||||
|
":blue",
|
||||||
|
":magenta",
|
||||||
|
":cyan",
|
||||||
|
":white",
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(Colors))
|
||||||
|
for k := range Colors {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(sort.StringSlice(keys))
|
||||||
|
|
||||||
|
for _, fg := range keys {
|
||||||
|
for _, bg := range bgColors {
|
||||||
|
fmt.Fprintln(stdout, padColor(fg, []string{"" + bg, "+b" + bg, "+bh" + bg, "+u" + bg}))
|
||||||
|
fmt.Fprintln(stdout, padColor(fg, []string{"+s" + bg, "+i" + bg}))
|
||||||
|
fmt.Fprintln(stdout, padColor(fg, []string{"+uh" + bg, "+B" + bg, "+Bb" + bg /* backgrounds */, "" + bg + "+h"}))
|
||||||
|
fmt.Fprintln(stdout, padColor(fg, []string{"+b" + bg + "+h", "+bh" + bg + "+h", "+u" + bg + "+h", "+uh" + bg + "+h"}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pad(s string, length int) string {
|
||||||
|
for len(s) < length {
|
||||||
|
s += " "
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func padColor(color string, styles []string) string {
|
||||||
|
buffer := ""
|
||||||
|
for _, style := range styles {
|
||||||
|
buffer += Color(pad(color+style, 20), color+style)
|
||||||
|
}
|
||||||
|
return buffer
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This source code refers to The Go Authors for copyright purposes.
|
||||||
|
# The master list of authors is in the main Go distribution,
|
||||||
|
# visible at http://tip.golang.org/AUTHORS.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This source code was written by the Go contributors.
|
||||||
|
# The master list of contributors is in the main Go distribution,
|
||||||
|
# visible at http://tip.golang.org/CONTRIBUTORS.
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,22 @@
|
||||||
|
Additional IP Rights Grant (Patents)
|
||||||
|
|
||||||
|
"This implementation" means the copyrightable works distributed by
|
||||||
|
Google as part of the Go project.
|
||||||
|
|
||||||
|
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||||
|
patent license to make, have made, use, offer to sell, sell, import,
|
||||||
|
transfer and otherwise run, modify and propagate the contents of this
|
||||||
|
implementation of Go, where such license applies only to those patent
|
||||||
|
claims, both currently owned or controlled by Google and acquired in
|
||||||
|
the future, licensable by Google that are necessarily infringed by this
|
||||||
|
implementation of Go. This grant does not include claims that would be
|
||||||
|
infringed only as a consequence of further modification of this
|
||||||
|
implementation. If you or your agent or exclusive licensee institute or
|
||||||
|
order or agree to the institution of patent litigation against any
|
||||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that this implementation of Go or any code incorporated within this
|
||||||
|
implementation of Go constitutes direct or contributory patent
|
||||||
|
infringement, or inducement of patent infringement, then any patent
|
||||||
|
rights granted to you under this License for this implementation of Go
|
||||||
|
shall terminate as of the date such litigation is filed.
|
|
@ -0,0 +1,705 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package transform provides reader and writer wrappers that transform the
|
||||||
|
// bytes passing through as well as various transformations. Example
|
||||||
|
// transformations provided by other packages include normalization and
|
||||||
|
// conversion between character sets.
|
||||||
|
package transform // import "golang.org/x/text/transform"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrShortDst means that the destination buffer was too short to
|
||||||
|
// receive all of the transformed bytes.
|
||||||
|
ErrShortDst = errors.New("transform: short destination buffer")
|
||||||
|
|
||||||
|
// ErrShortSrc means that the source buffer has insufficient data to
|
||||||
|
// complete the transformation.
|
||||||
|
ErrShortSrc = errors.New("transform: short source buffer")
|
||||||
|
|
||||||
|
// ErrEndOfSpan means that the input and output (the transformed input)
|
||||||
|
// are not identical.
|
||||||
|
ErrEndOfSpan = errors.New("transform: input and output are not identical")
|
||||||
|
|
||||||
|
// errInconsistentByteCount means that Transform returned success (nil
|
||||||
|
// error) but also returned nSrc inconsistent with the src argument.
|
||||||
|
errInconsistentByteCount = errors.New("transform: inconsistent byte count returned")
|
||||||
|
|
||||||
|
// errShortInternal means that an internal buffer is not large enough
|
||||||
|
// to make progress and the Transform operation must be aborted.
|
||||||
|
errShortInternal = errors.New("transform: short internal buffer")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transformer transforms bytes.
|
||||||
|
type Transformer interface {
|
||||||
|
// Transform writes to dst the transformed bytes read from src, and
|
||||||
|
// returns the number of dst bytes written and src bytes read. The
|
||||||
|
// atEOF argument tells whether src represents the last bytes of the
|
||||||
|
// input.
|
||||||
|
//
|
||||||
|
// Callers should always process the nDst bytes produced and account
|
||||||
|
// for the nSrc bytes consumed before considering the error err.
|
||||||
|
//
|
||||||
|
// A nil error means that all of the transformed bytes (whether freshly
|
||||||
|
// transformed from src or left over from previous Transform calls)
|
||||||
|
// were written to dst. A nil error can be returned regardless of
|
||||||
|
// whether atEOF is true. If err is nil then nSrc must equal len(src);
|
||||||
|
// the converse is not necessarily true.
|
||||||
|
//
|
||||||
|
// ErrShortDst means that dst was too short to receive all of the
|
||||||
|
// transformed bytes. ErrShortSrc means that src had insufficient data
|
||||||
|
// to complete the transformation. If both conditions apply, then
|
||||||
|
// either error may be returned. Other than the error conditions listed
|
||||||
|
// here, implementations are free to report other errors that arise.
|
||||||
|
Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error)
|
||||||
|
|
||||||
|
// Reset resets the state and allows a Transformer to be reused.
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpanningTransformer extends the Transformer interface with a Span method
|
||||||
|
// that determines how much of the input already conforms to the Transformer.
|
||||||
|
type SpanningTransformer interface {
|
||||||
|
Transformer
|
||||||
|
|
||||||
|
// Span returns a position in src such that transforming src[:n] results in
|
||||||
|
// identical output src[:n] for these bytes. It does not necessarily return
|
||||||
|
// the largest such n. The atEOF argument tells whether src represents the
|
||||||
|
// last bytes of the input.
|
||||||
|
//
|
||||||
|
// Callers should always account for the n bytes consumed before
|
||||||
|
// considering the error err.
|
||||||
|
//
|
||||||
|
// A nil error means that all input bytes are known to be identical to the
|
||||||
|
// output produced by the Transformer. A nil error can be returned
|
||||||
|
// regardless of whether atEOF is true. If err is nil, then n must
|
||||||
|
// equal len(src); the converse is not necessarily true.
|
||||||
|
//
|
||||||
|
// ErrEndOfSpan means that the Transformer output may differ from the
|
||||||
|
// input after n bytes. Note that n may be len(src), meaning that the output
|
||||||
|
// would contain additional bytes after otherwise identical output.
|
||||||
|
// ErrShortSrc means that src had insufficient data to determine whether the
|
||||||
|
// remaining bytes would change. Other than the error conditions listed
|
||||||
|
// here, implementations are free to report other errors that arise.
|
||||||
|
//
|
||||||
|
// Calling Span can modify the Transformer state as a side effect. In
|
||||||
|
// effect, it does the transformation just as calling Transform would, only
|
||||||
|
// without copying to a destination buffer and only up to a point it can
|
||||||
|
// determine the input and output bytes are the same. This is obviously more
|
||||||
|
// limited than calling Transform, but can be more efficient in terms of
|
||||||
|
// copying and allocating buffers. Calls to Span and Transform may be
|
||||||
|
// interleaved.
|
||||||
|
Span(src []byte, atEOF bool) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NopResetter can be embedded by implementations of Transformer to add a nop
|
||||||
|
// Reset method.
|
||||||
|
type NopResetter struct{}
|
||||||
|
|
||||||
|
// Reset implements the Reset method of the Transformer interface.
|
||||||
|
func (NopResetter) Reset() {}
|
||||||
|
|
||||||
|
// Reader wraps another io.Reader by transforming the bytes read.
|
||||||
|
type Reader struct {
|
||||||
|
r io.Reader
|
||||||
|
t Transformer
|
||||||
|
err error
|
||||||
|
|
||||||
|
// dst[dst0:dst1] contains bytes that have been transformed by t but
|
||||||
|
// not yet copied out via Read.
|
||||||
|
dst []byte
|
||||||
|
dst0, dst1 int
|
||||||
|
|
||||||
|
// src[src0:src1] contains bytes that have been read from r but not
|
||||||
|
// yet transformed through t.
|
||||||
|
src []byte
|
||||||
|
src0, src1 int
|
||||||
|
|
||||||
|
// transformComplete is whether the transformation is complete,
|
||||||
|
// regardless of whether or not it was successful.
|
||||||
|
transformComplete bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultBufSize = 4096
|
||||||
|
|
||||||
|
// NewReader returns a new Reader that wraps r by transforming the bytes read
|
||||||
|
// via t. It calls Reset on t.
|
||||||
|
func NewReader(r io.Reader, t Transformer) *Reader {
|
||||||
|
t.Reset()
|
||||||
|
return &Reader{
|
||||||
|
r: r,
|
||||||
|
t: t,
|
||||||
|
dst: make([]byte, defaultBufSize),
|
||||||
|
src: make([]byte, defaultBufSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements the io.Reader interface.
|
||||||
|
func (r *Reader) Read(p []byte) (int, error) {
|
||||||
|
n, err := 0, error(nil)
|
||||||
|
for {
|
||||||
|
// Copy out any transformed bytes and return the final error if we are done.
|
||||||
|
if r.dst0 != r.dst1 {
|
||||||
|
n = copy(p, r.dst[r.dst0:r.dst1])
|
||||||
|
r.dst0 += n
|
||||||
|
if r.dst0 == r.dst1 && r.transformComplete {
|
||||||
|
return n, r.err
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
} else if r.transformComplete {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to transform some source bytes, or to flush the transformer if we
|
||||||
|
// are out of source bytes. We do this even if r.r.Read returned an error.
|
||||||
|
// As the io.Reader documentation says, "process the n > 0 bytes returned
|
||||||
|
// before considering the error".
|
||||||
|
if r.src0 != r.src1 || r.err != nil {
|
||||||
|
r.dst0 = 0
|
||||||
|
r.dst1, n, err = r.t.Transform(r.dst, r.src[r.src0:r.src1], r.err == io.EOF)
|
||||||
|
r.src0 += n
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
if r.src0 != r.src1 {
|
||||||
|
r.err = errInconsistentByteCount
|
||||||
|
}
|
||||||
|
// The Transform call was successful; we are complete if we
|
||||||
|
// cannot read more bytes into src.
|
||||||
|
r.transformComplete = r.err != nil
|
||||||
|
continue
|
||||||
|
case err == ErrShortDst && (r.dst1 != 0 || n != 0):
|
||||||
|
// Make room in dst by copying out, and try again.
|
||||||
|
continue
|
||||||
|
case err == ErrShortSrc && r.src1-r.src0 != len(r.src) && r.err == nil:
|
||||||
|
// Read more bytes into src via the code below, and try again.
|
||||||
|
default:
|
||||||
|
r.transformComplete = true
|
||||||
|
// The reader error (r.err) takes precedence over the
|
||||||
|
// transformer error (err) unless r.err is nil or io.EOF.
|
||||||
|
if r.err == nil || r.err == io.EOF {
|
||||||
|
r.err = err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move any untransformed source bytes to the start of the buffer
|
||||||
|
// and read more bytes.
|
||||||
|
if r.src0 != 0 {
|
||||||
|
r.src0, r.src1 = 0, copy(r.src, r.src[r.src0:r.src1])
|
||||||
|
}
|
||||||
|
n, r.err = r.r.Read(r.src[r.src1:])
|
||||||
|
r.src1 += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement ReadByte (and ReadRune??).
|
||||||
|
|
||||||
|
// Writer wraps another io.Writer by transforming the bytes read.
|
||||||
|
// The user needs to call Close to flush unwritten bytes that may
|
||||||
|
// be buffered.
|
||||||
|
type Writer struct {
|
||||||
|
w io.Writer
|
||||||
|
t Transformer
|
||||||
|
dst []byte
|
||||||
|
|
||||||
|
// src[:n] contains bytes that have not yet passed through t.
|
||||||
|
src []byte
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter returns a new Writer that wraps w by transforming the bytes written
|
||||||
|
// via t. It calls Reset on t.
|
||||||
|
func NewWriter(w io.Writer, t Transformer) *Writer {
|
||||||
|
t.Reset()
|
||||||
|
return &Writer{
|
||||||
|
w: w,
|
||||||
|
t: t,
|
||||||
|
dst: make([]byte, defaultBufSize),
|
||||||
|
src: make([]byte, defaultBufSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements the io.Writer interface. If there are not enough
|
||||||
|
// bytes available to complete a Transform, the bytes will be buffered
|
||||||
|
// for the next write. Call Close to convert the remaining bytes.
|
||||||
|
func (w *Writer) Write(data []byte) (n int, err error) {
|
||||||
|
src := data
|
||||||
|
if w.n > 0 {
|
||||||
|
// Append bytes from data to the last remainder.
|
||||||
|
// TODO: limit the amount copied on first try.
|
||||||
|
n = copy(w.src[w.n:], data)
|
||||||
|
w.n += n
|
||||||
|
src = w.src[:w.n]
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
nDst, nSrc, err := w.t.Transform(w.dst, src, false)
|
||||||
|
if _, werr := w.w.Write(w.dst[:nDst]); werr != nil {
|
||||||
|
return n, werr
|
||||||
|
}
|
||||||
|
src = src[nSrc:]
|
||||||
|
if w.n == 0 {
|
||||||
|
n += nSrc
|
||||||
|
} else if len(src) <= n {
|
||||||
|
// Enough bytes from w.src have been consumed. We make src point
|
||||||
|
// to data instead to reduce the copying.
|
||||||
|
w.n = 0
|
||||||
|
n -= len(src)
|
||||||
|
src = data[n:]
|
||||||
|
if n < len(data) && (err == nil || err == ErrShortSrc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch err {
|
||||||
|
case ErrShortDst:
|
||||||
|
// This error is okay as long as we are making progress.
|
||||||
|
if nDst > 0 || nSrc > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case ErrShortSrc:
|
||||||
|
if len(src) < len(w.src) {
|
||||||
|
m := copy(w.src, src)
|
||||||
|
// If w.n > 0, bytes from data were already copied to w.src and n
|
||||||
|
// was already set to the number of bytes consumed.
|
||||||
|
if w.n == 0 {
|
||||||
|
n += m
|
||||||
|
}
|
||||||
|
w.n = m
|
||||||
|
err = nil
|
||||||
|
} else if nDst > 0 || nSrc > 0 {
|
||||||
|
// Not enough buffer to store the remainder. Keep processing as
|
||||||
|
// long as there is progress. Without this case, transforms that
|
||||||
|
// require a lookahead larger than the buffer may result in an
|
||||||
|
// error. This is not something one may expect to be common in
|
||||||
|
// practice, but it may occur when buffers are set to small
|
||||||
|
// sizes during testing.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
if w.n > 0 {
|
||||||
|
err = errInconsistentByteCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the io.Closer interface.
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
src := w.src[:w.n]
|
||||||
|
for {
|
||||||
|
nDst, nSrc, err := w.t.Transform(w.dst, src, true)
|
||||||
|
if _, werr := w.w.Write(w.dst[:nDst]); werr != nil {
|
||||||
|
return werr
|
||||||
|
}
|
||||||
|
if err != ErrShortDst {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
src = src[nSrc:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type nop struct{ NopResetter }
|
||||||
|
|
||||||
|
func (nop) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
n := copy(dst, src)
|
||||||
|
if n < len(src) {
|
||||||
|
err = ErrShortDst
|
||||||
|
}
|
||||||
|
return n, n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nop) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
return len(src), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type discard struct{ NopResetter }
|
||||||
|
|
||||||
|
func (discard) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
return 0, len(src), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Discard is a Transformer for which all Transform calls succeed
|
||||||
|
// by consuming all bytes and writing nothing.
|
||||||
|
Discard Transformer = discard{}
|
||||||
|
|
||||||
|
// Nop is a SpanningTransformer that copies src to dst.
|
||||||
|
Nop SpanningTransformer = nop{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// chain is a sequence of links. A chain with N Transformers has N+1 links and
|
||||||
|
// N+1 buffers. Of those N+1 buffers, the first and last are the src and dst
|
||||||
|
// buffers given to chain.Transform and the middle N-1 buffers are intermediate
|
||||||
|
// buffers owned by the chain. The i'th link transforms bytes from the i'th
|
||||||
|
// buffer chain.link[i].b at read offset chain.link[i].p to the i+1'th buffer
|
||||||
|
// chain.link[i+1].b at write offset chain.link[i+1].n, for i in [0, N).
|
||||||
|
type chain struct {
|
||||||
|
link []link
|
||||||
|
err error
|
||||||
|
// errStart is the index at which the error occurred plus 1. Processing
|
||||||
|
// errStart at this level at the next call to Transform. As long as
|
||||||
|
// errStart > 0, chain will not consume any more source bytes.
|
||||||
|
errStart int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chain) fatalError(errIndex int, err error) {
|
||||||
|
if i := errIndex + 1; i > c.errStart {
|
||||||
|
c.errStart = i
|
||||||
|
c.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type link struct {
|
||||||
|
t Transformer
|
||||||
|
// b[p:n] holds the bytes to be transformed by t.
|
||||||
|
b []byte
|
||||||
|
p int
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *link) src() []byte {
|
||||||
|
return l.b[l.p:l.n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *link) dst() []byte {
|
||||||
|
return l.b[l.n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain returns a Transformer that applies t in sequence.
|
||||||
|
func Chain(t ...Transformer) Transformer {
|
||||||
|
if len(t) == 0 {
|
||||||
|
return nop{}
|
||||||
|
}
|
||||||
|
c := &chain{link: make([]link, len(t)+1)}
|
||||||
|
for i, tt := range t {
|
||||||
|
c.link[i].t = tt
|
||||||
|
}
|
||||||
|
// Allocate intermediate buffers.
|
||||||
|
b := make([][defaultBufSize]byte, len(t)-1)
|
||||||
|
for i := range b {
|
||||||
|
c.link[i+1].b = b[i][:]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets the state of Chain. It calls Reset on all the Transformers.
|
||||||
|
func (c *chain) Reset() {
|
||||||
|
for i, l := range c.link {
|
||||||
|
if l.t != nil {
|
||||||
|
l.t.Reset()
|
||||||
|
}
|
||||||
|
c.link[i].p, c.link[i].n = 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make chain use Span (is going to be fun to implement!)
|
||||||
|
|
||||||
|
// Transform applies the transformers of c in sequence.
|
||||||
|
func (c *chain) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
// Set up src and dst in the chain.
|
||||||
|
srcL := &c.link[0]
|
||||||
|
dstL := &c.link[len(c.link)-1]
|
||||||
|
srcL.b, srcL.p, srcL.n = src, 0, len(src)
|
||||||
|
dstL.b, dstL.n = dst, 0
|
||||||
|
var lastFull, needProgress bool // for detecting progress
|
||||||
|
|
||||||
|
// i is the index of the next Transformer to apply, for i in [low, high].
|
||||||
|
// low is the lowest index for which c.link[low] may still produce bytes.
|
||||||
|
// high is the highest index for which c.link[high] has a Transformer.
|
||||||
|
// The error returned by Transform determines whether to increase or
|
||||||
|
// decrease i. We try to completely fill a buffer before converting it.
|
||||||
|
for low, i, high := c.errStart, c.errStart, len(c.link)-2; low <= i && i <= high; {
|
||||||
|
in, out := &c.link[i], &c.link[i+1]
|
||||||
|
nDst, nSrc, err0 := in.t.Transform(out.dst(), in.src(), atEOF && low == i)
|
||||||
|
out.n += nDst
|
||||||
|
in.p += nSrc
|
||||||
|
if i > 0 && in.p == in.n {
|
||||||
|
in.p, in.n = 0, 0
|
||||||
|
}
|
||||||
|
needProgress, lastFull = lastFull, false
|
||||||
|
switch err0 {
|
||||||
|
case ErrShortDst:
|
||||||
|
// Process the destination buffer next. Return if we are already
|
||||||
|
// at the high index.
|
||||||
|
if i == high {
|
||||||
|
return dstL.n, srcL.p, ErrShortDst
|
||||||
|
}
|
||||||
|
if out.n != 0 {
|
||||||
|
i++
|
||||||
|
// If the Transformer at the next index is not able to process any
|
||||||
|
// source bytes there is nothing that can be done to make progress
|
||||||
|
// and the bytes will remain unprocessed. lastFull is used to
|
||||||
|
// detect this and break out of the loop with a fatal error.
|
||||||
|
lastFull = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// The destination buffer was too small, but is completely empty.
|
||||||
|
// Return a fatal error as this transformation can never complete.
|
||||||
|
c.fatalError(i, errShortInternal)
|
||||||
|
case ErrShortSrc:
|
||||||
|
if i == 0 {
|
||||||
|
// Save ErrShortSrc in err. All other errors take precedence.
|
||||||
|
err = ErrShortSrc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Source bytes were depleted before filling up the destination buffer.
|
||||||
|
// Verify we made some progress, move the remaining bytes to the errStart
|
||||||
|
// and try to get more source bytes.
|
||||||
|
if needProgress && nSrc == 0 || in.n-in.p == len(in.b) {
|
||||||
|
// There were not enough source bytes to proceed while the source
|
||||||
|
// buffer cannot hold any more bytes. Return a fatal error as this
|
||||||
|
// transformation can never complete.
|
||||||
|
c.fatalError(i, errShortInternal)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// in.b is an internal buffer and we can make progress.
|
||||||
|
in.p, in.n = 0, copy(in.b, in.src())
|
||||||
|
fallthrough
|
||||||
|
case nil:
|
||||||
|
// if i == low, we have depleted the bytes at index i or any lower levels.
|
||||||
|
// In that case we increase low and i. In all other cases we decrease i to
|
||||||
|
// fetch more bytes before proceeding to the next index.
|
||||||
|
if i > low {
|
||||||
|
i--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
c.fatalError(i, err0)
|
||||||
|
}
|
||||||
|
// Exhausted level low or fatal error: increase low and continue
|
||||||
|
// to process the bytes accepted so far.
|
||||||
|
i++
|
||||||
|
low = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// If c.errStart > 0, this means we found a fatal error. We will clear
|
||||||
|
// all upstream buffers. At this point, no more progress can be made
|
||||||
|
// downstream, as Transform would have bailed while handling ErrShortDst.
|
||||||
|
if c.errStart > 0 {
|
||||||
|
for i := 1; i < c.errStart; i++ {
|
||||||
|
c.link[i].p, c.link[i].n = 0, 0
|
||||||
|
}
|
||||||
|
err, c.errStart, c.err = c.err, 0, nil
|
||||||
|
}
|
||||||
|
return dstL.n, srcL.p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use runes.Remove instead.
|
||||||
|
func RemoveFunc(f func(r rune) bool) Transformer {
|
||||||
|
return removeF(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
type removeF func(r rune) bool
|
||||||
|
|
||||||
|
func (removeF) Reset() {}
|
||||||
|
|
||||||
|
// Transform implements the Transformer interface.
|
||||||
|
func (t removeF) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for r, sz := rune(0), 0; len(src) > 0; src = src[sz:] {
|
||||||
|
|
||||||
|
if r = rune(src[0]); r < utf8.RuneSelf {
|
||||||
|
sz = 1
|
||||||
|
} else {
|
||||||
|
r, sz = utf8.DecodeRune(src)
|
||||||
|
|
||||||
|
if sz == 1 {
|
||||||
|
// Invalid rune.
|
||||||
|
if !atEOF && !utf8.FullRune(src) {
|
||||||
|
err = ErrShortSrc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// We replace illegal bytes with RuneError. Not doing so might
|
||||||
|
// otherwise turn a sequence of invalid UTF-8 into valid UTF-8.
|
||||||
|
// The resulting byte sequence may subsequently contain runes
|
||||||
|
// for which t(r) is true that were passed unnoticed.
|
||||||
|
if !t(r) {
|
||||||
|
if nDst+3 > len(dst) {
|
||||||
|
err = ErrShortDst
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nDst += copy(dst[nDst:], "\uFFFD")
|
||||||
|
}
|
||||||
|
nSrc++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !t(r) {
|
||||||
|
if nDst+sz > len(dst) {
|
||||||
|
err = ErrShortDst
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nDst += copy(dst[nDst:], src[:sz])
|
||||||
|
}
|
||||||
|
nSrc += sz
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// grow returns a new []byte that is longer than b, and copies the first n bytes
|
||||||
|
// of b to the start of the new slice.
|
||||||
|
func grow(b []byte, n int) []byte {
|
||||||
|
m := len(b)
|
||||||
|
if m <= 32 {
|
||||||
|
m = 64
|
||||||
|
} else if m <= 256 {
|
||||||
|
m *= 2
|
||||||
|
} else {
|
||||||
|
m += m >> 1
|
||||||
|
}
|
||||||
|
buf := make([]byte, m)
|
||||||
|
copy(buf, b[:n])
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialBufSize = 128
|
||||||
|
|
||||||
|
// String returns a string with the result of converting s[:n] using t, where
|
||||||
|
// n <= len(s). If err == nil, n will be len(s). It calls Reset on t.
|
||||||
|
func String(t Transformer, s string) (result string, n int, err error) {
|
||||||
|
t.Reset()
|
||||||
|
if s == "" {
|
||||||
|
// Fast path for the common case for empty input. Results in about a
|
||||||
|
// 86% reduction of running time for BenchmarkStringLowerEmpty.
|
||||||
|
if _, _, err := t.Transform(nil, nil, true); err == nil {
|
||||||
|
return "", 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate only once. Note that both dst and src escape when passed to
|
||||||
|
// Transform.
|
||||||
|
buf := [2 * initialBufSize]byte{}
|
||||||
|
dst := buf[:initialBufSize:initialBufSize]
|
||||||
|
src := buf[initialBufSize : 2*initialBufSize]
|
||||||
|
|
||||||
|
// The input string s is transformed in multiple chunks (starting with a
|
||||||
|
// chunk size of initialBufSize). nDst and nSrc are per-chunk (or
|
||||||
|
// per-Transform-call) indexes, pDst and pSrc are overall indexes.
|
||||||
|
nDst, nSrc := 0, 0
|
||||||
|
pDst, pSrc := 0, 0
|
||||||
|
|
||||||
|
// pPrefix is the length of a common prefix: the first pPrefix bytes of the
|
||||||
|
// result will equal the first pPrefix bytes of s. It is not guaranteed to
|
||||||
|
// be the largest such value, but if pPrefix, len(result) and len(s) are
|
||||||
|
// all equal after the final transform (i.e. calling Transform with atEOF
|
||||||
|
// being true returned nil error) then we don't need to allocate a new
|
||||||
|
// result string.
|
||||||
|
pPrefix := 0
|
||||||
|
for {
|
||||||
|
// Invariant: pDst == pPrefix && pSrc == pPrefix.
|
||||||
|
|
||||||
|
n := copy(src, s[pSrc:])
|
||||||
|
nDst, nSrc, err = t.Transform(dst, src[:n], pSrc+n == len(s))
|
||||||
|
pDst += nDst
|
||||||
|
pSrc += nSrc
|
||||||
|
|
||||||
|
// TODO: let transformers implement an optional Spanner interface, akin
|
||||||
|
// to norm's QuickSpan. This would even allow us to avoid any allocation.
|
||||||
|
if !bytes.Equal(dst[:nDst], src[:nSrc]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pPrefix = pSrc
|
||||||
|
if err == ErrShortDst {
|
||||||
|
// A buffer can only be short if a transformer modifies its input.
|
||||||
|
break
|
||||||
|
} else if err == ErrShortSrc {
|
||||||
|
if nSrc == 0 {
|
||||||
|
// No progress was made.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Equal so far and !atEOF, so continue checking.
|
||||||
|
} else if err != nil || pPrefix == len(s) {
|
||||||
|
return string(s[:pPrefix]), pPrefix, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Post-condition: pDst == pPrefix + nDst && pSrc == pPrefix + nSrc.
|
||||||
|
|
||||||
|
// We have transformed the first pSrc bytes of the input s to become pDst
|
||||||
|
// transformed bytes. Those transformed bytes are discontiguous: the first
|
||||||
|
// pPrefix of them equal s[:pPrefix] and the last nDst of them equal
|
||||||
|
// dst[:nDst]. We copy them around, into a new dst buffer if necessary, so
|
||||||
|
// that they become one contiguous slice: dst[:pDst].
|
||||||
|
if pPrefix != 0 {
|
||||||
|
newDst := dst
|
||||||
|
if pDst > len(newDst) {
|
||||||
|
newDst = make([]byte, len(s)+nDst-nSrc)
|
||||||
|
}
|
||||||
|
copy(newDst[pPrefix:pDst], dst[:nDst])
|
||||||
|
copy(newDst[:pPrefix], s[:pPrefix])
|
||||||
|
dst = newDst
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate Transform calls with atEOF being true at the end of
|
||||||
|
// the input. Also return if we have an unrecoverable error.
|
||||||
|
if (err == nil && pSrc == len(s)) ||
|
||||||
|
(err != nil && err != ErrShortDst && err != ErrShortSrc) {
|
||||||
|
return string(dst[:pDst]), pSrc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the remaining input, growing dst and src buffers as necessary.
|
||||||
|
for {
|
||||||
|
n := copy(src, s[pSrc:])
|
||||||
|
nDst, nSrc, err := t.Transform(dst[pDst:], src[:n], pSrc+n == len(s))
|
||||||
|
pDst += nDst
|
||||||
|
pSrc += nSrc
|
||||||
|
|
||||||
|
// If we got ErrShortDst or ErrShortSrc, do not grow as long as we can
|
||||||
|
// make progress. This may avoid excessive allocations.
|
||||||
|
if err == ErrShortDst {
|
||||||
|
if nDst == 0 {
|
||||||
|
dst = grow(dst, pDst)
|
||||||
|
}
|
||||||
|
} else if err == ErrShortSrc {
|
||||||
|
if nSrc == 0 {
|
||||||
|
src = grow(src, 0)
|
||||||
|
}
|
||||||
|
} else if err != nil || pSrc == len(s) {
|
||||||
|
return string(dst[:pDst]), pSrc, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a new byte slice with the result of converting b[:n] using t,
|
||||||
|
// where n <= len(b). If err == nil, n will be len(b). It calls Reset on t.
|
||||||
|
func Bytes(t Transformer, b []byte) (result []byte, n int, err error) {
|
||||||
|
return doAppend(t, 0, make([]byte, len(b)), b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends the result of converting src[:n] using t to dst, where
|
||||||
|
// n <= len(src), If err == nil, n will be len(src). It calls Reset on t.
|
||||||
|
func Append(t Transformer, dst, src []byte) (result []byte, n int, err error) {
|
||||||
|
if len(dst) == cap(dst) {
|
||||||
|
n := len(src) + len(dst) // It is okay for this to be 0.
|
||||||
|
b := make([]byte, n)
|
||||||
|
dst = b[:copy(b, dst)]
|
||||||
|
}
|
||||||
|
return doAppend(t, len(dst), dst[:cap(dst)], src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doAppend(t Transformer, pDst int, dst, src []byte) (result []byte, n int, err error) {
|
||||||
|
t.Reset()
|
||||||
|
pSrc := 0
|
||||||
|
for {
|
||||||
|
nDst, nSrc, err := t.Transform(dst[pDst:], src[pSrc:], true)
|
||||||
|
pDst += nDst
|
||||||
|
pSrc += nSrc
|
||||||
|
if err != ErrShortDst {
|
||||||
|
return dst[:pDst], pSrc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow the destination buffer, but do not grow as long as we can make
|
||||||
|
// progress. This may avoid excessive allocations.
|
||||||
|
if nDst == 0 {
|
||||||
|
dst = grow(dst, pDst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Code generated by "stringer -type=Kind"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package width
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[Neutral-0]
|
||||||
|
_ = x[EastAsianAmbiguous-1]
|
||||||
|
_ = x[EastAsianWide-2]
|
||||||
|
_ = x[EastAsianNarrow-3]
|
||||||
|
_ = x[EastAsianFullwidth-4]
|
||||||
|
_ = x[EastAsianHalfwidth-5]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Kind_name = "NeutralEastAsianAmbiguousEastAsianWideEastAsianNarrowEastAsianFullwidthEastAsianHalfwidth"
|
||||||
|
|
||||||
|
var _Kind_index = [...]uint8{0, 7, 25, 38, 53, 71, 89}
|
||||||
|
|
||||||
|
func (i Kind) String() string {
|
||||||
|
if i < 0 || i >= Kind(len(_Kind_index)-1) {
|
||||||
|
return "Kind(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _Kind_name[_Kind_index[i]:_Kind_index[i+1]]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,239 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package width
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type foldTransform struct {
|
||||||
|
transform.NopResetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foldTransform) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
for n < len(src) {
|
||||||
|
if src[n] < utf8.RuneSelf {
|
||||||
|
// ASCII fast path.
|
||||||
|
for n++; n < len(src) && src[n] < utf8.RuneSelf; n++ {
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, size := trie.lookup(src[n:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
err = transform.ErrShortSrc
|
||||||
|
} else {
|
||||||
|
n = len(src)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if elem(v)&tagNeedsFold != 0 {
|
||||||
|
err = transform.ErrEndOfSpan
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (foldTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for nSrc < len(src) {
|
||||||
|
if src[nSrc] < utf8.RuneSelf {
|
||||||
|
// ASCII fast path.
|
||||||
|
start, end := nSrc, len(src)
|
||||||
|
if d := len(dst) - nDst; d < end-start {
|
||||||
|
end = nSrc + d
|
||||||
|
}
|
||||||
|
for nSrc++; nSrc < end && src[nSrc] < utf8.RuneSelf; nSrc++ {
|
||||||
|
}
|
||||||
|
n := copy(dst[nDst:], src[start:nSrc])
|
||||||
|
if nDst += n; nDst == len(dst) {
|
||||||
|
nSrc = start + n
|
||||||
|
if nSrc == len(src) {
|
||||||
|
return nDst, nSrc, nil
|
||||||
|
}
|
||||||
|
if src[nSrc] < utf8.RuneSelf {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, size := trie.lookup(src[nSrc:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
return nDst, nSrc, transform.ErrShortSrc
|
||||||
|
}
|
||||||
|
size = 1 // gobble 1 byte
|
||||||
|
}
|
||||||
|
if elem(v)&tagNeedsFold == 0 {
|
||||||
|
if size != copy(dst[nDst:], src[nSrc:nSrc+size]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
nDst += size
|
||||||
|
} else {
|
||||||
|
data := inverseData[byte(v)]
|
||||||
|
if len(dst)-nDst < int(data[0]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
i := 1
|
||||||
|
for end := int(data[0]); i < end; i++ {
|
||||||
|
dst[nDst] = data[i]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
dst[nDst] = data[i] ^ src[nSrc+size-1]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
nSrc += size
|
||||||
|
}
|
||||||
|
return nDst, nSrc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type narrowTransform struct {
|
||||||
|
transform.NopResetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (narrowTransform) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
for n < len(src) {
|
||||||
|
if src[n] < utf8.RuneSelf {
|
||||||
|
// ASCII fast path.
|
||||||
|
for n++; n < len(src) && src[n] < utf8.RuneSelf; n++ {
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, size := trie.lookup(src[n:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
err = transform.ErrShortSrc
|
||||||
|
} else {
|
||||||
|
n = len(src)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if k := elem(v).kind(); byte(v) == 0 || k != EastAsianFullwidth && k != EastAsianWide && k != EastAsianAmbiguous {
|
||||||
|
} else {
|
||||||
|
err = transform.ErrEndOfSpan
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (narrowTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for nSrc < len(src) {
|
||||||
|
if src[nSrc] < utf8.RuneSelf {
|
||||||
|
// ASCII fast path.
|
||||||
|
start, end := nSrc, len(src)
|
||||||
|
if d := len(dst) - nDst; d < end-start {
|
||||||
|
end = nSrc + d
|
||||||
|
}
|
||||||
|
for nSrc++; nSrc < end && src[nSrc] < utf8.RuneSelf; nSrc++ {
|
||||||
|
}
|
||||||
|
n := copy(dst[nDst:], src[start:nSrc])
|
||||||
|
if nDst += n; nDst == len(dst) {
|
||||||
|
nSrc = start + n
|
||||||
|
if nSrc == len(src) {
|
||||||
|
return nDst, nSrc, nil
|
||||||
|
}
|
||||||
|
if src[nSrc] < utf8.RuneSelf {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v, size := trie.lookup(src[nSrc:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
return nDst, nSrc, transform.ErrShortSrc
|
||||||
|
}
|
||||||
|
size = 1 // gobble 1 byte
|
||||||
|
}
|
||||||
|
if k := elem(v).kind(); byte(v) == 0 || k != EastAsianFullwidth && k != EastAsianWide && k != EastAsianAmbiguous {
|
||||||
|
if size != copy(dst[nDst:], src[nSrc:nSrc+size]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
nDst += size
|
||||||
|
} else {
|
||||||
|
data := inverseData[byte(v)]
|
||||||
|
if len(dst)-nDst < int(data[0]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
i := 1
|
||||||
|
for end := int(data[0]); i < end; i++ {
|
||||||
|
dst[nDst] = data[i]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
dst[nDst] = data[i] ^ src[nSrc+size-1]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
nSrc += size
|
||||||
|
}
|
||||||
|
return nDst, nSrc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type wideTransform struct {
|
||||||
|
transform.NopResetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wideTransform) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
for n < len(src) {
|
||||||
|
// TODO: Consider ASCII fast path. Special-casing ASCII handling can
|
||||||
|
// reduce the ns/op of BenchmarkWideASCII by about 30%. This is probably
|
||||||
|
// not enough to warrant the extra code and complexity.
|
||||||
|
v, size := trie.lookup(src[n:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
err = transform.ErrShortSrc
|
||||||
|
} else {
|
||||||
|
n = len(src)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if k := elem(v).kind(); byte(v) == 0 || k != EastAsianHalfwidth && k != EastAsianNarrow {
|
||||||
|
} else {
|
||||||
|
err = transform.ErrEndOfSpan
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n += size
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wideTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for nSrc < len(src) {
|
||||||
|
// TODO: Consider ASCII fast path. Special-casing ASCII handling can
|
||||||
|
// reduce the ns/op of BenchmarkWideASCII by about 30%. This is probably
|
||||||
|
// not enough to warrant the extra code and complexity.
|
||||||
|
v, size := trie.lookup(src[nSrc:])
|
||||||
|
if size == 0 { // incomplete UTF-8 encoding
|
||||||
|
if !atEOF {
|
||||||
|
return nDst, nSrc, transform.ErrShortSrc
|
||||||
|
}
|
||||||
|
size = 1 // gobble 1 byte
|
||||||
|
}
|
||||||
|
if k := elem(v).kind(); byte(v) == 0 || k != EastAsianHalfwidth && k != EastAsianNarrow {
|
||||||
|
if size != copy(dst[nDst:], src[nSrc:nSrc+size]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
nDst += size
|
||||||
|
} else {
|
||||||
|
data := inverseData[byte(v)]
|
||||||
|
if len(dst)-nDst < int(data[0]) {
|
||||||
|
return nDst, nSrc, transform.ErrShortDst
|
||||||
|
}
|
||||||
|
i := 1
|
||||||
|
for end := int(data[0]); i < end; i++ {
|
||||||
|
dst[nDst] = data[i]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
dst[nDst] = data[i] ^ src[nSrc+size-1]
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
nSrc += size
|
||||||
|
}
|
||||||
|
return nDst, nSrc, nil
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||||
|
|
||||||
|
package width
|
||||||
|
|
||||||
|
// elem is an entry of the width trie. The high byte is used to encode the type
|
||||||
|
// of the rune. The low byte is used to store the index to a mapping entry in
|
||||||
|
// the inverseData array.
|
||||||
|
type elem uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
tagNeutral elem = iota << typeShift
|
||||||
|
tagAmbiguous
|
||||||
|
tagWide
|
||||||
|
tagNarrow
|
||||||
|
tagFullwidth
|
||||||
|
tagHalfwidth
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
numTypeBits = 3
|
||||||
|
typeShift = 16 - numTypeBits
|
||||||
|
|
||||||
|
// tagNeedsFold is true for all fullwidth and halfwidth runes except for
|
||||||
|
// the Won sign U+20A9.
|
||||||
|
tagNeedsFold = 0x1000
|
||||||
|
|
||||||
|
// The Korean Won sign is halfwidth, but SHOULD NOT be mapped to a wide
|
||||||
|
// variant.
|
||||||
|
wonSign rune = 0x20A9
|
||||||
|
)
|
|
@ -0,0 +1,206 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:generate stringer -type=Kind
|
||||||
|
//go:generate go run gen.go gen_common.go gen_trieval.go
|
||||||
|
|
||||||
|
// Package width provides functionality for handling different widths in text.
|
||||||
|
//
|
||||||
|
// Wide characters behave like ideographs; they tend to allow line breaks after
|
||||||
|
// each character and remain upright in vertical text layout. Narrow characters
|
||||||
|
// are kept together in words or runs that are rotated sideways in vertical text
|
||||||
|
// layout.
|
||||||
|
//
|
||||||
|
// For more information, see https://unicode.org/reports/tr11/.
|
||||||
|
package width // import "golang.org/x/text/width"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// 1) Reduce table size by compressing blocks.
|
||||||
|
// 2) API proposition for computing display length
|
||||||
|
// (approximation, fixed pitch only).
|
||||||
|
// 3) Implement display length.
|
||||||
|
|
||||||
|
// Kind indicates the type of width property as defined in https://unicode.org/reports/tr11/.
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Neutral characters do not occur in legacy East Asian character sets.
|
||||||
|
Neutral Kind = iota
|
||||||
|
|
||||||
|
// EastAsianAmbiguous characters that can be sometimes wide and sometimes
|
||||||
|
// narrow and require additional information not contained in the character
|
||||||
|
// code to further resolve their width.
|
||||||
|
EastAsianAmbiguous
|
||||||
|
|
||||||
|
// EastAsianWide characters are wide in its usual form. They occur only in
|
||||||
|
// the context of East Asian typography. These runes may have explicit
|
||||||
|
// halfwidth counterparts.
|
||||||
|
EastAsianWide
|
||||||
|
|
||||||
|
// EastAsianNarrow characters are narrow in its usual form. They often have
|
||||||
|
// fullwidth counterparts.
|
||||||
|
EastAsianNarrow
|
||||||
|
|
||||||
|
// Note: there exist Narrow runes that do not have fullwidth or wide
|
||||||
|
// counterparts, despite what the definition says (e.g. U+27E6).
|
||||||
|
|
||||||
|
// EastAsianFullwidth characters have a compatibility decompositions of type
|
||||||
|
// wide that map to a narrow counterpart.
|
||||||
|
EastAsianFullwidth
|
||||||
|
|
||||||
|
// EastAsianHalfwidth characters have a compatibility decomposition of type
|
||||||
|
// narrow that map to a wide or ambiguous counterpart, plus U+20A9 ₩ WON
|
||||||
|
// SIGN.
|
||||||
|
EastAsianHalfwidth
|
||||||
|
|
||||||
|
// Note: there exist runes that have a halfwidth counterparts but that are
|
||||||
|
// classified as Ambiguous, rather than wide (e.g. U+2190).
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: the generated tries need to return size 1 for invalid runes for the
|
||||||
|
// width to be computed correctly (each byte should render width 1)
|
||||||
|
|
||||||
|
var trie = newWidthTrie(0)
|
||||||
|
|
||||||
|
// Lookup reports the Properties of the first rune in b and the number of bytes
|
||||||
|
// of its UTF-8 encoding.
|
||||||
|
func Lookup(b []byte) (p Properties, size int) {
|
||||||
|
v, sz := trie.lookup(b)
|
||||||
|
return Properties{elem(v), b[sz-1]}, sz
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupString reports the Properties of the first rune in s and the number of
|
||||||
|
// bytes of its UTF-8 encoding.
|
||||||
|
func LookupString(s string) (p Properties, size int) {
|
||||||
|
v, sz := trie.lookupString(s)
|
||||||
|
return Properties{elem(v), s[sz-1]}, sz
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupRune reports the Properties of rune r.
|
||||||
|
func LookupRune(r rune) Properties {
|
||||||
|
var buf [4]byte
|
||||||
|
n := utf8.EncodeRune(buf[:], r)
|
||||||
|
v, _ := trie.lookup(buf[:n])
|
||||||
|
last := byte(r)
|
||||||
|
if r >= utf8.RuneSelf {
|
||||||
|
last = 0x80 + byte(r&0x3f)
|
||||||
|
}
|
||||||
|
return Properties{elem(v), last}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties provides access to width properties of a rune.
|
||||||
|
type Properties struct {
|
||||||
|
elem elem
|
||||||
|
last byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e elem) kind() Kind {
|
||||||
|
return Kind(e >> typeShift)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns the Kind of a rune as defined in Unicode TR #11.
|
||||||
|
// See https://unicode.org/reports/tr11/ for more details.
|
||||||
|
func (p Properties) Kind() Kind {
|
||||||
|
return p.elem.kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folded returns the folded variant of a rune or 0 if the rune is canonical.
|
||||||
|
func (p Properties) Folded() rune {
|
||||||
|
if p.elem&tagNeedsFold != 0 {
|
||||||
|
buf := inverseData[byte(p.elem)]
|
||||||
|
buf[buf[0]] ^= p.last
|
||||||
|
r, _ := utf8.DecodeRune(buf[1 : 1+buf[0]])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow returns the narrow variant of a rune or 0 if the rune is already
|
||||||
|
// narrow or doesn't have a narrow variant.
|
||||||
|
func (p Properties) Narrow() rune {
|
||||||
|
if k := p.elem.kind(); byte(p.elem) != 0 && (k == EastAsianFullwidth || k == EastAsianWide || k == EastAsianAmbiguous) {
|
||||||
|
buf := inverseData[byte(p.elem)]
|
||||||
|
buf[buf[0]] ^= p.last
|
||||||
|
r, _ := utf8.DecodeRune(buf[1 : 1+buf[0]])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wide returns the wide variant of a rune or 0 if the rune is already
|
||||||
|
// wide or doesn't have a wide variant.
|
||||||
|
func (p Properties) Wide() rune {
|
||||||
|
if k := p.elem.kind(); byte(p.elem) != 0 && (k == EastAsianHalfwidth || k == EastAsianNarrow) {
|
||||||
|
buf := inverseData[byte(p.elem)]
|
||||||
|
buf[buf[0]] ^= p.last
|
||||||
|
r, _ := utf8.DecodeRune(buf[1 : 1+buf[0]])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO for Properties:
|
||||||
|
// - Add Fullwidth/Halfwidth or Inverted methods for computing variants
|
||||||
|
// mapping.
|
||||||
|
// - Add width information (including information on non-spacing runes).
|
||||||
|
|
||||||
|
// Transformer implements the transform.Transformer interface.
|
||||||
|
type Transformer struct {
|
||||||
|
t transform.SpanningTransformer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset implements the transform.Transformer interface.
|
||||||
|
func (t Transformer) Reset() { t.t.Reset() }
|
||||||
|
|
||||||
|
// Transform implements the transform.Transformer interface.
|
||||||
|
func (t Transformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
return t.t.Transform(dst, src, atEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Span implements the transform.SpanningTransformer interface.
|
||||||
|
func (t Transformer) Span(src []byte, atEOF bool) (n int, err error) {
|
||||||
|
return t.t.Span(src, atEOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a new byte slice with the result of applying t to b.
|
||||||
|
func (t Transformer) Bytes(b []byte) []byte {
|
||||||
|
b, _, _ = transform.Bytes(t, b)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string with the result of applying t to s.
|
||||||
|
func (t Transformer) String(s string) string {
|
||||||
|
s, _, _ = transform.String(t, s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Fold is a transform that maps all runes to their canonical width.
|
||||||
|
//
|
||||||
|
// Note that the NFKC and NFKD transforms in golang.org/x/text/unicode/norm
|
||||||
|
// provide a more generic folding mechanism.
|
||||||
|
Fold Transformer = Transformer{foldTransform{}}
|
||||||
|
|
||||||
|
// Widen is a transform that maps runes to their wide variant, if
|
||||||
|
// available.
|
||||||
|
Widen Transformer = Transformer{wideTransform{}}
|
||||||
|
|
||||||
|
// Narrow is a transform that maps runes to their narrow variant, if
|
||||||
|
// available.
|
||||||
|
Narrow Transformer = Transformer{narrowTransform{}}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Consider the following options:
|
||||||
|
// - Treat Ambiguous runes that have a halfwidth counterpart as wide, or some
|
||||||
|
// generalized variant of this.
|
||||||
|
// - Consider a wide Won character to be the default width (or some generalized
|
||||||
|
// variant of this).
|
||||||
|
// - Filter the set of characters that gets converted (the preferred approach is
|
||||||
|
// to allow applying filters to transforms).
|
|
@ -3,6 +3,10 @@ code.gitea.io/gitea-vet
|
||||||
code.gitea.io/gitea-vet/checks
|
code.gitea.io/gitea-vet/checks
|
||||||
# code.gitea.io/sdk/gitea v0.13.0
|
# code.gitea.io/sdk/gitea v0.13.0
|
||||||
code.gitea.io/sdk/gitea
|
code.gitea.io/sdk/gitea
|
||||||
|
# github.com/AlecAivazis/survey/v2 v2.1.1
|
||||||
|
github.com/AlecAivazis/survey/v2
|
||||||
|
github.com/AlecAivazis/survey/v2/core
|
||||||
|
github.com/AlecAivazis/survey/v2/terminal
|
||||||
# github.com/alecthomas/chroma v0.7.3
|
# github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/alecthomas/chroma
|
github.com/alecthomas/chroma
|
||||||
github.com/alecthomas/chroma/formatters
|
github.com/alecthomas/chroma/formatters
|
||||||
|
@ -118,14 +122,20 @@ github.com/hashicorp/go-version
|
||||||
github.com/imdario/mergo
|
github.com/imdario/mergo
|
||||||
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
# github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||||
github.com/jbenet/go-context/io
|
github.com/jbenet/go-context/io
|
||||||
|
# github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
|
github.com/kballard/go-shellquote
|
||||||
# github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd
|
# github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd
|
||||||
github.com/kevinburke/ssh_config
|
github.com/kevinburke/ssh_config
|
||||||
# github.com/lucasb-eyer/go-colorful v1.0.3
|
# github.com/lucasb-eyer/go-colorful v1.0.3
|
||||||
github.com/lucasb-eyer/go-colorful
|
github.com/lucasb-eyer/go-colorful
|
||||||
|
# github.com/mattn/go-colorable v0.1.6
|
||||||
|
github.com/mattn/go-colorable
|
||||||
# github.com/mattn/go-isatty v0.0.12
|
# github.com/mattn/go-isatty v0.0.12
|
||||||
github.com/mattn/go-isatty
|
github.com/mattn/go-isatty
|
||||||
# github.com/mattn/go-runewidth v0.0.9
|
# github.com/mattn/go-runewidth v0.0.9
|
||||||
github.com/mattn/go-runewidth
|
github.com/mattn/go-runewidth
|
||||||
|
# github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||||
|
github.com/mgutz/ansi
|
||||||
# github.com/microcosm-cc/bluemonday v1.0.2
|
# github.com/microcosm-cc/bluemonday v1.0.2
|
||||||
github.com/microcosm-cc/bluemonday
|
github.com/microcosm-cc/bluemonday
|
||||||
# github.com/mitchellh/go-homedir v1.1.0
|
# github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
@ -196,6 +206,9 @@ golang.org/x/sys/cpu
|
||||||
golang.org/x/sys/internal/unsafeheader
|
golang.org/x/sys/internal/unsafeheader
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
golang.org/x/sys/windows
|
golang.org/x/sys/windows
|
||||||
|
# golang.org/x/text v0.3.2
|
||||||
|
golang.org/x/text/transform
|
||||||
|
golang.org/x/text/width
|
||||||
# golang.org/x/tools v0.0.0-20200721032237-77f530d86f9a
|
# golang.org/x/tools v0.0.0-20200721032237-77f530d86f9a
|
||||||
golang.org/x/tools/go/analysis
|
golang.org/x/tools/go/analysis
|
||||||
golang.org/x/tools/go/analysis/internal/analysisflags
|
golang.org/x/tools/go/analysis/internal/analysisflags
|
||||||
|
|
Loading…
Reference in New Issue