visidata 3.0.2__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 -202
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +7 -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 +54 -20
- 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 +17 -2
- 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 +16 -3
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- 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 +163 -12
- visidata/guide.py +57 -24
- 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 +14 -13
- visidata/mainloop.py +14 -6
- visidata/man/vd.1 +72 -39
- visidata/man/vd.txt +72 -41
- visidata/memory.py +15 -4
- 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/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +186 -108
- visidata/shell.py +22 -12
- 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 -5
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
- visidata-3.1.dist-info/RECORD +284 -0
- visidata-3.0.2.dist-info/RECORD +0 -258
- {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
- {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.2.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,31 +91,8 @@ 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
|
-
self.draw()
|
100
|
-
|
101
|
-
return self
|
102
|
-
|
103
|
-
def __exit__(self, *args):
|
104
|
-
pass
|
105
|
-
|
106
|
-
def cycle(self):
|
107
|
-
vd.disp_help = (vd.disp_help-1)%(vd.options.disp_help+1)
|
108
|
-
self.draw()
|
109
|
-
|
110
|
-
def draw(self):
|
111
|
-
if self.scr:
|
112
|
-
vd.drawInputHelp(self.scr, self.help)
|
113
|
-
|
114
|
-
|
115
94
|
@VisiData.api
|
116
|
-
def drawInputHelp(vd, scr
|
95
|
+
def drawInputHelp(vd, scr):
|
117
96
|
if not scr or not vd.cursesEnabled:
|
118
97
|
return
|
119
98
|
|
@@ -121,124 +100,19 @@ def drawInputHelp(vd, scr, help:str=''):
|
|
121
100
|
if not sheet:
|
122
101
|
return
|
123
102
|
|
124
|
-
|
125
|
-
if vd.disp_help == 0:
|
126
|
-
vd.drawSidebar(scr, sheet)
|
127
|
-
elif vd.disp_help == 1:
|
128
|
-
curhelp = help
|
129
|
-
sheet.drawSidebarText(scr, curhelp)
|
130
|
-
elif vd.disp_help >= 2:
|
131
|
-
curhelp = vd.getHelpPane('input', module='visidata')
|
132
|
-
sheet.drawSidebarText(scr, curhelp, title='Input Keystrokes Help')
|
103
|
+
vd.drawSidebar(scr, sheet)
|
133
104
|
|
134
105
|
|
135
106
|
def clean_printable(s):
|
136
107
|
'Escape unprintable characters.'
|
137
|
-
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))
|
138
109
|
|
139
110
|
|
140
111
|
def delchar(s, i, remove=1):
|
141
112
|
'Delete `remove` characters from str `s` beginning at position `i`.'
|
142
113
|
return s if i < 0 else s[:i] + s[i+remove:]
|
143
114
|
|
144
|
-
|
145
|
-
class CompleteState:
|
146
|
-
def __init__(self, completer_func):
|
147
|
-
self.comps_idx = -1
|
148
|
-
self.completer_func = completer_func
|
149
|
-
self.former_i = None
|
150
|
-
self.just_completed = False
|
151
|
-
|
152
|
-
def complete(self, v, i, state_incr):
|
153
|
-
self.just_completed = True
|
154
|
-
self.comps_idx += state_incr
|
155
|
-
|
156
|
-
if self.former_i is None:
|
157
|
-
self.former_i = i
|
158
|
-
try:
|
159
|
-
r = self.completer_func(v[:self.former_i], self.comps_idx)
|
160
|
-
except Exception as e:
|
161
|
-
# raise # beep/flash; how to report exception?
|
162
|
-
return v, i
|
163
|
-
|
164
|
-
if not r:
|
165
|
-
# beep/flash to indicate no matches?
|
166
|
-
return v, i
|
167
|
-
|
168
|
-
v = r + v[i:]
|
169
|
-
return v, len(v)
|
170
|
-
|
171
|
-
def reset(self):
|
172
|
-
if self.just_completed:
|
173
|
-
self.just_completed = False
|
174
|
-
else:
|
175
|
-
self.former_i = None
|
176
|
-
self.comps_idx = -1
|
177
|
-
|
178
|
-
class HistoryState:
|
179
|
-
def __init__(self, history):
|
180
|
-
self.history = history
|
181
|
-
self.hist_idx = None
|
182
|
-
self.prev_val = None
|
183
|
-
|
184
|
-
def up(self, v, i):
|
185
|
-
if self.hist_idx is None:
|
186
|
-
self.hist_idx = len(self.history)
|
187
|
-
self.prev_val = v
|
188
|
-
if self.hist_idx > 0:
|
189
|
-
self.hist_idx -= 1
|
190
|
-
v = self.history[self.hist_idx]
|
191
|
-
i = len(str(v))
|
192
|
-
return v, i
|
193
|
-
|
194
|
-
def down(self, v, i):
|
195
|
-
if self.hist_idx is None:
|
196
|
-
return v, i
|
197
|
-
elif self.hist_idx < len(self.history)-1:
|
198
|
-
self.hist_idx += 1
|
199
|
-
v = self.history[self.hist_idx]
|
200
|
-
else:
|
201
|
-
v = self.prev_val
|
202
|
-
self.hist_idx = None
|
203
|
-
i = len(str(v))
|
204
|
-
return v, i
|
205
|
-
|
206
|
-
|
207
|
-
# history: earliest entry first
|
208
|
-
@VisiData.api
|
209
|
-
def editline(vd, scr, y, x, w, i=0,
|
210
|
-
attr=ColorAttr(),
|
211
|
-
value='',
|
212
|
-
fillchar=' ',
|
213
|
-
truncchar='-',
|
214
|
-
unprintablechar='.',
|
215
|
-
completer=lambda text,idx: None,
|
216
|
-
history=[],
|
217
|
-
display=True,
|
218
|
-
updater=lambda val: None,
|
219
|
-
bindings={},
|
220
|
-
help='', # str|HelpPane
|
221
|
-
clear=True):
|
222
|
-
'''A better curses line editing widget.
|
223
|
-
If *clear* is True, clear whole editing area before displaying.
|
224
|
-
'''
|
225
|
-
with EnableCursor():
|
226
|
-
with HelpCycler(scr, help) as disp_help:
|
227
|
-
ESC='^['
|
228
|
-
TAB='^I'
|
229
|
-
history_state = HistoryState(history)
|
230
|
-
complete_state = CompleteState(completer)
|
231
|
-
insert_mode = True
|
232
|
-
first_action = True
|
233
|
-
v = str(value) # value under edit
|
234
|
-
|
235
|
-
# i = 0 # index into v, initial value can be passed in as argument as of 1.2
|
236
|
-
if i != 0:
|
237
|
-
first_action = False
|
238
|
-
|
239
|
-
left_truncchar = right_truncchar = truncchar
|
240
|
-
|
241
|
-
def find_nonword(s, a, b, incr):
|
115
|
+
def find_nonword(s, a, b, incr):
|
242
116
|
if not s: return 0
|
243
117
|
a = min(max(a, 0), len(s)-1)
|
244
118
|
b = min(max(b, 0), len(s)-1)
|
@@ -256,52 +130,117 @@ def editline(vd, scr, y, x, w, i=0,
|
|
256
130
|
a += incr
|
257
131
|
return min(max(a, 0), len(s))
|
258
132
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
169
|
+
|
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
|
+
|
263
194
|
|
264
|
-
|
265
|
-
|
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)
|
266
201
|
else:
|
267
|
-
dispval = '*' * len(
|
202
|
+
dispval = '*' * len(self.value)
|
268
203
|
|
269
|
-
dispi = i # the onscreen offset within the field where v[i] is displayed
|
270
204
|
if len(dispval) < w: # entire value fits
|
271
|
-
dispval += fillchar*(w-len(dispval)-1)
|
205
|
+
dispval += self.fillchar*(w-len(dispval)-1)
|
272
206
|
elif i == len(dispval): # cursor after value (will append)
|
273
|
-
|
274
|
-
dispval = left_truncchar + dispval[len(dispval)-w+2:] + fillchar
|
207
|
+
i = w-1
|
208
|
+
dispval = left_truncchar + dispval[len(dispval)-w+2:] + self.fillchar
|
275
209
|
elif i >= len(dispval)-w//2: # cursor within halfwidth of end
|
276
|
-
|
210
|
+
i = w-(len(dispval)-i)
|
277
211
|
dispval = left_truncchar + dispval[len(dispval)-w+1:]
|
278
212
|
elif i <= w//2: # cursor within halfwidth of beginning
|
279
213
|
dispval = dispval[:w-1] + right_truncchar
|
280
214
|
else:
|
281
|
-
|
215
|
+
i = w//2 # visual cursor stays right in the middle
|
282
216
|
k = 1 if w%2==0 else 0 # odd widths have one character more
|
283
|
-
dispval = left_truncchar + dispval[
|
284
|
-
|
285
|
-
prew = clipdraw(scr, y, x, dispval[:
|
286
|
-
clipdraw(scr, y, x+prew, dispval[
|
287
|
-
if scr:
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
292
231
|
elif ch == '^A' or ch == 'KEY_HOME': i = 0
|
293
232
|
elif ch == '^B' or ch == 'KEY_LEFT': i -= 1
|
294
|
-
elif ch in ('^C', '^Q',
|
233
|
+
elif ch in ('^C', '^Q', '^['): raise EscapeException(ch)
|
295
234
|
elif ch == '^D' or ch == 'KEY_DC': v = delchar(v, i)
|
296
235
|
elif ch == '^E' or ch == 'KEY_END': i = len(v)
|
297
236
|
elif ch == '^F' or ch == 'KEY_RIGHT': i += 1
|
298
237
|
elif ch == '^G':
|
299
|
-
|
300
|
-
|
238
|
+
vd.cycleSidebar()
|
239
|
+
return False # not considered a first keypress
|
301
240
|
elif ch in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i)
|
302
|
-
elif ch ==
|
303
|
-
elif ch == 'KEY_BTAB': v, i =
|
304
|
-
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
|
305
244
|
elif ch == '^K': v = v[:i] # ^Kill to end-of-line
|
306
245
|
elif ch == '^N':
|
307
246
|
c = ''
|
@@ -310,8 +249,8 @@ def editline(vd, scr, y, x, w, i=0,
|
|
310
249
|
c = vd.prettykeys(c)
|
311
250
|
i += len(c)
|
312
251
|
v += c
|
313
|
-
elif ch == '^O':
|
314
|
-
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
|
315
254
|
elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
|
316
255
|
elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
|
317
256
|
elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
|
@@ -323,13 +262,13 @@ def editline(vd, scr, y, x, w, i=0,
|
|
323
262
|
elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
|
324
263
|
elif ch == 'kUP5': pass
|
325
264
|
elif ch == 'kDN5': pass
|
326
|
-
elif history and ch == 'KEY_UP':
|
327
|
-
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)
|
328
267
|
elif len(ch) > 1: pass
|
329
268
|
else:
|
330
|
-
if first_action:
|
269
|
+
if self.first_action:
|
331
270
|
v = ''
|
332
|
-
if insert_mode:
|
271
|
+
if self.insert_mode:
|
333
272
|
v = splice(v, i, ch)
|
334
273
|
else:
|
335
274
|
v = v[:i] + ch + v[i+1:]
|
@@ -340,22 +279,87 @@ def editline(vd, scr, y, x, w, i=0,
|
|
340
279
|
# v may have a non-str type with no len()
|
341
280
|
v = str(v)
|
342
281
|
if i > len(v): i = len(v)
|
343
|
-
|
344
|
-
|
282
|
+
self.current_i = i
|
283
|
+
self.value = v
|
284
|
+
self.first_action = False
|
285
|
+
self.reset_completion()
|
286
|
+
return False
|
345
287
|
|
346
|
-
|
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
|
347
335
|
|
348
336
|
|
349
337
|
@VisiData.api
|
350
|
-
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):
|
351
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.'
|
352
343
|
v = None
|
353
344
|
if record and vd.cmdlog:
|
354
345
|
v = vd.getCommandInput()
|
355
346
|
|
356
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
|
+
|
357
357
|
try:
|
358
|
-
|
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)
|
359
363
|
except AcceptInput as e:
|
360
364
|
v = e.args[0]
|
361
365
|
|
@@ -367,13 +371,13 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
|
|
367
371
|
if record and vd.cmdlog:
|
368
372
|
vd.setLastArgs(v)
|
369
373
|
|
370
|
-
if
|
371
|
-
|
372
|
-
if isinstance(starting_value, (int, float)) and v[-1] == '%': #2082
|
374
|
+
if value:
|
375
|
+
if isinstance(value, (int, float)) and v[-1] == '%': #2082
|
373
376
|
pct = float(v[:-1])
|
374
|
-
v = pct*
|
377
|
+
v = pct*value/100
|
375
378
|
|
376
|
-
|
379
|
+
# convert back to type of original value
|
380
|
+
v = type(value)(v)
|
377
381
|
|
378
382
|
return v
|
379
383
|
|
@@ -411,17 +415,22 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
411
415
|
|
412
416
|
previnput = vd.getCommandInput()
|
413
417
|
if previnput is not None:
|
418
|
+
ret = None
|
414
419
|
if isinstance(previnput, str):
|
415
420
|
if previnput.startswith('{'):
|
416
|
-
|
421
|
+
ret = json.loads(previnput)
|
417
422
|
else:
|
418
423
|
ret = {k:v.get('value', '') for k,v in kwargs.items()}
|
419
424
|
primekey = list(ret.keys())[0]
|
420
425
|
ret[primekey] = previnput
|
421
|
-
return ret
|
422
426
|
|
423
427
|
if isinstance(previnput, dict):
|
424
|
-
|
428
|
+
ret = previnput
|
429
|
+
|
430
|
+
if ret:
|
431
|
+
if record and vd.cmdlog:
|
432
|
+
vd.setLastArgs(ret)
|
433
|
+
return ret
|
425
434
|
|
426
435
|
assert False, type(previnput)
|
427
436
|
|
@@ -457,8 +466,7 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
457
466
|
|
458
467
|
return updater(val)
|
459
468
|
|
460
|
-
|
461
|
-
while True:
|
469
|
+
while True:
|
462
470
|
try:
|
463
471
|
input_kwargs = kwargs[cur_input_key]
|
464
472
|
input_kwargs['value'] = vd.input(**input_kwargs,
|
@@ -481,16 +489,16 @@ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
|
|
481
489
|
cur_input_key = keys[(i+offset)%len(keys)]
|
482
490
|
|
483
491
|
retargs = {}
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
retargs[k] = v
|
492
|
+
lastargs = {}
|
493
|
+
for k, input_kwargs in kwargs.items():
|
494
|
+
v = input_kwargs.get('value', '')
|
495
|
+
retargs[k] = v
|
489
496
|
|
497
|
+
if input_kwargs.get('record', record):
|
490
498
|
if input_kwargs.get('display', True):
|
491
499
|
lastargs[k] = v
|
492
500
|
vd.addInputHistory(v, input_kwargs.get('type', ''))
|
493
|
-
|
501
|
+
if record:
|
494
502
|
if vd.cmdlog and lastargs:
|
495
503
|
vd.setLastArgs(lastargs)
|
496
504
|
|
@@ -504,8 +512,8 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
504
512
|
- *type*: string indicating the type of input to use for history.
|
505
513
|
- *history*: list of strings to use for input history.
|
506
514
|
- *defaultLast*: on empty input, if True, return last history item.
|
507
|
-
- *display*: pass False to not display input (for sensitive input, e.g. a password)
|
508
|
-
- *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).
|
509
517
|
- *completer*: ``completer(val, idx)`` is called on TAB to get next completed value.
|
510
518
|
- *updater*: ``updater(val)`` is called every keypress or timeout.
|
511
519
|
- *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
|
@@ -528,7 +536,8 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
528
536
|
import getpass
|
529
537
|
return getpass.getpass(prompt)
|
530
538
|
|
531
|
-
|
539
|
+
if not history:
|
540
|
+
history = list(vd.inputHistory.setdefault(type, {}).keys())
|
532
541
|
|
533
542
|
y = sheet.windowHeight-dy-1
|
534
543
|
promptlen = dispwidth(prompt)
|
@@ -542,15 +551,14 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
542
551
|
w = kwargs.pop('w', _drawPrompt())
|
543
552
|
ret = vd.editText(y, promptlen, w=w,
|
544
553
|
attr=colors.color_edit_cell,
|
545
|
-
|
546
|
-
truncchar=options.disp_truncator,
|
554
|
+
options=vd.options,
|
547
555
|
history=history,
|
548
556
|
updater=_drawPrompt,
|
549
557
|
**kwargs)
|
550
558
|
|
551
559
|
if ret:
|
552
|
-
|
553
|
-
|
560
|
+
if kwargs.get('record', True) and kwargs.get('display', True):
|
561
|
+
vd.addInputHistory(ret, type=type)
|
554
562
|
elif defaultLast:
|
555
563
|
history or vd.fail("no previous input")
|
556
564
|
ret = history[-1]
|
@@ -561,7 +569,7 @@ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None,
|
|
561
569
|
@VisiData.api
|
562
570
|
def confirm(vd, prompt, exc=EscapeException):
|
563
571
|
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
|
564
|
-
if options.batch and not options.interactive:
|
572
|
+
if vd.options.batch and not vd.options.interactive:
|
565
573
|
return vd.fail('cannot confirm in batch mode: ' + prompt)
|
566
574
|
|
567
575
|
yn = vd.input(prompt, value='no', record=False)[:1]
|
@@ -628,9 +636,7 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
628
636
|
bindings.update(kwargs.get('bindings', {}))
|
629
637
|
kwargs['bindings'] = bindings
|
630
638
|
|
631
|
-
editargs = dict(value=value,
|
632
|
-
fillchar=self.options.disp_edit_fill,
|
633
|
-
truncchar=self.options.disp_truncator)
|
639
|
+
editargs = dict(value=value, options=self.options)
|
634
640
|
|
635
641
|
editargs.update(kwargs) # update with user-specified args
|
636
642
|
r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
|
@@ -641,4 +647,4 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
|
|
641
647
|
return r
|
642
648
|
|
643
649
|
|
644
|
-
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
|
|