package main import ( "context" "fmt" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "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" ) var ( titleStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Right = "├" return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) }() infoStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Left = "┤" return titleStyle.Copy().BorderStyle(b) }() ) 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(), tea.WithMouseCellMotion()} } type errMsg error type word struct { content string distance float64 ranking int } type model struct { ready bool err error textInput textinput.Model ip string words []word wordsViewport viewport.Model 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 var cmds []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 } case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) verticalMarginHeight := headerHeight + footerHeight if !m.ready { m.wordsViewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) m.wordsViewport.YPosition = headerHeight m.ready = true } else { m.wordsViewport.Width = msg.Width m.wordsViewport.Height = msg.Height - verticalMarginHeight } case errMsg: m.err = msg return m, nil } m.wordsViewport, cmd = m.wordsViewport.Update(msg) cmds = append(cmds, cmd) m.textInput, cmd = m.textInput.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) InputWord() (model, tea.Cmd) { input := m.textInput.Value() m.lastWord = input if len(input) > m.maxLength { m.maxLength = len(input) } // TODO: Get distance and ranking from a file // TODO: Reorder the array w := word{input, 200*rand.Float64() - 100, rand.Intn(1000)} m.words = append(m.words, w) var content string for _, w := range m.words { content += w.View(m) } m.wordsViewport.SetContent(content) 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) headerView() string { var err string if m.err != nil { err = fmt.Sprintf("Erreur : %s", m.err) } msg := fmt.Sprintf( "Veuillez choisir un mot :\n\n%s\n\nDernier mot essayé : %s%s\n%s\n\n", m.textInput.View(), m.lastWord, strings.Repeat(" ", 26), err, ) title := titleStyle.Render("Mots précédents") line := strings.Repeat("─", int(math.Max(0, float64(m.wordsViewport.Width-lipgloss.Width(title))))) msg += lipgloss.JoinHorizontal(lipgloss.Center, title, line) return msg } func (m model) footerView() string { info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.wordsViewport.ScrollPercent()*100)) line := strings.Repeat("─", int(math.Max(0, float64(m.wordsViewport.Width-lipgloss.Width(info))))) return lipgloss.JoinHorizontal(lipgloss.Center, line, info) } func (m model) View() string { if !m.ready { return "\n\tInitializing..." } return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.wordsViewport.View(), m.footerView()) }