Skip to content

Commit

Permalink
[input] polish palette and use for jointype #1027
Browse files Browse the repository at this point in the history
  • Loading branch information
saulpw committed Nov 5, 2023
1 parent 5fcd132 commit e6a5b5a
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 59 deletions.
16 changes: 5 additions & 11 deletions visidata/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
vd.theme_option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None)

vd.option('input_history', '', 'basename of file to store persistent input history')
vd.disp_help = None # current level of help shown (up to vd.options.disp_help as maximum)
vd.disp_help = 1 # current level of help shown (up to vd.options.disp_help as maximum)

class AcceptInput(Exception):
'*args[0]* is the input to be accepted'
Expand Down Expand Up @@ -93,24 +93,17 @@ def splice(v:str, i:int, s:str):
# vd.options.disp_help is the effective maximum disp_help. The user can cycle through the various levels of help.
class HelpCycler:
def __init__(self, scr=None, help=''):
self.saved = False
self.help = help
self.scr = scr

def __enter__(self):
if self.scr:
vd.drawInputHelp(self.scr, self.help)

if vd.disp_help is None:
vd.disp_help = vd.options.disp_help
self.saved = True
# otherwise some other HelpCycler will unset it

return self

def __exit__(self, *args):
if self.saved:
vd.disp_help = None
pass

def cycle(self):
vd.disp_help = (vd.disp_help-1)%(vd.options.disp_help+1)
Expand All @@ -134,7 +127,7 @@ def drawInputHelp(vd, scr, help:str=''):
elif vd.disp_help == 1:
curhelp = help
sheet.drawSidebarText(scr, curhelp)
elif vd.disp_help == 2:
elif vd.disp_help >= 2:
curhelp = vd.getHelpPane('input', module='visidata')
sheet.drawSidebarText(scr, curhelp, title='Input Keystrokes Help')

Expand Down Expand Up @@ -224,7 +217,7 @@ def editline(vd, scr, y, x, w, i=0,
display=True,
updater=lambda val: None,
bindings={},
help='',
help='', # str|HelpPane
clear=True):
'''A better curses line editing widget.
If *clear* is True, clear whole editing area before displaying.
Expand Down Expand Up @@ -485,6 +478,7 @@ def input(self, prompt, type=None, defaultLast=False, history=[], dy=0, attr=Non
- *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
- *dy*: number of lines from bottom of pane
- *attr*: curses attribute for prompt
- *help*: string to include in help
'''

if attr is None:
Expand Down
21 changes: 10 additions & 11 deletions visidata/aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict

vd.help_aggrs = 'HELPTODO'
vd.help_aggregators = 'HELPTODO'

vd.option('null_value', None, 'a value to be counted as null', replay=True)

Expand Down Expand Up @@ -200,32 +200,31 @@ def aggregator_choices(vd):
]



def _format_match(s, positions):
out = list(s)
for p in positions:
out[p] = f'[:red]{out[p]}[/]'
return "".join(out)

@VisiData.api
def chooseAggregators(vd):
prompt = 'choose aggregators: '
def _fmt_aggr_summary(match, row, trigger_key):
formatted_aggrname = _format_match(row.key, match.positions.get('key', []))
formatted_aggrname = match.formatted.get('key', row.key) if match else row.key
r = ' '*(len(prompt)-3)
r += f'[:keystrokes]{trigger_key}[/] '
r += formatted_aggrname
if row.desc:
r += ' - ' + _format_match(row.desc, match.positions.get('desc', []))
r += ' - '
r += match.formatted.get('desc', row.desc) if match else row.desc
return r

r = vd.activeSheet.inputPalette(prompt,
vd.aggregator_choices,
value_key='key',
formatter=_fmt_aggr_summary,
type='aggregators',
help=vd.help_aggregators,
multiple=True)
return r.split()

