update editor bindings

This commit is contained in:
2025-07-13 19:53:38 -07:00
parent 76bcc8520c
commit 2fa8ebe741
2 changed files with 229 additions and 21 deletions

View File

@@ -2,6 +2,8 @@ package app
import ( import (
"fmt" "fmt"
"time"
"unicode"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -12,6 +14,15 @@ type EditCellModel struct {
colIndex int colIndex int
value string value string
cursor int cursor int
blinkState bool
}
type blinkMsg struct{}
func blinkCmd() tea.Cmd {
return tea.Tick(time.Millisecond*500, func(t time.Time) tea.Msg {
return blinkMsg{}
})
} }
func NewEditCellModel(shared *SharedData, rowIndex, colIndex int) *EditCellModel { func NewEditCellModel(shared *SharedData, rowIndex, colIndex int) *EditCellModel {
@@ -26,15 +37,20 @@ func NewEditCellModel(shared *SharedData, rowIndex, colIndex int) *EditCellModel
colIndex: colIndex, colIndex: colIndex,
value: value, value: value,
cursor: len(value), cursor: len(value),
blinkState: true,
} }
} }
func (m *EditCellModel) Init() tea.Cmd { func (m *EditCellModel) Init() tea.Cmd {
return nil return blinkCmd()
} }
func (m *EditCellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *EditCellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case blinkMsg:
m.blinkState = !m.blinkState
return m, blinkCmd()
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "esc": case "esc":
@@ -65,6 +81,21 @@ func (m *EditCellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.cursor++ m.cursor++
} }
case "home", "ctrl+a":
m.cursor = 0
case "end", "ctrl+e":
m.cursor = len(m.value)
case "ctrl+left":
m.cursor = m.wordLeft(m.value, m.cursor)
case "ctrl+right":
m.cursor = m.wordRight(m.value, m.cursor)
case "ctrl+w":
m.deleteWordLeft()
default: default:
if len(msg.String()) == 1 { if len(msg.String()) == 1 {
m.value = m.value[:m.cursor] + msg.String() + m.value[m.cursor:] m.value = m.value[:m.cursor] + msg.String() + m.value[m.cursor:]
@@ -83,15 +114,87 @@ func (m *EditCellModel) View() string {
content := TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)) + "\n\n" content := TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)) + "\n\n"
// Display value with visible cursor // Display value with properly positioned cursor like bubbles textinput
displayValue := m.value content += "Value: "
if m.cursor <= len(displayValue) { value := m.value
// Insert cursor character at cursor position pos := m.cursor
displayValue = displayValue[:m.cursor] + "_" + displayValue[m.cursor:]
// Text before cursor
if pos > 0 {
content += value[:pos]
} }
content += fmt.Sprintf("Value: %s\n\n", displayValue) // Cursor and character at cursor position
content += HelpStyle.Render("enter: save • esc: cancel") if pos < len(value) {
// Cursor over existing character
char := string(value[pos])
if m.blinkState {
content += SelectedStyle.Render(char) // Highlight the character
} else {
content += char
}
// Text after cursor
if pos+1 < len(value) {
content += value[pos+1:]
}
} else {
// Cursor at end of text
if m.blinkState {
content += "|"
}
}
content += "\n\n"
content += HelpStyle.Render("enter: save • esc: cancel • ctrl+w: delete word • ctrl+arrows: word nav")
return content return content
} }
// wordLeft finds the position of the start of the word to the left of the cursor
func (m *EditCellModel) wordLeft(text string, pos int) int {
if pos == 0 {
return 0
}
// Move left past any whitespace
for pos > 0 && unicode.IsSpace(rune(text[pos-1])) {
pos--
}
// Move left past the current word
for pos > 0 && !unicode.IsSpace(rune(text[pos-1])) {
pos--
}
return pos
}
// wordRight finds the position of the start of the word to the right of the cursor
func (m *EditCellModel) wordRight(text string, pos int) int {
if pos >= len(text) {
return len(text)
}
// Move right past the current word
for pos < len(text) && !unicode.IsSpace(rune(text[pos])) {
pos++
}
// Move right past any whitespace
for pos < len(text) && unicode.IsSpace(rune(text[pos])) {
pos++
}
return pos
}
// deleteWordLeft deletes the word to the left of the cursor
func (m *EditCellModel) deleteWordLeft() {
if m.cursor == 0 {
return
}
newPos := m.wordLeft(m.value, m.cursor)
m.value = m.value[:newPos] + m.value[m.cursor:]
m.cursor = newPos
}

View File

