visidata 3.1.1__py3-none-any.whl → 3.2__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 (96) hide show
  1. visidata/__init__.py +2 -2
  2. visidata/_input.py +70 -36
  3. visidata/_open.py +9 -6
  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 -1
  8. visidata/canvas.py +11 -7
  9. visidata/clipboard.py +11 -2
  10. visidata/cliptext.py +65 -23
  11. visidata/cmdlog.py +5 -1
  12. visidata/column.py +6 -2
  13. visidata/ddwplay.py +2 -2
  14. visidata/deprecated.py +91 -63
  15. visidata/errors.py +41 -5
  16. visidata/{features → experimental}/helloworld.py +1 -1
  17. visidata/expr.py +1 -0
  18. visidata/extensible.py +4 -0
  19. visidata/features/cmdpalette.py +3 -3
  20. visidata/features/describe.py +2 -2
  21. visidata/features/expand_cols.py +8 -5
  22. visidata/features/freeze.py +14 -2
  23. visidata/features/go_col.py +2 -1
  24. visidata/features/graph_zoom_y.py +47 -0
  25. visidata/features/incr.py +7 -3
  26. visidata/features/join.py +23 -12
  27. visidata/features/layout.py +8 -3
  28. visidata/features/melt.py +1 -0
  29. visidata/features/rank.py +103 -0
  30. visidata/features/reload_every.py +9 -6
  31. visidata/features/sysedit.py +14 -4
  32. visidata/features/transpose.py +1 -0
  33. visidata/features/window.py +12 -0
  34. visidata/form.py +4 -4
  35. visidata/freqtbl.py +47 -3
  36. visidata/fuzzymatch.py +8 -5
  37. visidata/graph.py +5 -3
  38. visidata/guides/AggregatorsSheet.md +84 -0
  39. visidata/guides/MacrosSheet.md +1 -1
  40. visidata/guides/RankGuide.md +51 -0
  41. visidata/guides/TypesSheet.md +1 -1
  42. visidata/guides/WindowFunctionGuide.md +49 -0
  43. visidata/help.py +3 -4
  44. visidata/indexsheet.py +1 -1
  45. visidata/loaders/_pandas.py +3 -1
  46. visidata/loaders/archive.py +6 -3
  47. visidata/loaders/csv.py +5 -1
  48. visidata/loaders/eml.py +2 -0
  49. visidata/loaders/f5log.py +2 -2
  50. visidata/loaders/fec.py +6 -9
  51. visidata/loaders/fixed_width.py +2 -0
  52. visidata/loaders/hdf5.py +34 -10
  53. visidata/loaders/npy.py +54 -23
  54. visidata/loaders/orgmode.py +3 -2
  55. visidata/loaders/pandas_freqtbl.py +4 -0
  56. visidata/loaders/psv.py +13 -0
  57. visidata/loaders/sqlite.py +1 -1
  58. visidata/loaders/vds.py +3 -4
  59. visidata/macros.py +4 -3
  60. visidata/main.py +11 -5
  61. visidata/mainloop.py +7 -4
  62. visidata/man/parse_options.py +3 -2
  63. visidata/man/vd.1 +26 -14
  64. visidata/man/vd.txt +25 -14
  65. visidata/menu.py +9 -9
  66. visidata/metasheets.py +3 -3
  67. visidata/mouse.py +1 -0
  68. visidata/pyobj.py +17 -9
  69. visidata/save.py +5 -1
  70. visidata/selection.py +29 -18
  71. visidata/settings.py +2 -2
  72. visidata/sheets.py +52 -24
  73. visidata/shell.py +2 -2
  74. visidata/sidebar.py +4 -2
  75. visidata/sort.py +89 -11
  76. visidata/statusbar.py +10 -9
  77. visidata/tests/test_cliptext.py +151 -0
  78. visidata/tests/test_commands.py +5 -2
  79. visidata/tests/test_menu.py +1 -1
  80. visidata/textsheet.py +34 -8
  81. visidata/themes/ascii8.py +2 -2
  82. visidata/themes/light.py +5 -0
  83. visidata/threads.py +16 -8
  84. visidata/undo.py +1 -1
  85. visidata/vendor/__init__.py +0 -0
  86. {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/vd.1 +26 -14
  87. {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/visidata.1 +26 -14
  88. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/METADATA +62 -15
  89. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/RECORD +95 -89
  90. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/WHEEL +1 -1
  91. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/entry_points.txt +1 -0
  92. visidata-3.1.1.data/scripts/vd +0 -6
  93. {visidata-3.1.1.data → visidata-3.2.data}/data/share/applications/visidata.desktop +0 -0
  94. {visidata-3.1.1.data → visidata-3.2.data}/scripts/vd2to3.vdx +0 -0
  95. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/LICENSE.gpl3 +0 -0
  96. {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/top_level.txt +0 -0
visidata/selection.py CHANGED
@@ -20,9 +20,10 @@ def isSelected(self, row):
20
20
 
21
21
  @Sheet.api
22
22
  @asyncthread
23
- def toggle(self, rows):
23
+ def toggle(self, rows, add_undo=True):
24
24
  'Toggle selection of given *rows*. Async.'
25
- self.addUndoSelection()
25
+ if add_undo:
26
+ self.addUndoSelection()
26
27
  for r in Progress(rows, 'toggling', total=len(rows)):
27
28
  if self.isSelected(r): #1671
28
29
  self.unselectRow(r)
@@ -30,17 +31,24 @@ def toggle(self, rows):
30
31
  self.selectRow(r)
31
32
 
32
33
 
34
+ @Sheet.before
35
+ def beforeLoad(self):
36
+ self._selectedRows.clear()
37
+
38
+
33
39
  @Sheet.api
34
- def select_row(self, row):
40
+ def select_row(self, row, add_undo=True):
35
41
  'Add single *row* to set of selected rows.'
36
- self.addUndoSelection()
42
+ if add_undo:
43
+ self.addUndoSelection()
37
44
  self.selectRow(row)
38
45
 
39
46
 
40
47
  @Sheet.api
41
- def toggle_row(self, row):
48
+ def toggle_row(self, row, add_undo=True):
42
49
  'Toggle selection of given *row*.'
43
- self.addUndoSelection()
50
+ if add_undo:
51
+ self.addUndoSelection()
44
52
  if self.isSelected(row):
45
53
  self.unselectRow(row)
46
54
  else:
@@ -48,9 +56,10 @@ def toggle_row(self, row):
48
56
 
49
57
 
50
58
  @Sheet.api
51
- def unselect_row(self, row):
59
+ def unselect_row(self, row, add_undo=True):
52
60
  'Remove single *row* from set of selected rows.'
53
- self.addUndoSelection()
61
+ if add_undo:
62
+ self.addUndoSelection()
54
63
  self.unselectRow(row) or vd.warning('row not selected')
55
64
 
56
65
 
@@ -77,9 +86,10 @@ def clearSelected(self):
77
86
 
78
87
  @Sheet.api
79
88
  @asyncthread
80
- def select(self, rows, status=True, progress=True):
81
- "Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False."
82
- self.addUndoSelection()
89
+ def select(self, rows, status=True, progress=True, add_undo=True):
90
+ "Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False. If *add_undo* is False, do not add an undo selection function to the undo history; useful for lowering memory consumption when caller is changing a large batch of selects in one command."
91
+ if add_undo:
92
+ self.addUndoSelection()
83
93
  before = self.nSelectedRows
84
94
  if self.options.bulk_select_clear:
85
95
  self.clearSelected()
@@ -94,9 +104,10 @@ def select(self, rows, status=True, progress=True):
94
104
 
95
105
  @Sheet.api
96
106
  @asyncthread
97
- def unselect(self, rows, status=True, progress=True):
98
- "Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False."
99
- self.addUndoSelection()
107
+ def unselect(self, rows, status=True, progress=True, add_undo=True):
108
+ "Remove *rows* from set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False. If *add_undo* is False, do not add an undo unselection function to the undo history; useful for lowering memory consumption when caller is changing a large batch of selects in one command."
109
+ if add_undo:
110
+ self.addUndoSelection()
100
111
  before = self.nSelectedRows
101
112
  for r in (Progress(rows, 'unselecting') if progress else rows):
102
113
  self.unselectRow(r)
@@ -191,10 +202,10 @@ Sheet.addCommand('\\', 'unselect-col-regex', 'unselectByIdx(searchInputRegex("un
191
202
  Sheet.addCommand('g|', 'select-cols-regex', 'selectByIdx(searchInputRegex("select", columns="visibleCols"))', 'select rows matching regex in any visible column')
192
203
  Sheet.addCommand('g\\', 'unselect-cols-regex', 'unselectByIdx(searchInputRegex("unselect", columns="visibleCols"))', 'unselect rows matching regex in any visible column')
193
204
 
194
- Sheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell in current column')
195
- Sheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
196
- Sheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')
197
- Sheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
205
+ Sheet.addCommand(',', 'select-equal-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorDisplay: c.getDisplayValue(r) == v), progress=False)', 'select rows matching current cell displayed value in current column')
206
+ Sheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getDisplayValue(r) == c.getDisplayValue(currow) for c in vcols])), progress=False)', 'select rows matching displayed values in current row in all visible columns')
207
+ Sheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell typed value in current column')
208
+ Sheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching typed values in current row in all visible columns')
198
209
 
