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.
- visidata/__init__.py +2 -2
- visidata/_input.py +106 -58
- visidata/_open.py +10 -7
- visidata/_types.py +2 -2
- visidata/aggregators.py +125 -16
- visidata/apps/vdsql/_ibis.py +8 -13
- visidata/basesheet.py +4 -3
- visidata/canvas.py +11 -7
- visidata/clipboard.py +11 -2
- visidata/cliptext.py +68 -23
- visidata/cmdlog.py +5 -1
- visidata/column.py +48 -33
- visidata/ddwplay.py +2 -2
- visidata/deprecated.py +96 -63
- visidata/errors.py +41 -5
- visidata/{features → experimental}/helloworld.py +1 -1
- visidata/experimental/liveupdate.py +1 -1
- visidata/expr.py +1 -0
- visidata/extensible.py +4 -0
- visidata/features/cmdpalette.py +64 -25
- visidata/features/describe.py +2 -2
- visidata/features/expand_cols.py +7 -5
- visidata/features/freeze.py +14 -2
- visidata/features/go_col.py +3 -3
- 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 -4
- visidata/features/melt.py +1 -0
- visidata/features/rank.py +103 -0
- visidata/features/reload_every.py +11 -8
- visidata/features/sysedit.py +14 -4
- visidata/features/transpose.py +1 -0
- visidata/features/window.py +12 -0
- visidata/form.py +10 -9
- visidata/freqtbl.py +47 -3
- visidata/fuzzymatch.py +11 -7
- visidata/graph.py +5 -3
- visidata/guides/AggregatorsSheet.md +84 -0
- visidata/guides/CommandsSheet.md +1 -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 +23 -6
- visidata/indexsheet.py +1 -1
- visidata/loaders/_pandas.py +3 -1
- visidata/loaders/archive.py +33 -6
- visidata/loaders/csv.py +12 -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 +5 -4
- visidata/main.py +21 -11
- visidata/mainloop.py +8 -5
- visidata/man/parse_options.py +3 -2
- visidata/man/vd.1 +38 -17
- visidata/man/vd.txt +47 -17
- visidata/menu.py +10 -10
- visidata/metasheets.py +3 -3
- visidata/mouse.py +3 -0
- visidata/movement.py +6 -3
- visidata/pyobj.py +17 -9
- visidata/save.py +10 -2
- visidata/selection.py +29 -18
- visidata/settings.py +9 -5
- visidata/sheets.py +124 -48
- visidata/shell.py +2 -2
- visidata/sidebar.py +11 -8
- visidata/sort.py +89 -11
- visidata/statusbar.py +10 -9
- visidata/tests/test_cliptext.py +164 -0
- visidata/tests/test_commands.py +6 -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 +38 -8
- visidata/utils.py +15 -1
- visidata/vendor/__init__.py +0 -0
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/vd.1 +38 -17
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/visidata.1 +38 -17
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/METADATA +62 -15
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/RECORD +98 -92
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/WHEEL +1 -1
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/entry_points.txt +1 -0
- visidata-3.1.1.data/scripts/vd +0 -6
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/applications/visidata.desktop +0 -0
- {visidata-3.1.1.data → visidata-3.3.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/LICENSE.gpl3 +0 -0
- {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
|
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.
|
303
|
-
self.rows = []
|
305
|
+
'Reset rows and sync load ``source`` via iterload. Overridable.'
|
304
306
|
try:
|
305
|
-
|
306
|
-
|
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.
|
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
|
-
|
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
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
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
|
-
|
652
|
-
|
653
|
-
|
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
|
-
|
661
|
-
|
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
|
-
|
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
|
683
|
-
|
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
|
-
|
690
|
-
|
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(
|
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+
|
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
|
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
|
-
|
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', '
|
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', '
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.'
|