@@ -3,6 +3,8 @@ package app
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"unicode"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -16,6 +18,7 @@ type QueryModel struct {
results [][]string results [][]string
columns []string columns []string
err error err error
blinkState bool
} }
func NewQueryModel(shared *SharedData) *QueryModel { func NewQueryModel(shared *SharedData) *QueryModel {
@@ -23,15 +26,24 @@ func NewQueryModel(shared *SharedData) *QueryModel {
Shared: shared, Shared: shared,
FocusOnInput: true, FocusOnInput: true,
selectedRow: 0, selectedRow: 0,
blinkState: true,
} }
} }
func (m *QueryModel) Init() tea.Cmd { func (m *QueryModel) Init() tea.Cmd {
return nil return tea.Tick(time.Millisecond*500, func(t time.Time) tea.Msg {
return blinkMsg{}
})
} }
func (m *QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case blinkMsg:
m.blinkState = !m.blinkState
return m, tea.Tick(time.Millisecond*500, func(t time.Time) tea.Msg {
return blinkMsg{}
})
case tea.KeyMsg: case tea.KeyMsg:
if m.FocusOnInput { if m.FocusOnInput {
return m.handleQueryInput(msg) return m.handleQueryInput(msg)
@@ -67,6 +79,21 @@ func (m *QueryModel) handleQueryInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.cursor++ m.cursor++
} }
case "home", "ctrl+a":
m.cursor = 0
case "end", "ctrl+e":
m.cursor = len(m.query)
case "ctrl+left":
m.cursor = m.wordLeft(m.query, m.cursor)
case "ctrl+right":
m.cursor = m.wordRight(m.query, m.cursor)
case "ctrl+w":
m.deleteWordLeft()
default: default:
if len(msg.String()) == 1 { if len(msg.String()) == 1 {
m.query = m.query[:m.cursor] + msg.String() + m.query[m.cursor:] m.query = m.query[:m.cursor] + msg.String() + m.query[m.cursor:]
@@ -282,6 +309,55 @@ func (m *QueryModel) handleQueryCompletion(msg QueryCompletedMsg) {
m.err = nil m.err = nil
} }
// wordLeft finds the position of the start of the word to the left of the cursor
func (m *QueryModel) wordLeft(text string, pos int) int {
if pos == 0 {
return 0
}
// Move left past any whitespace
for pos > 0 && unicode.IsSpace(rune(text[pos-1])) {
pos--
}
// Move left past the current word
for pos > 0 && !unicode.IsSpace(rune(text[pos-1])) {
pos--
}
return pos
}
// wordRight finds the position of the start of the word to the right of the cursor
func (m *QueryModel) wordRight(text string, pos int) int {
if pos >= len(text) {
return len(text)
}
// Move right past the current word
for pos < len(text) && !unicode.IsSpace(rune(text[pos])) {
pos++
}
// Move right past any whitespace
for pos < len(text) && unicode.IsSpace(rune(text[pos])) {
pos++
}
return pos
}
// deleteWordLeft deletes the word to the left of the cursor
func (m *QueryModel) deleteWordLeft() {
if m.cursor == 0 {
return
}
newPos := m.wordLeft(m.query, m.cursor)
m.query = m.query[:newPos] + m.query[m.cursor:]
m.cursor = newPos
}
func (m *QueryModel) View() string { func (m *QueryModel) View() string {
var content strings.Builder var content strings.Builder
@@ -291,7 +367,36 @@ func (m *QueryModel) View() string {
// Query input // Query input
content.WriteString("Query: ") content.WriteString("Query: ")
if m.FocusOnInput { if m.FocusOnInput {
content.WriteString(m.query + "_") // Display query with properly positioned cursor like bubbles textinput
query := m.query
pos := m.cursor
// Text before cursor
before := ""
if pos > 0 {
before = query[:pos]
}
content.WriteString(before)
// Cursor and character at cursor position
if pos < len(query) {
// Cursor over existing character
char := string(query[pos])
if m.blinkState {
content.WriteString(SelectedStyle.Render(char)) // Highlight the character
} else {
content.WriteString(char)
}
// Text after cursor
if pos+1 < len(query) {
content.WriteString(query[pos+1:])
}
} else {
// Cursor at end of text
if m.blinkState {
content.WriteString("|")
}
}
} else { } else {
content.WriteString(m.query) content.WriteString(m.query)
} }
@@ -353,7 +458,7 @@ func (m *QueryModel) View() string {
content.WriteString("\n") content.WriteString("\n")
if m.FocusOnInput { if m.FocusOnInput {
content.WriteString(HelpStyle.Render("enter: execute • esc: back")) content.WriteString(HelpStyle.Render("enter: execute • esc: back • ctrl+w: delete word • ctrl+arrows: word nav"))
} else { } else {
content.WriteString(HelpStyle.Render("↑/↓: navigate • enter: details • i: edit query • q: back")) content.WriteString(HelpStyle.Render("↑/↓: navigate • enter: details • i: edit query • q: back"))
} }