diff --git a/cmd/rank/rank.go~ b/cmd/rank/rank.go~ deleted file mode 100644 index 166086d..0000000 --- a/cmd/rank/rank.go~ +++ /dev/null @@ -1,436 +0,0 @@ -package rank - -import ( - "bytes" - "cmp" - "encoding/json" - "errors" - "fmt" - "os" - "slices" - "strconv" - "strings" - "text/template" - - "git.andreafazzi.eu/andrea/probo/pkg/models" - "git.andreafazzi.eu/andrea/probo/pkg/store/file" - "github.com/alecthomas/chroma/quick" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/spinner" - btTable "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" - "github.com/charmbracelet/lipgloss" - "github.com/d5/tengo/v2" - "github.com/d5/tengo/v2/stdlib" - foam "github.com/remogatto/sugarfoam" - "github.com/remogatto/sugarfoam/components/group" - "github.com/remogatto/sugarfoam/components/header" - "github.com/remogatto/sugarfoam/components/help" - "github.com/remogatto/sugarfoam/components/statusbar" - "github.com/remogatto/sugarfoam/components/table" - "github.com/remogatto/sugarfoam/components/viewport" - "github.com/remogatto/sugarfoam/layout" - "github.com/remogatto/sugarfoam/layout/tiled" -) - -var responseTmpl = ` -{{range $answer := .Answers}} -{{$answer.Quiz|toMarkdown $.Width}} - R: {{$answer|toLipgloss}} -{{end}} -` - -type ParticipantScore struct { - Participant *models.Participant `json:"participant"` - Response *models.Response `json:"response"` - Score int `json:"score"` -} - -type Rank struct { - Scores []*ParticipantScore `json:"scores"` -} - -type RankModel struct { - // UI - viewport *viewport.Model - table *table.Model - group *group.Model - help *help.Model - statusBar *statusbar.Model - spinner spinner.Model - - // Layout - document *layout.Layout - - // Key bindings - bindings *keyBindings - - // json - InputJson string - Result string - - // session - rank *Rank - - // response - responseTmpl *template.Template - - // filter file - scriptFilePath string - - state int -} - -func New(path string, stdin string) *RankModel { - viewport := viewport.New() - - table := table.New(table.WithRelWidths(10, 20, 30, 30, 10)) - table.Model.SetColumns([]btTable.Column{ - {Title: "Pos", Width: 5}, - {Title: "Token", Width: 10}, - {Title: "Lastname", Width: 40}, - {Title: "Firstname", Width: 40}, - {Title: "Score", Width: 5}, - }) - - group := group.New( - group.WithItems(table, viewport), - group.WithLayout( - layout.New( - layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 1)}), - layout.WithItem(tiled.New(table, viewport)), - ), - ), - ) - - bindings := newBindings(group) - statusBar := statusbar.New(bindings) - - s := spinner.New( - spinner.WithStyle( - lipgloss.NewStyle().Foreground(lipgloss.Color("265"))), - ) - s.Spinner = spinner.Dot - - header := header.New( - header.WithContent( - lipgloss.NewStyle(). - Bold(true). - Border(lipgloss.NormalBorder(), false, false, true, false). - Render("😎 Rank 😎"), - ), - ) - - help := help.New( - bindings, - help.WithStyles(&foam.Styles{NoBorder: lipgloss.NewStyle().Padding(1, 1)})) - - document := layout.New( - layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}), - layout.WithItem(header), - layout.WithItem(group), - layout.WithItem(help), - layout.WithItem(statusBar), - ) - - tmpl, err := template.New("response"). - Funcs(template.FuncMap{ - "toMarkdown": func(width int, quiz *models.Quiz) string { - md, err := models.QuizToMarkdown(quiz) - if err != nil { - panic(err) - } - - renderer, err := glamour.NewTermRenderer( - glamour.WithStandardStyle("dracula"), - glamour.WithWordWrap(width), - ) - if err != nil { - panic(err) - } - - result, err := renderer.Render(md) - if err != nil { - panic(err) - } - - return result - }, - "toLipgloss": func(answer *models.ParticipantAnswer) string { - color := "#ff0000" - if answer.Correct { - color = "#00ff00" - } - return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render(answer.Answer.Text) - }, - }). - Parse(responseTmpl) - if err != nil { - panic(err) - } - - return &RankModel{ - table: table, - viewport: viewport, - group: group, - statusBar: statusBar, - spinner: s, - document: document, - responseTmpl: tmpl, - bindings: bindings, - help: help, - scriptFilePath: path, - InputJson: stdin, - } - -} - -func (m *RankModel) Init() tea.Cmd { - var cmds []tea.Cmd - - cmds = append(cmds, m.group.Init(), m.executeScript(), m.spinner.Tick) - - m.group.Focus() - - return tea.Batch(cmds...) -} - -func (m *RankModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - - case tea.WindowSizeMsg: - m.handleWindowSize(msg) - - case tea.KeyMsg: - switch { - case key.Matches(msg, m.bindings.quit): - cmds = append(cmds, tea.Quit) - } - - case scriptExecutedMsg: - m.handleScriptExecuted(msg) - - case errorMsg: - m.handleError(msg) - m.state = ErrorState - } - - cmds = m.handleState(msg, cmds) - - return m, tea.Batch(cmds...) -} - -func (m *RankModel) View() string { - return m.document.View() -} - -func (m *RankModel) executeScript() tea.Cmd { - return func() tea.Msg { - if m.scriptFilePath == "" { - return nil - } - - rankJson, err := json.Marshal(Rank{Scores: make([]*ParticipantScore, 0)}) - if err != nil { - panic(err) - } - - script, err := os.ReadFile(m.scriptFilePath) - if err != nil { - panic(err) - } - - s := tengo.NewScript(script) - - s.SetImports(stdlib.GetModuleMap("fmt", "json", "rand", "times")) - _ = s.Add("input", m.InputJson) - _ = s.Add("output", string(rankJson)) - - c, err := s.Compile() - if err != nil { - panic(err) - } - - if err := c.Run(); err != nil { - panic(err) - } - - return scriptExecutedMsg{fmt.Sprintf("%s", c.Get("output"))} - - } -} - -func (m *RankModel) showErrorOnStatusBar(err error) { - m.statusBar.SetContent( - stateFormats[ErrorState][0], - fmt.Sprintf(stateFormats[ErrorState][1], err), - stateFormats[ErrorState][2], - ) -} - -func (m *RankModel) updateTableContent() { - rows := make([]btTable.Row, 0) - - for i, score := range m.rank.Scores { - rows = append(rows, btTable.Row{ - strconv.Itoa(i + 1), - score.Participant.Token, - score.Participant.Lastname, - score.Participant.Firstname, - strconv.Itoa(score.Score), - }) - - } - - m.table.SetRows(rows) -} - -func (m *RankModel) updateViewportContent() { - if len(m.table.Rows()) == 0 { - panic(errors.New("No scores available!")) - } - - currentPos := m.table.SelectedRow()[0] - - pos, err := strconv.Atoi(currentPos) - if err != nil { - panic(err) - } - - currentResponse := m.rank.Scores[pos-1] - - var buf bytes.Buffer - - var response struct { - *models.Response - Width int - } = struct { - *models.Response - Width int - }{currentResponse.Response, m.viewport.GetWidth()} - - err = m.responseTmpl.Execute(&buf, response) - if err != nil { - panic(err) - } - - m.viewport.SetContent(sanitize(buf.String())) - -} - -func (m *RankModel) handleWindowSize(msg tea.WindowSizeMsg) { - m.group.SetSize(msg.Width, msg.Height) - m.document.SetSize(msg.Width, msg.Height) -} - -func (m *RankModel) handleError(msg tea.Msg) { - err := msg.(errorMsg) - - m.statusBar.SetContent( - stateFormats[ErrorState][0], - fmt.Sprintf(stateFormats[ErrorState][1], err.error), - stateFormats[ErrorState][2], - ) -} - -func (m *RankModel) handleScriptExecuted(msg tea.Msg) { - rank := new(Rank) - jsonData := []byte(msg.(scriptExecutedMsg).result) - - err := json.Unmarshal(jsonData, &rank) - if err != nil { - panic(err) - } - - slices.SortFunc(rank.Scores, - func(a, b *ParticipantScore) int { - return cmp.Compare(b.Score, a.Score) - }) - - m.rank = rank - - m.updateTableContent() - m.updateViewportContent() - - m.state = BrowseState -} - -func (m *RankModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd { - _, cmd := m.group.Update(msg) - - if m.state == ExecutingScriptState { - return m.updateSpinner(msg, cmd, cmds) - } - - if m.state == BrowseState { - m.updateViewportContent() - } - - if m.state != ErrorState { - m.statusBar.SetContent( - stateFormats[BrowseState][0], - fmt.Sprintf(stateFormats[BrowseState][1], len(m.rank.Scores)), - stateFormats[BrowseState][2], - ) - } - - cmds = append(cmds, cmd) - - return cmds -} - -func (m *RankModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd { - m.spinner, cmd = m.spinner.Update(msg) - - m.statusBar.SetContent(fmt.Sprintf(stateFormats[m.state][0], m.spinner.View()), stateFormats[m.state][1], stateFormats[m.state][2]) - - cmds = append(cmds, cmd) - - return cmds -} - -func (m *RankModel) loadStore() tea.Cmd { - return func() tea.Msg { - sStore, err := file.NewDefaultSessionFileStore() - if err != nil { - panic(err) - } - - return storeLoadedMsg{sStore} - } -} - -func toColoredJson(data []any) (string, error) { - result, err := json.MarshalIndent(data, "", " ") - if err != nil { - return "", err - } - - coloredBytes := make([]byte, 0) - buffer := bytes.NewBuffer(coloredBytes) - - err = quick.Highlight(buffer, string(result), "json", "terminal16m", "dracula") - if err != nil { - panic(err) - } - - return sanitize(buffer.String()), nil -} - -func sanitize(text string) string { - // FIXME: The use of a standard '-' character causes rendering - // issues within the viewport. Further investigation is - // required to resolve this problem. - return strings.Replace(text, "-", "–", -1) -} - -func desanitize(text string) string { - // FIXME: The use of a standard '-' character causes rendering - // issues within the viewport. Further investigation is - // required to resolve this problem. - return strings.Replace(text, "–", "-", -1) -} diff --git a/embed/cli/root/description.tmpl~ b/embed/cli/root/description.tmpl~ deleted file mode 100644 index b61614c..0000000 --- a/embed/cli/root/description.tmpl~ +++ /dev/null @@ -1,62 +0,0 @@ -{{define "description"}} -# Probo - -`probo` is a quiz management and administration system designed for -command line and text interface enthusiasts. `probo` aims to highlight -themes such as privacy, interoperability, accessibility, -decentralization, and quick usage speed. - -`probo` organizes information in plain text files and folders so that -data can be easily revised through version control systems. Quizzes -can be written in Markdown format, filters use the `jq` language, exam -sessions, participants, and their quiz answers are JSON files. - -`probo` contains a built-in web server that allows quizzes to be -administered to participants and their answers received. - -`probo` is a self-contained application that can be distributed through -a simple executable containing all necessary assets for its operation. - -# Quickstart - -1. Initialize the working directory - -``` -probo init -``` - -2. Create our first quiz. - -``` -probo create quiz > data/quizzes/quiz_1.md -``` - -3. Create two participants. - -``` -probo create participant > data/participants/john.json -probo create participant > data/participants/mary.json -``` - -4. Filter participants and quizzes (in this case, all participants and -all quizzes present in the store will be selected) and create an exam -session. - -``` -probo filter participant -f '.' | probo filter quizzes -f '.' | probo create session --name="My First Session" > data/sessions/my_session.json -``` - -5. Run the web server to allow participants to respond. - -``` -probo serve -``` - -6. Share the *qrcode* generated from the previous step with the participants and wait for them to finish responding. - -7. Explore the results. - -``` -probo filter responses -f '.' | probo rank -``` -{{end}} \ No newline at end of file diff --git a/embed/embed.go~ b/embed/embed.go~ deleted file mode 100644 index e512117..0000000 --- a/embed/embed.go~ +++ /dev/null @@ -1,61 +0,0 @@ -package embed - -import ( - "embed" - "io" - "io/fs" - "os" - "path/filepath" -) - -var ( - //go:embed templates/* - Templates embed.FS - Assets embed.FS - //go:embed data/* - Data embed.FS -) - -func CopyToWorkingDirectory(data embed.FS) error { - currentDir, err := os.Getwd() - if err != nil { - return err - } - - if err := fs.WalkDir(data, ".", func(path string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - - fullPath := path - - if info.IsDir() { - dirPath := filepath.Join(currentDir, path) - if err := os.MkdirAll(dirPath, 0755); err != nil { - return err - } - } else { - srcFile, err := data.Open(fullPath) - if err != nil { - return err - } - - dstFile, err := os.Create(filepath.Join(currentDir, path)) - if err != nil { - return err - } - defer dstFile.Close() - - _, err = io.Copy(dstFile, srcFile) - if err != nil { - return err - } - } - - return nil - }); err != nil { - return err - } - - return nil -}