199
210
  Sheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select(gatherBy(lambda r, sheet=sheet, expr=expr, curcol=cursorCol: sheet.evalExpr(expr, r, curcol=curcol)), progress=False)', 'select rows matching Python expression in any visible column')
200
211
  Sheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr, curcol=cursorCol: sheet.evalExpr(expr, r, curcol=curcol)), progress=False)', 'unselect rows matching Python expression in any visible column')
visidata/settings.py CHANGED
@@ -446,7 +446,7 @@ def loadConfigAndPlugins(vd, args=AttrDict()):
446
446
  # autoload installed plugins first
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
- from importlib_metadata import entry_points # a backport which supports < 3.8 https://github.com/pypa/twine/pull/732
449
+ from importlib.metadata import entry_points
450
450
  try:
451
451
  eps = entry_points()
452
452
  eps_visidata = eps.select(group='visidata.plugins') if 'visidata.plugins' in eps.groups else []
@@ -550,7 +550,7 @@ def setPersistentOptions(vd, **kwargs):
550
550
  fp.write(f'options.{optname}={repr(optval)}\n')
551
551
 
552
552
 
553
- vd.option('visidata_dir', '~/.visidata/', 'directory to load and store additional files', sheettype=None)
553
+ vd.option('visidata_dir', user_config_dir('visidata'), 'directory to load and store additional files', sheettype=None)
554
554
 
