package main import ( "bufio" "fmt" "io" "io/ioutil" "os" "os/exec" "strings" "time" "unicode/utf8" "github.com/rivo/tview" ) /* Assumes: Match User join ForceCommand /town/src/town/cmd/signup/signup PubkeyAuthentication no KbdInteractiveAuthentication no PasswordAuthentication yes PermitEmptyPasswords yes DisableForwarding yes in sshd_config, and: auth [success=done default=ignore] pam_succeed_if.so user ingroup join in /etc/pam.d/sshd */ type TownApplication struct { Email string // TODO } type streams struct { In io.Reader Out io.Writer Err io.Writer } func cli(s *streams) error { pages := tview.NewPages() mainFlex := tview.NewFlex() innerFlex := tview.NewFlex() input := tview.NewTextArea() title := tview.NewTextView() title.SetDynamicColors(true) title.SetTextAlign(tview.AlignCenter) title.SetText("[purple]the tilde town sign up portal[-]") msgScroll := tview.NewTextView() msgScroll.SetDynamicColors(true) sidebar := tview.NewTextView() sidebar.SetBorder(true) sidebar.SetDynamicColors(true) sidebar.SetText("[-:-:b]pbbt[-:-:-]") innerFlex.SetDirection(tview.FlexColumn) innerFlex.AddItem(msgScroll, 0, 2, false) innerFlex.AddItem(sidebar, 0, 1, false) mainFlex.SetDirection(tview.FlexRow) mainFlex.AddItem(title, 1, -1, false) mainFlex.AddItem(innerFlex, 0, 1, false) mainFlex.AddItem(input, 5, -1, true) pages.AddPage("main", mainFlex, true, true) app := tview.NewApplication() app.SetRoot(pages, true) return app.Run() } func main() { s := &streams{ In: os.Stdin, Out: os.Stdout, Err: os.Stderr, } err := cli(s) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } type Prompter struct { in io.Reader out io.Writer width int } func (p *Prompter) Say(m string) { for _, line := range wrap(m, p.width) { fmt.Fprintln(p.out, line) } } func (p *Prompter) CharSay(c, m string) { sayPrefix := fmt.Sprintf("%s says: ", c) prefixWidth := utf8.RuneCountInString(sayPrefix) width := p.width - prefixWidth indent := "" for x := 0; x < utf8.RuneCountInString(sayPrefix); x++ { indent += " " } for i, line := range wrap(m, width) { if i == 0 { fmt.Fprintln(p.out, sayPrefix+line) } else { fmt.Fprintln(p.out, indent+line) } } } type InputAnswer struct { Value string } func runeLen(s string) int { return utf8.RuneCountInString(s) } func wrap(s string, width int) []string { fielded := strings.Fields(s) out := []string{} line := "" for i, field := range fielded { if runeLen(field)+runeLen(line)+1 < width { line += field + " " } else { out = append(out, line) line = field + " " continue } if i == len(fielded)-1 { out = append(out, line) } } return out } func (p *Prompter) Confirm(m string) (bool, error) { result := InputAnswer{} for { err := p.Ask(m, &result) if err != nil { return false, err } val := strings.TrimSpace(result.Value) switch val { case "yes", "y": return true, nil case "no", "n": return false, nil default: p.Say("sorry, please say y or n") p.Pause() } } } func (p *Prompter) Ask(m string, result *InputAnswer) error { fmt.Fprintf(p.out, "%s ", m) var val string var err error reader := bufio.NewReader(p.in) for val == "" { val, err = reader.ReadString('\n') if err != nil { return err } if val == "" { p.Say("hmm, what was that?") p.Pause() } } result.Value = val return nil } func (p *Prompter) AskLong(m string, result *InputAnswer) error { fmt.Fprintf(p.out, "%s\n", m) var val []byte var err error reader := bufio.NewReader(p.in) for len(val) == 0 { val, err = ioutil.ReadAll(reader) if err != nil { return err } if len(val) == 0 { fmt.Fprintf(p.out, "%s\n", m) } } result.Value = string(val) return nil } func (p *Prompter) Pause() { fmt.Fprintln(p.out) time.Sleep(1 * time.Second) } func NewPrompter(width int, s *streams) Prompter { return Prompter{ in: s.In, out: s.Out, width: width, } } type answers struct { username string email string applied time.Time howDay string howHeard string reasons string plans string socials string sshKey string } func _main(args []string, s *streams) error { a := answers{} p := NewPrompter(80, s) // disable input buffering exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() // LOL i don't think this will work //reader := bufio.NewReader(p.in) //s, _ := reader.ReadString(4) fmt.Printf("DBG %#v\n", s) //var b []byte = make([]byte, 100) //for { // os.Stdin.Read(b) // fmt.Println("I got the byte", b, "("+string(b)+")") //} var ia InputAnswer if err := p.AskLong("lol give me stuff hit ctrl+d", &ia); err != nil { return err } fmt.Printf("DBG %#v\n", ia.Value) p.Say("you are standing in a field.") p.Pause() p.Say("there are flowers around you. you are standing in a slight depression and before you is grass touching a purple sky.") p.Pause() p.Say("you are not sure how long it's been when a cube about a meter high appears at the top of the hill before you.") p.Say("the cube's surface is murky but iridescently reflective like an oil slick.") p.Pause() p.CharSay("cube", "hello. how is your day going?") p.Say("(you can type a response and hit enter to talk to the cube)") p.Pause() howDay := InputAnswer{} err := p.Ask("you say:", &howDay) if err != nil { return err } a.howDay = howDay.Value p.Say("the cube inclines towards you gently as if nodding.") p.CharSay("cube", "i see.") p.Pause() p.CharSay("cube", "i am guessing that if you are here, you want to be there.") p.Pause() p.Say("you blink and are somewhere else.") p.Pause() p.Say("the field of flowers is behind you and now you are up on the hill. the cube is next to you.") p.Pause() p.Say("you can see clear across a wide open plain. structures large and small dot the landscape. you catch a whiff of a savory smell and can just barely hear voices on the wind.") p.Pause() p.CharSay("cube", "the tilde town lies before us. if you want to continue, i'll ask you some questions about acquiring a home down in the town. you'll be free to edit your responses before i carry them off.") p.Pause() p.CharSay("cube", "first, i'm curious how you found out about the town?") p.Pause() answer := InputAnswer{} if err := p.Ask("you say:", &answer); err != nil { return err } a.howHeard = answer.Value p.CharSay("cube", "interesting, thanks.") p.CharSay("cube", "what sort of stuff do you want to get up to on the town?") if err := p.Ask("you say:", &answer); err != nil { return err } a.plans = answer.Value p.CharSay("cube", "thanks.") p.CharSay("cube", "what do you like about the town?") if err := p.Ask("you say:", &answer); err != nil { return err } a.reasons = answer.Value p.CharSay("cube", "i appreciate it.") p.CharSay("cube", "can you paste some links to other places you are active online? maybe a homepage or social media profile? if you aren't comfortable sharing or there are none, just say so.") if err := p.Ask("you say:", &answer); err != nil { return err } a.socials = answer.Value p.CharSay("cube", "ok, last thing. in order to break ground in the town, you'll need an SSH key. If you don't know what that is, you can check out this link: https://tilde.town/ssh.html .") p.Pause() if err := p.AskLong("paste SSH public key; press control+d to submit:", &answer); err != nil { return err } a.sshKey = answer.Value p.CharSay("cube", "i know that was a lot so i appreciate it. i've got everything written down here. before i carry it off, do you want to review and edit what you wrote?") confirmed, err := p.Confirm("type y or n: ") if err != nil { return err } if confirmed { err := edit(s, &a) if err != nil { return err } } fmt.Printf("DBG %#v\n", confirmed) fmt.Printf("DBG %#v\n", a) // TODO allow for editing // TODO write answers to disk // TODO add a log // TODO pretty colors // TODO ascii art // TODO IP throttling? return nil } func edit(s *streams, a *answers) error { // TODO make more real /* username string email string applied time.Time howDay string howHeard string reasons string plans string socials string sshKey string */ // TODO add note about tabbing around app := tview.NewApplication() form := tview.NewForm(). AddInputField("how did you hear about the town?", a.howHeard, 0, nil, nil). AddButton("cool i'm good", nil). AddButton("cancel and discard all this please", func() { app.Stop() }) form.SetBorder(true).SetTitle("edit your stuff").SetTitleAlign(tview.AlignCenter) return app.SetRoot(form, true).EnableMouse(true).Run() }