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