555
555
  BaseSheet.bindkey('^M', '^J') # for windows ENTER
556
556
 
visidata/sheets.py CHANGED
@@ -22,6 +22,7 @@ vd.option('disp_wrap_break_long_words', False, 'break words longer than column w
22
22
  vd.option('disp_wrap_replace_whitespace', False, 'replace whitespace with spaces in multiline')
23
23
  vd.option('disp_wrap_placeholder', '…', 'multiline string to indicate truncation')
24
24
  vd.option('disp_multiline_focus', True, 'only multiline cursor row')
25
+ vd.option('color_multiline_bottom', '', 'color of bottom line of multiline rows') #2715
25
26
  vd.option('color_aggregator', 'bold 255 white on 234 black', 'color of aggregator summary on bottom row')
26
27
 
27
28
 
@@ -299,24 +300,43 @@ class TableSheet(BaseSheet):
299
300
  self.setKeys(self.columns[:self.nKeys])
300
301
 
301
302
  def loader(self):
302
- 'Reset rows and sync load ``source`` via iterload. Overrideable.'
303
- self.rows = []
303
+ 'Reset rows and sync load ``source`` via iterload. Overridable.'
304
304
  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)
305
+ for r in self._iterloader():
306
+ pass
310
307
  except FileNotFoundError:
311
308
  return # let it be a blank sheet without error
312
309
 
310
+ def _iterloader(self):
311
+ self.rows = []
312
+ with vd.Progress(gerund='loading', total=0):
313
+ max_rows = self.options.max_rows
314
+ for i, r in enumerate(self.iterload()):
315
+ if self.precious and i >= max_rows:
316
+ break
317
+ self.addRow(r)
318
+ yield r
319
+
313
320
  def iterload(self):
314
321
  'Generate rows from ``self.source``. Override in subclass.'
315
322
  if False:
316
323
  yield vd.fail('no iterload for this loader yet')
317
324
 
325
+ def loadStart(self):
326
+ self.loaditer = self._iterloader()
327
+
328
+ def loadSome(self):
329
+ if not self.loaditer:
330
+ return False
331
+ try:
332
+ next(self.loaditer)
333
+ return True
334
+ except StopIteration:
335
+ self.loaditer = None
336
+ return False
337
+
318
338
  def afterLoad(self):
319
- 'hook for after loading has finished. Overrideable (be sure to call super).'
339
+ 'hook for after loading has finished. Overridable (be sure to call super).'
320
340
  # if an ordering has been specified, sort the sheet
321
341
  if self._ordering:
322
342
  vd.sync(self.sort())
@@ -508,6 +528,9 @@ class TableSheet(BaseSheet):
508
528
  'Raw value at current row and column.'
509
529
  return self.cursorCol.getValue(self.cursorRow)
510
530
 
531
+ def getTypedRow(self, rownum):
532
+ return [c.getTypedValue(self.rows[rownum]) for c in self.availCols]
533
+
511
534
  @property
