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/cmdlog.py
CHANGED
@@ -1,40 +1,47 @@
|
|
1
1
|
import threading
|
2
2
|
|
3
|
-
from visidata import
|
3
|
+
from visidata import vd, UNLOADED, namedlist, vlen, asyncthread, globalCommand, date
|
4
|
+
from visidata import VisiData, BaseSheet, Sheet, ColumnAttr, VisiDataMetaSheet, JsonLinesSheet, TypedWrapper, AttrDict, Progress, ErrorSheet, CompleteKey, Path
|
4
5
|
import visidata
|
5
6
|
|
6
7
|
vd.option('replay_wait', 0.0, 'time to wait between replayed commands, in seconds', sheettype=None)
|
7
|
-
vd.
|
8
|
-
vd.
|
9
|
-
vd.option('color_status_replay', 'green', 'color of replay status indicator')
|
10
|
-
vd.option('replay_movement', False, 'insert movements during replay', sheettype=None)
|
8
|
+
vd.theme_option('disp_replay_play', '▶', 'status indicator for active replay')
|
9
|
+
vd.theme_option('color_status_replay', 'green', 'color of replay status indicator')
|
11
10
|
|
12
11
|
# prefixes which should not be logged
|
13
12
|
nonLogged = '''forget exec-longname undo redo quit
|
14
13
|
show error errors statuses options threads jump
|
15
|
-
replay cancel save-cmdlog macro cmdlog-sheet menu repeat
|
14
|
+
replay cancel save-cmdlog macro cmdlog-sheet menu repeat reload-every
|
16
15
|
go- search scroll prev next page start end zoom resize visibility sidebar
|
17
16
|
mouse suspend redraw no-op help syscopy sysopen profile toggle'''.split()
|
18
17
|
|
19
18
|
vd.option('rowkey_prefix', 'キ', 'string prefix for rowkey in the cmdlog', sheettype=None)
|
20
|
-
vd.option('cmdlog_histfile', '', 'file to autorecord each cmdlog action to', sheettype=None)
|
21
19
|
|
22
20
|
vd.activeCommand = UNLOADED
|
21
|
+
vd._nextCommands = [] # list[str|CommandLogRow] for vd.queueCommand
|
22
|
+
|
23
|
+
CommandLogRow = namedlist('CommandLogRow', 'sheet col row longname input keystrokes comment undofuncs'.split())
|
24
|
+
|
25
|
+
@VisiData.api
|
26
|
+
def queueCommand(vd, longname, input=None, sheet=None, col=None, row=None):
|
27
|
+
'Add command to queue of next commands to execute.'
|
28
|
+
vd._nextCommands.append(CommandLogRow(longname=longname, input=input, sheet=sheet, col=col, row=row))
|
29
|
+
|
23
30
|
|
24
31
|
@VisiData.api
|
25
32
|
def open_vd(vd, p):
|
26
|
-
return CommandLog(p.
|
33
|
+
return CommandLog(p.base_stem, source=p, precious=True)
|
27
34
|
|
28
35
|
@VisiData.api
|
29
36
|
def open_vdj(vd, p):
|
30
|
-
return CommandLogJsonl(p.
|
37
|
+
return CommandLogJsonl(p.base_stem, source=p, precious=True)
|
31
38
|
|
32
39
|
VisiData.save_vd = VisiData.save_tsv
|
33
40
|
|
34
41
|
|
35
42
|
@VisiData.api
|
36
43
|
def save_vdj(vd, p, *vsheets):
|
37
|
-
with p.
|
44
|
+
with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:
|
38
45
|
fp.write("#!vd -p\n")
|
39
46
|
for vs in vsheets:
|
40
47
|
vs.write_jsonl(fp)
|
@@ -46,7 +53,7 @@ def checkVersion(vd, desired_version):
|
|
46
53
|
vd.fail("version %s required" % desired_version)
|
47
54
|
|
48
55
|
@VisiData.api
|
49
|
-
def fnSuffix(vd, prefix):
|
56
|
+
def fnSuffix(vd, prefix:str):
|
50
57
|
i = 0
|
51
58
|
fn = prefix + '.vdj'
|
52
59
|
while Path(fn).exists():
|
@@ -55,18 +62,6 @@ def fnSuffix(vd, prefix):
|
|
55
62
|
|
56
63
|
return fn
|
57
64
|
|
58
|
-
@BaseSheet.api
|
59
|
-
def inputLongname(sheet):
|
60
|
-
longnames = set(k for (k, obj), v in vd.commands.iter(sheet))
|
61
|
-
return vd.input("command name: ", completer=CompleteKey(sorted(longnames)), type='longname')
|
62
|
-
|
63
|
-
@BaseSheet.api
|
64
|
-
def exec_longname(sheet, longname):
|
65
|
-
if not sheet.getCommand(longname):
|
66
|
-
vd.warning(f'no command {longname}')
|
67
|
-
return
|
68
|
-
sheet.execCommand(longname)
|
69
|
-
|
70
65
|
def indexMatch(L, func):
|
71
66
|
'returns the smallest i for which func(L[i]) is true'
|
72
67
|
for i, x in enumerate(L):
|
@@ -74,7 +69,7 @@ def indexMatch(L, func):
|
|
74
69
|
return i
|
75
70
|
|
76
71
|
def keystr(k):
|
77
|
-
return options.rowkey_prefix+','.join(map(str, k))
|
72
|
+
return vd.options.rowkey_prefix+','.join(map(str, k))
|
78
73
|
|
79
74
|
@VisiData.api
|
80
75
|
def isLoggableCommand(vd, longname):
|
@@ -84,7 +79,7 @@ def isLoggableCommand(vd, longname):
|
|
84
79
|
return True
|
85
80
|
|
86
81
|
def isLoggableSheet(sheet):
|
87
|
-
return sheet is not vd.cmdlog and not isinstance(sheet, (OptionsSheet, ErrorSheet))
|
82
|
+
return sheet is not vd.cmdlog and not isinstance(sheet, (vd.OptionsSheet, ErrorSheet))
|
88
83
|
|
89
84
|
|
90
85
|
@Sheet.api
|
@@ -94,13 +89,7 @@ def moveToRow(vs, rowstr):
|
|
94
89
|
if rowidx is None:
|
95
90
|
return False
|
96
91
|
|
97
|
-
|
98
|
-
while vs.cursorRowIndex != rowidx:
|
99
|
-
vs.cursorRowIndex += 1 if (rowidx - vs.cursorRowIndex) > 0 else -1
|
100
|
-
while not vd.delay(0.5):
|
101
|
-
pass
|
102
|
-
else:
|
103
|
-
vs.cursorRowIndex = rowidx
|
92
|
+
vs.cursorRowIndex = rowidx
|
104
93
|
|
105
94
|
return True
|
106
95
|
|
@@ -126,13 +115,7 @@ def moveToCol(vs, col):
|
|
126
115
|
if vcolidx is None or vcolidx >= vs.nVisibleCols:
|
127
116
|
return False
|
128
117
|
|
129
|
-
|
130
|
-
while vs.cursorVisibleColIndex != vcolidx:
|
131
|
-
vs.cursorVisibleColIndex += 1 if (vcolidx - vs.cursorVisibleColIndex) > 0 else -1
|
132
|
-
while not vd.delay(0.5):
|
133
|
-
pass
|
134
|
-
else:
|
135
|
-
vs.cursorVisibleColIndex = vcolidx
|
118
|
+
vs.cursorVisibleColIndex = vcolidx
|
136
119
|
|
137
120
|
return True
|
138
121
|
|
@@ -147,7 +130,10 @@ def commandCursor(sheet, execstr):
|
|
147
130
|
rowname = keystr(k) if k else sheet.cursorRowIndex
|
148
131
|
|
149
132
|
if contains(execstr, 'cursorTypedValue', 'cursorDisplay', 'cursorValue', 'cursorCell', 'cursorCol', 'cursorVisibleCol', 'ColumnAtCursor'):
|
150
|
-
|
133
|
+
if sheet.cursorCol:
|
134
|
+
colname = sheet.cursorCol.name or sheet.visibleCols.index(sheet.cursorCol)
|
135
|
+
else:
|
136
|
+
colname = None
|
151
137
|
return colname, rowname
|
152
138
|
|
153
139
|
|
@@ -156,7 +142,7 @@ class CommandLogBase:
|
|
156
142
|
'Log of commands for current session.'
|
157
143
|
rowtype = 'logged commands'
|
158
144
|
precious = False
|
159
|
-
_rowtype =
|
145
|
+
_rowtype = CommandLogRow
|
160
146
|
columns = [
|
161
147
|
ColumnAttr('sheet'),
|
162
148
|
ColumnAttr('col'),
|
@@ -185,7 +171,7 @@ class CommandLogBase:
|
|
185
171
|
|
186
172
|
contains = lambda s, *substrs: any((a in s) for a in substrs)
|
187
173
|
if contains(cmd.execstr, 'pasteFromClipboard'):
|
188
|
-
args = vd.
|
174
|
+
args = vd.sysclipValue().strip()
|
189
175
|
|
190
176
|
|
191
177
|
comment = vd.currentReplayRow.comment if vd.currentReplayRow else cmd.helpstr
|
@@ -211,18 +197,10 @@ class CommandLogBase:
|
|
211
197
|
return
|
212
198
|
|
213
199
|
# remove user-aborted commands and simple movements (unless first command on the sheet, which created the sheet)
|
214
|
-
if not sheet.
|
200
|
+
if not sheet.cmdlog_sheet.rows or vd.isLoggableCommand(vd.activeCommand.longname):
|
215
201
|
if isLoggableSheet(sheet): # don't record actions from cmdlog or other internal sheets on global cmdlog
|
216
202
|
self.addRow(vd.activeCommand) # add to global cmdlog
|
217
203
|
sheet.cmdlog_sheet.addRow(vd.activeCommand) # add to sheet-specific cmdlog
|
218
|
-
if options.cmdlog_histfile:
|
219
|
-
name = date().strftime(options.cmdlog_histfile)
|
220
|
-
p = Path(name)
|
221
|
-
if not p.is_absolute():
|
222
|
-
p = Path(options.visidata_dir)/f'{name}.jsonl'
|
223
|
-
if not getattr(vd, 'sessionlog', None):
|
224
|
-
vd.sessionlog = vd.loadInternalSheet(CommandLog, p)
|
225
|
-
vd.sessionlog.append_tsv_row(vd.activeCommand)
|
226
204
|
|
227
205
|
vd.activeCommand = None
|
228
206
|
|
@@ -256,50 +234,27 @@ class CommandLogJsonl(CommandLogBase, JsonLinesSheet):
|
|
256
234
|
vd.paused = False
|
257
235
|
vd.currentReplay = None # CommandLog replaying currently
|
258
236
|
vd.currentReplayRow = None # must be global, to allow replay
|
259
|
-
vd.semaphore = threading.Semaphore(0)
|
260
|
-
|
261
|
-
|
262
|
-
@VisiData.api
|
263
|
-
def replay_pause(vd):
|
264
|
-
if not vd.currentReplay:
|
265
|
-
vd.fail('no replay to pause')
|
266
|
-
else:
|
267
|
-
if vd.paused:
|
268
|
-
vd.replay_advance()
|
269
|
-
vd.paused = not vd.paused
|
270
|
-
vd.status('paused' if vd.paused else 'resumed')
|
271
|
-
|
272
|
-
|
273
|
-
@VisiData.api
|
274
|
-
def replay_advance(vd):
|
275
|
-
vd.currentReplay or vd.fail("no replay to advance")
|
276
|
-
vd.semaphore.release()
|
277
237
|
|
278
238
|
|
279
239
|
@VisiData.api
|
280
240
|
def replay_cancel(vd):
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
vd.semaphore.release()
|
241
|
+
vd.currentReplayRow = None
|
242
|
+
vd.currentReplay = None
|
243
|
+
vd._nextCommands.clear()
|
285
244
|
|
286
245
|
|
287
246
|
@VisiData.api
|
288
247
|
def moveToReplayContext(vd, r, vs):
|
289
248
|
'set the sheet/row/col to the values in the replay row'
|
249
|
+
vs.ensureLoaded()
|
250
|
+
vd.sync()
|
290
251
|
vd.clearCaches()
|
252
|
+
|
291
253
|
if r.row not in [None, '']:
|
292
|
-
vs.moveToRow(r.row) or vd.error(f'no {r.row} row on {vs}')
|
254
|
+
vs.moveToRow(r.row) or vd.error(f'no "{r.row}" row on {vs}')
|
293
255
|
|
294
256
|
if r.col not in [None, '']:
|
295
|
-
vs.moveToCol(r.col) or vd.error(f'no {r.col} column on {vs}')
|
296
|
-
|
297
|
-
|
298
|
-
@VisiData.api
|
299
|
-
def delay(vd, factor=1):
|
300
|
-
'returns True if delay satisfied'
|
301
|
-
acquired = vd.semaphore.acquire(timeout=options.replay_wait*factor if not vd.paused else None)
|
302
|
-
return acquired or not vd.paused
|
257
|
+
vs.moveToCol(r.col) or vd.error(f'no "{r.col}" column on {vs}')
|
303
258
|
|
304
259
|
|
305
260
|
@VisiData.api
|
@@ -330,18 +285,21 @@ def replayOne(vd, r):
|
|
330
285
|
else:
|
331
286
|
vs = vs or vd.activeSheet
|
332
287
|
if vs:
|
333
|
-
vd.push
|
288
|
+
if vs in vd.sheets: # if already on sheet stack, push to top
|
289
|
+
vd.push(vs)
|
334
290
|
else:
|
335
291
|
vs = vd.cmdlog
|
336
292
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
293
|
+
try:
|
294
|
+
vd.moveToReplayContext(r, vs)
|
295
|
+
if r.comment:
|
296
|
+
vd.status(r.comment)
|
341
297
|
|
342
|
-
|
343
|
-
|
344
|
-
|
298
|
+
# <=v1.2 used keystrokes in longname column; getCommand fetches both
|
299
|
+
escaped = vs.execCommand(longname if longname else r.keystrokes, keystrokes=r.keystrokes)
|
300
|
+
except Exception as e:
|
301
|
+
vd.exceptionCaught(e)
|
302
|
+
escaped = True
|
345
303
|
|
346
304
|
vd.currentReplayRow = None
|
347
305
|
|
@@ -351,10 +309,21 @@ def replayOne(vd, r):
|
|
351
309
|
|
352
310
|
|
353
311
|
@VisiData.api
|
354
|
-
|
355
|
-
|
312
|
+
class DisableAsync:
|
313
|
+
def __enter__(self):
|
314
|
+
vd.execAsync = vd.execSync
|
315
|
+
|
316
|
+
def __exit__(self, exc_type, exc_val, tb):
|
317
|
+
vd.execAsync = lambda *args, vd=vd, **kwargs: visidata.VisiData.execAsync(vd, *args, **kwargs)
|
318
|
+
|
319
|
+
|
320
|
+
@VisiData.api
|
321
|
+
def replay_sync(vd, cmdlog):
|
322
|
+
'Replay all commands in *cmdlog*.'
|
323
|
+
with vd.DisableAsync():
|
356
324
|
cmdlog.cursorRowIndex = 0
|
357
325
|
vd.currentReplay = cmdlog
|
326
|
+
|
358
327
|
with Progress(total=len(cmdlog.rows)) as prog:
|
359
328
|
while cmdlog.cursorRowIndex < len(cmdlog.rows):
|
360
329
|
if vd.currentReplay is None:
|
@@ -377,22 +346,16 @@ def replay_sync(vd, cmdlog, live=False):
|
|
377
346
|
|
378
347
|
if vd.activeSheet:
|
379
348
|
vd.activeSheet.ensureLoaded()
|
380
|
-
vd.sync()
|
381
|
-
while not vd.delay():
|
382
|
-
pass
|
383
349
|
|
384
350
|
vd.status('replay complete')
|
385
351
|
vd.currentReplay = None
|
386
352
|
|
387
353
|
|
388
354
|
@VisiData.api
|
389
|
-
@asyncthread
|
390
355
|
def replay(vd, cmdlog):
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
thread.noblock = True
|
395
|
-
vd.replay_sync(cmdlog, live=True)
|
356
|
+
'Inject commands into live execution with interface.'
|
357
|
+
vd.push(cmdlog)
|
358
|
+
vd._nextCommands.extend(cmdlog.rows)
|
396
359
|
|
397
360
|
|
398
361
|
@VisiData.api
|
@@ -414,8 +377,9 @@ def setLastArgs(vd, args):
|
|
414
377
|
|
415
378
|
@VisiData.property
|
416
379
|
def replayStatus(vd):
|
417
|
-
|
418
|
-
return '
|
380
|
+
if vd._nextCommands:
|
381
|
+
return f' | [:status_replay] {len(vd._nextCommands)} {vd.options.disp_replay_play}[:]'
|
382
|
+
return ''
|
419
383
|
|
420
384
|
|
421
385
|
@BaseSheet.property
|
@@ -428,7 +392,14 @@ def cmdlog(sheet):
|
|
428
392
|
|
429
393
|
@BaseSheet.lazy_property
|
430
394
|
def cmdlog_sheet(sheet):
|
431
|
-
|
395
|
+
c = CommandLogJsonl(sheet.name+'_cmdlog', source=sheet, rows=[])
|
396
|
+
# copy over all existing globally set options
|
397
|
+
# you only need to do this for the first BaseSheet in a tree
|
398
|
+
if not isinstance(sheet.source, BaseSheet):
|
399
|
+
for r in vd.cmdlog.rows:
|
400
|
+
if r.sheet == 'global' and (r.longname == 'set-option') or (r.longname == 'unset-option'):
|
401
|
+
c.addRow(r)
|
402
|
+
return c
|
432
403
|
|
433
404
|
|
434
405
|
@BaseSheet.property
|
@@ -452,7 +423,7 @@ def shortcut(self):
|
|
452
423
|
def cmdlog(vd):
|
453
424
|
if not vd._cmdlog:
|
454
425
|
vd._cmdlog = CommandLogJsonl('cmdlog', rows=[]) # no reload
|
455
|
-
vd._cmdlog.
|
426
|
+
vd._cmdlog.resetCols()
|
456
427
|
vd.beforeExecHooks.append(vd._cmdlog.beforeExecHook)
|
457
428
|
return vd._cmdlog
|
458
429
|
|
@@ -489,27 +460,30 @@ BaseSheet.init('_shortcut')
|
|
489
460
|
globalCommand('gD', 'cmdlog-all', 'vd.push(vd.cmdlog)', 'open global CommandLog for all commands executed in current session')
|
490
461
|
globalCommand('D', 'cmdlog-sheet', 'vd.push(sheet.cmdlog)', "open current sheet's CommandLog with all other loose ends removed; includes commands from parent sheets")
|
491
462
|
globalCommand('zD', 'cmdlog-sheet-only', 'vd.push(sheet.cmdlog_sheet)', 'open CommandLog for current sheet with commands from parent sheets removed')
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
globalCommand('^K', 'replay-stop', 'vd.replay_cancel()', 'cancel current replay')
|
463
|
+
BaseSheet.addCommand('^D', 'save-cmdlog', 'saveSheets(inputPath("save cmdlog to: ", value=fnSuffix(name)), vd.cmdlog)', 'save CommandLog to filename.vdj file')
|
464
|
+
BaseSheet.bindkey('^N', 'no-op')
|
465
|
+
BaseSheet.addCommand('^K', 'replay-stop', 'vd.replay_cancel(); vd.warning("replay canceled")', 'cancel current replay')
|
496
466
|
|
497
467
|
globalCommand(None, 'show-status', 'status(input("status: "))', 'show given message on status line')
|
498
468
|
globalCommand('^V', 'show-version', 'status(__version_info__);', 'Show version and copyright information on status line')
|
499
469
|
globalCommand('z^V', 'check-version', 'checkVersion(input("require version: ", value=__version_info__))', 'check VisiData version against given version')
|
500
470
|
|
501
|
-
globalCommand(' ', 'exec-longname', 'exec_longname(inputLongname())', 'execute command by its longname')
|
502
|
-
|
503
471
|
CommandLog.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
|
504
472
|
CommandLog.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
|
505
|
-
CommandLog.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay')
|
506
473
|
|
507
474
|
CommandLogJsonl.addCommand('x', 'replay-row', 'vd.replayOne(cursorRow); status("replayed one row")', 'replay command in current row')
|
508
475
|
CommandLogJsonl.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog')
|
509
|
-
CommandLogJsonl.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay')
|
510
476
|
|
511
477
|
CommandLog.options.json_sort_keys = False
|
512
478
|
CommandLog.options.encoding = 'utf-8'
|
513
479
|
CommandLogJsonl.options.json_sort_keys = False
|
514
480
|
|
515
|
-
vd.addGlobals(
|
481
|
+
vd.addGlobals(CommandLogBase=CommandLogBase, CommandLogRow=CommandLogRow)
|
482
|
+
|
483
|
+
vd.addMenuItems('''
|
484
|
+
View > Command log > this sheet > cmdlog-sheet
|
485
|
+
View > Command log > this sheet only > cmdlog-sheet-only
|
486
|
+
View > Command log > all commands > cmdlog-all
|
487
|
+
System > Execute longname > exec-longname
|
488
|
+
Help > Version > show-version
|
489
|
+
''')
|
visidata/color.py
CHANGED
@@ -1,45 +1,83 @@
|
|
1
1
|
import curses
|
2
2
|
import functools
|
3
3
|
from copy import copy
|
4
|
+
from collections import namedtuple
|
5
|
+
from dataclasses import dataclass
|
4
6
|
|
5
|
-
from visidata import options, Extensible, drawcache, drawcache_property, VisiData
|
7
|
+
from visidata import vd, options, Extensible, drawcache, drawcache_property, VisiData
|
6
8
|
import visidata
|
7
|
-
from collections import namedtuple
|
8
9
|
|
9
|
-
__all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker']
|
10
|
+
__all__ = ['ColorAttr', 'colors', 'update_attr', 'ColorMaker', 'rgb_to_attr']
|
10
11
|
|
12
|
+
vd.help_color = '''Color syntax: `<attribute> <fg-color> on <bg-color>`
|
11
13
|
|
12
|
-
|
14
|
+
- attributes: [:bold]bold[/] [:underline]underline[/] [:italic]italic[/] [:reverse]reverse[/]
|
15
|
+
- colors: 0-255 or [:black on 238]black[/] [:red on 238]red[/] [:green on 238]green[/] [:yellow on 238]yellow[/] [:blue on 238]blue[/] [:magenta on 238]magenta[/] [:cyan on 238]cyan[/] [:white on 238]white[/]
|
16
|
+
- the second color is used as a fallback if the first color is not available
|
13
17
|
|
18
|
+
See [:onclick https://visidata.org/docs/colors]https://visidata.org/docs/colors[/] for more detailed info.
|
19
|
+
'''
|
14
20
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class ColorAttr:
|
24
|
+
fg:int = -1 # default is no foreground specified
|
25
|
+
bg:int = -1 # default is no background specified
|
26
|
+
attributes:int = 0 # default is no attributes
|
27
|
+
precedence:int = 0 # default is lowest priority
|
28
|
+
colorname:str = ''
|
29
|
+
|
30
|
+
def __init__(self, fg:int=-1, bg:int=-1, attributes:int=0, precedence:int=0, colorname:str=''):
|
31
|
+
assert fg < 256, fg
|
32
|
+
assert bg < 256, bg
|
33
|
+
self.fg = fg
|
34
|
+
self.bg = bg
|
35
|
+
self.attributes = attributes
|
36
|
+
self.precedence = precedence
|
37
|
+
self.colorname = colorname
|
38
|
+
|
39
|
+
def update(self, b:'ColorAttr', prec:int=10) -> 'ColorAttr':
|
40
|
+
return update_attr(self, b, prec)
|
41
|
+
|
42
|
+
@property
|
43
|
+
def attr(self) -> int:
|
44
|
+
a = colors._get_colorpair(self.fg, self.bg, self.colorname) | self.attributes
|
45
|
+
assert a >= 0, a
|
46
|
+
return a
|
47
|
+
|
48
|
+
def as_str(self) -> str:
|
49
|
+
attrnames = [attrname for attrname, attr in colors._attrs.items() if self.attributes & attr]
|
50
|
+
attrnames.append(f'{self.fg} on {self.bg}')
|
51
|
+
return ' '.join(attrnames)
|
52
|
+
|
53
|
+
|
54
|
+
def update_attr(oldattr:ColorAttr, updattr:ColorAttr, updprec:int=None) -> ColorAttr:
|
55
|
+
assert isinstance(updattr, ColorAttr), updattr
|
56
|
+
if updprec is None:
|
57
|
+
updprec = updattr.precedence
|
58
|
+
updfg = updattr.fg
|
59
|
+
updbg = updattr.bg
|
60
|
+
updattr = updattr.attributes
|
26
61
|
|
27
62
|
# starting values, work backwards
|
28
|
-
|
63
|
+
newfg = oldattr.fg
|
64
|
+
newbg = oldattr.bg
|
29
65
|
newattr = oldattr.attributes | updattr
|
30
66
|
newprec = oldattr.precedence
|
31
67
|
|
32
|
-
if
|
33
|
-
|
34
|
-
|
35
|
-
|
68
|
+
if newfg < 0 or (updfg >= 0 and updprec > newprec):
|
69
|
+
newfg = updfg
|
70
|
+
if newbg < 0 or (updbg >= 0 and updprec > newprec):
|
71
|
+
newbg = updbg
|
36
72
|
|
37
|
-
|
73
|
+
newprec = max(updprec, newprec)
|
74
|
+
|
75
|
+
return ColorAttr(newfg, newbg, newattr, newprec)
|
38
76
|
|
39
77
|
|
40
78
|
class ColorMaker:
|
41
79
|
def __init__(self):
|
42
|
-
self.color_pairs = {} # (fg,bg) -> (
|
80
|
+
self.color_pairs = {} # (fg,bg) -> (pairnum, colornamestr) (pairnum can be or'ed with other attrs)
|
43
81
|
self.color_cache = {} # colorname -> colorpair
|
44
82
|
|
45
83
|
@drawcache_property
|
@@ -47,30 +85,44 @@ class ColorMaker:
|
|
47
85
|
return {}
|
48
86
|
|
49
87
|
def setup(self):
|
50
|
-
|
88
|
+
try:
|
89
|
+
curses.use_default_colors()
|
90
|
+
except Exception as e:
|
91
|
+
pass
|
51
92
|
|
52
93
|
@drawcache_property
|
53
94
|
def colors(self):
|
54
95
|
'not computed until curses color has been initialized'
|
55
96
|
return {x[6:]:getattr(curses, x) for x in dir(curses) if x.startswith('COLOR_') and x != 'COLOR_PAIRS'}
|
56
97
|
|
57
|
-
def __getitem__(self,
|
58
|
-
|
98
|
+
def __getitem__(self, colorname:str) -> ColorAttr:
|
99
|
+
'colors["green"] or colors["foo"] returns parsed ColorAttr.'
|
100
|
+
return self.get_color(colorname)
|
59
101
|
|
60
|
-
def __getattr__(self, optname):
|
61
|
-
'colors.color_foo
|
62
|
-
return self.get_color(optname)
|
102
|
+
def __getattr__(self, optname) -> ColorAttr:
|
103
|
+
'colors.color_foo or colors.foo returns parsed ColorAttr.'
|
104
|
+
return self.get_color(optname)
|
63
105
|
|
64
106
|
@drawcache
|
65
107
|
def resolve_colors(self, colorstack):
|
66
108
|
'Returns the ColorAttr for the colorstack, a list of (prec, color_option_name) sorted highest-precedence color first.'
|
67
|
-
cattr = ColorAttr(
|
109
|
+
cattr = ColorAttr()
|
68
110
|
for prec, coloropt in colorstack:
|
69
111
|
c = self.get_color(coloropt)
|
70
112
|
cattr = update_attr(cattr, c, prec)
|
71
113
|
return cattr
|
72
114
|
|
73
|
-
def
|
115
|
+
def get_color(self, optname:str, precedence:int=0) -> ColorAttr:
|
116
|
+
'''Return ColorAttr for options.color_foo if *optname* of either "foo" or "color_foo",
|
117
|
+
Otherwise parse *optname* for colorstring like "bold 34 red on 135 blue".'''
|
118
|
+
r = self.colorcache.get(optname, None)
|
119
|
+
if r is None:
|
120
|
+
coloropt = vd.options._get(optname) or vd.options._get(f'color_{optname}')
|
121
|
+
colornamestr = coloropt.value if coloropt else optname
|
122
|
+
r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence)
|
123
|
+
return r
|
124
|
+
|
125
|
+
def _split_colorstr(self, colorstr):
|
74
126
|
'Return (fgstr, bgstr, attrlist) parsed from colorstr.'
|
75
127
|
fgbgattrs = ['', '', []] # fgstr, bgstr, attrlist
|
76
128
|
if not colorstr:
|
@@ -92,12 +144,19 @@ class ColorMaker:
|
|
92
144
|
if not fgbgattrs[i]: # keep first known color
|
93
145
|
if self._get_colornum(x) is not None: # only set known colors
|
94
146
|
fgbgattrs[i] = x
|
147
|
+
else:
|
148
|
+
fgbgattrs[i] = None
|
95
149
|
|
96
150
|
return fgbgattrs
|
97
151
|
|
98
|
-
def _get_colornum(self, colorname, default=-1):
|
152
|
+
def _get_colornum(self, colorname:'str|int', default:int=-1) -> int:
|
99
153
|
'Return terminal color number for colorname.'
|
100
|
-
if
|
154
|
+
if isinstance(colorname, int):
|
155
|
+
return colorname
|
156
|
+
|
157
|
+
if not colorname:
|
158
|
+
return default
|
159
|
+
|
101
160
|
r = self.color_cache.get(colorname, None)
|
102
161
|
if r is not None:
|
103
162
|
return r
|
@@ -119,41 +178,68 @@ class ColorMaker:
|
|
119
178
|
except ValueError: # Python 3.10+ issue #1227
|
120
179
|
return None
|
121
180
|
|
122
|
-
|
123
|
-
def _colornames_to_cattr(self, colornamestr, precedence=0):
|
124
|
-
fg, bg, attrlist = self.split_colorstr(colornamestr)
|
181
|
+
def _attrnames_to_num(self, attrnames:'list[str]') -> int:
|
125
182
|
attrs = 0
|
126
|
-
for attr in
|
183
|
+
for attr in attrnames:
|
127
184
|
attrs |= getattr(curses, 'A_'+attr.upper())
|
185
|
+
return attrs
|
128
186
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
187
|
+
@drawcache_property
|
188
|
+
def _attrs(self):
|
189
|
+
return {k[2:].lower():getattr(curses, k) for k in dir(curses) if k.startswith('A_') and k != 'A_ATTRIBUTES'}
|
190
|
+
|
191
|
+
@drawcache
|
192
|
+
def _colornames_to_cattr(self, colorname:str, precedence=0) -> ColorAttr:
|
193
|
+
fg, bg, attrlist = self._split_colorstr(colorname)
|
194
|
+
|
195
|
+
fg = self._get_colornum(fg)
|
196
|
+
bg = self._get_colornum(bg)
|
197
|
+
return ColorAttr(fg, bg,
|
198
|
+
self._attrnames_to_num(attrlist),
|
199
|
+
precedence, colorname)
|
200
|
+
|
201
|
+
def _get_colorpair(self, fg:'int|None', bg:'int|None', colorname:str) -> int:
|
202
|
+
pairnum, _ = self.color_pairs.get((fg, bg), (None, ''))
|
136
203
|
if pairnum is None:
|
137
204
|
if len(self.color_pairs) > 254:
|
138
205
|
self.color_pairs.clear() # start over
|
139
206
|
self.color_cache.clear()
|
140
207
|
pairnum = len(self.color_pairs)+1
|
208
|
+
if fg is None: fg = -1
|
209
|
+
if bg is None: bg = -1
|
141
210
|
try:
|
142
|
-
curses.init_pair(pairnum,
|
211
|
+
curses.init_pair(pairnum, fg, bg)
|
143
212
|
except curses.error as e:
|
144
|
-
return
|
145
|
-
self.color_pairs[
|
213
|
+
return 0 # do not cache
|
214
|
+
self.color_pairs[(fg, bg)] = (pairnum, colorname)
|
146
215
|
|
147
|
-
|
148
|
-
return ColorAttr(color, attrs, precedence, color | attrs)
|
216
|
+
return curses.color_pair(pairnum)
|
149
217
|
|
150
|
-
def get_color(self, optname, precedence=0):
|
151
|
-
'colors.color_foo returns colors[options.color_foo]'
|
152
|
-
r = self.colorcache.get(optname, None)
|
153
|
-
if r is None:
|
154
|
-
coloropt = options._get(optname)
|
155
|
-
colornamestr = coloropt.value if coloropt else optname
|
156
|
-
r = self.colorcache[optname] = self._colornames_to_cattr(colornamestr, precedence)
|
157
|
-
return r
|
158
218
|
|
159
219
|
colors = ColorMaker()
|
220
|
+
|
221
|
+
def rgb_to_xterm256(r:int,g:int,b:int,a:int=255) -> int:
|
222
|
+
if a == 0:
|
223
|
+
return -1
|
224
|
+
|
225
|
+
if max(r,g,b) - min(r,g,b) < 8:
|
226
|
+
if r <= 4: return 16
|
227
|
+
elif r <= 8: return 232
|
228
|
+
elif r >= 247: return 231
|
229
|
+
elif r >= 238: return 255
|
230
|
+
else:
|
231
|
+
return int(232 + (r-8)//10)
|
232
|
+
else:
|
233
|
+
r = max(0, r-(95-40)) // 40
|
234
|
+
g = max(0, g-(95-40)) // 40
|
235
|
+
b = max(0, b-(95-40)) // 40
|
236
|
+
return int(16 + r*36 + g*6 + b)
|
237
|
+
|
238
|
+
|
239
|
+
@functools.lru_cache(256)
|
240
|
+
def rgb_to_attr(r:int,g:int,b:int,a:int=255) -> str:
|
241
|
+
return str(rgb_to_xterm256(r,g,b,a))
|
242
|
+
|
243
|
+
|
244
|
+
import sys
|
245
|
+
vd.addGlobals({k:getattr(sys.modules[__name__], k) for k in __all__})
|