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.
- visidata/__init__.py +2 -2
- visidata/_input.py +70 -36
- visidata/_open.py +9 -6
- visidata/_types.py +2 -2
- visidata/aggregators.py +125 -16
- visidata/apps/vdsql/_ibis.py +8 -13
- visidata/basesheet.py +4 -1
- visidata/canvas.py +11 -7
- visidata/clipboard.py +11 -2
- visidata/cliptext.py +65 -23
- visidata/cmdlog.py +5 -1
- visidata/column.py +6 -2
- visidata/ddwplay.py +2 -2
- visidata/deprecated.py +91 -63
- visidata/errors.py +41 -5
- visidata/{features → experimental}/helloworld.py +1 -1
- visidata/expr.py +1 -0
- visidata/extensible.py +4 -0
- visidata/features/cmdpalette.py +3 -3
- visidata/features/describe.py +2 -2
- visidata/features/expand_cols.py +8 -5
- visidata/features/freeze.py +14 -2
- visidata/features/go_col.py +2 -1
- visidata/features/graph_zoom_y.py +47 -0
- visidata/features/incr.py +7 -3
- visidata/features/join.py +23 -12
- visidata/features/layout.py +8 -3
- visidata/features/melt.py +1 -0
- visidata/features/rank.py +103 -0
- visidata/features/reload_every.py +9 -6
- visidata/features/sysedit.py +14 -4
- visidata/features/transpose.py +1 -0
- visidata/features/window.py +12 -0
- visidata/form.py +4 -4
- visidata/freqtbl.py +47 -3
- visidata/fuzzymatch.py +8 -5
- visidata/graph.py +5 -3
- visidata/guides/AggregatorsSheet.md +84 -0
- visidata/guides/MacrosSheet.md +1 -1
- visidata/guides/RankGuide.md +51 -0
- visidata/guides/TypesSheet.md +1 -1
- visidata/guides/WindowFunctionGuide.md +49 -0
- visidata/help.py +3 -4
- visidata/indexsheet.py +1 -1
- visidata/loaders/_pandas.py +3 -1
- visidata/loaders/archive.py +6 -3
- visidata/loaders/csv.py +5 -1
- visidata/loaders/eml.py +2 -0
- visidata/loaders/f5log.py +2 -2
- visidata/loaders/fec.py +6 -9
- visidata/loaders/fixed_width.py +2 -0
- visidata/loaders/hdf5.py +34 -10
- visidata/loaders/npy.py +54 -23
- visidata/loaders/orgmode.py +3 -2
- visidata/loaders/pandas_freqtbl.py +4 -0
- visidata/loaders/psv.py +13 -0
- visidata/loaders/sqlite.py +1 -1
- visidata/loaders/vds.py +3 -4
- visidata/macros.py +4 -3
- visidata/main.py +11 -5
- visidata/mainloop.py +7 -4
- visidata/man/parse_options.py +3 -2
- visidata/man/vd.1 +26 -14
- visidata/man/vd.txt +25 -14
- visidata/menu.py +9 -9
- visidata/metasheets.py +3 -3
- visidata/mouse.py +1 -0
- visidata/pyobj.py +17 -9
- visidata/save.py +5 -1
- visidata/selection.py +29 -18
- visidata/settings.py +2 -2
- visidata/sheets.py +52 -24
- visidata/shell.py +2 -2
- visidata/sidebar.py +4 -2
- visidata/sort.py +89 -11
- visidata/statusbar.py +10 -9
- visidata/tests/test_cliptext.py +151 -0
- visidata/tests/test_commands.py +5 -2
- visidata/tests/test_menu.py +1 -1
- visidata/textsheet.py +34 -8
- visidata/themes/ascii8.py +2 -2
- visidata/themes/light.py +5 -0
- visidata/threads.py +16 -8
- visidata/undo.py +1 -1
- visidata/vendor/__init__.py +0 -0
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/vd.1 +26 -14
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/visidata.1 +26 -14
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/METADATA +62 -15
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/RECORD +95 -89
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/WHEEL +1 -1
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/entry_points.txt +1 -0
- visidata-3.1.1.data/scripts/vd +0 -6
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/applications/visidata.desktop +0 -0
- {visidata-3.1.1.data → visidata-3.2.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/LICENSE.gpl3 +0 -0
- {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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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', '
|
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.
|
303
|
-
self.rows = []
|
303
|
+
'Reset rows and sync load ``source`` via iterload. Overridable.'
|
304
304
|
try:
|
305
|
-
|
306
|
-
|
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.
|
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
|
-
|
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
|
-
|
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+
|
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
|
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', '
|
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.
|
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.
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
21
|
-
|
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('
|
88
|
-
Sheet.addCommand('
|
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}]' +
|
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.'
|