forked from tildetown/town
467 lines
13 KiB
Go
467 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"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
|
|
*/
|
|
|
|
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()
|
|
input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
|
|
input.SetTitle("press ctrl+d to send")
|
|
|
|
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")
|
|
|
|
currentScreen := "start"
|
|
|
|
sceneTransition := func(text string) {
|
|
fmt.Fprintln(msgScroll, heredoc.Doc(`
|
|
|
|
[purple]----------[-:-:-]
|
|
|
|
`))
|
|
fmt.Fprintln(msgScroll, text)
|
|
}
|
|
|
|
scenes := map[string]func(){
|
|
"start": func() {
|
|
fmt.Fprintln(msgScroll, 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[-:-:-]
|
|
`))
|
|
},
|
|
"nodded": func() {
|
|
sceneTransition(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.
|
|
`))
|
|
},
|
|
"leaned": func() {
|
|
sceneTransition(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.
|
|
`))
|
|
},
|
|
"spun": func() {
|
|
sceneTransition(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.
|
|
`))
|
|
},
|
|
"done": func() {
|
|
sceneTransition(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~
|
|
`))
|
|
},
|
|
}
|
|
|
|
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":
|
|
os.Exit(0)
|
|
// TODO refactor into a state machine
|
|
case "nod":
|
|
if currentScreen != "start" {
|
|
return
|
|
}
|
|
currentScreen = "nodded"
|
|
scenes[currentScreen]()
|
|
case "lean":
|
|
if currentScreen != "nodded" {
|
|
return
|
|
}
|
|
currentScreen = "leaned"
|
|
scenes[currentScreen]()
|
|
case "spin":
|
|
if currentScreen != "leaned" {
|
|
return
|
|
}
|
|
currentScreen = "spun"
|
|
scenes[currentScreen]()
|
|
case "open":
|
|
if currentScreen != "spun" {
|
|
return
|
|
}
|
|
currentScreen = "done"
|
|
scenes[currentScreen]()
|
|
}
|
|
return
|
|
}
|
|
fmt.Fprintln(msgScroll, player.Say(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) {
|
|
scenes[currentScreen]()
|
|
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.
|
|
|
|
one idea but i'm scratchin:
|
|
|
|
opening:
|
|
|
|
It's dark. Before you a button appears. It's a pink circle with a ~ at the center. It looks like a button. You could press it.
|
|
|
|
|
|
/press button
|
|
|
|
A thin seam splits the darkness from the top and buttom of the button. The darkness splits apart and you're standing in a forest clearing. Birds are singing in strange octaves.
|
|
|
|
A cube is here, floating.
|
|
|
|
cube: hello! how are you? you can respond to me by typing anything and pressing "ctrl+d"
|
|
|
|
<input>
|
|
|
|
cube: thank you for telling me. i'm just a program so i can't really understand. i hope you forgive me.
|
|
|
|
cube: i'm here to help you apply for a homestead over there in tilde town.
|
|
|
|
the cube inclines in the direction of what looks like a far off town.
|
|
|
|
cube: first, what is an email address we can use to contact you?
|
|
|
|
<input>
|
|
|
|
cube: cool, thank you. that's <email> right? if so, you can /nod to confirm. otherwise just tell me again what your email is.
|
|
|
|
<nod>
|
|
|
|
cube: excellent. next, you'll talk to
|
|
|
|
*/
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
From the start point the player advances the "form" by performing a tactile action. for now it's a button press but it could be something more interesting later. Keeping this as a one room, limited-verb thing is good for accessibility.
|
|
|
|
so the script is a list of strings that advance by tactility...is it dialogue? or scene description? i think i should fake dialogue /as/ scene description.
|
|
|
|
You open your eyes. You're standing in a small room. It's dim, lit softly by a mix of candles and electro-luminescent wire. Computers.
|
|
|
|
*/
|
|
|
|
/*
|
|
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 {
|
|
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?")
|
|
|
|
}
|
|
*/
|