Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sift is a lightweight terminal UI for displaying Go test results. It allows deve
## Installation

```bash
go install github.com/timtatt/sift@v0.11.0
go install github.com/timtatt/sift@v0.12.1
```

## Try it out!
Expand All @@ -23,18 +23,18 @@ You can try a demo of sift with the sample tests provided in the `samples` folde
git clone github.com/timtatt/sift.git

# Run sift
go test ./samples/... -v -json | sift
go test ./samples/... -json | sift
```

## Usage

`sift` works by consuming the verbose json output from the `go test` command. The easiest way to use it is to pipe `|` the output straight into `sift`

```bash
go test {your-go-package} -v -json | sift
go test {your-go-package} -json | sift

# eg.
go test ./... -v -json | sift
go test ./... -json | sift
```

## Demo (v0.9.0)
Expand All @@ -53,13 +53,13 @@ go test ./... -v -json | sift

```bash
# Run in non-interactive mode (inline output)
go test ./... -v -json | sift -n
go test ./... -json | sift -n

# Enable debug view
go test ./... -v -json | sift --debug
go test ./... -json | sift --debug

# Disable log prettification
go test ./... -v -json | sift --raw
go test ./... -json | sift --raw
```

### Keymaps
Expand Down
3 changes: 2 additions & 1 deletion cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ func (c *CLI) Run() error {
os.Exit(0)
}

return sift.Run(ctx, sift.SiftOptions{
return sift.Run(ctx, sift.ProgramOptions{
Debug: c.Debug,
NonInteractive: c.NonInteractive,
PrettifyLogs: !c.RawLogs,
})

}
7 changes: 6 additions & 1 deletion internal/sift/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type keyMap struct {
ClearSearch key.Binding
Help key.Binding
Quit key.Binding
ForceQuit key.Binding
ChangeMode key.Binding
}

Expand Down Expand Up @@ -131,8 +132,12 @@ var (
key.WithHelp("?", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithKeys("q"),
key.WithHelp("q", "quit"),
),
ForceQuit: key.NewBinding(
key.WithKeys("ctrl+c"),
key.WithHelp("ctrl+c", "force quit"),
),
}
)
75 changes: 31 additions & 44 deletions internal/sift/sift.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@
package sift

import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/timtatt/sift/internal/tests"
"golang.org/x/sync/errgroup"
)

type sift struct {
program *tea.Program
model *siftModel
}

func (s *sift) ScanStdin() error {
scanner := bufio.NewScanner(os.Stdin)

for scanner.Scan() {
var line tests.TestOutputLine

err := json.Unmarshal(scanner.Bytes(), &line)
if err != nil {
// TODO: write to a temp dir log
return errors.New("unable to parse json input. ensure to use the `-json` flag when running go tests")
}

s.model.testManager.AddTestOutput(line)
}

if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to scan stdin: %w", err)
// IsStdinTerminal checks if stdin is a terminal (no piped input)
func IsStdinTerminal() bool {
stat, err := os.Stdin.Stat()
if err != nil {
return false
}

s.model.endTime = time.Now()

return nil
return (stat.Mode() & os.ModeCharDevice) != 0
}

type FrameMsg struct{}

// sends a msg to bubbletea model on an interval to ensure the view is being updated according to framerate
func (s *sift) Frame(ctx context.Context, tps int) {
func FrameTicker(ctx context.Context, program *tea.Program, tps int) {
tick := time.NewTicker(time.Second / time.Duration(tps))
defer tick.Stop()

Expand All @@ -56,12 +35,12 @@ func (s *sift) Frame(ctx context.Context, tps int) {
case <-ctx.Done():
return
case <-tick.C:
s.program.Send(FrameMsg{})
program.Send(FrameMsg{})
}
}
}

type SiftOptions struct {
type ProgramOptions struct {
Debug bool
NonInteractive bool
PrettifyLogs bool
Expand All @@ -83,7 +62,7 @@ func initLogging() error {
return nil
}

func Run(ctx context.Context, opts SiftOptions) error {
func Run(ctx context.Context, opts ProgramOptions) error {

if opts.Debug {
if err := initLogging(); err != nil {
Expand All @@ -92,14 +71,25 @@ func Run(ctx context.Context, opts SiftOptions) error {
slog.DebugContext(ctx, "starting sift", "options", opts)
}

ctx, cancel := context.WithCancel(ctx)
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()

fps := 120

g, ctx := errgroup.WithContext(ctx)

m := NewSiftModel(opts)
testManager := tests.NewTestManager(tests.TestManagerOpts{
ParseLogs: opts.PrettifyLogs,
})

m, err := NewSiftModel(SiftModelOptions{
ProgramOptions: opts,
TestManager: testManager,
})

if err != nil {
return fmt.Errorf("unable to create sift model: %w", err)
}

programOpts := []tea.ProgramOption{
tea.WithFPS(fps),
Expand All @@ -112,16 +102,13 @@ func Run(ctx context.Context, opts SiftOptions) error {

p := tea.NewProgram(m, programOpts...)

sift := &sift{
model: m,
program: p,
}

g.Go(func() error {
if err := sift.ScanStdin(); err != nil {
if err := testManager.ScanStdin(ctx, os.Stdin); err != nil {
return err
}

m.endTime = time.Now()

return nil
})

Expand All @@ -135,17 +122,17 @@ func Run(ctx context.Context, opts SiftOptions) error {
})

g.Go(func() error {
sift.Frame(ctx, fps)
FrameTicker(ctx, p, fps)

return nil
})

err := g.Wait()
err = g.Wait()
if err != nil {
return err
}

m.quitting = false
fmt.Print(m.View())
fmt.Println(m.View())
return nil
}
12 changes: 12 additions & 0 deletions internal/sift/sift_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sift

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsStdinTerminal(t *testing.T) {
result := IsStdinTerminal()
assert.IsType(t, true, result, "IsStdinTerminal should return a boolean")
}
2 changes: 1 addition & 1 deletion internal/sift/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var (
colorGreen = lipgloss.AdaptiveColor{
Light: "#2D7F1E",
Dark: "#5FD700",
Dark: "#4b9c09",
}
colorRed = lipgloss.AdaptiveColor{
Light: "#C41E3A",
Expand Down
2 changes: 1 addition & 1 deletion internal/sift/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package sift

var Version = "v0.12.1"
var Version = "v0.12.2"
32 changes: 25 additions & 7 deletions internal/sift/view.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sift

import (
"errors"
"strings"
"time"

Expand All @@ -27,7 +28,7 @@ const (
)

type siftModel struct {
opts SiftOptions
opts ProgramOptions

testManager *tests.TestManager
testState map[tests.TestReference]*testState
Expand Down Expand Up @@ -61,7 +62,17 @@ type cursor struct {
log int // tracks the cursor log line
}

func NewSiftModel(opts SiftOptions) *siftModel {
type SiftModelOptions struct {
ProgramOptions
TestManager *tests.TestManager
}

func NewSiftModel(opts SiftModelOptions) (*siftModel, error) {

if opts.TestManager == nil {
return nil, errors.New("missing test manager")
}

ti := textinput.New()
ti.Placeholder = "search for tests"
ti.PlaceholderStyle = styleSecondary
Expand All @@ -74,10 +85,8 @@ func NewSiftModel(opts SiftOptions) *siftModel {
}

return &siftModel{
opts: opts,
testManager: tests.NewTestManager(tests.TestManagerOpts{
ParseLogs: opts.PrettifyLogs,
}),
opts: opts.ProgramOptions,
testManager: opts.TestManager,
testState: make(map[tests.TestReference]*testState),
autoToggleMode: false,
compileSpinner: spinner.New(spinner.WithSpinner(spinner.Dot)),
Expand All @@ -89,7 +98,7 @@ func NewSiftModel(opts SiftOptions) *siftModel {
},
searchInput: ti,
mode: mode,
}
}, nil
}

// normalizeSearchQuery removes spaces from the search query since Go replaces
Expand Down Expand Up @@ -452,6 +461,9 @@ func (m *siftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

if m.searchInput.Focused() {
switch {
case msg.String() == "ctrl+c":
m.quitting = true
return m, tea.Quit
case msg.String() == "esc":
// Exit search mode and clear query
m.searchInput.Blur()
Expand Down Expand Up @@ -575,6 +587,12 @@ func (m *siftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

case key.Matches(msg, keys.Help):
m.help.ShowAll = !m.help.ShowAll
case key.Matches(msg, keys.ForceQuit):
// ensure running inline mode for the final print
m.mode = viewModeInline
m.quitting = true

return m, nil
case key.Matches(msg, keys.Quit):
if m.mode == viewModeAlternate {
m.mode = viewModeInline
Expand Down
18 changes: 16 additions & 2 deletions internal/sift/view_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package sift

import (
"fmt"
"testing"

"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/timtatt/sift/internal/tests"
)

Expand Down Expand Up @@ -800,7 +802,14 @@ type testModelOpts struct {
}

func createTestModel(opts testModelOpts) *siftModel {
m := NewSiftModel(SiftOptions{})
m, err := NewSiftModel(SiftModelOptions{
TestManager: tests.NewTestManager(tests.TestManagerOpts{}),
})

if err != nil {
fmt.Println(err)
}

m.autoToggleMode = opts.autoToggleMode

testCount := opts.testCount
Expand Down Expand Up @@ -947,7 +956,12 @@ func TestIsTestVisible_SpaceHandling(t *testing.T) {

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
m := NewSiftModel(SiftOptions{})
m, err := NewSiftModel(SiftModelOptions{
TestManager: tests.NewTestManager(tests.TestManagerOpts{}),
})

require.NoError(t, err)

testRef := tests.TestReference{
Package: "test/package",
Test: tt.testName,
Expand Down
Loading