From 4298e47aba5c9971795d6a44c9dad515e5324cf0 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:12:40 -0700 Subject: [PATCH 01/16] [cliptext-] clipdraw_chunks: for string cattr, reset color between chunks This should change no current behavior, as clipdraw_chunks was only being called with cattr that were dicts or empty strings. --- visidata/cliptext.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/visidata/cliptext.py b/visidata/cliptext.py index 70767f222..05e677a53 100644 --- a/visidata/cliptext.py +++ b/visidata/cliptext.py @@ -246,10 +246,12 @@ def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clea for colorstate, chunk in chunks: if colorstate: if isinstance(colorstate, str): - cattr = cattr.update(colors.get_color(colorstate), 100) + cattr = origattr.update(colors.get_color(colorstate), 100) else: cattr = origattr.update(colorstate['cattr'], 100) link = colorstate['link'] + else: + cattr = origattr if not chunk: continue From 4d04c0e8325a96d812f7e93835b3b37a85ab7934 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:14:40 -0700 Subject: [PATCH 02/16] [cliptext-] make clipdraw_chunks accept ColorAttr objects instead of silently failing --- visidata/cliptext.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/visidata/cliptext.py b/visidata/cliptext.py index 05e677a53..0815b7106 100644 --- a/visidata/cliptext.py +++ b/visidata/cliptext.py @@ -245,7 +245,9 @@ def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clea try: for colorstate, chunk in chunks: if colorstate: - if isinstance(colorstate, str): + if isinstance(colorstate, ColorAttr): + cattr = origattr.update(colorstate, 100) + elif isinstance(colorstate, str): cattr = origattr.update(colors.get_color(colorstate), 100) else: cattr = origattr.update(colorstate['cattr'], 100) From 7242cee0b165b725c02932c4d4118163006d4d7c Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:27:20 -0700 Subject: [PATCH 03/16] [sheets-] add highlighting of search term to / and g/ --- visidata/cmdlog.py | 4 +-- visidata/column.py | 1 + visidata/search.py | 54 +++++++++++++++++++++++++++++---- visidata/sheets.py | 29 ++++++++++++++---- visidata/tests/test_commands.py | 2 ++ 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/visidata/cmdlog.py b/visidata/cmdlog.py index 795c16681..85456416e 100644 --- a/visidata/cmdlog.py +++ b/visidata/cmdlog.py @@ -14,8 +14,8 @@ nonLogged = '''forget exec-longname undo redo quit show error errors statuses options threads jump replay cancel save-cmdlog macro cmdlog-sheet menu repeat reload-every -search scroll prev next page start end zoom visibility sidebar -mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split() +scroll prev next page start end zoom visibility sidebar +mouse suspend no-op help syscopy sysopen profile toggle'''.split() vd.option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None) diff --git a/visidata/column.py b/visidata/column.py index 7d9cf37d5..19aad4697 100644 --- a/visidata/column.py +++ b/visidata/column.py @@ -80,6 +80,7 @@ def __init__(self, name=None, *, type=anytype, cache=False, **kwargs): self.displayer = '' self.defer = False self.disp_expert = 0 # do not show if 'nometacols' in options.disp_help_flags + self.highlight_regex = None self.setCache(cache) for k, v in kwargs.items(): diff --git a/visidata/search.py b/visidata/search.py index 2423eafc2..ff12a2c5b 100644 --- a/visidata/search.py +++ b/visidata/search.py @@ -86,8 +86,23 @@ def searchInputRegex(sheet, action:str, columns:str='cursorCol'): def moveInputRegex(sheet, action:str, type="regex", **kwargs): r = vd.inputMultiple(regex=dict(prompt=f"{action} regex: ", type=type, defaultLast=True, help=vd.help_regex), flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) + vd.moveRegex(sheet, regex=r['regex'], regex_flags=r['flags'], **kwargs) + return r - return vd.moveRegex(sheet, regex=r['regex'], regex_flags=r['flags'], **kwargs) +@Sheet.api +def setHighlightRegex(sheet, r, cols=[]): + if not sheet.options.highlight: + return + flagbits = sum(getattr(re, f.upper()) for f in r['flags']) + rc = re.compile(r['regex'], flagbits) + sheet.highlight_clear() + if cols is None: + vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) + sheet.highlight_regex = rc + else: + for col in cols: + vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) + col.highlight_regex = rc @Sheet.api @asyncthread @@ -102,15 +117,35 @@ def search_expr(sheet, expr, reverse=False, curcol=None): vd.fail(f'no {sheet.rowtype} where {expr}') +@Sheet.api +def highlight_input(sheet, cols=[]): + r = vd.inputMultiple(regex=dict(prompt=f"highlight regex: ", type="regex", defaultLast=True, help=vd.help_regex), + flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) + if not sheet.options.highlight: + vd.warning('highlight option needs to be set to True') + setHighlightRegex(sheet, r, cols) -Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; moveInputRegex("row key", type="regex-row", columns=keyCols or [visibleCols[0]]); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') -Sheet.addCommand('/', 'search-col', 'moveInputRegex("search", columns="cursorCol", backward=False)', 'search for regex forwards in current column') -Sheet.addCommand('?', 'searchr-col', 'moveInputRegex("reverse search", columns="cursorCol", backward=True)', 'search for regex backwards in current column') +@Sheet.api +def highlight_clear(sheet): + if not sheet.options.highlight: + return + for col in sheet.columns: + if col.highlight_regex: + vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) + col.highlight_regex = None + if sheet.highlight_regex: + vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) + sheet.highlight_regex = None + +Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; cols=keyCols or [visibleCols[0]]; r=moveInputRegex("row key", type="regex-row", columns=cols); setHighlightRegex(r, cols); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') +Sheet.addCommand('/', 'search-col', 'r=moveInputRegex("search", columns="cursorCol", backward=False); setHighlightRegex(r, [cursorCol])', 'search for regex forwards in current column') + +Sheet.addCommand('?', 'searchr-col', 'r=moveInputRegex("reverse search", columns="cursorCol", backward=True); setHighlightRegex(r, [cursorCol])', 'search for regex backwards in current column') Sheet.addCommand('n', 'search-next', 'vd.moveRegex(sheet, reverse=False)', 'go to next match from last regex search') Sheet.addCommand('N', 'searchr-next', 'vd.moveRegex(sheet, reverse=True)', 'go to previous match from last regex search') -Sheet.addCommand('g/', 'search-cols', 'moveInputRegex("g/", backward=False, columns="visibleCols")', 'search for regex forwards over all visible columns') -Sheet.addCommand('g?', 'searchr-cols', 'moveInputRegex("g?", backward=True, columns="visibleCols")', 'search for regex backwards over all visible columns') +Sheet.addCommand('g/', 'search-cols', 'r=moveInputRegex("g/", backward=False, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex forwards over all visible columns') +Sheet.addCommand('g?', 'searchr-cols', 'r=moveInputRegex("g?", backward=True, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex backwards over all visible columns') Sheet.addCommand('z/', 'search-expr', 'search_expr(inputExpr("search by expr: ") or fail("no expr"), curcol=cursorCol)', 'search by Python expression forwards in current column (with column names as variables)') Sheet.addCommand('z?', 'searchr-expr', 'search_expr(inputExpr("searchr by expr: ") or fail("no expr"), curcol=cursorCol, reverse=True)', 'search by Python expression backwards in current column (with column names as variables)') @@ -125,3 +160,10 @@ def search_expr(sheet, expr, reverse=False, curcol=None): View > Search backward > by Python expr > searchr-expr View > Search backward > again > searchr-next ''') + +vd.option('highlight', True, 'whether to highlight strings in searches') +vd.option('color_highlight_search', '21 on 15', 'color to use for highlighting search results', sheettype=None) #bright blue on white + +Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') +Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column') +Sheet.addCommand('', 'highlight-clear', 'highlight_clear()', 'clear the current highlight pattern') diff --git a/visidata/sheets.py b/visidata/sheets.py index b34831052..47b0e5c5a 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -2,6 +2,7 @@ import itertools from copy import copy, deepcopy import textwrap +import re from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException, Fanout from visidata import (options, Column, namedlist, SettableColumn, AttrDict, DisplayWrapper, @@ -196,6 +197,7 @@ def __init__(self, *names, rows=UNLOADED, **kwargs): self._ordering = list(type(self)._ordering) #2254 self._colorizers = self.classColorizers + self.highlight_regex = None self.recalc() # set .sheet on columns and start caches self.__dict__.update(kwargs) # also done earlier in BaseSheet.__init__ @@ -1040,20 +1042,35 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, elif len(lines) < height: lines.extend([[('', '')]]*(height-len(lines))) + if self.options.highlight: + hp = col.highlight_regex or self.highlight_regex + hl_attr = colors.get_color(options.color_highlight_search) + else: + hp = None for i, chunks in enumerate(lines): y = ybase+i sepchars = seps[i] pre = disp_truncator if hoffset != 0 else disp_column_fill - prechunks = [] + display_chunks = [] if colwidth > 2: - prechunks.append(('', pre)) + display_chunks.append(('', pre)) for attr, text in chunks: - prechunks.append((attr, text[hoffset:])) - - clipdraw_chunks(scr, y, x, prechunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) + last = hoffset if hoffset > 0 else 0 + # note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints + matches = re.finditer(hp, text) if hp else [] + for m in matches: + m1 = m.start() + m2 = m.end() + display_chunks.append((attr, shown[last:m1])) + display_chunks.append((hl_attr, shown[m1:m2])) + last = m2 + if last < len(text): + display_chunks.append((attr, shown[last:])) + + clipdraw_chunks(scr, y, x, display_chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell') if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth-1: @@ -1310,7 +1327,7 @@ def reload_or_replace(sheet): BaseSheet.addCommand('gTab', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen') BaseSheet.addCommand('zZ', 'splitwin-input', 'vd.options.disp_splitwin_pct = input("% height for split window: ", value=vd.options.disp_splitwin_pct)', 'set split pane to specific size') -BaseSheet.addCommand('Ctrl+L', 'redraw', 'sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen') +BaseSheet.addCommand('Ctrl+L', 'redraw', 'highlight_clear(); sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen') BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit') BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit') BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet') diff --git a/visidata/tests/test_commands.py b/visidata/tests/test_commands.py index 549a74dad..e8a215b2c 100644 --- a/visidata/tests/test_commands.py +++ b/visidata/tests/test_commands.py @@ -120,6 +120,8 @@ def isTestableCommand(longname, cmdlist): 'row': '5', 'addcol-aggregate': 'max', 'define-command': 'type-test cursorCol.type = str', + 'highlight-sheet': 'e..', + 'highlight-col': '[0-9]', } @pytest.mark.usefixtures('curses_setup') From e6ab8c62d15b5aadbedd8c7615f2c269ec49e3e6 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:49:55 -0700 Subject: [PATCH 04/16] [sheets-] highlight partially visible match on left edge of col --- visidata/sheets.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index 47b0e5c5a..3234d52e4 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1064,11 +1064,16 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, for m in matches: m1 = m.start() m2 = m.end() - display_chunks.append((attr, shown[last:m1])) - display_chunks.append((hl_attr, shown[m1:m2])) + if m1 < hoffset: + if m2 <= hoffset: + continue + m1 = hoffset + if m1 > last: + display_chunks.append((attr, text[last:m1])) + display_chunks.append((hl_attr, text[m1:m2])) last = m2 if last < len(text): - display_chunks.append((attr, shown[last:])) + display_chunks.append((attr, text[last:])) clipdraw_chunks(scr, y, x, display_chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell') From dc827c1a03f81f033bb91d23f305891cff039fce Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Fri, 26 Sep 2025 01:12:17 -0700 Subject: [PATCH 05/16] [sheets-] highlight truncator on column left edge --- visidata/sheets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index 3234d52e4..86d3a9531 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1052,11 +1052,9 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, sepchars = seps[i] - pre = disp_truncator if hoffset != 0 else disp_column_fill display_chunks = [] - if colwidth > 2: - display_chunks.append(('', pre)) + left_hl = False for attr, text in chunks: last = hoffset if hoffset > 0 else 0 # note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints @@ -1065,6 +1063,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, m1 = m.start() m2 = m.end() if m1 < hoffset: + left_hl = True if m2 <= hoffset: continue m1 = hoffset @@ -1074,6 +1073,9 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, last = m2 if last < len(text): display_chunks.append((attr, text[last:])) + if colwidth > 2: + pre = disp_truncator if hoffset != 0 else disp_column_fill + display_chunks.insert(0, (hl_attr if left_hl else cattr, pre)) clipdraw_chunks(scr, y, x, display_chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell') From 30e63053a7d83b3ae27813c2afddb741266cc34e Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 27 Sep 2025 01:49:00 -0700 Subject: [PATCH 06/16] [sheets-] highlight truncator on column right edge --- visidata/sheets.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index 86d3a9531..4e54ca37b 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1055,6 +1055,8 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, display_chunks = [] left_hl = False + right_hl = False + dispw = 0 for attr, text in chunks: last = hoffset if hoffset > 0 else 0 # note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints @@ -1068,16 +1070,29 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, continue m1 = hoffset if m1 > last: - display_chunks.append((attr, text[last:m1])) - display_chunks.append((hl_attr, text[m1:m2])) + s = text[last:m1] + display_chunks.append((attr, s)) + dispw += dispwidth(s) + s = text[m1:m2] + display_chunks.append((hl_attr, s)) + dispw += dispwidth(text[m1:m2]) + if dispw > colwidth-notewidth-1: + right_hl = True + last = len(text) + break last = m2 if last < len(text): - display_chunks.append((attr, text[last:])) + s = text[last:] + display_chunks.append((attr, s)) + dispw += dispwidth(s) if colwidth > 2: pre = disp_truncator if hoffset != 0 else disp_column_fill display_chunks.insert(0, (hl_attr if left_hl else cattr, pre)) clipdraw_chunks(scr, y, x, display_chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) + if right_hl: + hl_attr = update_attr(cattr, hl_attr, 100) + clipdraw(scr, y, x+(colwidth-notewidth-1), disp_truncator, hl_attr, w=dispwidth(disp_truncator)) vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell') if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth-1: From 610e67c404ccdadb3f2c8bcba4ec70934a797a2f Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Fri, 5 Dec 2025 00:58:01 -0800 Subject: [PATCH 07/16] [sheets-] draw truncator when highlighted clip ends at right edge --- visidata/sheets.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index 4e54ca37b..e1c94ab28 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1056,6 +1056,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, left_hl = False right_hl = False + truncate_right = False #becomes True only if the highlighted chunk ends at the col edge dispw = 0 for attr, text in chunks: last = hoffset if hoffset > 0 else 0 @@ -1071,19 +1072,31 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, m1 = hoffset if m1 > last: s = text[last:m1] - display_chunks.append((attr, s)) + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([attr, s]) dispw += dispwidth(s) s = text[m1:m2] - display_chunks.append((hl_attr, s)) + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([hl_attr, s]) dispw += dispwidth(text[m1:m2]) if dispw > colwidth-notewidth-1: right_hl = True last = len(text) break + if dispw == colwidth-notewidth-1: + #append any subsequent cell text to the highlighted chunk so it gets a truncator added by clipdraw() + truncate_right = True last = m2 if last < len(text): s = text[last:] - display_chunks.append((attr, s)) + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([attr, s]) dispw += dispwidth(s) if colwidth > 2: pre = disp_truncator if hoffset != 0 else disp_column_fill From 1d39352c099e3bd9241dc7c64132d2dbd95a6ac3 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:46:53 -0800 Subject: [PATCH 08/16] [sheets-] move highlight into its own function --- visidata/sheets.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index e1c94ab28..eb94b0c9f 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1047,13 +1047,11 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, hl_attr = colors.get_color(options.color_highlight_search) else: hp = None - for i, chunks in enumerate(lines): - y = ybase+i - sepchars = seps[i] + def _highlight(chunks): + '''consumes the generator *chunks*''' display_chunks = [] - left_hl = False right_hl = False truncate_right = False #becomes True only if the highlighted chunk ends at the col edge @@ -1101,8 +1099,15 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, if colwidth > 2: pre = disp_truncator if hoffset != 0 else disp_column_fill display_chunks.insert(0, (hl_attr if left_hl else cattr, pre)) + return display_chunks, right_hl + + for i, chunks in enumerate(lines): + y = ybase+i + + sepchars = seps[i] - clipdraw_chunks(scr, y, x, display_chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) + chunks, right_hl = _highlight(chunks) + clipdraw_chunks(scr, y, x, chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) if right_hl: hl_attr = update_attr(cattr, hl_attr, 100) clipdraw(scr, y, x+(colwidth-notewidth-1), disp_truncator, hl_attr, w=dispwidth(disp_truncator)) From 2190ba85d6ccc9d7fb31fdc10fb9558b89a066cb Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Wed, 7 Jan 2026 00:37:04 -0800 Subject: [PATCH 09/16] [cmdlog-] replace search- and redraw- in nonLogged --- visidata/cmdlog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/visidata/cmdlog.py b/visidata/cmdlog.py index 85456416e..795c16681 100644 --- a/visidata/cmdlog.py +++ b/visidata/cmdlog.py @@ -14,8 +14,8 @@ nonLogged = '''forget exec-longname undo redo quit show error errors statuses options threads jump replay cancel save-cmdlog macro cmdlog-sheet menu repeat reload-every -scroll prev next page start end zoom visibility sidebar -mouse suspend no-op help syscopy sysopen profile toggle'''.split() +search scroll prev next page start end zoom visibility sidebar +mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split() vd.option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None) From 7f439f4f17e249adbda90f56b341da16c0b22df0 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Wed, 7 Jan 2026 00:38:31 -0800 Subject: [PATCH 10/16] [search-] add non-numeric colors to highlight color --- visidata/search.py | 2 +- visidata/sheets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/visidata/search.py b/visidata/search.py index ff12a2c5b..c43113bca 100644 --- a/visidata/search.py +++ b/visidata/search.py @@ -162,7 +162,7 @@ def highlight_clear(sheet): ''') vd.option('highlight', True, 'whether to highlight strings in searches') -vd.option('color_highlight_search', '21 on 15', 'color to use for highlighting search results', sheettype=None) #bright blue on white +vd.option('color_highlight_search', '21 blue on 15 white', 'color to use for highlighting search results', sheettype=None) #bright blue on white Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column') diff --git a/visidata/sheets.py b/visidata/sheets.py index eb94b0c9f..ed2f4febc 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1044,7 +1044,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, if self.options.highlight: hp = col.highlight_regex or self.highlight_regex - hl_attr = colors.get_color(options.color_highlight_search) + hl_attr = colors.color_highlight_search else: hp = None From aacae39bd5704382c77ddb4905093e523dbddf42 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:48:20 -0800 Subject: [PATCH 11/16] [hlsearch-] move highlight code to features/ --- visidata/column.py | 1 - visidata/features/hlsearch.py | 110 ++++++++++++++++++++++++++++++++++ visidata/search.py | 52 ++-------------- visidata/sheets.py | 62 ++----------------- 4 files changed, 121 insertions(+), 104 deletions(-) create mode 100644 visidata/features/hlsearch.py diff --git a/visidata/column.py b/visidata/column.py index 19aad4697..7d9cf37d5 100644 --- a/visidata/column.py +++ b/visidata/column.py @@ -80,7 +80,6 @@ def __init__(self, name=None, *, type=anytype, cache=False, **kwargs): self.displayer = '' self.defer = False self.disp_expert = 0 # do not show if 'nometacols' in options.disp_help_flags - self.highlight_regex = None self.setCache(cache) for k, v in kwargs.items(): diff --git a/visidata/features/hlsearch.py b/visidata/features/hlsearch.py new file mode 100644 index 000000000..ed35e3bc9 --- /dev/null +++ b/visidata/features/hlsearch.py @@ -0,0 +1,110 @@ +from visidata import Sheet, TableSheet, Column +from visidata import vd, dispwidth +import re + + +TableSheet.init('highlight_regex', lambda: None, copy=False) +Column.init('highlight_regex', lambda: None, copy=False) + +@Sheet.api +def highlight_chunks(sheet, chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr): + '''Highlights *hp* which is a regex to highlight, or None if no highlighting. + Always handles formatting for *hoffset* even when *hp* is None. + Consumes the generator *chunks*. Returns a list of (attr, string) pairs. + ''' + display_chunks = [] + left_hl = False + right_hl = False + truncate_right = False #becomes True only if the highlighted chunk ends at the col edge + dispw = 0 + for attr, text in chunks: + last = hoffset if hoffset > 0 else 0 + # note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints + matches = re.finditer(hp, text) if hp else [] + for m in matches: + m1 = m.start() + m2 = m.end() + if m1 < hoffset: + left_hl = True + if m2 <= hoffset: + continue + m1 = hoffset + if m1 > last: + s = text[last:m1] + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([attr, s]) + dispw += dispwidth(s) + s = text[m1:m2] + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([hl_attr, s]) + dispw += dispwidth(text[m1:m2]) + if dispw > colwidth-notewidth-1: + right_hl = True + last = len(text) + break + if dispw == colwidth-notewidth-1: + #append any subsequent cell text to the highlighted chunk so it gets a truncator added by clipdraw() + truncate_right = True + last = m2 + if last < len(text): + s = text[last:] + if truncate_right: + display_chunks[-1][1] += s + else: + display_chunks.append([attr, s]) + dispw += dispwidth(s) + return display_chunks, left_hl, right_hl + +@Sheet.api +def setHighlightRegex(sheet, r, cols=[]): + if not sheet.options.highlight: + return + flagbits = sum(getattr(re, f.upper()) for f in r['flags']) + rc = re.compile(r['regex'], flagbits) + sheet.highlight_clear() + if cols is None: + vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) + sheet.highlight_regex = rc + else: + for col in cols: + vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) + col.highlight_regex = rc + +@Sheet.api +def highlight_input(sheet, cols=[]): + r = vd.inputMultiple(regex=dict(prompt=f"highlight regex: ", type="regex", defaultLast=True, help=vd.help_regex), + flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) + if not sheet.options.highlight: + vd.warning('highlight option needs to be set to True') + setHighlightRegex(sheet, r, cols) + +@Sheet.api +def highlight_clear(sheet): + if not sheet.options.highlight: + return + for col in sheet.columns: + if col.highlight_regex: + vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) + col.highlight_regex = None + if sheet.highlight_regex: + vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) + sheet.highlight_regex = None + +vd.option('highlight', True, 'whether to highlight strings in searches') +vd.option('color_highlight_search', '21 blue on 15 white', 'color to use for highlighting search results', sheettype=None) #bright blue on white + +Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') +Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column') +Sheet.addCommand('', 'highlight-clear', 'highlight_clear()', 'clear the current highlight pattern') +# redefine existing commands +Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; cols=keyCols or [visibleCols[0]]; r=moveInputRegex("row key", type="regex-row", columns=cols); setHighlightRegex(r, cols); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') +Sheet.addCommand('/', 'search-col', 'r=moveInputRegex("search", columns="cursorCol", backward=False); setHighlightRegex(r, [cursorCol])', 'search for regex forwards in current column') +Sheet.addCommand('?', 'searchr-col', 'r=moveInputRegex("reverse search", columns="cursorCol", backward=True); setHighlightRegex(r, [cursorCol])', 'search for regex backwards in current column') +Sheet.addCommand('g/', 'search-cols', 'r=moveInputRegex("g/", backward=False, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex forwards over all visible columns') +Sheet.addCommand('g?', 'searchr-cols', 'r=moveInputRegex("g?", backward=True, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex backwards over all visible columns') + +vd.addGlobals(highlight_chunks=highlight_chunks) diff --git a/visidata/search.py b/visidata/search.py index c43113bca..fb893faa2 100644 --- a/visidata/search.py +++ b/visidata/search.py @@ -89,21 +89,6 @@ def moveInputRegex(sheet, action:str, type="regex", **kwargs): vd.moveRegex(sheet, regex=r['regex'], regex_flags=r['flags'], **kwargs) return r -@Sheet.api -def setHighlightRegex(sheet, r, cols=[]): - if not sheet.options.highlight: - return - flagbits = sum(getattr(re, f.upper()) for f in r['flags']) - rc = re.compile(r['regex'], flagbits) - sheet.highlight_clear() - if cols is None: - vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) - sheet.highlight_regex = rc - else: - for col in cols: - vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) - col.highlight_regex = rc - @Sheet.api @asyncthread def search_expr(sheet, expr, reverse=False, curcol=None): @@ -117,35 +102,15 @@ def search_expr(sheet, expr, reverse=False, curcol=None): vd.fail(f'no {sheet.rowtype} where {expr}') -@Sheet.api -def highlight_input(sheet, cols=[]): - r = vd.inputMultiple(regex=dict(prompt=f"highlight regex: ", type="regex", defaultLast=True, help=vd.help_regex), - flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) - if not sheet.options.highlight: - vd.warning('highlight option needs to be set to True') - setHighlightRegex(sheet, r, cols) -@Sheet.api -def highlight_clear(sheet): - if not sheet.options.highlight: - return - for col in sheet.columns: - if col.highlight_regex: - vd.addUndo(setattr, col, 'highlight_regex', col.highlight_regex) - col.highlight_regex = None - if sheet.highlight_regex: - vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) - sheet.highlight_regex = None - -Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; cols=keyCols or [visibleCols[0]]; r=moveInputRegex("row key", type="regex-row", columns=cols); setHighlightRegex(r, cols); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') -Sheet.addCommand('/', 'search-col', 'r=moveInputRegex("search", columns="cursorCol", backward=False); setHighlightRegex(r, [cursorCol])', 'search for regex forwards in current column') - -Sheet.addCommand('?', 'searchr-col', 'r=moveInputRegex("reverse search", columns="cursorCol", backward=True); setHighlightRegex(r, [cursorCol])', 'search for regex backwards in current column') +Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; moveInputRegex("row key", type="regex-row", columns=keyCols or [visibleCols[0]]); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') +Sheet.addCommand('/', 'search-col', 'moveInputRegex("search", columns="cursorCol", backward=False)', 'search for regex forwards in current column') +Sheet.addCommand('?', 'searchr-col', 'moveInputRegex("reverse search", columns="cursorCol", backward=True)', 'search for regex backwards in current column') Sheet.addCommand('n', 'search-next', 'vd.moveRegex(sheet, reverse=False)', 'go to next match from last regex search') Sheet.addCommand('N', 'searchr-next', 'vd.moveRegex(sheet, reverse=True)', 'go to previous match from last regex search') -Sheet.addCommand('g/', 'search-cols', 'r=moveInputRegex("g/", backward=False, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex forwards over all visible columns') -Sheet.addCommand('g?', 'searchr-cols', 'r=moveInputRegex("g?", backward=True, columns="visibleCols"); setHighlightRegex(r, sheet.visibleCols)', 'search for regex backwards over all visible columns') +Sheet.addCommand('g/', 'search-cols', 'moveInputRegex("g/", backward=False, columns="visibleCols")', 'search for regex forwards over all visible columns') +Sheet.addCommand('g?', 'searchr-cols', 'moveInputRegex("g?", backward=True, columns="visibleCols")', 'search for regex backwards over all visible columns') Sheet.addCommand('z/', 'search-expr', 'search_expr(inputExpr("search by expr: ") or fail("no expr"), curcol=cursorCol)', 'search by Python expression forwards in current column (with column names as variables)') Sheet.addCommand('z?', 'searchr-expr', 'search_expr(inputExpr("searchr by expr: ") or fail("no expr"), curcol=cursorCol, reverse=True)', 'search by Python expression backwards in current column (with column names as variables)') @@ -160,10 +125,3 @@ def highlight_clear(sheet): View > Search backward > by Python expr > searchr-expr View > Search backward > again > searchr-next ''') - -vd.option('highlight', True, 'whether to highlight strings in searches') -vd.option('color_highlight_search', '21 blue on 15 white', 'color to use for highlighting search results', sheettype=None) #bright blue on white - -Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') -Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column') -Sheet.addCommand('', 'highlight-clear', 'highlight_clear()', 'clear the current highlight pattern') diff --git a/visidata/sheets.py b/visidata/sheets.py index ed2f4febc..e2ede87b7 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -197,7 +197,6 @@ def __init__(self, *names, rows=UNLOADED, **kwargs): self._ordering = list(type(self)._ordering) #2254 self._colorizers = self.classColorizers - self.highlight_regex = None self.recalc() # set .sheet on columns and start caches self.__dict__.update(kwargs) # also done earlier in BaseSheet.__init__ @@ -1048,65 +1047,16 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, else: hp = None - - def _highlight(chunks): - '''consumes the generator *chunks*''' - display_chunks = [] - left_hl = False - right_hl = False - truncate_right = False #becomes True only if the highlighted chunk ends at the col edge - dispw = 0 - for attr, text in chunks: - last = hoffset if hoffset > 0 else 0 - # note a limitation with Unicode: the regex can cut a grapheme cluster into codepoints - matches = re.finditer(hp, text) if hp else [] - for m in matches: - m1 = m.start() - m2 = m.end() - if m1 < hoffset: - left_hl = True - if m2 <= hoffset: - continue - m1 = hoffset - if m1 > last: - s = text[last:m1] - if truncate_right: - display_chunks[-1][1] += s - else: - display_chunks.append([attr, s]) - dispw += dispwidth(s) - s = text[m1:m2] - if truncate_right: - display_chunks[-1][1] += s - else: - display_chunks.append([hl_attr, s]) - dispw += dispwidth(text[m1:m2]) - if dispw > colwidth-notewidth-1: - right_hl = True - last = len(text) - break - if dispw == colwidth-notewidth-1: - #append any subsequent cell text to the highlighted chunk so it gets a truncator added by clipdraw() - truncate_right = True - last = m2 - if last < len(text): - s = text[last:] - if truncate_right: - display_chunks[-1][1] += s - else: - display_chunks.append([attr, s]) - dispw += dispwidth(s) - if colwidth > 2: - pre = disp_truncator if hoffset != 0 else disp_column_fill - display_chunks.insert(0, (hl_attr if left_hl else cattr, pre)) - return display_chunks, right_hl - - for i, chunks in enumerate(lines): + for i, chunks in enumerate(lines): #chunks is a generator y = ybase+i sepchars = seps[i] - chunks, right_hl = _highlight(chunks) + # chunks becomes a list + chunks, left_hl, right_hl = self.highlight_chunks(chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr) + if colwidth > 2: + pre = disp_truncator if hoffset != 0 else disp_column_fill + chunks.insert(0, (hl_attr if left_hl else cattr, pre)) clipdraw_chunks(scr, y, x, chunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth) if right_hl: hl_attr = update_attr(cattr, hl_attr, 100) From e75807cfa6dc9ad55617d90342e5e31a8d0a5b1c Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:22:38 -0800 Subject: [PATCH 12/16] [hlsearch-] rename option to highlight_search --- visidata/features/hlsearch.py | 8 ++++---- visidata/sheets.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/visidata/features/hlsearch.py b/visidata/features/hlsearch.py index ed35e3bc9..f31b729ac 100644 --- a/visidata/features/hlsearch.py +++ b/visidata/features/hlsearch.py @@ -61,7 +61,7 @@ def highlight_chunks(sheet, chunks, hp, hoffset, colwidth, notewidth, cattr, hl_ @Sheet.api def setHighlightRegex(sheet, r, cols=[]): - if not sheet.options.highlight: + if not sheet.options.highlight_search: return flagbits = sum(getattr(re, f.upper()) for f in r['flags']) rc = re.compile(r['regex'], flagbits) @@ -78,13 +78,13 @@ def setHighlightRegex(sheet, r, cols=[]): def highlight_input(sheet, cols=[]): r = vd.inputMultiple(regex=dict(prompt=f"highlight regex: ", type="regex", defaultLast=True, help=vd.help_regex), flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) - if not sheet.options.highlight: + if not sheet.options.highlight_search: vd.warning('highlight option needs to be set to True') setHighlightRegex(sheet, r, cols) @Sheet.api def highlight_clear(sheet): - if not sheet.options.highlight: + if not sheet.options.highlight_search: return for col in sheet.columns: if col.highlight_regex: @@ -94,7 +94,7 @@ def highlight_clear(sheet): vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) sheet.highlight_regex = None -vd.option('highlight', True, 'whether to highlight strings in searches') +vd.option('highlight_search', True, 'whether to highlight strings in searches') vd.option('color_highlight_search', '21 blue on 15 white', 'color to use for highlighting search results', sheettype=None) #bright blue on white Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') diff --git a/visidata/sheets.py b/visidata/sheets.py index e2ede87b7..3f44cfcbb 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1041,7 +1041,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, elif len(lines) < height: lines.extend([[('', '')]]*(height-len(lines))) - if self.options.highlight: + if self.options.highlight_search: hp = col.highlight_regex or self.highlight_regex hl_attr = colors.color_highlight_search else: From aef24eb26dbca107de7a296e1ae35e405d33d69f Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:23:29 -0800 Subject: [PATCH 13/16] [hlsearch-] fix unset vars when highlight_search is False --- visidata/features/hlsearch.py | 2 +- visidata/sheets.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/visidata/features/hlsearch.py b/visidata/features/hlsearch.py index f31b729ac..2ebf64e96 100644 --- a/visidata/features/hlsearch.py +++ b/visidata/features/hlsearch.py @@ -80,7 +80,7 @@ def highlight_input(sheet, cols=[]): flags=dict(prompt="regex flags: ", type="regex_flags", value=sheet.options.regex_flags, help=vd.help_regex_flags)) if not sheet.options.highlight_search: vd.warning('highlight option needs to be set to True') - setHighlightRegex(sheet, r, cols) + sheet.setHighlightRegex(r, cols) @Sheet.api def highlight_clear(sheet): diff --git a/visidata/sheets.py b/visidata/sheets.py index 3f44cfcbb..016053bca 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1052,8 +1052,11 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, sepchars = seps[i] - # chunks becomes a list - chunks, left_hl, right_hl = self.highlight_chunks(chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr) + left_hl = right_hl = False + if hp: # chunks becomes a list + chunks, left_hl, right_hl = self.highlight_chunks(chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr) + else: + chunks = list(chunks) if colwidth > 2: pre = disp_truncator if hoffset != 0 else disp_column_fill chunks.insert(0, (hl_attr if left_hl else cattr, pre)) From ac80d6da1ee3dee6de89b785cdd322fdb203db94 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:18:52 -0800 Subject: [PATCH 14/16] [search- hlsearch-] move highlight_clear to new stub clear_search --- visidata/features/hlsearch.py | 8 ++++---- visidata/search.py | 5 +++++ visidata/sheets.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/visidata/features/hlsearch.py b/visidata/features/hlsearch.py index 2ebf64e96..dba4a2686 100644 --- a/visidata/features/hlsearch.py +++ b/visidata/features/hlsearch.py @@ -65,7 +65,7 @@ def setHighlightRegex(sheet, r, cols=[]): return flagbits = sum(getattr(re, f.upper()) for f in r['flags']) rc = re.compile(r['regex'], flagbits) - sheet.highlight_clear() + sheet.clear_search() if cols is None: vd.addUndo(setattr, sheet, 'highlight_regex', sheet.highlight_regex) sheet.highlight_regex = rc @@ -82,8 +82,8 @@ def highlight_input(sheet, cols=[]): vd.warning('highlight option needs to be set to True') sheet.setHighlightRegex(r, cols) -@Sheet.api -def highlight_clear(sheet): +@Sheet.after +def clear_search(sheet): if not sheet.options.highlight_search: return for col in sheet.columns: @@ -99,7 +99,7 @@ def highlight_clear(sheet): Sheet.addCommand('', 'highlight-sheet', 'highlight_input(None)', 'highlight a regex in all columns') Sheet.addCommand('', 'highlight-col', 'highlight_input([cursorCol])', 'highlight a regex in current column') -Sheet.addCommand('', 'highlight-clear', 'highlight_clear()', 'clear the current highlight pattern') +Sheet.addCommand('', 'highlight-clear', 'clear_search()', 'clear the current highlight pattern') # redefine existing commands Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; cols=keyCols or [visibleCols[0]]; r=moveInputRegex("row key", type="regex-row", columns=cols); setHighlightRegex(r, cols); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') Sheet.addCommand('/', 'search-col', 'r=moveInputRegex("search", columns="cursorCol", backward=False); setHighlightRegex(r, [cursorCol])', 'search for regex forwards in current column') diff --git a/visidata/search.py b/visidata/search.py index fb893faa2..19fd5aad2 100644 --- a/visidata/search.py +++ b/visidata/search.py @@ -102,6 +102,11 @@ def search_expr(sheet, expr, reverse=False, curcol=None): vd.fail(f'no {sheet.rowtype} where {expr}') +@Sheet.api +def clear_search(sheet): + '''A stub function to clear any aftereffects of search, such as when + highlight_search is active.''' + pass Sheet.addCommand('r', 'search-keys', 'tmp=cursorVisibleColIndex; moveInputRegex("row key", type="regex-row", columns=keyCols or [visibleCols[0]]); sheet.cursorVisibleColIndex=tmp', 'go to next row with key matching regex') Sheet.addCommand('/', 'search-col', 'moveInputRegex("search", columns="cursorCol", backward=False)', 'search for regex forwards in current column') diff --git a/visidata/sheets.py b/visidata/sheets.py index 016053bca..825000f96 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -1320,7 +1320,7 @@ def reload_or_replace(sheet): BaseSheet.addCommand('gTab', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen') BaseSheet.addCommand('zZ', 'splitwin-input', 'vd.options.disp_splitwin_pct = input("% height for split window: ", value=vd.options.disp_splitwin_pct)', 'set split pane to specific size') -BaseSheet.addCommand('Ctrl+L', 'redraw', 'highlight_clear(); sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen') +BaseSheet.addCommand('Ctrl+L', 'redraw', 'clear_search(); sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen') BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit') BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit') BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet') From 897138e0c1896d262ba0e38eaadce227bf98370e Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:44:20 -0800 Subject: [PATCH 15/16] [sheets-] ensure highlight_search option exists Even if features/hlsearch.py is ever removed. Otherwise, reading the nonexistent option would raise ValueError. --- visidata/sheets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/visidata/sheets.py b/visidata/sheets.py index 825000f96..dad7ab6d7 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -27,6 +27,7 @@ vd.option('color_multiline_bottom', '', 'color of bottom line of multiline rows') #2715 vd.option('color_aggregator', 'bold 255 white on 240 black', 'color of aggregator summary on bottom row') +vd.option('highlight_search', False, 'whether to highlight strings in searches') #needs to be set here, is overriden in features/hlsearch.py @drawcache def _splitcell(sheet, s, width=0, maxheight=1): From ef21ba6e3150ddfea37eab33b399b3e0a97daf1d Mon Sep 17 00:00:00 2001 From: saulbert Date: Wed, 4 Mar 2026 15:23:48 -0800 Subject: [PATCH 16/16] [sheets-] fix hoffset in non-highlight path; use options.get for highlight_search #2861 Apply hoffset slicing when highlight_search is active but no regex is set, fixing horizontal scrolling. Use options.get() so the option can live solely in features/hlsearch.py without a stub in sheets.py. Co-Authored-By: Claude Opus 4.6 --- visidata/sheets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/visidata/sheets.py b/visidata/sheets.py index dad7ab6d7..90f6666ca 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -27,7 +27,6 @@ vd.option('color_multiline_bottom', '', 'color of bottom line of multiline rows') #2715 vd.option('color_aggregator', 'bold 255 white on 240 black', 'color of aggregator summary on bottom row') -vd.option('highlight_search', False, 'whether to highlight strings in searches') #needs to be set here, is overriden in features/hlsearch.py @drawcache def _splitcell(sheet, s, width=0, maxheight=1): @@ -1042,7 +1041,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, elif len(lines) < height: lines.extend([[('', '')]]*(height-len(lines))) - if self.options.highlight_search: + if self.options.get('highlight_search', False): hp = col.highlight_regex or self.highlight_regex hl_attr = colors.color_highlight_search else: @@ -1057,7 +1056,7 @@ def drawRow(self, scr, row, rowidx, ybase, rowcattr: ColorAttr, maxheight, if hp: # chunks becomes a list chunks, left_hl, right_hl = self.highlight_chunks(chunks, hp, hoffset, colwidth, notewidth, cattr, hl_attr) else: - chunks = list(chunks) + chunks = [(attr, text[hoffset:]) for attr, text in chunks] if colwidth > 2: pre = disp_truncator if hoffset != 0 else disp_column_fill chunks.insert(0, (hl_attr if left_hl else cattr, pre))