visidata 2.11.1__py3-none-any.whl → 3.0.1__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 +72 -91
- visidata/_input.py +259 -42
- visidata/_open.py +84 -29
- visidata/_types.py +21 -3
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +78 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- {vgit → visidata/apps/vgit}/blame.py +5 -2
- {vgit → visidata/apps/vgit}/branch.py +31 -16
- {vgit → visidata/apps/vgit}/config.py +3 -3
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- {vgit → visidata/apps/vgit}/grep.py +6 -5
- visidata/apps/vgit/log.py +81 -0
- {vgit → visidata/apps/vgit}/main.py +18 -5
- {vgit → visidata/apps/vgit}/remote.py +8 -4
- visidata/apps/vgit/repos.py +71 -0
- {vgit → visidata/apps/vgit}/setup.py +6 -4
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- {vgit → visidata/apps/vgit}/statusbar.py +2 -0
- visidata/basesheet.py +63 -51
- visidata/canvas.py +208 -93
- visidata/choose.py +6 -6
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +73 -17
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +88 -114
- visidata/color.py +142 -56
- visidata/column.py +121 -129
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +6 -2
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +22 -4
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +197 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +3 -1
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +32 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +77 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +200 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +20 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +2 -4
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +54 -12
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macros.py +129 -42
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -155
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +302 -149
- visidata/man/vd.txt +291 -154
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +78 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +113 -32
- visidata/pivot.py +73 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +55 -205
- visidata/rename_col.py +20 -0
- visidata/save.py +37 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -25
- visidata/sheets.py +239 -260
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +114 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/benchmark.csv +52 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +65 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +28 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +89 -40
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +17 -5
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.1.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
- {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
- visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
- visidata-3.0.1.dist-info/RECORD +258 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/WHEEL +1 -1
- vgit/__init__.py +0 -1
- vgit/gitsheet.py +0 -164
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.1.data/scripts/vgit +0 -9
- visidata-2.11.1.dist-info/RECORD +0 -155
- {vgit → visidata/apps/vgit}/__main__.py +0 -0
- {vgit → visidata/apps/vgit}/abort.py +0 -0
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.1.data → visidata-3.0.1.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
visidata/shell.py
CHANGED
@@ -10,19 +10,25 @@ except ImportError:
|
|
10
10
|
pass # pwd,grp modules not available on Windows
|
11
11
|
|
12
12
|
from visidata import Column, Sheet, LazyComputeRow, asynccache, BaseSheet, vd
|
13
|
-
from visidata import Path, ENTER, asyncthread,
|
13
|
+
from visidata import Path, ENTER, asyncthread, VisiData
|
14
14
|
from visidata import modtime, filesize, vstat, Progress, TextSheet
|
15
15
|
from visidata.type_date import date
|
16
16
|
|
17
17
|
|
18
|
-
vd.option('
|
18
|
+
vd.option('dir_depth', 0, 'folder recursion depth on DirSheet')
|
19
19
|
vd.option('dir_hidden', False, 'load hidden files on DirSheet')
|
20
20
|
|
21
21
|
|
22
|
+
@VisiData.api
|
23
|
+
def guess_dir(vd, p):
|
24
|
+
if p.is_dir():
|
25
|
+
return dict(filetype='dir')
|
26
|
+
|
27
|
+
|
22
28
|
@VisiData.lazy_property
|
23
29
|
def currentDirSheet(p):
|
24
30
|
'Support opening the current DirSheet from the vdmenu'
|
25
|
-
return DirSheet('.', source=Path('.'))
|
31
|
+
return DirSheet(Path('.').absolute().name, source=Path('.'))
|
26
32
|
|
27
33
|
@asyncthread
|
28
34
|
def exec_shell(*args):
|
@@ -35,11 +41,11 @@ def exec_shell(*args):
|
|
35
41
|
|
36
42
|
@VisiData.api
|
37
43
|
def open_dir(vd, p):
|
38
|
-
return DirSheet(p.
|
44
|
+
return DirSheet(p.base_stem, source=p)
|
39
45
|
|
40
46
|
@VisiData.api
|
41
47
|
def open_fdir(vd, p):
|
42
|
-
return FileListSheet(p.
|
48
|
+
return FileListSheet(p.base_stem, source=p)
|
43
49
|
|
44
50
|
@VisiData.api
|
45
51
|
def addShellColumns(vd, cmd, sheet):
|
@@ -76,6 +82,21 @@ class ColumnShell(Column):
|
|
76
82
|
|
77
83
|
class DirSheet(Sheet):
|
78
84
|
'Sheet displaying directory, using ENTER to open a particular file. Edited fields are applied to the filesystem.'
|
85
|
+
guide = '''
|
86
|
+
# Directory Sheet
|
87
|
+
This is a list of files in the {sheet.displaySource} folder.
|
88
|
+
|
89
|
+
- {help.commands.open_row_file}
|
90
|
+
- {help.commands.open_rows}
|
91
|
+
- (`open-dir-parent`) to open parent directory
|
92
|
+
- {help.commands.sysopen_row}
|
93
|
+
|
94
|
+
## Options (must reload to take effect)
|
95
|
+
|
96
|
+
- {help.options.dir_depth}
|
97
|
+
- [CLI] `-r` to include all files in all subfolders
|
98
|
+
- {help.options.dir_hidden}
|
99
|
+
'''
|
79
100
|
rowtype = 'files' # rowdef: Path
|
80
101
|
defer = True
|
81
102
|
columns = [
|
@@ -145,11 +166,14 @@ class DirSheet(Sheet):
|
|
145
166
|
|
146
167
|
def removeFile(self, path):
|
147
168
|
if path.is_dir():
|
148
|
-
|
169
|
+
if self.options.safety_first:
|
170
|
+
os.rmdir(path)
|
171
|
+
else:
|
172
|
+
shutil.rmtree(path) #1965
|
149
173
|
else:
|
150
174
|
path.unlink()
|
151
175
|
|
152
|
-
def
|
176
|
+
def commitDeleteRow(self, r):
|
153
177
|
self.removeFile(r)
|
154
178
|
|
155
179
|
def newRow(self):
|
@@ -158,7 +182,7 @@ class DirSheet(Sheet):
|
|
158
182
|
def iterload(self):
|
159
183
|
hidden_files = self.options.dir_hidden
|
160
184
|
|
161
|
-
def _walkfiles(p):
|
185
|
+
def _walkfiles(p, dir_depth:int=0):
|
162
186
|
basepath = str(p)
|
163
187
|
for folder, subdirs, files in os.walk(basepath):
|
164
188
|
subfolder = folder[len(basepath)+1:]
|
@@ -166,24 +190,23 @@ class DirSheet(Sheet):
|
|
166
190
|
if subfolder in ['.', '..']: continue
|
167
191
|
|
168
192
|
fpath = Path(folder)
|
169
|
-
|
193
|
+
if str(fpath) != str(p):
|
194
|
+
yield fpath
|
170
195
|
|
171
196
|
for fn in files:
|
172
197
|
yield fpath/fn
|
173
198
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
199
|
+
if dir_depth < len(fpath.parents)-len(p.parents)+1:
|
200
|
+
for d in subdirs:
|
201
|
+
yield fpath/d
|
202
|
+
subdirs.clear()
|
179
203
|
|
180
204
|
basepath = str(self.source)
|
181
205
|
|
182
206
|
folders = set()
|
183
|
-
f = _walkfiles if self.options.dir_recurse else _listfiles
|
184
207
|
|
185
|
-
for p in
|
186
|
-
if not hidden_files and p.
|
208
|
+
for p in _walkfiles(self.source, self.options.dir_depth):
|
209
|
+
if not hidden_files and str(p).startswith('.') and not str(p).startswith('..'):
|
187
210
|
continue
|
188
211
|
|
189
212
|
yield p
|
@@ -204,11 +227,14 @@ class DirSheet(Sheet):
|
|
204
227
|
self._deferredDels.clear()
|
205
228
|
self.reload()
|
206
229
|
|
230
|
+
def getDefaultSaveName(sheet):
|
231
|
+
return sheet.name + '.' + sheet.options.save_filetype
|
232
|
+
|
207
233
|
|
208
234
|
class FileListSheet(DirSheet):
|
209
235
|
_ordering = []
|
210
236
|
def iterload(self):
|
211
|
-
for fn in self.source.
|
237
|
+
for fn in self.source.open():
|
212
238
|
yield Path(fn.rstrip())
|
213
239
|
|
214
240
|
|
@@ -219,10 +245,10 @@ def inputShell(vd):
|
|
219
245
|
vd.warning('no $column in command')
|
220
246
|
return cmd
|
221
247
|
|
222
|
-
DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source
|
248
|
+
DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source.parent if source.resolve()!=Path(".").resolve() else os.path.dirname(source.resolve())))', 'open parent directory') #1801
|
223
249
|
BaseSheet.addCommand('', 'open-dir-current', 'vd.push(vd.currentDirSheet)', 'open Directory Sheet: browse properties of files in current directory')
|
224
250
|
|
225
|
-
Sheet.addCommand('z;', 'addcol-
|
251
|
+
Sheet.addCommand('z;', 'addcol-shell', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
|
226
252
|
|
227
253
|
DirSheet.addCommand(ENTER, 'open-row-file', 'vd.push(openSource(cursorRow or fail("no row"), filetype="dir" if cursorRow.is_dir() else LazyComputeRow(sheet, cursorRow).ext))', 'open current file as a new sheet')
|
228
254
|
DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets')
|
@@ -243,7 +269,7 @@ def copy_files(sheet, paths, dest):
|
|
243
269
|
try:
|
244
270
|
destpath = destdir/str(srcpath._path.name)
|
245
271
|
if srcpath.is_dir():
|
246
|
-
shutil.
|
272
|
+
shutil.copytree(srcpath, destpath)
|
247
273
|
else:
|
248
274
|
shutil.copyfile(srcpath, destpath)
|
249
275
|
except Exception as e:
|
@@ -253,3 +279,7 @@ def copy_files(sheet, paths, dest):
|
|
253
279
|
vd.addGlobals({
|
254
280
|
'DirSheet': DirSheet
|
255
281
|
})
|
282
|
+
|
283
|
+
vd.addMenuItems('''
|
284
|
+
Column > Add column > shell > addcol-shell
|
285
|
+
''')
|
visidata/sidebar.py
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
from typing import Optional, Union
|
2
|
+
import textwrap
|
3
|
+
|
4
|
+
from visidata import vd, VisiData, BaseSheet, colors, TextSheet, clipdraw, wraptext, dispwidth, AttrDict, wrmap
|
5
|
+
from visidata import CommandHelpGetter, OptionHelpGetter
|
6
|
+
|
7
|
+
|
8
|
+
vd.option('disp_sidebar', True, 'whether to display sidebar')
|
9
|
+
vd.option('disp_sidebar_fmt', '{guide}', 'format string for default sidebar')
|
10
|
+
vd.theme_option('disp_sidebar_width', 0, 'max width for sidebar')
|
11
|
+
vd.theme_option('disp_sidebar_height', 0, 'max height for sidebar')
|
12
|
+
vd.theme_option('color_sidebar', 'black on 114 blue', 'base color of sidebar')
|
13
|
+
vd.theme_option('color_sidebar_title', 'black on yellow', 'color of sidebar title')
|
14
|
+
|
15
|
+
@BaseSheet.property
|
16
|
+
def formatter_helpstr(sheet):
|
17
|
+
return AttrDict(commands=CommandHelpGetter(type(sheet)),
|
18
|
+
options=OptionHelpGetter())
|
19
|
+
|
20
|
+
|
21
|
+
@BaseSheet.property
|
22
|
+
def default_sidebar(sheet):
|
23
|
+
'Default to format options.disp_sidebar_fmt. Overridable.'
|
24
|
+
fmt = sheet.options.disp_sidebar_fmt
|
25
|
+
return sheet.formatString(fmt, help=sheet.formatter_helpstr)
|
26
|
+
|
27
|
+
|
28
|
+
@VisiData.property
|
29
|
+
def recentStatusMessages(vd) -> str:
|
30
|
+
r = ''
|
31
|
+
for (pri, msgparts), n in vd.statuses.items():
|
32
|
+
msg = '; '.join(wrmap(str, msgparts))
|
33
|
+
msg = f'[{n}x] {msg}' if n > 1 else msg
|
34
|
+
|
35
|
+
if pri == 3: msgattr = '[:error]'
|
36
|
+
elif pri == 2: msgattr = '[:warning]'
|
37
|
+
elif pri == 1: msgattr = '[:warning]'
|
38
|
+
else: msgattr = ''
|
39
|
+
|
40
|
+
if msgattr:
|
41
|
+
r += '\n' + f'{msgattr}{msg}[/]'
|
42
|
+
else:
|
43
|
+
r += '\n' + msg
|
44
|
+
|
45
|
+
if r:
|
46
|
+
return '# statuses' + r
|
47
|
+
|
48
|
+
return ''
|
49
|
+
|
50
|
+
|
51
|
+
@VisiData.api
|
52
|
+
def drawSidebar(vd, scr, sheet):
|
53
|
+
sidebar = vd.recentStatusMessages
|
54
|
+
bottommsg = ''
|
55
|
+
overflowmsg = '[:reverse] Ctrl+P to view all status messages [/]'
|
56
|
+
try:
|
57
|
+
if not sidebar and sheet.options.disp_sidebar:
|
58
|
+
sidebar = sheet.default_sidebar
|
59
|
+
if not sidebar and sheet.options.disp_help > 0:
|
60
|
+
sidebar = sheet.formatString(sheet.guide, help=sheet.formatter_helpstr)
|
61
|
+
|
62
|
+
if sheet.options.disp_help < 0:
|
63
|
+
bottommsg = '[:onclick sidebar-toggle][:reverse][x][:]'
|
64
|
+
overflowmsg = '[:onclick open-sidebar]…↓…[/]'
|
65
|
+
else:
|
66
|
+
bottommsg = '[:onclick sidebar-toggle][:reverse] b to toggle sidebar [:]'
|
67
|
+
overflowmsg = '[:reverse] see full sidebar with [:code]gb[/] [:]'
|
68
|
+
except Exception as e:
|
69
|
+
vd.exceptionCaught(e)
|
70
|
+
sidebar = f'# error\n{e}'
|
71
|
+
|
72
|
+
sheet.current_sidebar = sidebar
|
73
|
+
|
74
|
+
return sheet.drawSidebarText(scr, text=sheet.current_sidebar, overflowmsg=overflowmsg, bottommsg=bottommsg)
|
75
|
+
|
76
|
+
@BaseSheet.api
|
77
|
+
def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
|
78
|
+
scrh, scrw = scr.getmaxyx()
|
79
|
+
maxw = sheet.options.disp_sidebar_width or scrw//2
|
80
|
+
maxh = sheet.options.disp_sidebar_height or scrh-2
|
81
|
+
|
82
|
+
cattr = colors.get_color('color_sidebar')
|
83
|
+
|
84
|
+
text = text or ''
|
85
|
+
|
86
|
+
if hasattr(text, 'draw'): # like a HelpPane
|
87
|
+
maxlinew = text.width
|
88
|
+
winh = min(maxh, text.height+2)+1
|
89
|
+
else:
|
90
|
+
text = textwrap.dedent(text.strip('\n'))
|
91
|
+
|
92
|
+
if not text:
|
93
|
+
return
|
94
|
+
|
95
|
+
lines = text.splitlines()
|
96
|
+
if not title and lines and lines[0].strip().startswith('# '):
|
97
|
+
title = lines[0].strip()[2:]
|
98
|
+
text = '\n'.join(lines[1:])
|
99
|
+
|
100
|
+
if not text:
|
101
|
+
return
|
102
|
+
|
103
|
+
lines = list(wraptext(text, width=maxw-4))
|
104
|
+
maxlinew = 0
|
105
|
+
if lines:
|
106
|
+
maxlinew = max(maxlinew, max(dispwidth(textonly, maxwidth=maxw) for line, textonly in lines))
|
107
|
+
winh = min(maxh, len(lines)+2)
|
108
|
+
|
109
|
+
titlew = dispwidth(title)
|
110
|
+
|
111
|
+
maxlinew = max(maxlinew, dispwidth(overflowmsg)+4)
|
112
|
+
maxlinew = max(maxlinew, dispwidth(bottommsg)+4)
|
113
|
+
maxlinew = max(maxlinew, titlew)
|
114
|
+
winw = min(maxw, maxlinew+4)
|
115
|
+
x, y, w, h = scrw-winw-1, scrh-winh-1, winw, winh
|
116
|
+
|
117
|
+
sidebarscr = vd.subwindow(scr, x, y, w, h)
|
118
|
+
|
119
|
+
sidebarscr.erase()
|
120
|
+
sidebarscr.bkgd(' ', cattr.attr)
|
121
|
+
sidebarscr.border()
|
122
|
+
vd.onMouse(sidebarscr, 0, 0, w, h, BUTTON1_RELEASED='no-op', BUTTON1_PRESSED='no-op')
|
123
|
+
|
124
|
+
if hasattr(text, 'draw'): # like a HelpPane
|
125
|
+
text.draw(sidebarscr, attr=cattr)
|
126
|
+
else:
|
127
|
+
i = 0
|
128
|
+
for line, _ in lines:
|
129
|
+
if i >= h-2:
|
130
|
+
bottommsg = overflowmsg
|
131
|
+
break
|
132
|
+
|
133
|
+
x += clipdraw(sidebarscr, i+1, 2, line, cattr, w=w-3)
|
134
|
+
i += 1
|
135
|
+
|
136
|
+
x = max(0, w-titlew-6)
|
137
|
+
clipdraw(sidebarscr, 0, x, f"|[:sidebar_title] {title} [:]|", cattr, w=titlew+4)
|
138
|
+
if bottommsg:
|
139
|
+
clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|[:]', cattr)
|
140
|
+
|
141
|
+
sidebarscr.refresh()
|
142
|
+
|
143
|
+
|
144
|
+
@VisiData.api
|
145
|
+
class SidebarSheet(TextSheet):
|
146
|
+
guide = '''
|
147
|
+
# Sidebar Guide
|
148
|
+
The sidebar provides additional information about the current sheet, defaulting to a basic guide to the current sheet type.
|
149
|
+
It can be configured to show many useful attributes via `options.disp_sidebar_fmt`.
|
150
|
+
|
151
|
+
- `gb` to open the sidebar in a new sheet
|
152
|
+
- `b` to toggle the sidebar on/off for the current sheet
|
153
|
+
'''
|
154
|
+
|
155
|
+
BaseSheet.addCommand('b', 'sidebar-toggle', 'sheet.options.disp_sidebar = not sheet.options.disp_sidebar', 'toggle sidebar on/off')
|
156
|
+
BaseSheet.addCommand('gb', 'open-sidebar', 'sheet.current_sidebar = "" if not hasattr(sheet, "current_sidebar") else sheet.current_sidebar; vd.push(SidebarSheet(name, options.disp_sidebar_fmt, source=sheet.current_sidebar.splitlines()))', 'open sidebar in new sheet')
|
157
|
+
|
158
|
+
|
159
|
+
vd.addMenuItems('''
|
160
|
+
View > Sidebar > toggle > sidebar-toggle
|
161
|
+
View > Sidebar > open in new sheet > open-sidebar
|
162
|
+
''')
|
visidata/sort.py
CHANGED
@@ -36,7 +36,7 @@ class Reversor:
|
|
36
36
|
|
37
37
|
|
38
38
|
@Sheet.api
|
39
|
-
def sortkey(self, r
|
39
|
+
def sortkey(self, r):
|
40
40
|
ret = []
|
41
41
|
for col, reverse in self._ordering:
|
42
42
|
if isinstance(col, str):
|
@@ -44,8 +44,6 @@ def sortkey(self, r, prog=None):
|
|
44
44
|
val = col.getTypedValue(r)
|
45
45
|
ret.append(Reversor(val) if reverse else val)
|
46
46
|
|
47
|
-
if prog:
|
48
|
-
prog.addProgress(1)
|
49
47
|
|
50
48
|
return ret
|
51
49
|
|
@@ -58,7 +56,7 @@ def sort(self):
|
|
58
56
|
try:
|
59
57
|
with Progress(gerund='sorting', total=self.nRows) as prog:
|
60
58
|
# must not reassign self.rows: use .sort() instead of sorted()
|
61
|
-
self.rows.sort(key=lambda r,self=self,prog=prog: self.sortkey(r
|
59
|
+
self.rows.sort(key=lambda r,self=self,prog=prog: (prog.addProgress(1), self.sortkey(r))[1])
|
62
60
|
except TypeError as e:
|
63
61
|
vd.warning('sort incomplete due to TypeError; change column type')
|
64
62
|
vd.exceptionCaught(e, status=False)
|
@@ -75,3 +73,12 @@ Sheet.addCommand('z[', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by
|
|
75
73
|
Sheet.addCommand('z]', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria')
|
76
74
|
Sheet.addCommand('gz[', 'sort-keys-asc-add', 'orderBy(*keyCols)', 'sort ascending by all key columns; add to existing sort criteria')
|
77
75
|
Sheet.addCommand('gz]', 'sort-keys-desc-add', 'orderBy(*keyCols, reverse=True)', 'sort descending by all key columns; add to existing sort criteria')
|
76
|
+
|
77
|
+
vd.addMenuItems('''
|
78
|
+
Column > Sort by > current column only > ascending > sort-asc
|
79
|
+
Column > Sort by > current column only > descending > sort-desc
|
80
|
+
Column > Sort by > current column also > ascending > sort-asc-add
|
81
|
+
Column > Sort by > current column also > descending > sort-desc-add
|
82
|
+
Column > Sort by > key columns > ascending > sort-keys-asc
|
83
|
+
Column > Sort by > key columns > descending > sort-keys-desc
|
84
|
+
''')
|
visidata/statusbar.py
CHANGED
@@ -1,30 +1,43 @@
|
|
1
|
+
'''
|
2
|
+
Status messages get added with vd.{debug/aside/status/warning/fail/error}(), and cleared in mainloop
|
3
|
+
'''
|
4
|
+
|
5
|
+
import builtins
|
1
6
|
import collections
|
2
7
|
import curses
|
8
|
+
import sys
|
3
9
|
|
4
|
-
|
10
|
+
import visidata
|
11
|
+
from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr
|
5
12
|
|
6
13
|
|
7
|
-
vd.option('disp_rstatus_fmt', ' {sheet.longname}
|
8
|
-
vd.option('disp_status_fmt', '{sheet.shortcut}› {sheet.name}| ', 'status line prefix')
|
9
|
-
vd.
|
10
|
-
vd.
|
14
|
+
vd.option('disp_rstatus_fmt', '{sheet.threadStatus} {sheet.keystrokeStatus} [:longname]{sheet.longname}[/] {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus}{sheet.selectedStatus}{vd.replayStatus}', 'right-side status format string')
|
15
|
+
vd.option('disp_status_fmt', '[:onclick sheets-stack]{sheet.shortcut}› {sheet.name}[/]| ', 'status line prefix')
|
16
|
+
vd.theme_option('disp_lstatus_max', 0, 'maximum length of left status line')
|
17
|
+
vd.theme_option('disp_status_sep', '│', 'separator between statuses')
|
11
18
|
|
12
|
-
vd.
|
13
|
-
vd.
|
14
|
-
vd.
|
15
|
-
vd.
|
16
|
-
vd.
|
17
|
-
vd.
|
18
|
-
vd.
|
19
|
+
vd.theme_option('color_keystrokes', 'bold white on 237', 'color of input keystrokes')
|
20
|
+
vd.theme_option('color_longname', '6', 'color of command longnames')
|
21
|
+
vd.theme_option('color_keys', 'bold', 'color of keystrokes in help')
|
22
|
+
vd.theme_option('color_status', 'bold on 238', 'status line color')
|
23
|
+
vd.theme_option('color_error', '202 1', 'error message color')
|
24
|
+
vd.theme_option('color_warning', '166 15', 'warning message color')
|
25
|
+
vd.theme_option('color_top_status', 'underline', 'top window status bar color')
|
26
|
+
vd.theme_option('color_active_status', 'black on 68 blue', ' active window status bar color')
|
27
|
+
vd.theme_option('color_inactive_status', '8 on black', 'inactive window status bar color')
|
28
|
+
vd.theme_option('color_highlight_status', 'black on green', 'color of highlighted elements in statusbar')
|
19
29
|
|
20
30
|
BaseSheet.init('longname', lambda: '')
|
21
31
|
|
22
|
-
|
32
|
+
@BaseSheet.api
|
33
|
+
def _updateStatusBeforeExec(sheet, cmd, args, ks):
|
34
|
+
sheet.longname = cmd.longname
|
35
|
+
if sheet._scr:
|
36
|
+
vd.drawRightStatus(sheet._scr, sheet) #996 show longname during commands
|
37
|
+
sheet._scr.refresh()
|
23
38
|
|
24
39
|
|
25
|
-
|
26
|
-
def modifiedStatus(sheet):
|
27
|
-
return ' [M]' if sheet.hasBeenModified else ''
|
40
|
+
vd.beforeExecHooks.append(BaseSheet._updateStatusBeforeExec)
|
28
41
|
|
29
42
|
|
30
43
|
@VisiData.lazy_property
|
@@ -36,6 +49,11 @@ def statuses(vd):
|
|
36
49
|
def statusHistory(vd):
|
37
50
|
return list() # list of [priority, statusmsg, repeats] for all status messages ever
|
38
51
|
|
52
|
+
@VisiData.api
|
53
|
+
def getStatusSource(vd):
|
54
|
+
return None
|
55
|
+
|
56
|
+
|
39
57
|
@VisiData.api
|
40
58
|
def status(vd, *args, priority=0):
|
41
59
|
'Display *args* on status until next action.'
|
@@ -45,17 +63,25 @@ def status(vd, *args, priority=0):
|
|
45
63
|
k = (priority, tuple(map(str, args)))
|
46
64
|
vd.statuses[k] = vd.statuses.get(k, 0) + 1
|
47
65
|
|
48
|
-
|
66
|
+
source = vd.getStatusSource()
|
67
|
+
|
68
|
+
if not vd.cursesEnabled:
|
69
|
+
msg = '\r' + composeStatus(args)
|
70
|
+
if vd.options.debug:
|
71
|
+
msg += f' [{source}]'
|
72
|
+
builtins.print(msg, file=sys.stderr)
|
73
|
+
|
74
|
+
return vd.addToStatusHistory(*args, priority=priority, source=source)
|
49
75
|
|
50
76
|
@VisiData.api
|
51
|
-
def addToStatusHistory(vd, *args, priority=0):
|
77
|
+
def addToStatusHistory(vd, *args, priority=0, source=None):
|
52
78
|
if vd.statusHistory:
|
53
|
-
prevpri, prevargs,
|
79
|
+
prevpri, prevargs, _, _ = vd.statusHistory[-1]
|
54
80
|
if prevpri == priority and prevargs == args:
|
55
81
|
vd.statusHistory[-1][2] += 1
|
56
82
|
return True
|
57
83
|
|
58
|
-
vd.statusHistory.append([priority, args, 1])
|
84
|
+
vd.statusHistory.append([priority, args, 1, source])
|
59
85
|
return True
|
60
86
|
|
61
87
|
@VisiData.api
|
@@ -75,6 +101,11 @@ def warning(vd, *args):
|
|
75
101
|
'Display *args* on status as a warning.'
|
76
102
|
vd.status(*args, priority=1)
|
77
103
|
|
104
|
+
@VisiData.api
|
105
|
+
def aside(vd, *args, priority=0):
|
106
|
+
'Add a message to statuses without showing the message proactively.'
|
107
|
+
return vd.addToStatusHistory(*args, priority=priority, source=vd.getStatusSource())
|
108
|
+
|
78
109
|
@VisiData.api
|
79
110
|
def debug(vd, *args, **kwargs):
|
80
111
|
'Display *args* on status if options.debug is set.'
|
@@ -87,7 +118,7 @@ def middleTruncate(s, w):
|
|
87
118
|
return s[:w] + options.disp_truncator + s[-w:]
|
88
119
|
|
89
120
|
|
90
|
-
def composeStatus(msgparts, n):
|
121
|
+
def composeStatus(msgparts, n=1):
|
91
122
|
msg = '; '.join(wrmap(str, msgparts))
|
92
123
|
if n > 1:
|
93
124
|
msg = '[%sx] %s' % (n, msg)
|
@@ -97,13 +128,13 @@ def composeStatus(msgparts, n):
|
|
97
128
|
@BaseSheet.api
|
98
129
|
def leftStatus(sheet):
|
99
130
|
'Return left side of status bar for this sheet. Overridable.'
|
100
|
-
return
|
131
|
+
return sheet.formatString(sheet.options.disp_status_fmt)
|
101
132
|
|
102
133
|
|
103
134
|
@VisiData.api
|
104
135
|
def drawLeftStatus(vd, scr, vs):
|
105
136
|
'Draw left side of status bar.'
|
106
|
-
cattr = colors.get_color('
|
137
|
+
cattr = colors.get_color('color_active_status')
|
107
138
|
active = (vs is vd.activeSheet)
|
108
139
|
if active:
|
109
140
|
cattr = update_attr(cattr, colors.color_active_status, 1)
|
@@ -113,103 +144,77 @@ def drawLeftStatus(vd, scr, vs):
|
|
113
144
|
if scr is vd.winTop:
|
114
145
|
cattr = update_attr(cattr, colors.color_top_status, 1)
|
115
146
|
|
116
|
-
attr = cattr.attr
|
117
|
-
error_attr = update_attr(cattr, colors.color_error, 1).attr
|
118
|
-
warn_attr = update_attr(cattr, colors.color_warning, 2).attr
|
119
|
-
sep = options.disp_status_sep
|
120
|
-
|
121
147
|
x = 0
|
122
148
|
y = vs.windowHeight-1 # status for each window
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
BUTTON1_RELEASED='sheets-stack',
|
134
|
-
BUTTON3_PRESSED='rename-sheet',
|
135
|
-
BUTTON3_CLICKED='rename-sheet')
|
136
|
-
except Exception as e:
|
137
|
-
vd.exceptionCaught(e)
|
138
|
-
|
139
|
-
if not active:
|
140
|
-
return
|
141
|
-
|
142
|
-
one = False
|
143
|
-
for (pri, msgparts), n in sorted(vd.statuses.items(), key=lambda k: -k[0][0]):
|
144
|
-
try:
|
145
|
-
if x > vs.windowWidth:
|
146
|
-
break
|
147
|
-
if one: # any messages already:
|
148
|
-
x += clipdraw(scr, y, x, sep, attr, w=vs.windowWidth-x)
|
149
|
-
one = True
|
150
|
-
msg = composeStatus(msgparts, n)
|
151
|
-
|
152
|
-
if pri == 3: msgattr = error_attr
|
153
|
-
elif pri == 2: msgattr = warn_attr
|
154
|
-
elif pri == 1: msgattr = warn_attr
|
155
|
-
else: msgattr = attr
|
156
|
-
x += clipdraw(scr, y, x, msg, msgattr, w=vs.windowWidth-x)
|
157
|
-
except Exception as e:
|
158
|
-
vd.exceptionCaught(e)
|
149
|
+
lstatus = vs.leftStatus()
|
150
|
+
maxwidth = options.disp_lstatus_max
|
151
|
+
if maxwidth > 0:
|
152
|
+
lstatus = middleTruncate(lstatus, maxwidth//2)
|
153
|
+
|
154
|
+
x = clipdraw(scr, y, 0, lstatus, cattr, w=vs.windowWidth-1)
|
155
|
+
|
156
|
+
vd.onMouse(scr, 0, y, x, 1,
|
157
|
+
BUTTON3_PRESSED='rename-sheet',
|
158
|
+
BUTTON3_CLICKED='rename-sheet')
|
159
159
|
|
160
160
|
|
161
161
|
@VisiData.api
|
162
162
|
def rightStatus(vd, sheet):
|
163
163
|
'Return right side of status bar. Overridable.'
|
164
|
-
return
|
164
|
+
return sheet.formatString(sheet.options.disp_rstatus_fmt)
|
165
165
|
|
166
166
|
|
167
|
-
@
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
ret = 0
|
173
|
-
statcolors = [
|
174
|
-
(vd.rightStatus(vs), 'color_status'),
|
175
|
-
]
|
167
|
+
@BaseSheet.property
|
168
|
+
def keystrokeStatus(vs):
|
169
|
+
if vs is vd.activeSheet:
|
170
|
+
return f'[:keystrokes]{vd.keystrokes}[/]'
|
176
171
|
|
177
|
-
|
172
|
+
return ''
|
178
173
|
|
179
|
-
if active:
|
180
|
-
statcolors.append((f'{vd.prettykeys(vd.keystrokes)} ' or '', 'color_keystrokes'))
|
181
174
|
|
175
|
+
@BaseSheet.property
|
176
|
+
def threadStatus(vs) -> str:
|
182
177
|
if vs.currentThreads:
|
183
|
-
|
178
|
+
ret = str(vd.checkMemoryUsage())
|
184
179
|
gerunds = [p.gerund for p in vs.progresses if p.gerund] or ['processing']
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
rstatus = ' '+rstatus
|
195
|
-
cattr = colors.get_color(coloropt)
|
196
|
-
if scr is vd.winTop:
|
197
|
-
cattr = update_attr(cattr, colors.color_top_status, 0)
|
198
|
-
if active:
|
199
|
-
cattr = update_attr(cattr, colors.color_active_status, 0)
|
200
|
-
else:
|
201
|
-
cattr = update_attr(cattr, colors.color_inactive_status, 0)
|
202
|
-
statuslen = clipdraw(scr, vs.windowHeight-1, rightx, rstatus, cattr.attr, w=vs.windowWidth-1, rtl=True)
|
203
|
-
rightx -= statuslen
|
204
|
-
ret += statuslen
|
205
|
-
except Exception as e:
|
206
|
-
vd.exceptionCaught(e)
|
207
|
-
|
208
|
-
if scr:
|
209
|
-
curses.doupdate()
|
180
|
+
ret += f' [:working]{vs.progressPct} {gerunds[0]}…[/]'
|
181
|
+
return ret
|
182
|
+
return ''
|
183
|
+
|
184
|
+
@BaseSheet.property
|
185
|
+
def modifiedStatus(sheet):
|
186
|
+
ret = ' [M]' if sheet.hasBeenModified else ''
|
187
|
+
if not vd.couldOverwrite():
|
188
|
+
ret += ' [:highlight_status][RO][/] '
|
210
189
|
return ret
|
211
190
|
|
212
191
|
|
192
|
+
@Sheet.property
|
193
|
+
def selectedStatus(sheet):
|
194
|
+
if sheet.nSelectedRows:
|
195
|
+
return f' [:selected_row][:onclick dup-selected]{sheet.options.disp_selected_note}{sheet.nSelectedRows}[/][/] '
|
196
|
+
|
197
|
+
|
198
|
+
@VisiData.api
|
199
|
+
def drawRightStatus(vd, scr, vs):
|
200
|
+
'Draw right side of status bar. Return length displayed.'
|
201
|
+
rightx = vs.windowWidth
|
202
|
+
|
203
|
+
statuslen = 0
|
204
|
+
try:
|
205
|
+
cattr = ColorAttr()
|
206
|
+
if scr is vd.winTop:
|
207
|
+
cattr = update_attr(cattr, colors.color_top_status, 0)
|
208
|
+
cattr = update_attr(cattr, colors.color_active_status if vs is vd.activeSheet else colors.color_inactive_status, 0)
|
209
|
+
rstat = vd.rightStatus(vs)
|
210
|
+
x = max(2, rightx-dispwidth(rstat)-1)
|
211
|
+
statuslen = clipdraw(scr, vs.windowHeight-1, x, rstat, cattr, w=vs.windowWidth-1)
|
212
|
+
finally:
|
213
|
+
if scr:
|
214
|
+
curses.doupdate()
|
215
|
+
return statuslen
|
216
|
+
|
217
|
+
|
213
218
|
class StatusSheet(Sheet):
|
214
219
|
precious = False
|
215
220
|
rowtype = 'statuses' # rowdef: (priority, args, nrepeats)
|
@@ -217,7 +222,8 @@ class StatusSheet(Sheet):
|
|
217
222
|
ColumnItem('priority', 0, type=int, width=0),
|
218
223
|
ColumnItem('nrepeats', 2, type=int, width=0),
|
219
224
|
ColumnItem('args', 1, width=0),
|
220
|
-
Column('message', getter=lambda col,row: composeStatus(row[1], row[2])),
|
225
|
+
Column('message', width=50, getter=lambda col,row: composeStatus(row[1], row[2])),
|
226
|
+
ColumnItem('source', 3, max_help=1),
|
221
227
|
]
|
222
228
|
colorizers = [
|
223
229
|
RowColorizer(1, 'color_error', lambda s,c,r,v: r and r[0] == 3),
|
@@ -234,3 +240,7 @@ def statusHistorySheet(vd):
|
|
234
240
|
|
235
241
|
|
236
242
|
BaseSheet.addCommand('^P', 'open-statuses', 'vd.push(vd.statusHistorySheet)', 'open Status History')
|
243
|
+
|
244
|
+
vd.addMenuItems('''
|
245
|
+
View > Statuses > open-statuses
|
246
|
+
''')
|