visidata 2.11.dev0__py3-none-any.whl → 3.0__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 (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
visidata/_input.py CHANGED
@@ -3,25 +3,48 @@ import curses
3
3
 
4
4
  import visidata
5
5
 
6
- from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData
7
- from visidata import vd, options, colors
8
- from visidata import suspend, ColumnItem, AttrDict
6
+ from visidata import EscapeException, ExpectedException, clipdraw, Sheet, VisiData, BaseSheet
7
+ from visidata import vd, options, colors, dispwidth, ColorAttr
8
+ from visidata import AttrDict
9
9
 
10
10
 
11
- vd.option('color_edit_cell', 'white', 'cell color to use when editing cell')
12
- vd.option('disp_edit_fill', '_', 'edit field fill character')
13
- vd.option('disp_unprintable', '·', 'substitute character for unprintables')
11
+ vd.theme_option('color_edit_unfocused', '238 on 110', 'display color for unfocused input in form')
12
+ vd.theme_option('color_edit_cell', '233 on 110', 'cell color to use when editing cell')
13
+ vd.theme_option('disp_edit_fill', '_', 'edit field fill character')
14
+ vd.theme_option('disp_unprintable', '·', 'substitute character for unprintables')
15
+ vd.theme_option('mouse_interval', 1, 'max time between press/release for click (ms)', sheettype=None)
14
16
 
15
- vd.option('input_history', '', 'basename of file to store persistent input history')
17
+ vd.disp_help = 1 # current level of help shown (up to vd.options.disp_help as maximum)
16
18
 
17
19
  class AcceptInput(Exception):
18
20
  '*args[0]* is the input to be accepted'
19
21
 
20
- visidata.vd._nextCommands = []
22
+ vd._injectedInput = None # for vd.injectInput
23
+
24
+
25
+ @VisiData.api
26
+ def injectInput(vd, x):
27
+ 'Use *x* as input to next command.'
28
+ assert vd._injectedInput is None, vd._injectedInput
29
+ vd._injectedInput = x
30
+
21
31
 
22
32
  @VisiData.api
23
- def queueCommand(vd, longname): #, input=None, sheet=None, col=None, row=None):
24
- vd._nextCommands.append(longname)
33
+ def getCommandInput(vd):
34
+ if vd._injectedInput is not None:
35
+ r = vd._injectedInput
36
+ vd._injectedInput = None
37
+ return r
38
+
39
+ return vd.getLastArgs()
40
+
41
+
42
+ @BaseSheet.after
43
+ def execCommand(sheet, longname, *args, **kwargs):
44
+ if vd._injectedInput is not None:
45
+ vd.debug(f'{longname} did not consume input "{vd._injectedInput}"')
46
+ vd._injectedInput = None
47
+
25
48
 
26
49
  def acceptThenFunc(*longnames):
27
50
  def _acceptthen(v, i):
@@ -52,10 +75,12 @@ def until_get_wch(scr):
52
75
  ret = None
53
76
  while not ret:
54
77
  try:
55
- ret = scr.get_wch()
78
+ ret = vd.get_wch(scr)
56
79
  except curses.error:
57
80
  pass
58
81
 
82
+ if isinstance(ret, int):
83
+ return chr(ret)
59
84
  return ret
60
85
 
61
86
 
@@ -64,6 +89,48 @@ def splice(v:str, i:int, s:str):
64
89
  return v if i < 0 else v[:i] + s + v[i:]
65
90
 
66
91
 
92
+ # vd.options.disp_help is the effective maximum disp_help. The user can cycle through the various levels of help.
93
+ class HelpCycler:
94
+ def __init__(self, scr=None, help=''):
95
+ self.help = help
96
+ self.scr = scr
97
+
98
+ def __enter__(self):
99
+ if self.scr:
100
+ vd.drawInputHelp(self.scr, self.help)
101
+
102
+ return self
103
+
104
+ def __exit__(self, *args):
105
+ pass
106
+
107
+ def cycle(self):
108
+ vd.disp_help = (vd.disp_help-1)%(vd.options.disp_help+1)
109
+ if self.scr:
110
+ vd.drawInputHelp(self.scr, self.help)
111
+
112
+
113
+ @VisiData.api
114
+ def drawInputHelp(vd, scr, help:str=''):
115
+ if not scr or not vd.cursesEnabled:
116
+ return
117
+
118
+ sheet = vd.activeSheet
119
+ if not sheet:
120
+ return
121
+ vd.drawSheet(scr, sheet)
122
+
123
+ curhelp = ''
124
+ if vd.disp_help == 0:
125
+ vd.drawSidebar(scr, sheet)
126
+ elif vd.disp_help == 1:
127
+ curhelp = help
128
+ sheet.drawSidebarText(scr, curhelp)
129
+ elif vd.disp_help >= 2:
130
+ curhelp = vd.getHelpPane('input', module='visidata')
131
+ sheet.drawSidebarText(scr, curhelp, title='Input Keystrokes Help')
132
+
133
+
67
134
  def clean_printable(s):
68
135
  'Escape unprintable characters.'
69
136
  return ''.join(c if c.isprintable() else options.disp_unprintable for c in str(s))
@@ -138,14 +205,26 @@ class HistoryState:
138
205
 
139
206
  # history: earliest entry first
140
207
  @VisiData.api
141
- def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' ', truncchar='-', unprintablechar='.', completer=lambda text,idx: None, history=[], display=True, updater=lambda val: None, bindings={}, clear=True):
208
+ def editline(vd, scr, y, x, w, i=0,
209
+ attr=ColorAttr(),
210
+ value='',
211
+ fillchar=' ',
212
+ truncchar='-',
213
+ unprintablechar='.',
214
+ completer=lambda text,idx: None,
215
+ history=[],
216
+ display=True,
217
+ updater=lambda val: None,
218
+ bindings={},
219
+ help='', # str|HelpPane
220
+ clear=True):
142
221
  '''A better curses line editing widget.
143
222
  If *clear* is True, clear whole editing area before displaying.
144
223
  '''
