mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
update editor bindings
This commit is contained in:
@@ -2,16 +2,27 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EditCellModel struct {
|
type EditCellModel struct {
|
||||||
Shared *SharedData
|
Shared *SharedData
|
||||||
rowIndex int
|
rowIndex int
|
||||||
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 {
|
||||||
@@ -21,20 +32,25 @@ func NewEditCellModel(shared *SharedData, rowIndex, colIndex int) *EditCellModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &EditCellModel{
|
return &EditCellModel{
|
||||||
Shared: shared,
|
Shared: shared,
|
||||||
rowIndex: rowIndex,
|
rowIndex: rowIndex,
|
||||||
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor and character at cursor position
|
||||||
|
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 += fmt.Sprintf("Value: %s\n\n", displayValue)
|
content += "\n\n"
|
||||||
content += HelpStyle.Render("enter: save • esc: cancel")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user