package main import ( "context" "fmt" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/wish" bm "github.com/charmbracelet/wish/bubbletea" lm "github.com/charmbracelet/wish/logging" "github.com/gliderlabs/ssh" "log" "math" "math/rand" "os" "os/signal" "strings" "syscall" "time" ) func main() { log.Println("Welcome on Cemantics!") s, err := wish.NewServer( wish.WithAddress(fmt.Sprintf("%s:%d", "0.0.0.0", 2200)), wish.WithAddress(fmt.Sprintf("%s:%d", "[::]", 2200)), wish.WithHostKeyPath(".ssh/term_info_ed25519"), wish.WithMiddleware( lm.Middleware(), bm.Middleware(teaHandler), ), ) if err != nil { log.Fatalln(err) } done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) log.Printf("Starting SSH server on %s:%d", "[::]", 2200) go func() { if err = s.ListenAndServe(); err != nil { log.Fatalln(err) } }() <-done log.Println("Stopping SSH server") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer func() { cancel() }() if err := s.Shutdown(ctx); err != nil { log.Fatalln(err) } } func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { _, _, active := s.Pty() if !active { fmt.Println("no active terminal, skipping") return nil, nil } log.Println(s.RemoteAddr()) ti := textinput.New() ti.Placeholder = "Choisissez un mot ..." ti.Focus() ti.CharLimit = 26 ti.Width = 26 m := model{ textInput: ti, err: nil, ip: s.RemoteAddr().String(), words: []word{}, progressBar: progress.New(progress.WithScaledGradient("#FF7CCB", "#FDFF8C")), } return m, []tea.ProgramOption{tea.WithAltScreen()} } type errMsg error type word struct { content string distance float64 ranking int } type model struct { textInput textinput.Model err error ip string words []word progressBar progress.Model maxLength int lastWord string } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: return m.InputWord() case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit } // We handle errors just like any other message case errMsg: m.err = msg return m, nil } m.textInput, cmd = m.textInput.Update(msg) return m, cmd } func (m model) InputWord() (model, tea.Cmd) { input := m.textInput.Value() log.Printf("Chosen word: %s", input) m.lastWord = input if len(input) > m.maxLength { m.maxLength = len(input) } // TODO: Get distance and ranking from a file // TODO: Reorder the array m.words = append(m.words, word{input, 200*rand.Float64() - 100, rand.Intn(1000)}) m.textInput.SetValue("") m.textInput.CursorStart() return m, nil } func (w word) View(m model) string { var emoji string if w.ranking == 1000 { emoji = "\U0001F973" } else if w.ranking == 999 { emoji = "\U0001F631" } else if w.ranking >= 990 { emoji = "\U0001F525" } else if w.ranking >= 900 { emoji = "\U0001F975" } else if w.ranking >= 1 { emoji = "\U0001F60E" } else { emoji = "\U0001F976" } var progressBar string if w.ranking > 0 { progressBar = m.progressBar.ViewAs(float64(w.ranking) / 1000.0) } distStr := fmt.Sprintf("%.02f", w.distance) if math.Abs(w.distance) < 10 { distStr = " " + distStr } if w.distance >= 0 { distStr = " " + distStr } return fmt.Sprintf( "* %s\t%s %s\t%4d %s\n", w.content+strings.Repeat(" ", m.maxLength-len(w.content)), distStr, emoji, w.ranking, progressBar, ) } func (m model) View() string { msg := fmt.Sprintf( "Veuillez choisir un mot :\n\n%s\n\n%s", m.textInput.View(), "(Esc pour quitter)", ) + "\n\n" if m.lastWord != "" { msg += fmt.Sprintf("Dernier mot essayƩ : %s\n\n", m.lastWord) } if len(m.words) > 0 { msg += fmt.Sprintf("Liste des derniers mots :\n") for _, w := range m.words { msg += w.View(m) } } return msg }