diff options
Diffstat (limited to 'tui/ssh.go')
| -rw-r--r-- | tui/ssh.go | 197 |
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 +} |
