visidata 3.2__py3-none-any.whl → 3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. visidata/__init__.py +1 -1
  2. visidata/_input.py +36 -22
  3. visidata/_open.py +1 -1
  4. visidata/basesheet.py +0 -2
  5. visidata/cliptext.py +4 -1
  6. visidata/column.py +43 -32
  7. visidata/deprecated.py +5 -0
  8. visidata/experimental/liveupdate.py +1 -1
  9. visidata/features/cmdpalette.py +61 -22
  10. visidata/features/expand_cols.py +0 -1
  11. visidata/features/go_col.py +1 -2
  12. visidata/features/layout.py +1 -2
  13. visidata/features/reload_every.py +3 -3
  14. visidata/form.py +6 -5
  15. visidata/freqtbl.py +2 -2
  16. visidata/fuzzymatch.py +6 -5
  17. visidata/guides/CommandsSheet.md +1 -0
  18. visidata/help.py +20 -2
  19. visidata/loaders/archive.py +27 -3
  20. visidata/loaders/csv.py +7 -0
  21. visidata/macros.py +1 -1
  22. visidata/main.py +11 -7
  23. visidata/mainloop.py +1 -1
  24. visidata/man/vd.1 +13 -4
  25. visidata/man/vd.txt +23 -4
  26. visidata/menu.py +1 -1
  27. visidata/mouse.py +2 -0
  28. visidata/movement.py +6 -3
  29. visidata/save.py +5 -1
  30. visidata/settings.py +7 -3
  31. visidata/sheets.py +79 -31
  32. visidata/sidebar.py +7 -6
  33. visidata/tests/test_cliptext.py +13 -0
  34. visidata/tests/test_commands.py +1 -0
  35. visidata/threads.py +22 -0
  36. visidata/undo.py +1 -1
  37. visidata/utils.py +15 -1
  38. {visidata-3.2.data → visidata-3.3.data}/data/share/man/man1/vd.1 +13 -4
  39. {visidata-3.2.data → visidata-3.3.data}/data/share/man/man1/visidata.1 +13 -4
  40. {visidata-3.2.dist-info → visidata-3.3.dist-info}/METADATA +3 -3
  41. {visidata-3.2.dist-info → visidata-3.3.dist-info}/RECORD +47 -47
  42. {visidata-3.2.data → visidata-3.3.data}/data/share/applications/visidata.desktop +0 -0
  43. {visidata-3.2.data → visidata-3.3.data}/scripts/vd2to3.vdx +0 -0
  44. {visidata-3.2.dist-info → visidata-3.3.dist-info}/LICENSE.gpl3 +0 -0
  45. {visidata-3.2.dist-info → visidata-3.3.dist-info}/WHEEL +0 -0
  46. {visidata-3.2.dist-info → visidata-3.3.dist-info}/entry_points.txt +0 -0
  47. {visidata-3.2.dist-info → visidata-3.3.dist-info}/top_level.txt +0 -0
visidata/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  'VisiData: a curses interface for exploring and arranging tabular data'
2
2
 
3
- __version__ = '3.2'
3
+ __version__ = '3.3'
4
4
  __version_info__ = 'VisiData v' + __version__
5
5
  __author__ = 'Saul Pwanson <vd@saul.pw>'
6
6
  __status__ = 'Production/Stable'
visidata/_input.py CHANGED
@@ -245,7 +245,8 @@ class InputWidget:
245
245
  clipdraw(scr, y, x+w, ' ', attr, 1, clear=False, literal=True)
246
246
  if scr:
247
247
  prew = dispwidth(dispval[:i])
248
- scr.move(y, x+prew)
248
+ if x+prew < scr.getmaxyx()[1]: #move cursor back to where the user is editing
249
+ scr.move(y, x+prew)
249
250
 
250
251
  def handle_key(self, ch:str, scr) -> bool:
251
252
  'Return True to accept current input. Raise EscapeException on Ctrl+C, Ctrl+Q, or ESC.'
@@ -373,7 +374,7 @@ def editText(vd, y, x, w, attr=ColorAttr(), value='',
373
374
  help='',
374
375
  updater=lambda val: None, bindings={},
375
376
  display=True, record=True, clear=True, **kwargs):
376
- 'Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string.'
377
+ '''Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string. Callers should handle curses.error, which will be raised if the terminal is resized during the edit, in a way that moves the editor coordinates offscreen.'''
377
378
  v = None