512
535
  def statusLine(self):
513
536
  'Position of cursor and bounds of current sheet.'
@@ -576,7 +599,10 @@ class TableSheet(BaseSheet):
576
599
  index = 0
577
600
  ccol = self.cursorCol
578
601
  if ccol and not ccol.keycol:
579
- index = self.columns.index(ccol)+1
602
+ try:
603
+ index = self.columns.index(ccol)+1
604
+ except ValueError: # when all columns are hidden, the one column shown is not in self.columns
605
+ index = 0
580
606
 
581
607
  self.addColumn(*cols, index=index)
582
608
  firstnewcol = [c for c in cols if not c.hidden][0]
@@ -604,13 +630,6 @@ class TableSheet(BaseSheet):
604
630
  for col in cols:
605
631
  col.keycol = 0
606
632
 
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
633
  def rowkey(self, row):
615
634
  'Return tuple of the key for *row*.'
616
635
  return tuple(c.getTypedValue(row) for c in self.keyCols)
@@ -664,12 +683,13 @@ class TableSheet(BaseSheet):
664
683
  continue
665
684
 
666
685
  cur_x, cur_w = self._visibleColLayout[self.cursorVisibleColIndex]
667
- if cur_x+cur_w < self.windowWidth: # current columns fit entirely on screen
686
+ if cur_x+cur_w < self.windowWidth-1: # current columns fit entirely on screen
668
687
  break
669
688
  self.leftVisibleColIndex += 1 # once within the bounds, walk over one column at a time
670
689
 
671
690
  def calcColLayout(self):
672
691
  'Set right-most visible column, based on calculation.'
692
+ vd.clearCaches()
673
693
  minColWidth = dispwidth(self.options.disp_more_left)+dispwidth(self.options.disp_more_right)+2
674
694
  sepColWidth = dispwidth(self.options.disp_column_sep)
675
695
  winWidth = self.windowWidth
@@ -701,7 +721,8 @@ class TableSheet(BaseSheet):
701
721
 
702
722
  width = max(width, 1)
703
723
  if col in self.keyCols or vcolidx >= self.leftVisibleColIndex: # visible columns
704
- self._visibleColLayout[vcolidx] = [x, min(width, self.windowWidth-x)]
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)]
705
726
  return width
706
727
 
707
728
 
@@ -744,7 +765,7 @@ class TableSheet(BaseSheet):
744
765
  clipdraw(scr, y+i, x, name, hdrcattr, w=colwidth)
745
766
  vd.onMouse(scr, x, y+i, colwidth, 1, BUTTON3_RELEASED='rename-col')
746
767
 
747
- if C and x+colwidth+len(C) < self.windowWidth and y+i < self.windowHeight:
768
+ if C and x+colwidth+dispwidth(C) < self.windowWidth and y+i < self.windowHeight:
748
769
  scr.addstr(y+i, x+colwidth, C, sepcattr.attr)
749
770
 
750
771
  clipdraw(scr, y+h-1, min(x+colwidth, self.windowWidth-1)-dispwidth(T), T, hdrcattr)
@@ -777,6 +798,8 @@ class TableSheet(BaseSheet):
777
798
  'Return dict of aggname -> list of cols with that aggregator.'
778
799
  allaggs = collections.defaultdict(list) # aggname -> list of cols with that aggregator
779
800
  for vcolidx, (x, colwidth) in sorted(self._visibleColLayout.items()):
801
+ if vcolidx >= len(self.availCols):
802
+ break #2607 #2763
780
803
  col = self.availCols[vcolidx]
781
804
  if not col.hidden:
782
805
  for aggr in col.aggregators:
@@ -935,10 +958,12 @@ class TableSheet(BaseSheet):
935
958
  colseps = [topsep] + [midsep]*(height-2) + [botsep]
936
959
  endseps = [endtopsep] + [endmidsep]*(height-2) + [endbotsep]
937
960
  keyseps = [keytopsep] + [keymidsep]*(height-2) + [keybotsep]
961
+ color_multiline_bottom = colors.get_color('color_multiline_bottom', 2)
938
962
  else:
939
963
  colseps = [colsep]
940
964
  endseps = [endsep]
941
965
  keyseps = [keysep]
966
+ color_multiline_bottom = 0
942
967
 
943
968
  for vcolidx, (col, cellval, lines) in displines.items():
944
969
  if vcolidx not in self._visibleColLayout:
@@ -957,6 +982,7 @@ class TableSheet(BaseSheet):
957
982
 