145
224
  with EnableCursor():
225
+ with HelpCycler(scr, help) as disp_help:
146
226
  ESC='^['
147
227
  TAB='^I'
148
-
149
228
  history_state = HistoryState(history)
150
229
  complete_state = CompleteState(completer)
151
230
  insert_mode = True
@@ -176,10 +255,8 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
176
255
  a += incr
177
256
  return min(max(a, 0), len(s))
178
257
 
179
-
180
258
  while True:
181
259
  updater(v)
182
- vd.getHelpPane('input', module=__name__).draw(scr, y=y)
183
260
 
184
261
  if display:
185
262
  dispval = clean_printable(v)
@@ -202,11 +279,12 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
202
279
  k = 1 if w%2==0 else 0 # odd widths have one character more
203
280
  dispval = left_truncchar + dispval[i-w//2+1:i+w//2-k] + right_truncchar
204
281
 
205
- prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear)
206
- clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear)
207
- scr.move(y, x+prew)
282
+ prew = clipdraw(scr, y, x, dispval[:dispi], attr, w, clear=clear, literal=True)
283
+ clipdraw(scr, y, x+prew, dispval[dispi:], attr, w-prew+1, clear=clear, literal=True)
284
+ if scr: scr.move(y, x+prew)
208
285
  ch = vd.getkeystroke(scr)
209
286
  if ch == '': continue
287
+ elif ch in bindings: v, i = bindings[ch](v, i)
210
288
  elif ch == 'KEY_IC': insert_mode = not insert_mode
211
289
  elif ch == '^A' or ch == 'KEY_HOME': i = 0
212
290
  elif ch == '^B' or ch == 'KEY_LEFT': i -= 1
@@ -214,20 +292,29 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
214
292
  elif ch == '^D' or ch == 'KEY_DC': v = delchar(v, i)
215
293
  elif ch == '^E' or ch == 'KEY_END': i = len(v)
216
294
  elif ch == '^F' or ch == 'KEY_RIGHT': i += 1
217
- elif ch == '^G': vd.show_help = not vd.show_help; continue # not a first keypress
295
+ elif ch == '^G':
296
+ disp_help.cycle()
297
+ continue # not considered a first keypress
218
298
  elif ch in ('^H', 'KEY_BACKSPACE', '^?'): i -= 1; v = delchar(v, i)
219
299
  elif ch == TAB: v, i = complete_state.complete(v, i, +1)
220
300
  elif ch == 'KEY_BTAB': v, i = complete_state.complete(v, i, -1)
221
301
  elif ch in ['^J', '^M']: break # ENTER to accept value
222
302
  elif ch == '^K': v = v[:i] # ^Kill to end-of-line
223
- elif ch == '^O': v = vd.launchExternalEditor(v)
303
+ elif ch == '^N':
304
+ c = ''
305
+ while not c:
306
+ c = vd.getkeystroke(scr)
307
+ c = vd.prettykeys(c)
308
+ i += len(c)
309
+ v += c
310
+ elif ch == '^O': v = vd.launchExternalEditor(v); break
224
311
  elif ch == '^R': v = str(value) # ^Reload initial value
225
- elif ch == '^T': v = delchar(splice(v, i-2, v[i-1]), i) # swap chars
312
+ elif ch == '^T': v = delchar(splice(v, i-2, v[i-1:i]), i) # swap chars
226
313
  elif ch == '^U': v = v[i:]; i = 0 # clear to beginning
227
314
  elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character
228
315
  elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word
229
316
  elif ch == '^Y': v = splice(v, i, str(vd.memory.clipval))
230
- elif ch == '^Z': suspend()
317
+ elif ch == '^Z': vd.suspend()
231
318
  # CTRL+arrow
232
319
  elif ch == 'kLFT5': i = find_nonword(v, 0, i-1, -1)+1; # word left
233
320
  elif ch == 'kRIT5': i = find_nonword(v, i+1, len(v)-1, +1)+1; # word right
@@ -235,7 +322,6 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
235
322
  elif ch == 'kDN5': pass
236
323
  elif history and ch == 'KEY_UP': v, i = history_state.up(v, i)
237
324
  elif history and ch == 'KEY_DOWN': v, i = history_state.down(v, i)
238
- elif ch in bindings: v, i = bindings[ch](v, i)
239
325
  elif len(ch) > 1: pass
240
326
  else:
241
327
  if first_action:
@@ -254,7 +340,7 @@ def editline(vd, scr, y, x, w, i=0, attr=curses.A_NORMAL, value='', fillchar=' '
254
340
  first_action = False
255
341
  complete_state.reset()
256
342
 
257
- return type(value)(v)
343
+ return v
258
344
 
259
345
 
260
346
  @VisiData.api
@@ -262,7 +348,7 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
262
348
  'Invoke modal single-line editor at (*y*, *x*) for *w* terminal chars. Use *display* is False for sensitive input like passphrases. If *record* is True, get input from the cmdlog in batch mode, and save input to the cmdlog if *display* is also True. Return new value as string.'
263
349
  v = None
264
350
  if record and vd.cmdlog:
265
- v = vd.getLastArgs()
351
+ v = vd.getCommandInput()
266
352
 
267
353
  if v is None:
268
354
  try:
@@ -270,27 +356,32 @@ def editText(vd, y, x, w, record=True, display=True, **kwargs):
270
356
  except AcceptInput as e:
271
357
  v = e.args[0]
272
358
 
273
- if vd.scrFull: # check if curses initialised
359
+ if vd.cursesEnabled:
274
360
  # clear keyboard buffer to neutralize multi-line pastes (issue#585)
275
361
  curses.flushinp()
276
362
 
277
363
  if display:
278
- vd.status('"%s"' % v)
279
364
  if record and vd.cmdlog:
280
365
  vd.setLastArgs(v)
281
366
 
282
- return v
367
+ if 'value' in kwargs:
368
+ starting_value = kwargs['value']
369
+ if isinstance(starting_value, (int, float)) and v[-1] == '%': #2082
370
+ pct = float(v[:-1])
371
+ v = pct*starting_value/100
372
+
373
+ v = type(starting_value)(v)
283
374
 
375
+ return v
284
376
 
285
377
  @VisiData.api
286
378
  def inputsingle(vd, prompt, record=True):
287
379
  'Display prompt and return single character of user input.'
288
380
  sheet = vd.activeSheet
289
- rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
290
381
 
291
382
  v = None
292
383
  if record and vd.cmdlog:
293
- v = vd.getLastArgs()
384
+ v = vd.getCommandInput()
294
385
 
295
386
  if v is not None:
296
387
  return v
@@ -300,16 +391,111 @@ def inputsingle(vd, prompt, record=True):
300
391
  rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
301
392
  promptlen = clipdraw(sheet._scr, y, 0, prompt, 0, w=w-rstatuslen-1)
302
393
  sheet._scr.move(y, w-promptlen-rstatuslen-2)
303
- v = vd.getkeystroke(sheet._scr)
394
+
395
+ while not v:
396
+ v = vd.getkeystroke(sheet._scr)
304
397
 
305
398
  if record and vd.cmdlog:
306
399
  vd.setLastArgs(v)
307
400
 
308
401
  return v
309
402
 
403
+ @VisiData.api
404
+ def inputMultiple(vd, updater=lambda val: None, record=True, **kwargs):
405
+ 'A simple form, where each input is an entry in `kwargs`, with the key being the key in the returned dict, and the value being a dictionary of kwargs to the singular input().'
406
+ sheet = vd.activeSheet
407
+ scr = sheet._scr
408
+
409
+ previnput = vd.getCommandInput()
410
+ if previnput is not None:
411
+ if isinstance(previnput, str):
412
+ if previnput.startswith('{'):
413
+ return json.loads(previnput)
414
+ else:
415
+ ret = {k:v.get('value', '') for k,v in kwargs.items()}
416
+ primekey = list(ret.keys())[0]
417
+ ret[primekey] = previnput
418
+ return ret
419
+
420
+ if isinstance(previnput, dict):
421
+ return previnput
422
+
423
+ assert False, type(previnput)
424
+
425
+ y = sheet.windowHeight-1
426
+ maxw = sheet.windowWidth//2
427
+ attr = colors.color_edit_unfocused
428
+
429
+ keys = list(kwargs.keys())
430
+ cur_input_key = keys[0]
431
+
432
+ if scr:
433
+ scr.erase()
434
+
435
+ for i, (k, v) in enumerate(kwargs.items()):
436
+ v['dy'] = i
437
+ v['w'] = maxw-dispwidth(v.get('prompt'))
438
+
439
+ class ChangeInput(Exception):
440
+ pass
441
+
442
+ def change_input(offset):
443
+ def _throw(v, i):
444
+ if scr:
445
+ scr.erase()
446
+ raise ChangeInput(v, offset)
447
+ return _throw
448
+
449
+ def _drawPrompt(val):
450
+ for k, v in kwargs.items():
451
+ maxw = min(sheet.windowWidth-1, max(dispwidth(v.get('prompt')), dispwidth(str(v.get('value', '')))))
452
+ promptlen = clipdraw(scr, y-v.get('dy'), 0, v.get('prompt'), attr, w=maxw) #1947
453
+ promptlen = clipdraw(scr, y-v.get('dy'), promptlen, v.get('value', ''), attr, w=maxw)
454
+
455
+ return updater(val)
456
+
457
+ with HelpCycler() as disp_help:
458
+ while True:
459
+ try:
460
+ input_kwargs = kwargs[cur_input_key]
461
+ input_kwargs['value'] = vd.input(**input_kwargs,
462
+ attr=colors.color_edit_cell,
463
+ updater=_drawPrompt,
464
+ record=False,
465
+ bindings={
466
+ 'KEY_BTAB': change_input(-1),
467
+ '^I': change_input(+1),
468
+ 'KEY_SR': change_input(-1),
469
+ 'KEY_SF': change_input(+1),
470
+ 'kUP': change_input(-1),
471
+ 'kDN': change_input(+1),
472
+ })
473
+ break
474
+ except ChangeInput as e:
475
+ input_kwargs['value'] = e.args[0]
476
+ offset = e.args[1]
477
+ i = keys.index(cur_input_key)
478
+ cur_input_key = keys[(i+offset)%len(keys)]
479
+
480
+ retargs = {}
481
+ if record:
482
+ lastargs = {}
483
+ for k, input_kwargs in kwargs.items():
484
+ v = input_kwargs.get('value', '')
485
+ retargs[k] = v
486
+
487
+ if input_kwargs.get('display', True):
488
+ lastargs[k] = v
489
+ vd.addInputHistory(v, input_kwargs.get('type', ''))
490
+
491
+ if vd.cmdlog and lastargs:
492
+ vd.setLastArgs(lastargs)
493
+
494
+ return retargs
495
+
310
496
 
311
497
  @VisiData.api
312
- def input(self, prompt, type=None, defaultLast=False, history=[], **kwargs):
498
+ def input(vd, prompt, type=None, defaultLast=False, history=[], dy=0, attr=None, updater=lambda v: None, **kwargs):
313
499
  '''Display *prompt* and return line of user input.
314
500
 
315
501
  - *type*: string indicating the type of input to use for history.
@@ -320,23 +506,47 @@ def input(self, prompt, type=None, defaultLast=False, history=[], **kwargs):
320
506
  - *completer*: ``completer(val, idx)`` is called on TAB to get next completed value.
321
507
  - *updater*: ``updater(val)`` is called every keypress or timeout.
322
508
  - *bindings*: dict of keystroke to func(v, i) that returns updated (v, i)
509
+ - *dy*: number of lines from bottom of pane
510
+ - *attr*: curses attribute for prompt
511
+ - *help*: string to include in help
323
512
  '''
324
513
 
325
- history = self.lastInputsSheet.history(type)
514
+ if attr is None:
515
+ attr = ColorAttr()
516
+ sheet = vd.activeSheet
517
+ if not vd.cursesEnabled:
518
+ if kwargs.get('record', True) and vd.cmdlog:
519
+ return vd.getCommandInput()
520
+
521
+ if kwargs.get('display', True):
522
+ import builtins
523
+ return builtins.input(prompt)
524
+ else:
525
+ import getpass
526
+ return getpass.getpass(prompt)
527
+
528
+ history = list(vd.inputHistory.setdefault(type, {}).keys())
529
+
530
+ y = sheet.windowHeight-dy-1
531
+ promptlen = dispwidth(prompt)
326
532
 
327
- sheet = self.activeSheet
328
- rstatuslen = self.drawRightStatus(sheet._scr, sheet)
329
- attr = 0
330
- promptlen = clipdraw(sheet._scr, sheet.windowHeight-1, 0, prompt, attr, w=sheet.windowWidth-rstatuslen-1)
331
- ret = self.editText(sheet.windowHeight-1, promptlen, sheet.windowWidth-promptlen-rstatuslen-2,
533
+ def _drawPrompt(val=''):
534
+ rstatuslen = vd.drawRightStatus(sheet._scr, sheet)
535
+ clipdraw(sheet._scr, y, 0, prompt, attr, w=sheet.windowWidth-rstatuslen-1)
536
+ updater(val)
537
+ return sheet.windowWidth-promptlen-rstatuslen-2
538
+
539
+ w = kwargs.pop('w', _drawPrompt())
540
+ ret = vd.editText(y, promptlen, w=w,
332
541
  attr=colors.color_edit_cell,
333
542
  unprintablechar=options.disp_unprintable,
334
543
  truncchar=options.disp_truncator,
335
544
  history=history,
545
+ updater=_drawPrompt,
336
546
  **kwargs)
337
547
 
338
548
  if ret:
339
- self.lastInputsSheet.appendRow(AttrDict(type=type, input=ret))
549
+ vd.addInputHistory(ret, type=type)
340
550
 
341
551
  elif defaultLast:
342
552
  history or vd.fail("no previous input")
@@ -348,7 +558,7 @@ def input(self, prompt, type=None, defaultLast=False, history=[], **kwargs):
348
558
  @VisiData.api
349
559
  def confirm(vd, prompt, exc=EscapeException):
350
560
  'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
351
- if options.batch:
561
+ if options.batch and not options.interactive:
352
562
  return vd.fail('cannot confirm in batch mode: ' + prompt)
353
563
 
354
564
  yn = vd.input(prompt, value='no', record=False)[:1]
@@ -401,17 +611,26 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
401
611
  'KEY_SF': acceptThenFunc('go-down', 'rename-col' if rowidx < 0 else 'edit-cell'),
402
612
  'KEY_SRIGHT': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
403
613
  'KEY_SLEFT': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
614
+ '^I': acceptThenFunc('go-right', 'rename-col' if rowidx < 0 else 'edit-cell'),
615
+ 'KEY_BTAB': acceptThenFunc('go-left', 'rename-col' if rowidx < 0 else 'edit-cell'),
404
616
  }
405
617
 
618
+ if vcolidx >= self.nVisibleCols-1:
619
+ bindings['^I'] = acceptThenFunc('go-down', 'go-leftmost', 'edit-cell')
620
+
621
+ if vcolidx <= 0:
622
+ bindings['KEY_BTAB'] = acceptThenFunc('go-up', 'go-rightmost', 'edit-cell')
623
+
624
+ # update local bindings with kwargs.bindings instead of the inverse, to preserve kwargs.bindings for caller
406
625
  bindings.update(kwargs.get('bindings', {}))
407
626
  kwargs['bindings'] = bindings
408
627
 
409
628
  editargs = dict(value=value,
410
- fillchar=options.disp_edit_fill,
411
- truncchar=options.disp_truncator)
629
+ fillchar=self.options.disp_edit_fill,
630
+ truncchar=self.options.disp_truncator)
412
631
 
413
632
  editargs.update(kwargs) # update with user-specified args
414
- r = vd.editText(y, x, w, **editargs)
633
+ r = vd.editText(y, x, w, attr=colors.color_edit_cell, **editargs)
415
634
 
416
635
  if rowidx >= 0: # if not header
417
636
  r = col.type(r) # convert input to column type, let exceptions be raised
@@ -419,4 +638,4 @@ def editCell(self, vcolidx=None, rowidx=None, value=None, **kwargs):
419
638
  return r
420
639
 
421
640
 
422
- vd.addGlobals({'CompleteKey': CompleteKey})
641
+ vd.addGlobals({'CompleteKey': CompleteKey, 'AcceptInput': AcceptInput})
visidata/_open.py CHANGED
@@ -1,4 +1,7 @@
1
- from visidata import *
1
+ import os
2
+ import os.path
3
+
4
+ from visidata import VisiData, vd, Path, BaseSheet, TableSheet, TextSheet, SettableColumn
2
5
 
3
6
 
4
7
  vd.option('filetype', '', 'specify file type', replay=True)
@@ -6,7 +9,13 @@ vd.option('filetype', '', 'specify file type', replay=True)
6
9
 
7
10
  @VisiData.api
8
11
  def inputFilename(vd, prompt, *args, **kwargs):
9
- return vd.input(prompt, type="filename", *args, completer=_completeFilename, **kwargs).strip()
12
+ completer= _completeFilename
13
+ if not vd.couldOverwrite(): #1805 don't suggest an existing file
14
+ completer = None
15
+ v = kwargs.get('value', '')
16
+ if v and Path(v).exists():
17
+ kwargs['value'] = ''
18
+ return vd.input(prompt, type="filename", *args, completer=completer, **kwargs).strip()
10
19
 
11
20
 
12
21
  @VisiData.api
@@ -35,6 +44,38 @@ def _completeFilename(val, state):
35
44
  return files[state%len(files)]
36
45
 
37
46
 
47
+ @VisiData.api
48
+ def guessFiletype(vd, p, *args, funcprefix='guess_'):
49
+ '''Call all vd.guess_<filetype>(p) functions and return best candidate sheet based on file contents.'''
50
+
51
+ guessfuncs = [getattr(vd, x) for x in dir(vd) if x.startswith(funcprefix)]
52
+ filetypes = []
53
+ for f in guessfuncs:
54
+ try:
55
+ filetype = f(p, *args)
56
+ if filetype:
57
+ filetype['_guesser'] = f.__name__
58
+ filetypes.append(filetype)
59
+ except FileNotFoundError:
60
+ pass
61
+ except Exception as e:
62
+ vd.debug(f'{f.__name__}: {e}')
63
+
64
+ if filetypes:
65
+ return sorted(filetypes, key=lambda r: -r.get('_likelihood', 1))[0]
66
+
67
+ return {}
68
+
69
+
70
+ @VisiData.api
71
+ def guess_extension(vd, path):
72
+ # try auto-detect from extension
73
+ ext = path.suffix[1:].lower()
74
+ openfunc = getattr(vd, f'open_{ext}', vd.getGlobals().get(f'open_{ext}'))
75
+ if openfunc:
76
+ return dict(filetype=ext, _likelihood=3)
77
+
78
+
38
79
  @VisiData.api
39
80
  def openPath(vd, p, filetype=None, create=False):
40
81
  '''Call ``open_<filetype>(p)`` or ``openurl_<p.scheme>(p, filetype)``. Return constructed but unloaded sheet of appropriate type.
@@ -53,27 +94,43 @@ def openPath(vd, p, filetype=None, create=False):
53
94
  return None
54
95
 
55
96
  if not filetype:
56
- if p.is_dir():
57
- filetype = 'dir'
58
- else:
59
- filetype = p.ext or options.filetype or 'txt'
97
+ filetype = p.ext or vd.options.filetype
60
98
 
61
99
  filetype = filetype.lower()
62
100
 
63
101
  if not p.exists():
64
- if not create:
65
- return None
66
102
  newfunc = getattr(vd, 'new_' + filetype, vd.getGlobals().get('new_' + filetype))
67
103
  if not newfunc:
68
104
  vd.warning('%s does not exist, creating new sheet' % p)
69
- return vd.newSheet(p.name, 1, source=p)
105
+ return vd.newSheet(p.base_stem, 1, source=p)
70
106
 
71
107
  vd.status('creating blank %s' % (p.given))
72
108
  return newfunc(p)
73
109
 
74
- openfunc = getattr(vd, 'open_' + filetype, vd.getGlobals().get('open_' + filetype))
110
+ if p.is_fifo():
111
+ # read the file as text, into a RepeatFile that can be opened multiple times
112
+ p = Path(p.given, fp=p.open(mode='rb'))
113
+
114
+ openfuncname = 'open_' + filetype
115
+ openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
75
116
  if not openfunc:
117
+ opts = vd.guessFiletype(p)
118
+ if opts and 'filetype' in opts:
119
+ filetype = opts['filetype']
120
+ openfuncname = 'open_' + filetype
121
+ openfunc = getattr(vd, openfuncname, vd.getGlobals().get(openfuncname))
122
+ if not openfunc:
123
+ vd.error(f'guessed {filetype} but no {openfuncname}')
124
+
125
+ vs = openfunc(p)
126
+ for k, v in opts.items():
127
+ if k != 'filetype' and not k.startswith('_'):
128
+ setattr(vs.options, k, v)
129
+ vd.warning('guessed "%s" filetype based on contents' % opts['filetype'])
130
+ return vs
131
+
76
132
  vd.warning('unknown "%s" filetype' % filetype)
133
+
77
134
  filetype = 'txt'
78
135
  openfunc = vd.open_txt
79
136
 
@@ -81,13 +138,16 @@ def openPath(vd, p, filetype=None, create=False):
81
138
 
82
139
  return openfunc(p)
83
140
 
84
-
85
141
  @VisiData.api
86
142
  def openSource(vd, p, filetype=None, create=False, **kwargs):
87
143
  '''Return unloaded sheet object for *p* opened as the given *filetype* and with *kwargs* as option overrides. *p* can be a Path or a string (filename, url, or "-" for stdin).
