visidata 3.0.1__py3-none-any.whl → 3.1__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 (151) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -199
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +9 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +66 -24
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +23 -4
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +18 -5
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/slide.py +15 -17
  34. visidata/features/status_source.py +3 -1
  35. visidata/features/sysedit.py +1 -1
  36. visidata/features/transpose.py +2 -1
  37. visidata/features/type_ipaddr.py +2 -4
  38. visidata/features/unfurl.py +1 -0
  39. visidata/form.py +2 -2
  40. visidata/freqtbl.py +16 -11
  41. visidata/fuzzymatch.py +1 -0
  42. visidata/graph.py +173 -12
  43. visidata/guide.py +61 -25
  44. visidata/guides/ClipboardGuide.md +48 -0
  45. visidata/guides/ColumnsGuide.md +52 -0
  46. visidata/guides/CommandsSheet.md +28 -0
  47. visidata/guides/DirSheet.md +34 -0
  48. visidata/guides/ErrorsSheet.md +17 -0
  49. visidata/guides/FrequencyTable.md +42 -0
  50. visidata/guides/GrepSheet.md +28 -0
  51. visidata/guides/JsonSheet.md +38 -0
  52. visidata/guides/MacrosSheet.md +19 -0
  53. visidata/guides/MeltGuide.md +52 -0
  54. visidata/guides/MemorySheet.md +7 -0
  55. visidata/guides/MenuGuide.md +26 -0
  56. visidata/guides/ModifyGuide.md +38 -0
  57. visidata/guides/PivotGuide.md +71 -0
  58. visidata/guides/RegexGuide.md +107 -0
  59. visidata/guides/SelectionGuide.md +44 -0
  60. visidata/guides/SlideGuide.md +26 -0
  61. visidata/guides/SortGuide.md +0 -0
  62. visidata/guides/SplitpaneGuide.md +15 -0
  63. visidata/guides/TypesSheet.md +43 -0
  64. visidata/guides/XsvGuide.md +36 -0
  65. visidata/help.py +6 -6
  66. visidata/hint.py +2 -1
  67. visidata/indexsheet.py +2 -2
  68. visidata/interface.py +13 -14
  69. visidata/keys.py +4 -1
  70. visidata/loaders/api_airtable.py +1 -1
  71. visidata/loaders/archive.py +1 -1
  72. visidata/loaders/csv.py +9 -5
  73. visidata/loaders/eml.py +11 -6
  74. visidata/loaders/f5log.py +1 -0
  75. visidata/loaders/fec.py +18 -42
  76. visidata/loaders/fixed_width.py +19 -3
  77. visidata/loaders/grep.py +121 -0
  78. visidata/loaders/html.py +1 -0
  79. visidata/loaders/http.py +6 -1
  80. visidata/loaders/json.py +22 -4
  81. visidata/loaders/jsonla.py +8 -2
  82. visidata/loaders/mailbox.py +1 -0
  83. visidata/loaders/markdown.py +25 -6
  84. visidata/loaders/msgpack.py +19 -0
  85. visidata/loaders/npy.py +0 -1
  86. visidata/loaders/odf.py +18 -4
  87. visidata/loaders/orgmode.py +1 -1
  88. visidata/loaders/rec.py +6 -4
  89. visidata/loaders/sas.py +11 -4
  90. visidata/loaders/scrape.py +0 -1
  91. visidata/loaders/texttables.py +2 -0
  92. visidata/loaders/tsv.py +24 -7
  93. visidata/loaders/unzip_http.py +127 -3
  94. visidata/loaders/vds.py +4 -0
  95. visidata/loaders/vdx.py +1 -1
  96. visidata/loaders/xlsx.py +5 -0
  97. visidata/loaders/xml.py +2 -1
  98. visidata/macros.py +14 -31
  99. visidata/main.py +20 -15
  100. visidata/mainloop.py +17 -6
  101. visidata/man/vd.1 +74 -39
  102. visidata/man/vd.txt +73 -41
  103. visidata/memory.py +16 -5
  104. visidata/menu.py +14 -3
  105. visidata/metasheets.py +5 -6
  106. visidata/modify.py +4 -4
  107. visidata/mouse.py +2 -0
  108. visidata/movement.py +14 -28
  109. visidata/optionssheet.py +3 -5
  110. visidata/path.py +59 -37
  111. visidata/pivot.py +8 -5
  112. visidata/pyobj.py +63 -9
  113. visidata/rename_col.py +18 -1
  114. visidata/save.py +16 -9
  115. visidata/search.py +4 -4
  116. visidata/selection.py +10 -56
  117. visidata/settings.py +37 -35
  118. visidata/sheets.py +189 -118
  119. visidata/shell.py +23 -14
  120. visidata/sidebar.py +71 -16
  121. visidata/sort.py +21 -6
  122. visidata/statusbar.py +42 -5
  123. visidata/stored_list.py +5 -2
  124. visidata/tests/conftest.py +1 -0
  125. visidata/tests/test_commands.py +9 -1
  126. visidata/tests/test_completer.py +18 -0
  127. visidata/tests/test_edittext.py +3 -2
  128. visidata/text_source.py +7 -4
  129. visidata/textsheet.py +20 -6
  130. visidata/themes/ascii8.py +9 -6
  131. visidata/themes/asciimono.py +14 -4
  132. visidata/threads.py +13 -3
  133. visidata/tuiwin.py +5 -1
  134. visidata/type_currency.py +1 -2
  135. visidata/type_date.py +6 -1
  136. visidata/undo.py +10 -13
  137. visidata/utils.py +9 -3
  138. visidata/vdobj.py +21 -1
  139. visidata/wrappers.py +9 -1
  140. {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  141. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
  142. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
  143. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
  144. visidata-3.1.dist-info/RECORD +284 -0
  145. visidata-3.0.1.dist-info/RECORD +0 -258
  146. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
  147. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  148. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  149. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  150. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  151. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/sheets.py CHANGED
@@ -4,8 +4,8 @@ from copy import copy, deepcopy
4
4
  import textwrap
5
5
 
6
6
  from visidata import VisiData, Extensible, globalCommand, ColumnAttr, ColumnItem, vd, ENTER, EscapeException, drawcache, drawcache_property, LazyChainMap, asyncthread, ExpectedException
7
- from visidata import (options, Column, namedlist, SettableColumn,
8
- TypedExceptionWrapper, BaseSheet, UNLOADED,
7
+ from visidata import (options, Column, namedlist, SettableColumn, AttrDict, DisplayWrapper,
8
+ TypedExceptionWrapper, BaseSheet, UNLOADED, wrapply,
9
9
  clipdraw, clipdraw_chunks, ColorAttr, update_attr, colors, undoAttrFunc, vlen, dispwidth)
10
10
  import visidata
11
11
 
@@ -13,20 +13,30 @@ import visidata
13
13
  vd.activePane = 1 # pane numbering starts at 1; pane 0 means active pane
14
14
 
15
15
 
16
- vd.option('name_joiner', '_', 'string to join sheet or column names', max_help=0)
17
- vd.option('value_joiner', ' ', 'string to join display values', max_help=0)
16
+ vd.option('name_joiner', '_', 'string to join sheet or column names')
17
+ vd.option('value_joiner', ' ', 'string to join display values')
18
+ vd.option('max_rows', 1_000_000_000, 'number of rows to load from source')
19
+
20
+ vd.option('disp_wrap_max_lines', 3, 'max lines for multiline view')
21
+ vd.option('disp_wrap_break_long_words', False, 'break words longer than column width in multiline')
22
+ vd.option('disp_wrap_replace_whitespace', False, 'replace whitespace with spaces in multiline')
23
+ vd.option('disp_wrap_placeholder', '…', 'multiline string to indicate truncation')
24
+ vd.option('disp_multiline_focus', True, 'only multiline cursor row')
25
+ vd.option('color_aggregator', 'bold 255 white on 234 black', 'color of aggregator summary on bottom row')
18
26
 
19
27
 
20
28
  @drawcache
21
29
  def _splitcell(sheet, s, width=0, maxheight=1):
22
- if width <= 0 or not sheet.options.textwrap_cells:
30
+ height = max(maxheight, sheet.options.disp_wrap_max_lines or 0)
31
+ if width <= 0 or height <= 0:
23
32
  return [s]
24
33
 
34
+ wrap_kwargs = sheet.options.getall('disp_wrap_')
35
+ wrap_kwargs['max_lines'] = height
36
+
25
37
  ret = []
26
38
  for attr, text in s:
27
- for line in textwrap.wrap(
28
- text, width=width, break_long_words=False, replace_whitespace=False
29
- ):
39
+ for line in textwrap.wrap(text, width=width, **wrap_kwargs):
30
40
  if len(ret) >= maxheight:
31
41
  ret[-1][0][1] += ' ' + line
32
42
  break
@@ -34,26 +44,42 @@ def _splitcell(sheet, s, width=0, maxheight=1):
34
44
  ret.append([[attr, line]])
35
45
  return ret
36
46
 
37
- disp_column_fill = ' ' # pad chars after column value
47
+ disp_column_fill = ' ' # pad chars before column value
48
+
49
+ class Colorizer:
50
+ '''higher precedence color overrides lower; all non-color attributes combine.
51
+ coloropt is the color option name (like 'color_error').
52
+ func(sheet,col,row,value) should return a true value if coloropt should be applied
53
+ If coloropt is None, func() should return a coloropt (or None) instead'''
54
+
55
+ def __init__(self, precedence:int, coloropt:str, func=lambda s,c,r,v: None):
56
+ self.precedence = precedence
57
+ self.coloropt = coloropt
58
+ self._func = func
59
+
60
+ class RowColorizer(Colorizer):
61
+ def func(self, s, c, r, v):
62
+ return r is not None and self._func(s,c,r,v)
63
+
64
+ class ColumnColorizer(Colorizer):
65
+ def func(self, s, c, r, v):
66
+ return c is not None and self._func(s,c,r,v)
67
+
68
+ class CellColorizer(Colorizer):
69
+ def func(self, s, c, r, v):
70
+ return r is not None and c is not None and self._func(s,c,r,v)
38
71
 
39
- # higher precedence color overrides lower; all non-color attributes combine
40
- # coloropt is the color option name (like 'color_error')
41
- # func(sheet,col,row,value) should return a true value if coloropt should be applied
42
- # if coloropt is None, func() should return a coloropt (or None) instead
43
- Colorizer = collections.namedtuple('Colorizer', 'precedence coloropt func')
44
- RowColorizer = collections.namedtuple('RowColorizer', 'precedence coloropt func')
45
- CellColorizer = collections.namedtuple('CellColorizer', 'precedence coloropt func')
46
- ColumnColorizer = collections.namedtuple('ColumnColorizer', 'precedence coloropt func')
47
72
 
48
73
  class RecursiveExprException(Exception):
49
74
  pass
50
75
 
51
76
  class LazyComputeRow:
52
77
  'Calculate column values as needed.'
53
- def __init__(self, sheet, row, col=None):
78
+ def __init__(self, sheet, row, col=None, **kwargs):
54
79
  self.row = row
55
80
  self.col = col
56
81
  self.sheet = sheet
82
+ self.extra = AttrDict(kwargs) # extra bindings
57
83
  self._usedcols = set()
58
84
 
59
85
  self._lcm.clear() # reset locals on lcm
@@ -62,11 +88,11 @@ class LazyComputeRow:
62
88
  def _lcm(self):
63
89
  lcmobj = self.col or self.sheet
64
90
  if not hasattr(lcmobj, '_lcm'):
65
- lcmobj._lcm = LazyChainMap(self.sheet, self.col, *vd.contexts)
91
+ lcmobj._lcm = LazyChainMap(self.sheet, self.col, self.extra, *vd.contexts)
66
92
  return lcmobj._lcm
67
93
 
68
94
  def __iter__(self):
69
- yield from self.sheet._ordered_colnames
95
+ yield from self.sheet.availColnames
70
96
  yield from self._lcm.keys()
71
97
  yield 'row'
72
98
  yield 'sheet'
@@ -86,18 +112,19 @@ class LazyComputeRow:
86
112
 
87
113
  def __getitem__(self, colid):
88
114
  try:
89
- i = self.sheet._ordered_colnames.index(colid)
90
- c = self.sheet._ordered_cols[i]
115
+ i = self.sheet.availColnames.index(colid)
116
+ c = self.sheet.availCols[i]
91
117
  if c is self.col: # ignore current column
92
- j = self.sheet._ordered_colnames[i+1:].index(colid)
93
- c = self.sheet._ordered_cols[i+j+1]
118
+ j = self.sheet.availColnames[i+1:].index(colid)
119
+ c = self.sheet.availCols[i+j+1]
94
120
 
95
121
  except ValueError:
96
122
  try:
97
123
  c = self._lcm[colid]
98
124
  except (KeyError, AttributeError) as e:
99
125
  if colid == 'sheet': return self.sheet
100
- elif colid == 'row': c = self.row
126
+ elif colid == 'row': return self
127
+ elif colid == '_row': return self.row
101
128
  elif colid == 'col': c = self.col
102
129
  else:
103
130
  raise KeyError(colid) from e
@@ -125,7 +152,7 @@ class TableSheet(BaseSheet):
125
152
  _coltype = SettableColumn
126
153
 
127
154
  rowtype = 'rows'
128
- guide = '# {sheet.help_title}\n{sheet.help_columns}\n'
155
+ guide = '# {sheet.help_title}\n'
129
156
 
130
157
  @property
131
158
  def help_title(self):
@@ -134,13 +161,6 @@ class TableSheet(BaseSheet):
134
161
  else:
135
162
  return 'Table Sheet'
136
163
 
137
- @property
138
- def help_columns(self):
139
- hiddenCols = [c for c in self.columns if c.hidden]
140
- if hiddenCols:
141
- return f'- `gv` to unhide {len(hiddenCols)} hidden columns'
142
- return ''
143
-
144
164
  columns = [] # list of Column
145
165
  colorizers = [ # list of Colorizer
146
166
  CellColorizer(2, 'color_default_hdr', lambda s,c,r,v: r is None),
@@ -148,9 +168,11 @@ class TableSheet(BaseSheet):
148
168
  ColumnColorizer(1, 'color_key_col', lambda s,c,r,v: c and c.keycol),
149
169
  CellColorizer(0, 'color_default', lambda s,c,r,v: True),
150
170
  RowColorizer(1, 'color_error', lambda s,c,r,v: isinstance(r, (Exception, TypedExceptionWrapper))),
151
- CellColorizer(3, 'color_current_cell', lambda s,c,r,v: c is s.cursorCol and r is s.cursorRow)
171
+ CellColorizer(3, 'color_current_cell', lambda s,c,r,v: c is s.cursorCol and r is s.cursorRow),
172
+ ColumnColorizer(1, 'color_hidden_col', lambda s,c,r,v: c and c.hidden),
152
173
  ]
153
174
  nKeys = 0 # columns[:nKeys] are key columns
175
+ _ordering = [] # list of (col:Column|str, reverse:bool)
154
176
 
155
177
  def __init__(self, *names, rows=UNLOADED, **kwargs):
156
178
  super().__init__(*names, rows=rows, **kwargs)
@@ -169,11 +191,10 @@ class TableSheet(BaseSheet):
169
191
  self.initialCols = kwargs.pop('columns', None) or type(self).columns
170
192
  self.resetCols()
171
193
 
194
+ self._ordering = list(type(self)._ordering) #2254
172
195
  self._colorizers = self.classColorizers
173
196
  self.recalc() # set .sheet on columns and start caches
174
197
 
175
- self._ordering = [] # list of (col:Column, reverse:bool)
176
-
177
198
  self.__dict__.update(kwargs) # also done earlier in BaseSheet.__init__
178
199
 
179
200
  @property
@@ -272,7 +293,7 @@ class TableSheet(BaseSheet):
272
293
  self.columns = []
273
294
  for c in self.initialCols:
274
295
  self.addColumn(deepcopy(c))
275
- if self.options.disp_help > c.max_help:
296
+ if self.options.disp_expert < c.disp_expert:
276
297
  c.hide()
277
298
 
278
299
  self.setKeys(self.columns[:self.nKeys])
@@ -282,7 +303,9 @@ class TableSheet(BaseSheet):
282
303
  self.rows = []
283
304
  try:
284
305
  with vd.Progress(gerund='loading', total=0):
285
- for r in self.iterload():
306
+ for i, r in enumerate(self.iterload()):
307
+ if self.precious and i > self.options.max_rows:
308
+ break
286
309
  self.addRow(r)
287
310
  except FileNotFoundError:
288
311
  return # let it be a blank sheet without error
@@ -367,10 +390,15 @@ class TableSheet(BaseSheet):
367
390
  def __repr__(self):
368
391
  return f'<{type(self).__name__}: {self.name}>'
369
392
 
370
- def evalExpr(self, expr, row=None, col=None):
393
+ @drawcache_property
394
+ def currow(self):
395
+ return LazyComputeRow(self, self.cursorRow, self.cursorCol)
396
+
397
+ def evalExpr(self, expr:str, row=None, col=None, **kwargs):
398
+ 'eval() expr in the context of (row, col), with extra bindings in kwargs'
371
399
  if row is not None:
372
400
  # contexts are cached by sheet/rowid for duration of drawcycle
373
- contexts = vd._evalcontexts.setdefault((self, self.rowid(row), col), LazyComputeRow(self, row, col=col))
401
+ contexts = vd._evalcontexts.setdefault((self, self.rowid(row), col), LazyComputeRow(self, row, col, **kwargs))
374
402
  else:
375
403
  contexts = dict(sheet=self)
376
404
 
@@ -383,22 +411,25 @@ class TableSheet(BaseSheet):
383
411
  @property
384
412
  def nScreenRows(self):
385
413
  'Number of visible rows at the current window height.'
386
- return (self.windowHeight-self.nHeaderRows-self.nFooterRows)//self.rowHeight
414
+ n = (self.windowHeight-self.nHeaderRows-self.nFooterRows)
415
+ if self.options.disp_multiline_focus: # focus multiline mode
416
+ return n-self.rowHeight+1
417
+ return n//self.rowHeight
387
418
 
388
419
  @drawcache_property
389
420
  def nHeaderRows(self):
390
421
  vcols = self.visibleCols
391
- return max(len(col.name.split('\n')) for col in vcols) if vcols else 0
422
+ return max(0, 1, *(len(col.name.split('\n')) for col in vcols))
392
423
 
393
424
  @property
394
425
  def nFooterRows(self):
395
426
  'Number of lines reserved at the bottom, including status line.'
396
- return 1
427
+ return len(self.allAggregators) + 1
397
428
 
398
429
  @property
399
430
  def cursorCol(self):
400
431
  'Current Column object.'
401
- vcols = self.visibleCols
432
+ vcols = self.availCols
402
433
  return vcols[min(self.cursorVisibleColIndex, len(vcols)-1)] if vcols else None
403
434
 
404
435
  @property
@@ -415,7 +446,7 @@ class TableSheet(BaseSheet):
415
446
  @drawcache_property
416
447
  def visibleCols(self): # non-hidden cols
417
448
  'List of non-hidden columns in display order.'
418
- return self.keyCols + [c for c in self.columns if not c.hidden and not c.keycol]
449
+ return (self.keyCols + [c for c in self.columns if not c.hidden and not c.keycol]) or [Column('', sheet=self)]
419
450
 
420
451
  @drawcache_property
421
452
  def keyCols(self):
@@ -423,14 +454,14 @@ class TableSheet(BaseSheet):
423
454
  return sorted([c for c in self.columns if c.keycol and not c.hidden], key=lambda c:c.keycol)
424
455
 
425
456
  @drawcache_property
426
- def _ordered_cols(self):
427
- 'List of all columns, visible columns first.'
457
+ def availCols(self):
458
+ 'List of all available columns, visible columns first.'
428
459
  return self.visibleCols + [c for c in self.columns if c.hidden]
429
460
 
430
461
  @drawcache_property
431
- def _ordered_colnames(self):
432
- 'List of all column names, visible columns first.'
433
- return [c.name for c in self._ordered_cols]
462
+ def availColnames(self):
463
+ 'List of all available column names, visible columns first.'
464
+ return [c.name for c in self.availCols]
434
465
 
435
466
  @property
436
467
  def cursorColIndex(self):
@@ -438,7 +469,7 @@ class TableSheet(BaseSheet):
438
469
  try:
439
470
  return self.columns.index(self.cursorCol)
440
471
  except ValueError:
441
- return None
472
+ return 0
442
473
 
443
474
  @property
444
475
  def nonKeyVisibleCols(self):
@@ -530,7 +561,13 @@ class TableSheet(BaseSheet):
530
561
  idx = len(self.columns) if index is None else index
531
562
  col.recalc(self)
532
563
  self.columns.insert(idx+i, col)
533
- Sheet.visibleCols.fget.cache_clear()
564
+
565
+ # statements after addColumn in the same command may want to use these cached properties
566
+ Sheet.keyCols.fget.cache_clear()
567
+ Sheet.visibleCols.fget.cache_clear()
568
+ Sheet.availCols.fget.cache_clear()
569
+ Sheet.availColnames.fget.cache_clear()
570
+ Sheet.colsByName.fget.cache_clear()
534
571
 
535
572
  return cols[0]
536
573
 
@@ -578,7 +615,7 @@ class TableSheet(BaseSheet):
578
615
  'Return tuple of the key for *row*.'
579
616
  return tuple(c.getTypedValue(row) for c in self.keyCols)
580
617
 
581
- def keystr(self, row):
618
+ def rowname(self, row):
582
619
  'Return string of the key for *row*.'
583
620
  return ','.join(map(str, self.rowkey(row)))
584
621
 
@@ -592,8 +629,8 @@ class TableSheet(BaseSheet):
592
629
 
593
630
  if self.cursorVisibleColIndex <= 0:
594
631
  self.cursorVisibleColIndex = 0
595
- elif self.cursorVisibleColIndex >= self.nVisibleCols:
596
- self.cursorVisibleColIndex = self.nVisibleCols-1
632
+ elif self.cursorVisibleColIndex >= len(self.availCols):
633
+ self.cursorVisibleColIndex = len(self.availCols)-1
597
634
 
598
635
  if self.topRowIndex < 0:
599
636
  self.topRowIndex = 0
@@ -639,8 +676,8 @@ class TableSheet(BaseSheet):
639
676
  self._visibleColLayout = {}
640
677
  x = 0
641
678
  vcolidx = 0
642
- for vcolidx in range(0, self.nVisibleCols):
643
- width = self.calcSingleColLayout(vcolidx, x, minColWidth)
679
+ for vcolidx, col in enumerate(self.availCols):
680
+ width = self.calcSingleColLayout(col, vcolidx, x, minColWidth)
644
681
  if width:
645
682
  x += width+sepColWidth
646
683
  if x > winWidth-1:
@@ -648,17 +685,21 @@ class TableSheet(BaseSheet):
648
685
 
649
686
  self.rightVisibleColIndex = vcolidx
650
687
 
651
- def calcSingleColLayout(self, vcolidx:int, x:int=0, minColWidth:int=4):
652
- col = self.visibleCols[vcolidx]
688
+ def calcSingleColLayout(self, col:Column, vcolidx:int, x:int=0, minColWidth:int=4):
653
689
  if col.width is None and len(self.visibleRows) > 0:
654
690
  vrows = self.visibleRows if self.nRows > 1000 else self.rows[:1000] #1964
655
691
  # handle delayed column width-finding
656
692
  col.width = max(col.getMaxWidth(vrows), minColWidth)
657
- if vcolidx != self.nVisibleCols-1: # let last column fill up the max width
693
+ if vcolidx < self.nVisibleCols-1: # let last column fill up the max width
658
694
  col.width = min(col.width, self.options.default_width)
695
+
659
696
  width = col.width if col.width is not None else self.options.default_width
660
- if col in self.keyCols:
661
- width = max(width, 1) # keycols must all be visible
697
+
698
+ # when cursor showing a hidden column
699
+ if vcolidx >= self.nVisibleCols and vcolidx == self.cursorVisibleColIndex:
700
+ width = self.options.default_width
701
+
702
+ width = max(width, 1)
662
703
  if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns
663
704
  self._visibleColLayout[vcolidx] = [x, min(width, self.windowWidth-x)]
664
705
  return width
@@ -666,18 +707,18 @@ class TableSheet(BaseSheet):
666
707
 
667
708
  def drawColHeader(self, scr, y, h, vcolidx):
668
709
  'Compose and draw column header for given vcolidx.'
669
- col = self.visibleCols[vcolidx]
710
+ col = self.availCols[vcolidx]
670
711
 
671
712
  # hdrattr highlights whole column header
672
713
  # sepattr is for header separators and indicators
673
- sepcattr = colors.get_color('color_column_sep')
714
+ sepcattr = update_attr(colors.color_default, colors.get_color('color_column_sep'), 2)
674
715
 
675
716
  hdrcattr = self._colorize(col, None)
676
717
  if vcolidx == self.cursorVisibleColIndex:
677
718
  hdrcattr = update_attr(hdrcattr, colors.color_current_hdr, 2)
678
719
 
679
720
  C = self.options.disp_column_sep
680
- if (self.keyCols and col is self.keyCols[-1]) or vcolidx == self.rightVisibleColIndex:
721
+ if (self.keyCols and col is self.keyCols[-1]) or vcolidx == self.nVisibleCols-1:
681
722
  C = self.options.disp_keycol_sep
682
723
 
683
724
  x, colwidth = self._visibleColLayout[vcolidx]
@@ -699,13 +740,14 @@ class TableSheet(BaseSheet):
699
740
  if i == h-1:
700
741
  hdrcattr = update_attr(hdrcattr, colors.color_bottom_hdr, 5)
701
742
 
702
- clipdraw(scr, y+i, x, name, hdrcattr, w=colwidth)
743
+ if y+i < self.windowHeight:
744
+ clipdraw(scr, y+i, x, name, hdrcattr, w=colwidth)
703
745
  vd.onMouse(scr, x, y+i, colwidth, 1, BUTTON3_RELEASED='rename-col')
704
746
 
705
- if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowWidth:
747
+ if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowHeight:
706
748
  scr.addstr(y+i, x+colwidth, C, sepcattr.attr)
707
749
 
708
- clipdraw(scr, y+h-1, x+colwidth-len(T), T, hdrcattr)
750
+ clipdraw(scr, y+h-1, min(x+colwidth, self.windowWidth-1)-dispwidth(T), T, hdrcattr)
709
751
 
710
752
  try:
711
753
  if vcolidx == self.leftVisibleColIndex and col not in self.keyCols and self.nonKeyVisibleCols.index(col) > 0:
@@ -718,7 +760,7 @@ class TableSheet(BaseSheet):
718
760
  A = ''
719
761
  for j, (sortcol, sortdir) in enumerate(self._ordering):
720
762
  if isinstance(sortcol, str):
721
- sortcol = self.column(sortcol)
763
+ sortcol = self.colsByName.get(sortcol) # self.column will fail if sortcol was renamed
722
764
  if col is sortcol:
723
765
  A = self.options.disp_sort_desc[j] if sortdir else self.options.disp_sort_asc[j]
724
766
  scr.addstr(y+h-1, x, A, hdrcattr.attr)
@@ -730,6 +772,17 @@ class TableSheet(BaseSheet):
730
772
  'Return boolean: is given column index a key column?'
731
773
  return self.visibleCols[vcolidx] in self.keyCols
732
774
 
775
+ @drawcache_property
776
+ def allAggregators(self):
777
+ 'Return dict of aggname -> list of cols with that aggregator.'
778
+ allaggs = collections.defaultdict(list) # aggname -> list of cols with that aggregator
779
+ for vcolidx, (x, colwidth) in sorted(self._visibleColLayout.items()):
780
+ col = self.availCols[vcolidx]
781
+ if not col.hidden:
782
+ for aggr in col.aggregators:
783
+ allaggs[aggr.name].append(vcolidx)
784
+ return allaggs
785
+
733
786
  def draw(self, scr):
734
787
  'Draw entire screen onto the `scr` curses object.'
735
788
  if not self.columns:
@@ -767,7 +820,7 @@ class TableSheet(BaseSheet):
767
820
 
768
821
  y = headerRow + numHeaderRows
769
822
 
770
- rows = self.rows[self.topRowIndex:min(self.topRowIndex+self.nScreenRows+1, self.nRows)]
823
+ rows = self.rows[self.topRowIndex:min(self.topRowIndex+self.nScreenRows, self.nRows)]
771
824
  vd.callNoExceptions(self.checkCursor)
772
825
 
773
826
  for rowidx, row in enumerate(rows):
@@ -781,15 +834,42 @@ class TableSheet(BaseSheet):
781
834
  if vcolidx+1 < self.nVisibleCols:
782
835
  scr.addstr(headerRow, self.windowWidth-2, self.options.disp_more_right, colors.color_column_sep.attr)
783
836
 
837
+ # draw bottom-row aggregators #2209
838
+ rightx, rightw = self._visibleColLayout[self.rightVisibleColIndex]
839
+ rightx += rightw+1
840
+
841
+ for aggrname, colidxs in self.allAggregators.items():
842
+ clipdraw(scr, y, 0, ' '*rightx + f' {aggrname:9}', colors.color_aggregator, truncator='+')
843
+
844
+ for vcolidx in colidxs:
845
+ x, colwidth = self._visibleColLayout[vcolidx]
846
+ col = self.availCols[vcolidx]
847
+
848
+ if not col.hidden:
849
+ dw = DisplayWrapper('')
850
+ try:
851
+ agg = vd.aggregators[aggrname]
852
+ dw.value = col.aggregateTotal(agg)
853
+ dw.typedval = wrapply(agg.type or col.type, dw.value)
854
+ dw.text = col.format(dw.typedval)
855
+ except Exception as e:
856
+ dw.note = self.options.disp_note_typeexc
857
+ dw.notecolor = 'color_warning'
858
+ vd.exceptionCaught(e, status=False)
859
+ disps = [('', ' ')] + list(col.display(dw, width=colwidth))
860
+ clipdraw_chunks(scr, y, x, disps, colors.color_aggregator, w=colwidth)
861
+ y += 1
862
+
863
+
784
864
  def calc_height(self, row, displines=None, isNull=None, maxheight=1):
785
- 'render cell contents ifor row into displines'
865
+ 'render cell contents for row into displines'
786
866
  if displines is None:
787
867
  displines = {} # [vcolidx] -> list of lines in that cell
788
868
 
789
869
  for vcolidx, (x, colwidth) in sorted(self._visibleColLayout.items()):
790
870
  if x < self.windowWidth: # only draw inside window
791
- vcols = self.visibleCols
792
- if vcolidx >= len(vcols):
871
+ vcols = self.availCols
872
+ if vcolidx >= self.nVisibleCols and vcolidx != self.cursorVisibleColIndex:
793
873
  continue
794
874
  col = vcols[vcolidx]
795
875
  cellval = col.getCell(row)
@@ -842,14 +922,35 @@ class TableSheet(BaseSheet):
842
922
 
843
923
  # calc_height renders cell contents into displines
844
924
  displines = {} # [vcolidx] -> list of lines in that cell
845
- self.calc_height(row, displines, maxheight=self.rowHeight)
925
+ if options.disp_multiline_focus:
926
+ height = self.rowHeight if rowidx == self.cursorRowIndex else 1
927
+ else:
928
+ height = min(self.rowHeight, maxheight) or 1 # display even empty rows
929
+
930
+ self.calc_height(row, displines, maxheight=height)
846
931
 
847
- height = min(self.rowHeight, maxheight) or 1 # display even empty rows
848
932
  self._rowLayout[rowidx] = (ybase, height)
849
933
 
934
+ if height > 1:
935
+ colseps = [topsep] + [midsep]*(height-2) + [botsep]
936
+ endseps = [endtopsep] + [endmidsep]*(height-2) + [endbotsep]
937
+ keyseps = [keytopsep] + [keymidsep]*(height-2) + [keybotsep]
938
+ else:
939
+ colseps = [colsep]
940
+ endseps = [endsep]
941
+ keyseps = [keysep]
942
+
850
943
  for vcolidx, (col, cellval, lines) in displines.items():
851
944
  if vcolidx not in self._visibleColLayout:
852
945
  continue
946
+
947
+ if vcolidx == self.nVisibleCols-1: # right edge of sheet
948
+ seps = endseps
949
+ elif (self.keyCols and col is self.keyCols[-1]): # last keycol
950
+ seps = keyseps
951
+ else:
952
+ seps = colseps
953
+
853
954
  x, colwidth = self._visibleColLayout[vcolidx]
854
955
  hoffset = col.hoffset
855
956
  voffset = col.voffset
@@ -861,7 +962,7 @@ class TableSheet(BaseSheet):
861
962
  notewidth = 1 if note else 0
862
963
  if note:
863
964
  notecattr = update_attr(cattr, colors.get_color(cellval.notecolor), 10)
864
- scr.addstr(ybase, x+colwidth-notewidth, note, notecattr.attr)
965
+ clipdraw(scr, ybase, x+colwidth-notewidth, note, notecattr)
865
966
 
866
967
  lines = lines[voffset:]
867
968
 
@@ -873,36 +974,7 @@ class TableSheet(BaseSheet):
873
974
  for i, chunks in enumerate(lines):
874
975
  y = ybase+i
875
976
 
876
- if vcolidx == self.nVisibleCols-1: # right edge of sheet
877
- if len(lines) == 1:
878
- sepchars = endsep
879
- else:
880
- if i == 0:
881
- sepchars = endtopsep
882
- elif i == len(lines)-1:
883
- sepchars = endbotsep
884
- else:
885
- sepchars = endmidsep
886
- elif (self.keyCols and col is self.keyCols[-1]): # last keycol
887
- if len(lines) == 1:
888
- sepchars = keysep
889
- else:
890
- if i == 0:
891
- sepchars = keytopsep
892
- elif i == len(lines)-1:
893
- sepchars = keybotsep
894
- else:
895
- sepchars = keymidsep
896
- else:
897
- if len(lines) == 1:
898
- sepchars = colsep
899
- else:
900
- if i == 0:
901
- sepchars = topsep
902
- elif i == len(lines)-1:
903
- sepchars = botsep
904
- else:
905
- sepchars = midsep
977
+ sepchars = seps[i]
906
978
 
907
979
  pre = disp_truncator if hoffset != 0 else disp_column_fill
908
980
  prechunks = []
@@ -915,7 +987,7 @@ class TableSheet(BaseSheet):
915
987
  clipdraw_chunks(scr, y, x, prechunks, cattr, w=colwidth-notewidth)
916
988
  vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell')
917
989
 
918
- if x+colwidth+len(sepchars) <= self.windowWidth:
990
+ if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth:
919
991
  scr.addstr(y, x+colwidth, sepchars, sepcattr.attr)
920
992
 
921
993
  for notefunc in vd.rowNoters:
@@ -976,7 +1048,9 @@ class SequenceSheet(Sheet):
976
1048
 
977
1049
  self.rows = []
978
1050
  # add the rest of the rows
979
- for r in vd.Progress(itsource, gerund='loading', total=0):
1051
+ for i, r in enumerate(vd.Progress(itsource, gerund='loading', total=0)):
1052
+ if self.precious and i > self.options.max_rows:
1053
+ break
980
1054
  self.addRow(r)
981
1055
 
982
1056
 
@@ -1032,6 +1106,8 @@ def push(vd, vs, pane=0, load=True):
1032
1106
 
1033
1107
  if load:
1034
1108
  vs.ensureLoaded()
1109
+ if vd.activeCommand:
1110
+ vs.longname = vd.activeCommand.longname
1035
1111
 
1036
1112
 
1037
1113
  @VisiData.api
@@ -1042,6 +1118,8 @@ def quit(vd, *sheets):
1042
1118
  vs.confirmQuit('quit')
1043
1119
  vs.pane = 0
1044
1120
  vd.remove(vs)
1121
+ if vd.activeCommand:
1122
+ vd.activeSheet.longname = vd.activeCommand.longname
1045
1123
 
1046
1124
 
1047
1125
  @BaseSheet.api
@@ -1064,7 +1142,7 @@ def preloadHook(sheet):
1064
1142
 
1065
1143
  @VisiData.api
1066
1144
  def newSheet(vd, name, ncols, **kwargs):
1067
- return Sheet(name, columns=[SettableColumn() for i in range(ncols)], **kwargs)
1145
+ return Sheet(name, columns=[SettableColumn(width=vd.options.default_width) for i in range(ncols)], **kwargs)
1068
1146
 
1069
1147
 
1070
1148
  @BaseSheet.api
@@ -1081,13 +1159,6 @@ def quitAndReleaseMemory(vs):
1081
1159
  vd.allSheets.remove(vs)
1082
1160
 
1083
1161
 
1084
- @Sheet.api
1085
- def updateColNames(sheet, rows, cols, overwrite=False):
1086
- vd.addUndoColNames(cols)
1087
- for c in cols:
1088
- if not c._name or overwrite:
1089
- c.name = "\n".join(c.getDisplayValue(r) for r in rows)
1090
-
1091
1162
  @BaseSheet.api
1092
1163
  def splitPane(sheet, pct=None):
1093
1164
  if vd.activeStack[1:]:
@@ -1116,7 +1187,7 @@ BaseSheet.init('pane', lambda: 1)
1116
1187
 
1117
1188
 
1118
1189
  BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload()', 'Reload current sheet')
1119
- Sheet.addCommand('^G', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1190
+ Sheet.addCommand('', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1120
1191
 
1121
1192
  Sheet.addCommand('!', 'key-col', 'toggleKeys([cursorCol])', 'toggle current column as a key column')
1122
1193
  Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current column as a key column')
@@ -1158,7 +1229,7 @@ BaseSheet.addCommand('A', 'open-new', 'vd.push(vd.newSheet("unnamed", 1))', 'Ope
1158
1229
  BaseSheet.addCommand('`', 'open-source', 'vd.push(source)', 'open source sheet')
1159
1230
  BaseSheet.addCommand(None, 'rename-sheet', 'sheet.name = input("rename sheet to: ", value=cleanName(sheet.name))', 'Rename current sheet')
1160
1231
 
1161
- Sheet.addCommand('', 'addcol-source', 'source .addColumn(copy(cursorCol)) if isinstance (source, BaseSheet) else error("source must be sheet")', 'add copy of current column to source sheet') #988 frosencrantz
1232
+ Sheet.addCommand('', 'addcol-source', 'source.addColumn(copy(cursorCol)) if isinstance (source, BaseSheet) else error("source must be sheet")', 'add copy of current column to source sheet') #988 frosencrantz
1162
1233
 
1163
1234
 
1164
1235
  @Column.api