378
379
  if record and vd.cmdlog:
379
380
  v = vd.getCommandInput()
@@ -391,8 +392,8 @@ def editText(vd, y, x, w, attr=ColorAttr(), value='',
391
392
  try:
392
393
  widget = InputWidget(value=str(value), display=display, **kwargs)
393
394
 
394
- with vd.AddedHelp(vd.getHelpPane('input', module='visidata'), 'Input Keystrokes Help'), \
395
- vd.AddedHelp(help, 'Input Field Help'):
395
+ with vd.AddedHelp(vd.getHelpPane('input', module='visidata'), 'Input Keystrokes Help', 'inputkeys'), \
396
+ vd.AddedHelp(help, 'Input Field Help', 'inputfield'):
396
397
  v = widget.editline(vd.activeSheet._scr, y, x, w, attr=attr, updater=updater, bindings=bindings, clear=clear)
397
398
  except AcceptInput as e:
398
399
  v = e.args[0]
@@ -468,7 +469,6 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
468
469
 
469
470
  assert False, type(previnput)
470
471
 
471
- y = sheet.windowHeight-1
472
472
  maxw = sheet.windowWidth//2
473
473
  attr = colors.color_edit_unfocused
474
474
 
@@ -494,9 +494,11 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
494
494
 
495
495
  def _drawPrompt(val):
496
496
  for k, v in kwargs.items():
497
+ #recalculate y to adjust for screen resizes during input()
498
+ y = sheet.windowHeight-v.get('dy')-1
497
499
  maxw = min(sheet.windowWidth-1, max(dispwidth(v.get('prompt')), dispwidth(str(v.get('value', '')))))
498
- promptlen = clipdraw(scr, y-v.get('dy'), 0, v.get('prompt'), attr, w=maxw) #1947
499
- promptlen = clipdraw(scr, y-v.get('dy'), promptlen, v.get('value', ''), attr, w=maxw)
500
+ promptlen = clipdraw(scr, y, 0, v.get('prompt'), attr, w=maxw) #1947
501
+ promptlen = clipdraw(scr, y, promptlen, v.get('value', ''), attr, w=maxw)
500
502
 
501
503
  return updater(val)
502
504
 
@@ -583,21 +585,30 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
583
585
  return sheet.windowWidth-promptlen-rstatuslen-2
584
586
 
585
587
  w = kwargs.pop('w', _drawPrompt())
586
- ret = vd.editText(y, promptlen, w=w,
587
- attr=colors.color_edit_cell,
588
- options=vd.options,
589
- history=history,
590
- updater=_drawPrompt,
591
- **kwargs)
592
-
593
- if ret:
594
- if kwargs.get('record', True) and kwargs.get('display', True):
595
- vd.addInputHistory(ret, type=type)
596
- elif defaultLast:
597
- history or vd.fail("no previous input")
598
- ret = history[-1]
588
+ restarts = 0
589
+ while restarts < 100:
590
+ #recalculate y to handle resize events
591
+ y = sheet.windowHeight-dy-1
592
+ try:
593
+ ret = vd.editText(y, promptlen, w=w,
594
+ attr=colors.color_edit_cell,
595
+ options=vd.options,
596
+ history=history,
597
+ updater=_drawPrompt,
598
+ **kwargs)
599
+ if ret:
600
+ if kwargs.get('record', True) and kwargs.get('display', True):
601
+ vd.addInputHistory(ret, type=type)
602
+ elif defaultLast:
603
+ history or vd.fail("no previous input")
604
+ ret = history[-1]
599
605
 
600
- return ret
606
+ return ret
607
+ except curses.error:
608
+ vd.warning('restarting input due to resize')
609
+ restarts += 1
610
+ # if it keeps happening, it's probably not resize events, so give some debug output
611
+ vd.error(f'aborting input: y={y}, w={w}, windowHeight={sheet.windowHeight}, windowWidth={sheet.windowWidth}')
601
612
 
602
613
 
603
614
  @VisiData.api
@@ -673,7 +684,10 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
673
684
  editargs = dict(value=value, options=self.options)
674
685
 
675
686
  editargs.update(kwargs) # update with user-specified args
676
- r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
687
+ try:
688
+ r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
689
+ except curses.error:
690
+ vd.fail(f'aborting edit due to resize')
677
691
 
678
692
  if rowidx >= 0: # if not header
679
693
  r = col.type(r) # convert input to column type, let exceptions be raised
visidata/_open.py CHANGED
@@ -183,7 +183,7 @@ def open_txt(vd, p):
183
183
  if delimiter and delimiter in next(fp): # peek at the first line
184
184
  return vd.open_tsv(p) # TSV often have .txt extension
185
185
  except StopIteration:
186
- return TableSheet(p.base_stem, columns=[SettableColumn(width=vd.options.default_width)], source=p)
186
+ return vd.newSheet(p.base_stem, 1, source=p)
187
187
  return TextSheet(p.base_stem, source=p)
188
188
 
189
189
 
visidata/basesheet.py CHANGED
@@ -302,8 +302,6 @@ class BaseSheet(DrawablePane):
302
302
  'Return formatted string with *sheet* and *vd* accessible to expressions. Missing expressions return empty strings instead of error.'
303
303
  return MissingAttrFormatter().format(fmt, sheet=self, vd=vd, **kwargs)
304
304
 
305
-
306
-
307
305
  @VisiData.api
308
306
  def redraw(vd):
309
307
  'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.'
visidata/cliptext.py CHANGED
@@ -171,7 +171,7 @@ def _clipstr(s, dispw, trunch='', oddspacech='', combch='', modch=''):
171
171
 
172
172
  #if the next character will fit
173
173
  if w+chlen <= dispw:
174
- ret += c
174
+ ret += newc
175
175
  w += chlen
176
176
  #move the truncation spot forward only when the truncation character can fit
177
177
  if w+trunchlen <= dispw:
@@ -318,6 +318,9 @@ def wraptext(text, width=80, indent=''):
318
318
  line = _markdown_to_internal(line)
319
319
  chunks = re.split(internal_markup_re, line)
320
320
  textchunks = [x for x in chunks if not is_vdcode(x)]
321
+ if ''.join(textchunks) == '': #for markup with no contents, like '[:tag][/]' or '[:]' or '[/]'
322
+ yield '', ''
323
+ continue
321
324
  # textwrap.wrap does not handle variable-width characters #2416
322
325
  for linenum, textline in enumerate(textwrap.wrap(''.join(textchunks), width=width, drop_whitespace=False)):
323
326
  txt = textline
visidata/column.py CHANGED
@@ -40,21 +40,11 @@ class DisplayWrapper:
40
40
  def __eq__(self, other):
41
41
  return self.value == other
42
42
 
43
- def _default_colnames():
44
- 'A B C .. Z AA AB .. ZZ AAA .. to infinity'
45
- i=0
46
- while True:
47
- i += 1
48
- for x in itertools.product(string.ascii_uppercase, repeat=i):
49
- yield ''.join(x)
50
-
51
- default_colnames = _default_colnames()
52
-
53
43
 
54
44
  class Column(Extensible):
55
45
  '''Base class for all column types.
56
46
 
57
- - *name*: name of this column.
47
+ - *name*: name of this column; if None, current sheet will assign a name
58
48
  - *type*: ``anytype str int float date`` or other type-like conversion function.
59
49
  - *cache*: cache behavior
60
50
 
@@ -71,7 +61,10 @@ class Column(Extensible):
71
61
  def __init__(self, name=None, *, type=anytype, cache=False, **kwargs):
72
62
  self.sheet = ExplodingMock('use addColumn() on all columns') # owning Sheet, set in .recalc() via Sheet.addColumn
73
63
  if name is None:
74
- name = next(default_colnames)
64
+ if vd.sheet: # get a column name from the current sheet
65
+ name = vd.sheet.incremented_colname()
66
+ else:
67
+ name = ''
75
68
  self.name = str(name) # display visible name
76
69
  self.fmtstr = '' # by default, use str()
77
70
  self._type = type # anytype/str/int/float/date/func
@@ -86,7 +79,7 @@ class Column(Extensible):
86
79
  self.formatter = ''
87
80
  self.displayer = ''
88
81
  self.defer = False
89
- self.disp_expert = 0 # auto-hide if options.disp_expert less than col.disp_expert
82
+ self.disp_expert = 0 # do not show if 'nometacols' in options.disp_help_flags
90
83
 
91
84
  self.setCache(cache)
92
85
  for k, v in kwargs.items():
@@ -243,7 +236,9 @@ class Column(Extensible):
243
236
  return self.make_formatter()(*args, **kwargs)
244
237
 
245
238
  def formatValue(self, typedval, width=None):
246
- 'Return displayable string of *typedval* according to ``Column.fmtstr``.'
239
+ '''Return displayable string of *typedval* according to ``Column.fmtstr``.
240
+ If *width* is not None, values are clipped to that width when *typedval*
241
+ is a dict/list/tuple, but not for other types.'''
247
242
  if typedval is None:
248
243
  return None
249
244
 
@@ -359,7 +354,8 @@ class Column(Extensible):
359
354
  return ret
360
355
 
361
356
  def getCell(self, row):
362
- 'Return DisplayWrapper for displayable cell value.'
357
+ '''Return DisplayWrapper for displayable cell value.
358
+ For dict/list/tuple cells, the width of the value returned is capped at the column width.'''
363
359
  cellval = wrapply(self.getValue, row)
364
360
  typedval = wrapply(self.type, cellval)
365
361
 
@@ -398,7 +394,7 @@ class Column(Extensible):
398
394
  dw.typedval = typedval
399
395
 
400
396
  try:
401
- dw.text = self.format(typedval, width=(self.width or 0)*2) or ''
397
+ dw.text = self.format(typedval, width=self.width) or ''
402
398
 
403
399
  # annotate cells with raw value type in anytype columns, except for strings
404
400
  if self.type is anytype and type(cellval) is not str:
@@ -421,7 +417,8 @@ class Column(Extensible):
421
417
  return dw
422
418
 
423
419
  def getDisplayValue(self, row):
424
- 'Return string displayed in this column for given *row*.'
420
+ '''Return string displayed in this column for given *row*.
421
+ For dict/list/tuple cells, the width of the display value returned is capped at the column width.'''
425
422
  return self.getCell(row).text
426
423
 
427
424
  def putValue(self, row, val):
@@ -461,22 +458,36 @@ class Column(Extensible):
461
458
  return vd.status('set %d cells to %d values' % (len(rows), len(values)))
462
459
 
463
460
  def getMaxWidth(self, rows):
464
- 'Return the maximum length of any cell in column or its header (up to window width).'
465
- w = 0
461
+ 'Return the maximum length of any cell in column or its header (up to drawable window width).'
462
+ drawable_width = self.sheet.windowWidth-1
466
463
  nlen = dispwidth(self.name)
467
- if len(rows) > 0:
468
- w_max = 0
469
- for r in rows:
470
- row_w = dispwidth(self.getDisplayValue(r), maxwidth=self.sheet.windowWidth)
471
- if w_max < row_w:
472
- w_max = row_w
473
- if w_max >= self.sheet.windowWidth:
474
- break #1747 early out to speed up wide columns
475
- w = w_max
476
- w = max(w, nlen)+2
477
- w = min(w, self.sheet.windowWidth-1)
478
- return w
479
-
464
+ w_max = nlen
465
+ for r in rows:
466
+ row_w = self.measureValueWidthCapped(r, maxwidth=drawable_width)
467
+ if w_max < row_w:
468
+ w_max = row_w
469
+ if w_max >= self.sheet.windowWidth:
470
+ break #1747 early out to speed up wide columns
471
+ return min(w_max+2, drawable_width)
472
+
473
+ def measureValueWidthCapped(self, row, maxwidth=None):
474
+ '''Measure the width of the contents of a cell. Stop measuring at *maxwidth*,
475
+ to save time iterating over very long dict/list/tuple values.
476
+ If *maxwidth* is None, return the full width.'''
477
+ # The value classification logic here is taken from getCell,
478
+ # modified to cap the width examined for any dict/list/tuple
479
+ cellval = wrapply(self.getValue, row)
480
+ typedval = wrapply(self.type, cellval)
481
+ if isinstance(typedval, (TypedWrapper, threading.Thread)):
482
+ return dispwidth(self.getCell(row).text, maxwidth=maxwidth)
483
+ try:
484
+ text = self.format(typedval, width=maxwidth) or ''
485
+ except Exception as e: # formatting failure
486
+ try:
487
+ text = str(cellval)
488
+ except Exception as e:
489
+ text = str(e)
490
+ return dispwidth(text, maxwidth=maxwidth)
480
491
 
481
492
 
482
493
  # ---- basic Columns
visidata/deprecated.py CHANGED
@@ -278,3 +278,8 @@ def toggleKeys(self, cols):
278
278
  vd.optalias('disp_pixel_random', 'disp_graph_pixel_random') #2661
279
279
 
280
280
  vd.addGlobals(deprecated_warn=deprecated_warn)
281
+
282
+ # v3.3
283
+
284
+ #vd.option('disp_expert', 'max level of options and columns to include')
285
+ #vd.option('disp_help', '')
@@ -42,4 +42,4 @@ def addcol_expr(sheet):
42
42
 
43
43
 
44
44
  Sheet.addCommand(None, 'addcol-expr', 'sheet.addcol_expr()', "create new column from Python expression, updating the column's calculated values live")
45
- Sheet.addCommand(None, 'addcol-new', 'c=addColumnAtIndex(SettableColumn(width=options.default_width)); draw(sheet._scr); cursorVisibleColIndex=visibleCols.index(c); c.name=editCell(cursorVisibleColIndex, -1); c.width=None', 'append new column, updating the column name live')
45
+ Sheet.addCommand(None, 'addcol-new', 'c=addColumnAtCursor(SettableColumn(name="", width=options.default_width)); draw(sheet._scr); cursorVisibleColIndex=visibleCols.index(c); c.name=editCell(cursorVisibleColIndex, -1); c.width=None', 'append new column, updating the column name live')
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import math
2
3
  from functools import partial
3
4
  from visidata import DrawablePane, BaseSheet, vd, VisiData, CompleteKey, clipdraw, HelpSheet, colors, AcceptInput, AttrDict, drawcache_property, dispwidth
4
5
 
@@ -58,14 +59,21 @@ def inputPalette(sheet, prompt, items,
58
59
  formatter=lambda m, item, trigger_key: f'{trigger_key} {item}',
59
60
  multiple=False,
60
61
  **kwargs):
61
- if sheet.options.disp_expert >= 5:
62
+ if not vd.wantsHelp('cmdpalette'):
62
63
  return vd.input(prompt,
63
64
  completer=CompleteKey(sorted(item[value_key] for item in items)),
64
65
  **kwargs)
65
66
 
66
67
  bindings = dict()
67
68
 
69
+ #state variables for navigating display of matches
70
+ prev_value = None
68
71
  tabitem = -1
72
+ offset = 0
73
+ def reset_display():
74
+ nonlocal tabitem, offset
75
+ tabitem = -1
76
+ offset = 0
69
77
 
70
78
  def tab(n, nitems):
71
79
  nonlocal tabitem
@@ -73,7 +81,11 @@ def inputPalette(sheet, prompt, items,
73
81
  tabitem = (tabitem + n) % nitems
74
82
 
75
83
  def _draw_palette(value):
76
- words = value.lower().split()
84
+ nonlocal prev_value
85
+ words = value.split()
86
+ if value != prev_value:
87
+ reset_display()
88
+ prev_value = value
77
89
 
78
90
  if multiple and words:
79
91
  if value.endswith(' '):
@@ -83,8 +95,8 @@ def inputPalette(sheet, prompt, items,
83
95
  finished_words = words[:-1]
84
96
  unfinished_words = [words[-1]]
85
97
  else:
86
- unfinished_words = words
87
98
  finished_words = []
99
+ unfinished_words = words
88
100
 
89
101
  unuseditems = [item for item in items if item[value_key] not in finished_words]
90
102
 
@@ -92,25 +104,50 @@ def inputPalette(sheet, prompt, items,
92
104
 
93
105
  h = sheet.windowHeight
94
106
  w = min(100, sheet.windowWidth)
95
- nitems = min(h-1, sheet.options.disp_cmdpal_max)
107
+ nitems = min(h-2, sheet.options.disp_cmdpal_max)
108
+ if nitems <= 0:
109
+ return None
96
110
 
97
111
  useditems = []
98
112
  palrows = []
99
-
100
- for m in matches[:nitems]:
101
- useditems.append(m.match)
102
- palrows.append((m, m.match))
103
-
104
- favitems = sorted([item for item in unuseditems if item not in useditems],
105
- key=lambda item: -vd.usedInputs.get(item[value_key], 0))
106
-
107
- for item in favitems[:nitems-len(palrows)]:
108
- palrows.append((None, item))
113
+ n_results = 0
114
+ def read_matches(offset):
115
+ nonlocal useditems, palrows, value, n_results
116
+
117
+ useditems = []
118
+ palrows = []
119
+ for m in matches[offset:offset+nitems]:
120
+ useditems.append(m.match)
121
+ palrows.append((m, m.match))
122
+ n_results += len(matches)
123
+
124
+ #List matches only, usually. But list the available choices when there's no input,
125
+ #or (if multiple is True) they've just pressed space after a word.
126
+ if not unfinished_words:
127
+ favitems = sorted([item for item in unuseditems if item not in useditems],
128
+ key=lambda item: -vd.usedInputs.get(item[value_key], 0))
129
+ for item in favitems[offset-len(palrows):offset+nitems-len(palrows)]:
130
+ palrows.append((None, item))
131
+ n_results += len(favitems)
132
+ read_matches(offset)
133
+
134
+ def change_page(dir=+1):
135
+ nonlocal offset, n_results, nitems
136
+ new_offset = offset + dir*nitems
137
+ # constrain offset to be a multiple of nitems
138
+ new_offset = min(new_offset, ((n_results-1) // nitems)*nitems)
139
+ new_offset = max(new_offset, 0)
140
+ if new_offset == offset: return None
141
+ offset = new_offset
109
142
 
110
143
  navailitems = min(len(palrows), nitems)
111
144
 
112
145
  bindings['^I'] = lambda *args: tab(1, navailitems) or args
113
146
  bindings['KEY_BTAB'] = lambda *args: tab(-1, navailitems) or args
147
+ bindings['KEY_PPAGE'] = lambda *args: (change_page(-1) and read_matches(offset)) or args
148
+ bindings['KEY_NPAGE'] = lambda *args: (change_page(+1) and read_matches(offset)) or args
149
+ for numkey in '1234567890':
150
+ bindings.pop(numkey, None)
114
151
 
115
152
  for i in range(nitems-len(palrows)):
116
153
  palrows.append((None, None))
@@ -129,14 +166,13 @@ def inputPalette(sheet, prompt, items,
129
166
 
130
167
  if tabitem < 0 and palrows:
131
168
  _ , topitem = palrows[0]
132
- if not topitem: return
133
- if multiple:
134
- bindings[' '] = partial(add_to_input, value=topitem[value_key])
135
- bindings['^J'] = partial(accept_input_if_subset, value=topitem[value_key])
136
- else:
137
- bindings['^J'] = partial(accept_input, value=topitem[value_key])
169
+ if topitem:
170
+ if multiple:
171
+ bindings['^J'] = partial(accept_input_if_subset, value=topitem[value_key])
172
+ bindings[' '] = partial(add_to_input, value=topitem[value_key])
173
+ else:
174
+ bindings['^J'] = partial(accept_input, value=topitem[value_key])
138
175
  elif item and i == tabitem:
139
- if not item: return
140
176
  if multiple:
141
177
  bindings['^J'] = partial(accept_input_if_subset, value=item[value_key])
142
178
  bindings[' '] = partial(add_to_input, value=item[value_key])
@@ -146,7 +182,10 @@ def inputPalette(sheet, prompt, items,
146
182
 
147
183
  match_summary = formatter(m, item, trigger_key) if item else ' '
148
184
 
149
- clipdraw(sheet._scr, h-nitems-1+i, 0, match_summary, attr, w=w)
185
+ clipdraw(sheet._scr, h-nitems-2+i, 0, match_summary, attr, w=w)
186
+ attr = colors.color_cmdpalette
187
+ instr = 'Press [:keystrokes]PgUp/PgDn[/] to scroll items, [:keystrokes]Tab/Shift+Tab/Enter[/] to choose, [:keystrokes]Esc[/] to cancel.'
188
+ clipdraw(sheet._scr, h-2, 0, instr, attr, w=w)
150
189
 
151
190
  return None
152
191
 
@@ -143,7 +143,6 @@ def contract_cols(sheet, cols, depth=1): # depth == 0 means contract all the wa
143
143
  col.width = sheet.options.default_width
144
144
 
145
145
  sheet.columns = [col for col in sheet.columns if getattr(col, 'origCol', None) not in origCols]
146
- sheet.calcColLayout()
147
146
 
148
147
 
149
148
  @Sheet.api
@@ -25,8 +25,7 @@ def nextColName(sheet, show_cells=True):
25
25
  if show_cells and len(sheet.rows) > 0:
26
26
  dv = c.getDisplayValue(sheet.cursorRow)
27
27
  #the underscore that starts _cursor_cell excludes it from being fuzzy matched
28
- item = AttrDict(name_lower=c.name.lower(),
29
- name=c.name,
28
+ item = AttrDict(name=c.name,
30
29
  _cursor_cell=dv)
31
30
  colnames.append(item)
32
31
 
@@ -49,7 +49,6 @@ def hide_uniform_cols(sheet):
49
49
  if i <= idx:
50
50
  sheet.cursorRight(-1)
51
51
  col.hide()
52
- Sheet.clear_all_caches() #2578
53
52
 
54
53
  Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
55
54
  Sheet.addCommand('z_', 'resize-col-input', 'width = int(input("set width= ", value=cursorCol.width)); cursorCol.setWidth(width)', 'adjust width of current column to N')
@@ -57,7 +56,7 @@ Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.toggleWidth(c
57
56
  Sheet.addCommand('gz_', 'resize-cols-input', 'width = int(input("set width= ", value=cursorCol.width)); Fanout(visibleCols).setWidth(width)', 'adjust widths of all visible columns to N')
58
57
 
59
58
  Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
60
- Sheet.addCommand('z-', 'resize-col-half', 'cursorCol.setWidth(cursorCol.width//2)', 'reduce width of current column by half')
59
+ Sheet.addCommand('z-', 'resize-col-half', 'width = cursorCol.width if cursorCol.width is not None else options.default_width; cursorCol.setWidth(width//2)', 'reduce width of current column by half')
61
60
  Sheet.addCommand('g-', 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
62
61
 
63
62
  Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import time
3
3
 
4
- from visidata import vd, BaseSheet, Sheet, asyncignore, asyncthread, Path, ScopedSetattr
4
+ from visidata import vd, BaseSheet, Sheet, asyncignore, asyncthread, asyncsingle_queue, Path, ScopedSetattr
5
5
 
6
6
 
7
7
  @BaseSheet.api
@@ -33,9 +33,9 @@ def reload_modified(sheet):
33
33
 
34
34
 
35
35
  @Sheet.api
36
- @asyncthread
36
+ @asyncsingle_queue
37
37
  def reload_rows(self):
38
- 'Reload rows from ``self.source``, keeping current columns intact. Async.'
38
+ '''Reload rows from ``self.source``, keeping current columns intact. Async. If previous calls are running, waits for them to finish.'''
39
39
  with (ScopedSetattr(self, 'loading', True),
40
40
  ScopedSetattr(self, 'checkCursor', lambda: True),
41
41
  ScopedSetattr(self, 'cursorRowIndex', self.cursorRowIndex)):
visidata/form.py CHANGED
@@ -102,7 +102,7 @@ class FormCanvas(BaseSheet):
102
102
  @functools.wraps(VisiData.confirm)
103
103
  @VisiData.api
104
104
  def confirm(vd, prompt, exc=EscapeException):
105
- 'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
105
+ 'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Return True when proceeding, otherwise raise *exc*, or if *exc* is falsy, return False'
106
106
  if vd.options.batch:
107
107
  return vd.fail('cannot confirm in batch mode: ' + prompt)
108
108
 
@@ -115,10 +115,11 @@ def confirm(vd, prompt, exc=EscapeException):
115
115
  ])
116
116
 
117
117
  ret = FormCanvas(source=form).run(vd.scrFull)
118
- if not ret:
119
- raise exc('')
120
- yn = ret['yn'][:1]
121
- if not yn or yn not in 'Yy':
118
+ confirmed = False # default is to disconfirm, if user exited the confirmation via: Esc ^C ^Q q
119
+ if ret:
120
+ yn = ret['yn'][:1]
121
+ confirmed = yn and yn in 'Yy'
122
+ if not confirmed:
122
123
  msg = 'disconfirmed: ' + prompt
123
124
  if exc:
124
125
  raise exc(msg)
visidata/freqtbl.py CHANGED
@@ -61,11 +61,11 @@ Each row on this sheet corresponds to a *bin* of rows on the source sheet that h
61
61
 
62
62
  def selectRow(self, row):
63
63
  # Does not create an undo-operation for the select on the source rows. The caller should create undo-information itself.
64
- self.source.select(row.sourcerows, add_undo=False) # select all entries in the bin on the source sheet
64
+ self.source.select(row.sourcerows, status=False, add_undo=False) # select all entries in the bin on the source sheet
65
65
  return super().selectRow(row) # then select the bin itself on this sheet
66
66
 
67
67
  def unselectRow(self, row):
68
- self.source.unselect(row.sourcerows, add_undo=False)
68
+ self.source.unselect(row.sourcerows, status=False, add_undo=False)
69
69
  return super().unselectRow(row)
70
70
 
71
71
  def addUndoSelection(self):
visidata/fuzzymatch.py CHANGED
@@ -366,9 +366,10 @@ CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')
366
366
 
367
367
 
368
368
  @VisiData.api
369
- def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]"):
370
- '''Perform case-insensitive matching. Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict.'''
371
- needles = [ p.lower() for p in needles]
369
+ def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]", case_sensitive=False):
370
+ '''Perform matching that is case-insensitive by default. Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict. Set *case_sensitive* to match case.'''
371
+ if not case_sensitive:
372
+ needles = [ p.lower() for p in needles]
372
373
  matches = []
373
374
  for h in haystack:
374
375
  match = {}
@@ -376,9 +377,9 @@ def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[
376
377
  for k, v in h.items():
377
378
  if k[0] == '_': continue
378
379
  positions = set()
379
- v = v.lower()
380
+ v_match = v if case_sensitive else v.lower()
380
381
  for p in needles:
381
- mr = _fuzzymatch(v, p)
382
+ mr = _fuzzymatch(v_match, p)
382
383
  if mr.score > 0:
383
384
  match.setdefault(k, []).append(mr)
384
385
  positions |= set(mr.positions)
@@ -8,6 +8,7 @@ Start typing a command longname or keyword in its helpstring.
8
8
 
9
9
  - [:code]Enter[/] to execute top command.
10
10
  - [:code]Tab[/] to highlight top command and provide a numeric jumplist.
11
+ - [:code]PgUp[/]/[:code]PgDn[/] to scroll through commands.
11
12
 
12
13
  When a command is highlighted:
13
14
 
visidata/help.py CHANGED
@@ -4,7 +4,24 @@ import collections
4
4
  from visidata import VisiData, MetaSheet, ColumnAttr, Column, BaseSheet, VisiDataMetaSheet, SuspendCurses
5
5
  from visidata import vd, asyncthread, ENTER, drawcache, AttrDict, TextSheet
6
6
 
7
- vd.option('disp_expert', 0, 'max level of options and columns to include')
7
+
8
+ vd.option('disp_help_flags', 'cmdpalette guides help hints inputfield inputkeys nometacols sidebar',
9
+ '''list of helper features to enable (space-separated):
10
+ - "cmdpalette": exec-longname suggestions
11
+ - "guides": guides in sidebar
12
+ - "help": help sidebar collapsed by default
13
+ - "hints": context-sensitive hints on menu line
14
+ - "inputfield": context-sensitive help for each input field
15
+ - "inputkeys": input quick reference in sidebar
16
+ - "nometacols": hide expert columns on metasheets
17
+ - "sidebar": context-sensitive sheet help in sidebar
18
+ - "all": enable all helper features''')
19
+
20
+
21
+ @VisiData.api
22
+ def wantsHelp(vd, feat):
23
+ return feat in vd.options.disp_help_flags or 'all' in vd.options.disp_help_flags
24
+
8
25
 
9
26
  @BaseSheet.api
10
27
  def hint_basichelp(sheet):
@@ -99,12 +116,13 @@ class HelpPane:
99
116
 
100
117
  def draw(self, scr, x=None, y=None, **kwargs):
101
118
  if not scr: return
102
- # if vd.options.disp_help <= 0:
119
+ # if not vd.wantsHelp('statushelp'):
103
120
  # if self.scr:
104
121
  # self.scr.erase()
105
122
  # self.scr.refresh()
106
123
  # self.scr = None
107
124
  # return
125
+
108
126
  if y is None: y=0 # show at top of screen by default
109
127
  if x is None: x=0
110
128
  hneeded = self.amgr.maxHeight+3