summaryrefslogtreecommitdiff
path: root/menu
diff options
context:
space:
mode:
Diffstat (limited to 'menu')
-rw-r--r--menu/banner.go92
-rw-r--r--menu/confirm.go43
-rw-r--r--menu/icon.go28
-rw-r--r--menu/menu.go47
-rw-r--r--menu/model.go101
-rw-r--r--menu/source.go70
-rw-r--r--menu/style/style.go30
-rw-r--r--menu/target.go56
8 files changed, 467 insertions, 0 deletions
diff --git a/menu/banner.go b/menu/banner.go
new file mode 100644
index 0000000..c9d8e56
--- /dev/null
+++ b/menu/banner.go
@@ -0,0 +1,92 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "os/exec"
+ "pik/menu/style"
+ "pik/model"
+ "pik/paths"
+ "strings"
+)
+
+var (
+ BannerStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ BannerSourceLabelStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Faint(true).MarginRight(1)
+ })
+ BannerSubItemStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Faint(true).MarginRight(1)
+ })
+ BannerSubStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ BannerSelfStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().MarginRight(1)
+ })
+ BannerPromptStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ BannerArgsStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().MarginLeft(1)
+ })
+ BannerArgStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ BannerTerminatorColor = lipgloss.Color("1")
+ BannerTerminatorStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Faint(true).Foreground(BannerTerminatorColor)
+ })
+)
+
+func Banner(source *model.Source, target model.Target, args ...string) string {
+ var parts, argParts []string
+ parts = append(parts, BannerPromptStyle.Render("> "))
+ parts = append(parts, BannerSelfStyle.Render("pik"))
+ parts = append(parts, BannerSourceLabelStyle.Render(source.Label()))
+ if sub := target.Sub(); sub != nil {
+ for i, s := range sub {
+ sub[i] = BannerSubItemStyle.Render(s)
+ }
+ parts = append(parts, BannerSubStyle.Render(sub...))
+ }
+ parts = append(parts, target.ShortestId())
+ if args != nil {
+ needsTerminator := false
+ for _, a := range args {
+ if strings.HasPrefix(a, "-") {
+ needsTerminator = true
+ }
+ argParts = append(argParts, BannerArgStyle.Render(a))
+ }
+
+ if needsTerminator {
+ argParts = append([]string{BannerTerminatorStyle.Render("--")}, argParts...)
+ }
+
+ parts = append(parts, BannerArgsStyle.Render(argParts...))
+ }
+ result := BannerStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, parts...))
+ return result
+}
+
+var (
+ CmdStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Faint(true)
+ })
+ CmdDirStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+ CmdArgStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+)
+
+func InlineCmd(cmd *exec.Cmd) string {
+ var args []string
+ for _, a := range cmd.Args {
+ args = append(args, paths.ReplaceHome(a))
+ }
+ return CmdStyle.Render(" # "+CmdDirStyle.Render(paths.ReplaceHome(cmd.Dir)+":"), CmdArgStyle.Render(args...))
+}
diff --git a/menu/confirm.go b/menu/confirm.go
new file mode 100644
index 0000000..b9063c9
--- /dev/null
+++ b/menu/confirm.go
@@ -0,0 +1,43 @@
+package menu
+
+import (
+ "bufio"
+ "fmt"
+ "github.com/charmbracelet/lipgloss"
+ "io"
+ "os"
+ "pik/menu/style"
+ "pik/model"
+ "slices"
+)
+
+var confirmations = []rune{
+ 'y',
+ 'Y',
+ ' ',
+ '\n',
+}
+
+var (
+ PromptColor = lipgloss.Color("1")
+ PromptStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle().Foreground(PromptColor)
+ })
+ ConfirmStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+)
+
+func Confirm(r io.Reader, source *model.Source, target model.Target, args ...string) bool {
+ banner := ConfirmStyle.Render(PromptStyle.Render("[Y/n]"), Banner(source, target, args...), "? ")
+ fmt.Print(banner)
+ scanner := bufio.NewScanner(r)
+ scanner.Split(bufio.ScanRunes)
+ scanner.Scan()
+ if slices.Contains(confirmations, []rune(scanner.Text())[0]) {
+ return true
+ } else {
+ _, _ = fmt.Fprint(os.Stderr, "confirmation was not given.")
+ }
+ return false
+}
diff --git a/menu/icon.go b/menu/icon.go
new file mode 100644
index 0000000..ec2135f
--- /dev/null
+++ b/menu/icon.go
@@ -0,0 +1,28 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+ "strings"
+)
+
+var (
+ IconStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().Width(2).Height(1)
+ return st
+ })
+)
+
+func Icon(input string) string {
+ if strings.TrimSpace(input) == "" {
+ return ""
+ }
+ return IconStyle.Render(input)
+}
+
+func PaddedIcon(input string) string {
+ if strings.TrimSpace(input) == "" {
+ return Icon(" ")
+ }
+ return Icon(input)
+}
diff --git a/menu/menu.go b/menu/menu.go
new file mode 100644
index 0000000..232a837
--- /dev/null
+++ b/menu/menu.go
@@ -0,0 +1,47 @@
+package menu
+
+import (
+ "errors"
+ tea "github.com/charmbracelet/bubbletea"
+ "pik/model"
+ "pik/spool"
+)
+
+var WrongModelTypeError = errors.New("wrong model type")
+
+func Show(st *model.State, hydrators []model.Hydrator) (*model.HydratedSource, model.HydratedTarget, error) {
+ md := NewModel(st, hydrators)
+ program := tea.NewProgram(md)
+ resultModel, err := program.Run()
+ if err != nil {
+ return nil, nil, err
+ }
+ result, ok := resultModel.(*Model)
+ if !ok {
+ return nil, nil, WrongModelTypeError
+ }
+
+ src, t := result.Result()
+ return src, t, nil
+}
+
+func Hydrate(st *model.State, hydrators []model.Hydrator) *model.HydratedState {
+ hyd := &model.HydratedState{
+ State: st,
+ HydratedSources: make([]*model.HydratedSource, len(st.Sources)),
+ }
+ for i, s := range st.Sources {
+ hydSrc := s.Hydrate(hydrators)
+
+ for _, h := range hydrators {
+ err := h.Hydrate(s, hydSrc)
+ if err != nil {
+ spool.Warn("%v\n", err)
+ continue
+ }
+ }
+
+ hyd.HydratedSources[i] = hydSrc
+ }
+ return hyd
+}
diff --git a/menu/model.go b/menu/model.go
new file mode 100644
index 0000000..ceeb861
--- /dev/null
+++ b/menu/model.go
@@ -0,0 +1,101 @@
+package menu
+
+import (
+ tea "github.com/charmbracelet/bubbletea"
+ "pik/model"
+ "pik/spool"
+)
+
+type Model struct {
+ *model.HydratedState
+ Index int
+ Indices map[int]model.HydratedTarget
+ SourceIndices map[int]*model.HydratedSource
+}
+
+func (m *Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var err error
+ var result tea.Cmd
+ switch mt := msg.(type) {
+ case tea.KeyMsg:
+ result, err = m.HandleInput(mt)
+ case tea.Cmd:
+ result, err = m.HandleSignal(mt)
+ }
+ if err != nil {
+ spool.Warn("%v\n", err)
+ }
+ return m, result
+}
+
+func (m *Model) HandleInput(msg tea.KeyMsg) (tea.Cmd, error) {
+ var cmd tea.Cmd
+ switch msg.String() {
+ case "up", "k":
+ m.Index--
+ case "down", "j":
+ m.Index++
+ case "q", "esc":
+ cmd = tea.Quit
+ case "space", " ", "enter":
+ cmd = tea.Quit
+ }
+
+ m.Validate()
+
+ return cmd, nil
+}
+
+func (m *Model) HandleSignal(cmd tea.Cmd) (tea.Cmd, error) {
+ return nil, nil
+}
+
+func (m *Model) View() string {
+ return m.State(m.HydratedState)
+}
+
+func (m *Model) Result() (*model.HydratedSource, model.HydratedTarget) {
+ return m.SourceIndices[m.Index], m.Indices[m.Index]
+}
+
+func (m *Model) Validate() {
+ if m.Index < 0 {
+ m.Index = 0
+ }
+ if m.Index > len(m.Indices)-1 {
+ m.Index = len(m.Indices) - 1
+ }
+}
+
+func NewModel(st *model.State, hydrators []model.Hydrator) *Model {
+ m := &Model{
+ HydratedState: Hydrate(st, hydrators),
+ Index: 0,
+ Indices: make(map[int]model.HydratedTarget),
+ SourceIndices: make(map[int]*model.HydratedSource),
+ }
+ idx := 0
+ for _, src := range st.Sources {
+ hydSrc := src.Hydrate(hydrators)
+ for _, target := range src.Targets {
+
+ if !target.Visible() {
+ continue
+ }
+
+ hydTarget, err := target.Hydrate(src)
+ m.Indices[idx] = hydTarget
+ if err != nil {
+ spool.Warn("%v\n", err)
+ }
+ m.SourceIndices[idx] = hydSrc
+
+ idx++
+ }
+ }
+ return m
+}
diff --git a/menu/source.go b/menu/source.go
new file mode 100644
index 0000000..39b3280
--- /dev/null
+++ b/menu/source.go
@@ -0,0 +1,70 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+ "pik/model"
+)
+
+var (
+ SourceStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle()
+ return st
+ })
+ SourceHeaderBackground = lipgloss.Color("5")
+ SourceHeaderStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle()
+ return st
+
+ })
+ SourceLabelStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().Border(lipgloss.OuterHalfBlockBorder(), false, false, false, true).Background(SourceHeaderBackground).BorderBackground(SourceHeaderBackground).PaddingRight(1).PaddingLeft(1).MarginRight(1)
+ return st
+
+ })
+ SourceIconStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().PaddingRight(1)
+ return st
+
+ })
+ SourceTargetsStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle()
+ return st
+ })
+ SourcePathStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().Faint(true)
+ return st
+ })
+)
+
+func (m *Model) Source(src *model.HydratedSource) string {
+ targets := make([]string, 0, len(src.Targets))
+ for _, t := range src.HydratedTargets {
+ targets = append(targets, m.Target(t))
+ }
+
+ targetContent := lipgloss.JoinVertical(lipgloss.Top, targets...)
+
+ icon := SourceIconStyle.Render(Icon(src.Icon))
+
+ return SourceStyle.Render(lipgloss.JoinVertical(lipgloss.Top,
+ SourceHeaderStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, SourceLabelStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, icon, src.Label()), SourcePathStyle.Render(src.ShortPath())))),
+ SourceTargetsStyle.Render(targetContent),
+ ))
+
+}
+
+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...))
+}
diff --git a/menu/style/style.go b/menu/style/style.go
new file mode 100644
index 0000000..e7164e1
--- /dev/null
+++ b/menu/style/style.go
@@ -0,0 +1,30 @@
+package style
+
+import "github.com/charmbracelet/lipgloss"
+
+type StyleBuilder func() lipgloss.Style
+
+type Style struct {
+ style *lipgloss.Style
+ builder StyleBuilder
+}
+
+func New(builder StyleBuilder) Style {
+ return Style{
+ builder: builder,
+ }
+}
+
+func (s *Style) Get() lipgloss.Style {
+
+ if s.style == nil {
+ st := s.builder()
+ s.style = &st
+ }
+
+ return *s.style
+}
+
+func (s *Style) Render(input ...string) string {
+ return s.Get().Render(input...)
+}
diff --git a/menu/target.go b/menu/target.go
new file mode 100644
index 0000000..b17ed68
--- /dev/null
+++ b/menu/target.go
@@ -0,0 +1,56 @@
+package menu
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "pik/menu/style"
+ "pik/model"
+)
+
+var (
+ TargetBackgroundColor = lipgloss.Color("8")
+ SelectedTargetBackgroundColor = lipgloss.Color("2")
+ TargetStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().Border(lipgloss.OuterHalfBlockBorder(), false, false, false, true).BorderBackground(TargetBackgroundColor).Background(TargetBackgroundColor)
+ return st
+ })
+ SelectedTargetStyle = style.New(func() lipgloss.Style {
+ return TargetStyle.Get().BorderBackground(SelectedTargetBackgroundColor).Background(SelectedTargetBackgroundColor)
+ })
+ TargetLabelStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().MarginRight(1)
+ return st
+ })
+ TargetDescriptionStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().Faint(true).MarginLeft(1)
+ return st
+ })
+ SelectedTargetDescriptionStyle = style.New(func() lipgloss.Style {
+ st := TargetDescriptionStyle.Get().Faint(false)
+ return st
+ })
+ TargetIconStyle = style.New(func() lipgloss.Style {
+ st := lipgloss.NewStyle().PaddingLeft(1)
+ return st
+ })
+ TargetSubStyle = style.New(func() lipgloss.Style {
+ return lipgloss.NewStyle()
+ })
+)
+
+func (m *Model) Target(t model.HydratedTarget) string {
+ icon := TargetIconStyle.Render(PaddedIcon(t.Icon()))
+ selectionStyle := TargetStyle
+ selectionDescriptionStyle := TargetDescriptionStyle
+ _, sel := m.Result()
+ if sel.Target() == t.Target() {
+ selectionStyle = SelectedTargetStyle
+ selectionDescriptionStyle = SelectedTargetDescriptionStyle
+ }
+ var labelParts []string
+ labelParts = append(labelParts, icon)
+ if t.Sub() != nil {
+ labelParts = append(labelParts, TargetSubStyle.Render(t.Sub()...))
+ }
+ labelParts = append(labelParts, TargetLabelStyle.Render(t.Label()))
+ return lipgloss.JoinHorizontal(lipgloss.Left, selectionStyle.Render(labelParts...), selectionDescriptionStyle.Render(t.Description()))
+}