summaryrefslogtreecommitdiff
path: root/menu
diff options
context:
space:
mode:
authorewy <ewy0@protonmail.com>2026-04-29 00:47:34 +0200
committerewy <ewy0@protonmail.com>2026-04-29 00:47:34 +0200
commit630d77e1962b43ee95e88a664f5e8b8993213060 (patch)
tree8849c2a87443cd231786aed53b76dc7e4ea11aed /menu
parent8efcf029576ad82908b4ae80b2c92022dfb857d2 (diff)
big stuff
* send empty screen after tui confirmation * add scroll view / viewport * auto enable scroll view on short terminals * add motd tips * add subdirs as categories * add inline toggle hotkey (i)
Diffstat (limited to 'menu')
-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
7 files changed, 187 insertions, 81 deletions
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)))
+}