summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indexers/pikdex/index.go15
-rw-r--r--menu/git.go54
-rw-r--r--menu/input.go17
-rw-r--r--menu/menu.go4
-rw-r--r--menu/model.go50
-rw-r--r--menu/source.go78
-rw-r--r--menu/state.go29
-rw-r--r--menu/target.go36
-rw-r--r--motd/motd.go16
-rw-r--r--order/order.go50
-rw-r--r--runner/shell/shell.go6
-rw-r--r--runner/shell/target.go9
-rw-r--r--viewport/scroll.go46
-rw-r--r--viewport/viewport.go67
-rw-r--r--viewport/viewport_test.go54
15 files changed, 441 insertions, 90 deletions
diff --git a/indexers/pikdex/index.go b/indexers/pikdex/index.go
index aec6b00..64b111f 100644
--- a/indexers/pikdex/index.go
+++ b/indexers/pikdex/index.go
@@ -121,7 +121,20 @@ func (u *pikdex) Index(absPath string, f fs.FS, runners []model.Runner) ([]model
if err != nil {
spool.Warn("%v\n", err)
}
- targets = append(targets, t)
+ sub := t.Sub()
+ if strings.Join(sub, " ") == t.ShortestId() {
+ desiredIndex := 0
+ for i := len(targets) - 1; i >= 0; i-- {
+ if slices.Equal(targets[i].Sub(), sub) {
+ desiredIndex = i
+ } else {
+ break
+ }
+ }
+ targets = slices.Insert(targets, desiredIndex, t)
+ } else {
+ targets = append(targets, t)
+ }
return nil
}
if err != nil {
diff --git a/menu/git.go b/menu/git.go
new file mode 100644
index 0000000..ed5a595
--- /dev/null
+++ b/menu/git.go
@@ -0,0 +1,54 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+ "pik/model"
+ "strconv"
+)
+
+var (
+ GitColor = lipgloss.Color("4")
+ GitInfoStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Background(GitColor).Border(lipgloss.OuterHalfBlockBorder(), false, false, false, true).BorderBackground(GitColor).Padding(0, 1)
+ })
+ GitStatusStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Bold(true).Background(GitColor).PaddingLeft(1)
+ })
+ GitAddColor = lipgloss.Color("2")
+ GitAddStyle = style.New(func() lipgloss.Style {
+ return GitStatusStyle.Get().Foreground(GitAddColor)
+ })
+ GitRemoveColor = lipgloss.Color("1")
+ GitRemoveStyle = style.New(func() lipgloss.Style {
+ return GitStatusStyle.Get().Foreground(GitRemoveColor)
+ })
+ GitChangeColor = lipgloss.Color("5")
+ GitChangeStyle = style.New(func() lipgloss.Style {
+ return GitStatusStyle.Get().Foreground(GitChangeColor)
+ })
+)
+
+func Git(info *model.GitInfo) string {
+ var parts = []string{
+ " ",
+ info.Branch,
+ }
+
+ if info.Insertions > 0 {
+ parts = append(parts, GitAddStyle.Render("+"+strconv.Itoa(info.Insertions)))
+ }
+ if info.Deletions > 0 {
+ parts = append(parts, GitRemoveStyle.Render("-"+strconv.Itoa(info.Deletions)))
+ }
+ if info.Changes > 0 {
+ parts = append(parts, GitChangeStyle.Render("~"+strconv.Itoa(info.Changes)))
+ }
+ if info.Changes == 0 && info.Deletions == 0 && info.Insertions == 0 {
+ parts = append(parts, GitAddStyle.Render("clean"))
+ }
+
+ return GitInfoStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left,
+ parts...,
+ ))
+}
diff --git a/menu/input.go b/menu/input.go
index 6d05749..cd07543 100644
--- a/menu/input.go
+++ b/menu/input.go
@@ -5,6 +5,16 @@ import tea "github.com/charmbracelet/bubbletea"
func (m *Model) HandleInput(msg tea.KeyMsg) (tea.Cmd, error) {
var cmd tea.Cmd
switch msg.String() {
+ case "i", "I":
+ if m.Alt {
+ m.Alt = false
+ m.AutoAlt = false
+ return tea.ExitAltScreen, nil
+ } else {
+ m.Alt = true
+ m.AutoAlt = false
+ return tea.EnterAltScreen, nil
+ }
case "h", "left":
m.Leap(-1)
case "l", "right":
@@ -14,10 +24,11 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (tea.Cmd, error) {
case "down", "j":
m.Index++
case "q", "esc", "ctrl+c":
- m.Quit = true
- cmd = tea.Quit
+ m.Cancel = true
+ return tea.Quit, nil
case "space", " ", "enter", "ctrl+d":
- cmd = tea.Quit
+ m.Done = true
+ return tea.Quit, nil
}
m.Validate()
diff --git a/menu/menu.go b/menu/menu.go
index fdb1c6b..2677259 100644
--- a/menu/menu.go
+++ b/menu/menu.go
@@ -3,7 +3,6 @@ package menu
import (
"errors"
tea "github.com/charmbracelet/bubbletea"
- "pik/flags"
"pik/model"
"pik/spool"
)
@@ -17,9 +16,6 @@ func Show(st *model.State, hydrators []model.Modder) (*model.HydratedSource, mod
}
md := NewModel(st, hydrators)
var opts []tea.ProgramOption
- if !*flags.Inline {
- opts = append(opts, tea.WithAltScreen())
- }
program := tea.NewProgram(md, opts...)
resultModel, err := program.Run()
if err != nil {
diff --git a/menu/model.go b/menu/model.go
index 4eb2577..d1b9435 100644
--- a/menu/model.go
+++ b/menu/model.go
@@ -2,8 +2,12 @@ package menu
import (
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/x/term"
+ "github.com/spf13/pflag"
"pik/model"
+ "pik/motd"
"pik/spool"
+ "pik/viewport"
)
type Model struct {
@@ -11,24 +15,55 @@ type Model struct {
Index int
Indices map[int]model.HydratedTarget
SourceIndices map[int]*model.HydratedSource
- Quit bool
+ Cancel bool
+ Done bool
+ Height int
+ Alt bool
+ AutoAlt bool
+ Motd string
}
func (m *Model) Init() tea.Cmd {
+ _, h, err := term.GetSize(0)
+ if err != nil {
+ _, _ = spool.Warn("%v\n", err)
+ }
+ m.Height = h
+ wantsAlt := viewport.NeedsViewport(m.State(), m.Height)
+ if m.AutoAlt && wantsAlt {
+ return tea.EnterAltScreen
+ }
return nil
}
+func (m *Model) HandleResize(msg tea.WindowSizeMsg) tea.Cmd {
+ if !m.AutoAlt {
+ return nil
+ }
+ m.Height = msg.Height
+ if viewport.NeedsViewport(m.State(), msg.Height) {
+ m.Alt = true
+ return tea.EnterAltScreen
+ } else {
+ m.Alt = false
+ return tea.ExitAltScreen
+ }
+
+}
+
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var err error
var result tea.Cmd
switch mt := msg.(type) {
+ case tea.WindowSizeMsg:
+ result = m.HandleResize(mt)
case tea.KeyMsg:
result, err = m.HandleInput(mt)
case tea.Cmd:
result, err = m.HandleSignal(mt)
}
if err != nil {
- spool.Warn("%v\n", err)
+ _, _ = spool.Warn("%v\n", err)
}
return m, result
}
@@ -38,11 +73,16 @@ func (m *Model) HandleSignal(cmd tea.Cmd) (tea.Cmd, error) {
}
func (m *Model) View() string {
- return m.State(m.HydratedState)
+ if m.Cancel || m.Done {
+ return ""
+ }
+ result := m.State()
+ result = viewport.Process(result, m.Height)
+ return result
}
func (m *Model) Result() (*model.HydratedSource, model.HydratedTarget) {
- if m.Quit {
+ if m.Cancel {
return nil, nil
}
return m.SourceIndices[m.Index], m.Indices[m.Index]
@@ -63,6 +103,8 @@ func NewModel(st *model.State, hydrators []model.Modder) *Model {
Index: 0,
Indices: make(map[int]model.HydratedTarget),
SourceIndices: make(map[int]*model.HydratedSource),
+ AutoAlt: !pflag.Lookup("inline").Changed,
+ Motd: motd.One(),
}
idx := 0
for _, src := range st.Sources {
diff --git a/menu/source.go b/menu/source.go
index b5d462b..073c044 100644
--- a/menu/source.go
+++ b/menu/source.go
@@ -4,12 +4,13 @@ import (
"github.com/charmbracelet/lipgloss"
"pik/menu/style"
"pik/model"
- "strconv"
+ "slices"
+ "strings"
)
var (
SourceStyle = style.New(func() lipgloss.Style {
- st := lipgloss.NewStyle().PaddingBottom(1)
+ st := lipgloss.NewStyle()
return st
})
SourceHeaderBackground = lipgloss.Color("5")
@@ -35,8 +36,18 @@ var (
func (m *Model) Source(src *model.HydratedSource) string {
targets := make([]string, 0, len(src.Targets))
+ var sub []string
for _, t := range src.HydratedTargets {
- targets = append(targets, m.Target(t))
+ ts := t.Sub()
+ header := !slices.Equal(sub, ts)
+ if header {
+ sub = ts
+ }
+ if header && strings.Join(ts, " ") != t.ShortestId() {
+ targets = append(targets, m.Category(strings.Join(ts, " "), ""))
+ header = false
+ }
+ targets = append(targets, m.Target(t, header))
}
targetContent := lipgloss.JoinVertical(lipgloss.Top, targets...)
@@ -56,64 +67,3 @@ func (m *Model) Source(src *model.HydratedSource) string {
parts...,
))
}
-
-var (
- StateStyle = style.New(func() lipgloss.Style {
- return lipgloss.NewStyle().MarginBottom(1)
- })
-)
-
-func (m *Model) State(st *model.HydratedState) string {
- sources := make([]string, 0, len(st.Sources))
- for _, hs := range st.HydratedSources {
- sources = append(sources, m.Source(hs))
- }
-
- return StateStyle.Render(lipgloss.JoinVertical(lipgloss.Top, sources...))
-}
-
-var (
- GitColor = lipgloss.Color("4")
- GitInfoStyle = style.New(func() lipgloss.Style {
- return lipgloss.NewStyle().Background(GitColor).Border(lipgloss.OuterHalfBlockBorder(), false, false, false, true).BorderBackground(GitColor).Padding(0, 1)
- })
- GitStatusStyle = style.New(func() lipgloss.Style {
- return lipgloss.NewStyle().Bold(true).Background(GitColor).PaddingLeft(1)
- })
- GitAddColor = lipgloss.Color("2")
- GitAddStyle = style.New(func() lipgloss.Style {
- return GitStatusStyle.Get().Foreground(GitAddColor)
- })
- GitRemoveColor = lipgloss.Color("1")
- GitRemoveStyle = style.New(func() lipgloss.Style {
- return GitStatusStyle.Get().Foreground(GitRemoveColor)
- })
- GitChangeColor = lipgloss.Color("5")
- GitChangeStyle = style.New(func() lipgloss.Style {
- return GitStatusStyle.Get().Foreground(GitChangeColor)
- })
-)
-
-func Git(info *model.GitInfo) string {
- var parts = []string{
- " ",
- info.Branch,
- }
-
- if info.Insertions > 0 {
- parts = append(parts, GitAddStyle.Render("+"+strconv.Itoa(info.Insertions)))
- }
- if info.Deletions > 0 {
- parts = append(parts, GitRemoveStyle.Render("-"+strconv.Itoa(info.Deletions)))
- }
- if info.Changes > 0 {
- parts = append(parts, GitChangeStyle.Render("~"+strconv.Itoa(info.Changes)))
- }
- if info.Changes == 0 && info.Deletions == 0 && info.Insertions == 0 {
- parts = append(parts, GitAddStyle.Render("clean"))
- }
-
- return GitInfoStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left,
- parts...,
- ))
-}
diff --git a/menu/state.go b/menu/state.go
new file mode 100644
index 0000000..16702a2
--- /dev/null
+++ b/menu/state.go
@@ -0,0 +1,29 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+)
+
+var (
+ StateStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().MarginBottom(1)
+ })
+ MotdStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Faint(true).PaddingLeft(1)
+ })
+)
+
+func (m *Model) State() string {
+ st := m.HydratedState
+ sources := make([]string, 0, len(st.Sources))
+ for i, hs := range st.HydratedSources {
+ src := m.Source(hs)
+ if i != len(st.HydratedSources)-1 {
+ src += "\n"
+ }
+ sources = append(sources, src)
+ }
+
+ return StateStyle.Render(lipgloss.JoinVertical(lipgloss.Top, sources...), MotdStyle.Render("\n \U000F08B7 "+m.Motd))
+}
diff --git a/menu/target.go b/menu/target.go
index 04dfea1..6d6fdec 100644
--- a/menu/target.go
+++ b/menu/target.go
@@ -4,6 +4,7 @@ import (
"github.com/charmbracelet/lipgloss"
"pik/menu/style"
"pik/model"
+ "pik/viewport"
)
var (
@@ -32,25 +33,48 @@ var (
st := lipgloss.NewStyle().PaddingLeft(1)
return st
})
+ TargetIconSelectedStyle = style.New(func() lipgloss.Style {
+ return TargetIconStyle.Get().MarginLeft(1).PaddingLeft(0)
+ })
TargetSubStyle = style.New(func() lipgloss.Style {
return lipgloss.NewStyle()
})
+ CategoryColor = lipgloss.Color("7")
+ CategoryStyle = style.New(func() lipgloss.Style {
+ return TargetStyle.Get().BorderForeground(CategoryColor).Background(CategoryColor).BorderBackground(CategoryColor)
+ })
+ OrphanCategoryStyle = style.New(func() lipgloss.Style {
+ return TargetLabelStyle.Get().PaddingLeft(1)
+ })
)
-func (m *Model) Target(t model.HydratedTarget) string {
- icon := TargetIconStyle.Render(PaddedIcon(t.Icon()))
+func (m *Model) Target(t model.HydratedTarget, header bool) string {
+ _, selection := m.Result()
+ selected := selection != nil && selection.Target() == t.Target()
+ icon := ""
+ if selected {
+ icon = TargetIconSelectedStyle.Render(PaddedIcon(viewport.Caret))
+ } else {
+ icon = TargetIconStyle.Render(PaddedIcon(t.Icon()))
+ }
selectionStyle := TargetStyle
selectionDescriptionStyle := TargetDescriptionStyle
- _, sel := m.Result()
- if sel != nil && sel.Target() == t.Target() {
+ if selected {
selectionStyle = SelectedTargetStyle
selectionDescriptionStyle = SelectedTargetDescriptionStyle
+ } else if header {
+ selectionStyle = CategoryStyle
}
var labelParts []string
labelParts = append(labelParts, icon)
- if t.Sub() != nil {
- labelParts = append(labelParts, TargetSubStyle.Render(t.Sub()...))
+ sub := t.Sub()
+ if sub != nil && sub[len(sub)-1] != t.ShortestId() {
+ labelParts = append(labelParts, TargetSubStyle.Render(sub...))
}
labelParts = append(labelParts, TargetLabelStyle.Render(t.Label()))
return lipgloss.JoinHorizontal(lipgloss.Left, selectionStyle.Render(labelParts...), selectionDescriptionStyle.Render(t.Description()))
}
+
+func (m *Model) Category(input string, desc string) string {
+ return CategoryStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, OrphanCategoryStyle.Render(input), TargetDescriptionStyle.Render(desc)))
+}
diff --git a/motd/motd.go b/motd/motd.go
new file mode 100644
index 0000000..e57e8b0
--- /dev/null
+++ b/motd/motd.go
@@ -0,0 +1,16 @@
+package motd
+
+import "math/rand/v2"
+
+var Messages = []string{
+ "use the -a flag to invoke from anywhere",
+ "combine -a with -y to invoke from anywhere without confirming",
+ "`--env dev` will include .env, .env.dev, .env-dev, and dev.env",
+ "pik will start in a viewport if the terminal is too thin",
+ "instead of .pik you can use .tasks, or .bin, or any of these with _",
+}
+
+func One() string {
+ idx := rand.Int() % len(Messages)
+ return Messages[idx]
+}
diff --git a/order/order.go b/order/order.go
new file mode 100644
index 0000000..40e6e73
--- /dev/null
+++ b/order/order.go
@@ -0,0 +1,50 @@
+package order
+
+import (
+ "bufio"
+ "io"
+ "io/fs"
+ "os"
+ "pik/describe"
+ "pik/identity"
+ "strings"
+)
+
+type Element struct {
+ Identifier identity.Identity
+ Description string
+}
+
+type Order struct {
+ Elements []Element
+}
+
+var Empty = Order{}
+
+func FromFile(f fs.FS, path string) (Order, error) {
+ fd, err := os.Open(path)
+ if err != nil {
+ return Empty, err
+ }
+ defer fd.Close()
+ return FromReader(fd)
+}
+
+func FromReader(r io.Reader) (Order, error) {
+ scanner := bufio.NewScanner(r)
+ scanner.Split(bufio.ScanLines)
+ for scanner.Scan() {
+ line := scanner.Text()
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+
+ for _, p := range describe.DescriptionPrefixes {
+ if strings.HasPrefix(line, p) {
+ continue
+ }
+ }
+ }
+
+}
diff --git a/runner/shell/shell.go b/runner/shell/shell.go
index a4aee9d..f8e353c 100644
--- a/runner/shell/shell.go
+++ b/runner/shell/shell.go
@@ -99,10 +99,10 @@ func (s *shell) CreateTarget(fs fs.FS, src string, file string, _ fs.DirEntry) (
BaseTarget: runner.BaseTarget{
Identity: identity.New(filename),
MyTags: model.TagsFromFilename(filename),
+ Sub: sub,
},
- Shell: shell,
- Script: filepath.Join(src, file),
- SubValue: sub,
+ Shell: shell,
+ Script: filepath.Join(src, file),
}, nil
}
diff --git a/runner/shell/target.go b/runner/shell/target.go
index 0194b5d..8422cf0 100644
--- a/runner/shell/target.go
+++ b/runner/shell/target.go
@@ -8,9 +8,8 @@ import (
type Target struct {
runner.BaseTarget
- Shell string
- Script string
- SubValue []string
+ Shell string
+ Script string
}
func (s *Target) String() string {
@@ -22,13 +21,13 @@ func (s *Target) Hydrate(_ *model.Source) (model.HydratedTarget, error) {
}
func (s *Target) Sub() []string {
- return s.SubValue
+ return s.BaseTarget.Sub
}
func (s *Target) Label() string {
return s.Identity.Full
}
-func (s *Target) Create(src *model.Source) *exec.Cmd {
+func (s *Target) Create(_ *model.Source) *exec.Cmd {
return exec.Command(s.Shell, s.Script)
}
diff --git a/viewport/scroll.go b/viewport/scroll.go
new file mode 100644
index 0000000..809fe17
--- /dev/null
+++ b/viewport/scroll.go
@@ -0,0 +1,46 @@
+package viewport
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+ "strings"
+)
+
+var (
+ ScrollTop = StyleBarBackground.Render("╷")
+ ScrollSpace = StyleBarBackground.Render("│")
+ ScrollBarTopEnd = StyleBar.Render("╓")
+ ScrollBar = StyleBar.Render("║")
+ ScrollBarBottomEnd = StyleBar.Render("╙")
+ ScrollBottom = StyleBarBackground.Render("╵")
+)
+
+var (
+ StyleBar = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ StyleBarBackground = style.New(func() lipgloss.Style {
+ return StyleBar.Get().Faint(true)
+ })
+)
+
+func WithScroll(input string, barBegin int, barEnd int) string {
+ lines := strings.Split(input, "\n")
+ for i, line := range lines {
+ selection := ScrollSpace
+ switch {
+ case i == barBegin:
+ selection = StyleBar.Render(ScrollBarTopEnd)
+ case i == 0:
+ selection = StyleBarBackground.Render(ScrollTop)
+ case i == barEnd:
+ selection = StyleBar.Render(ScrollBarBottomEnd)
+ case i > barBegin && i < barEnd:
+ selection = StyleBar.Render(ScrollBar)
+ case i == len(lines)-1:
+ selection = StyleBar.Render(ScrollBottom)
+ }
+ lines[i] = selection + " " + line
+ }
+ return strings.Join(lines, "\n")
+}
diff --git a/viewport/viewport.go b/viewport/viewport.go
new file mode 100644
index 0000000..ff1ca05
--- /dev/null
+++ b/viewport/viewport.go
@@ -0,0 +1,67 @@
+package viewport
+
+import (
+ "strings"
+)
+
+const Caret = "⏵"
+
+func NeedsViewport(input string, height int) bool {
+ lines := strings.Split(input, "\n")
+ return len(lines)-1 > height
+}
+
+func Process(input string, height int) string {
+ lines := strings.Split(input, "\n")
+ if len(lines) > height {
+ cropped, top, bottom := Crop(input, lines, height)
+ return WithScroll(cropped, int(top*float32(height)), int(bottom*float32(height)))
+ }
+ return input
+}
+
+func Focus(lines []string, needle string) int {
+ for i, l := range lines {
+ if strings.Contains(l, needle) {
+ return i
+ }
+ }
+ return -1
+}
+
+func Crop(input string, lines []string, height int) (output string, scrollStart float32, scrollEnd float32) {
+ output = input
+ selectionIndex := Focus(lines, Caret)
+ size := len(lines)
+ if size <= height {
+ return output, 0, 1
+ }
+
+ linesAbove := height / 2
+ linesBelow := height - linesAbove
+ if linesAbove*2 < selectionIndex {
+ linesBelow++
+ }
+
+ start := selectionIndex - linesAbove
+ end := selectionIndex + linesBelow
+
+ if start < 0 {
+ end += -start
+ start = 0
+ }
+
+ if end >= size {
+ diff := size - 1 - end
+ start += diff
+ end += diff
+ }
+
+ scrollStart = float32(start) / float32(size)
+ scrollEnd = float32(end)/float32(size) + float32(1)/float32(size)
+ if scrollEnd > 1 {
+ scrollEnd = 1
+ }
+
+ return strings.Join(lines[start:end], "\n"), scrollStart, scrollEnd
+}
diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go
new file mode 100644
index 0000000..9da4c3c
--- /dev/null
+++ b/viewport/viewport_test.go
@@ -0,0 +1,54 @@
+//go:build test
+
+package viewport
+
+import (
+ "github.com/stretchr/testify/assert"
+ "strings"
+ "testing"
+)
+
+func TestCrop(t *testing.T) {
+ input := `0000
+AAAA
+` + Caret + `BBB
+CCC`
+ expected := `AAAA
+` + Caret + `BBB`
+ result, _, _ := Crop(input, strings.Split(input, "\n"), 2)
+ assert.Equal(t, expected, result)
+}
+
+func TestCrop_Under(t *testing.T) {
+ input := `0000
+AAAA
+` + Caret + `BBB`
+ expected := `AAAA
+` + Caret + `BBB`
+ result, _, _ := Crop(input, strings.Split(input, "\n"), 2)
+ assert.Equal(t, expected, result)
+}
+
+func TestCrop_Unnecessary(t *testing.T) {
+ input := `AAAA
+` + Caret + `BBB
+CCC
+DDDD`
+ expected := input
+ result, _, _ := Crop(input, strings.Split(input, "\n"), 8)
+ assert.Equal(t, expected, result)
+}
+
+func TestNeedsViewport(t *testing.T) {
+ amount := 3
+ input := strings.Repeat("\n", amount)
+ output := NeedsViewport(input, 4)
+ assert.Equal(t, false, output)
+}
+
+func TestNeedsViewport_Negative(t *testing.T) {
+ amount := 8
+ input := strings.Repeat("\n", amount)
+ output := NeedsViewport(input, 4)
+ assert.Equal(t, true, output)
+}