aggrs = r.split()
for aggr in aggrs:
vd.usedInputs[aggr] += 1
return aggrs

Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'Add aggregator to current column')
Sheet.addCommand('z+', 'memo-aggregate', 'for agg in chooseAggregators(): cursorCol.memo_aggregate(aggregators[agg], selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
Expand Down
76 changes: 51 additions & 25 deletions visidata/features/cmdpalette.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import collections
from visidata import BaseSheet, vd, CompleteKey, clipdraw, HelpSheet, colors, AcceptInput, AttrDict
from visidata import DrawablePane, BaseSheet, vd, VisiData, CompleteKey, clipdraw, HelpSheet, colors, AcceptInput, AttrDict


vd.theme_option('color_cmdpalette', 'black on 72', 'base color of command palette')
vd.theme_option('disp_cmdpal_max', 5, 'max number of suggestions for command palette')
vd.theme_option('disp_cmdpal_max', 10, 'max number of suggestions for command palette')



def _format_match(s, positions):
out = list(s)
for p in positions:
out[p] = f'[:red]{out[p]}[/]'
return "".join(out)

def make_acceptor(value, multiple=False):
def _acceptor(v, i):
if multiple:
items = list(v.split())
if v.endswith(' '):
if not v or v.endswith(' '):
items.append(value)
else:
items[-1] = value
Expand All @@ -28,6 +22,14 @@ def _acceptor(v, i):
return _acceptor


@VisiData.lazy_property
def usedInputs(vd):
return collections.defaultdict(int)

@DrawablePane.after
def execCommand2(sheet, cmd, *args, **kwargs):
vd.usedInputs[cmd.longname] += 1

@BaseSheet.api
def inputPalette(sheet, prompt, items,
value_key='key',
Expand All @@ -39,27 +41,51 @@ def inputPalette(sheet, prompt, items,
def _draw_palette(value):
words = value.lower().split()

matches = vd.fuzzymatch(items, [words[-1]] if multiple and words else words)
if multiple and words:
if value.endswith(' '):
finished_words = words
unfinished_words = []
else:
finished_words = words[:-1]
unfinished_words = [words[-1]]
else:
unfinished_words = words
finished_words = []

unuseditems = [item for item in items if item[value_key] not in finished_words]

matches = vd.fuzzymatch(unuseditems, unfinished_words)

# do the drawing
h, w = sheet._scr.getmaxyx()
cmdpal_h = min(h-2, sheet.options.disp_cmdpal_max)
m_max = min(len(matches), cmdpal_h)
h = sheet.windowHeight
w = min(100, sheet.windowWidth)
nitems = min(h-1, sheet.options.disp_cmdpal_max)

for i, m in enumerate(matches[:m_max]):
useditems = []
palrows = []

for m in matches[:nitems]:
useditems.append(m.match)
palrows.append((m, m.match))

favitems = sorted([item for item in unuseditems if item not in useditems],
key=lambda item: -vd.usedInputs.get(item[value_key], 0))

for item in favitems[:nitems-len(palrows)]:
palrows.append((None, item))

for i in range(nitems-len(palrows)):
palrows.append((None, None))

for i, (m, item) in enumerate(palrows):
trigger_key = ' '

if i < 9:
if i < 9 and item:
trigger_key = f'{i+1}'
bindings[trigger_key] = make_acceptor(m.match[value_key], multiple=multiple)

match_summary = formatter(m, m.match, trigger_key)
bindings[trigger_key] = make_acceptor(item[value_key], multiple=multiple)

clipdraw(sheet._scr, h-(m_max+1)+i, 0, match_summary, colors.color_cmdpalette, w=120)
match_summary = formatter(m, item, trigger_key) if item else ' '

# add some empty rows for visual appeal and dropping previous (not-anymore-)matches
for i in range(cmdpal_h - m_max):
clipdraw(sheet._scr, h-(cmdpal_h+1)+i, 0, ' ', colors.color_cmdpalette, w=120)
clipdraw(sheet._scr, h-nitems-1+i, 0, match_summary, colors.color_cmdpalette, w=w)

return None

Expand All @@ -86,12 +112,12 @@ def inputLongname(sheet):

def _fmt_cmdpal_summary(match, row, trigger_key):
keystrokes = this_sheets_help.revbinds.get(row.longname, [None])[0] or ' '
formatted_longname = _format_match(row.longname, match.positions.get('longname', []))
formatted_longname = match.formatted.get('longname', row.longname) if match else row.longname
formatted_name = f'[:onclick {row.longname}]{formatted_longname}[/]'
r = f' [:keystrokes]{keystrokes.rjust(len(prompt)-5)}[/] '
r += f'[:keystrokes]{trigger_key}[/] {formatted_name}'
if row.description:
formatted_desc = _format_match(row.description, match.positions.get('description', []))
formatted_desc = match.formatted.get('description', row.description) if match else row.description
r += f' - {formatted_desc}'
if vd.options.debug:
debug_info = f'[{m.score}]'
Expand Down
2 changes: 1 addition & 1 deletion visidata/features/describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from visidata import BaseSheet, TableSheet, ColumnsSheet, SheetsSheet


vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggrs)
vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggregators)


@Column.api
Expand Down
21 changes: 18 additions & 3 deletions visidata/features/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import functools
from copy import copy

from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet
from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet, AttrDict

@VisiData.api
def ensureLoaded(vd, sheets):
Expand Down Expand Up @@ -84,7 +84,7 @@ def openJoin(sheet, others, jointype=''):
sheetKeyCols={s:s.keyCols for s in sheets})