958
983
  cattr = self._colorize(col, row, cellval)
959
984
  cattr = update_attr(cattr, basecellcattr)
985
+ bottomcattr = update_attr(cattr, color_multiline_bottom) if height > 1 else cattr
960
986
 
961
987
  note = getattr(cellval, 'note', None)
962
988
  notewidth = 1 if note else 0
@@ -984,10 +1010,10 @@ class TableSheet(BaseSheet):
984
1010
  for attr, text in chunks:
985
1011
  prechunks.append((attr, text[hoffset:]))
986
1012
 
987
- clipdraw_chunks(scr, y, x, prechunks, cattr, w=colwidth-notewidth)
1013
+ clipdraw_chunks(scr, y, x, prechunks, cattr if i < height-1 else bottomcattr, w=colwidth-notewidth)
988
1014
  vd.onMouse(scr, x, y, colwidth, 1, BUTTON3_RELEASED='edit-cell')
989
1015
 
990
- if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth:
1016
+ if sepchars and x+colwidth+dispwidth(sepchars) <= self.windowWidth-1:
991
1017
  scr.addstr(y, x+colwidth, sepchars, sepcattr.attr)
992
1018
 
993
1019
  for notefunc in vd.rowNoters:
@@ -1048,8 +1074,9 @@ class SequenceSheet(Sheet):
1048
1074
 
1049
1075
  self.rows = []
1050
1076
  # add the rest of the rows
1077
+ max_rows = self.options.max_rows
1051
1078
  for i, r in enumerate(vd.Progress(itsource, gerund='loading', total=0)):
1052
- if self.precious and i > self.options.max_rows:
1079
+ if self.precious and i >= max_rows:
1053
1080
  break
1054
1081
  self.addRow(r)
1055
1082
 
@@ -1136,8 +1163,8 @@ def confirmQuit(vs, verb='quit'):
1136
1163
  def preloadHook(sheet):
1137
1164
  'Override to setup for reload().'
1138
1165
  sheet.confirmQuit('reload')
1139
-
1140
1166
  sheet.hasBeenModified = False
1167
+ sheet.calcColLayout()
1141
1168
 
1142
1169
 
1143
1170
  @VisiData.api
@@ -1189,7 +1216,8 @@ BaseSheet.init('pane', lambda: 1)
1189
1216
  BaseSheet.addCommand('^R', 'reload-sheet', 'preloadHook(); reload()', 'Reload current sheet')
1190
1217
  Sheet.addCommand('', 'show-cursor', 'status(statusLine)', 'show cursor position and bounds of current sheet on status line')
1191
1218
 
1192
- Sheet.addCommand('!', 'key-col', 'toggleKeys([cursorCol])', 'toggle current column as a key column')
1219
+ 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)
1220
+ Sheet.addCommand('', 'key-col-on', 'setKeys([cursorCol])', 'set current column as a key column')
1193
1221
  Sheet.addCommand('z!', 'key-col-off', 'unsetKeys([cursorCol])', 'unset current column as a key column')
1194
1222
 
1195
1223
  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')
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
@@ -130,7 +130,7 @@ def drawSidebar(vd, scr, sheet):
130
130
  def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
131
131
  scrh, scrw = scr.getmaxyx()
132
132
  maxw = sheet.options.disp_sidebar_width or scrw//2
133
- maxh = sheet.options.disp_sidebar_height or scrh-2
133
+ maxh = sheet.options.disp_sidebar_height or max(scrh-2, 1)
134
134
 
135
135
  cattr = colors.get_color('color_sidebar')
136
136
 
@@ -158,6 +158,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
158
158
  if lines:
159
159
  maxlinew = max(maxlinew, max(dispwidth(textonly, maxwidth=maxw) for line, textonly in lines))
160
160
  winh = min(maxh, len(lines)+2)
161
+ winh = max(winh, 1)
161
162
 
162
163
  titlew = dispwidth(title)
163
164
 
@@ -166,6 +167,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
166
167
  maxlinew = max(maxlinew, titlew)
167
168
  winw = min(maxw, maxlinew+4)
168
169
  x, y, w, h = scrw-winw-1, scrh-winh-1, winw, winh
170
+ y = max(y, 0)
169
171
 
170
172
  sidebarscr = vd.subwindow(scr, x, y, w, h)
171
173
 
@@ -191,7 +193,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
191
193
  if bottommsg:
192
194
  clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|', cattr)
193
195
 
194
- sidebarscr.refresh()
196
+ sidebarscr.noutrefresh()
195
197
 
196
198
 
197
199
  @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.'