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.
Files changed (149) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -202
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +7 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +54 -20
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +17 -2
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +16 -3
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/status_source.py +3 -1
  34. visidata/features/sysedit.py +1 -1
  35. visidata/features/transpose.py +2 -1
  36. visidata/features/type_ipaddr.py +2 -4
  37. visidata/features/unfurl.py +1 -0
  38. visidata/form.py +2 -2
  39. visidata/freqtbl.py +16 -11
  40. visidata/fuzzymatch.py +1 -0
  41. visidata/graph.py +163 -12
  42. visidata/guide.py +57 -24
  43. visidata/guides/ClipboardGuide.md +48 -0
  44. visidata/guides/ColumnsGuide.md +52 -0
  45. visidata/guides/CommandsSheet.md +28 -0
  46. visidata/guides/DirSheet.md +34 -0
  47. visidata/guides/ErrorsSheet.md +17 -0
  48. visidata/guides/FrequencyTable.md +42 -0
  49. visidata/guides/GrepSheet.md +28 -0
  50. visidata/guides/JsonSheet.md +38 -0
  51. visidata/guides/MacrosSheet.md +19 -0
  52. visidata/guides/MeltGuide.md +52 -0
  53. visidata/guides/MemorySheet.md +7 -0
  54. visidata/guides/MenuGuide.md +26 -0
  55. visidata/guides/ModifyGuide.md +38 -0
  56. visidata/guides/PivotGuide.md +71 -0
  57. visidata/guides/RegexGuide.md +107 -0
  58. visidata/guides/SelectionGuide.md +44 -0
  59. visidata/guides/SlideGuide.md +26 -0
  60. visidata/guides/SortGuide.md +0 -0
  61. visidata/guides/SplitpaneGuide.md +15 -0
  62. visidata/guides/TypesSheet.md +43 -0
  63. visidata/guides/XsvGuide.md +36 -0
  64. visidata/help.py +6 -6
  65. visidata/hint.py +2 -1
  66. visidata/indexsheet.py +2 -2
  67. visidata/interface.py +13 -14
  68. visidata/keys.py +4 -1
  69. visidata/loaders/api_airtable.py +1 -1
  70. visidata/loaders/archive.py +1 -1
  71. visidata/loaders/csv.py +9 -5
  72. visidata/loaders/eml.py +11 -6
  73. visidata/loaders/f5log.py +1 -0
  74. visidata/loaders/fec.py +18 -42
  75. visidata/loaders/fixed_width.py +19 -3
  76. visidata/loaders/grep.py +121 -0
  77. visidata/loaders/html.py +1 -0
  78. visidata/loaders/http.py +6 -1
  79. visidata/loaders/json.py +22 -4
  80. visidata/loaders/jsonla.py +8 -2
  81. visidata/loaders/mailbox.py +1 -0
  82. visidata/loaders/markdown.py +25 -6
  83. visidata/loaders/msgpack.py +19 -0
  84. visidata/loaders/npy.py +0 -1
  85. visidata/loaders/odf.py +18 -4
  86. visidata/loaders/orgmode.py +1 -1
  87. visidata/loaders/rec.py +6 -4
  88. visidata/loaders/sas.py +11 -4
  89. visidata/loaders/scrape.py +0 -1
  90. visidata/loaders/texttables.py +2 -0
  91. visidata/loaders/tsv.py +24 -7
  92. visidata/loaders/unzip_http.py +127 -3
  93. visidata/loaders/vds.py +4 -0
  94. visidata/loaders/vdx.py +1 -1
  95. visidata/loaders/xlsx.py +5 -0
  96. visidata/loaders/xml.py +2 -1
  97. visidata/macros.py +14 -31
  98. visidata/main.py +14 -13
  99. visidata/mainloop.py +14 -6
  100. visidata/man/vd.1 +72 -39
  101. visidata/man/vd.txt +72 -41
  102. visidata/memory.py +15 -4
  103. visidata/menu.py +14 -3
  104. visidata/metasheets.py +5 -6
  105. visidata/modify.py +4 -4
  106. visidata/mouse.py +2 -0
  107. visidata/movement.py +14 -28
  108. visidata/optionssheet.py +3 -5
  109. visidata/path.py +59 -37
  110. visidata/pivot.py +8 -5
  111. visidata/pyobj.py +63 -9
  112. visidata/save.py +16 -9
  113. visidata/search.py +4 -4
  114. visidata/selection.py +10 -56
  115. visidata/settings.py +37 -35
  116. visidata/sheets.py +186 -108
  117. visidata/shell.py +22 -12
  118. visidata/sidebar.py +71 -16
  119. visidata/sort.py +21 -6
  120. visidata/statusbar.py +42 -5
  121. visidata/stored_list.py +5 -2
  122. visidata/tests/conftest.py +1 -0
  123. visidata/tests/test_commands.py +9 -1
  124. visidata/tests/test_completer.py +18 -0
  125. visidata/tests/test_edittext.py +3 -2
  126. visidata/text_source.py +7 -4
  127. visidata/textsheet.py +20 -6
  128. visidata/themes/ascii8.py +9 -6
  129. visidata/themes/asciimono.py +14 -4
  130. visidata/threads.py +13 -3
  131. visidata/tuiwin.py +5 -1
  132. visidata/type_currency.py +1 -2
  133. visidata/type_date.py +6 -1
  134. visidata/undo.py +10 -5
  135. visidata/utils.py +9 -3
  136. visidata/vdobj.py +21 -1
  137. visidata/wrappers.py +9 -1
  138. {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  139. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
  140. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
  141. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
  142. visidata-3.1.dist-info/RECORD +284 -0
  143. visidata-3.0.2.dist-info/RECORD +0 -258
  144. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
  145. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  146. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  147. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  148. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  149. {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.0.2'
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*, which are mappings of names to functions.
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
- vd.importStar('visidata.deprecated')
142
+ import visidata.deprecated
144
143
 
145
- vd.importStar('builtins')
146
- vd.importStar('copy')
147
- vd.importStar('math')
148
- vd.importStar('random')
149
- vd.importStar('itertools')
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, options, colors, dispwidth, ColorAttr
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 = 1 # current level of help shown (up to vd.options.disp_help as maximum)
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, help:str=''):
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
- curhelp = ''
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
- while True:
260
- vd.drawSheet(scr, vd.activeSheet)
261
- updater(v)
262
- disp_help.draw()
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
- if display:
265
- dispval = clean_printable(v)
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(v)
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
- dispi = w-1
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
- dispi = w-(len(dispval)-i)
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
- dispi = w//2 # visual cursor stays right in the middle
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[i-w//2+1:i+w//2-k] + right_truncchar
284
-
285
- prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear, literal=True)
286
- clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear, literal=True)
287
- if scr: scr.move(y, x+prew)
288
- ch = vd.getkeystroke(scr)
289
- if ch == '': continue
290
- elif ch in bindings: v, i = bindings[ch](v, i)
291
- elif ch == 'KEY_IC': insert_mode = not insert_mode
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', ESC): raise EscapeException(ch)
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
- disp_help.cycle()
300
- continue # not considered a first keypress
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 == TAB: v, i = complete_state.complete(v, i, +1)
303
- elif ch == 'KEY_BTAB': v, i = complete_state.complete(v, i, -1)
304
- elif ch in ['^J', '^M']: break # ENTER to accept value
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': v = vd.launchExternalEditor(v); break
314
- elif ch == '^R': v = str(value) # ^Reload initial value
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': v, i = history_state.up(v, i)
327
- elif history and ch == 'KEY_DOWN': v, i = history_state.down(v, i)
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
- first_action = False
344
- complete_state.reset()
282
+ self.current_i = i
283
+ self.value = v
284
+ self.first_action = False
285
+ self.reset_completion()
286
+ return False
345
287
 
346
- return v
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, record=True, display=True, **kwargs):
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
- v = vd.editline(vd.activeSheet._scr, y, x, w, display=display, **kwargs)
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 'value' in kwargs:
371
- starting_value = kwargs['value']
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*starting_value/100
377
+ v = pct*value/100
375
378
 
376
- v = type(starting_value)(v)
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
- return json.loads(previnput)
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
- return previnput
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
- with HelpCycler() as disp_help:
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
- if record:
485
- lastargs = {}
486
- for k, input_kwargs in kwargs.items():
487
- v = input_kwargs.get('value', '')
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
- history = list(vd.inputHistory.setdefault(type, {}).keys())
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
- unprintablechar=options.disp_unprintable,
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
- vd.addInputHistory(ret, type=type)
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({'CompleteKey': CompleteKey, 'AcceptInput': AcceptInput})
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