88
144
  when true, *create* will return a blank sheet, if file does not exist.'''
89
- if not filetype:
90
- filetype = options.getonly('filetype', 'global', '')
145
+
146
+ if isinstance(p, BaseSheet):
147
+ return p
148
+
149
+ filetype = filetype or vd.options.getonly('filetype', str(p), '') #1710
150
+ filetype = filetype or vd.options.getonly('filetype', 'global', '')
91
151
 
92
152
  vs = None
93
153
  if isinstance(p, str):
@@ -111,26 +171,21 @@ def openSource(vd, p, filetype=None, create=False, **kwargs):
111
171
  def open_txt(vd, p):
112
172
  'Create sheet from `.txt` file at Path `p`, checking whether it is TSV.'
113
173
  if p.exists(): #1611
114
- with p.open_text(encoding=vd.options.encoding) as fp:
174
+ with p.open(encoding=vd.options.encoding) as fp:
175
+ delimiter = vd.options.delimiter
115
176
  try:
116
- if options.delimiter in next(fp): # peek at the first line
177
+ if delimiter and delimiter in next(fp): # peek at the first line
117
178
  return vd.open_tsv(p) # TSV often have .txt extension
118
179
  except StopIteration:
119
- return Sheet(p.name, columns=[SettableColumn()], source=p)
120
- return TextSheet(p.name, source=p)
121
-
122
-
123
- @VisiData.api
124
- def loadInternalSheet(vd, cls, p, **kwargs):
125
- 'Load internal sheet of given class.'
126
- vs = cls(p.name, source=p, **kwargs)
127
- options._set('encoding', 'utf8', vs)
128
- if p.exists():
129
- # vd.sheets.insert(0, vs) # broke replay with macros.reload()
130
- vs.reload.__wrapped__(vs)
131
- # vd.sheets.pop(0)
132
- return vs
180
+ return TableSheet(p.base_stem, columns=[SettableColumn()], source=p)
181
+ return TextSheet(p.base_stem, source=p)
133
182
 
134
183
 
135
184
  BaseSheet.addCommand('o', 'open-file', 'vd.push(openSource(inputFilename("open: "), create=True))', 'Open file or URL')
136
185
  TableSheet.addCommand('zo', 'open-cell-file', 'vd.push(openSource(cursorDisplay) or fail(f"file {cursorDisplay} does not exist"))', 'Open file or URL from path in current cell')
186
+ BaseSheet.addCommand('gU', 'undo-last-quit', 'push(allSheets[-1])', 'reopen most recently closed sheet')
187
+
188
+ vd.addMenuItems('''
189
+ File > Open > input file/url > open-file
190
+ File > Reopen last closed > undo-last-quit
191
+ ''')