package tui // An example Bubble Tea server. This will put an ssh session into alt screen // and continually print up to date terminal information. import ( "fmt" "io/fs" "net" "git.sr.ht/~a73x/home" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" "github.com/charmbracelet/wish/activeterm" "github.com/charmbracelet/wish/bubbletea" "github.com/charmbracelet/wish/logging" ) func New(host, port string) (*ssh.Server, error) { s, err := wish.NewServer( wish.WithAddress(net.JoinHostPort(host, port)), wish.WithHostKeyPath(".ssh/id_ed25519"), wish.WithMiddleware( bubbletea.Middleware(teaHandler), activeterm.Middleware(), // Bubble Tea apps usually require a PTY. logging.Middleware(), ), ) if err != nil { log.Error("Could not start server", "error", err) } return s, err } // You can wire any Bubble Tea model up to the middleware with a function that // handles the incoming ssh.Session. Here we just grab the terminal info and // pass it to the new model. You can also return tea.ProgramOptions (such as // tea.WithAltScreen) on a session by session basis. func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { // This should never fail, as we are using the activeterm middleware. pty, _, _ := s.Pty() // When running a Bubble Tea app over SSH, you shouldn't use the default // lipgloss.NewStyle function. // That function will use the color profile from the os.Stdin, which is the // server, not the client. // We provide a MakeRenderer function in the bubbletea middleware package, // so you can easily get the correct renderer for the current session, and // use it to create the styles. // The recommended way to use these styles is to then pass them down to // your Bubble Tea model. renderer := bubbletea.MakeRenderer(s) txtStyle := renderer.NewStyle().Foreground(lipgloss.Color("10")) quitStyle := renderer.NewStyle().Foreground(lipgloss.Color("8")) bg := "light" if renderer.HasDarkBackground() { bg = "dark" } posts, err := home.Content.ReadDir("posts") if err != nil { // fk knows } postFiles := make([]string, 0, len(posts)) for _, post := range posts { postFiles = append(postFiles, "posts/"+post.Name()) } m := model{ term: pty.Term, profile: renderer.ColorProfile().Name(), width: pty.Window.Width, height: pty.Window.Height, bg: bg, txtStyle: txtStyle, quitStyle: quitStyle, posts: postFiles, } return m, []tea.ProgramOption{tea.WithAltScreen()} } // Just a generic tea.Model to demo terminal information of ssh. type model struct { term string profile string width int height int bg string txtStyle lipgloss.Style quitStyle lipgloss.Style posts []string cursor int viewer *PostModel err error } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd if m.viewer != nil { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": m.viewer = nil return m, nil case "esc": return m, nil } } viewer, cmd := m.viewer.Update(msg) m.viewer = &viewer return m, cmd } switch msg := msg.(type) { case tea.WindowSizeMsg: m.height = msg.Height m.width = msg.Width case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.posts)-1 { m.cursor++ } case "enter": content, err := fs.ReadFile(home.Content, m.posts[m.cursor]) if err != nil { m.err = err return m, nil } out, err := glamour.Render(string(content), "dark") if err != nil { m.err = err return m, nil } m.viewer = &PostModel{ content: out, } fakeWindowMsg := tea.WindowSizeMsg{ Width: m.width, Height: m.height - 1, } viewer, cmd := m.viewer.Update(fakeWindowMsg) m.viewer = &viewer return m, cmd } } return m, tea.Batch(cmds...) } func (m model) View() string { if m.err != nil { return m.err.Error() } if m.viewer != nil { return m.viewer.View() + "\n arrow keys to scroll and q to go back" } s := "a73x\n\n" for i, choice := range m.posts { // Is the cursor pointing at this choice? cursor := " " // no cursor if m.cursor == i { cursor = ">" // cursor! } // Render the row s += fmt.Sprintf("%s %s\n", cursor, choice) } s += "\narrow keys to select post and enter to select" s += "\npress q to quit" return s }