Skip to content

Commit

Permalink
Add command for arbitrary outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
Evertras committed Jan 27, 2024
1 parent d95c72f commit bc820f7
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 0 deletions.
13 changes: 13 additions & 0 deletions examples/command.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
layout:
screens:
- stack: vertical
children:
- name: Hostname
module: command
config:
bash: hostname
- name: Date
module: command
config:
interval: 5s
bash: date
69 changes: 69 additions & 0 deletions pkg/modules/command/captured/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package captured

import (
"context"
"fmt"
"os/exec"
)

type Cmd struct {
cmd *exec.Cmd
stdout *capturedOutput
stderr *capturedOutput
}

func New(ctx context.Context, command string, args ...string) *Cmd {
cmd := exec.CommandContext(ctx, command, args...)

c := &Cmd{
cmd: cmd,
stdout: newCapturedOutput(),
stderr: newCapturedOutput(),
}

cmd.Stdout = c.stdout
cmd.Stderr = c.stderr

return c
}

func (c *Cmd) Run() error {
err := c.cmd.Start()

if err != nil {
return fmt.Errorf("cmd.Start: %w", err)
}

return c.cmd.Wait()
}

func (r *Cmd) Stop() error {
if r == nil || r.cmd == nil {
return fmt.Errorf("cmd is nil")
}

err := r.cmd.Process.Kill()

if err != nil {
return fmt.Errorf("process.Kill: %w", err)
}

return nil
}

func (r *Cmd) Stdout() string {
output := r.stdout.String()

return output
}

func (r *Cmd) Stderr() string {
output := r.stderr.String()

return output
}

func (r *Cmd) ResetOutput() {
r.stdout.reset()
r.stderr.reset()
}
37 changes: 37 additions & 0 deletions pkg/modules/command/captured/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package captured

import "sync"

type capturedOutput struct {
sync.RWMutex
data []byte
}

func newCapturedOutput() *capturedOutput {
return &capturedOutput{
data: make([]byte, 0, 1024),
}
}

func (c *capturedOutput) Write(data []byte) (n int, err error) {
c.Lock()
defer c.Unlock()

c.data = append(c.data, data...)

return len(data), nil
}

func (c *capturedOutput) reset() {
c.Lock()
defer c.Unlock()

c.data = make([]byte, 0, 1024)
}

func (c *capturedOutput) String() string {
c.RLock()
defer c.RUnlock()

return string(c.data)
}
95 changes: 95 additions & 0 deletions pkg/modules/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package command

import (
"fmt"
"time"

tea "github.com/charmbracelet/bubbletea"

"github.com/evertras/yakdash/pkg/id"
"github.com/mitchellh/mapstructure"
)

type model struct {
id id.ID
config config

lastOutput string
}

func New(cfg interface{}) (model, error) {
var config config
err := mapstructure.Decode(cfg, &config)

if err != nil {
return model{}, fmt.Errorf("failed to decode config: %w", err)
}

config, err = config.setDefaultsAndParse()

if err != nil {
return model{}, fmt.Errorf("invalid config: %w", err)
}

m := model{
id: id.New(),
config: config,
}

return m, nil
}

type tickMsg struct {
id id.ID
}

func (m model) doTick() tea.Cmd {
// Tick on a regular interval so that all commands
// from different panes can try to update at the
// same time, if they use the same interval.
// The command will timeout before the next tick.
return tea.Batch(
tea.Tick(m.config.interval, func(time.Time) tea.Msg {
return tickMsg{m.id}
}),
m.runCommand(),
)
}

func (m model) Init() tea.Cmd {
return m.doTick()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmds []tea.Cmd
)

switch msg := msg.(type) {
case tickMsg:
if msg.id != m.id {
break
}

cmds = append(cmds, m.doTick())

case commandResult:
if msg.id != m.id {
break
}

if msg.err != nil {
m.lastOutput = fmt.Sprintf("Error: %s", msg.err)
break
}

m.lastOutput = msg.stdout

}

return m, tea.Batch(cmds...)
}

func (m model) View() string {
return m.lastOutput
}
32 changes: 32 additions & 0 deletions pkg/modules/command/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package command

import (
"fmt"
"time"
)

type config struct {
Bash string `mapstructure:"bash,omitempty"`
IntervalRaw string `mapstructure:"interval,omitempty"`

interval time.Duration
}

func (c config) setDefaultsAndParse() (config, error) {
if c.Bash == "" {
return c, fmt.Errorf("must supply a command to run")
}

if c.IntervalRaw == "" {
c.IntervalRaw = "10s"
}

var err error
c.interval, err = time.ParseDuration(c.IntervalRaw)

if err != nil {
return c, fmt.Errorf("failed to parse time %q: %w", c.IntervalRaw, err)
}

return c, nil
}
34 changes: 34 additions & 0 deletions pkg/modules/command/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package command

import (
"context"

tea "github.com/charmbracelet/bubbletea"

"github.com/evertras/yakdash/pkg/id"
"github.com/evertras/yakdash/pkg/modules/command/captured"
)

type commandResult struct {
id id.ID
stdout string
stderr string
err error
}

func (m model) runCommand() tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), m.config.interval)
defer cancel()
cmd := captured.New(ctx, "bash", "-c", m.config.Bash)

err := cmd.Run()

return commandResult{
id: m.id,
stdout: cmd.Stdout(),
stderr: cmd.Stderr(),
err: err,
}
}
}
4 changes: 4 additions & 0 deletions pkg/yakdash/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/evertras/yakdash/pkg/layout"
"github.com/evertras/yakdash/pkg/modules/clock"
"github.com/evertras/yakdash/pkg/modules/command"
"github.com/evertras/yakdash/pkg/modules/text"
)

Expand All @@ -13,6 +14,9 @@ func loadModule(l layout.Node) (tea.Model, error) {
case "clock":
return clock.New(l.Config)

case "command":
return command.New(l.Config)

default:
return text.New("Unknown module: " + l.Module), nil
}
Expand Down

0 comments on commit bc820f7

Please sign in to comment.