mirror of
https://github.com/ddworken/hishtory.git
synced 2025-01-25 23:59:06 +01:00
Add bolding of matching search results for #112, currently behind the beta-mode flag
This commit is contained in:
parent
ca155b3a1f
commit
beb907d59e
@ -170,6 +170,21 @@ func BuildTableRow(ctx context.Context, columnNames []string, entry data.History
|
|||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make a regex that matches the non-tokenized bits of the given query
|
||||||
|
func MakeRegexFromQuery(query string) string {
|
||||||
|
tokens := tokenize(strings.TrimSpace(query))
|
||||||
|
r := ""
|
||||||
|
for _, token := range tokens {
|
||||||
|
if !strings.HasPrefix(token, "-") && !containsUnescaped(token, ":") {
|
||||||
|
if r != "" {
|
||||||
|
r += "|"
|
||||||
|
}
|
||||||
|
r += fmt.Sprintf("(%s)", regexp.QuoteMeta(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func stringArrayToAnyArray(arr []string) []any {
|
func stringArrayToAnyArray(arr []string) []any {
|
||||||
ret := make([]any, 0)
|
ret := make([]any, 0)
|
||||||
for _, item := range arr {
|
for _, item := range arr {
|
||||||
@ -711,10 +726,7 @@ func where(tx *gorm.DB, s string, v1 any, v2 any) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
|
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
|
||||||
tokens, err := tokenize(query)
|
tokens := tokenize(query)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to tokenize query: %w", err)
|
|
||||||
}
|
|
||||||
tx := db.Model(&data.HistoryEntry{}).Where("true")
|
tx := db.Model(&data.HistoryEntry{}).Where("true")
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
if strings.HasPrefix(token, "-") {
|
if strings.HasPrefix(token, "-") {
|
||||||
@ -891,11 +903,11 @@ func getAllCustomColumnNames(ctx context.Context) ([]string, error) {
|
|||||||
return ccNames, nil
|
return ccNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenize(query string) ([]string, error) {
|
func tokenize(query string) []string {
|
||||||
if query == "" {
|
if query == "" {
|
||||||
return []string{}, nil
|
return []string{}
|
||||||
}
|
}
|
||||||
return splitEscaped(query, ' ', -1), nil
|
return splitEscaped(query, ' ', -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitEscaped(query string, separator rune, maxSplit int) []string {
|
func splitEscaped(query string, separator rune, maxSplit int) []string {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
|
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
|
||||||
|
// Also includes https://github.com/charmbracelet/bubbles/pull/397/files to support cell styling
|
||||||
|
|
||||||
package table
|
package table
|
||||||
|
|
||||||
@ -31,6 +32,13 @@ type Model struct {
|
|||||||
hcursor int
|
hcursor int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CellPosition holds row and column indexes.
|
||||||
|
type CellPosition struct {
|
||||||
|
RowID int
|
||||||
|
Column int
|
||||||
|
IsRowSelected bool
|
||||||
|
}
|
||||||
|
|
||||||
// Row represents one line in the table.
|
// Row represents one line in the table.
|
||||||
type Row []string
|
type Row []string
|
||||||
|
|
||||||
@ -108,6 +116,32 @@ type Styles struct {
|
|||||||
Header lipgloss.Style
|
Header lipgloss.Style
|
||||||
Cell lipgloss.Style
|
Cell lipgloss.Style
|
||||||
Selected lipgloss.Style
|
Selected lipgloss.Style
|
||||||
|
|
||||||
|
// RenderCell is a low-level primitive for stylizing cells.
|
||||||
|
// It is responsible for rendering the selection style. Styles.Cell is ignored.
|
||||||
|
//
|
||||||
|
// Example implementation:
|
||||||
|
// s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
|
||||||
|
// cellStyle := s.Cell.Copy()
|
||||||
|
//
|
||||||
|
// switch {
|
||||||
|
// case position.IsRowSelected:
|
||||||
|
// return cellStyle.Background(lipgloss.Color("57")).Render(value)
|
||||||
|
// case position.Column == 1:
|
||||||
|
// return cellStyle.Foreground(lipgloss.Color("21")).Render(value)
|
||||||
|
// default:
|
||||||
|
// return cellStyle.Render(value)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
RenderCell func(model Model, value string, position CellPosition) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Styles) renderCell(model Model, value string, position CellPosition) string {
|
||||||
|
if s.RenderCell != nil {
|
||||||
|
return s.RenderCell(model, value, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Cell.Render(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultStyles returns a set of default style definitions for this table.
|
// DefaultStyles returns a set of default style definitions for this table.
|
||||||
@ -445,21 +479,30 @@ func (m Model) headersView() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderRow(rowID int) string {
|
func (m *Model) renderRow(rowID int) string {
|
||||||
|
isRowSelected := rowID == m.cursor
|
||||||
var s = make([]string, 0, len(m.cols))
|
var s = make([]string, 0, len(m.cols))
|
||||||
for i, value := range m.rows[rowID] {
|
for i, value := range m.rows[rowID] {
|
||||||
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
|
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
|
||||||
|
|
||||||
|
position := CellPosition{
|
||||||
|
RowID: rowID,
|
||||||
|
Column: i,
|
||||||
|
IsRowSelected: isRowSelected,
|
||||||
|
}
|
||||||
|
|
||||||
var renderedCell string
|
var renderedCell string
|
||||||
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
|
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
|
||||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…")))
|
renderedCell = style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…"))
|
||||||
} else {
|
} else {
|
||||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
|
renderedCell = style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))
|
||||||
}
|
}
|
||||||
|
renderedCell = m.styles.renderCell(*m, renderedCell, position)
|
||||||
s = append(s, renderedCell)
|
s = append(s, renderedCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
||||||
|
|
||||||
if rowID == m.cursor {
|
if isRowSelected {
|
||||||
return m.styles.Selected.Render(row)
|
return m.styles.Selected.Render(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
const TABLE_HEIGHT = 20
|
const TABLE_HEIGHT = 20
|
||||||
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
|
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
|
||||||
|
|
||||||
|
var CURRENT_QUERY_FOR_HIGHLIGHTING string = ""
|
||||||
var SELECTED_COMMAND string = ""
|
var SELECTED_COMMAND string = ""
|
||||||
|
|
||||||
var baseStyle = lipgloss.NewStyle().
|
var baseStyle = lipgloss.NewStyle().
|
||||||
@ -207,6 +209,7 @@ func initialModel(ctx context.Context, initialQuery string) model {
|
|||||||
if initialQuery != "" {
|
if initialQuery != "" {
|
||||||
queryInput.SetValue(initialQuery)
|
queryInput.SetValue(initialQuery)
|
||||||
}
|
}
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = initialQuery
|
||||||
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New()}
|
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New()}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +322,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.queryInput = i
|
m.queryInput = i
|
||||||
searchQuery := m.queryInput.Value()
|
searchQuery := m.queryInput.Value()
|
||||||
m.runQuery = &searchQuery
|
m.runQuery = &searchQuery
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = searchQuery
|
||||||
cmd3 := runQueryAndUpdateTable(m, false, false)
|
cmd3 := runQueryAndUpdateTable(m, false, false)
|
||||||
preventTableOverscrolling(m)
|
preventTableOverscrolling(m)
|
||||||
return m, tea.Batch(pendingCommands, cmd2, cmd3)
|
return m, tea.Batch(pendingCommands, cmd2, cmd3)
|
||||||
@ -579,6 +583,68 @@ func makeTable(ctx context.Context, rows []table.Row) (table.Model, error) {
|
|||||||
Foreground(lipgloss.Color("229")).
|
Foreground(lipgloss.Color("229")).
|
||||||
Background(lipgloss.Color("57")).
|
Background(lipgloss.Color("57")).
|
||||||
Bold(false)
|
Bold(false)
|
||||||
|
if config.BetaMode {
|
||||||
|
s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
|
||||||
|
MATCH_NOTHING_REGEXP := regexp.MustCompile("a^")
|
||||||
|
var re *regexp.Regexp
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = strings.TrimSpace(CURRENT_QUERY_FOR_HIGHLIGHTING)
|
||||||
|
if CURRENT_QUERY_FOR_HIGHLIGHTING == "" {
|
||||||
|
re = MATCH_NOTHING_REGEXP
|
||||||
|
} else {
|
||||||
|
queryRegex := lib.MakeRegexFromQuery(CURRENT_QUERY_FOR_HIGHLIGHTING)
|
||||||
|
r, err := regexp.Compile(queryRegex)
|
||||||
|
if err != nil {
|
||||||
|
// Failed to compile the regex for highlighting matches, this should never happen. In this
|
||||||
|
// case, just use a regexp that matches nothing to ensure that the TUI doesn't crash.
|
||||||
|
re = MATCH_NOTHING_REGEXP
|
||||||
|
} else {
|
||||||
|
re = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChunk := func(v string, isMatching, isLeftMost, isRightMost bool) string {
|
||||||
|
hctx.GetLogger().Infof("Rendering chunk=%#v with isMatching=%#v", v, isMatching)
|
||||||
|
baseStyle := lipgloss.NewStyle()
|
||||||
|
if position.IsRowSelected {
|
||||||
|
baseStyle = s.Selected.Copy()
|
||||||
|
}
|
||||||
|
if isLeftMost {
|
||||||
|
baseStyle = baseStyle.PaddingLeft(1)
|
||||||
|
}
|
||||||
|
if isRightMost {
|
||||||
|
baseStyle = baseStyle.PaddingRight(1)
|
||||||
|
}
|
||||||
|
if isMatching {
|
||||||
|
baseStyle = baseStyle.Bold(true)
|
||||||
|
}
|
||||||
|
return baseStyle.Render(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := re.FindAllStringIndex(value, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return renderChunk(value, false, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := ""
|
||||||
|
lastIncludedIdx := 0
|
||||||
|
for _, match := range re.FindAllStringIndex(value, -1) {
|
||||||
|
matchStartIdx := match[0]
|
||||||
|
matchEndIdx := match[1]
|
||||||
|
beforeMatch := value[lastIncludedIdx:matchStartIdx]
|
||||||
|
if beforeMatch != "" {
|
||||||
|
ret += renderChunk(beforeMatch, false, lastIncludedIdx == 0, false)
|
||||||
|
}
|
||||||
|
match := value[matchStartIdx:matchEndIdx]
|
||||||
|
ret += renderChunk(match, true, matchStartIdx == 0, matchEndIdx == len(value))
|
||||||
|
lastIncludedIdx = matchEndIdx
|
||||||
|
}
|
||||||
|
if lastIncludedIdx != len(value) {
|
||||||
|
ret += renderChunk(value[lastIncludedIdx:], false, false, true)
|
||||||
|
}
|
||||||
|
hctx.GetLogger().Infof("bolded=%#v, original=%#v", ret, s.Cell.Render(value))
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
t.SetStyles(s)
|
t.SetStyles(s)
|
||||||
t.Focus()
|
t.Focus()
|
||||||
return t, nil
|
return t, nil
|
||||||
|
Loading…
Reference in New Issue
Block a user