Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stream content to a viewport #294

Open
oxodao opened this issue Nov 16, 2022 · 4 comments
Open

Stream content to a viewport #294

oxodao opened this issue Nov 16, 2022 · 4 comments

Comments

@oxodao
Copy link

oxodao commented Nov 16, 2022

Hi,

Let's say I'm running some kind of external software and I want to display the logs of it during its lifetime in a viewport.
How can I do this ? As far as I understand, the viewport can only be updated through the SetContent method.
Ideally, the viewport should update every new line but I don't want to do some weird things if I missed something

Minimal code:
-> /tmp/some-software.sh

while true; do echo Hey; sleep 1; done
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
)

type CmdMsg struct {
	path string
}

type Model struct {
	logs io.Writer

	ready    bool
	viewport viewport.Model
}

func (x Model) Init() tea.Cmd {
	return func() tea.Msg {
		return CmdMsg{path: "/tmp/some-software.sh"}
	}
}

func (x Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
			return x, tea.Quit
		}
	case CmdMsg:
		cmd := exec.Command("/usr/bin/bash", msg.path)

		var logs bytes.Buffer
		logsWriter := bufio.NewWriter(&logs)
		cmd.Stdout = logsWriter
		cmd.Stderr = logsWriter

		x.logs = logsWriter
		// ???

		cmd.Start()

	case tea.WindowSizeMsg:
		if !x.ready {
			x.viewport = viewport.New(msg.Width, msg.Height-10)
			x.viewport.YPosition = 5
			x.viewport.HighPerformanceRendering = false
			x.viewport.SetContent( /* ???? */ )
			x.ready = true
		} else {
			x.viewport.Width = msg.Width
			x.viewport.Height = msg.Height - 10
		}
	}

	return x, nil
}

func (x Model) View() string {
	return x.viewport.View()
}

func main() {
	p := tea.NewProgram(
		Model{},
		tea.WithAltScreen(),
	)

	if _, err := p.Run(); err != nil {
		fmt.Println("could not run program:", err)
		os.Exit(1)
	}
}

Thanks!

@rew1nter
Copy link

I've the same question

@coxley
Copy link

coxley commented Feb 10, 2023

Firstly, you're doing I/O inside of Update() which is going to make weird things happen.

Check out this tutorial: https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands/

Secondly, because there's no viewport.AppendContent(), you'll need a separate buffer as a middle-man. Append to that when content is updated, then write the whole thing to the viewport. You'll probably want viewport.GotoBottom() after that.

@polds
Copy link

polds commented Feb 21, 2023

I figured out how to do this, if anyone has any other ideas please let know:

package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
)

type Model struct {
	content  strings.Builder
	ready    bool
	viewport viewport.Model
}

func (m *Model) runCmd() tea.Msg {
	cmd := exec.Command("bash", "loop.sh")
	stdout, _ := cmd.StdoutPipe()

	if err := cmd.Start(); err != nil {
		return tea.Quit()
	}

	go func() {
		buf := make([]byte, 1024)
		for {
			n, err := stdout.Read(buf)
			if err != nil {
				break
			}
			m.content.Write(buf[:n])
		}
	}()

	return nil
}

func (m *Model) Init() tea.Cmd {
	return m.runCmd
}

func (m *Model) View() string {
	if !m.ready {
		return "Loading..."
	}
	return m.viewport.View()
}

func (m *Model) Update(msg tea.Msg) (tea.Model, 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 tickMsg:
		m.viewport.SetContent(m.content.String())
		m.viewport.GotoBottom()

	case tea.WindowSizeMsg:
		if !m.ready {
			m.viewport = viewport.New(msg.Width, msg.Height-10)
			m.viewport.YPosition = 5
			m.viewport.HighPerformanceRendering = false
			m.viewport.SetContent("Loading...")
			m.ready = true
			break
		}
		m.viewport.Width = msg.Width
		m.viewport.Height = msg.Height - 10
	}

	return m, nil
}

type tickMsg time.Time

func main() {
	cmd := tea.NewProgram(&Model{})
	go func() {
		for c := range time.Tick(300 * time.Millisecond) {
			cmd.Send(tickMsg(c))
		}
	}()

	if _, err := cmd.Run(); err != nil {
		fmt.Printf("Uh oh: %v\n", err)
		os.Exit(1)
	}
}

@jbcpollak
Copy link

I was hoping to do the same thing with my logs. I have concerns with using Viewport - I'm not sure how this is normally done but because SetContent() copies the entire string every call then replaces and splits every line, it just feed really inefficient. I'm not sure what the right methodology is but it would be great to wire an io.Writer directly to the viewport.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants