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/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
@@ -22,6 +23,7 @@ vd.option('disp_wrap_break_long_words', False, 'break words longer than column w
22
23
  vd.option('disp_wrap_replace_whitespace', False, 'replace whitespace with spaces in multiline')
23
24
  vd.option('disp_wrap_placeholder', '…', 'multiline string to indicate truncation')
24
25
  vd.option('disp_multiline_focus', True, 'only multiline cursor row')
26
+ vd.option('color_multiline_bottom', '', 'color of bottom line of multiline rows') #2715
25
27
  vd.option('color_aggregator', 'bold 255 white on 234 black', 'color of aggregator summary on bottom row')
26
28
 
27
29
 
@@ -189,6 +191,7 @@ class TableSheet(BaseSheet):
189
191
 
190
192
  # list of all columns in display order
191
193
  self.initialCols = kwargs.pop('columns', None) or type(self).columns
194
+ self.colname_ctr = 0
192
195
  self.resetCols()
193
196
 
194
197
  self._ordering = list(type(self)._ordering) #2254
@@ -289,34 +292,53 @@ class TableSheet(BaseSheet):
289
292
  pass
290
293
 
291
294
  def resetCols(self):
292
- 'Reset columns to class settings'
295
+ 'Reset columns to class settings or constructor settings'
293
296
  self.columns = []
294
297
  for c in self.initialCols:
295
298
  self.addColumn(deepcopy(c))
296
- if self.options.disp_expert < c.disp_expert:
299
+ if c.disp_expert and vd.wantsHelp('nometacols'):
297
300
  c.hide()
298
301
 
299
302
  self.setKeys(self.columns[:self.nKeys])
300
303
 
301
304
  def loader(self):
302
- 'Reset rows and sync load ``source`` via iterload. Overrideable.'
303
- self.rows = []
305
+ 'Reset rows and sync load ``source`` via iterload. Overridable.'
304
306
  try:
305
- with vd.Progress(gerund='loading', total=0):
306
- for i, r in enumerate(self.iterload()):
307
- if self.precious and i > self.options.max_rows:
308
- break
309
- self.addRow(r)
307
+ for r in self._iterloader():
308
+ pass
310
309
  except FileNotFoundError:
311
310
  return # let it be a blank sheet without error
312
311
 
312
+ def _iterloader(self):
313
+ self.rows = []
314
+ with vd.Progress(gerund='loading', total=0):
315
+ max_rows = self.options.max_rows
316
+ for i, r in enumerate(self.iterload()):
317
+ if self.precious and i >= max_rows:
318
+ break
319
+ self.addRow(r)
320
+ yield r
321
+
313
322
  def iterload(self):
314
323
  'Generate rows from ``self.source``. Override in subclass.'
315
324
  if False:
316
325
  yield vd.fail('no iterload for this loader yet')
317
326
 
327
+ def loadStart(self):
328
+ self.loaditer = self._iterloader()
329
+
330
+ def loadSome(self):
331
+ if not self.loaditer:
332
+ return False
333
+ try:
334
+ next(self.loaditer)
335
+ return True
336
+ except StopIteration:
337
+ self.loaditer = None
338
+ return False
339
+
318
340
  def afterLoad(self):
319
- 'hook for after loading has finished. Overrideable (be sure to call super).'
341
+ 'hook for after loading has finished. Overridable (be sure to call super).'
320
342
  # if an ordering has been specified, sort the sheet
321
343
  if self._ordering:
322
344
  vd.sync(self.sort())
@@ -508,6 +530,9 @@ class TableSheet(BaseSheet):
508
530
  'Raw value at current row and column.'
509
531
  return self.cursorCol.getValue(self.cursorRow)
510
532
 
533
+ def getTypedRow(self, rownum):
534
+ return [c.getTypedValue(self.rows[rownum]) for c in self.availCols]
535
+
511
536
  @property
512
537
  def statusLine(self):
513
538
  'Position of cursor and bounds of current sheet.'
@@ -537,7 +562,6 @@ class TableSheet(BaseSheet):
537
562
  def cursorRight(self, n=1):
538
563
  'Move cursor right `n` visible columns (or left if `n` is negative).'
539
564
  self.cursorVisibleColIndex += n
540
- self.calcColLayout()
541
565
 
542
566
  def addColumn(self, *cols, index=None):
543
567
  '''Insert all *cols* into columns at *index*, or append to end of columns if *index* is None.
@@ -576,7 +600,10 @@ class TableSheet(BaseSheet):
576
600
  index = 0
577
601
  ccol = self.cursorCol
578
602
  if ccol and not ccol.keycol:
579
- index = self.columns.index(ccol)+1
603
+ try:
604
+ index = self.columns.index(ccol)+1
605
+ except ValueError: # when all columns are hidden, the one column shown is not in self.columns
606
+ index = 0
580
607
 
581
608
  self.addColumn(*cols, index=index)
582
609
  firstnewcol = [c for c in cols if not c.hidden][0]
@@ -604,13 +631,6 @@ class TableSheet(BaseSheet):
604
631
  for col in cols:
605
632
  col.keycol = 0
606
633
 
607
- def toggleKeys(self, cols):
608
- for col in cols:
609
- if col.keycol:
610
- self.unsetKeys([col])
611
- else:
612
- self.setKeys([col])
613
-
614
634
  def rowkey(self, row):
615
635
  'Return tuple of the key for *row*.'
616
636
  return tuple(c.getTypedValue(row) for c in self.keyCols)
@@ -637,39 +657,57 @@ class TableSheet(BaseSheet):
637
657
  elif self.topRowIndex > self.nRows-1:
638
658
  self.topRowIndex = self.nRows-1
639
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.
640
665
  # check bounds, scroll if necessary
641
666
  if self.topRowIndex > self.cursorRowIndex:
642
667
  self.topRowIndex = self.cursorRowIndex
643
668
  elif self.bottomRowIndex < self.cursorRowIndex:
644
669
  self.bottomRowIndex = self.cursorRowIndex
645
670
 
646
- if self.cursorCol and self.cursorCol.keycol:
647
- return
648
-
649
- 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
650
678
  self.leftVisibleColIndex = self.cursorVisibleColIndex
651
- else:
652
- while True:
653
- if self.leftVisibleColIndex == self.cursorVisibleColIndex: # not much more we can do
654
- 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
655
682
  self.calcColLayout()
656
683
  if not self._visibleColLayout:
657
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.
658
688
  mincolidx, maxcolidx = min(self._visibleColLayout.keys()), max(self._visibleColLayout.keys())
659
689
  if self.cursorVisibleColIndex < mincolidx:
660
- self.leftVisibleColIndex -= max((self.cursorVisibleColIndex - mincolidx)//2, 1)
661
- 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
662
694
  elif self.cursorVisibleColIndex > maxcolidx:
663
- 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
664
698
  continue
665
699
 
666
700
  cur_x, cur_w = self._visibleColLayout[self.cursorVisibleColIndex]
667
- if cur_x+cur_w < self.windowWidth: # current columns fit entirely on screen
701
+ if cur_x+cur_w < self.windowWidth-1: # current columns fit entirely on screen
668
702
  break
669
703
  self.leftVisibleColIndex += 1 # once within the bounds, walk over one column at a time
670
704
 
705
+ if self.leftVisibleColIndex == self.cursorVisibleColIndex: #will happen after cursor: jumped left, jumped right, or stayed in place
706
+ self.calcColLayout()
707
+
671
708
  def calcColLayout(self):
672
- 'Set right-most visible column, based on calculation.'
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.'''
673
711
  minColWidth = dispwidth(self.options.disp_more_left)+dispwidth(self.options.disp_more_right)+2
674
712
  sepColWidth = dispwidth(self.options.disp_column_sep)
675
713
  winWidth = self.windowWidth
@@ -678,18 +716,28 @@ class TableSheet(BaseSheet):
678
716
  vcolidx = 0
679
717
  for vcolidx, col in enumerate(self.availCols):
680
718
  width = self.calcSingleColLayout(col, vcolidx, x, minColWidth)
681
- if width:
682
- x += width+sepColWidth
683
- 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:
684
724
  break
685
725
 
686
726
  self.rightVisibleColIndex = vcolidx
687
727
 
688
728
  def calcSingleColLayout(self, col:Column, vcolidx:int, x:int=0, minColWidth:int=4):
689
- if col.width is None and len(self.visibleRows) > 0:
690
- 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
691
739
  # handle delayed column width-finding
692
- col.width = max(col.getMaxWidth(vrows), minColWidth)
740
+ col.width = max(col.getMaxWidth(measure_rows), minColWidth)
693
741
  if vcolidx < self.nVisibleCols-1: # let last column fill up the max width
694
742
  col.width = min(col.width, self.options.default_width)
695
743
 
@@ -699,9 +747,10 @@ class TableSheet(BaseSheet):
699
747
  if vcolidx >= self.nVisibleCols and vcolidx == self.cursorVisibleColIndex:
700
748
  width = self.options.default_width
701
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)
702
752
  width = max(width, 1)
703
753
  if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns
704
- self._visibleColLayout[vcolidx] = [x, min(width, self.windowWidth-x)]
705
754
  return width
706
755
 
707
756
 
@@ -744,7 +793,7 @@ class TableSheet(BaseSheet):
744
793
  clipdraw(scr, y+i, x, name, hdrcattr, w=colwidth)
745
794
  vd.onMouse(scr, x, y+i, colwidth, 1, BUTTON3_RELEASED='rename-col')
746
795
 
747
- if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowHeight:
796
+ if C and x+colwidth+dispwidth(C) < self.windowWidth and y+i < self.windowHeight:
748
797
  scr.addstr(y+i, x+colwidth, C, sepcattr.attr)
749
798
 
750
799
  clipdraw(scr, y+h-1, min(x+colwidth, self.windowWidth-1)-dispwidth(T), T, hdrcattr)
@@ -935,10 +984,12 @@ class TableSheet(BaseSheet):
935
984
  colseps = [topsep] + [midsep]*(height-2) + [botsep]
936
985
  endseps = [endtopsep] + [endmidsep]*(height-2) + [endbotsep]
937
986
  keyseps = [keytopsep] + [keymidsep]*(height-2) + [keybotsep]
987
+ color_multiline_bottom = colors.get_color('color_multiline_bottom', 2)
938
988
  else:
