summaryrefslogtreecommitdiff
path: root/tui/ssh.go
diff options
context:
space:
mode:
Diffstat (limited to 'tui/ssh.go')
-rw-r--r--tui/ssh.go197
1 files changed, 197 insertions, 0 deletions
diff --git a/tui/ssh.go b/tui/ssh.go
new file mode 100644
index 0000000..2309b3f
--- /dev/null
+++ b/tui/ssh.go
@@ -0,0 +1,197 @@
+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
+}