visidata 2.11.1__py3-none-any.whl → 3.0.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 (256) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +78 -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. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +63 -51
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +6 -2
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +22 -4
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +197 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +77 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +200 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +20 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +54 -12
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +302 -149
  187. visidata/man/vd.txt +291 -154
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +55 -205
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +239 -260
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +114 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/benchmark.csv +52 -0
  213. visidata/tests/conftest.py +3 -3
  214. visidata/tests/test_cliptext.py +39 -0
  215. visidata/tests/test_commands.py +65 -7
  216. visidata/tests/test_edittext.py +2 -2
  217. visidata/tests/test_features.py +28 -0
  218. visidata/tests/test_menu.py +14 -0
  219. visidata/tests/test_path.py +13 -4
  220. visidata/text_source.py +53 -0
  221. visidata/textsheet.py +10 -3
  222. visidata/theme.py +44 -0
  223. visidata/themes/__init__.py +0 -0
  224. visidata/themes/ascii8.py +84 -0
  225. visidata/themes/asciimono.py +84 -0
  226. visidata/themes/light.py +17 -0
  227. visidata/threads.py +89 -40
  228. visidata/tuiwin.py +22 -0
  229. visidata/type_currency.py +22 -3
  230. visidata/type_date.py +31 -9
  231. visidata/type_floatsi.py +5 -1
  232. visidata/undo.py +17 -5
  233. visidata/utils.py +106 -23
  234. visidata/vdobj.py +28 -17
  235. visidata/windows.py +10 -0
  236. visidata/wrappers.py +9 -3
  237. visidata-3.0.1.data/data/share/applications/visidata.desktop +7 -0
  238. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
  239. {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
  240. visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
  241. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
  242. visidata-3.0.1.dist-info/RECORD +258 -0
  243. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/WHEEL +1 -1
  244. vgit/__init__.py +0 -1
  245. vgit/gitsheet.py +0 -164
  246. visidata/layout.py +0 -44
  247. visidata/misc.py +0 -5
  248. visidata-2.11.1.data/scripts/vgit +0 -9
  249. visidata-2.11.1.dist-info/RECORD +0 -155
  250. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  251. {vgit → visidata/apps/vgit}/abort.py +0 -0
  252. /visidata/{repeat.py → features/repeat.py} +0 -0
  253. {visidata-2.11.1.data → visidata-3.0.1.data}/scripts/vd +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
  255. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
  256. {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
visidata/ddw/regex.ddw ADDED
@@ -0,0 +1,57 @@
1
+ {"id": null, "type": null, "x": 5, "y": 1, "text": ".", "color": "keystrokes", "tags": [], "group": "", "frame": null, "rows": null, "duration_ms": null, "ref": null, "href": null}
2
+ {"x": 5, "y": 2, "text": "^", "color": "keystrokes", "tags": [], "group": ""}
3
+ {"x": 5, "y": 3, "text": "$", "color": "keystrokes", "tags": [], "group": ""}
4
+ {"x": 38, "y": 1, "text": "*", "color": "keystrokes", "tags": [], "group": ""}
5
+ {"x": 38, "y": 2, "text": "+", "color": "keystrokes", "tags": [], "group": ""}
6
+ {"x": 38, "y": 3, "text": "?", "color": "keystrokes", "tags": [], "group": ""}
7
+ {"x": 34, "y": 5, "text": "{m,n}", "color": "keystrokes", "tags": [], "group": ""}
8
+ {"x": 8, "y": 1, "text": "any char except newline", "color": "", "tags": [], "group": ""}
9
+ {"x": 8, "y": 2, "text": "start of string", "color": "", "tags": [], "group": ""}
10
+ {"x": 8, "y": 3, "text": "end of string", "color": "", "tags": [], "group": ""}
11
+ {"x": 42, "y": 1, "text": "0 or more", "color": "", "tags": [], "group": ""}
12
+ {"x": 42, "y": 2, "text": "1 or more", "color": "", "tags": [], "group": ""}
13
+ {"x": 42, "y": 3, "text": "0 or 1", "color": "", "tags": [], "group": ""}
14
+ {"x": 42, "y": 5, "text": "between m and n", "color": "", "tags": [], "group": ""}
15
+ {"x": 1, "y": 11, "text": "[abc]", "color": "keystrokes", "tags": [], "group": ""}
16
+ {"x": 0, "y": 12, "text": "[^abc]", "color": "keystrokes", "tags": [], "group": ""}
17
+ {"x": 4, "y": 4, "text": "\\d", "color": "keystrokes", "tags": [], "group": ""}
18
+ {"x": 4, "y": 6, "text": "\\s", "color": "keystrokes", "tags": [], "group": ""}
19
+ {"x": 4, "y": 8, "text": "\\w", "color": "keystrokes", "tags": [], "group": ""}
20
+ {"x": 8, "y": 4, "text": "digit char", "color": "", "tags": [], "group": ""}
21
+ {"x": 8, "y": 6, "text": "whitespace char", "color": "", "tags": [], "group": ""}
22
+ {"x": 4, "y": 10, "text": "\\b", "color": "keystrokes", "tags": [], "group": ""}
23
+ {"x": 8, "y": 10, "text": "word boundary", "color": "", "tags": [], "group": ""}
24
+ {"x": 8, "y": 8, "text": "word char", "color": "", "tags": [], "group": ""}
25
+ {"x": 18, "y": 8, "text": "[a-zA-Z0-9_]", "color": "keystrokes", "tags": [], "group": ""}
26
+ {"x": 36, "y": 7, "text": "(\u2026)", "color": "keystrokes", "tags": [], "group": ""}
27
+ {"x": 34, "y": 8, "text": "(?:\u2026)", "color": "keystrokes", "tags": [], "group": ""}
28
+ {"x": 8, "y": 11, "text": "any of", "color": "", "tags": [], "group": ""}
29
+ {"x": 15, "y": 11, "text": "a", "color": "keystrokes", "tags": [], "group": ""}
30
+ {"x": 17, "y": 11, "text": "or", "color": "", "tags": [], "group": ""}
31
+ {"x": 20, "y": 11, "text": "b", "color": "keystrokes", "tags": [], "group": ""}
32
+ {"x": 22, "y": 11, "text": "or", "color": "", "tags": [], "group": ""}
33
+ {"x": 25, "y": 11, "text": "c", "color": "keystrokes", "tags": [], "group": ""}
34
+ {"x": 19, "y": 4, "text": "[0-9]", "color": "keystrokes", "tags": [], "group": ""}
35
+ {"x": 42, "y": 7, "text": "capturing group", "color": "", "tags": [], "group": ""}
36
+ {"x": 42, "y": 8, "text": "non-capturing group", "color": "", "tags": [], "group": ""}
37
+ {"x": 9, "y": 12, "text": "", "color": "", "tags": [], "group": ""}
38
+ {"x": 8, "y": 12, "text": "not", "color": "", "tags": [], "group": ""}
39
+ {"x": 12, "y": 12, "text": "a", "color": "keystrokes", "tags": [], "group": ""}
40
+ {"x": 14, "y": 12, "text": "or", "color": "", "tags": [], "group": ""}
41
+ {"x": 17, "y": 12, "text": "b", "color": "keystrokes", "tags": [], "group": ""}
42
+ {"x": 19, "y": 12, "text": "or", "color": "", "tags": [], "group": ""}
43
+ {"x": 22, "y": 12, "text": "c", "color": "keystrokes", "tags": [], "group": ""}
44
+ {"x": 38, "y": 10, "text": "|", "color": "keystrokes", "tags": [], "group": ""}
45
+ {"x": 42, "y": 10, "text": "logical OR", "color": "", "tags": [], "group": ""}
46
+ {"x": 4, "y": 5, "text": "\\D", "color": "keystrokes", "tags": [], "group": ""}
47
+ {"x": 4, "y": 7, "text": "\\S", "color": "keystrokes", "tags": [], "group": ""}
48
+ {"x": 4, "y": 9, "text": "\\W", "color": "keystrokes", "tags": [], "group": ""}
49
+ {"x": 8, "y": 5, "text": "non-digit char", "color": "", "tags": [], "group": ""}
50
+ {"x": 8, "y": 7, "text": "non-whitespace char", "color": "", "tags": [], "group": ""}
51
+ {"x": 8, "y": 9, "text": "non-word char", "color": "", "tags": [], "group": ""}
52
+ {"x": 36, "y": 4, "text": "{m}", "color": "keystrokes", "tags": [], "group": ""}
53
+ {"x": 42, "y": 4, "text": "exactly m", "color": "", "tags": [], "group": ""}
54
+ {"x": 4, "y": 0, "text": "Character classes", "color": "bold underline", "tags": [], "group": ""}
55
+ {"x": 35, "y": 0, "text": "Repetition", "color": "bold underline", "tags": [], "group": ""}
56
+ {"x": 2, "y": 14, "text": "For full documentation on Python regular expressions, see", "color": "", "tags": [], "group": ""}
57
+ {"x": 6, "y": 15, "text": "https://docs.python.org/3/library/re.html", "color": "underline", "tags": [], "group": "", "href": "https://docs.python.org/3/library/re.html"}
visidata/ddwplay.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from collections import defaultdict
2
2
  import json
3
3
  import time
4
- from visidata import colors, vd
4
+ from visidata import colors, vd, clipdraw, ColorAttr
5
5
 
6
6
  __all__ = ['Animation', 'AnimationMgr']
7
7
 
@@ -31,24 +31,38 @@ class Animation:
31
31
  self.width = 0
32
32
  self.load_from(fp)
33
33
 
34
- def iterdeep(self, rows, x=0, y=0, parents=None):
35
- 'Walk rows deeply and generate (row, x, y, [ancestors]) for each row.'
34
+ def iterdeep(self, rows, x=0, y=0, parents=None, **kwargs):
35
+ 'Walk rows deeply and generate (row, x, y, [ancestors]) for each row, filtering on kwargs.'
36
36
  for r in rows:
37
37
  newparents = (parents or []) + [r]
38
38
  if r.type == 'frame': continue
39
39
  if r.ref:
40
40
  assert r.type == 'ref'
41
41
  g = self.groups[r.ref]
42
- yield from self.iterdeep(map(AttrDict, g.rows or []), x+r.x, y+r.y, newparents)
42
+ if self.matches(r, kwargs):
43
+ yield from self.iterdeep(map(AttrDict, g.rows or []), x+r.x, y+r.y, newparents)
43
44
  else:
44
- yield r, x+r.x, y+r.y, newparents
45
- yield from self.iterdeep(map(AttrDict, r.rows or []), x+r.x, x+r.y, newparents)
45
+ if self.matches(r, kwargs):
46
+ yield r, x+r.x, y+r.y, newparents
47
+ yield from self.iterdeep(map(AttrDict, r.rows or []), x+r.x, x+r.y, newparents)
48
+
49
+ def matches(self, row, values):
50
+ for k, v in values.items():
51
+ actualv = getattr(row, k, None)
52
+ if isinstance(actualv, (list, tuple)) and isinstance(v, (list, tuple)):
53
+ if not any(x in actualv for x in v):
54
+ return False
55
+ elif v != actualv:
56
+ return False
57
+ return True
46
58
 
47
59
  def load_from(self, fp):
48
60
  for line in fp.readlines():
49
61
  r = AttrDict(json.loads(line))
50
62
  if r.type == 'frame':
51
- self.frames[r.id].update(r)
63
+ f = self.frames[r.id]
64
+ f.update(r)
65
+ f.rows = []
52
66
  elif r.type == 'group':
53
67
  self.groups[r.id].update(r)
54
68
 
@@ -66,9 +80,12 @@ class Animation:
66
80
  self.width = max(self.width, x+len(r.text))
67
81
  self.height = max(self.height, y)
68
82
 
69
- def draw(self, scr, *, t=0, x=0, y=0, loop=False, **kwargs):
70
- for r, dx, dy, _ in self.iterdeep(self.frames[''].rows):
71
- scr.addstr(y+dy, x+dx, r.text, colors[r.color])
83
+ def draw(self, scr, *, t=0, x=0, y=0, loop=False, attr=ColorAttr(), **kwargs):
84
+ windowHeight, windowWidth = scr.getmaxyx()
85
+ for r, dx, dy, _ in self.iterdeep(self.frames[''].rows, **kwargs):
86
+ text = f'[:onclick {r.href}]{r.text}[/]' if r.href else r.text
87
+ if y+dy < windowHeight:
88
+ clipdraw(scr, y+dy, x+dx, text, attr.update(colors[r.color], 2))
72
89
 
73
90
  if not self.total_ms:
74
91
  return None
@@ -77,8 +94,10 @@ class Animation:
77
94
  for f in self.frames.values():
78
95
  ms -= int(f.duration_ms or 0)
79
96
  if ms < 0:
80
- for r, dx, dy, _ in self.iterdeep(f.rows):
81
- scr.addstr(y+dy, x+dx, r.text, colors[r.color])
97
+ for r, dx, dy, _ in self.iterdeep(f.rows, **kwargs):
98
+ text = f'[:onclick {r.href}]{r.text}[/]' if r.href else r.text
99
+ if y+dy < windowHeight:
100
+ clipdraw(scr, y+dy, x+dx, text, colors[r.color])
82
101
 
83
102
  return -ms/1000
84
103
 
@@ -117,9 +136,9 @@ class AnimationMgr:
117
136
  for row in self.active:
118
137
  startt, anim, akwargs = row
119
138
  kwargs.update(akwargs)
120
- nextt = anim.draw(scr, t=t-startt, **kwargs)
139
+ nextt = anim.draw(scr, t=t+startt, **kwargs)
121
140
  if nextt is None:
122
- if not akwargs.get('loop'):
141
+ if not akwargs.get('loop', True):
123
142
  done.append(row)
124
143
  else:
125
144
  times.append(t+nextt)
visidata/deprecated.py CHANGED
@@ -73,7 +73,7 @@ visidata.Sheet.exec_command = deprecated('2.0')(visidata.Sheet.execCommand)
73
73
  @VisiData.api
74
74
  def filetype(vd, ext, constructor):
75
75
  'Add constructor to handle the given file type/extension.'
76
- globals().setdefault('open_'+ext, lambda p,ext=ext: constructor(p.name, source=p, filetype=ext))
76
+ globals().setdefault('open_'+ext, lambda p,ext=ext: constructor(p.base_stem, source=p, filetype=ext))
77
77
 
78
78
  @deprecated('2.0', 'Sheet(namepart1, namepart2, ...)')
79
79
  @VisiData.global_api
@@ -135,11 +135,12 @@ alias('show-aggregate', 'memo-aggregate')
135
135
 
136
136
  # 2.6
137
137
 
138
- clean_name = visidata.cleanName
138
+ def clean_name(s):
139
+ return visidata.vd.cleanName(s)
139
140
 
140
141
  def maybe_clean(s, vs):
141
142
  if (vs or visidata.vd).options.clean_names:
142
- s = visidata.cleanName(s)
143
+ s = visidata.vd.cleanName(s)
143
144
  return s
144
145
 
145
146
  def load_tsv(fn):
@@ -164,3 +165,76 @@ exceptionCaught = deprecated('2.6', 'vd.exceptionCaught')(vd.exceptionCaught)
164
165
  openSource = deprecated('2.6', 'vd.openSource')(vd.openSource)
165
166
  globalCommand = visidata.BaseSheet.addCommand
166
167
  visidata.Sheet.StaticColumn = deprecated('2.11', 'Sheet.freeze_col')(visidata.Sheet.freeze_col)
168
+ visidata.Path.open_text = deprecated('3.0', 'visidata.Path.open')(visidata.Path.open)
169
+
170
+ vd.sysclip_value = deprecated('3.0', 'vd.sysclipValue')(vd.sysclipValue)
171
+
172
+ def itemsetter(i):
173
+ def g(obj, v):
174
+ obj[i] = v
175
+ return g
176
+
177
+
178
+ vd.optalias('force_valid_colnames', 'clean_names')
179
+ vd.optalias('dir_recurse', 'dir_depth', 100000)
180
+ vd.optalias('confirm_overwrite', 'overwrite', 'confirm')
181
+ vd.optalias('show_graph_labels', 'disp_graph_labels')
182
+ vd.optalias('zoom_incr', 'disp_zoom_incr')
183
+
184
+ alias('visibility-sheet', 'toggle-multiline')
185
+ alias('visibility-col', 'toggle-multiline')
186
+
187
+ def clean_to_id(s):
188
+ return visidata.vd.cleanName(s)
189
+
190
+ @deprecated('3.0', 'use try/finally')
191
+ class OnExit:
192
+ '"with OnExit(func, ...):" calls func(...) when the context is exited'
193
+ def __init__(self, func, *args, **kwargs):
194
+ self.func = func
195
+ self.args = args
196
+ self.kwargs = kwargs
197
+
198
+ def __enter__(self):
199
+ return self
200
+
201
+ def __exit__(self, exc_type, exc_value, exc_traceback):
202
+ try:
203
+ self.func(*self.args, **self.kwargs)
204
+ except Exception as e:
205
+ vd.exceptionCaught(e)
206
+
207
+ alias('open-inputs', 'open-input-history')
208
+
209
+ #vd.option('plugins_url', 'https://visidata.org/plugins/plugins.jsonl', 'source of plugins sheet')
210
+
211
+ @visidata.VisiData.api
212
+ def inputRegexSubstOld(vd, prompt):
213
+ 'Input regex transform via oneliner (separated with `/`). Return parsed transformer as dict(before=, after=).'
214
+ rex = vd.inputRegex(prompt, type='regex-subst')
215
+ before, after = vd.parse_sed_transform(rex)
216
+ return dict(before=before, after=after)
217
+
218
+
219
+ visidata.Sheet.addCommand('', 'addcol-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, **inputRegexSubstOld("transform column by regex: "))))', 'add column derived from current column, replacing regex with subst (may include \1 backrefs)', deprecated=True)
220
+ visidata.Sheet.addCommand('', 'setcol-subst', 'setValuesFromRegex([cursorCol], someSelectedRows, **inputRegexSubstOld("transform column by regex: "))', 'regex/subst - modify selected rows in current column, replacing regex with subst, (may include backreferences \\1 etc)', deprecated=True)
221
+ visidata.Sheet.addCommand('', 'setcol-subst-all', 'setValuesFromRegex(visibleCols, someSelectedRows, **inputRegexSubstOld(f"transform {nVisibleCols} columns by regex: "))', 'modify selected rows in all visible columns, replacing regex with subst (may include \\1 backrefs)', deprecated=True)
222
+
223
+ visidata.Sheet.addCommand('', 'split-col', 'addRegexColumns(makeRegexSplitter, cursorCol, inputRegex("split regex: ", type="regex-split"))', 'Add new columns from regex split', deprecated=True)
224
+ visidata.Sheet.addCommand('', 'capture-col', 'addRegexColumns(makeRegexMatcher, cursorCol, inputRegex("capture regex: ", type="regex-capture"))', 'add new column from capture groups of regex; requires example row', deprecated=True)
225
+
226
+ #vd.option('cmdlog_histfile', '', 'file to autorecord each cmdlog action to', sheettype=None)
227
+ #BaseSheet.bindkey('KEY_BACKSPACE', 'menu-help')
228
+
229
+ @deprecated('3.0', 'vd.callNoExceptions(col.setValue, row, value)')
230
+ @visidata.Column.api
231
+ def setValueSafe(self, row, value):
232
+ 'setValue and ignore exceptions.'
233
+ return vd.callNoExceptions(self.setValue, row, value)
234
+
235
+ @deprecated('3.0', 'vd.callNoExceptions(sheet.checkCursor)')
236
+ @visidata.BaseSheet.api
237
+ def checkCursorNoExceptions(sheet):
238
+ return vd.callNoExceptions(sheet.checkCursor)
239
+
240
+ vd.addGlobals(globals())
@@ -0,0 +1,7 @@
1
+ [Desktop Entry]
2
+ Type=Application
3
+ Terminal=true
4
+ Name=VisiData
5
+ Icon=utilities-terminal
6
+ Exec=bash -i -c 'vd %f'
7
+ Categories=Application;
visidata/editor.py CHANGED
@@ -12,14 +12,16 @@ visidata.vd.tstp_signal = None
12
12
  class SuspendCurses:
13
13
  'Context manager to leave windowed mode on enter and restore it on exit.'
14
14
  def __enter__(self):
15
- curses.endwin()
15
+ if visidata.vd.scrFull:
16
+ curses.endwin()
16
17
  if visidata.vd.tstp_signal:
17
18
  signal.signal(signal.SIGTSTP, visidata.vd.tstp_signal)
18
19
 
19
20
  def __exit__(self, exc_type, exc_val, tb):
20
- curses.reset_prog_mode()
21
- visidata.vd.scrFull.refresh()
22
- curses.doupdate()
21
+ if visidata.vd.scrFull:
22
+ curses.reset_prog_mode()
23
+ visidata.vd.scrFull.refresh()
24
+ curses.doupdate()
23
25
 
24
26
 
25
27
  @visidata.VisiData.api
@@ -34,7 +36,7 @@ def launchEditor(vd, *args):
34
36
  @visidata.VisiData.api
35
37
  def launchBrowser(vd, *args):
36
38
  'Launch $BROWSER with *args* as arguments.'
37
- browser = os.environ.get('BROWSER') or vd.fail('(no $BROWSER) for %s' % args[0])
39
+ browser = os.environ.get('BROWSER') or vd.fail('no $BROWSER for %s' % args[0])
38
40
  args = [browser] + list(args)
39
41
  subprocess.call(args)
40
42
 
@@ -44,6 +46,7 @@ def launchExternalEditor(vd, v, linenum=0):
44
46
  'Launch $EDITOR to edit string *v* starting on line *linenum*.'
45
47
  import tempfile
46
48
  with tempfile.NamedTemporaryFile() as temp:
49
+ temp.close() #2118 must close before re-opening on windows
47
50
  with open(temp.name, 'w') as fp:
48
51
  fp.write(v)
49
52
  return vd.launchExternalEditorPath(visidata.Path(temp.name), linenum)
@@ -65,7 +68,8 @@ def launchExternalEditorPath(vd, path, linenum=0):
65
68
  return ''
66
69
 
67
70
 
68
- def suspend():
71
+ @visidata.VisiData.api
72
+ def suspend(vd):
69
73
  import signal
70
74
  with SuspendCurses():
71
75
  os.kill(os.getpid(), signal.SIGSTOP)
@@ -94,3 +98,5 @@ sys.breakpointhook = _breakpoint
94
98
 
95
99
  visidata.BaseSheet.addCommand('^Z', 'suspend', 'suspend()', 'suspend VisiData process')
96
100
  visidata.BaseSheet.addCommand('', 'breakpoint', 'breakpoint()', 'drop into pdb REPL')
101
+
102
+ visidata.vd.addGlobals(SuspendCurses=SuspendCurses)
visidata/errors.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import traceback
2
2
 
3
- from visidata import vd, VisiData, options
3
+ from visidata import vd, VisiData
4
+
5
+ vd.option('debug', False, 'exit on error and display stacktrace', max_help=0)
4
6
 
5
- __all__ = ['stacktrace', 'ExpectedException']
6
7
 
7
8
  class ExpectedException(Exception):
8
9
  'Controlled Exception from fail() or confirm(). Status or other interface update is done by raiser.'
@@ -28,4 +29,7 @@ def exceptionCaught(vd, exc=None, status=True, **kwargs):
28
29
  if vd.options.debug:
29
30
  raise
30
31
 
32
+
33
+ vd.addGlobals(stacktrace=stacktrace, ExpectedException=ExpectedException)
34
+
31
35
  # see textsheet.py for ErrorSheet and associated commands
File without changes
@@ -0,0 +1,29 @@
1
+ # command setdiff-sheet adds a diff colorizer for all sheets against current sheet
2
+
3
+ from visidata import Sheet, CellColorizer, vd
4
+
5
+ vd.theme_option('color_diff', 'red', 'color of values different from --diff source')
6
+ vd.theme_option('color_diff_add', 'yellow', 'color of rows/columns added to --diff source')
7
+
8
+
9
+ def makeDiffColorizer(othersheet):
10
+ def colorizeDiffs(sheet, col, row, cellval):
11
+ if row is None or col is None:
12
+ return None
13
+ vcolidx = sheet.visibleCols.index(col)
14
+ rowidx = sheet.rows.index(row)
15
+ if vcolidx < len(othersheet.visibleCols) and rowidx < len(othersheet.rows):
16
+ otherval = othersheet.visibleCols[vcolidx].getDisplayValue(othersheet.rows[rowidx])
17
+ if cellval.display != otherval:
18
+ return 'color_diff'
19
+ else:
20
+ return 'color_diff_add'
21
+ return colorizeDiffs
22
+
23
+
24
+ @Sheet.api
25
+ def setDiffSheet(vs):
26
+ Sheet.colorizers.append(CellColorizer(8, None, makeDiffColorizer(vs)))
27
+
28
+
29
+ Sheet.addCommand(None, 'setdiff-sheet', 'setDiffSheet()', 'set this sheet as diff sheet for all new sheets')
@@ -0,0 +1,6 @@
1
+ 'Enter edit mode automatically when typing numeric digits.'
2
+
3
+ from visidata import Sheet
4
+
5
+ for i in range(0, 10):
6
+ Sheet.addCommand(str(i), 'autoedit-%s' % i, 'cursorCol.setValues([cursorRow], editCell(cursorVisibleColIndex, value="%s", i=1))' % i, 'replace cell value with input starting with %s' % i)
@@ -0,0 +1,89 @@
1
+ import visidata
2
+ from visidata import vd, VisiData, Sheet, IndexSheet, SequenceSheet, ColumnItem, Path, AttrDict, ColumnAttr, asyncthread, Progress, ColumnExpr, date
3
+
4
+ from .gsheets import GSheetsIndex
5
+
6
+ @VisiData.api
7
+ def open_gdrive(vd, p):
8
+ return GDriveSheet(p.base_stem)
9
+
10
+
11
+ FILES_FIELDS_VISIBLE='''name size modifiedTime mimeType description'''.split()
12
+
13
+ FILES_FIELDS='''
14
+ id name size modifiedTime mimeType description owners
15
+ starred properties spaces version webContentLink webViewLink sharingUser lastModifyingUser shared
16
+ ownedByMe originalFilename md5Checksum size quotaBytesUsed headRevisionId imageMediaMetadata videoMediaMetadata parents
17
+ exportLinks contentRestrictions contentHints trashed
18
+ '''.split()
19
+
20
+
21
+ @VisiData.cached_property
22
+ def _drivebuild(vd):
23
+ return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive.readonly'))
24
+
25
+ @VisiData.cached_property
26
+ def _gdrive(self):
27
+ return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive.readonly')).files()
28
+
29
+ @VisiData.cached_property
30
+ def _gdrive_rw(self):
31
+ return vd.google_discovery.build("drive", "v3", credentials=vd.google_auth('drive')).files()
32
+
33
+
34
+ class GDriveSheet(Sheet):
35
+ rowtype='files' # rowdef: AttrDict of result from Google Drive files.list API
36
+ defer=True
37
+ columns = [
38
+ ColumnItem('name'),
39
+ ColumnItem('size', type=int),
40
+ ColumnItem('modifiedTime', type=date),
41
+ ColumnItem('mimeType'),
42
+ ColumnItem('name'),
43
+ ColumnExpr('owner', expr='owners[0]["displayName"]')
44
+ ] + [
45
+ ColumnItem(x, width=0) for x in FILES_FIELDS if x not in FILES_FIELDS_VISIBLE
46
+ ]
47
+
48
+ def iterload(self):
49
+ self.results = []
50
+ page_token = None
51
+ while True:
52
+ ret = vd._gdrive.list(
53
+ pageSize=1000,
54
+ pageToken=page_token,
55
+ fields="nextPageToken, files(%s)" % ','.join(FILES_FIELDS)
56
+ ).execute()
57
+
58
+ self.results.append(ret)
59
+
60
+ for r in ret.get('files', []):
61
+ yield AttrDict(r)
62
+
63
+ page_token = ret.get('nextPageToken', None)
64
+ if not page_token:
65
+ break
66
+
67
+ def openRow(self, r):
68
+ if r.mimeType == 'application/vnd.google-apps.spreadsheet':
69
+ return GSheetsIndex(r.name, source=r.id)
70
+ if r.mimeType.startswith('image'):
71
+ return vd.launchBrowser(r.webViewLink)
72
+ return vd.openSource(r.webContentLink)
73
+
74
+ @asyncthread
75
+ def deleteFile(self, **kwargs):
76
+ with Progress(total=1) as prog:
77
+ vd._gdrive_rw.delete(**kwargs).execute()
78
+ prog.addProgress(1)
79
+
80
+ @asyncthread
81
+ def putChanges(self):
82
+ adds, mods, dels = self.getDeferredChanges()
83
+
84
+ for row in Progress(dels.values()):
85
+ self.deleteFile(fileId=row.id)
86
+
87
+ vd.sync()
88
+ self.preloadHook()
89
+ self.reload()
@@ -0,0 +1,37 @@
1
+ '''
2
+ # Using VisiData with Google Sheets and Google Drive
3
+
4
+ ## Setup and Authentication
5
+
6
+ Add to .visidatarc:
7
+
8
+ import visidata.experimental.google
9
+
10
+ When VisiData attempts to use the Google API, it uses the "web authentication flow", which causes a web page to open asking for permissions to read and/or write your Google Sheets.
11
+ After granting permissions, VisiData caches the auth token in the .visidata directory. Remove `.visidata/google-*.pickle` to unauthenticate.
12
+
13
+ ## Load a Google Sheet into VisiData
14
+
15
+ Use VisiData to open the URL or spreadsheet id, with filetype `g` (or `gsheets`):
16
+
17
+ vd https://docs.google.com/spreadsheets/d/1WV0JI_SsGfmoocXWJILK2nhfcxU1H7roqL1HE7zBdsY/ -f g
18
+
19
+ VisiData assumes the first row is the header row with column names.
20
+
21
+ ## Save one or more sheets in VisiData as a Google Sheet
22
+
23
+ Save to `<sheetname>.g` using either `Ctrl+S` (current sheet only) or `g Ctrl+S` (all sheets on the sheet stack).
24
+ <sheetname> will be the visible name of the Spreadsheet in Google Drive; each sheet tab within the Spreadsheet will be named according to the sheet name within VisiData.
25
+
26
+ ## List files in Google Drive
27
+
28
+ Use the `gdrive` filetype (the path doesn't matter):
29
+
30
+ vd . -f gdrive
31
+
32
+ - Files can be marked for deletion with `d` and execute those deletions with `z Ctrl+S` (same as on the DirSheet for the local filesystem).
33
+ - Images can be viewed with `Enter` (in browser).
34
+ '''
35
+
36
+ import visidata.experimental.gdrive
37
+ import visidata.experimental.gsheets
@@ -0,0 +1,79 @@
1
+ import re
2
+ from visidata import vd, VisiData, Sheet, IndexSheet, SequenceSheet, ColumnItem, Path, AttrDict, ColumnAttr, asyncthread
3
+
4
+ SPREADSHEET_FIELDS='properties sheets namedRanges spreadsheetUrl developerMetadata dataSources dataSourceSchedules'.split()
5
+ SHEET_FIELDS='merges conditionalFormats filterViews protectedRanges basicFilter charts bandedRanges developerMetadata rowGroups columnGroups slicers'.split()
6
+
7
+ @VisiData.api
8
+ def open_gsheets(vd, p):
9
+ m = re.search(r'([A-z0-9_]{44})', p.given)
10
+ if m:
11
+ return GSheetsIndex(p.base_stem, source=m.groups()[0])
12
+
13
+ vd.open_g = vd.open_gsheets
14
+
15
+ @VisiData.lazy_property
16
+ def google_discovery(self):
17
+ googleapiclient = vd.importExternal('googleapiclient', 'google-api-python-client')
18
+ from googleapiclient import discovery
19
+ return discovery
20
+
21
+
22
+ @VisiData.cached_property
23
+ def _gsheets(vd):
24
+ return vd.google_discovery.build("sheets", "v4", credentials=vd.google_auth('spreadsheets.readonly')).spreadsheets()
25
+
26
+
27
+ @VisiData.cached_property
28
+ def _gsheets_rw(vd):
29
+ return vd.google_discovery.build("sheets", "v4", credentials=vd.google_auth('spreadsheets')).spreadsheets()
30
+
31
+
32
+ class GSheetsIndex(Sheet):
33
+ columns = [
34
+ ColumnAttr('title', 'properties.title'),
35
+ ColumnAttr('type', 'properties.sheetType', width=0),
36
+ ColumnAttr('nRows', 'properties.gridProperties.rowCount', type=int),
37
+ ColumnAttr('nCols', 'properties.gridProperties.columnCount', type=int),
38
+ ]
39
+ def iterload(self):
40
+ googlesheet = vd._gsheets.get(spreadsheetId=self.source, fields=','.join(SPREADSHEET_FIELDS)).execute()
41
+ vd.status(googlesheet['properties']['title'])
42
+
43
+ for gsheet in googlesheet['sheets']:
44
+ yield AttrDict(gsheet)
45
+
46
+ def openRow(self, r):
47
+ return GSheet(r.properties.title, source=self.source)
48
+
49
+
50
+ class GSheet(SequenceSheet):
51
+ '.source is gsheet id; .name is sheet name'
52
+ def iterload(self):
53
+ result = vd._gsheets.values().get(spreadsheetId=self.source, range=self.name).execute()
54
+ yield from result.get('values', [])
55
+
56
+
57
+ @VisiData.api
58
+ def save_gsheets(vd, p, *sheets):
59
+ gsheet = vd._gsheets_rw.create(body={
60
+ 'properties': { 'title': p.base_stem },
61
+ 'sheets': list({'properties': { 'title': vs.name }} for vs in sheets),
62
+ }, fields='spreadsheetId').execute()
63
+
64
+ gsheetId = gsheet.get('spreadsheetId')
65
+ vd.status(f'https://docs.google.com/spreadsheets/d/{gsheetId}/')
66
+
67
+ for vs in sheets:
68
+ rows = [list(c.name for c in vs.visibleCols)]
69
+ rows += list(list(val for col, val in row.items())
70
+ for row in vs.iterdispvals(*vs.visibleCols, format=True))
71
+
72
+ vd._gsheets_rw.values().append(
73
+ spreadsheetId=gsheetId,
74
+ valueInputOption='RAW',
75
+ range=vs.name,
76
+ body=dict(values=rows)
77
+ ).execute()
78
+
79
+ vd.save_g = vd.save_gsheets
@@ -0,0 +1,37 @@
1
+ from copy import copy
2
+
3
+ from visidata import Sheet, vd, asyncsingle
4
+
5
+ @Sheet.api
6
+ def dup_search(sheet, cols='cursorCol'):
7
+ vs = copy(sheet)
8
+ vs.name += "_search"
9
+ vs.rows = sheet.rows
10
+ vs.source = sheet
11
+ vs.search = ''
12
+
13
+ @asyncsingle
14
+ def live_search_async(val, status=False):
15
+ if not val:
16
+ vs.rows = vs.source.rows
17
+ else:
18
+ vs.rows = []
19
+ for i in vd.searchRegex(vs.source, regex=val, columns=cols, printStatus=status):
20
+ vs.addRow(vs.source.rows[i])
21
+
22
+ def live_search(val):
23
+ vs.draw(vs._scr)
24
+ vd.drawRightStatus(vs._scr, vs)
25
+ val = val.rstrip('\n')
26
+ if val == vs.search:
27
+ return
28
+ vs.search = val
29
+ live_search_async(val, sheet=vs, status=False)
30
+
31
+ vd.input("search regex: ", updater=live_search)
32
+ vd.push(vs)
33
+ vs.name = vs.source.name+'_'+vs.search
34
+
35
+
36
+ Sheet.addCommand('^[s', 'dup-search', 'dup_search("cursorCol")', 'search for regex forwards in current column, creating duplicate sheet with matching rows live')
37
+ Sheet.addCommand('g^[s', 'dup-search-cols', 'dup_search("visibleCols")', 'search for regex forwards in all columns, creating duplicate sheet with matching rows live')