mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
make things editable
This commit is contained in:
67
README.md
67
README.md
@@ -6,10 +6,14 @@ A fully-featured terminal user interface for browsing SQLite databases built wit
|
|||||||
|
|
||||||
- **Table Browser**: Browse all tables in your SQLite database with pagination
|
- **Table Browser**: Browse all tables in your SQLite database with pagination
|
||||||
- **Search Functionality**: Search tables by name using `/` key
|
- **Search Functionality**: Search tables by name using `/` key
|
||||||
- **Data Viewer**: View table data with pagination (20 rows per page)
|
- **Data Viewer**: View table data with pagination and row highlighting
|
||||||
|
- **Row-Level Navigation**: Navigate through data rows with cursor highlighting
|
||||||
|
- **Data Search**: Search within table data using `/` key
|
||||||
|
- **Row Detail Modal**: View individual rows in a 2-column format (Column | Value)
|
||||||
|
- **Cell Editing**: Edit individual cell values with live database updates
|
||||||
- **SQL Query Interface**: Execute custom SQL queries with parameter support
|
- **SQL Query Interface**: Execute custom SQL queries with parameter support
|
||||||
- **Responsive Design**: Adapts to terminal size and fits content to screen
|
- **Responsive Design**: Adapts to terminal size and fits content to screen
|
||||||
- **Navigation**: Intuitive keyboard navigation
|
- **Navigation**: Intuitive keyboard navigation throughout all modes
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -39,11 +43,31 @@ go run main.go sample.db
|
|||||||
- `Backspace`: Delete characters
|
- `Backspace`: Delete characters
|
||||||
|
|
||||||
### Table Data Mode
|
### Table Data Mode
|
||||||
|
- `↑/↓` or `k/j`: Navigate between data rows (with highlighting)
|
||||||
- `←/→` or `h/l`: Navigate between data pages
|
- `←/→` or `h/l`: Navigate between data pages
|
||||||
|
- `/`: Start searching within table data
|
||||||
|
- `Enter`: View selected row in detail modal
|
||||||
- `Esc`: Return to table list
|
- `Esc`: Return to table list
|
||||||
- `r`: Refresh current table data
|
- `r`: Refresh current table data
|
||||||
- `q` or `Ctrl+C`: Quit
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
### Data Search Mode (when searching within table data)
|
||||||
|
- Type to search within all columns of the table
|
||||||
|
- `Enter` or `Esc`: Finish search
|
||||||
|
- `Backspace`: Delete characters
|
||||||
|
|
||||||
|
### Row Detail Modal
|
||||||
|
- `↑/↓` or `k/j`: Navigate between fields (Column | Value format)
|
||||||
|
- `Enter`: Edit selected field value
|
||||||
|
- `Esc`: Return to table data view
|
||||||
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
### Cell Edit Mode
|
||||||
|
- Type new value for the selected cell
|
||||||
|
- `Enter`: Save changes to database
|
||||||
|
- `Esc`: Cancel editing and return to row detail
|
||||||
|
- `Backspace`: Delete characters
|
||||||
|
|
||||||
### SQL Query Mode
|
### SQL Query Mode
|
||||||
- Type your SQL query
|
- Type your SQL query
|
||||||
- `Enter`: Execute query
|
- `Enter`: Execute query
|
||||||
@@ -56,13 +80,28 @@ go run main.go sample.db
|
|||||||
1. **Table Browsing**: Lists all tables in the database with pagination
|
1. **Table Browsing**: Lists all tables in the database with pagination
|
||||||
2. **Table Search**: Filter tables by name using `/` to search
|
2. **Table Search**: Filter tables by name using `/` to search
|
||||||
3. **Paginated Data View**: Shows table data with pagination (20 rows per page)
|
3. **Paginated Data View**: Shows table data with pagination (20 rows per page)
|
||||||
4. **Screen-Aware Display**: Content automatically fits terminal size
|
4. **Row Highlighting**: Cursor-based row selection with visual highlighting
|
||||||
5. **SQL Query Execution**: Execute custom SQL queries and view results
|
5. **Data Search**: Search within table data across all columns
|
||||||
6. **Error Handling**: Displays database errors gracefully
|
6. **Row Detail Modal**: 2-column view showing Column | Value for selected row
|
||||||
7. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
7. **Cell Editing**: Live editing of individual cell values with database updates
|
||||||
8. **Column Information**: Shows column names and handles NULL values
|
8. **Primary Key Detection**: Uses primary keys for reliable row updates
|
||||||
9. **Navigation**: Intuitive keyboard shortcuts for all operations
|
9. **Screen-Aware Display**: Content automatically fits terminal size
|
||||||
10. **Dynamic Column Width**: Columns adjust to terminal width
|
10. **SQL Query Execution**: Execute custom SQL queries and view results
|
||||||
|
11. **Error Handling**: Displays database errors gracefully
|
||||||
|
12. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||||
|
13. **Column Information**: Shows column names and handles NULL values
|
||||||
|
14. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||||
|
15. **Dynamic Column Width**: Columns adjust to terminal width
|
||||||
|
|
||||||
|
## Navigation Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Table List → Table Data → Row Detail → Cell Edit
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
Search Data Search Field Nav Value Edit
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
SQL Query Row Select Cell Select Save/Cancel
|
||||||
|
```
|
||||||
|
|
||||||
## Sample Database
|
## Sample Database
|
||||||
|
|
||||||
@@ -74,4 +113,12 @@ The included `sample.db` contains:
|
|||||||
|
|
||||||
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - TUI framework
|
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - TUI framework
|
||||||
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) - Styling
|
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) - Styling
|
||||||
- [go-sqlite3](https://github.com/mattn/go-sqlite3) - SQLite driver
|
- [go-sqlite3](https://github.com/mattn/go-sqlite3) - SQLite driver
|
||||||
|
|
||||||
|
## Database Updates
|
||||||
|
|
||||||
|
The application supports live editing of database records:
|
||||||
|
- Uses primary keys when available for reliable row identification
|
||||||
|
- Falls back to full-row matching when no primary key exists
|
||||||
|
- Updates are immediately reflected in the interface
|
||||||
|
- All changes are committed to the database in real-time
|
||||||
387
main.go
387
main.go
@@ -22,27 +22,37 @@ const (
|
|||||||
modeTableList viewMode = iota
|
modeTableList viewMode = iota
|
||||||
modeTableData
|
modeTableData
|
||||||
modeQuery
|
modeQuery
|
||||||
|
modeRowDetail
|
||||||
|
modeEditCell
|
||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
mode viewMode
|
mode viewMode
|
||||||
tables []string
|
tables []string
|
||||||
filteredTables []string
|
filteredTables []string
|
||||||
selectedTable int
|
selectedTable int
|
||||||
tableListPage int
|
tableListPage int
|
||||||
tableData [][]string
|
tableData [][]string
|
||||||
columns []string
|
filteredData [][]string
|
||||||
currentPage int
|
columns []string
|
||||||
totalRows int
|
primaryKeys []string
|
||||||
query string
|
currentPage int
|
||||||
queryInput string
|
totalRows int
|
||||||
searchInput string
|
selectedRow int
|
||||||
searching bool
|
selectedCol int
|
||||||
cursor int
|
query string
|
||||||
err error
|
queryInput string
|
||||||
width int
|
searchInput string
|
||||||
height int
|
dataSearchInput string
|
||||||
|
searching bool
|
||||||
|
dataSearching bool
|
||||||
|
editingValue string
|
||||||
|
originalValue string
|
||||||
|
cursor int
|
||||||
|
err error
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -80,15 +90,20 @@ func initialModel(dbPath string) model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := model{
|
m := model{
|
||||||
db: db,
|
db: db,
|
||||||
mode: modeTableList,
|
mode: modeTableList,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
tableListPage: 0,
|
tableListPage: 0,
|
||||||
filteredTables: []string{},
|
filteredTables: []string{},
|
||||||
searchInput: "",
|
filteredData: [][]string{},
|
||||||
searching: false,
|
searchInput: "",
|
||||||
width: 80, // default width
|
dataSearchInput: "",
|
||||||
height: 24, // default height
|
searching: false,
|
||||||
|
dataSearching: false,
|
||||||
|
selectedRow: 0,
|
||||||
|
selectedCol: 0,
|
||||||
|
width: 80, // default width
|
||||||
|
height: 24, // default height
|
||||||
}
|
}
|
||||||
|
|
||||||
m.loadTables()
|
m.loadTables()
|
||||||
@@ -158,7 +173,7 @@ func (m *model) loadTableData() {
|
|||||||
|
|
||||||
tableName := m.filteredTables[m.selectedTable]
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
|
|
||||||
// Get column info
|
// Get column info and primary keys
|
||||||
rows, err := m.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
rows, err := m.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.err = err
|
m.err = err
|
||||||
@@ -167,6 +182,7 @@ func (m *model) loadTableData() {
|
|||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
m.columns = []string{}
|
m.columns = []string{}
|
||||||
|
m.primaryKeys = []string{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var cid int
|
var cid int
|
||||||
var name, dataType string
|
var name, dataType string
|
||||||
@@ -178,6 +194,9 @@ func (m *model) loadTableData() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.columns = append(m.columns, name)
|
m.columns = append(m.columns, name)
|
||||||
|
if pk == 1 {
|
||||||
|
m.primaryKeys = append(m.primaryKeys, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get total row count
|
// Get total row count
|
||||||
@@ -222,6 +241,111 @@ func (m *model) loadTableData() {
|
|||||||
}
|
}
|
||||||
m.tableData = append(m.tableData, row)
|
m.tableData = append(m.tableData, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply data filtering
|
||||||
|
m.filterData()
|
||||||
|
|
||||||
|
// Reset row selection if needed
|
||||||
|
if m.selectedRow >= len(m.filteredData) {
|
||||||
|
m.selectedRow = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) filterData() {
|
||||||
|
if m.dataSearchInput == "" {
|
||||||
|
m.filteredData = make([][]string, len(m.tableData))
|
||||||
|
copy(m.filteredData, m.tableData)
|
||||||
|
} else {
|
||||||
|
m.filteredData = [][]string{}
|
||||||
|
searchLower := strings.ToLower(m.dataSearchInput)
|
||||||
|
for _, row := range m.tableData {
|
||||||
|
// Search in all columns of the row
|
||||||
|
found := false
|
||||||
|
for _, cell := range row {
|
||||||
|
if strings.Contains(strings.ToLower(cell), searchLower) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
m.filteredData = append(m.filteredData, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) updateCell(rowIndex, colIndex int, newValue string) error {
|
||||||
|
if rowIndex >= len(m.filteredData) || colIndex >= len(m.columns) {
|
||||||
|
return fmt.Errorf("invalid row or column index")
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
|
columnName := m.columns[colIndex]
|
||||||
|
|
||||||
|
// Build WHERE clause using primary keys or all columns if no primary key
|
||||||
|
var whereClause strings.Builder
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
if len(m.primaryKeys) > 0 {
|
||||||
|
// Use primary keys for WHERE clause
|
||||||
|
for i, pkCol := range m.primaryKeys {
|
||||||
|
if i > 0 {
|
||||||
|
whereClause.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
// Find the column index for this primary key
|
||||||
|
pkIndex := -1
|
||||||
|
for j, col := range m.columns {
|
||||||
|
if col == pkCol {
|
||||||
|
pkIndex = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkIndex >= 0 {
|
||||||
|
whereClause.WriteString(fmt.Sprintf("%s = ?", pkCol))
|
||||||
|
args = append(args, m.filteredData[rowIndex][pkIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use all columns for WHERE clause (less reliable but works)
|
||||||
|
for i, col := range m.columns {
|
||||||
|
if i > 0 {
|
||||||
|
whereClause.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
whereClause.WriteString(fmt.Sprintf("%s = ?", col))
|
||||||
|
args = append(args, m.filteredData[rowIndex][i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute UPDATE
|
||||||
|
updateQuery := fmt.Sprintf("UPDATE %s SET %s = ? WHERE %s", tableName, columnName, whereClause.String())
|
||||||
|
args = append([]any{newValue}, args...)
|
||||||
|
|
||||||
|
_, err := m.db.Exec(updateQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local data
|
||||||
|
m.filteredData[rowIndex][colIndex] = newValue
|
||||||
|
// Also update the original data if it exists
|
||||||
|
for i, row := range m.tableData {
|
||||||
|
if len(row) > colIndex {
|
||||||
|
// Simple comparison - this might not work perfectly for all cases
|
||||||
|
match := true
|
||||||
|
for j, cell := range row {
|
||||||
|
if j < len(m.filteredData[rowIndex]) && cell != m.filteredData[rowIndex][j] && j != colIndex {
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
m.tableData[i][colIndex] = newValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) executeQuery() {
|
func (m *model) executeQuery() {
|
||||||
@@ -284,7 +408,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// Handle search mode first
|
// Handle edit mode first
|
||||||
|
if m.mode == modeEditCell {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
m.mode = modeRowDetail
|
||||||
|
m.editingValue = ""
|
||||||
|
case "enter":
|
||||||
|
if err := m.updateCell(m.selectedRow, m.selectedCol, m.editingValue); err != nil {
|
||||||
|
m.err = err
|
||||||
|
} else {
|
||||||
|
m.mode = modeRowDetail
|
||||||
|
m.editingValue = ""
|
||||||
|
}
|
||||||
|
case "backspace":
|
||||||
|
if len(m.editingValue) > 0 {
|
||||||
|
m.editingValue = m.editingValue[:len(m.editingValue)-1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(msg.String()) == 1 {
|
||||||
|
m.editingValue += msg.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle search modes
|
||||||
if m.searching {
|
if m.searching {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
@@ -308,6 +457,29 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.dataSearching {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
m.dataSearching = false
|
||||||
|
m.dataSearchInput = ""
|
||||||
|
m.filterData()
|
||||||
|
case "enter":
|
||||||
|
m.dataSearching = false
|
||||||
|
m.filterData()
|
||||||
|
case "backspace":
|
||||||
|
if len(m.dataSearchInput) > 0 {
|
||||||
|
m.dataSearchInput = m.dataSearchInput[:len(m.dataSearchInput)-1]
|
||||||
|
m.filterData()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(msg.String()) == 1 {
|
||||||
|
m.dataSearchInput += msg.String()
|
||||||
|
m.filterData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
@@ -317,12 +489,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case modeTableData, modeQuery:
|
case modeTableData, modeQuery:
|
||||||
m.mode = modeTableList
|
m.mode = modeTableList
|
||||||
m.err = nil
|
m.err = nil
|
||||||
|
case modeRowDetail:
|
||||||
|
m.mode = modeTableData
|
||||||
}
|
}
|
||||||
|
|
||||||
case "/":
|
case "/":
|
||||||
if m.mode == modeTableList {
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
m.searching = true
|
m.searching = true
|
||||||
m.searchInput = ""
|
m.searchInput = ""
|
||||||
|
case modeTableData:
|
||||||
|
m.dataSearching = true
|
||||||
|
m.dataSearchInput = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
case "enter":
|
case "enter":
|
||||||
@@ -331,8 +509,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if len(m.filteredTables) > 0 {
|
if len(m.filteredTables) > 0 {
|
||||||
m.mode = modeTableData
|
m.mode = modeTableData
|
||||||
m.currentPage = 0
|
m.currentPage = 0
|
||||||
|
m.selectedRow = 0
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
|
case modeTableData:
|
||||||
|
if len(m.filteredData) > 0 {
|
||||||
|
m.mode = modeRowDetail
|
||||||
|
m.selectedCol = 0
|
||||||
|
}
|
||||||
|
case modeRowDetail:
|
||||||
|
if len(m.filteredData) > 0 && m.selectedRow < len(m.filteredData) && m.selectedCol < len(m.columns) {
|
||||||
|
m.mode = modeEditCell
|
||||||
|
m.originalValue = m.filteredData[m.selectedRow][m.selectedCol]
|
||||||
|
m.editingValue = m.originalValue
|
||||||
|
}
|
||||||
case modeQuery:
|
case modeQuery:
|
||||||
m.executeQuery()
|
m.executeQuery()
|
||||||
}
|
}
|
||||||
@@ -348,6 +538,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.tableListPage--
|
m.tableListPage--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case modeTableData:
|
||||||
|
if m.selectedRow > 0 {
|
||||||
|
m.selectedRow--
|
||||||
|
}
|
||||||
|
case modeRowDetail:
|
||||||
|
if m.selectedCol > 0 {
|
||||||
|
m.selectedCol--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
@@ -361,6 +559,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.tableListPage++
|
m.tableListPage++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case modeTableData:
|
||||||
|
if m.selectedRow < len(m.filteredData)-1 {
|
||||||
|
m.selectedRow++
|
||||||
|
}
|
||||||
|
case modeRowDetail:
|
||||||
|
if m.selectedCol < len(m.columns)-1 {
|
||||||
|
m.selectedCol++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "left", "h":
|
case "left", "h":
|
||||||
@@ -368,6 +574,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case modeTableData:
|
case modeTableData:
|
||||||
if m.currentPage > 0 {
|
if m.currentPage > 0 {
|
||||||
m.currentPage--
|
m.currentPage--
|
||||||
|
m.selectedRow = 0
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
case modeTableList:
|
case modeTableList:
|
||||||
@@ -382,9 +589,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "right", "l":
|
case "right", "l":
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeTableData:
|
case modeTableData:
|
||||||
maxPage := (m.totalRows - 1) / pageSize
|
maxPage := max(0, (m.totalRows-1)/pageSize)
|
||||||
if m.currentPage < maxPage {
|
if m.currentPage < maxPage {
|
||||||
m.currentPage++
|
m.currentPage++
|
||||||
|
m.selectedRow = 0
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
case modeTableList:
|
case modeTableList:
|
||||||
@@ -409,9 +617,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "r":
|
case "r":
|
||||||
if m.mode == modeTableList {
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
m.loadTables()
|
m.loadTables()
|
||||||
} else if m.mode == modeTableData {
|
case modeTableData:
|
||||||
|
m.loadTableData()
|
||||||
|
case modeRowDetail:
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,14 +710,28 @@ func (m model) View() string {
|
|||||||
maxPage := max(0, (m.totalRows-1)/pageSize)
|
maxPage := max(0, (m.totalRows-1)/pageSize)
|
||||||
|
|
||||||
content.WriteString(titleStyle.Render(fmt.Sprintf("Table: %s (Page %d/%d)", tableName, m.currentPage+1, maxPage+1)))
|
content.WriteString(titleStyle.Render(fmt.Sprintf("Table: %s (Page %d/%d)", tableName, m.currentPage+1, maxPage+1)))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Search bar for data
|
||||||
|
if m.dataSearching {
|
||||||
|
content.WriteString("\nSearch data: " + m.dataSearchInput + "_")
|
||||||
|
content.WriteString("\n")
|
||||||
|
} else if m.dataSearchInput != "" {
|
||||||
|
content.WriteString(fmt.Sprintf("\nFiltered by: %s (%d/%d rows)", m.dataSearchInput, len(m.filteredData), len(m.tableData)))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
if len(m.tableData) == 0 {
|
if len(m.filteredData) == 0 {
|
||||||
content.WriteString("No data in table")
|
if m.dataSearchInput != "" {
|
||||||
|
content.WriteString("No rows match your search")
|
||||||
|
} else {
|
||||||
|
content.WriteString("No data in table")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Limit rows to fit screen
|
// Limit rows to fit screen
|
||||||
visibleRows := m.getVisibleDataRowCount()
|
visibleRows := m.getVisibleDataRowCount()
|
||||||
displayRows := min(len(m.tableData), visibleRows)
|
displayRows := min(len(m.filteredData), visibleRows)
|
||||||
|
|
||||||
// Create table header
|
// Create table header
|
||||||
var headerRow strings.Builder
|
var headerRow strings.Builder
|
||||||
@@ -534,9 +759,12 @@ func (m model) View() string {
|
|||||||
content.WriteString(separator.String())
|
content.WriteString(separator.String())
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
// Add data rows
|
// Add data rows with highlighting
|
||||||
for i := 0; i < displayRows; i++ {
|
for i := 0; i < displayRows; i++ {
|
||||||
row := m.tableData[i]
|
if i >= len(m.filteredData) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
row := m.filteredData[i]
|
||||||
var dataRow strings.Builder
|
var dataRow strings.Builder
|
||||||
for j, cell := range row {
|
for j, cell := range row {
|
||||||
if j > 0 {
|
if j > 0 {
|
||||||
@@ -544,13 +772,90 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||||
}
|
}
|
||||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
if i == m.selectedRow {
|
||||||
|
content.WriteString(selectedStyle.Render(dataRow.String()))
|
||||||
|
} else {
|
||||||
|
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||||
|
}
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(helpStyle.Render(fmt.Sprintf("←/→: prev/next page • Total rows: %d • esc: back • r: refresh • q: quit", m.totalRows)))
|
if m.dataSearching {
|
||||||
|
content.WriteString(helpStyle.Render("Type to search • enter/esc: finish search"))
|
||||||
|
} else {
|
||||||
|
content.WriteString(helpStyle.Render("↑/↓: select row • ←/→: page • /: search • enter: view row • esc: back • r: refresh • q: quit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
case modeRowDetail:
|
||||||
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
|
content.WriteString(titleStyle.Render(fmt.Sprintf("Row Detail: %s", tableName)))
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
if m.selectedRow >= len(m.filteredData) {
|
||||||
|
content.WriteString("Invalid row selection")
|
||||||
|
} else {
|
||||||
|
row := m.filteredData[m.selectedRow]
|
||||||
|
|
||||||
|
// Show as 2-column table: Column | Value
|
||||||
|
colWidth := max(15, m.width/3)
|
||||||
|
valueWidth := max(20, m.width-colWidth-5)
|
||||||
|
|
||||||
|
// Header
|
||||||
|
headerRow := fmt.Sprintf("%-*s | %-*s", colWidth, "Column", valueWidth, "Value")
|
||||||
|
content.WriteString(selectedStyle.Render(headerRow))
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
separator := strings.Repeat("-", colWidth) + "-+-" + strings.Repeat("-", valueWidth)
|
||||||
|
content.WriteString(separator)
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Data rows
|
||||||
|
visibleRows := m.getVisibleDataRowCount() - 4 // Account for header and title
|
||||||
|
displayRows := min(len(m.columns), visibleRows)
|
||||||
|
|
||||||
|
for i := 0; i < displayRows; i++ {
|
||||||
|
if i >= len(m.columns) || i >= len(row) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
col := m.columns[i]
|
||||||
|
val := row[i]
|
||||||
|
|
||||||
|
dataRow := fmt.Sprintf("%-*s | %-*s",
|
||||||
|
colWidth, truncateString(col, colWidth),
|
||||||
|
valueWidth, truncateString(val, valueWidth))
|
||||||
|
|
||||||
|
if i == m.selectedCol {
|
||||||
|
content.WriteString(selectedStyle.Render(dataRow))
|
||||||
|
} else {
|
||||||
|
content.WriteString(normalStyle.Render(dataRow))
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(helpStyle.Render("↑/↓: select field • enter: edit • esc: back • q: quit"))
|
||||||
|
|
||||||
|
case modeEditCell:
|
||||||
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
|
columnName := ""
|
||||||
|
if m.selectedCol < len(m.columns) {
|
||||||
|
columnName = m.columns[m.selectedCol]
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString(titleStyle.Render(fmt.Sprintf("Edit: %s.%s", tableName, columnName)))
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
content.WriteString("Original: " + m.originalValue)
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString("New: " + m.editingValue + "_")
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
content.WriteString(helpStyle.Render("Type new value • enter: save • esc: cancel"))
|
||||||
|
|
||||||
case modeQuery:
|
case modeQuery:
|
||||||
content.WriteString(titleStyle.Render("SQL Query"))
|
content.WriteString(titleStyle.Render("SQL Query"))
|
||||||
|
|||||||
Reference in New Issue
Block a user