diff options
| author | ewy <ewy0@protonmail.com> | 2026-04-14 16:37:17 +0200 |
|---|---|---|
| committer | ewy <ewy0@protonmail.com> | 2026-04-14 16:37:17 +0200 |
| commit | 45a297a8e526094e8fce6e2c5c0fd89b381d1765 (patch) | |
| tree | 852ebc3a0112c94dc9726d0b27ab057bf6383660 /menu | |
i have to commit at some point!
Diffstat (limited to 'menu')
| -rw-r--r-- | menu/banner.go | 92 | ||||
| -rw-r--r-- | menu/confirm.go | 43 | ||||
| -rw-r--r-- | menu/icon.go | 28 | ||||
| -rw-r--r-- | menu/menu.go | 47 | ||||
| -rw-r--r-- | menu/model.go | 101 | ||||
| -rw-r--r-- | menu/source.go | 70 | ||||
| -rw-r--r-- | menu/style/style.go | 30 | ||||
| -rw-r--r-- | menu/target.go | 56 |
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())) +} |
