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
@@ -2,6 +2,7 @@ import pathlib
2
2
  import tarfile
3
3
  import zipfile
4
4
  import datetime
5
+ import os.path
5
6
  from visidata.loaders import unzip_http
6
7
 
7
8
  from visidata import vd, VisiData, asyncthread, Sheet, Progress, Menu, options
@@ -84,13 +85,13 @@ Commands:
84
85
  return vd.openSource(Path(fi.filename, fp=fp, filesize=fi.file_size), filetype=options.filetype)
85
86
 
86
87
  def extract(self, *rows, path=None):
87
- path = path or pathlib.Path('.')
88
+ path = path or Path('.')
88
89
 
89
90
  files = []
90
91
  for row in rows:
91
92
  r, _ = row
92
93
  vd.confirmOverwrite(path/r.filename) #1452
93
- self.extract_async(row)
94
+ self.extract_async(row, path=path)
94
95
 
95
96
  def sysopen_row(self, row):
96
97
  'Extract file in row to tempdir and launch $EDITOR. Modifications will be discarded.'
@@ -112,6 +113,12 @@ Commands:
112
113
  if '://' in str(self.source):
113
114
  unzip_http.warning = vd.warning
114
115
  self._zfp = unzip_http.RemoteZipFile(str(self.source))
116
+ elif isinstance(self.source, Path):
117
+ if self.source.has_fp(): #when opening a zip inside tar or zip
118
+ fp = self.source.open('rb')
119
+ else:
120
+ fp = self.source
121
+ self._zfp = zipfile.ZipFile(fp, 'r')
115
122
  else:
116
123
  self._zfp = zipfile.ZipFile(str(self.source), 'r')
117
124
 
@@ -122,18 +129,35 @@ Commands:
122
129
  yield [zi, Path(zi.filename)]
123
130
 
124
131
 
132
+ #from https://docs.python.org/3/library/tarfile.html#tarfile.REGTYPE
133
+ tarfile_type_names = {
134
+ tarfile.REGTYPE:"file",
135
+ tarfile.AREGTYPE:"file",
136
+ tarfile.LNKTYPE:"hard link",
137
+ tarfile.SYMTYPE:"symbolic link",
138
+ tarfile.CHRTYPE:"character device",
139
+ tarfile.BLKTYPE:"block device",
140
+ tarfile.DIRTYPE:"directory",
141
+ tarfile.FIFOTYPE:"FIFO",
142
+ tarfile.CONTTYPE:"contiguous file",
143
+ tarfile.GNUTYPE_LONGNAME:"GNU tar longname",
144
+ tarfile.GNUTYPE_LONGLINK:"GNU tar longlink",
145
+ tarfile.GNUTYPE_SPARSE:"GNU tar sparse file",
146
+ }
125
147
  class TarSheet(Sheet):
126
148
  'Wrapper for `tarfile` library.'
127
149
  rowtype = 'files' # rowdef TarInfo
128
150
  columns = [
129
151
  ColumnAttr('name'),
152
+ Column('ext', getter=lambda col,row: row.isdir() and '/' or os.path.splitext(row.name)[1][1:]),
130
153
  ColumnAttr('size', type=int),
131
154
  ColumnAttr('mtime', type=date),
132
- ColumnAttr('type', type=int),
155
+ Column('type', getter=lambda col, row: tarfile_type_names.get(row.type, 'unknown')),
133
156
  ColumnAttr('mode', type=int),
134
157
  ColumnAttr('uname'),
135
158
  ColumnAttr('gname')
136
159
  ]
160
+ nKeys=1
137
161
 
138
162
  def openRow(self, fi):
139
163
  tfp = tarfile.open(name=str(self.source))
