forked from tildetown/town
		
	
		
			
				
	
	
		
			352 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/MakeNowJust/heredoc/v2"
 | 
						|
	"github.com/gdamore/tcell/v2"
 | 
						|
	"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
 | 
						|
*/
 | 
						|
 | 
						|
const maxInputLength int = 4000
 | 
						|
 | 
						|
type TownApplication struct {
 | 
						|
	Email    string
 | 
						|
	HowFound string
 | 
						|
	Why      string
 | 
						|
	Where    string
 | 
						|
}
 | 
						|
 | 
						|
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()
 | 
						|
	input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
 | 
						|
	input.SetTitle("press ctrl+d to send")
 | 
						|
	input.SetMaxLength(2000)
 | 
						|
 | 
						|
	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)
 | 
						|
	msgScroll.SetBackgroundColor(tcell.ColorBlack)
 | 
						|
	msgScroll.SetTextColor(tcell.ColorWhite)
 | 
						|
 | 
						|
	sidebar := tview.NewTextView()
 | 
						|
	sidebar.SetBorder(true)
 | 
						|
	sidebar.SetDynamicColors(true)
 | 
						|
	sidebar.SetBackgroundColor(tcell.ColorBlack)
 | 
						|
	sidebar.SetTextColor(tcell.ColorGray)
 | 
						|
	sidebar.SetText(heredoc.Doc(`
 | 
						|
		[-:-:b]hey here are some hints[-:-:-]
 | 
						|
 | 
						|
		quit by pressing [-:-:b]ctrl-c[-:-:-]
 | 
						|
		(your responses won't be saved)
 | 
						|
 | 
						|
		type stuff. send it with [-:-:b]ctrl+d[-:-:-]
 | 
						|
 | 
						|
		try a [-:-:b]verb[-:-:-] using [-:-:b]/[-:-:-] like:
 | 
						|
 | 
						|
		[-:-:b]/nod[-:-:-]
 | 
						|
	`))
 | 
						|
 | 
						|
	innerFlex.SetDirection(tview.FlexColumn)
 | 
						|
	innerFlex.AddItem(msgScroll, 0, 3, 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)
 | 
						|
 | 
						|
	player := newCharacter("you", "TODO")
 | 
						|
 | 
						|
	type scene struct {
 | 
						|
		Name        string
 | 
						|
		Description string
 | 
						|
		InBuff      io.ReadWriter
 | 
						|
		InLength    int
 | 
						|
		Host        *character
 | 
						|
	}
 | 
						|
 | 
						|
	scenes := []*scene{
 | 
						|
		{
 | 
						|
			Name: "start",
 | 
						|
			Description: heredoc.Doc(`
 | 
						|
				You open your eyes.
 | 
						|
 | 
						|
				You're in some kind of workshop. 
 | 
						|
				Wires and computers in various state of disrepair are strewn across
 | 
						|
				tables and shelves. It smells faintly of burnt cedar.
 | 
						|
 | 
						|
				The wires and components before you slowly drag 
 | 
						|
				themselves into the shape of a small humanoid.
 | 
						|
 | 
						|
				[-:-:b]wire guy says:[-:-:-]
 | 
						|
					hello, welcome to the application for membership in tilde town.
 | 
						|
					first, please let me know what a good [-:-:b]email address[-:-:-] is for you?
 | 
						|
					just say it out loud. as many times as you need. to get it right.
 | 
						|
					when you're ready to move on, [-:-:b]/nod[-:-:-]
 | 
						|
			`),
 | 
						|
			Host:   newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
 | 
						|
			InBuff: bytes.NewBuffer([]byte{}),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "nodded",
 | 
						|
			Description: heredoc.Doc(`
 | 
						|
				The workshop fades away. You hear the sound of a dial up modem 
 | 
						|
				in the distance.
 | 
						|
 | 
						|
				Trees spring up out of the ground around you: birches, oaks, maples,
 | 
						|
				firs, yews, pines, cypresses complete with tiny swamps around their trunks,
 | 
						|
				junipers, redwoods, cedars, towering palms waving gently in a breeze, eucalyptus, 
 | 
						|
				banyan. the smell is riotous like a canvas with all the colors splashed on. birds 
 | 
						|
				start to sing.
 | 
						|
 | 
						|
				a shrike alights on a branch in front of you.
 | 
						|
 | 
						|
				[-:-:b]the shrike says:[-:-:-]
 | 
						|
					phweeturpff. how did you find out about the town? did anyone refer you?
 | 
						|
 | 
						|
					just say your answer out loud. when you've said what you want, [-:-:b]/lean[-:-:-]
 | 
						|
					against a tree.
 | 
						|
			`),
 | 
						|
			InBuff: bytes.NewBuffer([]byte{}),
 | 
						|
			Host:   newCharacter("the shrike", "a little grey bird. it has a pretty song."),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "leaned",
 | 
						|
			Description: heredoc.Doc(`
 | 
						|
				You sink backwards into the forest. You find yourself floating in darkness.
 | 
						|
 | 
						|
				At the far reaches of your vision you can make out a faint neon grid. Around you
 | 
						|
				float pieces of consumer electronic appliances from 1980s Earth. A VCR approaches
 | 
						|
				you and speaks, flapping its tape slot cover with each word.
 | 
						|
 | 
						|
				[-:-:b]the vcr says:[-:-:-]
 | 
						|
					welcome! thank you for coming this far. just two questions left. what about
 | 
						|
					tilde town interests you? what kind of stuff might you want to get up to here?
 | 
						|
 | 
						|
					as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-]
 | 
						|
					around in this void.
 | 
						|
			`),
 | 
						|
			InBuff: bytes.NewBuffer([]byte{}),
 | 
						|
			Host:   newCharacter("the vcr", "a black and grey VCR from 1991"),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "spun",
 | 
						|
			Description: heredoc.Doc(`
 | 
						|
				You realize your eyes have been shut. You open them and, in an instant,
 | 
						|
				the neon grid and polygons are gone. You're in a convenience store. Outside
 | 
						|
				it's dark besides a single pool of light coming from a street lamp. it's illuminating
 | 
						|
				some litter and a rusty, blue 1994 pontiac grand am.
 | 
						|
 | 
						|
				The shelves around you are stocked with products you've never heard of before like
 | 
						|
				Visible Pants, Petty Burgers, Gentle Rice, Boo Sponge, Power Banjo, Superware, Kneephones,
 | 
						|
				and Diet Coagulator. A mop is mopping the floor and turns to you.
 | 
						|
 | 
						|
				[-:-:b]the mop says:[-:-:-]
 | 
						|
					swishy slop. last question. where online can we get to know you? do you have a personal 
 | 
						|
					website or social media presence? we'll take whatever.
 | 
						|
 | 
						|
					say some links and words out loud, you know the drill.
 | 
						|
 | 
						|
					when you're happy you can submit this whole experience by leaving the
 | 
						|
					store. just [-:-:b]/open[-:-:-] the door.
 | 
						|
			`),
 | 
						|
			InBuff: bytes.NewBuffer([]byte{}),
 | 
						|
			Host:   newCharacter("the mop", "a greying mop with a wooden handle."),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			Name: "spun",
 | 
						|
			Description: heredoc.Doc(`
 | 
						|
				thank you for applying to tilde.town! 
 | 
						|
 | 
						|
				please be on the look out for an email from [-:-:b]root@tilde.town[-:-:-]
 | 
						|
 | 
						|
				you can [-:-:b]/quit[-:-:-] now
 | 
						|
 | 
						|
				ok bye have a good one~
 | 
						|
			`),
 | 
						|
			InBuff: bytes.NewBuffer([]byte{}),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	sceneIx := 0
 | 
						|
	currentScene := scenes[sceneIx]
 | 
						|
 | 
						|
	advanceScene := func(fromScene, sorryMsg string) {
 | 
						|
		if currentScene.Name != fromScene {
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if currentScene.InLength == 0 {
 | 
						|
			fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg))
 | 
						|
			return
 | 
						|
		}
 | 
						|
		sceneIx++
 | 
						|
		currentScene = scenes[sceneIx]
 | 
						|
		fmt.Fprintln(msgScroll, heredoc.Doc(`
 | 
						|
 | 
						|
			[purple]----------[-:-:-]
 | 
						|
			
 | 
						|
		`))
 | 
						|
		fmt.Fprintln(msgScroll, currentScene.Description)
 | 
						|
	}
 | 
						|
 | 
						|
	handleInput := func(msg string) {
 | 
						|
		trimmed := strings.TrimSpace(msg)
 | 
						|
		if trimmed == "" {
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if strings.HasPrefix(trimmed, "/") {
 | 
						|
			split := strings.Split(trimmed, " ")
 | 
						|
			if len(split) > 0 {
 | 
						|
				trimmed = split[0]
 | 
						|
			}
 | 
						|
			switch strings.TrimPrefix(trimmed, "/") {
 | 
						|
			case "quit":
 | 
						|
				app.Stop()
 | 
						|
			case "look":
 | 
						|
				fmt.Fprintln(msgScroll, "")
 | 
						|
				fmt.Fprintln(msgScroll, currentScene.Description)
 | 
						|
			// TODO refactor into a state machine
 | 
						|
			case "nod":
 | 
						|
				advanceScene("start",
 | 
						|
					"i'm sorry, before going further could you share an email with me?")
 | 
						|
			case "lean":
 | 
						|
				advanceScene("nodded", "phweeturpff")
 | 
						|
			case "spin":
 | 
						|
				advanceScene("leaned", "hmm did you say something?")
 | 
						|
			case "open":
 | 
						|
				advanceScene("spun", "just the one last thing please")
 | 
						|
			}
 | 
						|
			return
 | 
						|
		}
 | 
						|
		fmt.Fprintln(msgScroll, player.Say(msg))
 | 
						|
		fmt.Fprintln(currentScene.InBuff, msg)
 | 
						|
		currentScene.InLength += len(msg)
 | 
						|
	}
 | 
						|
 | 
						|
	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
 | 
						|
		switch event.Key() {
 | 
						|
		case tcell.KeyCtrlD:
 | 
						|
			handleInput(input.GetText())
 | 
						|
			input.SetText("", false)
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		return event
 | 
						|
	})
 | 
						|
 | 
						|
	app.SetAfterDrawFunc(func(_ tcell.Screen) {
 | 
						|
		fmt.Fprintln(msgScroll, currentScene.Description)
 | 
						|
		app.SetAfterDrawFunc(nil)
 | 
						|
	})
 | 
						|
 | 
						|
	return app.Run()
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 | 
						|
I need a script for how this interaction should go. It should feel more organic and fluid than just filling out a form. It shouldn't, however, take more than 10 minutes to get through stuff. The end result is to collect:
 | 
						|
 | 
						|
- an email address
 | 
						|
- how a user found out about the town
 | 
						|
- if they have a referral from an existing member
 | 
						|
- what interests them about the town
 | 
						|
- any links to personal websites or social media
 | 
						|
 | 
						|
A given is that the applicant is interacting with a "host" that guides them through the process. Should there be more than one host?
 | 
						|
 | 
						|
How many rooms should there be? 1? more?
 | 
						|
 | 
						|
in terms of data collection, I intend to just save the transcript of their interaction instead of more structured data. The only real structured data are email and referral.
 | 
						|
 | 
						|
another idea/thought/dream:
 | 
						|
 | 
						|
the whole point of this is to spatialize a form. it's a form! a set of questions and corresponding textarea elements. but in order to trigger self reflection -- as well as a feeling of being seen by another (in a good way) -- i want to turn the form into a shape. i'm thinking about this like the room in Eclipse Penumbra:
 | 
						|
 | 
						|
(For the Hollow Head was drug paraphenalia you could walk into. The building itself was the syringe, or the hookah, or the sniff-tube)
 | 
						|
 | 
						|
so, spatialized, every room is a question. the rooms take shape as a linear script the user moves through by:
 | 
						|
 | 
						|
1. answering a question
 | 
						|
2. executing a verb
 | 
						|
 | 
						|
and finally at the end, a verb to confirm submission.
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
type character struct {
 | 
						|
	Name        string
 | 
						|
	Description string
 | 
						|
}
 | 
						|
 | 
						|
func newCharacter(name, description string) *character {
 | 
						|
	return &character{
 | 
						|
		Name:        name,
 | 
						|
		Description: description,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (c *character) Say(msg string) string {
 | 
						|
	verb := "says"
 | 
						|
	if c.Name == "you" {
 | 
						|
		verb = "say"
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("[-:-:b]%s[-:-:-] %s: '%s'",
 | 
						|
		c.Name,
 | 
						|
		verb,
 | 
						|
		strings.TrimSpace(msg))
 | 
						|
}
 | 
						|
 | 
						|
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)
 | 
						|
	}
 | 
						|
}
 |