summaryrefslogtreecommitdiff
path: root/tui
diff options
context:
space:
mode:
Diffstat (limited to 'tui')
-rw-r--r--tui/post.go126
-rw-r--r--tui/ssh.go197
2 files changed, 323 insertions, 0 deletions
diff --git a/tui/post.go b/tui/post.go
new file mode 100644
index 0000000..7762b56
--- /dev/null
+++ b/tui/post.go
@@ -0,0 +1,126 @@
+package tui
+
+// An example program demonstrating the pager component from the Bubbles
+// component library.
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// You generally won't need this unless you're processing stuff with
+// complicated ANSI escape sequences. Turn it on if you notice flickering.
+//
+// Also keep in mind that high performance rendering only works for programs
+// that use the full size of the terminal. We're enabling that below with
+// tea.EnterAltScreen().
+const useHighPerformanceRenderer = false
+
+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.BorderStyle(b)
+ }()
+)
+
+type PostModel struct {
+ content string
+ ready bool
+ viewport viewport.Model
+}
+
+func (m PostModel) Init() tea.Cmd {
+ return nil
+}
+
+func (m PostModel) Update(msg tea.Msg) (PostModel, tea.Cmd) {
+ var (
+ cmd tea.Cmd
+ cmds []tea.Cmd
+ )
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
+ return m, tea.Quit
+ }
+
+ case tea.WindowSizeMsg:
+ headerHeight := lipgloss.Height(m.headerView())
+ footerHeight := lipgloss.Height(m.footerView())
+ verticalMarginHeight := headerHeight + footerHeight
+
+ if !m.ready {
+ // Since this program is using the full size of the viewport we
+ // need to wait until we've received the window dimensions before
+ // we can initialize the viewport. The initial dimensions come in
+ // quickly, though asynchronously, which is why we wait for them
+ // here.
+ m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
+ m.viewport.YPosition = headerHeight
+ m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
+ m.viewport.SetContent(m.content)
+ m.ready = true
+
+ // This is only necessary for high performance rendering, which in
+ // most cases you won't need.
+ //
+ // Render the viewport one line below the header.
+ m.viewport.YPosition = headerHeight + 1
+ } else {
+ m.viewport.Width = msg.Width
+ m.viewport.Height = msg.Height - verticalMarginHeight
+ }
+
+ if useHighPerformanceRenderer {
+ // Render (or re-render) the whole viewport. Necessary both to
+ // initialize the viewport and when the window is resized.
+ //
+ // This is needed for high-performance rendering only.
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
+ }
+
+ // Handle keyboard and mouse events in the viewport
+ m.viewport, cmd = m.viewport.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m PostModel) View() string {
+ if !m.ready {
+ return "\n Initializing..."
+ }
+ return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
+}
+
+func (m PostModel) headerView() string {
+ title := titleStyle.Render("Mr. Pager")
+ line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
+ return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
+}
+
+func (m PostModel) footerView() string {
+ info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
+ line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
+ return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
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
+}