visidata/loaders/csv.py CHANGED
@@ -12,6 +12,13 @@ vd.option('csv_lineterminator', '\r\n', 'lineterminator passed to csv.writer', r
12
12
  vd.option('safety_first', False, 'sanitize input/output to handle edge cases, with a performance cost', replay=True)
13
13
 
14
14
 
15
+ @VisiData.api
16
+ def guess_csv_delimiter(vd, p):
17
+ 'If csv_delimiter option has been modified from default, assume CSV format.'
18
+
19
+ if vd.options.csv_delimiter != vd.options.getdefault('csv_delimiter'):
20
+ return dict(filetype='csv', _likelihood=2)
21
+
15
22
  @VisiData.api
16
23
  def guess_csv(vd, p):
17
24
  import csv
visidata/macros.py CHANGED
@@ -40,7 +40,7 @@ class MacroSheet(IndexSheet):
40
40
  def putChanges(self):
41
41
  self.commitDeletes() #1569 apply deletes early for saveSheets below
42
42
 
43
- vd.saveSheets(self.source, self, confirm_overwrite=False)
43
+ vd.sync(vd.saveSheets(self.source, self, confirm_overwrite=False))
44
44
  self._deferredDels.clear()
45
45
  self.reload()
46
46
 
visidata/main.py CHANGED
@@ -2,7 +2,7 @@
2
2
  # Usage: $0 [<options>] [<input> ...]
3
3
  # $0 [<options>] --play <cmdlog> [--batch] [-w <waitsecs>] [-o <output>] [field=value ...]
4
4
 
5
- __version__ = '3.2'
5
+ __version__ = '3.3'
6
6
  __version_info__ = 'saul.pw/VisiData v' + __version__
7
7
 
8
8
  from copy import copy
@@ -255,7 +255,11 @@ def main_vd():
255
255
  optval = sys.argv[i+1]
256
256
  i += 1
257
257
 
258
- current_args[optname] = optval
258
+ # batch and interactive are only meaningful when applied globally,
259
+ # so exclude them from sheet-specific options. Those would
260
+ # override any later change to vd.options.batch in global settings.
261
+ if optname not in ('batch', 'interactive'):
262
+ current_args[optname] = optval
259
263
  if flGlobal:
260
264
  global_args[optname] = optval
261
265
  elif arg.startswith('+'): # position cursor at start
@@ -285,7 +289,7 @@ def main_vd():
285
289
  # fetch motd *after* options parsing/setting
286
290
  vd.domotd()
287
291
 
288
- if args.batch:
292
+ if options.batch:
289
293
  if not vd.options.interactive:
290
294
  options.undo = False
291
295
  options.quitguard = False
@@ -316,7 +320,7 @@ def main_vd():
316
320
  for vs in reversed(sources):
317
321
  vd.push(vs, load=False) #1471, 1555
318
322
 
319
- if not vd.sheets and not args.play and not args.batch:
323
+ if not vd.sheets and not args.play and not options.batch:
320
324
  if 'filetype' in current_args:
321
325
  newfunc = getattr(vd, 'new_' + current_args['filetype'], vd.getGlobals().get('new_' + current_args['filetype']))
322
326
  datestr = datetime.date.today().strftime('%Y-%m-%d')
@@ -333,14 +337,14 @@ def main_vd():
333
337
  vd.cmdlog.openHook(vd.currentDirSheet, vd.currentDirSheet.source)
334
338
 
335
339
  if not args.play:
336
- if args.batch:
340
+ if options.batch:
337
341
  if sources:
338
342
  vd.push(sources[0])
339
343
 
340
344
  for (f, *parms) in after_config:
341
345
  f(sources, *parms)
342
346
 
343
- if not args.batch:
347
+ if not options.batch:
344
348
  run(vd.sheets[0])
345
349
  else:
346
350
  if args.play == '-':
@@ -351,7 +355,7 @@ def main_vd():
351
355
  vdfile = Path(args.play)
352
356
 
353
357
  vs = eval_vd(vdfile, *fmtargs, **fmtkwargs)
354
- if args.batch:
358
+ if options.batch:
355
359
  if not args.debug:
356
360
  vd.outputProgressThread = visidata.VisiData.execAsync(vd, vd.outputProgressEvery, vs, seconds=0.5, sheet=BaseSheet()) #1182
357
361
  vd.reloadMacros()
visidata/mainloop.py CHANGED
@@ -167,7 +167,7 @@ def mainloop(vd, scr):
167
167
  numTimeouts = 0
168
168
  prefixWaiting = False
169
169
  vd.scrFull = scr
170
- if vd.options.disp_expert >= 5:
170
+ if not vd.wantsHelp('help'):
171
171
  vd.disp_help = -1
172
172
 
173
173
  vd.keystrokes = ''
visidata/man/vd.1 CHANGED
@@ -1029,7 +1029,7 @@ source of randomized startup messages
1029
1029
  folder recursion depth on DirSheet
1030
1030
  .It Sy --dir-hidden No " False"
1031
1031
  load hidden files on DirSheet
1032
- .It Sy --config Ns = Ns Ar "Path " No "/home/saul/.visidatarc"
1032
+ .It Sy --config Ns = Ns Ar "Path " No "/home/anja/.visidatarc"
1033
1033
  config file to exec in Python
1034
1034
  .It Sy --play Ns = Ns Ar "str " No ""
1035
1035
  file.vdj to replay
@@ -1063,7 +1063,7 @@ device ID associated with matrix login
1063
1063
  client_id for reddit api
1064
1064
  .It Sy --reddit-client-secret Ns = Ns Ar "str " No ""
1065
1065
  client_secret for reddit api
1066
- .It Sy --reddit-user-agent Ns = Ns Ar "str " No "3.2"
1066
+ .It Sy --reddit-user-agent Ns = Ns Ar "str " No "3.3"
1067
1067
  user_agent for reddit api
1068
1068
  .It Sy --zulip-batch-size Ns = Ns Ar "int " No "-100"
1069
1069
  number of messages to fetch per call (<0 to fetch before anchor)
@@ -1397,8 +1397,17 @@ charset to render vertical reference lines on graph
1397
1397
  charset to render horizontal reference lines on graph
1398
1398
  .It Sy "disp_graph_multiple_reflines_char" No "\[u2592]"
1399
1399
  char to render multiple parallel reflines
1400
- .It Sy "disp_expert " No "0"
1401
- max level of options and columns to include
1400
+ .It Sy "disp_help_flags " No "cmdpalette guides help hints inputfield inputkeys nometacols sidebar"
1401
+ list of helper features to enable (space-separated):
1402
+ - "cmdpalette": exec-longname suggestions
1403
+ - "guides": guides in sidebar
1404
+ - "help": help sidebar collapsed by default
1405
+ - "hints": context-sensitive hints on menu line
1406
+ - "inputfield": context-sensitive help for each input field
1407
+ - "inputkeys": input quick reference in sidebar
1408
+ - "nometacols": hide expert columns on metasheets
1409
+ - "sidebar": context-sensitive sheet help in sidebar
1410
+ - "all": enable all helper features
1402
1411
  .It Sy "color_add_pending " No "green"
1403
1412
  color for rows pending add
1404
1413
  .It Sy "color_change_pending" No "reverse yellow"
visidata/man/vd.txt CHANGED
@@ -709,7 +709,7 @@ COMMANDLINE OPTIONS
709
709
  DirSheet
710
710
  --dir-hidden False load hidden files on
711
711
  DirSheet
712
- --config=Path /home/saul/.visidatarc
712
+ --config=Path /home/anja/.visidatarc
713
713
  config file to exec in
714
714
  Python
715
715
  --play=str file.vdj to replay
@@ -746,7 +746,7 @@ COMMANDLINE OPTIONS
746
746
  --reddit-client-id=str client_id for reddit api
747
747
  --reddit-client-secret=str client_secret for reddit
748
748
  api
749
- --reddit-user-agent=str 3.2 user_agent for reddit api
749
+ --reddit-user-agent=str 3.3 user_agent for reddit api
750
750
  --zulip-batch-size=int -100 number of messages to
751
751
  fetch per call (<0 to
752
752
  fetch before anchor)
@@ -1061,8 +1061,27 @@ COMMANDLINE OPTIONS
1061
1061
  erence lines on graph
1062
1062
  disp_graph_multiple_reflines_char ▒ char to render multiple parallel
1063
1063
  reflines
1064
- disp_expert 0 max level of options and columns
1065
- to include
1064
+ disp_help_flags cmdpalette guides help hints inputfield inputkeys
1065
+ nometacols sidebar
1066
+ list of helper features to enable
1067
+ (space-separated):
1068
+ - "cmdpalette": exec-longname
1069
+ suggestions
1070
+ - "guides": guides in sidebar
1071
+ - "help": help sidebar col‐
1072
+ lapsed by default
1073
+ - "hints": context-sensitive
1074
+ hints on menu line
1075
+ - "inputfield": context-sen‐
1076
+ sitive help for each input field
1077
+ - "inputkeys": input quick
1078
+ reference in sidebar
1079
+ - "nometacols": hide expert
1080
+ columns on metasheets
1081
+ - "sidebar": context-sensi‐
1082
+ tive sheet help in sidebar
1083
+ - "all": enable all helper
1084
+ features
1066
1085
  color_add_pending green color for rows pending add
1067
1086
  color_change_pending reverse yellow color for cells pending modifica‐
1068
1087
  tion
visidata/menu.py CHANGED
@@ -25,7 +25,7 @@ vd.menuRunning = False
25
25
 
26
26
  @VisiData.property
27
27
  def hintStatus(vd):
28
- if vd.options.disp_expert <= 0:
28
+ if vd.wantsHelp('hints'):
29
29
  if int(time.time()/60) % 2 == 0:
30
30
  return 'Alt+H for help menu'
31
31
  else:
visidata/mouse.py CHANGED
@@ -13,6 +13,8 @@ BaseSheet.init('mouseY', int)
13
13
 
14
14
  @VisiData.after
15
15
  def initCurses(vd):
16
+ if not getattr(curses, 'mousemask', None):
17
+ return
16
18
  curses.MOUSE_ALL = 0xffffffff
17
19
  curses.mousemask(curses.MOUSE_ALL if vd.options.mouse_interval else 0)
18
20
  curses.def_prog_mode()
visidata/movement.py CHANGED
@@ -84,9 +84,12 @@ def moveToNextRow(vs, func, reverse=False, msg='no different value up this colum
84
84
  def visibleWidth(self):
85
85
  'Width of column as is displayed in terminal'
86
86
  vcolidx = self.sheet.visibleCols.index(self)
87
- if vcolidx not in self.sheet._visibleColLayout:
88
- self.sheet.calcSingleColLayout(vcolidx)
89
- return self.sheet._visibleColLayout[vcolidx][1]
87
+ if vcolidx in self.sheet._visibleColLayout:
88
+ w = self.sheet._visibleColLayout[vcolidx][1]
89
+ else: #this case should never happen in normal use
90
+ #the width can be inaccurate if the column is not at x=0
91
+ w = self.sheet.calcSingleColLayout(vcolidx)
92
+ return w
90
93
 
91
94
 
92
95
  Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left', replay=False)
visidata/save.py CHANGED
@@ -85,6 +85,8 @@ def getDefaultSaveName(sheet):
85
85
  if hasattr(src, 'scheme') and src.scheme:
86
86
  return src.name + src.suffix
87
87
  if isinstance(src, Path):
88
+ if src.given == '-':
89
+ return f'stdin.{sheet.options.save_filetype}'
88
90
  if sheet.options.is_set('save_filetype', sheet):
89
91
  # if save_filetype is over-ridden from default, use it as the extension
90
92
  return str(src.with_suffix('')) + '.' + sheet.options.save_filetype
@@ -109,7 +111,9 @@ def saveCols(vd, cols):
109
111
 
110
112
  @VisiData.api
111
113
  def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):
112
- 'Save all *vsheets* to *givenpath*.'
114
+ '''Save all *vsheets* to *givenpath*. Async.
115
+ Callers should be careful not to call reload() while saveSheets is still running.
116
+ Use vd.sync(saveSheets) to wait for the save to finish.'''
113
117
 
114
118
  if not vsheets: # blank tuple
115
119
  vd.warning('no sheets to save')
visidata/settings.py CHANGED
@@ -447,12 +447,16 @@ def loadConfigAndPlugins(vd, args=AttrDict()):
447
447
  args_plugins_autoload = args.plugins_autoload if 'plugins_autoload' in args else True
448
448
  if not args.nothing and args_plugins_autoload and vd.options.plugins_autoload:
449
449
  from importlib.metadata import entry_points
450
+ eps_visidata = []
450
451
  try:
451
452
  eps = entry_points()
452
- eps_visidata = eps.select(group='visidata.plugins') if 'visidata.plugins' in eps.groups else []
453
+ vp = 'visidata.plugins'
454
+ if hasattr(eps, 'groups'): #Python >= 3.10
455
+ eps_visidata = eps.select(group=vp)
456
+ else: #Python < 3.10
457
+ eps_visidata = eps.get(vp, [])
453
458
  except Exception as e:
454
- eps_visidata = []
455
- vd.warning('plugin autoload failed; see issue #1529')
459
+ vd.warning(f'plugin autoload failed; see issue #1529: {e}')
456
460
 
457
461
  for ep in eps_visidata:
458
462
  try:
visidata/sheets.py CHANGED
@@ -8,6 +8,7 @@ from visidata import (options, Column, namedlist, SettableColumn, AttrDict, Disp
8
8
  TypedExceptionWrapper, BaseSheet, UNLOADED, wrapply,
9
9
  clipdraw, clipdraw_chunks, ColorAttr, update_attr, colors, undoAttrFunc, vlen, dispwidth)
10
10
  import visidata
11
+ from visidata.utils import colname_letters
11
12
 
12
13
 
13
14
  vd.activePane = 1 # pane numbering starts at 1; pane 0 means active pane
@@ -190,6 +191,7 @@ class TableSheet(BaseSheet):
190
191
 
191
192
  # list of all columns in display order
192
193
  self.initialCols = kwargs.pop('columns', None) or type(self).columns
194
+ self.colname_ctr = 0
193
195
  self.resetCols()
194
196
 
195
197
  self._ordering = list(type(self)._ordering) #2254
@@ -290,11 +292,11 @@ class TableSheet(BaseSheet):
290
292
  pass
291
293
 
292
294
  def resetCols(self):
293
- 'Reset columns to class settings'
295
+ 'Reset columns to class settings or constructor settings'
294
296
  self.columns = []
295
297
  for c in self.initialCols:
296
298
  self.addColumn(deepcopy(c))
297
- if self.options.disp_expert < c.disp_expert:
299
+ if c.disp_expert and vd.wantsHelp('nometacols'):
298
300
  c.hide()
299
301
 
300
302
  self.setKeys(self.columns[:self.nKeys])
@@ -560,7 +562,6 @@ class TableSheet(BaseSheet):
560
562
  def cursorRight(self, n=1):
561
563
  'Move cursor right `n` visible columns (or left if `n` is negative).'
562
564
  self.cursorVisibleColIndex += n
563
- self.calcColLayout()
564
565
 
565
566
  def addColumn(self, *cols, index=None):
566
567
  '''Insert all *cols* into columns at *index*, or append to end of columns if *index* is None.
@@ -656,30 +657,44 @@ class TableSheet(BaseSheet):
656
657
  elif self.topRowIndex > self.nRows-1:
657
658
  self.topRowIndex = self.nRows-1
658
659
 
660
+ self.adjustColLayout()
661
+
662
+ # calculations that rely on nScreenRows, like bottomRowIndex, need to be done after
663
+ # col layout has been adjusted. nScreenRows requires an accurate count of
664
+ # allAggregators, which requires knowing col visibility.
659
665
  # check bounds, scroll if necessary
660
666
  if self.topRowIndex > self.cursorRowIndex:
661
667
  self.topRowIndex = self.cursorRowIndex
662
668
  elif self.bottomRowIndex < self.cursorRowIndex:
663
669
  self.bottomRowIndex = self.cursorRowIndex
664
670
 
665
- if self.cursorCol and self.cursorCol.keycol:
666
- return
667
-
668
- if self.leftVisibleColIndex >= self.cursorVisibleColIndex:
671
+ def adjustColLayout(self):
672
+ '''Move the left visible column to try to keep the cursorCol visible.
673
+ though the cursorCol cannot be visible when screen is totally filled by keycols.
674
+ Run calcColLayout() at least once.'''
675
+ # jumping to a column left of the previously on-screen columns: put cursorCol as leftmost col
676
+ # jumping to a column right of the previously on-screen columns: put cursorCol as far right as possible
677
+ if self.leftVisibleColIndex > self.cursorVisibleColIndex: # e.g. when jumping/moving left
669
678
  self.leftVisibleColIndex = self.cursorVisibleColIndex
670
- else:
671
- while True:
672
- if self.leftVisibleColIndex == self.cursorVisibleColIndex: # not much more we can do
673
- break
679
+ elif self.leftVisibleColIndex < self.cursorVisibleColIndex: # e.g. when jumping/moving right
680
+ #move leftVisibleCol until the cursor column fits fully on screen
681
+ while self.leftVisibleColIndex < self.cursorVisibleColIndex: #ensures termination even if screen is completely filled by keycols
674
682
  self.calcColLayout()
675
683
  if not self._visibleColLayout:
676
684
  break
685
+
686
+ # If the cursor is outside the visible columns currently laid out (1 window wide).
687
+ # One way to trigger this is with zc, jump to a column never seen yet.
677
688
  mincolidx, maxcolidx = min(self._visibleColLayout.keys()), max(self._visibleColLayout.keys())
678
689
  if self.cursorVisibleColIndex < mincolidx:
679
- self.leftVisibleColIndex -= max((self.cursorVisibleColIndex - mincolidx)//2, 1)
680
- continue
690
+ # This case is expected never to occur. _visibleColLayout keys are enumerated from 0,
691
+ # so mincolidx is always 0. and cursorVisibleColIndex is kept >= 0 (by checkCursor).
692
+ self.leftVisibleColIndex = self.cursorVisibleColIndex
693
+ break
681
694
  elif self.cursorVisibleColIndex > maxcolidx:
682
- self.leftVisibleColIndex += max((maxcolidx - self.cursorVisibleColIndex)//2, 1)
695
+ # some cases: 1) jumping rightward, so cursor has just moved to a column that is offscreen to the right
696
+ # 2) when keycols fill entire screen
697
+ self.leftVisibleColIndex += 1
683
698
  continue
684
699
 
685
700
  cur_x, cur_w = self._visibleColLayout[self.cursorVisibleColIndex]
@@ -687,9 +702,12 @@ class TableSheet(BaseSheet):
687
702
  break
688
703
  self.leftVisibleColIndex += 1 # once within the bounds, walk over one column at a time
689
704
 
705
+ if self.leftVisibleColIndex == self.cursorVisibleColIndex: #will happen after cursor: jumped left, jumped right, or stayed in place
706
+ self.calcColLayout()
707
+
690
708
  def calcColLayout(self):
691
- 'Set right-most visible column, based on calculation.'
692
- vd.clearCaches()
709
+ '''Set right-most visible column, based on calculation.
710
+ Assign x coordinates and width to every column that fits on screen, visible or hidden.'''
693
711
  minColWidth = dispwidth(self.options.disp_more_left)+dispwidth(self.options.disp_more_right)+2
694
712
  sepColWidth = dispwidth(self.options.disp_column_sep)
695
713
  winWidth = self.windowWidth
@@ -698,18 +716,28 @@ class TableSheet(BaseSheet):
698
716
  vcolidx = 0
699
717
  for vcolidx, col in enumerate(self.availCols):
700
718
  width = self.calcSingleColLayout(col, vcolidx, x, minColWidth)
701
- if width:
702
- x += width+sepColWidth
703
- if x > winWidth-1:
719
+ if width is not None:
720
+ if x < winWidth-1:
721
+ self._visibleColLayout[vcolidx] = [x, width]
722
+ x += width+sepColWidth
723
+ if x >= winWidth-1:
704
724
  break
705
725
 
706
726
  self.rightVisibleColIndex = vcolidx
707
727
 
708
728
  def calcSingleColLayout(self, col:Column, vcolidx:int, x:int=0, minColWidth:int=4):
709
- if col.width is None and len(self.visibleRows) > 0:
710
- vrows = self.visibleRows if self.nRows > 1000 else self.rows[:1000] #1964
729
+ '''Return the width, for key columns, or for columns that are rightward of
730
+ the leftmost visibleCol, even if they are offscreen or hidden. Return
731
+ None for columns left of cursorVisibleColIndex, if they are not key columns.'''
732
+ # We use a slice of rows that is similar to self.visibleRows but simpler,
733
+ # and larger. The goal is to avoid using nFooterRows. Because nFooterRows
734
+ # cannot in general be calculated properly until after calcColLayout() has
735
+ # determined which columns are visible.
736
+ vrows = self.rows[self.topRowIndex:self.topRowIndex+self.windowHeight]
737
+ if col.width is None and len(vrows) > 0:
738
+ measure_rows = vrows if self.nRows > 1000 else self.rows[:1000] #1964
711
739
  # handle delayed column width-finding
712
- col.width = max(col.getMaxWidth(vrows), minColWidth)
740
+ col.width = max(col.getMaxWidth(measure_rows), minColWidth)
713
741
  if vcolidx < self.nVisibleCols-1: # let last column fill up the max width
714
742
  col.width = min(col.width, self.options.default_width)
715
743
 
@@ -719,10 +747,10 @@ class TableSheet(BaseSheet):
719
747
  if vcolidx >= self.nVisibleCols and vcolidx == self.cursorVisibleColIndex:
720
748
  width = self.options.default_width
721
749
 
750
+ #subtract 1 character of empty space from windowWidth, for the margin to the right of the sheet
751
+ width = min(width, self.windowWidth-x-1)
722
752
  width = max(width, 1)
723
753
  if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns
724
- #subtract 1 character of empty space from windowWidth, for the margin to the right of the sheet
725
- self._visibleColLayout[vcolidx] = [x, max(min(width, self.windowWidth-x-1), 1)]
726
754
  return width
727
755
 
728
756
 
@@ -798,8 +826,6 @@ class TableSheet(BaseSheet):
798
826
  'Return dict of aggname -> list of cols with that aggregator.'
799
827
  allaggs = collections.defaultdict(list) # aggname -> list of cols with that aggregator
800
828
  for vcolidx, (x, colwidth) in sorted(self._visibleColLayout.items()):
801
- if vcolidx >= len(self.availCols):
802
- break #2607 #2763
803
829
  col = self.availCols[vcolidx]
804
830
  if not col.hidden:
805
831
  for aggr in col.aggregators:
@@ -1024,6 +1050,11 @@ class TableSheet(BaseSheet):
1024
1050
 
1025
1051
  return height
1026
1052
 
1053
+ def incremented_colname(self):
1054
+ vd.addUndo(setattr, self, 'colname_ctr', self.colname_ctr)
1055
+ self.colname_ctr += 1
1056
+ return colname_letters(self.colname_ctr)
1057
+
1027
1058
  vd.rowNoters = [
1028
1059
  # f(sheet, row) -> character to be displayed on the left side of row
1029
1060
  ]
@@ -1163,14 +1194,16 @@ def confirmQuit(vs, verb='quit'):
1163
1194
  def preloadHook(sheet):
1164
1195
  'Override to setup for reload().'
1165
1196
  sheet.confirmQuit('reload')
1197
+
1166
1198
  sheet.hasBeenModified = False
1167
- sheet.calcColLayout()
1168
1199
 
1169
1200
 
1170
1201
  @VisiData.api
1171
1202
  def newSheet(vd, name, ncols, **kwargs):
1172
- return Sheet(name, columns=[SettableColumn(width=vd.options.default_width) for i in range(ncols)], **kwargs)
1173
-
1203
+ cols = [SettableColumn(width=vd.options.default_width, name=f'{colname_letters(i+1)}') for i in range(ncols)]
1204
+ vs = Sheet(name, columns=cols, **kwargs)
1205
+ vs.colname_ctr = ncols
1206
+ return vs
1174
1207
 
1175
1208
  @BaseSheet.api
1176
1209
  def quitAndReleaseMemory(vs):
@@ -1208,12 +1241,27 @@ def async_deepcopy(sheet, rowlist):
1208
1241
  _async_deepcopy(ret, rowlist)
1209
1242
  return ret
1210
1243
 
1244
+ @Sheet.api
1245
+ def reload_or_replace(sheet):
1246
+ sheet.preloadHook()
1247
+ if isinstance(sheet.source, visidata.Path) and \
1248
+ sheet.source.is_url() and sheet.source.scheme != 'file': #2825
1249
+ #retrieve data again, because the earlier data saved in sheet.source may be outdated
1250
+ vs = vd.openSource(visidata.Path(sheet.source.given))
1251
+ if type(vs) != type(sheet): #new data may have a different filetype
1252
+ vd.push(vs)
1253
+ vd.remove(sheet)
1254
+ #user needs feedback that sheet changed, since the new sheet has a different shortcut
1255
+ vd.status('replaced sheet due to changed filetype')
1256
+ return
1257
+ sheet.source = vs.source
1258
+ sheet.reload()
1211
1259
 
1212
1260
 
1213
1261
  BaseSheet.init('pane', lambda: 1)
1214
1262
 
1215
1263
 
1216
- BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload()', 'Reload current sheet')
1264
+ BaseSheet.addCommand('^R', 'reload-sheet', 'reload_or_replace()', 'Reload current sheet')
1217
1265
  Sheet.addCommand('', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1218
1266
 
1219
1267
  Sheet.addCommand('!', 'key-col', 'exec_longname("key-col-off") if cursorCol.keycol else exec_longname("key-col-on")', 'toggle current column as a key column', replay=False)
@@ -1245,7 +1293,7 @@ BaseSheet.addCommand('^I', 'splitwin-swap', 'vd.activePane = 1 if sheet.pane ==
1245
1293
  BaseSheet.addCommand('g^I', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen')
1246
1294
  BaseSheet.addCommand('zZ', 'splitwin-input', 'vd.options.disp_splitwin_pct = input("% height for split window: ", value=vd.options.disp_splitwin_pct)', 'set split pane to specific size')
1247
1295
 
1248
- BaseSheet.addCommand('^L', 'redraw', 'sheet.refresh(); vd.redraw()', 'Refresh screen')
1296
+ BaseSheet.addCommand('^L', 'redraw', 'sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen')
1249
1297
  BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit')
1250
1298
  BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit')
1251
1299
  BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet')
visidata/sidebar.py CHANGED
@@ -15,11 +15,12 @@ vd.theme_option('color_sidebar_title', 'black on yellow', 'color of sidebar titl
15
15
  @VisiData.api
16
16
  class AddedHelp:
17
17
  '''Context manager to add help text/screen to list of available sidebars.'''
18
- def __init__(self, text:Union[str,'HelpPane'], title=''):
18
+ def __init__(self, text:Union[str,'HelpPane'], title='', help_flag=''):
19
+ self.helpfunc = None
19
20
  if text:
21
+ if not vd.wantsHelp(help_flag):
22
+ return
20
23
  self.helpfunc = lambda: (text, title)
21
- else:
22
- self.helpfunc = None
23
24
 
24
25
  def __enter__(self):
25
26
  if self.helpfunc:
@@ -33,7 +34,7 @@ class AddedHelp:
33
34
  vd.clearCaches()
34
35
 
35
36
 
36
- @BaseSheet.property
37
+ @BaseSheet.lazy_property
37
38
  def formatter_helpstr(sheet):
38
39
  return AttrDict(commands=CommandHelpGetter(type(sheet)),
39
40
  options=OptionHelpGetter())
@@ -73,7 +74,7 @@ def help_sidebars(sheet) -> 'list[Callable[[], tuple[str,str]]]':
73
74
  @VisiData.cached_property
74
75
  def sidebarStatus(vd) -> str:
75
76
  if vd.sheet.help_sidebars:
76
- if vd.options.disp_sidebar and vd.disp_help >= 0:
77
+ if vd.wantsHelp('sidebar') and vd.disp_help >= 0:
77
78
  n = vd.disp_help+1
78
79
  return f'[:onclick sidebar-toggle][:sidebar][{n}/{len(vd.sheet.help_sidebars)}][/]'
79
80
  else:
@@ -111,7 +112,7 @@ def drawSidebar(vd, scr, sheet):
111
112
  bottommsg = ''
112
113
  overflowmsg = '[:reverse] Ctrl+P to view all status messages [/]'
113
114
  try:
114
- if not sidebar and vd.options.disp_sidebar and vd.disp_help >= 0:
115
+ if not sidebar and vd.options.disp_sidebar and vd.wantsHelp('guides') and sheet.help_sidebars:
115
116
  sidebar, title = sheet.help_sidebars[vd.disp_help%len(sheet.help_sidebars)]()
116
117
 
117
118
  # bottommsg = sheet.formatString('[:onclick sidebar-toggle][:reverse] {help.commands.sidebar_toggle} [:]', help=sheet.formatter_helpstr)
@@ -103,6 +103,19 @@ class TestClipText:
103
103
  assert clips == clippeds
104
104
  assert clipw == clippedw
105
105
 
106
+ @pytest.mark.parametrize('s, w, truncator, clippeds, clippedw', [
107
+ ('first\nsecond\n\nthird\n\n\n', 22, '', 'first·second··third···', 22),
108
+ ('first\nsecond\n\nthird\n\n\n', 22, '…', 'first·second··third···', 22),
109
+ ('first\nsecond\n\nthird\n\n\n', 21, '', 'first·second··third··', 21),
110
+ ('first\nsecond\n\nthird\n\n\n', 21, '…', 'first·second··third·…', 21),
111
+ (''.join([chr(i) for i in range(256)]), 256, '',
112
+ '································ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~··································¡¢£¤¥¦§¨©ª«¬\xad®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ', 256),
113
+ ])
114
+ def test_clipstr_unprintable(self, s, w, truncator, clippeds, clippedw):
115
+ clips, clipw = visidata.clipstr(s, w, truncator=truncator, oddspace='·')
116
+ assert clips == clippeds
117
+ assert clipw == clippedw
118
+
106
119
  @pytest.mark.parametrize('s, w, clippeds, clippedw', [
107
120
  ('b to', 4, 'b to', 4),
108
121
  ('abcde', 8, 'abcde', 5),
@@ -30,6 +30,7 @@ nonTested = (
30
30
  'breakpoint',
31
31
  'redraw',
32
32
  'menu',
33
+ 'sysedit',
33
34
  'sysopen',
34
35
  'open-memusage',
35
36
  )