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.
Files changed (151) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -199
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +9 -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 +66 -24
  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 +23 -4
  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 +18 -5
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/slide.py +15 -17
  34. visidata/features/status_source.py +3 -1
  35. visidata/features/sysedit.py +1 -1
  36. visidata/features/transpose.py +2 -1
  37. visidata/features/type_ipaddr.py +2 -4
  38. visidata/features/unfurl.py +1 -0
  39. visidata/form.py +2 -2
  40. visidata/freqtbl.py +16 -11
  41. visidata/fuzzymatch.py +1 -0
  42. visidata/graph.py +173 -12
  43. visidata/guide.py +61 -25
  44. visidata/guides/ClipboardGuide.md +48 -0
  45. visidata/guides/ColumnsGuide.md +52 -0
  46. visidata/guides/CommandsSheet.md +28 -0
  47. visidata/guides/DirSheet.md +34 -0
  48. visidata/guides/ErrorsSheet.md +17 -0
  49. visidata/guides/FrequencyTable.md +42 -0
  50. visidata/guides/GrepSheet.md +28 -0
  51. visidata/guides/JsonSheet.md +38 -0
  52. visidata/guides/MacrosSheet.md +19 -0
  53. visidata/guides/MeltGuide.md +52 -0
  54. visidata/guides/MemorySheet.md +7 -0
  55. visidata/guides/MenuGuide.md +26 -0
  56. visidata/guides/ModifyGuide.md +38 -0
  57. visidata/guides/PivotGuide.md +71 -0
  58. visidata/guides/RegexGuide.md +107 -0
  59. visidata/guides/SelectionGuide.md +44 -0
  60. visidata/guides/SlideGuide.md +26 -0
  61. visidata/guides/SortGuide.md +0 -0
  62. visidata/guides/SplitpaneGuide.md +15 -0
  63. visidata/guides/TypesSheet.md +43 -0
  64. visidata/guides/XsvGuide.md +36 -0
  65. visidata/help.py +6 -6
  66. visidata/hint.py +2 -1
  67. visidata/indexsheet.py +2 -2
  68. visidata/interface.py +13 -14
  69. visidata/keys.py +4 -1
  70. visidata/loaders/api_airtable.py +1 -1
  71. visidata/loaders/archive.py +1 -1
  72. visidata/loaders/csv.py +9 -5
  73. visidata/loaders/eml.py +11 -6
  74. visidata/loaders/f5log.py +1 -0
  75. visidata/loaders/fec.py +18 -42
  76. visidata/loaders/fixed_width.py +19 -3
  77. visidata/loaders/grep.py +121 -0
  78. visidata/loaders/html.py +1 -0
  79. visidata/loaders/http.py +6 -1
  80. visidata/loaders/json.py +22 -4
  81. visidata/loaders/jsonla.py +8 -2
  82. visidata/loaders/mailbox.py +1 -0
  83. visidata/loaders/markdown.py +25 -6
  84. visidata/loaders/msgpack.py +19 -0
  85. visidata/loaders/npy.py +0 -1
  86. visidata/loaders/odf.py +18 -4
  87. visidata/loaders/orgmode.py +1 -1
  88. visidata/loaders/rec.py +6 -4
  89. visidata/loaders/sas.py +11 -4
  90. visidata/loaders/scrape.py +0 -1
  91. visidata/loaders/texttables.py +2 -0
  92. visidata/loaders/tsv.py +24 -7
  93. visidata/loaders/unzip_http.py +127 -3
  94. visidata/loaders/vds.py +4 -0
  95. visidata/loaders/vdx.py +1 -1
  96. visidata/loaders/xlsx.py +5 -0
  97. visidata/loaders/xml.py +2 -1
  98. visidata/macros.py +14 -31
  99. visidata/main.py +20 -15
  100. visidata/mainloop.py +17 -6
  101. visidata/man/vd.1 +74 -39
  102. visidata/man/vd.txt +73 -41
  103. visidata/memory.py +16 -5
  104. visidata/menu.py +14 -3
  105. visidata/metasheets.py +5 -6
  106. visidata/modify.py +4 -4
  107. visidata/mouse.py +2 -0
  108. visidata/movement.py +14 -28
  109. visidata/optionssheet.py +3 -5
  110. visidata/path.py +59 -37
  111. visidata/pivot.py +8 -5
  112. visidata/pyobj.py +63 -9
  113. visidata/rename_col.py +18 -1
  114. visidata/save.py +16 -9
  115. visidata/search.py +4 -4
  116. visidata/selection.py +10 -56
  117. visidata/settings.py +37 -35
  118. visidata/sheets.py +189 -118
  119. visidata/shell.py +23 -14
  120. visidata/sidebar.py +71 -16
  121. visidata/sort.py +21 -6
  122. visidata/statusbar.py +42 -5
  123. visidata/stored_list.py +5 -2
  124. visidata/tests/conftest.py +1 -0
  125. visidata/tests/test_commands.py +9 -1
  126. visidata/tests/test_completer.py +18 -0
  127. visidata/tests/test_edittext.py +3 -2
  128. visidata/text_source.py +7 -4
  129. visidata/textsheet.py +20 -6
  130. visidata/themes/ascii8.py +9 -6
  131. visidata/themes/asciimono.py +14 -4
  132. visidata/threads.py +13 -3
  133. visidata/tuiwin.py +5 -1
  134. visidata/type_currency.py +1 -2
  135. visidata/type_date.py +6 -1
  136. visidata/undo.py +10 -13
  137. visidata/utils.py +9 -3
  138. visidata/vdobj.py +21 -1
  139. visidata/wrappers.py +9 -1
  140. {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  141. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
  142. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
  143. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
  144. visidata-3.1.dist-info/RECORD +284 -0
  145. visidata-3.0.1.dist-info/RECORD +0 -258
  146. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
  147. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  148. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  149. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  150. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  151. {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.0.1'
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,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, help:str=''):
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
- curhelp = ''
124
- if vd.disp_help == 0:
125
- vd.drawSidebar(scr, sheet)
126
- elif vd.disp_help == 1:
127
- curhelp = help
128
- sheet.drawSidebarText(scr, curhelp)
129
- elif vd.disp_help >= 2:
130
- curhelp = vd.getHelpPane('input', module='visidata')
131
- sheet.drawSidebarText(scr, curhelp, title='Input Keystrokes Help')
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
- while True:
259
- updater(v)
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
- if display:
262
- dispval = clean_printable(v)
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(v)
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
- dispi = w-1
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
- dispi = w-(len(dispval)-i)
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
- dispi = w//2 # visual cursor stays right in the middle
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[i-w//2+1:i+w//2-k] + right_truncchar
281
-
282
- prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear, literal=True)
283
- clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear, literal=True)
284
- if scr: scr.move(y, x+prew)
285
- ch = vd.getkeystroke(scr)
286
- if ch == '': continue
287
- elif ch in bindings: v, i = bindings[ch](v, i)
288
- 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
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', ESC): raise EscapeException(ch)
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
- disp_help.cycle()
297
- continue # not considered a first keypress
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 == TAB: v, i = complete_state.complete(v, i, +1)
300
- elif ch == 'KEY_BTAB': v, i = complete_state.complete(v, i, -1)
301
- 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
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': v = vd.launchExternalEditor(v); break
311
- 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
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': v, i = history_state.up(v, i)
324
- 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)
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
- first_action = False
341
- complete_state.reset()
282
+ self.current_i = i
283
+ self.value = v
284
+ self.first_action = False
285
+ self.reset_completion()
286
+ return False
342
287
 
343
- 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
344
335
 
345
336
 
346
337
  @VisiData.api
347
- 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):
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
- 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)
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 'value' in kwargs:
368
- starting_value = kwargs['value']
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*starting_value/100
377
+ v = pct*value/100
372
378
 
373
- v = type(starting_value)(v)
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
- return json.loads(previnput)
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
- return previnput
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
- with HelpCycler() as disp_help:
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
- if record:
482
- lastargs = {}
483
- for k, input_kwargs in kwargs.items():
484
- v = input_kwargs.get('value', '')
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
- history = list(vd.inputHistory.setdefault(type, {}).keys())
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
- unprintablechar=options.disp_unprintable,
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
- vd.addInputHistory(ret, type=type)
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({'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