mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -63,3 +63,4 @@ keybindings:
|
|||||||
input:
|
input:
|
||||||
select: "enter"
|
select: "enter"
|
||||||
reset: "r"
|
reset: "r"
|
||||||
|
filter: "/"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type PowerKeys struct {
|
|||||||
type InputKeys struct {
|
type InputKeys struct {
|
||||||
Select string `mapstructure:"select"`
|
Select string `mapstructure:"select"`
|
||||||
Reset string `mapstructure:"reset"`
|
Reset string `mapstructure:"reset"`
|
||||||
|
Filter string `mapstructure:"filter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Keybindings struct {
|
type Keybindings struct {
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ import (
|
|||||||
type InputKeyMap struct {
|
type InputKeyMap struct {
|
||||||
Select key.Binding
|
Select key.Binding
|
||||||
Reset key.Binding
|
Reset key.Binding
|
||||||
|
Filter key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInputKeyMap(cfg config.InputKeys) InputKeyMap {
|
func newInputKeyMap(cfg config.InputKeys) InputKeyMap {
|
||||||
return InputKeyMap{
|
return InputKeyMap{
|
||||||
Select: binding(cfg.Select, "apply layout"),
|
Select: binding(cfg.Select, "apply layout"),
|
||||||
Reset: binding(cfg.Reset, "reset to config"),
|
Reset: binding(cfg.Reset, "reset to config"),
|
||||||
|
Filter: binding(cfg.Filter, "filter"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i InputKeyMap) Bindings() []key.Binding {
|
func (i InputKeyMap) Bindings() []key.Binding {
|
||||||
return []key.Binding{i.Select, i.Reset}
|
return []key.Binding{i.Select, i.Reset, i.Filter}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package input
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
@@ -39,11 +40,41 @@ func (m *Model) handleSetLayout(msg setLayoutMsg) tea.Cmd {
|
|||||||
return fetchLayoutCmd()
|
return fetchLayoutCmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// visibleLayouts returns the layouts matching the current filter query (by
|
||||||
|
// code or name, case-insensitive), or every layout when there's no query.
|
||||||
|
func (m Model) visibleLayouts() []inputpkg.Layout {
|
||||||
|
q := strings.ToLower(strings.TrimSpace(m.filterInput.Value()))
|
||||||
|
if q == "" {
|
||||||
|
return inputpkg.Layouts
|
||||||
|
}
|
||||||
|
matches := make([]inputpkg.Layout, 0, len(inputpkg.Layouts))
|
||||||
|
for _, l := range inputpkg.Layouts {
|
||||||
|
if strings.Contains(strings.ToLower(l.Name), q) || strings.Contains(strings.ToLower(l.Code), q) {
|
||||||
|
matches = append(matches, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampCursor keeps the cursor within the currently visible (filtered)
|
||||||
|
// list, called whenever the filter query — and so the list length —
|
||||||
|
// changes.
|
||||||
|
func (m *Model) clampCursor() {
|
||||||
|
n := len(m.visibleLayouts())
|
||||||
|
switch {
|
||||||
|
case m.cursor >= n:
|
||||||
|
m.cursor = n - 1
|
||||||
|
case m.cursor < 0:
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) selectedLayout() *inputpkg.Layout {
|
func (m Model) selectedLayout() *inputpkg.Layout {
|
||||||
if m.cursor < 0 || m.cursor >= len(inputpkg.Layouts) {
|
list := m.visibleLayouts()
|
||||||
|
if m.cursor < 0 || m.cursor >= len(list) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &inputpkg.Layouts[m.cursor]
|
return &list[m.cursor]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) updateLayout(msg tea.Msg) tea.Cmd {
|
func (m *Model) updateLayout(msg tea.Msg) tea.Cmd {
|
||||||
@@ -54,13 +85,36 @@ func (m *Model) updateLayout(msg tea.Msg) tea.Cmd {
|
|||||||
g := keys.Keys.Global
|
g := keys.Keys.Global
|
||||||
in := keys.Keys.Input
|
in := keys.Keys.Input
|
||||||
|
|
||||||
|
if m.filtering {
|
||||||
switch {
|
switch {
|
||||||
|
case key.Matches(keyMsg, g.Escape):
|
||||||
|
m.filtering = false
|
||||||
|
m.filterInput.Blur()
|
||||||
|
m.filterInput.SetValue("")
|
||||||
|
m.cursor = 0
|
||||||
|
case key.Matches(keyMsg, in.Select):
|
||||||
|
m.filtering = false
|
||||||
|
m.filterInput.Blur()
|
||||||
|
m.clampCursor()
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.filterInput, cmd = m.filterInput.Update(keyMsg)
|
||||||
|
m.clampCursor()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case key.Matches(keyMsg, in.Filter):
|
||||||
|
m.filtering = true
|
||||||
|
m.filterInput.Focus()
|
||||||
case key.Matches(keyMsg, g.Up):
|
case key.Matches(keyMsg, g.Up):
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
m.cursor--
|
m.cursor--
|
||||||
}
|
}
|
||||||
case key.Matches(keyMsg, g.Down):
|
case key.Matches(keyMsg, g.Down):
|
||||||
if m.cursor < len(inputpkg.Layouts)-1 {
|
if m.cursor < len(m.visibleLayouts())-1 {
|
||||||
m.cursor++
|
m.cursor++
|
||||||
}
|
}
|
||||||
case key.Matches(keyMsg, in.Select):
|
case key.Matches(keyMsg, in.Select):
|
||||||
@@ -90,16 +144,32 @@ func (m Model) renderLayout(width, height int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
current := subtleStyle().Render("Current: ") + inputpkg.LayoutName(m.currentLayout) + subtleStyle().Render(" ("+m.currentLayout+")")
|
current := subtleStyle().Render("Current: ") + inputpkg.LayoutName(m.currentLayout) + subtleStyle().Render(" ("+m.currentLayout+")")
|
||||||
lines := []string{title, "", current, ""}
|
lines := []string{title, "", current}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case m.filtering:
|
||||||
|
lines = append(lines, m.filterInput.View())
|
||||||
|
case m.filterInput.Value() != "":
|
||||||
|
lines = append(lines, subtleStyle().Render(fmt.Sprintf("Filter: %q — press / to change", m.filterInput.Value())))
|
||||||
|
}
|
||||||
|
lines = append(lines, "")
|
||||||
|
|
||||||
|
list := m.visibleLayouts()
|
||||||
|
|
||||||
available := height - len(lines) - 2
|
available := height - len(lines) - 2
|
||||||
if available < 1 {
|
if available < 1 {
|
||||||
available = 1
|
available = 1
|
||||||
}
|
}
|
||||||
p, start, end := paginate(m.cursor, len(inputpkg.Layouts), available)
|
|
||||||
|
if len(list) == 0 {
|
||||||
|
lines = append(lines, ilovetui.S.Faint.Render("No layout matches the filter."))
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, start, end := paginate(m.cursor, len(list), available)
|
||||||
|
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
lines = append(lines, m.renderLayoutRow(inputpkg.Layouts[i], i == m.cursor))
|
lines = append(lines, m.renderLayoutRow(list[i], i == m.cursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
footer := renderPaginatorDots(p)
|
footer := renderPaginatorDots(p)
|
||||||
|
|||||||
@@ -24,12 +24,17 @@ func (k inputKeyMap) ShortHelp() []key.Binding {
|
|||||||
if k.m.checkErr != nil {
|
if k.m.checkErr != nil {
|
||||||
return []key.Binding{g.Help}
|
return []key.Binding{g.Help}
|
||||||
}
|
}
|
||||||
|
if k.m.filtering {
|
||||||
|
confirm := relabel(in.Select, "confirm")
|
||||||
|
cancel := relabel(g.Escape, "cancel")
|
||||||
|
return []key.Binding{confirm, cancel}
|
||||||
|
}
|
||||||
switch k.m.focus {
|
switch k.m.focus {
|
||||||
case focusMouse:
|
case focusMouse:
|
||||||
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
||||||
return []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus, g.Help}
|
return []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus, g.Help}
|
||||||
default:
|
default:
|
||||||
return []key.Binding{g.Up, g.Down, in.Select, in.Reset, g.CycleFocus, g.Help}
|
return []key.Binding{g.Up, g.Down, in.Select, in.Filter, in.Reset, g.CycleFocus, g.Help}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +42,12 @@ func (k inputKeyMap) FullHelp() [][]key.Binding {
|
|||||||
g := keys.Keys.Global
|
g := keys.Keys.Global
|
||||||
in := keys.Keys.Input
|
in := keys.Keys.Input
|
||||||
|
|
||||||
|
if k.m.filtering {
|
||||||
|
confirm := relabel(in.Select, "confirm filter")
|
||||||
|
cancel := relabel(g.Escape, "cancel filter")
|
||||||
|
return keys.ChunkByWidth([]key.Binding{confirm, cancel}, k.width)
|
||||||
|
}
|
||||||
|
|
||||||
var bindings []key.Binding
|
var bindings []key.Binding
|
||||||
switch {
|
switch {
|
||||||
case k.m.checkErr != nil:
|
case k.m.checkErr != nil:
|
||||||
@@ -45,7 +56,7 @@ func (k inputKeyMap) FullHelp() [][]key.Binding {
|
|||||||
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
speedDown, speedUp := relabel(g.Left, "speed down"), relabel(g.Right, "speed up")
|
||||||
bindings = []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus}
|
bindings = []key.Binding{speedDown, speedUp, in.Reset, g.CycleFocus}
|
||||||
default:
|
default:
|
||||||
bindings = []key.Binding{g.Up, g.Down, in.Select, in.Reset, g.CycleFocus}
|
bindings = []key.Binding{g.Up, g.Down, in.Select, in.Filter, in.Reset, g.CycleFocus}
|
||||||
}
|
}
|
||||||
bindings = append(bindings, g.CommonBindings()...)
|
bindings = append(bindings, g.CommonBindings()...)
|
||||||
return keys.ChunkByWidth(bindings, k.width)
|
return keys.ChunkByWidth(bindings, k.width)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ package input
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"charm.land/bubbles/v2/help"
|
"charm.land/bubbles/v2/help"
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
ilovetui "github.com/anotherhadi/ilovetui"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ type Model struct {
|
|||||||
cursorInit bool // whether cursor has been snapped to the current layout yet
|
cursorInit bool // whether cursor has been snapped to the current layout yet
|
||||||
layoutPending bool
|
layoutPending bool
|
||||||
layoutFeedback string
|
layoutFeedback string
|
||||||
|
filterInput textinput.Model
|
||||||
|
filtering bool
|
||||||
|
|
||||||
sensitivityErr error
|
sensitivityErr error
|
||||||
sensitivity float64
|
sensitivity float64
|
||||||
@@ -42,7 +46,16 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
return Model{help: ilovetui.NewHelp()}
|
ti := textinput.New()
|
||||||
|
ti.Prompt = "/"
|
||||||
|
s := ti.Styles()
|
||||||
|
s.Focused.Prompt = lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
|
ti.SetStyles(s)
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
help: ilovetui.NewHelp(),
|
||||||
|
filterInput: ti,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
@@ -57,11 +70,12 @@ func (m *Model) Activate() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) IsEditing() bool {
|
func (m Model) IsEditing() bool {
|
||||||
return false
|
return m.filtering
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) {
|
func (m *Model) SetSize(w, h int) {
|
||||||
m.width = w
|
m.width = w
|
||||||
m.height = h
|
m.height = h
|
||||||
m.help.SetWidth(w - 2)
|
m.help.SetWidth(w - 2)
|
||||||
|
m.filterInput.SetWidth(w - 4)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, m.handleReset(msg)
|
return m, m.handleReset(msg)
|
||||||
|
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
|
if m.filtering {
|
||||||
|
// Every key goes straight to the filter box while typing (a
|
||||||
|
// query like "tab" or "r" must not also trigger CycleFocus or
|
||||||
|
// Reset), same precedence the About page's search box uses.
|
||||||
|
return m, m.updateLayout(msg)
|
||||||
|
}
|
||||||
|
|
||||||
if key.Matches(msg, keys.Keys.Global.Help) {
|
if key.Matches(msg, keys.Keys.Global.Help) {
|
||||||
m.help.ShowAll = !m.help.ShowAll
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user