Use templates for rendering long CLI description
This commit is contained in:
parent
1a9c9e6b8a
commit
6127260b91
34 changed files with 962 additions and 71 deletions
|
@ -11,53 +11,20 @@ import (
|
|||
"git.andreafazzi.eu/andrea/probo/cmd/filter"
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var longDescription string = `
|
||||
# Filters
|
||||
|
||||
**Filters can made selection over stores.**
|
||||
|
||||
Filters allow you to narrow down selections across various stores. By
|
||||
using filters, you can select participants, quizzes, and
|
||||
responses. The command triggers a Text User Interface (TUI) that runs
|
||||
a jq filter, displaying the outcome in real-time. After you're content
|
||||
with the filtered JSON, pressing ⏎ will present the result on
|
||||
stdout, enabling you to further process it by piping it forward.
|
||||
|
||||
## Examples
|
||||
|
||||
Filter over participants store.
|
||||
|
||||
**probo filter participants**
|
||||
|
||||
Filter over quizzes store using the jq filter in tags.jq file
|
||||
|
||||
**probo filter quizzes -i data/filters/tags.jq**
|
||||
|
||||
Filter over participants and pipe the result on the next filter. The result is then stored in a JSON file.
|
||||
|
||||
**probo filter participants | probo filter quizzes > data/json/selection.json**
|
||||
`
|
||||
|
||||
func init() {
|
||||
desc, err := glamour.Render(fmt.Sprintf("```\n%s```\n%s", logo, longDescription), "dark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
filterCmd := &cobra.Command{
|
||||
var filterCmd = &cobra.Command{
|
||||
Use: "filter {participants,quizzes,responses}",
|
||||
Short: "Filter the given store",
|
||||
Long: desc,
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/filter/*.tmpl"),
|
||||
Run: runFilter,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(filterCmd)
|
||||
filterCmd.PersistentFlags().StringP("input", "i", "", "Specify an input file")
|
||||
}
|
||||
|
|
38
cmd/init.go
Normal file
38
cmd/init.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
"git.andreafazzi.eu/andrea/probo/embed"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// initCmd represents the init command
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize a working directory",
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/init/*.tmpl"),
|
||||
Run: runInit,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
|
||||
func runInit(cmd *cobra.Command, args []string) {
|
||||
err := embed.CopyToWorkingDirectory(embed.Data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = embed.CopyToWorkingDirectory(embed.Templates)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = embed.CopyToWorkingDirectory(embed.Public)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
12
cmd/root.go
12
cmd/root.go
|
@ -25,20 +25,24 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var (
|
||||
cfgFile string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "probo",
|
||||
Short: "A Quiz Management System for Hackers!",
|
||||
Long: `
|
||||
Short: "A Quiz Management System",
|
||||
/*Long: `
|
||||
Probo is a CLI/TUI application that allows for the quick
|
||||
creation of quizzes from markdown files and their distribution
|
||||
as web pages.`,
|
||||
as web pages.`,*/
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/root/*.tmpl"),
|
||||
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
|
|
82
cmd/serve.go
Normal file
82
cmd/serve.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/serve"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// serveCmd represents the serve command
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Launch a web server to adminster exam sessions",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: runServer,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
func runServer(cmd *cobra.Command, args []string) {
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
log.Fatal("Session store loading", "err", err)
|
||||
}
|
||||
|
||||
rStore, err := file.NewDefaultResponseFileStore()
|
||||
if err != nil {
|
||||
log.Fatal("Session store loading", "err", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
loginController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/login/layout-login.html.tmpl",
|
||||
"templates/login/login.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.LoginHandler)
|
||||
|
||||
sessionsController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/sessions/layout-sessions.html.tmpl",
|
||||
"templates/sessions/sessions.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.SessionsHandler)
|
||||
|
||||
examController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/exam/layout-exam.html.tmpl",
|
||||
"templates/exam/exam.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.ExamHandler)
|
||||
|
||||
mux.Handle("GET /login", serve.Recover(loginController))
|
||||
mux.Handle("POST /login", serve.Recover(loginController))
|
||||
|
||||
mux.Handle("GET /sessions", serve.Recover(sessionsController))
|
||||
mux.Handle("GET /sessions/{uuid}/exams/{token}", serve.Recover(examController))
|
||||
mux.Handle("POST /sessions/{uuid}/exams/{token}", serve.Recover(examController))
|
||||
|
||||
mux.Handle("GET /public/", http.StripPrefix("/public", http.FileServer(http.Dir("public"))))
|
||||
|
||||
log.Info("Listening...")
|
||||
|
||||
err = http.ListenAndServe(":3000", mux)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
64
cmd/serve/controller.go
Normal file
64
cmd/serve/controller.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
|
||||
sStore *file.SessionFileStore
|
||||
rStore *file.ResponseFileStore
|
||||
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
func NewController(sStore *file.SessionFileStore, rStore *file.ResponseFileStore) *Controller {
|
||||
return &Controller{
|
||||
sStore: sStore,
|
||||
rStore: rStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) WithHandlerFunc(f func(c *Controller, w http.ResponseWriter, r *http.Request)) *Controller {
|
||||
hf := func(w http.ResponseWriter, r *http.Request) {
|
||||
f(c, w, r)
|
||||
}
|
||||
|
||||
c.handlerFunc = hf
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Controller) WithTemplates(paths ...string) *Controller {
|
||||
tmpl, err := template.ParseFiles(paths...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.template = tmpl
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Controller) ExecuteTemplate(w http.ResponseWriter, data any) error {
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := c.template.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
|
||||
buf.WriteTo(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.handlerFunc.ServeHTTP(w, r)
|
||||
}
|
78
cmd/serve/exam.go
Normal file
78
cmd/serve/exam.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
var ExamHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
_, err := ValidateJwtCookie(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
participantToken := r.PathValue("token")
|
||||
|
||||
session, err := c.sStore.Read(r.PathValue("uuid"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
exam, ok := session.Exams[participantToken]
|
||||
if !ok {
|
||||
panic(errors.New("Exam not found in the store!"))
|
||||
}
|
||||
|
||||
examWithSession := struct {
|
||||
*models.Exam
|
||||
SessionID string
|
||||
}{exam, session.ID}
|
||||
|
||||
switch r.Method {
|
||||
|
||||
case http.MethodGet:
|
||||
log.Info("Sending exam to", "participant", exam.Participant, "exam", exam)
|
||||
|
||||
err = c.ExecuteTemplate(w, examWithSession)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case http.MethodPost:
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
answers := make([]*models.ParticipantAnswer, 0)
|
||||
|
||||
for quizID, values := range r.Form {
|
||||
correct := false
|
||||
quiz := session.Quizzes[quizID]
|
||||
|
||||
for _, answerID := range values {
|
||||
log.Info(answerID)
|
||||
if quiz.Correct.ID == answerID {
|
||||
correct = true
|
||||
}
|
||||
answers = append(answers, &models.ParticipantAnswer{Quiz: quiz, Answer: session.Answers[answerID], Correct: correct})
|
||||
}
|
||||
}
|
||||
|
||||
response, err := c.rStore.Create(
|
||||
&models.Response{
|
||||
SessionID: session.ID,
|
||||
Answers: answers,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Saving response", "response", response)
|
||||
|
||||
}
|
||||
|
||||
}
|
36
cmd/serve/jwt.go
Normal file
36
cmd/serve/jwt.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
const jwtExpiresAt = time.Hour
|
||||
|
||||
type Claims struct {
|
||||
Token string `json:"token"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
var (
|
||||
jwtKey = []byte("my-secret")
|
||||
)
|
||||
|
||||
func ValidateJwtCookie(r *http.Request) (*jwt.Token, error) {
|
||||
cookie, err := r.Cookie("Authorize")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtKey, nil
|
||||
})
|
||||
|
||||
return token, err
|
||||
}
|
76
cmd/serve/login.go
Normal file
76
cmd/serve/login.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var LoginHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pToken := r.FormValue("participantToken")
|
||||
if pToken == "" {
|
||||
panic(errors.New("Token not found parsing the request!"))
|
||||
}
|
||||
|
||||
log.Info("Received", "participantToken", pToken)
|
||||
|
||||
var loggedParticipant *models.Participant
|
||||
|
||||
done := false
|
||||
for _, session := range c.sStore.ReadAll() {
|
||||
if done {
|
||||
break
|
||||
}
|
||||
for _, exam := range session.Exams {
|
||||
if pToken == exam.Participant.Token {
|
||||
loggedParticipant = exam.Participant
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Participant logged in as", "participant", loggedParticipant)
|
||||
|
||||
if loggedParticipant == nil {
|
||||
panic(errors.New("Participant not found!"))
|
||||
}
|
||||
|
||||
claims := &Claims{
|
||||
Token: pToken,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(jwtExpiresAt).Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "Authorize",
|
||||
Value: tokenString,
|
||||
Expires: time.Now().Add(jwtExpiresAt),
|
||||
})
|
||||
|
||||
log.Info("Released", "jwt", tokenString)
|
||||
log.Info("Redirect to", "url", "/sessions")
|
||||
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
err := c.ExecuteTemplate(w, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
22
cmd/serve/recover.go
Normal file
22
cmd/serve/recover.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func Recover(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
log.Error("Recovering from", "err", err)
|
||||
http.Error(w, err.(error).Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
39
cmd/serve/sessions.go
Normal file
39
cmd/serve/sessions.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var SessionsHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
token, err := ValidateJwtCookie(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
var participantSessions []struct {
|
||||
Token string
|
||||
*models.Session
|
||||
}
|
||||
|
||||
for _, session := range c.sStore.ReadAll() {
|
||||
for _, exam := range session.Exams {
|
||||
if exam.Participant.Token == claims["token"] {
|
||||
s := struct {
|
||||
Token string
|
||||
*models.Session
|
||||
}{exam.Participant.Token, session}
|
||||
participantSessions = append(participantSessions, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = c.ExecuteTemplate(w, participantSessions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -213,7 +213,7 @@ func (m *SessionModel) View() string {
|
|||
return m.document.View()
|
||||
}
|
||||
|
||||
func (m *SessionModel) executeScript(path string) tea.Cmd {
|
||||
func (m *SessionModel) executeScript() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.scriptFilePath == "" {
|
||||
return nil
|
||||
|
@ -302,7 +302,7 @@ func (m *SessionModel) updateViewportContent(session *models.Session) {
|
|||
m.showErrorOnStatusBar(err)
|
||||
}
|
||||
|
||||
m.viewport.SetContent(result)
|
||||
m.viewport.SetContent(sanitize(result))
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{{define "description"}}
|
||||
{{.Description}}
|
||||
{{end}}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
*/
|
||||
package cmd
|
||||
|
||||
|
@ -13,7 +12,7 @@ import (
|
|||
// updateCmd represents the update command
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "A brief description of your command",
|
||||
Short: "Create or update an entity",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
|
@ -27,14 +26,4 @@ to quickly create a Cobra application.`,
|
|||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// updateCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// updateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
|
|
@ -2,14 +2,39 @@ package util
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/embed"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
)
|
||||
|
||||
func RenderMarkdownTemplates(paths ...string) string {
|
||||
tmpl, err := template.ParseFS(embed.CLI, paths...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result, err := glamour.Render(buf.String(), "dark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func LogToFile() *os.File {
|
||||
if len(os.Getenv("DEBUG")) > 0 {
|
||||
f, err := tea.LogToFile("probo-debug.log", "[DEBUG]")
|
||||
|
|
33
embed/cli/filter/description.tmpl
Normal file
33
embed/cli/filter/description.tmpl
Normal file
|
@ -0,0 +1,33 @@
|
|||
{{define "description"}}
|
||||
# Filters
|
||||
|
||||
**Filters can be used to make selections among stores.**
|
||||
|
||||
Filters allow you to narrow down selections across various stores. By
|
||||
using filters, you can select participants, quizzes, and
|
||||
responses. The command triggers a Text User Interface (TUI) that runs
|
||||
a `jq` filter, displaying the outcome in real-time. After you're content
|
||||
with the filtered JSON, pressing ⏎ will present the result on
|
||||
stdout, enabling you to further process it by piping it forward.
|
||||
|
||||
## Examples
|
||||
|
||||
1. Apply a filter to participants:
|
||||
|
||||
```
|
||||
probo filter participants
|
||||
```
|
||||
|
||||
2. Apply a filter to quizzes using the `jq` filter in `tags.jq` file:
|
||||
|
||||
```
|
||||
probo filter quizzes -i data/filters/tags.jq
|
||||
```
|
||||
|
||||
3. Apply a filter to participants, then pass the output through
|
||||
another filter. The final result is saved in a JSON file:
|
||||
|
||||
```
|
||||
probo filter participants | probo filter quizzes > data/json/selection.json
|
||||
```
|
||||
{{end}}
|
27
embed/cli/init/description.tmpl
Normal file
27
embed/cli/init/description.tmpl
Normal file
|
@ -0,0 +1,27 @@
|
|||
{{define "description"}}
|
||||
# Init
|
||||
|
||||
**Init initializes the current working directory.**
|
||||
|
||||
The `init` command creates, within the current directory, a file and
|
||||
folder structure prepared for immediate use of `probo`. The filesystem
|
||||
structure is as follows:
|
||||
```
|
||||
|
||||
├── filters
|
||||
├── json
|
||||
├── participants
|
||||
├── quizzes
|
||||
├── responses
|
||||
├── sessions
|
||||
└── templates
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
1. Initialize the current directory:
|
||||
|
||||
```
|
||||
probo init
|
||||
```
|
||||
{{end}}
|
|
@ -1,2 +1,2 @@
|
|||
{{.Logo}}
|
||||
{{template "logo" .}}
|
||||
{{template "description" .}}
|
10
embed/cli/logo.tmpl
Normal file
10
embed/cli/logo.tmpl
Normal file
|
@ -0,0 +1,10 @@
|
|||
{{define "logo"}}
|
||||
```
|
||||
____ _
|
||||
| _ \ _ __ ___ | |__ ___
|
||||
| |_) | '__/ _ \| '_ \ / _ \
|
||||
| __/| | | (_) | |_) | (_) |
|
||||
|_| |_| \___/|_.__/ \___/____
|
||||
|_____|
|
||||
```
|
||||
{{end}}
|
50
embed/cli/root/description.tmpl
Normal file
50
embed/cli/root/description.tmpl
Normal file
|
@ -0,0 +1,50 @@
|
|||
{{define "description"}}
|
||||
# Probo
|
||||
|
||||
`probo` is a quiz management system designed for command line
|
||||
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.
|
||||
|
||||
`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. The command will build a scaffold
|
||||
containing example participants, quizzes, and filters
|
||||
|
||||
```
|
||||
probo init
|
||||
```
|
||||
|
||||
2. Filter participants and quizzes and create an exam
|
||||
session.
|
||||
|
||||
```
|
||||
probo filter participant -i data/filters/9th_grade.jq \
|
||||
| probo filter quizzes -i data/filters/difficult_easy.jq \
|
||||
| probo create session --name="My First Session" > data/sessions/my_session.json
|
||||
```
|
||||
|
||||
3. Run the web server to allow participants to respond.
|
||||
|
||||
```
|
||||
probo serve
|
||||
```
|
||||
|
||||
4. Share the *qrcode* generated from the previous step with the
|
||||
participants and wait for them to finish responding.
|
||||
|
||||
5. Explore the results.
|
||||
|
||||
```
|
||||
probo filter responses -f '.' | probo rank
|
||||
```
|
||||
{{end}}
|
71
embed/embed.go
Normal file
71
embed/embed.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package embed
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed cli/*
|
||||
CLI embed.FS
|
||||
|
||||
//go:embed templates/*
|
||||
Templates embed.FS
|
||||
|
||||
//go:embed public/*
|
||||
Public 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 {
|
||||
log.Info(err)
|
||||
return err
|
||||
}
|
||||
fullDstPath := filepath.Join(currentDir, path)
|
||||
|
||||
if info.IsDir() {
|
||||
log.Info("Creating folder", "path", path)
|
||||
if err := os.MkdirAll(fullDstPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
srcFile, err := data.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(fullDstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
log.Info("Copying file", "path", path)
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
40
embed/public/css/login.css
Normal file
40
embed/public/css/login.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.container {
|
||||
display: grid;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.container input[type="text"],
|
||||
.container input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.container button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
23
embed/templates/exam/exam.html.tmpl
Normal file
23
embed/templates/exam/exam.html.tmpl
Normal file
|
@ -0,0 +1,23 @@
|
|||
{{ define "content" }}
|
||||
<div class="container">
|
||||
{{range $quiz := .Quizzes }}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{$quiz.Question}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">Una sola scelta possibile</h6>
|
||||
{{range $answer := $quiz.Answers}}
|
||||
<input type="radio"
|
||||
id="{{$quiz.ID}}_{{$answer.ID}}" name="{{$quiz.ID}}"
|
||||
value="{{$answer.ID}}">
|
||||
<label
|
||||
for="{{$quiz.ID}}_{{$answer.ID}}">{{$answer.Text}}</label><br>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
42
embed/templates/exam/layout-exam.html.tmpl
Normal file
42
embed/templates/exam/layout-exam.html.tmpl
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test di {{.Participant}}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="d-flex flex-wrap align-items-center justify-content-between py-3 border-bottom p-3 text-bg-dark border-bottom sticky-top">
|
||||
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
|
||||
<span class="fs-4 mx-2">Probo_</span>
|
||||
</a>
|
||||
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Salva" class="btn btn-primary me-2" form="submit-exam-form"/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form action="/sessions/{{.SessionID}}/exams/{{.Participant.Token}}" method="POST" id="submit-exam-form"/>
|
||||
|
||||
{{template "content" .}}
|
||||
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
||||
<div class="col-md-4 d-flex align-items-center">
|
||||
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
|
||||
<svg class="bi" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
</a>
|
||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2024 Andrea Fazzi</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#twitter"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#instagram"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#facebook"></use></svg></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
29
embed/templates/login/layout-login.html.tmpl
Normal file
29
embed/templates/login/layout-login.html.tmpl
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Probo login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
{{template "content" .}}
|
||||
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top fixed-bottom">
|
||||
<div class="col-md-4 d-flex align-items-center">
|
||||
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
|
||||
<svg class="bi" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
</a>
|
||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2024 Andrea Fazzi</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#twitter"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#instagram"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#facebook"></use></svg></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
25
embed/templates/login/login.html.tmpl
Normal file
25
embed/templates/login/login.html.tmpl
Normal file
|
@ -0,0 +1,25 @@
|
|||
{{ define "content" }}
|
||||
<div class="container col-xl-10 col-xxl-8 px-4 py-5">
|
||||
<div class="row align-items-center g-lg-5 py-5">
|
||||
<div class="col-lg-7 text-center text-lg-start">
|
||||
<h1 class="display-4 fw-bold lh-1 text-body-emphasis mb-3">🎓 Probo_</h1>
|
||||
<p class="col-lg-10 fs-4">
|
||||
Una piattaforma per la creazione, la gestione e la
|
||||
somministrazione di test <em>the hacker way</em>. Inserisci il
|
||||
tuo <strong>token</strong> personale per iniziare.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-10 mx-auto col-lg-5">
|
||||
<form method="post" action="/login" class="p-4 p-md-5 border rounded-3 bg-body-tertiary">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" name="participantToken" id="participantToken" placeholder="Token">
|
||||
<label for="participantToken">Inserisci il token...</label>
|
||||
</div>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Inizia</button>
|
||||
<hr class="my-4">
|
||||
<small class="text-body-secondary">Fai un bel respiro e non aver paura: non è un test su di te ma su quello che sai!</small>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
55
embed/templates/sessions/layout-sessions.html.tmpl
Normal file
55
embed/templates/sessions/layout-sessions.html.tmpl
Normal file
|
@ -0,0 +1,55 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Probo login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="mb-4 p-3 text-bg-dark border-bottom">
|
||||
<div class="container">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
|
||||
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
|
||||
<span class="fs-4 mx-2">Probo_</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
|
||||
<li><a href="#" class="nav-link px-2 text-secondary">I tuoi test</a></li>
|
||||
</ul>
|
||||
|
||||
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
|
||||
<input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Cerca..." aria-label="Search">
|
||||
</form>
|
||||
|
||||
<div class="text-end">
|
||||
<button type="button" class="btn btn-outline-light me-2">Cerca...</button>
|
||||
<button type="button" class="btn btn-warning">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{template "content" .}}
|
||||
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
||||
<div class="col-md-4 d-flex align-items-center">
|
||||
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
|
||||
<svg class="bi" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
</a>
|
||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2024 Andrea Fazzi</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#twitter"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#instagram"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#facebook"></use></svg></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
18
embed/templates/sessions/sessions.html.tmpl
Normal file
18
embed/templates/sessions/sessions.html.tmpl
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{ define "content" }}
|
||||
<div class="container">
|
||||
{{range $session := . }}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{$session.Title}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">{{$session.CreatedAt}}</h6>
|
||||
<p class="card-text">{{$session.Description}}</p>
|
||||
<a href="/sessions/{{$session.ID}}/exams/{{$session.Token}}" class="btn btn-primary">Inizia!</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
13
go.mod
13
go.mod
|
@ -1,15 +1,18 @@
|
|||
module git.andreafazzi.eu/andrea/probo
|
||||
|
||||
go 1.21.6
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/charmbracelet/huh v0.3.0
|
||||
github.com/charmbracelet/lipgloss v0.10.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/d5/tengo/v2 v2.17.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/itchyny/gojq v0.12.14
|
||||
github.com/lmittmann/tint v1.0.4
|
||||
|
@ -24,10 +27,13 @@ require (
|
|||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
|
@ -38,10 +44,12 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
|
@ -52,9 +60,12 @@ require (
|
|||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
|
|
36
go.sum
36
go.sum
|
@ -6,18 +6,25 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
|
|||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181 h1:ntdtXC9+kcgQYvqa6nyLZLniCEUOhWQknLlz38JpDpM=
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181/go.mod h1:Zlzkn8WOd6QS7RC1BXAY1iw1VLq+xT70UZ1vkEtnrvo=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
|
||||
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
|
||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
|
@ -34,12 +41,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
@ -58,13 +71,18 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
|
@ -73,8 +91,11 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
@ -117,24 +138,39 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
|
||||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
4
go.work
4
go.work
|
@ -1,4 +1,6 @@
|
|||
go 1.21.6
|
||||
go 1.22.2
|
||||
|
||||
toolchain go1.22.3
|
||||
|
||||
use (
|
||||
.
|
||||
|
|
|
@ -5,8 +5,6 @@ cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LR
|
|||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
|
||||
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
|
@ -51,6 +49,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
|||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
|
||||
|
@ -65,7 +65,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
|
|
4
main.go
4
main.go
|
@ -21,7 +21,9 @@ THE SOFTWARE.
|
|||
*/
|
||||
package main
|
||||
|
||||
import "git.andreafazzi.eu/andrea/probo/cmd"
|
||||
import (
|
||||
"git.andreafazzi.eu/andrea/probo/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
|
|
|
@ -88,7 +88,7 @@ func DefaultIndexDirFunc[T FileStorable, K store.Storer[T]](s *FileStore[T, K])
|
|||
|
||||
err = entity.Unmarshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err)
|
||||
}
|
||||
|
||||
mEntity, err := s.Create(entity, fullPath)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
@ -60,7 +61,7 @@ func DefaultQuizIndexDirFunc(s *QuizFileStore) error {
|
|||
|
||||
err = entity.Unmarshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err)
|
||||
}
|
||||
|
||||
var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
|
||||
|
|
Loading…
Reference in a new issue