visidata 3.1.1__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 (99) hide show
  1. visidata/__init__.py +2 -2
  2. visidata/_input.py +106 -58
  3. visidata/_open.py +10 -7
  4. visidata/_types.py +2 -2
  5. visidata/aggregators.py +125 -16
  6. visidata/apps/vdsql/_ibis.py +8 -13
  7. visidata/basesheet.py +4 -3
  8. visidata/canvas.py +11 -7
  9. visidata/clipboard.py +11 -2
  10. visidata/cliptext.py +68 -23
  11. visidata/cmdlog.py +5 -1
  12. visidata/column.py +48 -33
  13. visidata/ddwplay.py +2 -2
  14. visidata/deprecated.py +96 -63
  15. visidata/errors.py +41 -5
  16. visidata/{features → experimental}/helloworld.py +1 -1
  17. visidata/experimental/liveupdate.py +1 -1
  18. visidata/expr.py +1 -0
  19. visidata/extensible.py +4 -0
  20. visidata/features/cmdpalette.py +64 -25
  21. visidata/features/describe.py +2 -2
  22. visidata/features/expand_cols.py +7 -5
  23. visidata/features/freeze.py +14 -2
  24. visidata/features/go_col.py +3 -3
  25. visidata/features/graph_zoom_y.py +47 -0
  26. visidata/features/incr.py +7 -3
  27. visidata/features/join.py +23 -12
  28. visidata/features/layout.py +8 -4
  29. visidata/features/melt.py +1 -0
  30. visidata/features/rank.py +103 -0
  31. visidata/features/reload_every.py +11 -8
  32. visidata/features/sysedit.py +14 -4
  33. visidata/features/transpose.py +1 -0
  34. visidata/features/window.py +12 -0
  35. visidata/form.py +10 -9
  36. visidata/freqtbl.py +47 -3
  37. visidata/fuzzymatch.py +11 -7
  38. visidata/graph.py +5 -3
  39. visidata/guides/AggregatorsSheet.md +84 -0
  40. visidata/guides/CommandsSheet.md +1 -0
  41. visidata/guides/MacrosSheet.md +1 -1
  42. visidata/guides/RankGuide.md +51 -0
  43. visidata/guides/TypesSheet.md +1 -1
  44. visidata/guides/WindowFunctionGuide.md +49 -0
  45. visidata/help.py +23 -6
  46. visidata/indexsheet.py +1 -1
  47. visidata/loaders/_pandas.py +3 -1
  48. visidata/loaders/archive.py +33 -6
  49. visidata/loaders/csv.py +12 -1
  50. visidata/loaders/eml.py +2 -0
  51. visidata/loaders/f5log.py +2 -2
  52. visidata/loaders/fec.py +6 -9
  53. visidata/loaders/fixed_width.py +2 -0
  54. visidata/loaders/hdf5.py +34 -10
  55. visidata/loaders/npy.py +54 -23
  56. visidata/loaders/orgmode.py +3 -2
  57. visidata/loaders/pandas_freqtbl.py +4 -0
  58. visidata/loaders/psv.py +13 -0
  59. visidata/loaders/sqlite.py +1 -1
  60. visidata/loaders/vds.py +3 -4
  61. visidata/macros.py +5 -4
  62. visidata/main.py +21 -11
  63. visidata/mainloop.py +8 -5
  64. visidata/man/parse_options.py +3 -2
  65. visidata/man/vd.1 +38 -17
  66. visidata/man/vd.txt +47 -17
  67. visidata/menu.py +10 -10
  68. visidata/metasheets.py +3 -3
  69. visidata/mouse.py +3 -0
  70. visidata/movement.py +6 -3
  71. visidata/pyobj.py +17 -9
  72. visidata/save.py +10 -2
  73. visidata/selection.py +29 -18
  74. visidata/settings.py +9 -5
  75. visidata/sheets.py +124 -48
  76. visidata/shell.py +2 -2
  77. visidata/sidebar.py +11 -8
  78. visidata/sort.py +89 -11
  79. visidata/statusbar.py +10 -9
  80. visidata/tests/test_cliptext.py +164 -0
  81. visidata/tests/test_commands.py +6 -2
  82. visidata/tests/test_menu.py +1 -1
  83. visidata/textsheet.py +34 -8
  84. visidata/themes/ascii8.py +2 -2
  85. visidata/themes/light.py +5 -0
  86. visidata/threads.py +38 -8
  87. visidata/utils.py +15 -1
  88. visidata/vendor/__init__.py +0 -0
  89. {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/vd.1 +38 -17
  90. {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/visidata.1 +38 -17
  91. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/METADATA +62 -15
  92. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/RECORD +98 -92
  93. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/WHEEL +1 -1
  94. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/entry_points.txt +1 -0
  95. visidata-3.1.1.data/scripts/vd +0 -6
  96. {visidata-3.1.1.data → visidata-3.3.data}/data/share/applications/visidata.desktop +0 -0
  97. {visidata-3.1.1.data → visidata-3.3.data}/scripts/vd2to3.vdx +0 -0
  98. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/LICENSE.gpl3 +0 -0
  99. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/top_level.txt +0 -0
visidata/basesheet.py CHANGED
@@ -76,6 +76,8 @@ class DrawablePane(Extensible):
76
76
 
77
77
  try:
78
78
  self.sheet = self
79
+ if cmd.deprecated:
80
+ vd.deprecated_warn(cmd.longname, cmd.deprecated, 'a different command')
79
81
  code = compile(cmd.execstr, cmd.longname, 'exec')
80
82
  exec(code, vdglobals, LazyChainMap(vd, self))
81
83
  return False
@@ -105,6 +107,7 @@ class BaseSheet(DrawablePane):
105
107
  precious = True # False for a few discardable metasheets
106
108
  defer = False # False for not deferring changes until save
107
109
  guide = '' # default to show in sidebar
110
+ icon = '›'
108
111
 
109
112
  def _obj_options(self):
110
113
  return vd.OptionsObject(vd._options, obj=self)
@@ -212,7 +215,7 @@ class BaseSheet(DrawablePane):
212
215
  try:
213
216
  for hookfunc in vd.beforeExecHooks:
214
217
  hookfunc(self, cmd, '', keystrokes)
215
- escaped = super().execCommand2(cmd, vdglobals=vdglobals)
218
+ escaped = self.execCommand2(cmd, vdglobals=vdglobals)
216
219
  except Exception as e:
217
220
  vd.debug(cmd.execstr)
218
221
  err = vd.exceptionCaught(e)
@@ -299,8 +302,6 @@ class BaseSheet(DrawablePane):
299
302
  'Return formatted string with *sheet* and *vd* accessible to expressions. Missing expressions return empty strings instead of error.'
300
303
  return MissingAttrFormatter().format(fmt, sheet=self, vd=vd, **kwargs)
301
304
 
302
-
303
-
304
305
  @VisiData.api
305
306
  def redraw(vd):
306
307
  'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.'
visidata/canvas.py CHANGED
@@ -11,7 +11,7 @@ from visidata.bezier import bezier
11
11
  vd.theme_option('disp_graph_labels', True, 'show axes and legend on graph')
12
12
  vd.theme_option('plot_colors', 'green red yellow cyan magenta white 38 136 168', 'list of distinct colors to use for plotting distinct objects')
13
13
  vd.theme_option('disp_canvas_charset', ''.join(chr(0x2800+i) for i in range(256)), 'charset to render 2x4 blocks on canvas')
14
- vd.theme_option('disp_pixel_random', False, 'randomly choose attr from set of pixels instead of most common')
14
+ vd.theme_option('disp_graph_pixel_random', False, 'randomly choose attr from set of pixels instead of most common')
15
15
  vd.theme_option('disp_zoom_incr', 2.0, 'amount to multiply current zoomlevel when zooming')
16
16
  vd.theme_option('color_graph_hidden', '238 blue', 'color of legend for hidden attribute')
17
17
  vd.theme_option('color_graph_selected', 'bold', 'color of selected graph points')
@@ -258,7 +258,7 @@ class Plotter(BaseSheet):
258
258
  disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
259
259
  if self.pixels:
260
260
  cursorBBox = self.plotterCursorBox
261
- getPixelAttr = self.getPixelAttrRandom if self.options.disp_pixel_random else self.getPixelAttrMost
261
+ getPixelAttr = self.getPixelAttrRandom if self.options.disp_graph_pixel_random else self.getPixelAttrMost
262
262
 
263
263
  for char_y in range(0, self.plotheight//4):
264
264
  for char_x in range(0, self.plotwidth//2):
@@ -304,8 +304,8 @@ class Plotter(BaseSheet):
304
304
  def _overlaps(a, b):
305
305
  a_x1, _, a_txt, _, _ = a
306
306
  b_x1, _, b_txt, _, _ = b
307
- a_x2 = a_x1 + len(a_txt)
308
- b_x2 = b_x1 + len(b_txt)
307
+ a_x2 = a_x1 + dispwidth(a_txt)
308
+ b_x2 = b_x1 + dispwidth(b_txt)
309
309
  if a_x1 < b_x1 < a_x2 or a_x1 < b_x2 < a_x2 or \
310
310
  b_x1 < a_x1 < b_x2 or b_x1 < a_x2 < b_x2:
311
311
  return True
@@ -325,10 +325,10 @@ class Plotter(BaseSheet):
325
325
  for pix_x, pix_y, txt, attr, row in self.labels:
326
326
  if attr in self.hiddenAttrs:
327
327
  continue
328
- if row is not None:
329
- pix_x -= len(txt)/2*2
330
328
  char_y = int(pix_y/4)
331
329
  char_x = int(pix_x/2)
330
+ if row is not None:
331
+ char_x -= math.ceil(dispwidth(txt)/2)*2
332
332
  o = (char_x, char_y, txt, attr, row)
333
333
  _mark_overlap_text(labels_by_line[char_y], o)
334
334
 
@@ -356,6 +356,7 @@ class Canvas(Plotter):
356
356
  rightMarginPixels = 4*2
357
357
  topMarginPixels = 0*4
358
358
  bottomMarginPixels = 1*4 # reserve bottom line for x axis
359
+ guide = '# Canvas\n'
359
360
 
360
361
  def __init__(self, *names, **kwargs):
361
362
  self.left_margin = self.leftMarginPixels
@@ -384,6 +385,9 @@ class Canvas(Plotter):
384
385
  def reset(self):
385
386
  'clear everything in preparation for a fresh reload()'
386
387
  self.polylines.clear()
388
+ self.canvasBox = None
389
+ self.visibleBox = None
390
+ self.cursorBox = None
387
391
  self.left_margin = self.leftMarginPixels
388
392
  self.legends.clear()
389
393
  self.legendwidth = 0
@@ -727,7 +731,7 @@ class Canvas(Plotter):
727
731
  def plot_elements(self, invert_y=False):
728
732
  'plots points and lines and text onto the plotter'
729
733
 
730
- self.resetBounds()
734
+ self.resetBounds(refresh=False)
731
735
 
732
736
  bb = self.visibleBox
733
737
  xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
visidata/clipboard.py CHANGED
@@ -7,13 +7,21 @@ import tempfile
7
7
  import functools
8
8
  import os
9
9
  import itertools
10
+ import platform
10
11
 
11
12
  from visidata import VisiData, vd, asyncthread, SettableColumn
12
13
  from visidata import Sheet, Path, Column
13
14
 
14
- if sys.platform == 'win32':
15
+ if (
16
+ # Windows
17
+ sys.platform == 'win32'
18
+ # WSL 2
19
+ or "microsoft-standard-WSL2" in platform.uname().release
20
+ # WSL 1
21
+ or sys.platform == 'linux' and platform.uname().release.endswith("-Microsoft")
22
+ ):
15
23
  syscopy_cmd_default = 'clip.exe'
16
- syspaste_cmd_default = 'powershell -command Get-Clipboard'
24
+ syspaste_cmd_default = 'powershell.exe -noprofile -command Get-Clipboard'
17
25
  elif sys.platform == 'darwin':
18
26
  syscopy_cmd_default = 'pbcopy w'
19
27
  syspaste_cmd_default = 'pbpaste'
@@ -196,6 +204,7 @@ Sheet.addCommand('Y', 'syscopy-row', 'syscopyCells(visibleCols, [cursorRow])', '
196
204
 
197
205
  Sheet.addCommand('gY', 'syscopy-selected', 'syscopyCells(visibleCols, onlySelectedRows)', 'yank (copy) selected rows to system clipboard (using options.clipboard_copy_cmd)')
198
206
  Sheet.addCommand('zY', 'syscopy-cell', 'syscopyValue(cursorDisplay)', 'yank (copy) current cell to system clipboard (using options.clipboard_copy_cmd)')
207
+ Sheet.addCommand('', 'syscopy-colname', 'syscopyValue(cursorCol.name)', 'yank (copy) current column header to system clipboard (using options.clipboard_copy_cmd)')
199
208
  Sheet.addCommand('gzY', 'syscopy-cells', 'syscopyCells([cursorCol], onlySelectedRows, filetype="txt")', 'yank (copy) contents of current column from selected rows to system clipboard (using options.clipboard_copy_cmd')
200
209
 
201
210
  Sheet.addCommand('x', 'cut-row', 'copyRows([sheet.delete_row(cursorRowIndex)]); defer and cursorDown(1)', 'delete (cut) current row and move it to clipboard')
visidata/cliptext.py CHANGED
@@ -142,42 +142,52 @@ def iterchars(x):
142
142
 
143
143
  @functools.lru_cache(maxsize=100000)
144
144
  def _clipstr(s, dispw, trunch='', oddspacech='', combch='', modch=''):
145
- '''Return clipped string and width in terminal display characters.
146
- Note: width may differ from len(s) if East Asian chars are 'fullwidth'.'''
147
- if not s:
145
+ ''' *s* is a string or an iterator that contains characters.
146
+ *dispw* is the integer screen width that the clipped string will fit inside, or None.
147
+ Return clipped string and width in terminal display characters.
148
+ Note: width may differ from len(s) if chars are 'fullwidth'.
149
+ If *dispw* is None, no clipping occurs.
150
+ If *trunch* has a width greater than *dispw*, the empty string
151
+ will be used as a truncator instead.'''
152
+ if not s or (dispw is not None and dispw < 1): #iterator s would be truthy
148
153
  return '', 0
149
154
 
150
- if dispw == 1:
151
- return s[0], 1
152
-
153
155
  w = 0
154
156
  ret = ''
157
+ trunc_i = 0
158
+ w_truncated = 0
155
159
 
156
160
  trunchlen = dispwidth(trunch)
161
+ if dispw is None:
162
+ s = ''.join(s)
163
+ return s, dispwidth(s)
164
+ if trunchlen > dispw: #if the truncator cannot fit, use a truncator of ''
165
+ return _clipstr(s, dispw, trunch='', oddspacech=oddspacech, combch=combch, modch=modch)
157
166
  for c in s:
158
167
  newc, chlen = _dispch(c, oddspacech=oddspacech, combch=combch, modch=modch)
159
168
  if not newc:
160
169
  newc = c
161
170
  chlen = dispwidth(c)
162
171
 
163
- if dispw and w+chlen > dispw:
164
- if trunchlen and dispw > trunchlen:
165
- lastchlen = _dispch(ret[-1])[1]
166
- if w+trunchlen > dispw:
167
- ret = ret[:-1]
168
- w -= lastchlen
169
- ret += trunch # replace final char with ellipsis
170
- w += trunchlen
171
- break
172
-
173
- w += chlen
174
- ret += newc
172
+ #if the next character will fit
173
+ if w+chlen <= dispw:
174
+ ret += newc
175
+ w += chlen
176
+ #move the truncation spot forward only when the truncation character can fit
177
+ if w+trunchlen <= dispw:
178
+ trunc_i += 1
179
+ w_truncated += chlen
180
+ continue
181
+ # if we reach this line, a character did not fit, and the result needs truncation
182
+ return ret[:trunc_i] + trunch, w_truncated+trunchlen
175
183
 
176
184
  return ret, w
177
185
 
178
186
 
179
187
  @drawcache
180
188
  def clipstr(s, dispw, truncator=None, oddspace=None):
189
+ ''' *s* is a string or an iterator that contains characters.
190
+ *dispw* is the integer screen width that the clipped string will fit inside, or None.'''
181
191
  if options.visibility:
182
192
  return _clipstr(s, dispw,
183
193
  trunch=options.disp_truncator if truncator is None else truncator,
@@ -205,8 +215,6 @@ def clipdraw(scr, y, x, s, attr, w=None, clear=True, literal=False, **kwargs):
205
215
 
206
216
  x = max(0, x)
207
217
  y = max(0, y)
208
- assert x >= 0, x
209
- assert y >= 0, y
210
218
 
211
219
  return clipdraw_chunks(scr, y, x, chunks, attr, w=w, clear=clear, **kwargs)
212
220
 
@@ -310,6 +318,10 @@ def wraptext(text, width=80, indent=''):
310
318
  line = _markdown_to_internal(line)
311
319
  chunks = re.split(internal_markup_re, line)
312
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
324
+ # textwrap.wrap does not handle variable-width characters #2416
313
325
  for linenum, textline in enumerate(textwrap.wrap(''.join(textchunks), width=width, drop_whitespace=False)):
314
326
  txt = textline
315
327
  r = ''
@@ -345,8 +357,39 @@ def clipbox(scr, lines, attr, title=''):
345
357
  for i, line in enumerate(lines):
346
358
  clipdraw(scr, i+1, 2, line, attr)
347
359
 
348
- clipdraw(scr, 0, w-len(title)-6, f"| {title} |", attr)
349
-
360
+ clipdraw(scr, 0, w-dispwidth(title)-6, f"| {title} |", attr)
361
+
362
+ def clipstr_start(dispval, w, truncator=''):
363
+ '''Return a tuple (frag, dw), where *frag* is the longest ending substring
364
+ of *dispval* that will fit in a space *w* terminal display characters wide,
365
+ and *dw* is the substring's display width as an int.'''
366
+ # Note: this implementation is likely incorrect for unusual Unicode
367
+ # strings or encodings, where trimming an initial character produces
368
+ # an invalid string or does not make the string shorter.
369
+ if w <= 0: return '', 0
370
+ j = len(dispval)
371
+ while j >= 1:
372
+ if dispwidth((truncator if j > 1 else '') + dispval[j-1:]) <= w:
373
+ j -= 1
374
+ else:
375
+ break
376
+ frag = (truncator if j > 0 else '') + dispval[j:]
377
+ return frag, dispwidth(frag)
378
+
379
+ def clipstr_middle(s, n=10, truncator='…'):
380
+ '''Return a string having a display width <= *n*. Excess characters are
381
+ trimmed from the middle of the string, and replaced by a single
382
+ instance of *truncator*.'''
383
+ if n == 0: return '', 0
384
+ if dispwidth(s) > n:
385
+ #for even widths, give the leftover 1 space to the right fragment
386
+ l_space = n//2 if n%2 == 1 else max(n//2-1, 0)
387
+ l_frag, l_w = _clipstr(s, l_space)
388
+ #if left fragment did not fill its space, give the unused space to the right fragment
389
+ r_frag = clipstr_start(s, n//2+(l_space-l_w))[0]
390
+ res = l_frag + truncator + r_frag
391
+ return res, dispwidth(res)
392
+ return s, dispwidth(s)
350
393
 
351
394
  vd.addGlobals(clipstr=clipstr,
352
395
  clipdraw=clipdraw,
@@ -355,4 +398,6 @@ vd.addGlobals(clipstr=clipstr,
355
398
  dispwidth=dispwidth,
356
399
  iterchars=iterchars,
357
400
  iterchunks=iterchunks,
358
- wraptext=wraptext)
401
+ wraptext=wraptext,
402
+ clipstr_start=clipstr_start,
403
+ clipstr_middle=clipstr_middle)
visidata/cmdlog.py CHANGED
@@ -330,6 +330,9 @@ def replay_sync(vd, cmdlog):
330
330
  with vd.DisableAsync():
331
331
  vd.sync() #2352 let cmdlog finish loading
332
332
  cmdlog.cursorRowIndex = 0
333
+ # save current replay, for cmdlogs that replay other cmdlogs, such as a macro executing another macro
334
+ prev_replay = vd.currentReplay
335
+ prev_replay_row = vd.currentReplayRow
333
336
  vd.currentReplay = cmdlog
334
337
 
335
338
  with Progress(total=len(cmdlog.rows)) as prog:
@@ -356,7 +359,8 @@ def replay_sync(vd, cmdlog):
356
359
  vd.activeSheet.ensureLoaded()
357
360
 
358
361
  vd.status('replay complete')
359
- vd.currentReplay = None
362
+ vd.currentReplay = prev_replay
363
+ vd.currentReplayRow = prev_replay_row
360
364
 
361
365
 
362
366
  @VisiData.api
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,12 +236,16 @@ 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
 
250
245
  if self.type is anytype:
251
246
  if isinstance(typedval, (dict, list, tuple)):
247
+ if width is None:
248
+ return ''.join(iterchars(typedval))
252
249
  dispval, dispw = clipstr(iterchars(typedval), width)
253
250
  return dispval
254
251
 
@@ -264,7 +261,9 @@ class Column(Extensible):
264
261
 
265
262
  The 'generic' displayer does not do any formatting.
266
263
  '''
267
- if width is not None and width > 1 and vd.isNumeric(self):
264
+ if width is not None and width > 1 and \
265
+ vd.isNumeric(self) and \
266
+ isinstance(dw.typedval, (int, float)):
268
267
  yield ('', dw.text.rjust(width-2))
269
268
  else:
270
269
  yield ('', dw.text)
@@ -355,7 +354,8 @@ class Column(Extensible):
355
354
  return ret
356
355
 
357
356
  def getCell(self, row):
358
- '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.'''
359
359
  cellval = wrapply(self.getValue, row)
360
360
  typedval = wrapply(self.type, cellval)
361
361
 
@@ -394,7 +394,7 @@ class Column(Extensible):
394
394
  dw.typedval = typedval
395
395
 
396
396
  try:
397
- dw.text = self.format(typedval, width=(self.width or 0)*2) or ''
397
+ dw.text = self.format(typedval, width=self.width) or ''
398
398
 
399
399
  # annotate cells with raw value type in anytype columns, except for strings
400
400
  if self.type is anytype and type(cellval) is not str:
@@ -417,7 +417,8 @@ class Column(Extensible):
417
417
  return dw
418
418
 
419
419
  def getDisplayValue(self, row):
420
- '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.'''
421
422
  return self.getCell(row).text
422
423
 
423
424
  def putValue(self, row, val):
@@ -457,22 +458,36 @@ class Column(Extensible):
457
458
  return vd.status('set %d cells to %d values' % (len(rows), len(values)))
458
459
 
459
460
  def getMaxWidth(self, rows):
460
- 'Return the maximum length of any cell in column or its header (up to window width).'
461
- 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
462
463
  nlen = dispwidth(self.name)
463
- if len(rows) > 0:
464
- w_max = 0
465
- for r in rows:
466
- row_w = dispwidth(self.getDisplayValue(r), maxwidth=self.sheet.windowWidth)
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
- w = w_max
472
- w = max(w, nlen)+2
473
- w = min(w, self.sheet.windowWidth)
474
- return w
475
-
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)
476
491
 
477
492
 
478
493
  # ---- basic Columns
visidata/ddwplay.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from collections import defaultdict
2
2
  import json
3
3
  import time
4
- from visidata import colors, vd, clipdraw, ColorAttr
4
+ from visidata import colors, vd, clipdraw, ColorAttr, dispwidth
5
5
 
6
6
  __all__ = ['Animation', 'AnimationMgr']
7
7
 
@@ -77,7 +77,7 @@ class Animation:
77
77
  self.total_ms = sum(f.duration_ms or 0 for f in self.frames.values())
78
78
  for f in self.frames.values():
79
79
  for r, x, y, _ in self.iterdeep(f.rows):
80
- self.width = max(self.width, x+len(r.text))
80
+ self.width = max(self.width, x+dispwidth(r.text))
81
81
  self.height = max(self.height, y)
82
82
 
83
83
  def draw(self, scr, *, t=0, x=0, y=0, loop=False, attr=ColorAttr(), **kwargs):