visidata 3.0.1__py3-none-any.whl → 3.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 +12 -10
- visidata/_input.py +208 -199
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +9 -11
- visidata/apps/vdsql/clickhouse.py +2 -2
- visidata/apps/vdsql/snowflake.py +1 -1
- visidata/apps/vgit/status.py +1 -1
- visidata/basesheet.py +11 -4
- visidata/canvas.py +66 -24
- visidata/clipboard.py +13 -6
- visidata/cliptext.py +7 -6
- visidata/cmdlog.py +40 -27
- visidata/column.py +14 -49
- visidata/ddw/regex.ddw +3 -2
- visidata/deprecated.py +14 -2
- visidata/desktop/visidata.desktop +2 -2
- visidata/editor.py +1 -0
- visidata/errors.py +1 -1
- visidata/experimental/sort_selected.py +54 -0
- visidata/expr.py +69 -18
- visidata/features/change_precision.py +1 -3
- visidata/features/cmdpalette.py +23 -4
- visidata/features/colorsheet.py +1 -1
- visidata/features/dedupe.py +3 -3
- visidata/features/go_col.py +71 -0
- visidata/features/graph_seaborn.py +1 -1
- visidata/features/join.py +20 -10
- visidata/features/layout.py +18 -5
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- visidata/features/slide.py +15 -17
- visidata/features/status_source.py +3 -1
- visidata/features/sysedit.py +1 -1
- visidata/features/transpose.py +2 -1
- visidata/features/type_ipaddr.py +2 -4
- visidata/features/unfurl.py +1 -0
- visidata/form.py +2 -2
- visidata/freqtbl.py +16 -11
- visidata/fuzzymatch.py +1 -0
- visidata/graph.py +173 -12
- visidata/guide.py +61 -25
- visidata/guides/ClipboardGuide.md +48 -0
- visidata/guides/ColumnsGuide.md +52 -0
- visidata/guides/CommandsSheet.md +28 -0
- visidata/guides/DirSheet.md +34 -0
- visidata/guides/ErrorsSheet.md +17 -0
- visidata/guides/FrequencyTable.md +42 -0
- visidata/guides/GrepSheet.md +28 -0
- visidata/guides/JsonSheet.md +38 -0
- visidata/guides/MacrosSheet.md +19 -0
- visidata/guides/MeltGuide.md +52 -0
- visidata/guides/MemorySheet.md +7 -0
- visidata/guides/MenuGuide.md +26 -0
- visidata/guides/ModifyGuide.md +38 -0
- visidata/guides/PivotGuide.md +71 -0
- visidata/guides/RegexGuide.md +107 -0
- visidata/guides/SelectionGuide.md +44 -0
- visidata/guides/SlideGuide.md +26 -0
- visidata/guides/SortGuide.md +0 -0
- visidata/guides/SplitpaneGuide.md +15 -0
- visidata/guides/TypesSheet.md +43 -0
- visidata/guides/XsvGuide.md +36 -0
- visidata/help.py +6 -6
- visidata/hint.py +2 -1
- visidata/indexsheet.py +2 -2
- visidata/interface.py +13 -14
- visidata/keys.py +4 -1
- visidata/loaders/api_airtable.py +1 -1
- visidata/loaders/archive.py +1 -1
- visidata/loaders/csv.py +9 -5
- visidata/loaders/eml.py +11 -6
- visidata/loaders/f5log.py +1 -0
- visidata/loaders/fec.py +18 -42
- visidata/loaders/fixed_width.py +19 -3
- visidata/loaders/grep.py +121 -0
- visidata/loaders/html.py +1 -0
- visidata/loaders/http.py +6 -1
- visidata/loaders/json.py +22 -4
- visidata/loaders/jsonla.py +8 -2
- visidata/loaders/mailbox.py +1 -0
- visidata/loaders/markdown.py +25 -6
- visidata/loaders/msgpack.py +19 -0
- visidata/loaders/npy.py +0 -1
- visidata/loaders/odf.py +18 -4
- visidata/loaders/orgmode.py +1 -1
- visidata/loaders/rec.py +6 -4
- visidata/loaders/sas.py +11 -4
- visidata/loaders/scrape.py +0 -1
- visidata/loaders/texttables.py +2 -0
- visidata/loaders/tsv.py +24 -7
- visidata/loaders/unzip_http.py +127 -3
- visidata/loaders/vds.py +4 -0
- visidata/loaders/vdx.py +1 -1
- visidata/loaders/xlsx.py +5 -0
- visidata/loaders/xml.py +2 -1
- visidata/macros.py +14 -31
- visidata/main.py +20 -15
- visidata/mainloop.py +17 -6
- visidata/man/vd.1 +74 -39
- visidata/man/vd.txt +73 -41
- visidata/memory.py +16 -5
- visidata/menu.py +14 -3
- visidata/metasheets.py +5 -6
- visidata/modify.py +4 -4
- visidata/mouse.py +2 -0
- visidata/movement.py +14 -28
- visidata/optionssheet.py +3 -5
- visidata/path.py +59 -37
- visidata/pivot.py +8 -5
- visidata/pyobj.py +63 -9
- visidata/rename_col.py +18 -1
- visidata/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +189 -118
- visidata/shell.py +23 -14
- visidata/sidebar.py +71 -16
- visidata/sort.py +21 -6
- visidata/statusbar.py +42 -5
- visidata/stored_list.py +5 -2
- visidata/tests/conftest.py +1 -0
- visidata/tests/test_commands.py +9 -1
- visidata/tests/test_completer.py +18 -0
- visidata/tests/test_edittext.py +3 -2
- visidata/text_source.py +7 -4
- visidata/textsheet.py +20 -6
- visidata/themes/ascii8.py +9 -6
- visidata/themes/asciimono.py +14 -4
- visidata/threads.py +13 -3
- visidata/tuiwin.py +5 -1
- visidata/type_currency.py +1 -2
- visidata/type_date.py +6 -1
- visidata/undo.py +10 -13
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
- visidata-3.1.dist-info/RECORD +284 -0
- visidata-3.0.1.dist-info/RECORD +0 -258
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
'VisiData: a curses interface for exploring and arranging tabular data'
|
2
2
|
|
3
|
-
__version__ = '3.
|
3
|
+
__version__ = '3.1'
|
4
4
|
__version_info__ = 'VisiData v' + __version__
|
5
5
|
__author__ = 'Saul Pwanson <vd@saul.pw>'
|
6
6
|
__status__ = 'Production/Stable'
|
@@ -13,8 +13,7 @@ class EscapeException(BaseException):
|
|
13
13
|
|
14
14
|
|
15
15
|
def addGlobals(*args, **kwargs):
|
16
|
-
'''Update the VisiData globals dict with items from *args* and *kwargs
|
17
|
-
Importers can call ``addGlobals(globals())`` to have their globals accessible to execstrings.
|
16
|
+
'''Update the VisiData globals dict with items from *args* and *kwargs*; to add symbols available to command execstrings and eval strings like command expr.'
|
18
17
|
|
19
18
|
Dunder methods are ignored, to prevent accidentally overwriting housekeeping methods.'''
|
20
19
|
drop_dunder = lambda d: {k: v for k, v in d.items() if not k.startswith("__")}
|
@@ -140,13 +139,16 @@ def importFeatures():
|
|
140
139
|
vd.importSubmodules('visidata.features')
|
141
140
|
vd.importSubmodules('visidata.themes')
|
142
141
|
|
143
|
-
|
142
|
+
import visidata.deprecated
|
144
143
|
|
145
|
-
vd.
|
146
|
-
vd.
|
147
|
-
vd.
|
148
|
-
vd.
|
149
|
-
vd.
|
144
|
+
vd.importModule('copy', 'copy deepcopy'.split())
|
145
|
+
vd.importModule('builtins', 'abs all any ascii bin bool bytes callable chr complex dict dir divmod enumerate eval filter float format getattr hex int len list map max min next oct ord pow range repr reversed round set sorted str sum tuple type zip'.split())
|
146
|
+
vd.importModule('math', 'acos acosh asin asinh atan atan2 atanh ceil copysign cos cosh degrees dist erf erfc exp expm1 fabs factorial floor fmod frexp fsum gamma gcd hypot isclose isfinite isinf isnan isqrt lcm ldexp lgamma log log1p log10 log2 modf radians remainder sin sinh sqrt tan tanh trunc prod perm comb nextafter ulp pi e tau inf nan'.split())
|
147
|
+
vd.importModule('random', 'randrange randint choice choices sample uniform gauss lognormvariate'.split())
|
148
|
+
vd.importModule('string', 'ascii_letters ascii_lowercase ascii_uppercase digits hexdigits punctuation printable whitespace'.split())
|
149
|
+
vd.importModule('json')
|
150
|
+
vd.importModule('itertools')
|
151
|
+
vd.importModule('curses')
|
150
152
|
|
151
153
|
import visidata.experimental # import nothing by default but make package accessible
|
152
154
|
|
@@ -154,4 +156,4 @@ vd.finalInit() # call all VisiData.init() from modules
|
|
154
156
|
|
155
157
|
importFeatures()
|
156
158
|
|
157
|
-
vd.addGlobals(globals())
|
159
|
+
vd.addGlobals(vd=vd) # globals())
|
visidata/_input.py
CHANGED
@@ -4,7 +4,7 @@ import curses
|
|
4
4
|
import visidata
|
5
5
|
|
6
6
|
from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData, BaseSheet
|
7
|
-
from visidata import vd,
|
7
|
+
from visidata import vd, colors, dispwidth, ColorAttr
|
8
8
|
from visidata import AttrDict
|
9
9
|
|
10
10
|
|
@@ -14,7 +14,9 @@ vd.theme_option('disp_edit_fill', '_', 'edit field fill character')
|
|
14
14
|
vd.theme_option('disp_unprintable', '·', 'substitute character for unprintables')
|
15
15
|
vd.theme_option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None)
|
16
16
|
|
17
|
-
vd.disp_help =
|
17
|
+
vd.disp_help = 0 # current page of help shown
|
18
|
+
vd._help_sidebars = [] # list of (help:str|HelpPane, title:str)
|
19
|
+
|
18
20
|
|
19
21
|
class AcceptInput(Exception):
|
20
22
|
'*args[0]* is the input to be accepted'
|
@@ -64,7 +66,7 @@ class EnableCursor:
|
|
64
66
|
def __exit__(self, exc_type, exc_val, tb):
|
65
67
|
with suppress(curses.error):
|
66
68
|
curses.curs_set(0)
|
67
|
-
if options.mouse_interval:
|
69
|
+
if vd.options.mouse_interval:
|
68
70
|
curses.mousemask(curses.MOUSE_ALL if hasattr(curses, "MOUSE_ALL") else 0xffffffff)
|
69
71
|
else:
|
70
72
|
curses.mousemask(0)
|
@@ -89,155 +91,28 @@ def splice(v:str, i:int, s:str):
|
|
89
91
|
return v if i < 0 else v[:i] + s + v[i:]
|
90
92
|
|
91
93
|
|
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
94
|
@VisiData.api
|
114
|
-
def drawInputHelp(vd, scr
|
95
|
+
def drawInputHelp(vd, scr):
|
115
96
|
if not scr or not vd.cursesEnabled:
|
116
97
|
return
|
117
98
|
|
118
99
|
sheet = vd.activeSheet
|
119
100
|
if not sheet:
|
120
101
|
return
|
121
|
-
vd.drawSheet(scr, sheet)
|
122
102
|
|
123
|
-
|
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')
|
103
|
+
vd.drawSidebar(scr, sheet)
|
132
104
|
|
133
105
|
|
134
106
|
def clean_printable(s):
|
135
107
|
'Escape unprintable characters.'
|
136
|
-
return ''.join(c if c.isprintable() else options.disp_unprintable for c in str(s))
|
108
|
+
return ''.join(c if c.isprintable() else vd.options.disp_unprintable for c in str(s))
|
137
109
|
|
138
110
|
|
139
111
|
def delchar(s, i, remove=1):
|
140
112
|
'Delete `remove` characters from str `s` beginning at position `i`.'
|
141
113
|
return s if i < 0 else s[:i] + s[i+remove:]
|
142
114
|
|
143
|
-
|
144
|
-
class CompleteState:
|
145
|
-
def __init__(self, completer_func):
|
146
|
-
self.comps_idx = -1
|
147
|
-
self.completer_func = completer_func
|
148
|
-
self.former_i = None
|
149
|
-
self.just_completed = False
|
150
|
-
|
151
|
-
def complete(self, v, i, state_incr):
|
152
|
-
self.just_completed = True
|
153
|
-
self.comps_idx += state_incr
|
154
|
-
|
155
|
-
if self.former_i is None:
|
156
|
-
self.former_i = i
|
157
|
-
try:
|
158
|
-
r = self.completer_func(v[:self.former_i], self.comps_idx)
|
159
|
-
except Exception as e:
|
160
|
-
# raise # beep/flash; how to report exception?
|
161
|
-
return v, i
|
162
|
-
|
163
|
-
if not r:
|
164
|
-
# beep/flash to indicate no matches?
|
165
|
-
return v, i
|
166
|
-
|
167
|
-
v = r + v[i:]
|
168
|
-
return v, len(v)
|
169
|
-
|
170
|
-
def reset(self):
|
171
|
-
if self.just_completed:
|
172
|
-
self.just_completed = False
|
173
|
-
else:
|
174
|
-
self.former_i = None
|
175
|
-
self.comps_idx = -1
|
176
|
-
|
177
|
-
class HistoryState:
|
178
|
-
def __init__(self, history):
|
179
|
-
self.history = history
|
180
|
-
self.hist_idx = None
|
181
|
-
self.prev_val = None
|
182
|
-
|
183
|
-
def up(self, v, i):
|
184
|
-
if self.hist_idx is None:
|
185
|
-
self.hist_idx = len(self.history)
|
186
|
-
self.prev_val = v
|
187
|
-
if self.hist_idx > 0:
|
188
|
-
self.hist_idx -= 1
|
189
|
-
v = self.history[self.hist_idx]
|
190
|
-
i = len(str(v))
|
191
|
-
return v, i
|
192
|
-
|
193
|
-
def down(self, v, i):
|
194
|
-
if self.hist_idx is None:
|
195
|
-
return v, i
|
196
|
-
elif self.hist_idx < len(self.history)-1:
|
197
|
-
self.hist_idx += 1
|
198
|
-
v = self.history[self.hist_idx]
|
199
|
-
else:
|
200
|
-
v = self.prev_val
|
201
|
-
self.hist_idx = None
|
202
|
-
i = len(str(v))
|
203
|
-
return v, i
|
204
|
-
|
205
|
-
|
206
|
-
# history: earliest entry first
|
207
|
-
@VisiData.api
|
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):
|
221
|
-
'''A better curses line editing widget.
|
222
|
-
If *clear* is True, clear whole editing area before displaying.
|
223
|
-
'''
|
224
|
-
with EnableCursor():
|
225
|
-
with HelpCycler(scr, help) as disp_help:
|
226
|
-
ESC='^['
|
227
|
-
TAB='^I'
|
228
|
-
history_state = HistoryState(history)
|
229
|
-
complete_state = CompleteState(completer)
|
230
|
-
insert_mode = True
|
231
|
-
first_action = True
|
232
|
-
v = str(value) # value under edit
|
233
|
-
|
234
|
-
# i = 0 # index into v, initial value can be passed in as argument as of 1.2
|
235
|
-
if i != 0:
|
236
|
-
first_action = False
|
237
|
-
|
238
|
-
left_truncchar = right_truncchar = truncchar
|
239
|
-
|
240
|
-
def find_nonword(s, a, b, incr):
|
115
|
+
def find_nonword(s, a, b, incr):
|
241
116
|
if not s: return 0
|
242
117
|
a = min(max(a, 0), len(s)-1)
|
243
118
|
b = min(max(b, 0), len(s)-1)
|
@@ -255,50 +130,117 @@ def editline(vd, scr, y, x, w, i=0,
|
|
255
130
|
a += incr
|
256
131
|
return min(max(a, 0), len(s))
|
257
132
|
|
258
|
-
|
259
|
-
|
133
|
+
class InputWidget:
|
134
|
+
def __init__(self,
|
135
|
+
value:str='',
|
136
|
+
i=0,
|
137
|
+
display=True,
|
138
|
+
history=[],
|
139
|
+
completer=lambda text,idx: None,
|
140
|
+
options=None,
|
141
|
+
fillchar=''):
|
142
|
+
'''
|
143
|
+
- value: starting value
|
144
|
+
- i: starting index into value
|
145
|
+
- display: False to not display input (for sensitive input, e.g. a password)
|
146
|
+
- history: list of strings; earliest entry first.
|
147
|
+
- completer: func(value:str, idx:int) takes the current value and tab completion index, and returns a string if there is a completion available, or None if not.
|
148
|
+
- options: sheet.options; defaults to vd.options.
|
149
|
+
'''
|
150
|
+
options = options or vd.options
|
151
|
+
|
152
|
+
self.orig_value = value
|
153
|
+
self.first_action = (i == 0) # whether this would be the 'first action'; if so, clear text on input
|
154
|
+
|
155
|
+
# display theme
|
156
|
+
self.fillchar = fillchar or options.disp_edit_fill
|
157
|
+
self.truncchar = options.disp_truncator
|
158
|
+
self.display = display # if False, obscure before displaying
|
159
|
+
|
160
|
+
# main state
|
161
|
+
self.value = self.orig_value # value under edit
|
162
|
+
self.current_i = i
|
163
|
+
self.insert_mode = True
|
164
|
+
|
165
|
+
# history state
|
166
|
+
self.history = history
|
167
|
+
self.hist_idx = None
|
168
|
+
self.prev_val = None
|
260
169
|
|
261
|
-
|
262
|
-
|
170
|
+
# completion state
|
171
|
+
self.comps_idx = -1
|
172
|
+
self.completer_func = completer
|
173
|
+
self.former_i = None
|
174
|
+
self.just_completed = False
|
175
|
+
|
176
|
+
def editline(self, scr, y, x, w, attr=ColorAttr(), updater=lambda val: None, bindings={}, clear=True) -> str:
|
177
|
+
'If *clear* is True, clear whole editing area before displaying.'
|
178
|
+
with EnableCursor():
|
179
|
+
while True:
|
180
|
+
vd.drawSheet(scr, vd.activeSheet)
|
181
|
+
if updater:
|
182
|
+
updater(self.value)
|
183
|
+
|
184
|
+
vd.drawInputHelp(scr)
|
185
|
+
|
186
|
+
self.draw(scr, y, x, w, attr, clear=clear)
|
187
|
+
ch = vd.getkeystroke(scr)
|
188
|
+
if ch in bindings:
|
189
|
+
self.value, self.current_i = bindings[ch](self.value, self.current_i)
|
190
|
+
else:
|
191
|
+
if self.handle_key(ch, scr):
|
192
|
+
return self.value
|
193
|
+
|
194
|
+
|
195
|
+
def draw(self, scr, y, x, w, attr=ColorAttr(), clear=True):
|
196
|
+
i = self.current_i # the onscreen offset within the field where v[i] is displayed
|
197
|
+
left_truncchar = right_truncchar = self.truncchar
|
198
|
+
|
199
|
+
if self.display:
|
200
|
+
dispval = clean_printable(self.value)
|
263
201
|
else:
|
264
|
-
dispval = '*' * len(
|
202
|
+
dispval = '*' * len(self.value)
|
265
203
|
|
266
|
-
dispi = i # the onscreen offset within the field where v[i] is displayed
|
267
204
|
if len(dispval) < w: # entire value fits
|
268
|
-
dispval += fillchar*(w-len(dispval)-1)
|
205
|
+
dispval += self.fillchar*(w-len(dispval)-1)
|
269
206
|
elif i == len(dispval): # cursor after value (will append)
|
270
|
-
|
271
|
-
dispval = left_truncchar + dispval[len(dispval)-w+2:] + fillchar
|
207
|
+
i = w-1
|
208
|
+
dispval = left_truncchar + dispval[len(dispval)-w+2:] + self.fillchar
|
272
209
|
elif i >= len(dispval)-w//2: # cursor within halfwidth of end
|
273
|
-
|
210
|
+
i = w-(len(dispval)-i)
|
274
211
|
dispval = left_truncchar + dispval[len(dispval)-w+1:]
|
275
212
|
elif i <= w//2: # cursor within halfwidth of beginning
|
276
213
|
dispval = dispval[:w-1] + right_truncchar
|
277
214
|
else:
|
278
|
-
|
215
|
+
i = w//2 # visual cursor stays right in the middle
|
279
216
|
k = 1 if w%2==0 else 0 # odd widths have one character more
|
280
|
-
dispval = left_truncchar + dispval[
|
281
|
-
|
282
|
-
prew = clipdraw(scr, y, x, dispval[:
|
283
|
-
clipdraw(scr, y, x+prew, dispval[
|
284
|
-
if scr:
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
217
|
+
dispval = left_truncchar + dispval[self.current_i-w//2+1:self.current_i+w//2-k] + right_truncchar
|
218
|
+
|
219
|
+
prew = clipdraw(scr, y, x, dispval[:i], attr, w, clear=clear, literal=True)
|
220
|
+
clipdraw(scr, y, x+prew, dispval[i:], attr, w-prew+1, clear=clear, literal=True)
|
221
|
+
if scr:
|
222
|
+
scr.move(y, x+prew)
|
223
|
+
|
224
|
+
def handle_key(self, ch:str, scr) -> bool:
|
225
|
+
'Return True to accept current input. Raise EscapeException on Ctrl+C, Ctrl+Q, or ESC.'
|
226
|
+
i = self.current_i
|
227
|
+
v = self.value
|
228
|
+
|
229
|
+
if ch == '': return False
|
230
|
+
elif ch == 'KEY_IC': self.insert_mode = not self.insert_mode
|
289
231
|
elif ch == '^A' or ch == 'KEY_HOME': i = 0
|
290
232
|
elif ch == '^B' or ch == 'KEY_LEFT': i -= 1
|
291
|
-
elif ch in ('^C', '^Q',
|
233
|
+
elif ch in ('^C', '^Q', '^['): raise EscapeException(ch)
|
292
234
|
elif ch == '^D' or ch == 'KEY_DC': v = delchar(v, i)
|
293
235
|
elif ch == '^E' or ch == 'KEY_END': i = len(v)
|
294
236
|
elif ch == '^F' or ch == 'KEY_RIGHT': i += 1
|
295
237
|
elif ch == '^G':
|
296
|
-
|
297
|
-
|
238
|
+
vd.cycleSidebar()
|
239
|
+
return False # not considered a first keypress
|
298
240
|
elif ch in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i)
|
299
|
-
elif ch ==
|
300
|
-
elif ch == 'KEY_BTAB': v, i =
|
301
|
-
elif ch in ['^J', '^M']:
|
241
|
+
elif ch == '^I': v, i = self.completion(v, i, +1)
|
242
|
+
elif ch == 'KEY_BTAB': v, i = self.completion(v, i, -1)
|
243
|
+
elif ch in ['^J', '^M']: return True # ENTER to accept value
|
302
244
|
elif ch == '^K': v = v[:i] # ^Kill to end-of-line
|
303
245
|
elif ch == '^N':
|
304
246
|
c = ''
|
@@ -307,8 +249,8 @@ def editline(vd, scr, y, x, w, i=0,
|
|
307
249
|
c = vd.prettykeys(c)
|
308
250
|
i += len(c)
|
309
251
|
v += c
|
310
|
-
elif ch == '^O':
|
311
|
-
elif ch == '^R': v =
|
252
|
+
elif ch == '^O': self.value = vd.launchExternalEditor(v); return True # auto-accept after $EDITOR
|
253
|
+
elif ch == '^R': v = self.orig_value # ^Reload initial value
|
312
254
|
elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
|
313
255
|
elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
|
314
256
|
elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
|
@@ -320,13 +262,13 @@ def editline(vd, scr, y, x, w, i=0,
|
|
320
262
|
elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
|
321
263
|
elif ch == 'kUP5': pass
|
322
264
|
elif ch == 'kDN5': pass
|
323
|
-
elif history and ch == 'KEY_UP':
|
324
|
-
elif history and ch == 'KEY_DOWN':
|
265
|
+
elif self.history and ch == 'KEY_UP': v, i = self.prev_history(v, i)
|
266
|
+
elif self.history and ch == 'KEY_DOWN': v, i = self.next_history(v, i)
|
325
267
|
elif len(ch) > 1: pass
|
326
268
|
else:
|
327
|
-
if first_action:
|
269
|
+
if self.first_action:
|
328
270
|
v = ''
|
329
|
-
if insert_mode:
|
271
|
+
if self.insert_mode:
|
330
272
|
v = splice(v, i, ch)
|
331
273
|
else:
|
332
274
|
v = v[:i] + ch + v[i+1:]
|
@@ -337,22 +279,87 @@ def editline(vd, scr, y, x, w, i=0,
|
|
337
279
|
# v may have a non-str type with no len()
|
338
280
|
v = str(v)
|
339
281
|
if i > len(v): i = len(v)
|
340
|
-
|
341
|
-
|
282
|
+
self.current_i = i
|
283
|
+
self.value = v
|
284
|
+
self.first_action = False
|
285
|
+
self.reset_completion()
|
286
|
+
return False
|
342
287
|
|
343
|
-
|
288
|
+
def completion(self, v, i, state_incr):
|
289
|
+
self.just_completed = True
|
290
|
+
self.comps_idx += state_incr
|
291
|
+
|
292
|
+
if self.former_i is None:
|
293
|
+
self.former_i = i
|
294
|
+
try:
|
295
|
+
r = self.completer_func(v[:self.former_i], self.comps_idx)
|
296
|
+
except Exception as e:
|
297
|
+
# raise # beep/flash; how to report exception?
|
298
|
+
return v, i
|
299
|
+
|
300
|
+
if not r:
|
301
|
+
# beep/flash to indicate no matches?
|
302
|
+
return v, i
|
303
|
+
|
304
|
+
v = r + v[i:]
|
305
|
+
return v, len(v)
|
306
|
+
|
307
|
+
def reset_completion(self):
|
308
|
+
if self.just_completed:
|
309
|
+
self.just_completed = False
|
310
|
+
else:
|
311
|
+
self.former_i = None
|
312
|
+
self.comps_idx = -1
|
313
|
+
|
314
|
+
def prev_history(self, v, i):
|
315
|
+
if self.hist_idx is None:
|
316
|
+
self.hist_idx = len(self.history)
|
317
|
+
self.prev_val = v
|
318
|
+
if self.hist_idx > 0:
|
319
|
+
self.hist_idx -= 1
|
320
|
+
v = self.history[self.hist_idx]
|
321
|
+
i = len(str(v))
|
322
|
+
return v, i
|
323
|
+
|
324
|
+
def next_history(self, v, i):
|
325
|
+
if self.hist_idx is None:
|
326
|
+
return v, i
|
327
|
+
elif self.hist_idx < len(self.history)-1:
|
328
|
+
self.hist_idx += 1
|
329
|
+
v = self.history[self.hist_idx]
|
330
|
+
else:
|
331
|
+
v = self.prev_val
|
332
|
+
self.hist_idx = None
|
333
|
+
i = len(str(v))
|
334
|
+
return v, i
|
344
335
|
|
345
336
|
|
346
337
|
@VisiData.api
|
347
|
-
def editText(vd, y, x, w,
|
338
|
+
def editText(vd, y, x, w, attr=ColorAttr(), value='',
|
339
|
+
help='',
|
340
|
+
updater=None, bindings={},
|
341
|
+
display=True, record=True, clear=True, **kwargs):
|
348
342
|
'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.'
|
349
343
|
v = None
|
350
344
|
if record and vd.cmdlog:
|
351
345
|
v = vd.getCommandInput()
|
352
346
|
|
353
347
|
if v is None:
|
348
|
+
if vd.options.batch:
|
349
|
+
return ''
|
350
|
+
|
351
|
+
if vd.activeSheet._scr is None:
|
352
|
+
raise Exception('active sheet does not have a screen')
|
353
|
+
|
354
|
+
if value is None:
|
355
|
+
value = ''
|
356
|
+
|
354
357
|
try:
|
355
|
-
|
358
|
+
widget = InputWidget(value=str(value), display=display, **kwargs)
|
359
|
+
|
360
|
+
with vd.AddedHelp(vd.getHelpPane('input', module='visidata'), 'Input Keystrokes Help'), \
|
361
|
+
vd.AddedHelp(help, 'Input Field Help'):
|
362
|
+
v = widget.editline(vd.activeSheet._scr, y, x, w, attr=attr, updater=updater, bindings=bindings, clear=clear)
|
356
363
|
except AcceptInput as e:
|
357
364
|
v = e.args[0]
|
358
365
|
|
@@ -364,13 +371,13 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
|
|
364
371
|
if record and vd.cmdlog:
|
365
372
|
vd.setLastArgs(v)
|
366
373
|
|
367
|
-
if
|
368
|
-
|
369
|
-
if isinstance(starting_value, (int, float)) and v[-1] == '%': #2082
|
374
|
+
if value:
|
375
|
+
if isinstance(value, (int, float)) and v[-1] == '%': #2082
|
370
376
|
pct = float(v[:-1])
|
371
|
-
v = pct*
|
377
|
+
v = pct*value/100
|
372
378
|
|
373
|
-
|
379
|
+
# convert back to type of original value
|
380
|
+
v = type(value)(v)
|
374
381
|
|
375
382
|
return v
|
376
383
|
|
@@ -408,17 +415,22 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
408
415
|
|
409
416
|
previnput = vd.getCommandInput()
|
410
417
|
if previnput is not None:
|
418
|
+
ret = None
|
411
419
|
if isinstance(previnput, str):
|
412
420
|
if previnput.startswith('{'):
|
413
|
-
|
421
|
+
ret = json.loads(previnput)
|
414
422
|
else:
|
415
423
|
ret = {k:v.get('value', '') for k,v in kwargs.items()}
|
416
424
|
primekey = list(ret.keys())[0]
|
417
425
|
ret[primekey] = previnput
|
418
|
-
return ret
|
419
426
|
|
420
427
|
if isinstance(previnput, dict):
|
421
|
-
|
428
|
+
ret = previnput
|
429
|
+
|
430
|
+
if ret:
|
431
|
+
if record and vd.cmdlog:
|
432
|
+
vd.setLastArgs(ret)
|
433
|
+
return ret
|
422
434
|
|
423
435
|
assert False, type(previnput)
|
424
436
|
|
@@ -454,8 +466,7 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
454
466
|
|
455
467
|
return updater(val)
|
456
468
|
|
457
|
-
|
458
|
-
while True:
|
469
|
+
while True:
|
459
470
|
try:
|
460
471
|
input_kwargs = kwargs[cur_input_key]
|
461
472
|
input_kwargs['value'] = vd.input(**input_kwargs,
|
@@ -478,16 +489,16 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
478
489
|
cur_input_key = keys[(i+offset)%len(keys)]
|
479
490
|
|
480
491
|
retargs = {}
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
retargs[k] = v
|
492
|
+
lastargs = {}
|
493
|
+
for k, input_kwargs in kwargs.items():
|
494
|
+
v = input_kwargs.get('value', '')
|
495
|
+
retargs[k] = v
|
486
496
|
|
497
|
+
if input_kwargs.get('record', record):
|
487
498
|
if input_kwargs.get('display', True):
|
488
499
|
lastargs[k] = v
|
489
500
|
vd.addInputHistory(v, input_kwargs.get('type', ''))
|
490
|
-
|
501
|
+
if record:
|
491
502
|
if vd.cmdlog and lastargs:
|
492
503
|
vd.setLastArgs(lastargs)
|
493
504
|
|
@@ -501,8 +512,8 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
501
512
|
- *type*: string indicating the type of input to use for history.
|
502
513
|
- *history*: list of strings to use for input history.
|
503
514
|
- *defaultLast*: on empty input, if True, return last history item.
|
504
|
-
- *display*: pass False to not display input (for sensitive input, e.g. a password)
|
505
|
-
- *record*: pass False to not record input on cmdlog (for sensitive or inconsequential input).
|
515
|
+
- *display*: pass False to not display input (for sensitive input, e.g. a password), and to also prevent recording input as if *record* is False
|
516
|
+
- *record*: pass False to not record input on cmdlog or input history (for sensitive or inconsequential input).
|
506
517
|
- *completer*: ``completer(val, idx)`` is called on TAB to get next completed value.
|
507
518
|
- *updater*: ``updater(val)`` is called every keypress or timeout.
|
508
519
|
- *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
|
@@ -525,7 +536,8 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
525
536
|
import getpass
|
526
537
|
return getpass.getpass(prompt)
|
527
538
|
|
528
|
-
|
539
|
+
if not history:
|
540
|
+
history = list(vd.inputHistory.setdefault(type, {}).keys())
|
529
541
|
|
530
542
|
y = sheet.windowHeight-dy-1
|
531
543
|
promptlen = dispwidth(prompt)
|
@@ -539,15 +551,14 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
539
551
|
w = kwargs.pop('w', _drawPrompt())
|
540
552
|
ret = vd.editText(y, promptlen, w=w,
|
541
553
|
attr=colors.color_edit_cell,
|
542
|
-
|
543
|
-
truncchar=options.disp_truncator,
|
554
|
+
options=vd.options,
|
544
555
|
history=history,
|
545
556
|
updater=_drawPrompt,
|
546
557
|
**kwargs)
|
547
558
|
|
548
559
|
if ret:
|
549
|
-
|
550
|
-
|
560
|
+
if kwargs.get('record', True) and kwargs.get('display', True):
|
561
|
+
vd.addInputHistory(ret, type=type)
|
551
562
|
elif defaultLast:
|
552
563
|
history or vd.fail("no previous input")
|
553
564
|
ret = history[-1]
|
@@ -558,7 +569,7 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
558
569
|
@VisiData.api
|
559
570
|
def confirm(vd, prompt, exc=EscapeException):
|
560
571
|
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
|
561
|
-
if options.batch and not options.interactive:
|
572
|
+
if vd.options.batch and not vd.options.interactive:
|
562
573
|
return vd.fail('cannot confirm in batch mode: ' + prompt)
|
563
574
|
|
564
575
|
yn = vd.input(prompt, value='no', record=False)[:1]
|
@@ -625,9 +636,7 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
625
636
|
bindings.update(kwargs.get('bindings', {}))
|
626
637
|
kwargs['bindings'] = bindings
|
627
638
|
|
628
|
-
editargs = dict(value=value,
|
629
|
-
fillchar=self.options.disp_edit_fill,
|
630
|
-
truncchar=self.options.disp_truncator)
|
639
|
+
editargs = dict(value=value, options=self.options)
|
631
640
|
|
632
641
|
editargs.update(kwargs) # update with user-specified args
|
633
642
|
r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
|
@@ -638,4 +647,4 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
638
647
|
return r
|
639
648
|
|
640
649
|
|
641
|
-
vd.addGlobals(
|
650
|
+
vd.addGlobals(CompleteKey=CompleteKey, AcceptInput=AcceptInput, InputWidget=InputWidget)
|
visidata/_open.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
import os.path
|
3
|
+
import sys
|
3
4
|
|
4
5
|
from visidata import VisiData, vd, Path, BaseSheet, TableSheet, TextSheet, SettableColumn
|
5
6
|
|
@@ -154,6 +155,8 @@ def openSource(vd, p, filetype=None, create=False, **kwargs):
|
|
154
155
|
if '://' in p:
|
155
156
|
vs = vd.openPath(Path(p), filetype=filetype) # convert to Path and recurse
|
156
157
|
elif p == '-':
|
158
|
+
if sys.stdin.isatty():
|
159
|
+
vd.fail('cannot open stdin when it is a tty')
|
157
160
|
vs = vd.openPath(vd.stdinSource, filetype=filetype)
|
158
161
|
else:
|
159
162
|
vs = vd.openPath(Path(p), filetype=filetype, create=create) # convert to Path and recurse
|
@@ -177,7 +180,7 @@ def open_txt(vd, p):
|
|
177
180
|
if delimiter and delimiter in next(fp): # peek at the first line
|
178
181
|
return vd.open_tsv(p) # TSV often have .txt extension
|
179
182
|
except StopIteration:
|
180
|
-
return TableSheet(p.base_stem, columns=[SettableColumn()], source=p)
|
183
|
+
return TableSheet(p.base_stem, columns=[SettableColumn(width=vd.options.default_width)], source=p)
|
181
184
|
return TextSheet(p.base_stem, source=p)
|
182
185
|
|
183
186
|
|