mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
add search, pagination for table view
This commit is contained in:
31
README.md
31
README.md
@@ -4,11 +4,12 @@ A fully-featured terminal user interface for browsing SQLite databases built wit
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Table Browser**: Browse all tables in your SQLite database
|
- **Table Browser**: Browse all tables in your SQLite database with pagination
|
||||||
|
- **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 (20 rows per page)
|
||||||
- **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
|
||||||
- **Navigation**: Intuitive keyboard navigation
|
- **Navigation**: Intuitive keyboard navigation
|
||||||
- **Responsive Design**: Adapts to terminal size
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -25,13 +26,20 @@ go run main.go sample.db
|
|||||||
|
|
||||||
### Table List Mode
|
### Table List Mode
|
||||||
- `↑/↓` or `k/j`: Navigate between tables
|
- `↑/↓` or `k/j`: Navigate between tables
|
||||||
|
- `←/→` or `h/l`: Navigate between table list pages
|
||||||
|
- `/`: Start searching tables
|
||||||
- `Enter`: View selected table data
|
- `Enter`: View selected table data
|
||||||
- `s`: Switch to SQL query mode
|
- `s`: Switch to SQL query mode
|
||||||
- `r`: Refresh table list
|
- `r`: Refresh table list
|
||||||
- `q` or `Ctrl+C`: Quit
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
### Search Mode (when searching tables)
|
||||||
|
- Type to search table names
|
||||||
|
- `Enter` or `Esc`: Finish search
|
||||||
|
- `Backspace`: Delete characters
|
||||||
|
|
||||||
### Table Data Mode
|
### Table Data Mode
|
||||||
- `←/→` or `h/l`: Navigate between pages
|
- `←/→` or `h/l`: Navigate between data pages
|
||||||
- `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
|
||||||
@@ -45,13 +53,16 @@ go run main.go sample.db
|
|||||||
|
|
||||||
## Features Implemented
|
## Features Implemented
|
||||||
|
|
||||||
1. **Table Browsing**: Lists all tables in the database with navigation
|
1. **Table Browsing**: Lists all tables in the database with pagination
|
||||||
2. **Paginated Data View**: Shows table data with pagination (20 rows per page)
|
2. **Table Search**: Filter tables by name using `/` to search
|
||||||
3. **SQL Query Execution**: Execute custom SQL queries and view results
|
3. **Paginated Data View**: Shows table data with pagination (20 rows per page)
|
||||||
4. **Error Handling**: Displays database errors gracefully
|
4. **Screen-Aware Display**: Content automatically fits terminal size
|
||||||
5. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
5. **SQL Query Execution**: Execute custom SQL queries and view results
|
||||||
6. **Column Information**: Shows column names and handles NULL values
|
6. **Error Handling**: Displays database errors gracefully
|
||||||
7. **Navigation**: Intuitive keyboard shortcuts for all operations
|
7. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||||
|
8. **Column Information**: Shows column names and handles NULL values
|
||||||
|
9. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||||
|
10. **Dynamic Column Width**: Columns adjust to terminal width
|
||||||
|
|
||||||
## Sample Database
|
## Sample Database
|
||||||
|
|
||||||
|
|||||||
271
main.go
271
main.go
@@ -25,20 +25,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
mode viewMode
|
mode viewMode
|
||||||
tables []string
|
tables []string
|
||||||
selectedTable int
|
filteredTables []string
|
||||||
tableData [][]string
|
selectedTable int
|
||||||
columns []string
|
tableListPage int
|
||||||
currentPage int
|
tableData [][]string
|
||||||
totalRows int
|
columns []string
|
||||||
query string
|
currentPage int
|
||||||
queryInput string
|
totalRows int
|
||||||
cursor int
|
query string
|
||||||
err error
|
queryInput string
|
||||||
width int
|
searchInput string
|
||||||
height int
|
searching bool
|
||||||
|
cursor int
|
||||||
|
err error
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -76,9 +80,15 @@ func initialModel(dbPath string) model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := model{
|
m := model{
|
||||||
db: db,
|
db: db,
|
||||||
mode: modeTableList,
|
mode: modeTableList,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
|
tableListPage: 0,
|
||||||
|
filteredTables: []string{},
|
||||||
|
searchInput: "",
|
||||||
|
searching: false,
|
||||||
|
width: 80, // default width
|
||||||
|
height: 24, // default height
|
||||||
}
|
}
|
||||||
|
|
||||||
m.loadTables()
|
m.loadTables()
|
||||||
@@ -102,14 +112,51 @@ func (m *model) loadTables() {
|
|||||||
}
|
}
|
||||||
m.tables = append(m.tables, name)
|
m.tables = append(m.tables, name)
|
||||||
}
|
}
|
||||||
|
m.filterTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) filterTables() {
|
||||||
|
if m.searchInput == "" {
|
||||||
|
m.filteredTables = make([]string, len(m.tables))
|
||||||
|
copy(m.filteredTables, m.tables)
|
||||||
|
} else {
|
||||||
|
m.filteredTables = []string{}
|
||||||
|
searchLower := strings.ToLower(m.searchInput)
|
||||||
|
for _, table := range m.tables {
|
||||||
|
if strings.Contains(strings.ToLower(table), searchLower) {
|
||||||
|
m.filteredTables = append(m.filteredTables, table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset selection and page if needed
|
||||||
|
if m.selectedTable >= len(m.filteredTables) {
|
||||||
|
m.selectedTable = 0
|
||||||
|
m.tableListPage = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) getVisibleTableCount() int {
|
||||||
|
// Reserve space for title (3 lines), help (3 lines), search bar (2 lines if searching)
|
||||||
|
reservedLines := 8
|
||||||
|
if m.searching {
|
||||||
|
reservedLines += 2
|
||||||
|
}
|
||||||
|
return max(1, m.height-reservedLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) getVisibleDataRowCount() int {
|
||||||
|
// Reserve space for title (3 lines), header (3 lines), help (3 lines)
|
||||||
|
reservedLines := 9
|
||||||
|
return max(1, m.height-reservedLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) loadTableData() {
|
func (m *model) loadTableData() {
|
||||||
if m.selectedTable >= len(m.tables) {
|
if m.selectedTable >= len(m.filteredTables) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tableName := m.tables[m.selectedTable]
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
|
|
||||||
// Get column info
|
// Get column info
|
||||||
rows, err := m.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
rows, err := m.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||||
@@ -237,6 +284,30 @@ 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
|
||||||
|
if m.searching {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
m.searching = false
|
||||||
|
m.searchInput = ""
|
||||||
|
m.filterTables()
|
||||||
|
case "enter":
|
||||||
|
m.searching = false
|
||||||
|
m.filterTables()
|
||||||
|
case "backspace":
|
||||||
|
if len(m.searchInput) > 0 {
|
||||||
|
m.searchInput = m.searchInput[:len(m.searchInput)-1]
|
||||||
|
m.filterTables()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(msg.String()) == 1 {
|
||||||
|
m.searchInput += msg.String()
|
||||||
|
m.filterTables()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
@@ -248,10 +319,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = nil
|
m.err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "/":
|
||||||
|
if m.mode == modeTableList {
|
||||||
|
m.searching = true
|
||||||
|
m.searchInput = ""
|
||||||
|
}
|
||||||
|
|
||||||
case "enter":
|
case "enter":
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeTableList:
|
case modeTableList:
|
||||||
if len(m.tables) > 0 {
|
if len(m.filteredTables) > 0 {
|
||||||
m.mode = modeTableData
|
m.mode = modeTableData
|
||||||
m.currentPage = 0
|
m.currentPage = 0
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
@@ -265,14 +342,24 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case modeTableList:
|
case modeTableList:
|
||||||
if m.selectedTable > 0 {
|
if m.selectedTable > 0 {
|
||||||
m.selectedTable--
|
m.selectedTable--
|
||||||
|
// Check if we need to scroll up
|
||||||
|
visibleCount := m.getVisibleTableCount()
|
||||||
|
if m.selectedTable < m.tableListPage*visibleCount {
|
||||||
|
m.tableListPage--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeTableList:
|
case modeTableList:
|
||||||
if m.selectedTable < len(m.tables)-1 {
|
if m.selectedTable < len(m.filteredTables)-1 {
|
||||||
m.selectedTable++
|
m.selectedTable++
|
||||||
|
// Check if we need to scroll down
|
||||||
|
visibleCount := m.getVisibleTableCount()
|
||||||
|
if m.selectedTable >= (m.tableListPage+1)*visibleCount {
|
||||||
|
m.tableListPage++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +370,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.currentPage--
|
m.currentPage--
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
|
case modeTableList:
|
||||||
|
if m.tableListPage > 0 {
|
||||||
|
m.tableListPage--
|
||||||
|
// Adjust selection to stay in view
|
||||||
|
visibleCount := m.getVisibleTableCount()
|
||||||
|
m.selectedTable = m.tableListPage * visibleCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "right", "l":
|
case "right", "l":
|
||||||
@@ -293,6 +387,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.currentPage++
|
m.currentPage++
|
||||||
m.loadTableData()
|
m.loadTableData()
|
||||||
}
|
}
|
||||||
|
case modeTableList:
|
||||||
|
visibleCount := m.getVisibleTableCount()
|
||||||
|
maxPage := (len(m.filteredTables) - 1) / visibleCount
|
||||||
|
if m.tableListPage < maxPage {
|
||||||
|
m.tableListPage++
|
||||||
|
// Adjust selection to stay in view
|
||||||
|
m.selectedTable = m.tableListPage * visibleCount
|
||||||
|
if m.selectedTable >= len(m.filteredTables) {
|
||||||
|
m.selectedTable = len(m.filteredTables) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "s":
|
case "s":
|
||||||
@@ -318,8 +423,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
if m.mode == modeQuery {
|
if m.mode == modeQuery {
|
||||||
m.queryInput += msg.String()
|
if len(msg.String()) == 1 {
|
||||||
m.query = m.queryInput
|
m.queryInput += msg.String()
|
||||||
|
m.query = m.queryInput
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,13 +443,34 @@ func (m model) View() string {
|
|||||||
|
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeTableList:
|
case modeTableList:
|
||||||
|
// Title
|
||||||
content.WriteString(titleStyle.Render("SQLite TUI - Tables"))
|
content.WriteString(titleStyle.Render("SQLite TUI - Tables"))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
if m.searching {
|
||||||
|
content.WriteString("\nSearch: " + m.searchInput + "_")
|
||||||
|
content.WriteString("\n")
|
||||||
|
} else if m.searchInput != "" {
|
||||||
|
content.WriteString(fmt.Sprintf("\nFiltered by: %s (%d/%d tables)", m.searchInput, len(m.filteredTables), len(m.tables)))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
if len(m.tables) == 0 {
|
// Table list with pagination
|
||||||
content.WriteString("No tables found in database")
|
if len(m.filteredTables) == 0 {
|
||||||
|
if m.searchInput != "" {
|
||||||
|
content.WriteString("No tables match your search")
|
||||||
|
} else {
|
||||||
|
content.WriteString("No tables found in database")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for i, table := range m.tables {
|
visibleCount := m.getVisibleTableCount()
|
||||||
|
startIdx := m.tableListPage * visibleCount
|
||||||
|
endIdx := min(startIdx+visibleCount, len(m.filteredTables))
|
||||||
|
|
||||||
|
for i := startIdx; i < endIdx; i++ {
|
||||||
|
table := m.filteredTables[i]
|
||||||
if i == m.selectedTable {
|
if i == m.selectedTable {
|
||||||
content.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", table)))
|
content.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", table)))
|
||||||
} else {
|
} else {
|
||||||
@@ -350,14 +478,25 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show pagination info
|
||||||
|
if len(m.filteredTables) > visibleCount {
|
||||||
|
totalPages := (len(m.filteredTables) - 1) / visibleCount + 1
|
||||||
|
content.WriteString(fmt.Sprintf("\nPage %d/%d", m.tableListPage+1, totalPages))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Help
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(helpStyle.Render("↑/↓: navigate • enter: view table • s: SQL query • r: refresh • q: quit"))
|
if m.searching {
|
||||||
|
content.WriteString(helpStyle.Render("Type to search • enter/esc: finish search"))
|
||||||
|
} else {
|
||||||
|
content.WriteString(helpStyle.Render("↑/↓: navigate • ←/→: page • /: search • enter: view • s: SQL • r: refresh • q: quit"))
|
||||||
|
}
|
||||||
|
|
||||||
case modeTableData:
|
case modeTableData:
|
||||||
tableName := m.tables[m.selectedTable]
|
tableName := m.filteredTables[m.selectedTable]
|
||||||
maxPage := (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\n")
|
||||||
@@ -365,13 +504,21 @@ func (m model) View() string {
|
|||||||
if len(m.tableData) == 0 {
|
if len(m.tableData) == 0 {
|
||||||
content.WriteString("No data in table")
|
content.WriteString("No data in table")
|
||||||
} else {
|
} else {
|
||||||
|
// Limit rows to fit screen
|
||||||
|
visibleRows := m.getVisibleDataRowCount()
|
||||||
|
displayRows := min(len(m.tableData), visibleRows)
|
||||||
|
|
||||||
// Create table header
|
// Create table header
|
||||||
var headerRow strings.Builder
|
var headerRow strings.Builder
|
||||||
|
colWidth := 10 // default minimum width
|
||||||
|
if len(m.columns) > 0 && m.width > 0 {
|
||||||
|
colWidth = max(10, (m.width-len(m.columns)*3)/len(m.columns))
|
||||||
|
}
|
||||||
for i, col := range m.columns {
|
for i, col := range m.columns {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
headerRow.WriteString(" | ")
|
headerRow.WriteString(" | ")
|
||||||
}
|
}
|
||||||
headerRow.WriteString(fmt.Sprintf("%-15s", truncateString(col, 15)))
|
headerRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(col, colWidth)))
|
||||||
}
|
}
|
||||||
content.WriteString(selectedStyle.Render(headerRow.String()))
|
content.WriteString(selectedStyle.Render(headerRow.String()))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
@@ -382,19 +529,20 @@ func (m model) View() string {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
separator.WriteString("-+-")
|
separator.WriteString("-+-")
|
||||||
}
|
}
|
||||||
separator.WriteString(strings.Repeat("-", 15))
|
separator.WriteString(strings.Repeat("-", colWidth))
|
||||||
}
|
}
|
||||||
content.WriteString(separator.String())
|
content.WriteString(separator.String())
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
// Add data rows
|
// Add data rows
|
||||||
for _, row := range m.tableData {
|
for i := 0; i < displayRows; i++ {
|
||||||
|
row := m.tableData[i]
|
||||||
var dataRow strings.Builder
|
var dataRow strings.Builder
|
||||||
for i, cell := range row {
|
for j, cell := range row {
|
||||||
if i > 0 {
|
if j > 0 {
|
||||||
dataRow.WriteString(" | ")
|
dataRow.WriteString(" | ")
|
||||||
}
|
}
|
||||||
dataRow.WriteString(fmt.Sprintf("%-15s", truncateString(cell, 15)))
|
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||||
}
|
}
|
||||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
@@ -414,13 +562,21 @@ func (m model) View() string {
|
|||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
if len(m.tableData) > 0 {
|
if len(m.tableData) > 0 {
|
||||||
|
// Limit rows to fit screen
|
||||||
|
visibleRows := m.getVisibleDataRowCount() - 2 // Account for query input
|
||||||
|
displayRows := min(len(m.tableData), visibleRows)
|
||||||
|
|
||||||
// Show query results
|
// Show query results
|
||||||
|
colWidth := 10 // default minimum width
|
||||||
|
if len(m.columns) > 0 && m.width > 0 {
|
||||||
|
colWidth = max(10, (m.width-len(m.columns)*3)/len(m.columns))
|
||||||
|
}
|
||||||
var headerRow strings.Builder
|
var headerRow strings.Builder
|
||||||
for i, col := range m.columns {
|
for i, col := range m.columns {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
headerRow.WriteString(" | ")
|
headerRow.WriteString(" | ")
|
||||||
}
|
}
|
||||||
headerRow.WriteString(fmt.Sprintf("%-15s", truncateString(col, 15)))
|
headerRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(col, colWidth)))
|
||||||
}
|
}
|
||||||
content.WriteString(selectedStyle.Render(headerRow.String()))
|
content.WriteString(selectedStyle.Render(headerRow.String()))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
@@ -430,29 +586,44 @@ func (m model) View() string {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
separator.WriteString("-+-")
|
separator.WriteString("-+-")
|
||||||
}
|
}
|
||||||
separator.WriteString(strings.Repeat("-", 15))
|
separator.WriteString(strings.Repeat("-", colWidth))
|
||||||
}
|
}
|
||||||
content.WriteString(separator.String())
|
content.WriteString(separator.String())
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
for _, row := range m.tableData {
|
for i := 0; i < displayRows; i++ {
|
||||||
|
row := m.tableData[i]
|
||||||
var dataRow strings.Builder
|
var dataRow strings.Builder
|
||||||
for i, cell := range row {
|
for j, cell := range row {
|
||||||
if i > 0 {
|
if j > 0 {
|
||||||
dataRow.WriteString(" | ")
|
dataRow.WriteString(" | ")
|
||||||
}
|
}
|
||||||
dataRow.WriteString(fmt.Sprintf("%-15s", truncateString(cell, 15)))
|
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||||
}
|
}
|
||||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(m.tableData) > displayRows {
|
||||||
|
content.WriteString(helpStyle.Render(fmt.Sprintf("... and %d more rows", len(m.tableData)-displayRows)))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(helpStyle.Render("enter: execute query • esc: back • q: quit"))
|
content.WriteString(helpStyle.Render("enter: execute query • esc: back • q: quit"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return tableStyle.Render(content.String())
|
// Ensure content fits in screen height
|
||||||
|
lines := strings.Split(content.String(), "\n")
|
||||||
|
maxLines := max(1, m.height-2)
|
||||||
|
if len(lines) > maxLines {
|
||||||
|
lines = lines[:maxLines]
|
||||||
|
content.Reset()
|
||||||
|
content.WriteString(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func truncateString(s string, maxLen int) string {
|
func truncateString(s string, maxLen int) string {
|
||||||
@@ -462,6 +633,20 @@ func truncateString(s string, maxLen int) string {
|
|||||||
return s[:maxLen-3] + "..."
|
return s[:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Println("Usage: go-sqlite-tui <database.db>")
|
fmt.Println("Usage: go-sqlite-tui <database.db>")
|
||||||
|
|||||||
Reference in New Issue
Block a user