~edwargix/tallyard

9dba2c258c1d8d2675c73e1041e994e7f8d0949d — David Florness 4 years ago 089f988
Move all program control flow to main function

The program control flow is currently handled in tui.go, where it doesn't
belong.
2 files changed, 105 insertions(+), 74 deletions(-)

M cmd/tallyard/main.go
M ui/tui.go
M cmd/tallyard/main.go => cmd/tallyard/main.go +42 -5
@@ 93,19 93,54 @@ func main() {
		}
	}()

	el := ui.TUI(client, elections)
	if el == nil || el.LocalVoter.Ballot == nil {
		// user likely hit C-c
	// select a room
	roomID, err := ui.RoomListTUI(client, elections)
	if err != nil {
		panic(err)
	}
	if roomID == "" {
		// no room selected; user likely hit C-c
		return
	}

	// select an election
	el, err := ui.RoomTUI(client, roomID, elections)
	if err != nil {
		panic(err)
	}
	if el == nil || el.LocalVoter == nil {
		// no election selected; user likely hit C-c
		return
	}

	el.Lock()
	// wait for election to start
	err = ui.ElectionWaitTUI(client, el)
	if err != nil {
		panic(err)
	}
	if el.StartEvt == nil {
		// election never started; user likely hit C-c
		return
	}

	// vote if we need to (user may have voted in previous tallyard
	// invocation)
	if el.LocalVoter.Ballot == nil {
		candidates := el.Candidates
		ballot := ui.VoteTUI(candidates)
		el.LocalVoter.Ballot = &ballot
		el.Save()
	}

	// set random poly with ballot
	el.LocalVoter.Poly = math.NewRandomPoly(uint(len(*el.FinalVoters)-1), 1024, *el.LocalVoter.Ballot)
	el.Unlock()
	el.Save()

	// wait for other voters to finish
	el.WaitForVoters(client)

	// send eval if we need to (if LocalVoter.Eval is set, we've already
	// sent the event)
	if el.LocalVoter.Eval == nil {
		err = el.SendEvals(client)
		if err != nil {


@@ 113,6 148,8 @@ func main() {
		}
	}

	// send sum if we need to (if LocalVoter.Sum is set, we've already sent
	// the event)
	if el.LocalVoter.Sum == nil {
		err = el.SendSum(client)
		if err != nil {

M ui/tui.go => ui/tui.go +63 -69
@@ 18,12 18,11 @@ import (
	"tallyard.xyz/election"
)

func TUI(client *mautrix.Client, elections *election.ElectionsMap) (el *election.Election) {
	alive := true
	app := tview.NewApplication()
func RoomListTUI(client *mautrix.Client, elections *election.ElectionsMap) (roomID id.RoomID, err error) {
	app := newTallyardApplication()
	resp, err := client.JoinedRooms()
	if err != nil {
		panic(err)
		return
	}
	// sorts rooms by name (putting rooms without names at the end)
	cmpRooms := func(i, j id.RoomID) bool {


@@ 70,25 69,22 @@ func TUI(client *mautrix.Client, elections *election.ElectionsMap) (el *election
		}
	}
	go func() {
		for alive {
		for app.alive {
			app.QueueUpdateDraw(update)
			time.Sleep(300 * time.Millisecond)
		}
	}()
	list.SetSelectedFunc(func(i int, _ string, _ string, _ rune) {
		alive = false
		app.Stop()
		el = RoomTUI(client, resp.JoinedRooms[i], elections)
		roomID = resp.JoinedRooms[i]
	})
	if err := app.SetRoot(list, true).Run(); err != nil {
		panic(err)
	}
	app.SetRoot(list, true)
	err = app.Run()
	return
}

func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap) (el *election.Election) {
	alive := true
	app := tview.NewApplication()
func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.ElectionsMap) (el *election.Election, err error) {
	app := newTallyardApplication()
	list := tview.NewList().
		AddItem("Create Election", "start a new election in this room", 0, nil)
	list.SetTitle("Select election").SetBorder(true)


@@ 113,13 109,12 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect
		}
	}
	go func() {
		for alive {
		for app.alive {
			app.QueueUpdateDraw(update)
			time.Sleep(1 * time.Second)
		}
	}()
	list.SetSelectedFunc(func(i int, _ string, _ string, _ rune) {
		alive = false
		app.Stop()
		elections.RLock()
		// don't do anything if the election under the cursor has


@@ 131,21 126,16 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect

		if i > 0 {
			// user wants to join election

			el = L[i - 1]
			// don't need to lock because this goroutine controls LocalVoter
			if el.LocalVoter != nil {
				if el.LocalVoter.Ballot == nil {
					ElectionTUI(client, el)
				}
			} else if joinElectionConfirmation(el) {
				err := el.JoinElection(client)
				if err != nil {
					panic(err)
			if el.LocalVoter == nil {
				// ask user if s/he wants to join election
				if joinElectionConfirmation(el) {
					err = el.JoinElection(client)
				} else {
					// user needs to select a different election
					el, err = RoomTUI(client, roomID, elections)
				}
				ElectionTUI(client, el)
			} else {
				el = RoomTUI(client, roomID, elections)
			}
			return
		}


@@ 155,27 145,24 @@ func RoomTUI(client *mautrix.Client, roomID id.RoomID, elections *election.Elect
		if candidates == nil {
			return
		}
		log.Debugf("title: %s", title)
		log.Debugf("candidates: %s", candidates)
		var err error
		log.Debugf("created election title: %s", title)
		log.Debugf("created election candidates: %s", candidates)
		el, err = election.CreateElection(client, candidates, title, roomID, elections)
		if err != nil {
			panic(err)
			return
		}
		err = el.JoinElection(client)
		if err != nil {
			panic(err)
			return
		}
		ElectionTUI(client, el)
	})
	if err := app.SetRoot(list, true).Run(); err != nil {
		panic(err)
	}
	app.SetRoot(list, true)
	err = app.Run()
	return
}

func joinElectionConfirmation(el *election.Election) (shouldJoin bool) {
	app := tview.NewApplication()
	app := newTallyardApplication()

	var buttons []string
	var text string


@@ 202,7 189,8 @@ func joinElectionConfirmation(el *election.Election) (shouldJoin bool) {
				shouldJoin = false
			}
		})
	if err := app.SetRoot(modal, false).Run(); err != nil {
	app.SetRoot(modal, false)
	if err := app.Run(); err != nil {
		panic(err)
	}
	return


@@ 211,7 199,7 @@ func joinElectionConfirmation(el *election.Election) (shouldJoin bool) {
func CreateElectionTUI() (title string, candidates []election.Candidate) {
	var form *tview.Form
	n := 2
	app := tview.NewApplication()
	app := newTallyardApplication()
	plus := func() {
		form.AddInputField(fmt.Sprintf("%d.", n+1), "", 50, nil, nil)
		n++


@@ 243,21 231,23 @@ func CreateElectionTUI() (title string, candidates []election.Candidate) {
		AddButton("-", minus).
		AddButton("Done", done)
	form.SetTitle("Create an election").SetBorder(true)
	if err := app.SetRoot(form, true).Run(); err != nil {
	app.SetRoot(form, true)
	if err := app.Run(); err != nil {
		panic(err)
	}
	if !hitSubmit {
		// user didn't hit submit button; consider form incomplete
		// user didn't hit submit button (maybe hit C-c); consider form
		// incomplete
		return "", nil
	}
	return title, candidates
}

func ElectionTUI(client *mautrix.Client, el *election.Election) {
func ElectionWaitTUI(client *mautrix.Client, el *election.Election) error {
	votersTextView := tview.NewTextView()
	frame := tview.NewFrame(votersTextView)
	frame.SetTitle(el.Title).SetBorder(true)
	app := tview.NewApplication()
	app := newTallyardApplication()
	el.RLock()
	if el.Creator == el.LocalVoter.UserID {
		frame.AddText("Press enter to start the election", false, tview.AlignCenter, tcell.ColorWhite)


@@ 292,7 282,7 @@ func ElectionTUI(client *mautrix.Client, el *election.Election) {
		votersTextView.SetText(text)
	}
	go func() {
		for {
		for app.alive {
			app.QueueUpdateDraw(update)

			// has the election started?


@@ 307,27 297,15 @@ func ElectionTUI(client *mautrix.Client, el *election.Election) {
			time.Sleep(1 * time.Second)
		}
	}()
	if err := app.SetRoot(frame, true).Run(); err != nil {
		panic(err)
	}

	if el.StartEvt == nil {
		// election was not started; perhaps user hit C-c?
		return
	}

	el.RLock()
	candidates := el.Candidates
	el.RUnlock()

	ballot := Vote(candidates)
	el.LocalVoter.Ballot = &ballot
	app.SetRoot(frame, true)
	err := app.Run()
	return err
}

// displays a voting UI to the user and returns the encoded ballot
func Vote(candidates []election.Candidate) []byte {
func VoteTUI(candidates []election.Candidate) []byte {
	ranks := make([]int, len(candidates))
	app := tview.NewApplication()
	app := newTallyardApplication()
	form := tview.NewForm()
	form.SetTitle("Vote").SetBorder(true)



@@ 350,14 328,30 @@ func Vote(candidates []election.Candidate) []byte {
		}
	})

	if err := app.SetRoot(form, true).Run(); err != nil {
	app.SetRoot(form, true)
	if err := app.Run(); err != nil {
		panic(err)
	}

	return GetBallotFromRankings(ranks)
	return getBallotFromRankings(ranks)
}

type tallyardApplication struct {
	*tview.Application
	alive bool
}

func newTallyardApplication() *tallyardApplication {
	return &tallyardApplication{tview.NewApplication(), true}
}

func (t *tallyardApplication) Run() error {
	err := t.Application.Run()
	t.alive = false
	return err
}

func GetBallotFromRankings(ranks []int) []byte {
func getBallotFromRankings(ranks []int) []byte {
	n := len(ranks)
	candidates := make([]int, n)



@@ 366,7 360,7 @@ func GetBallotFromRankings(ranks []int) []byte {
	}

	// sort candidates by ranking
	cr := CandidateRanking{candidates, ranks}
	cr := candidateRanking{candidates, ranks}
	sort.Sort(&cr)

	// TODO: support more than 255 voters (limit from usage of byte)


@@ 399,20 393,20 @@ func GetBallotFromRankings(ranks []int) []byte {
	return barray
}

type CandidateRanking struct {
type candidateRanking struct {
	candidates []int // becomes list of candidate IDs sorted by rank
	ranks      []int // maps candidate ID to rank
}

func (cr *CandidateRanking) Len() int {
func (cr *candidateRanking) Len() int {
	return len(cr.ranks)
}

func (cr *CandidateRanking) Less(i, j int) bool {
func (cr *candidateRanking) Less(i, j int) bool {
	return cr.ranks[cr.candidates[i]] < cr.ranks[cr.candidates[j]]
}

func (cr *CandidateRanking) Swap(i, j int) {
func (cr *candidateRanking) Swap(i, j int) {
	tmp := cr.candidates[i]
	cr.candidates[i] = cr.candidates[j]
	cr.candidates[j] = tmp