mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
- Add 20+ tests for utility functions, SharedData, and Model - Tests cover: LoadTables, LoadTableData, UpdateCell, pagination, table inference, focus/blur, empty database, invalid indices - Update Go to 1.26.1, upgrade all dependencies - Replace custom Min/Max with Go builtin min/max - Format all files with goimports - Add staticcheck to CI workflow
199 lines
4.3 KiB
Go
199 lines
4.3 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/help"
|
|
"github.com/charmbracelet/bubbles/key"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
type RowDetailModel struct {
|
|
Shared *SharedData
|
|
rowIndex int
|
|
selectedCol int
|
|
FromQuery bool
|
|
gPressed bool
|
|
keyMap RowDetailKeyMap
|
|
help help.Model
|
|
showFullHelp bool
|
|
focused bool
|
|
id int
|
|
}
|
|
|
|
// RowDetailOption is a functional option for configuring RowDetailModel
|
|
type RowDetailOption func(*RowDetailModel)
|
|
|
|
// WithRowDetailKeyMap sets the key map
|
|
func WithRowDetailKeyMap(km RowDetailKeyMap) RowDetailOption {
|
|
return func(m *RowDetailModel) {
|
|
m.keyMap = km
|
|
}
|
|
}
|
|
|
|
func NewRowDetailModel(shared *SharedData, rowIndex int, opts ...RowDetailOption) *RowDetailModel {
|
|
m := &RowDetailModel{
|
|
Shared: shared,
|
|
rowIndex: rowIndex,
|
|
selectedCol: 0,
|
|
FromQuery: false,
|
|
keyMap: DefaultRowDetailKeyMap(),
|
|
help: help.New(),
|
|
focused: true,
|
|
id: nextID(),
|
|
}
|
|
|
|
// Apply options
|
|
for _, opt := range opts {
|
|
opt(m)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// ID returns the unique ID of the model
|
|
func (m RowDetailModel) ID() int {
|
|
return m.id
|
|
}
|
|
|
|
// Focus sets the focus state
|
|
func (m *RowDetailModel) Focus() {
|
|
m.focused = true
|
|
}
|
|
|
|
// Blur removes focus
|
|
func (m *RowDetailModel) Blur() {
|
|
m.focused = false
|
|
}
|
|
|
|
// Focused returns the focus state
|
|
func (m RowDetailModel) Focused() bool {
|
|
return m.focused
|
|
}
|
|
|
|
func (m *RowDetailModel) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (m *RowDetailModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if !m.focused {
|
|
return m, nil
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case ToggleHelpMsg:
|
|
m.showFullHelp = !m.showFullHelp
|
|
return m, nil
|
|
|
|
case tea.KeyMsg:
|
|
return m.handleNavigation(msg)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *RowDetailModel) handleNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
switch {
|
|
case key.Matches(msg, m.keyMap.Escape), key.Matches(msg, m.keyMap.Back):
|
|
m.gPressed = false
|
|
if m.FromQuery {
|
|
return m, func() tea.Msg { return ReturnToQueryMsg{} }
|
|
}
|
|
return m, func() tea.Msg { return SwitchToTableDataMsg{TableIndex: m.Shared.SelectedTable} }
|
|
|
|
case key.Matches(msg, m.keyMap.GoToStart):
|
|
if m.gPressed {
|
|
// Second g - go to beginning (gg pattern like vim)
|
|
m.selectedCol = 0
|
|
m.gPressed = false
|
|
} else {
|
|
// First g - wait for second g to complete gg sequence
|
|
m.gPressed = true
|
|
}
|
|
return m, nil
|
|
|
|
case key.Matches(msg, m.keyMap.GoToEnd):
|
|
// Go to end (G pattern like vim)
|
|
if len(m.Shared.Columns) > 0 {
|
|
m.selectedCol = len(m.Shared.Columns) - 1
|
|
}
|
|
m.gPressed = false
|
|
return m, nil
|
|
|
|
case key.Matches(msg, m.keyMap.Enter):
|
|
m.gPressed = false
|
|
return m, func() tea.Msg {
|
|
return SwitchToEditCellMsg{RowIndex: m.rowIndex, ColIndex: m.selectedCol}
|
|
}
|
|
|
|
case key.Matches(msg, m.keyMap.Up):
|
|
m.gPressed = false
|
|
if m.selectedCol > 0 {
|
|
m.selectedCol--
|
|
}
|
|
|
|
case key.Matches(msg, m.keyMap.Down):
|
|
m.gPressed = false
|
|
if m.selectedCol < len(m.Shared.Columns)-1 {
|
|
m.selectedCol++
|
|
}
|
|
|
|
default:
|
|
// Any other key resets the g state
|
|
m.gPressed = false
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *RowDetailModel) View() string {
|
|
var content strings.Builder
|
|
|
|
content.WriteString(TitleStyle.Render("Row Details"))
|
|
content.WriteString("\n\n")
|
|
|
|
if m.rowIndex >= len(m.Shared.FilteredData) {
|
|
content.WriteString("Row not found")
|
|
return content.String()
|
|
}
|
|
|
|
row := m.Shared.FilteredData[m.rowIndex]
|
|
|
|
// Show each column and its value
|
|
for i, col := range m.Shared.Columns {
|
|
if i >= len(row) {
|
|
break
|
|
}
|
|
|
|
value := row[i]
|
|
// Calculate available width for value display
|
|
// Account for column name, ": ", and indentation
|
|
availableWidth := m.Shared.Width - len(col) - 4 // 4 for ": " and "> " prefix
|
|
if availableWidth < 20 {
|
|
availableWidth = 20 // Minimum width
|
|
}
|
|
|
|
if len(value) > availableWidth {
|
|
// Wrap long values
|
|
lines := WrapText(value, availableWidth)
|
|
value = strings.Join(lines, "\n ")
|
|
}
|
|
|
|
line := fmt.Sprintf("%s: %s", col, value)
|
|
if i == m.selectedCol {
|
|
content.WriteString(SelectedStyle.Render("> " + line))
|
|
} else {
|
|
content.WriteString(NormalStyle.Render(" " + line))
|
|
}
|
|
content.WriteString("\n")
|
|
}
|
|
|
|
content.WriteString("\n")
|
|
if m.showFullHelp {
|
|
content.WriteString(m.help.FullHelpView(m.keyMap.FullHelp()))
|
|
} else {
|
|
content.WriteString(m.help.ShortHelpView(m.keyMap.ShortHelp()))
|
|
}
|
|
|
|
return content.String()
|
|
}
|