939
989
  colseps = [colsep]
940
990
  endseps = [endsep]
941
991
  keyseps = [keysep]
992
+ color_multiline_bottom = 0
942
993
 
943
994
  for vcolidx, (col, cellval, lines) in displines.items():
944
995
  if vcolidx not in self._visibleColLayout:
@@ -957,6 +1008,7 @@ class TableSheet(BaseSheet):
957
1008
 
958
1009
  cattr = self._colorize(col, row, cellval)
959
1010
  cattr = update_attr(cattr, basecellcattr)
1011
+ bottomcattr = update_attr(cattr, color_multiline_bottom) if height > 1 else cattr
960
1012
 
961
1013
  note = getattr(cellval, 'note', None)
962
1014
  notewidth = 1 if note else 0
@@ -984,10 +1036,10 @@ class TableSheet(BaseSheet):
984
1036
  for attr, text in chunks:
985
1037
  prechunks.append((attr, text[hoffset:]))
986
1038
 
987
- clipdraw_chunks(scr, y, x, prechunks, cattr, w=colwidth-notewidth)
1039
+ clipdraw_chunks(scr, y, x, prechunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth)
988
1040
  vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell')
989
1041
 
990
- if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth:
1042
+ if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth-1:
991
1043
  scr.addstr(y, x+colwidth, sepchars, sepcattr.attr)
992
1044
 
993
1045
  for notefunc in vd.rowNoters:
@@ -998,6 +1050,11 @@ class TableSheet(BaseSheet):
998
1050
 
999
1051
  return height
1000
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
+
1001
1058
  vd.rowNoters = [
1002
1059
  # f(sheet, row) -> character to be displayed on the left side of row
1003
1060
  ]
@@ -1048,8 +1105,9 @@ class SequenceSheet(Sheet):
1048
1105
 
1049
1106
  self.rows = []
1050
1107
  # add the rest of the rows
1108
+ max_rows = self.options.max_rows
1051
1109
  for i, r in enumerate(vd.Progress(itsource, gerund='loading', total=0)):
1052
- if self.precious and i > self.options.max_rows:
1110
+ if self.precious and i >= max_rows:
1053
1111
  break
1054
1112
  self.addRow(r)
1055
1113
 
@@ -1142,8 +1200,10 @@ def preloadHook(sheet):
1142
1200
 
1143
1201
  @VisiData.api
1144
1202
  def newSheet(vd, name, ncols, **kwargs):
1145
- return Sheet(name, columns=[SettableColumn(width=vd.options.default_width) for i in range(ncols)], **kwargs)
1146
-
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
1147
1207
 
1148
1208
  @BaseSheet.api
1149
1209
  def quitAndReleaseMemory(vs):
@@ -1181,15 +1241,31 @@ def async_deepcopy(sheet, rowlist):
1181
1241
  _async_deepcopy(ret, rowlist)
1182
1242
  return ret
1183
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()
1184
1259
 
1185
1260
 
1186
1261
  BaseSheet.init('pane', lambda: 1)
1187
1262
 
1188
1263
 
1189
- BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload()', 'Reload current sheet')
1264
+ BaseSheet.addCommand('^R', 'reload-sheet', 'reload_or_replace()', 'Reload current sheet')
1190
1265
  Sheet.addCommand('', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1191
1266
 
1192
- Sheet.addCommand('!', 'key-col', 'toggleKeys([cursorCol])', 'toggle current column as a key column')
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)
1268
+ Sheet.addCommand('', 'key-col-on', 'setKeys([cursorCol])', 'set current column as a key column')
1193
1269
  Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current column as a key column')
1194
1270
 
1195
1271
  Sheet.addCommand('e', 'edit-cell', 'cursorCol.setValues([cursorRow], editCell(cursorVisibleColIndex)) if not (cursorRow is None) else fail("no rows to edit")', 'edit contents of current cell')
@@ -1217,7 +1293,7 @@ BaseSheet.addCommand('^I', 'splitwin-swap', 'vd.activePane = 1 if sheet.pane ==
1217
1293
  BaseSheet.addCommand('g^I', 'splitwin-swap-pane', 'vd.options.disp_splitwin_pct=-vd.options.disp_splitwin_pct', 'swap panes onscreen')
1218
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')
1219
1295
 
1220
- BaseSheet.addCommand('^L', 'redraw', 'sheet.refresh(); vd.redraw()', 'Refresh screen')
1296
+ BaseSheet.addCommand('^L', 'redraw', 'sheet.refresh(); vd.redraw(); vd.draw_all()', 'Refresh screen')
1221
1297
  BaseSheet.addCommand(None, 'guard-sheet', 'options.set("quitguard", True, sheet); status("guarded")', 'Set quitguard on current sheet to confirm before quit')
1222
1298
  BaseSheet.addCommand(None, 'guard-sheet-off', 'options.set("quitguard", False, sheet); status("unguarded")', 'Unset quitguard on current sheet to not confirm before quit')
1223
1299
  BaseSheet.addCommand(None, 'open-source', 'vd.replace(source)', 'jump to the source of this sheet')
visidata/shell.py CHANGED
@@ -22,7 +22,7 @@ vd.option('dir_hidden', False, 'load hidden files on DirSheet')
22
22
  @VisiData.api
23
23
  def guess_dir(vd, p):
24
24
  if p.is_dir():
25
- return dict(filetype='dir')
25
+ return dict(filetype='dir', _likelihood=10)
26
26
 
27
27
 
28
28
  @VisiData.lazy_property
@@ -133,7 +133,7 @@ class DirSheet(Sheet):
133
133
  Column('filetype', width=0, cache='async', getter=lambda col,row: subprocess.Popen(['file', '--brief', row], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].strip()),
134
134
  ]
135
135
  nKeys = 2
136
- _ordering = [('modtime', True)] # sort by reverse modtime initially
136
+ _ordering = [('modtime', True), ('filename', False)] # sort by reverse modtime initially
137
137
 
138
138
  @staticmethod
139
139
  def colorOwner(sheet, col, row, val):
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)
@@ -130,7 +131,7 @@ def drawSidebar(vd, scr, sheet):
130
131
  def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
131
132
  scrh, scrw = scr.getmaxyx()
132
133
  maxw = sheet.options.disp_sidebar_width or scrw//2
133
- maxh = sheet.options.disp_sidebar_height or scrh-2
134
+ maxh = sheet.options.disp_sidebar_height or max(scrh-2, 1)
134
135
 
135
136
  cattr = colors.get_color('color_sidebar')
136
137
 
@@ -158,6 +159,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
158
159
  if lines:
159
160
  maxlinew = max(maxlinew, max(dispwidth(textonly, maxwidth=maxw) for line, textonly in lines))
160
161
  winh = min(maxh, len(lines)+2)
162
+ winh = max(winh, 1)
161
163
 
162
164
  titlew = dispwidth(title)
163
165
 
@@ -166,6 +168,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
166
168
  maxlinew = max(maxlinew, titlew)
167
169
  winw = min(maxw, maxlinew+4)
168
170
  x, y, w, h = scrw-winw-1, scrh-winh-1, winw, winh
171
+ y = max(y, 0)
169
172
 
170
173
  sidebarscr = vd.subwindow(scr, x, y, w, h)
171
174
 
@@ -191,7 +194,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
191
194
  if bottommsg:
192
195
  clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|', cattr)
193
196
 
194
- sidebarscr.refresh()
197
+ sidebarscr.noutrefresh()
195
198
 
196
199
 
197
200
  @VisiData.api
visidata/sort.py CHANGED
@@ -1,15 +1,32 @@
1
1
  from copy import copy
2
- from visidata import vd, asyncthread, Progress, Sheet, options, UNLOADED
2
+ from visidata import vd, asyncthread, Progress, Sheet, Column, options, UNLOADED, ColumnsSheet
3
+ import re
4
+
5
+ cmdlog_col_prefix='\u241f' #string ␟ to mark the start of column info in an ordering string
3
6
 
4
7
  @Sheet.api
5
- def orderBy(sheet, *cols, reverse=False):
6
- 'Add *cols* to internal ordering and re-sort the rows accordingly. Pass *reverse* as True to order these *cols* descending. Pass empty *cols* (or cols[0] of None) to clear internal ordering.'
8
+ def orderBy(sheet, *cols, reverse=False, change_column=False, save_cmd_input=True):
9
+ '''Add *cols* to internal ordering and re-sort the rows accordingly.
10
+ Pass *reverse* as True to order these *cols* descending.
11
+ Pass empty *cols* (or cols[0] of None) to clear internal ordering.
12
+ Set *change_column* to True to change the sort status of a single column: add/remove/invert it.
13
+ When changing a column, *cols* must have length 1. Sort columns that had higher priority are unchanged. Lower-priority columns are removed.
14
+ If *change_column* is False, *cols* will be added to the existing ordering.
15
+ If *save_cmd_input* is True, the full ordering that results will be saved in the cmdlog for future replay in the 'input' parameter.
16
+ '''
17
+
7
18
  if options.undo:
8
19
  vd.addUndo(setattr, sheet, '_ordering', copy(sheet._ordering))
9
- if sheet._ordering:
10
- vd.addUndo(sheet.sort)
11
- else:
12
- vd.addUndo(setattr, sheet, 'rows', copy(sheet.rows))
20
+ vd.addUndo(setattr, sheet, 'rows', copy(sheet.rows))
21
+
22
+ # for replay, read the full column ordering from the cmdlog input parameter #2688
23
+ input = vd.getLastArgs()
24
+ if input:
25
+ sheet._ordering = order_from_string(sheet, input)
26
+ sheet.sort()
27
+ if save_cmd_input:
28
+ vd.activeCommand.input = order_string(sheet)
29
+ return
13
30
 
14
31
  do_sort = False
15
32
  if not cols or cols[0] is None:
@@ -17,12 +34,21 @@ def orderBy(sheet, *cols, reverse=False):
17
34
  cols = cols[1:]
18
35
  do_sort = True
19
36
 
20
- for c in cols:
21
- sheet._ordering.append((c, reverse))
37
+ if change_column:
38
+ if len(cols) > 1:
39
+ vd.fail('sort order edit must only be applied to a single column')
40
+ new_ordering = edit_ordering(sheet._ordering, cols[0], reverse)
41
+ sheet._ordering = new_ordering
22
42
  do_sort = True
43
+ else:
44
+ for c in cols:
45
+ sheet._ordering.append((c, reverse))
46
+ do_sort = True
23
47
 
24
48
  if do_sort:
25
49
  sheet.sort()
50
+ if save_cmd_input:
51
+ vd.activeCommand.input = order_string(sheet)
26
52
 
27
53
  class Reversor:
28
54
  def __init__(self, obj):
@@ -34,7 +60,43 @@ class Reversor:
34
60
  def __lt__(self, other):
35
61
  return other.obj < self.obj
36
62
 
63
+ def order_string(sheet):
64
+ sheet._ordering = sheet.ordering #converts any ambiguous colname strings to unambiguous Column objects
65
+ ret = ''.join([cmdlog_col_prefix+('>' if reverse else '<') + str(col.name) for col, reverse in sheet._ordering])
66
+ return ret
37
67
 
68
+ def order_from_string(sheet, s):
69
+ instructions = re.split(cmdlog_col_prefix + '(?=[<>])', s)[1:]
70
+ ordering = []
71
+ for instr in instructions:
72
+ c = sheet.column(instr[1:])
73
+ if instr[0] == '<':
74
+ reverse = False
75
+ elif instr[0] == '>':
76
+ reverse = True
77
+ ordering.append((c, reverse))
78
+ return ordering
79
+
80
+ def edit_ordering(ordering, col, reverse):
81
+ '''Return a modified ordering based on editing a single column *col*: add it, remove it, or flip its direction.
82
+ Columns after *col* in the ordering (with lower sort priority) are also removed from the ordering.
83
+ *ordering* is a list of tuples: (Column, boolean), where the boolean defines the sort direction.
84
+ '''
85
+ new_ordering = []
86
+ # handle changes to status of columns that are already in the ordering: add/remove/flip
87
+ changed = False
88
+ for c, old_reverse in ordering:
89
+ if c is col:
90
+ if reverse != old_reverse: # reverse the column's sort direction
91
+ new_ordering.append((c, reverse))
92
+ # if the sort direction is unchanged, remove the column from the ordering
93
+ changed = True
94
+ # columns after the edited column will be dropped from the ordering
95
+ break
96
+ new_ordering.append((c, old_reverse))
97
+ if not changed:
98
+ new_ordering.append((col, reverse))
99
+ return new_ordering
38
100
 
39
101
  @Sheet.cached_property
40
102
  def ordering(sheet) -> 'list[tuple[Column, bool]]':
@@ -76,6 +138,20 @@ def sort(self):
76
138
  vd.warning('sort incomplete due to TypeError; change column type')
77
139
  vd.exceptionCaught(e, status=False)
78
140
 
141
+ ColumnsSheet.columns += [
142
+ Column('sortorder',
143
+ type=int,
144
+ getter=lambda c,r: _sort_order(c, r),
145
+ help='sort priority and direction in source sheet')
146
+ ]
147
+
148
+ def _sort_order(col, srccol):
149
+ sort_cols = [(n+1, reverse) for n, (c, reverse) in enumerate(srccol.sheet.ordering) if c is srccol]
150
+ if not sort_cols:
151
+ return None
152
+ n, reverse = sort_cols[0]
153
+ return -n if reverse else n
154
+
79
155
 
80
156
  # replace existing sort criteria
81
157
  Sheet.addCommand('[', 'sort-asc', 'orderBy(None, cursorCol)', 'sort ascending by current column; replace any existing sort criteria')
@@ -84,8 +160,10 @@ Sheet.addCommand('g[', 'sort-keys-asc', 'orderBy(None, *keyCols)', 'sort ascendi
84
160
  Sheet.addCommand('g]', 'sort-keys-desc', 'orderBy(None, *keyCols, reverse=True)', 'sort descending by all key columns; replace any existing sort criteria')
85
161
 
86
162
  # add to existing sort criteria
87
- Sheet.addCommand('z[', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by current column; add to existing sort criteria')
88
- Sheet.addCommand('z]', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria')
163
+ Sheet.addCommand('', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by current column; add to existing sort criteria')
164
+ Sheet.addCommand('', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria')
165
+ Sheet.addCommand('z[', 'sort-asc-change', 'orderBy(cursorCol, change_column=True)', 'sort ascending by current column; keep higher priority sort criteria')
166
+ Sheet.addCommand('z]', 'sort-desc-change', 'orderBy(cursorCol, reverse=True, change_column=True)', 'sort descending by current column; keep higher priority sort criteria')
89
167
  Sheet.addCommand('gz[', 'sort-keys-asc-add', 'orderBy(*keyCols)', 'sort ascending by all key columns; add to existing sort criteria')
90
168
  Sheet.addCommand('gz]', 'sort-keys-desc-add', 'orderBy(*keyCols, reverse=True)', 'sort descending by all key columns; add to existing sort criteria')
91
169
 
visidata/statusbar.py CHANGED
@@ -8,7 +8,7 @@ import curses
8
8
  import sys
9
9
 
10
10
  import visidata
11
- from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr
11
+ from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr, clipstr_middle
12
12
 
13
13
 
14
14
 
@@ -31,11 +31,6 @@ vd.theme_option('color_highlight_status', 'black on green', 'color of highlighte
31
31
 
32
32
  BaseSheet.init('longname', lambda: '')
33
33
 
34
- def fitWithin(s, n=10):
35
- if len(s) > n:
36
- return s[:n//2-1] + '…' + s[-n//2+1:]
37
- return s
38
-
39
34
  @BaseSheet.property
40
35
  def ancestors(sheet):
41
36
  if isinstance(sheet.source, BaseSheet):
@@ -53,14 +48,16 @@ def sheetlist(sheet):
53
48
 
54
49
  sheetnames = []
55
50
  for vs in sheets:
51
+ if not vs.precious: #2573
52
+ continue
56
53
  if isinstance(vs, BaseSheet):
57
54
  shortcut = ' '
58
55
  if vs.shortcut in '1 2 3 4 5 6 7 8 9 10'.split():
59
- shortcut = vs.shortcut[-1] + '›'
56
+ shortcut = vs.shortcut[-1] + vs.icon
60
57
  if vs is vd.sheet:
61
58
  sheetnames.append(f'[:menu_active]{shortcut}{vs.name}[:]')
62
59
  else:
63
- sheetnames.append(f'[:onclick jump-sheet-{vs.shortcut}]' + fitWithin(f'{shortcut}{vs.name}', 20) + '[:]')
60
+ sheetnames.append(f'[:onclick jump-sheet-{vs.shortcut}]' + clipstr_middle(f'{shortcut}{vs.name}', 20)[0] + '[:]')
64
61
  else:
65
62
  sheetnames.append(vs)
66
63
 
@@ -226,12 +223,16 @@ def modifiedStatus(sheet):
226
223
  return ret
227
224
 
228
225
 
226
+ @BaseSheet.property
227
+ def selectedStatus(sheet):
228
+ return ''
229
+
230
+
229
231
  @Sheet.property
230
232
  def selectedStatus(sheet):
231
233
  if sheet.nSelectedRows:
232
234
  return f' [:selected_row][:onclick dup-selected]{sheet.options.disp_selected_note}{sheet.nSelectedRows}[/][/] '
233
235
 
234
-
235
236
  @VisiData.api
236
237
  def drawRightStatus(vd, scr, vs):
237
238
  'Draw right side of status bar. Return length displayed.'