vd.jointypes = [{'key': k, 'desc': v} for k, v in {
vd.jointypes = [AttrDict(key=k, desc=v) for k, v in {
'inner': 'only rows with matching keys on all sheets',
'outer': 'only rows with matching keys on first selected sheet',
'full': 'all rows from all sheets (union)',
Expand Down Expand Up @@ -349,7 +349,22 @@ def iterload(self):

@VisiData.api
def chooseJointype(vd):
return vd.chooseOne(vd.jointypes, type="jointype")
prompt = 'choose jointype: '
def _fmt_aggr_summary(match, row, trigger_key):
formatted_jointype = match.formatted.get('key', row.key) if match else row.key
r = ' '*(len(prompt)-3)
r += f'[:keystrokes]{trigger_key}[/] '
r += formatted_jointype
if row.desc:
r += ' - '
r += match.formatted.get('desc', row.desc) if match else row.desc
return r

return vd.activeSheet.inputPalette(prompt,
vd.jointypes,
value_key='key',
formatter=_fmt_aggr_summary,
type='jointype')


IndexSheet.addCommand('&', 'join-selected', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype=chooseJointype()))', 'merge selected sheets with visible columns from all, keeping rows according to jointype')
Expand Down
22 changes: 14 additions & 8 deletions visidata/fuzzymatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,13 @@ def _fuzzymatch(target: str, pattern: str) -> MatchResult:
return MatchResult(j, maxScorePos + 1, int(maxScore), pos)


CombinedMatch = collections.namedtuple('CombinedMatch', 'score positions match')
def _format_match(s, positions):
out = list(s)
for p in positions:
out[p] = f'[:red]{out[p]}[/]'
return "".join(out)

CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')


@VisiData.api
Expand All @@ -364,18 +370,18 @@ def fuzzymatch(vd, haystack:list[dict[str, str]], needles:list[str]) -> list[Com
matches = []
for h in haystack:
match = {}
formatted_hay = {}
for k, v in h.items():
for p in needles:
m = _fuzzymatch(v, p)
if m.score > 0:
match[k] = m
mr = _fuzzymatch(v, p)
if mr.score > 0:
match[k] = mr
formatted_hay[k] = _format_match(v, mr.positions)

if match:
m = CombinedMatch(score=sum(mr.score**2 for mr in match.values()),
positions={k:mr.positions for k, mr in match.items()},
match=h)
# square to prefer larger scores in a single haystack
matches.append(m)
score = sum(mr.score**2 for mr in match.values())
matches.append(CombinedMatch(score=score, formatted=formatted_hay, match=h))

return sorted(matches, key=lambda m: -m.score)

Expand Down

0 comments on commit e6a5b5a

Please sign in to comment.