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/_input.py
CHANGED
@@ -3,25 +3,48 @@ import curses
|
|
3
3
|
|
4
4
|
import visidata
|
5
5
|
|
6
|
-
from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData
|
7
|
-
from visidata import vd, options, colors
|
8
|
-
from visidata import
|
6
|
+
from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData, BaseSheet
|
7
|
+
from visidata import vd, options, colors, dispwidth, ColorAttr
|
8
|
+
from visidata import AttrDict
|
9
9
|
|
10
10
|
|
11
|
-
vd.
|
12
|
-
vd.
|
13
|
-
vd.
|
11
|
+
vd.theme_option('color_edit_unfocused', '238 on 110', 'display color for unfocused input in form')
|
12
|
+
vd.theme_option('color_edit_cell', '233 on 110', 'cell color to use when editing cell')
|
13
|
+
vd.theme_option('disp_edit_fill', '_', 'edit field fill character')
|
14
|
+
vd.theme_option('disp_unprintable', '·', 'substitute character for unprintables')
|
15
|
+
vd.theme_option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None)
|
14
16
|
|
15
|
-
vd.
|
17
|
+
vd.disp_help = 1 # current level of help shown (up to vd.options.disp_help as maximum)
|
16
18
|
|
17
19
|
class AcceptInput(Exception):
|
18
20
|
'*args[0]* is the input to be accepted'
|
19
21
|
|
20
|
-
|
22
|
+
vd._injectedInput = None # for vd.injectInput
|
23
|
+
|
24
|
+
|
25
|
+
@VisiData.api
|
26
|
+
def injectInput(vd, x):
|
27
|
+
'Use *x* as input to next command.'
|
28
|
+
assert vd._injectedInput is None, vd._injectedInput
|
29
|
+
vd._injectedInput = x
|
30
|
+
|
21
31
|
|
22
32
|
@VisiData.api
|
23
|
-
def
|
24
|
-
vd.
|
33
|
+
def getCommandInput(vd):
|
34
|
+
if vd._injectedInput is not None:
|
35
|
+
r = vd._injectedInput
|
36
|
+
vd._injectedInput = None
|
37
|
+
return r
|
38
|
+
|
39
|
+
return vd.getLastArgs()
|
40
|
+
|
41
|
+
|
42
|
+
@BaseSheet.after
|
43
|
+
def execCommand(sheet, longname, *args, **kwargs):
|
44
|
+
if vd._injectedInput is not None:
|
45
|
+
vd.debug(f'{longname} did not consume input "{vd._injectedInput}"')
|
46
|
+
vd._injectedInput = None
|
47
|
+
|
25
48
|
|
26
49
|
def acceptThenFunc(*longnames):
|
27
50
|
def _acceptthen(v, i):
|
@@ -52,10 +75,12 @@ def until_get_wch(scr):
|
|
52
75
|
ret = None
|
53
76
|
while not ret:
|
54
77
|
try:
|
55
|
-
ret =
|
78
|
+
ret = vd.get_wch(scr)
|
56
79
|
except curses.error:
|
57
80
|
pass
|
58
81
|
|
82
|
+
if isinstance(ret, int):
|
83
|
+
return chr(ret)
|
59
84
|
return ret
|
60
85
|
|
61
86
|
|
@@ -64,6 +89,48 @@ def splice(v:str, i:int, s:str):
|
|
64
89
|
return v if i < 0 else v[:i] + s + v[i:]
|
65
90
|
|
66
91
|
|
92
|
+
# vd.options.disp_help is the effective maximum disp_help. The user can cycle through the various levels of help.
|
93
|
+
class HelpCycler:
|
94
|
+
def __init__(self, scr=None, help=''):
|
95
|
+
self.help = help
|
96
|
+
self.scr = scr
|
97
|
+
|
98
|
+
def __enter__(self):
|
99
|
+
if self.scr:
|
100
|
+
vd.drawInputHelp(self.scr, self.help)
|
101
|
+
|
102
|
+
return self
|
103
|
+
|
104
|
+
def __exit__(self, *args):
|
105
|
+
pass
|
106
|
+
|
107
|
+
def cycle(self):
|
108
|
+
vd.disp_help = (vd.disp_help-1)%(vd.options.disp_help+1)
|
109
|
+
if self.scr:
|
110
|
+
vd.drawInputHelp(self.scr, self.help)
|
111
|
+
|
112
|
+
|
113
|
+
@VisiData.api
|
114
|
+
def drawInputHelp(vd, scr, help:str=''):
|
115
|
+
if not scr or not vd.cursesEnabled:
|
116
|
+
return
|
117
|
+
|
118
|
+
sheet = vd.activeSheet
|
119
|
+
if not sheet:
|
120
|
+
return
|
121
|
+
vd.drawSheet(scr, sheet)
|
122
|
+
|
123
|
+
curhelp = ''
|
124
|
+
if vd.disp_help == 0:
|
125
|
+
vd.drawSidebar(scr, sheet)
|
126
|
+
elif vd.disp_help == 1:
|
127
|
+
curhelp = help
|
128
|
+
sheet.drawSidebarText(scr, curhelp)
|
129
|
+
elif vd.disp_help >= 2:
|
130
|
+
curhelp = vd.getHelpPane('input', module='visidata')
|
131
|
+
sheet.drawSidebarText(scr, curhelp, title='Input Keystrokes Help')
|
132
|
+
|
133
|
+
|
67
134
|
def clean_printable(s):
|
68
135
|
'Escape unprintable characters.'
|
69
136
|
return ''.join(c if c.isprintable() else options.disp_unprintable for c in str(s))
|
@@ -138,14 +205,26 @@ class HistoryState:
|
|
138
205
|
|
139
206
|
# history: earliest entry first
|
140
207
|
@VisiData.api
|
141
|
-
def editline(vd, scr, y, x, w, i=0,
|
208
|
+
def editline(vd, scr, y, x, w, i=0,
|
209
|
+
attr=ColorAttr(),
|
210
|
+
value='',
|
211
|
+
fillchar=' ',
|
212
|
+
truncchar='-',
|
213
|
+
unprintablechar='.',
|
214
|
+
completer=lambda text,idx: None,
|
215
|
+
history=[],
|
216
|
+
display=True,
|
217
|
+
updater=lambda val: None,
|
218
|
+
bindings={},
|
219
|
+
help='', # str|HelpPane
|
220
|
+
clear=True):
|
142
221
|
'''A better curses line editing widget.
|
143
222
|
If *clear* is True, clear whole editing area before displaying.
|
144
223
|
'''
|
145
224
|
with EnableCursor():
|
225
|
+
with HelpCycler(scr, help) as disp_help:
|
146
226
|
ESC='^['
|
147
227
|
TAB='^I'
|
148
|
-
|
149
228
|
history_state = HistoryState(history)
|
150
229
|
complete_state = CompleteState(completer)
|
151
230
|
insert_mode = True
|
@@ -176,10 +255,8 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
|
|
176
255
|
a += incr
|
177
256
|
return min(max(a, 0), len(s))
|
178
257
|
|
179
|
-
|
180
258
|
while True:
|
181
259
|
updater(v)
|
182
|
-
vd.getHelpPane('input', module=__name__).draw(scr, y=y)
|
183
260
|
|
184
261
|
if display:
|
185
262
|
dispval = clean_printable(v)
|
@@ -202,11 +279,12 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
|
|
202
279
|
k = 1 if w%2==0 else 0 # odd widths have one character more
|
203
280
|
dispval = left_truncchar + dispval[i-w//2+1:i+w//2-k] + right_truncchar
|
204
281
|
|
205
|
-
prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear)
|
206
|
-
clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear)
|
207
|
-
scr.move(y, x+prew)
|
282
|
+
prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear, literal=True)
|
283
|
+
clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear, literal=True)
|
284
|
+
if scr: scr.move(y, x+prew)
|
208
285
|
ch = vd.getkeystroke(scr)
|
209
286
|
if ch == '': continue
|
287
|
+
elif ch in bindings: v, i = bindings[ch](v, i)
|
210
288
|
elif ch == 'KEY_IC': insert_mode = not insert_mode
|
211
289
|
elif ch == '^A' or ch == 'KEY_HOME': i = 0
|
212
290
|
elif ch == '^B' or ch == 'KEY_LEFT': i -= 1
|
@@ -214,20 +292,29 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
|
|
214
292
|
elif ch == '^D' or ch == 'KEY_DC': v = delchar(v, i)
|
215
293
|
elif ch == '^E' or ch == 'KEY_END': i = len(v)
|
216
294
|
elif ch == '^F' or ch == 'KEY_RIGHT': i += 1
|
217
|
-
elif ch == '^G':
|
295
|
+
elif ch == '^G':
|
296
|
+
disp_help.cycle()
|
297
|
+
continue # not considered a first keypress
|
218
298
|
elif ch in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i)
|
219
299
|
elif ch == TAB: v, i = complete_state.complete(v, i, +1)
|
220
300
|
elif ch == 'KEY_BTAB': v, i = complete_state.complete(v, i, -1)
|
221
301
|
elif ch in ['^J', '^M']: break # ENTER to accept value
|
222
302
|
elif ch == '^K': v = v[:i] # ^Kill to end-of-line
|
223
|
-
elif ch == '^
|
303
|
+
elif ch == '^N':
|
304
|
+
c = ''
|
305
|
+
while not c:
|
306
|
+
c = vd.getkeystroke(scr)
|
307
|
+
c = vd.prettykeys(c)
|
308
|
+
i += len(c)
|
309
|
+
v += c
|
310
|
+
elif ch == '^O': v = vd.launchExternalEditor(v); break
|
224
311
|
elif ch == '^R': v = str(value) # ^Reload initial value
|
225
312
|
elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
|
226
313
|
elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
|
227
314
|
elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
|
228
315
|
elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word
|
229
316
|
elif ch == '^Y': v = splice(v, i, str(vd.memory.clipval))
|
230
|
-
elif ch == '^Z': suspend()
|
317
|
+
elif ch == '^Z': vd.suspend()
|
231
318
|
# CTRL+arrow
|
232
319
|
elif ch == 'kLFT5': i = find_nonword(v, 0, i-1, -1)+1; # word left
|
233
320
|
elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
|
@@ -235,7 +322,6 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
|
|
235
322
|
elif ch == 'kDN5': pass
|
236
323
|
elif history and ch == 'KEY_UP': v, i = history_state.up(v, i)
|
237
324
|
elif history and ch == 'KEY_DOWN': v, i = history_state.down(v, i)
|
238
|
-
elif ch in bindings: v, i = bindings[ch](v, i)
|
239
325
|
elif len(ch) > 1: pass
|
240
326
|
else:
|
241
327
|
if first_action:
|
@@ -254,7 +340,7 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
|
|
254
340
|
first_action = False
|
255
341
|
complete_state.reset()
|
256
342
|
|
257
|
-
return
|
343
|
+
return v
|
258
344
|
|
259
345
|
|
260
346
|
@VisiData.api
|
@@ -262,7 +348,7 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
|
|
262
348
|
'Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string.'
|
263
349
|
v = None
|
264
350
|
if record and vd.cmdlog:
|
265
|
-
v = vd.
|
351
|
+
v = vd.getCommandInput()
|
266
352
|
|
267
353
|
if v is None:
|
268
354
|
try:
|
@@ -270,27 +356,32 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
|
|
270
356
|
except AcceptInput as e:
|
271
357
|
v = e.args[0]
|
272
358
|
|
273
|
-
if vd.
|
359
|
+
if vd.cursesEnabled:
|
274
360
|
# clear keyboard buffer to neutralize multi-line pastes (issue#585)
|
275
361
|
curses.flushinp()
|
276
362
|
|
277
363
|
if display:
|
278
|
-
vd.status('"%s"' % v)
|
279
364
|
if record and vd.cmdlog:
|
280
365
|
vd.setLastArgs(v)
|
281
366
|
|
282
|
-
|
367
|
+
if 'value' in kwargs:
|
368
|
+
starting_value = kwargs['value']
|
369
|
+
if isinstance(starting_value, (int, float)) and v[-1] == '%': #2082
|
370
|
+
pct = float(v[:-1])
|
371
|
+
v = pct*starting_value/100
|
372
|
+
|
373
|
+
v = type(starting_value)(v)
|
283
374
|
|
375
|
+
return v
|
284
376
|
|
285
377
|
@VisiData.api
|
286
378
|
def inputsingle(vd, prompt, record=True):
|
287
379
|
'Display prompt and return single character of user input.'
|
288
380
|
sheet = vd.activeSheet
|
289
|
-
rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
|
290
381
|
|
291
382
|
v = None
|
292
383
|
if record and vd.cmdlog:
|
293
|
-
v = vd.
|
384
|
+
v = vd.getCommandInput()
|
294
385
|
|
295
386
|
if v is not None:
|
296
387
|
return v
|
@@ -309,9 +400,102 @@ def inputsingle(vd, prompt, record=True):
|
|
309
400
|
|
310
401
|
return v
|
311
402
|
|
403
|
+
@VisiData.api
|
404
|
+
def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
405
|
+
'A simple form, where each input is an entry in `kwargs`, with the key being the key in the returned dict, and the value being a dictionary of kwargs to the singular input().'
|
406
|
+
sheet = vd.activeSheet
|
407
|
+
scr = sheet._scr
|
408
|
+
|
409
|
+
previnput = vd.getCommandInput()
|
410
|
+
if previnput is not None:
|
411
|
+
if isinstance(previnput, str):
|
412
|
+
if previnput.startswith('{'):
|
413
|
+
return json.loads(previnput)
|
414
|
+
else:
|
415
|
+
ret = {k:v.get('value', '') for k,v in kwargs.items()}
|
416
|
+
primekey = list(ret.keys())[0]
|
417
|
+
ret[primekey] = previnput
|
418
|
+
return ret
|
419
|
+
|
420
|
+
if isinstance(previnput, dict):
|
421
|
+
return previnput
|
422
|
+
|
423
|
+
assert False, type(previnput)
|
424
|
+
|
425
|
+
y = sheet.windowHeight-1
|
426
|
+
maxw = sheet.windowWidth//2
|
427
|
+
attr = colors.color_edit_unfocused
|
428
|
+
|
429
|
+
keys = list(kwargs.keys())
|
430
|
+
cur_input_key = keys[0]
|
431
|
+
|
432
|
+
if scr:
|
433
|
+
scr.erase()
|
434
|
+
|
435
|
+
for i, (k, v) in enumerate(kwargs.items()):
|
436
|
+
v['dy'] = i
|
437
|
+
v['w'] = maxw-dispwidth(v.get('prompt'))
|
438
|
+
|
439
|
+
class ChangeInput(Exception):
|
440
|
+
pass
|
441
|
+
|
442
|
+
def change_input(offset):
|
443
|
+
def _throw(v, i):
|
444
|
+
if scr:
|
445
|
+
scr.erase()
|
446
|
+
raise ChangeInput(v, offset)
|
447
|
+
return _throw
|
448
|
+
|
449
|
+
def _drawPrompt(val):
|
450
|
+
for k, v in kwargs.items():
|
451
|
+
maxw = min(sheet.windowWidth-1, max(dispwidth(v.get('prompt')), dispwidth(str(v.get('value', '')))))
|
452
|
+
promptlen = clipdraw(scr, y-v.get('dy'), 0, v.get('prompt'), attr, w=maxw) #1947
|
453
|
+
promptlen = clipdraw(scr, y-v.get('dy'), promptlen, v.get('value', ''), attr, w=maxw)
|
454
|
+
|
455
|
+
return updater(val)
|
456
|
+
|
457
|
+
with HelpCycler() as disp_help:
|
458
|
+
while True:
|
459
|
+
try:
|
460
|
+
input_kwargs = kwargs[cur_input_key]
|
461
|
+
input_kwargs['value'] = vd.input(**input_kwargs,
|
462
|
+
attr=colors.color_edit_cell,
|
463
|
+
updater=_drawPrompt,
|
464
|
+
record=False,
|
465
|
+
bindings={
|
466
|
+
'KEY_BTAB': change_input(-1),
|
467
|
+
'^I': change_input(+1),
|
468
|
+
'KEY_SR': change_input(-1),
|
469
|
+
'KEY_SF': change_input(+1),
|
470
|
+
'kUP': change_input(-1),
|
471
|
+
'kDN': change_input(+1),
|
472
|
+
})
|
473
|
+
break
|
474
|
+
except ChangeInput as e:
|
475
|
+
input_kwargs['value'] = e.args[0]
|
476
|
+
offset = e.args[1]
|
477
|
+
i = keys.index(cur_input_key)
|
478
|
+
cur_input_key = keys[(i+offset)%len(keys)]
|
479
|
+
|
480
|
+
retargs = {}
|
481
|
+
if record:
|
482
|
+
lastargs = {}
|
483
|
+
for k, input_kwargs in kwargs.items():
|
484
|
+
v = input_kwargs.get('value', '')
|
485
|
+
retargs[k] = v
|
486
|
+
|
487
|
+
if input_kwargs.get('display', True):
|
488
|
+
lastargs[k] = v
|
489
|
+
vd.addInputHistory(v, input_kwargs.get('type', ''))
|
490
|
+
|
491
|
+
if vd.cmdlog and lastargs:
|
492
|
+
vd.setLastArgs(lastargs)
|
493
|
+
|
494
|
+
return retargs
|
495
|
+
|
312
496
|
|
313
497
|
@VisiData.api
|
314
|
-
def input(
|
498
|
+
def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None, updater=lambda v: None, **kwargs):
|
315
499
|
'''Display *prompt* and return line of user input.
|
316
500
|
|
317
501
|
- *type*: string indicating the type of input to use for history.
|
@@ -322,23 +506,47 @@ def input(self, prompt, type=None, defaultLast=False, history=[], **kwargs):
|
|
322
506
|
- *completer*: ``completer(val, idx)`` is called on TAB to get next completed value.
|
323
507
|
- *updater*: ``updater(val)`` is called every keypress or timeout.
|
324
508
|
- *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
|
509
|
+
- *dy*: number of lines from bottom of pane
|
510
|
+
- *attr*: curses attribute for prompt
|
511
|
+
- *help*: string to include in help
|
325
512
|
'''
|
326
513
|
|
327
|
-
|
514
|
+
if attr is None:
|
515
|
+
attr = ColorAttr()
|
516
|
+
sheet = vd.activeSheet
|
517
|
+
if not vd.cursesEnabled:
|
518
|
+
if kwargs.get('record', True) and vd.cmdlog:
|
519
|
+
return vd.getCommandInput()
|
520
|
+
|
521
|
+
if kwargs.get('display', True):
|
522
|
+
import builtins
|
523
|
+
return builtins.input(prompt)
|
524
|
+
else:
|
525
|
+
import getpass
|
526
|
+
return getpass.getpass(prompt)
|
527
|
+
|
528
|
+
history = list(vd.inputHistory.setdefault(type, {}).keys())
|
529
|
+
|
530
|
+
y = sheet.windowHeight-dy-1
|
531
|
+
promptlen = dispwidth(prompt)
|
328
532
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
533
|
+
def _drawPrompt(val=''):
|
534
|
+
rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
|
535
|
+
clipdraw(sheet._scr, y, 0, prompt, attr, w=sheet.windowWidth-rstatuslen-1)
|
536
|
+
updater(val)
|
537
|
+
return sheet.windowWidth-promptlen-rstatuslen-2
|
538
|
+
|
539
|
+
w = kwargs.pop('w', _drawPrompt())
|
540
|
+
ret = vd.editText(y, promptlen, w=w,
|
334
541
|
attr=colors.color_edit_cell,
|
335
542
|
unprintablechar=options.disp_unprintable,
|
336
543
|
truncchar=options.disp_truncator,
|
337
544
|
history=history,
|
545
|
+
updater=_drawPrompt,
|
338
546
|
**kwargs)
|
339
547
|
|
340
548
|
if ret:
|
341
|
-
|
549
|
+
vd.addInputHistory(ret, type=type)
|
342
550
|
|
343
551
|
elif defaultLast:
|
344
552
|
history or vd.fail("no previous input")
|
@@ -350,7 +558,7 @@ def input(self, prompt, type=None, defaultLast=False, history=[], **kwargs):
|
|
350
558
|
@VisiData.api
|
351
559
|
def confirm(vd, prompt, exc=EscapeException):
|
352
560
|
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
|
353
|
-
if options.batch:
|
561
|
+
if options.batch and not options.interactive:
|
354
562
|
return vd.fail('cannot confirm in batch mode: ' + prompt)
|
355
563
|
|
356
564
|
yn = vd.input(prompt, value='no', record=False)[:1]
|
@@ -403,17 +611,26 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
403
611
|
'KEY_SF': acceptThenFunc('go-down', 'rename-col' if rowidx < 0 else 'edit-cell'),
|
404
612
|
'KEY_SRIGHT': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
|
405
613
|
'KEY_SLEFT': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
|
614
|
+
'^I': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
|
615
|
+
'KEY_BTAB': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
|
406
616
|
}
|
407
617
|
|
618
|
+
if vcolidx >= self.nVisibleCols-1:
|
619
|
+
bindings['^I'] = acceptThenFunc('go-down', 'go-leftmost', 'edit-cell')
|
620
|
+
|
621
|
+
if vcolidx <= 0:
|
622
|
+
bindings['KEY_BTAB'] = acceptThenFunc('go-up', 'go-rightmost', 'edit-cell')
|
623
|
+
|
624
|
+
# update local bindings with kwargs.bindings instead of the inverse, to preserve kwargs.bindings for caller
|
408
625
|
bindings.update(kwargs.get('bindings', {}))
|
409
626
|
kwargs['bindings'] = bindings
|
410
627
|
|
411
628
|
editargs = dict(value=value,
|
412
|
-
fillchar=options.disp_edit_fill,
|
413
|
-
truncchar=options.disp_truncator)
|
629
|
+
fillchar=self.options.disp_edit_fill,
|
630
|
+
truncchar=self.options.disp_truncator)
|
414
631
|
|
415
632
|
editargs.update(kwargs) # update with user-specified args
|
416
|
-
r = vd.editText(y, x, w, **editargs)
|
633
|
+
r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
|
417
634
|
|
418
635
|
if rowidx >= 0: # if not header
|
419
636
|
r = col.type(r) # convert input to column type, let exceptions be raised
|
@@ -421,4 +638,4 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
421
638
|
return r
|
422
639
|
|
423
640
|
|
424
|
-
vd.addGlobals({'CompleteKey': CompleteKey})
|
641
|
+
vd.addGlobals({'CompleteKey': CompleteKey, 'AcceptInput': AcceptInput})
|
visidata/_open.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
import os
|
2
|
+
import os.path
|
3
|
+
|
4
|
+
from visidata import VisiData, vd, Path, BaseSheet, TableSheet, TextSheet, SettableColumn
|
2
5
|
|
3
6
|
|
4
7
|
vd.option('filetype', '', 'specify file type', replay=True)
|
@@ -6,7 +9,13 @@ vd.option('filetype', '', 'specify file type', replay=True)
|
|
6
9
|
|
7
10
|
@VisiData.api
|
8
11
|
def inputFilename(vd, prompt, *args, **kwargs):
|
9
|
-
|
12
|
+
completer= _completeFilename
|
13
|
+
if not vd.couldOverwrite(): #1805 don't suggest an existing file
|
14
|
+
completer = None
|
15
|
+
v = kwargs.get('value', '')
|
16
|
+
if v and Path(v).exists():
|
17
|
+
kwargs['value'] = ''
|
18
|
+
return vd.input(prompt, type="filename", *args, completer=completer, **kwargs).strip()
|
10
19
|
|
11
20
|
|
12
21
|
@VisiData.api
|
@@ -35,6 +44,38 @@ def _completeFilename(val, state):
|
|
35
44
|
return files[state%len(files)]
|
36
45
|
|
37
46
|
|
47
|
+
@VisiData.api
|
48
|
+
def guessFiletype(vd, p, *args, funcprefix='guess_'):
|
49
|
+
'''Call all vd.guess_<filetype>(p) functions and return best candidate sheet based on file contents.'''
|
50
|
+
|
51
|
+
guessfuncs = [getattr(vd, x) for x in dir(vd) if x.startswith(funcprefix)]
|
52
|
+
filetypes = []
|
53
|
+
for f in guessfuncs:
|
54
|
+
try:
|
55
|
+
filetype = f(p, *args)
|
56
|
+
if filetype:
|
57
|
+
filetype['_guesser'] = f.__name__
|
58
|
+
filetypes.append(filetype)
|
59
|
+
except FileNotFoundError:
|
60
|
+
pass
|
61
|
+
except Exception as e:
|
62
|
+
vd.debug(f'{f.__name__}: {e}')
|
63
|
+
|
64
|
+
if filetypes:
|
65
|
+
return sorted(filetypes, key=lambda r: -r.get('_likelihood', 1))[0]
|
66
|
+
|
67
|
+
return {}
|
68
|
+
|
69
|
+
|
70
|
+
@VisiData.api
|
71
|
+
def guess_extension(vd, path):
|
72
|
+
# try auto-detect from extension
|
73
|
+
ext = path.suffix[1:].lower()
|
74
|
+
openfunc = getattr(vd, f'open_{ext}', vd.getGlobals().get(f'open_{ext}'))
|
75
|
+
if openfunc:
|
76
|
+
return dict(filetype=ext, _likelihood=3)
|
77
|
+
|
78
|
+
|
38
79
|
@VisiData.api
|
39
80
|
def openPath(vd, p, filetype=None, create=False):
|
40
81
|
'''Call ``open_<filetype>(p)`` or ``openurl_<p.scheme>(p, filetype)``. Return constructed but unloaded sheet of appropriate type.
|
@@ -53,27 +94,43 @@ def openPath(vd, p, filetype=None, create=False):
|
|
53
94
|
return None
|
54
95
|
|
55
96
|
if not filetype:
|
56
|
-
|
57
|
-
filetype = 'dir'
|
58
|
-
else:
|
59
|
-
filetype = p.ext or options.filetype or 'txt'
|
97
|
+
filetype = p.ext or vd.options.filetype
|
60
98
|
|
61
99
|
filetype = filetype.lower()
|
62
100
|
|
63
101
|
if not p.exists():
|
64
|
-
if not create:
|
65
|
-
return None
|
66
102
|
newfunc = getattr(vd, 'new_' + filetype, vd.getGlobals().get('new_' + filetype))
|
67
103
|
if not newfunc:
|
68
104
|
vd.warning('%s does not exist, creating new sheet' % p)
|
69
|
-
return vd.newSheet(p.
|
105
|
+
return vd.newSheet(p.base_stem, 1, source=p)
|
70
106
|
|
71
107
|
vd.status('creating blank %s' % (p.given))
|
72
108
|
return newfunc(p)
|
73
109
|
|
74
|
-
|
110
|
+
if p.is_fifo():
|
111
|
+
# read the file as text, into a RepeatFile that can be opened multiple times
|
112
|
+
p = Path(p.given, fp=p.open(mode='rb'))
|
113
|
+
|
114
|
+
openfuncname = 'open_' + filetype
|
115
|
+
openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
|
75
116
|
if not openfunc:
|
117
|
+
opts = vd.guessFiletype(p)
|
118
|
+
if opts and 'filetype' in opts:
|
119
|
+
filetype = opts['filetype']
|
120
|
+
openfuncname = 'open_' + filetype
|
121
|
+
openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
|
122
|
+
if not openfunc:
|
123
|
+
vd.error(f'guessed {filetype} but no {openfuncname}')
|
124
|
+
|
125
|
+
vs = openfunc(p)
|
126
|
+
for k, v in opts.items():
|
127
|
+
if k != 'filetype' and not k.startswith('_'):
|
128
|
+
setattr(vs.options, k, v)
|
129
|
+
vd.warning('guessed "%s" filetype based on contents' % opts['filetype'])
|
130
|
+
return vs
|
131
|
+
|
76
132
|
vd.warning('unknown "%s" filetype' % filetype)
|
133
|
+
|
77
134
|
filetype = 'txt'
|
78
135
|
openfunc = vd.open_txt
|
79
136
|
|
@@ -81,13 +138,16 @@ def openPath(vd, p, filetype=None, create=False):
|
|
81
138
|
|
82
139
|
return openfunc(p)
|
83
140
|
|
84
|
-
|
85
141
|
@VisiData.api
|
86
142
|
def openSource(vd, p, filetype=None, create=False, **kwargs):
|
87
143
|
'''Return unloaded sheet object for *p* opened as the given *filetype* and with *kwargs* as option overrides. *p* can be a Path or a string (filename, url, or "-" for stdin).
|
88
144
|
when true, *create* will return a blank sheet, if file does not exist.'''
|
89
|
-
|
90
|
-
|
145
|
+
|
146
|
+
if isinstance(p, BaseSheet):
|
147
|
+
return p
|
148
|
+
|
149
|
+
filetype = filetype or vd.options.getonly('filetype', str(p), '') #1710
|
150
|
+
filetype = filetype or vd.options.getonly('filetype', 'global', '')
|
91
151
|
|
92
152
|
vs = None
|
93
153
|
if isinstance(p, str):
|
@@ -111,26 +171,21 @@ def openSource(vd, p, filetype=None, create=False, **kwargs):
|
|
111
171
|
def open_txt(vd, p):
|
112
172
|
'Create sheet from `.txt` file at Path `p`, checking whether it is TSV.'
|
113
173
|
if p.exists(): #1611
|
114
|
-
with p.
|
174
|
+
with p.open(encoding=vd.options.encoding) as fp:
|
175
|
+
delimiter = vd.options.delimiter
|
115
176
|
try:
|
116
|
-
if
|
177
|
+
if delimiter and delimiter in next(fp): # peek at the first line
|
117
178
|
return vd.open_tsv(p) # TSV often have .txt extension
|
118
179
|
except StopIteration:
|
119
|
-
return
|
120
|
-
return TextSheet(p.
|
121
|
-
|
122
|
-
|
123
|
-
@VisiData.api
|
124
|
-
def loadInternalSheet(vd, cls, p, **kwargs):
|
125
|
-
'Load internal sheet of given class.'
|
126
|
-
vs = cls(p.name, source=p, **kwargs)
|
127
|
-
options._set('encoding', 'utf8', vs)
|
128
|
-
if p.exists():
|
129
|
-
# vd.sheets.insert(0, vs) # broke replay with macros.reload()
|
130
|
-
vs.reload.__wrapped__(vs)
|
131
|
-
# vd.sheets.pop(0)
|
132
|
-
return vs
|
180
|
+
return TableSheet(p.base_stem, columns=[SettableColumn()], source=p)
|
181
|
+
return TextSheet(p.base_stem, source=p)
|
133
182
|
|
134
183
|
|
135
184
|
BaseSheet.addCommand('o', 'open-file', 'vd.push(openSource(inputFilename("open: "), create=True))', 'Open file or URL')
|
136
185
|
TableSheet.addCommand('zo', 'open-cell-file', 'vd.push(openSource(cursorDisplay) or fail(f"file {cursorDisplay} does not exist"))', 'Open file or URL from path in current cell')
|
186
|
+
BaseSheet.addCommand('gU', 'undo-last-quit', 'push(allSheets[-1])', 'reopen most recently closed sheet')
|
187
|
+
|
188
|
+
vd.addMenuItems('''
|
189
|
+
File > Open > input file/url > open-file
|
190
|
+
File > Reopen last closed > undo-last-quit
|
191
|
+
''')
|