package main import ( "encoding/json" "errors" "fmt" "math/rand" "os" "os/user" "path" "strings" "time" tuser "git.tilde.town/tildetown/town/user" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) const ( signupDir = "/town/signups" acceptedDir = "/town/signups/accepted" rejectedDir = "/town/signups/rejected" ) func getTitle() string { titles := []string{ "yo bum rush the show", "can i kick it?", "super nintendo sega genesis", "birthdays was the worst days", "where were you when we were getting high?", "it's real time, real time, time to get real", } return titles[rand.Intn(len(titles))] } type townSignup struct { When time.Time DecisionTime time.Time Decision string Filename string Answers map[string]string } func (s townSignup) Accept() error { return s.review("accept") } func (s townSignup) Reject() error { return s.review("reject") } func (s townSignup) review(decision string) error { s.DecisionTime = time.Now() s.Decision = "decision" oldpath := path.Join(signupDir, s.Filename) newpath := path.Join(acceptedDir, s.Filename) if decision == "reject" { newpath = path.Join(rejectedDir, s.Filename) } return os.Rename(oldpath, newpath) } func (s townSignup) Render() string { out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.When.Format("2006-01-02 15:04")) for k, v := range s.Answers { out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k) out += strings.TrimSpace(v) out += "\n\n" } return out } func getSignups() ([]townSignup, error) { entries, err := os.ReadDir(signupDir) if err != nil { return nil, fmt.Errorf("failed to read '%s': %w", signupDir, err) } out := []townSignup{} for _, entry := range entries { if !strings.HasSuffix(entry.Name(), "json") { continue } abs := path.Join(signupDir, entry.Name()) data, err := os.ReadFile(abs) if err != nil { return nil, fmt.Errorf("could not read signup file '%s': %w", abs, err) } fmt.Println(string(data)) var signup townSignup err = json.Unmarshal(data, &signup) if err != nil { return nil, fmt.Errorf("could not unmarshal signup file '%s': %w", abs, err) } signup.Filename = entry.Name() out = append(out, signup) } return out, nil } func _main() error { u, err := user.Current() if err != nil { return fmt.Errorf("that's my purse. I don't know you! %w", err) } isAdmin, err := tuser.IsAdmin(u) if err != nil { return fmt.Errorf("that's my purse. I don't know you! %w", err) } if !isAdmin { return errors.New("this command can only be run by a town admin") } rand.Seed(time.Now().Unix()) signups, err := getSignups() if err != nil { return fmt.Errorf("could not get signups: %w", err) } signupIx := 0 title := tview.NewTextView() title.SetText(getTitle()) title.SetTextAlign(tview.AlignCenter) title.SetTextColor(tcell.ColorPurple) title.SetBackgroundColor(tcell.ColorBlack) appView := tview.NewTextView() appView.SetDynamicColors(true) if len(signups) == 0 { appView.SetText("no signups found.") } else { appView.SetText(signups[signupIx].Render()) } legend := tview.NewTextView() legend.SetText("s: skip r: random A: approve R: reject N: notate Q: quit") legend.SetTextColor(tcell.ColorPurple) legend.SetTextAlign(tview.AlignCenter) legend.SetBackgroundColor(tcell.ColorBlack) count := tview.NewTextView() count.SetDynamicColors(true) updateCount := func() { count.SetText(fmt.Sprintf("[-:-:b]%d pending signups[-:-:-]", len(signups))) } updateCount() bottomFlex := tview.NewFlex() bottomFlex.SetDirection(tview.FlexColumn) bottomFlex.AddItem(count, 0, 1, false) bottomFlex.AddItem(legend, 0, 10, false) mainFlex := tview.NewFlex() mainFlex.SetDirection(tview.FlexRow) mainFlex.AddItem(title, 1, -1, false) mainFlex.AddItem(appView, 0, 1, true) mainFlex.AddItem(bottomFlex, 1, -1, false) pages := tview.NewPages() errorModal := tview.NewModal() errorModal.AddButtons([]string{"damn"}) errorModal.SetDoneFunc(func(ix int, _ string) { pages.SwitchToPage("main") }) notate := tview.NewForm() notate.AddTextArea("note", "", 80, 10, 1000, func(string) {}) notate.AddButton("submit", func() { // TODO add note; this will require re-serializing and saving the file pages.SwitchToPage("main") }) notate.AddButton("cancel", func() { pages.SwitchToPage("main") }) pages.AddPage("main", mainFlex, true, true) pages.AddPage("error", errorModal, false, false) pages.AddPage("notate", notate, true, false) app := tview.NewApplication() app.SetRoot(pages, true) // TODO replace imperative shit with a signupManager advanceSignup := func() { if len(signups) == 0 { appView.SetText("no signups found.") return } signupIx++ if signupIx == len(signups) { signupIx = 0 } appView.SetText(signups[signupIx].Render()) } removeSignup := func(signup townSignup) { newSignups := []townSignup{} for ix, s := range signups { if ix != signupIx { newSignups = append(newSignups, s) } } signups = newSignups if len(signups) > 0 { if signupIx >= len(signups) { signupIx = 0 } } appView.SetText(signups[signupIx].Render()) } app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case 's': advanceSignup() case 'r': if len(signups) > 0 { signupIx = rand.Intn(len(signups)) appView.SetText(signups[signupIx].Render()) } case 'A': if len(signups) == 0 { return nil } signup := signups[signupIx] err := signup.Accept() if err != nil { errorModal.SetText(fmt.Sprintf("error! failed to approve '%s': %s", signup.Filename, err.Error())) pages.SwitchToPage("error") return nil } removeSignup(signup) updateCount() case 'R': if len(signups) == 0 { return nil } signup := signups[signupIx] err = signup.Reject() if err != nil { errorModal.SetText(fmt.Sprintf("error! failed to reject '%s': %s", signup.Filename, err.Error())) pages.SwitchToPage("error") return nil } removeSignup(signup) updateCount() case 'N': if len(signups) == 0 { return nil } pages.SwitchToPage("notate") return nil case 'Q': app.Stop() } return event }) return app.Run() } func main() { err := _main() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }