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/graph.py CHANGED
@@ -1,6 +1,10 @@
1
- from visidata import *
1
+ import math
2
2
 
3
- vd.option('color_graph_axis', 'bold', 'color for graph axis labels')
3
+ from visidata import VisiData, Canvas, Sheet, Progress, BoundingBox, Point
4
+ from visidata import vd, asyncthread, dispwidth, colors, clipstr
5
+
6
+ vd.theme_option('color_graph_axis', 'bold', 'color for graph axis labels')
7
+ vd.theme_option('disp_graph_tick_x', '╵', 'character for graph x-axis ticks')
4
8
 
5
9
 
6
10
  @VisiData.api
@@ -9,37 +13,67 @@ def numericCols(vd, cols):
9
13
 
10
14
 
11
15
  class InvertedCanvas(Canvas):
16
+ @asyncthread
17
+ def render_async(self):
18
+ self.plot_elements(invert_y=True)
19
+
20
+ def fixPoint(self, plotterPoint, canvasPoint):
21
+ 'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
22
+ self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
23
+ self.visibleBox.ymin = canvasPoint.y - self.canvasH(self.plotviewBox.ymax-plotterPoint.y)
24
+ self.refresh()
25
+
12
26
  def zoomTo(self, bbox):
13
27
  super().zoomTo(bbox)
14
- self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymax), bbox.xymin)
15
-
16
- def plotpixel(self, x, y, attr, row=None):
17
- y = self.plotviewBox.ymax-y
18
- self.pixels[y][x][attr].append(row)
28
+ self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymin),
29
+ Point(bbox.xmin, bbox.ymax + 1/4*self.canvasCharHeight))
19
30
 
20
31
  def scaleY(self, canvasY):
21
- 'returns plotter y coordinate, with y-axis inverted'
22
- plotterY = super().scaleY(canvasY)
23
- return (self.plotviewBox.ymax-plotterY+4)
32
+ 'returns a plotter y coordinate for a canvas y coordinate, with the y direction inverted'
33
+ return self.plotviewBox.ymax-round((canvasY-self.visibleBox.ymin)*self.yScaler)
24
34
 
25
- def canvasH(self, plotterY):
26
- return (self.plotviewBox.ymax-plotterY)/self.yScaler
35
+ def unscaleY(self, plotterY_inverted):
36
+ 'performs the inverse of scaleY, returns a canvas y coordinate'
37
+ return (self.plotviewBox.ymax-plotterY_inverted)/self.yScaler + self.visibleBox.ymin
27
38
 
28
39
  @property
29
40
  def canvasMouse(self):
30
41
  p = super().canvasMouse
31
- p.y = self.visibleBox.ymin + (self.plotviewBox.ymax-self.plotterMouse.y)/self.yScaler
42
+ if not p: return None
43
+ p.y = self.unscaleY(self.plotterMouse.y)
32
44
  return p
33
45
 
46
+ def calcTopCursorY(self):
47
+ 'ymin for the cursor that will align its top with the top edge of the graph'
48
+ return self.visibleBox.ymax - self.cursorBox.h
49
+
50
+ def calcBottomCursorY(self):
51
+ # Shift by 1 plotter pixel, like with goTopCursorY for Canvas. But shift in the
52
+ # opposite direction, because the y-coordinate system is inverted.
53
+ 'ymin for the cursor that will align its bottom with the bottom edge of the graph'
54
+ return self.visibleBox.ymin - (1/4 * self.canvasCharHeight)
55
+
56
+ def startCursor(self):
57
+ res = super().startCursor()
58
+ if not res: return None
59
+ # Since the y coordinates for plotting increase in the opposite
60
+ # direction from Canvas, the cursor has to be shifted.
61
+ self.cursorBox.ymin -= self.canvasCharHeight
34
62
 
35
63
  # provides axis labels, legend
36
64
  class GraphSheet(InvertedCanvas):
37
65
  def __init__(self, *names, **kwargs):
66
+ self.ylabel_maxw = 0
38
67
  super().__init__(*names, **kwargs)
39
68
 
40
69
  vd.numericCols(self.xcols) or vd.fail('at least one numeric key col necessary for x-axis')
41
70
  self.ycols or vd.fail('%s is non-numeric' % '/'.join(yc.name for yc in kwargs.get('ycols')))
42
71
 
72
+ def resetCanvasDimensions(self, windowHeight, windowWidth):
73
+ if self.left_margin < self.ylabel_maxw:
74
+ self.left_margin = self.ylabel_maxw
75
+ super().resetCanvasDimensions(windowHeight, windowWidth)
76
+
43
77
  @asyncthread
44
78
  def reload(self):
45
79
  nerrors = 0
@@ -62,10 +96,10 @@ class GraphSheet(InvertedCanvas):
62
96
  attr = self.plotColor(k)
63
97
  self.point(graph_x, graph_y, attr, row)
64
98
  nplotted += 1
65
- except Exception:
99
+ except Exception as e:
66
100
  nerrors += 1
67
- if options.debug:
68
- raise
101
+ if vd.options.debug:
102
+ vd.exceptionCaught(e)
69
103
 
70
104
 
71
105
  vd.status('loaded %d points (%d errors)' % (nplotted, nerrors))
@@ -97,6 +131,26 @@ class GraphSheet(InvertedCanvas):
97
131
  srccol = self.ycols[0]
98
132
  return srccol.format(srccol.type(amt))
99
133
 
134
+ def formatXLabel(self, amt):
135
+ if self.xzoomlevel < 1:
136
+ labels = []
137
+ for xcol in self.xcols:
138
+ if vd.isNumeric(xcol):
139
+ col_amt = float(amt) if xcol.type is int else xcol.type(amt)
140
+ else:
141
+ continue
142
+ labels.append(xcol.format(col_amt))
143
+ return ','.join(labels)
144
+ else:
145
+ return self.formatX(amt)
146
+
147
+ def formatYLabel(self, amt):
148
+ srccol = self.ycols[0]
149
+ if srccol.type is int and self.yzoomlevel < 1:
150
+ return srccol.format(float(amt))
151
+ else:
152
+ return self.formatY(amt)
153
+
100
154
  def parseX(self, txt):
101
155
  return self.xcols[0].type(txt)
102
156
 
@@ -104,26 +158,39 @@ class GraphSheet(InvertedCanvas):
104
158
  return self.ycols[0].type(txt)
105
159
 
106
160
  def add_y_axis_label(self, frac):
107
- txt = self.formatY(self.visibleBox.ymin + frac*self.visibleBox.h)
161
+ txt = self.formatYLabel(self.visibleBox.ymin + frac*self.visibleBox.h)
162
+ w = (dispwidth(txt)+1)*2
163
+ if self.ylabel_maxw < w:
164
+ self.ylabel_maxw = w
108
165
 
109
166
  # plot y-axis labels on the far left of the canvas, but within the plotview height-wise
110
- attr = colors.color_graph_axis
111
- self.plotlabel(0, self.plotviewBox.ymin + (1.0-frac)*self.plotviewBox.h, txt, attr)
167
+ self.plotlabel(0, self.plotviewBox.ymin + (1.0-frac)*self.plotviewBox.h, txt, 'graph_axis')
112
168
 
113
169
  def add_x_axis_label(self, frac):
114
- txt = self.formatX(self.visibleBox.xmin + frac*self.visibleBox.w)
170
+ txt = self.formatXLabel(self.visibleBox.xmin + frac*self.visibleBox.w)
171
+ tick = vd.options.disp_graph_tick_x or ''
115
172
 
116
173
  # plot x-axis labels below the plotviewBox.ymax, but within the plotview width-wise
117
- attr = colors.color_graph_axis
118
- xmin = self.plotviewBox.xmin + frac*self.plotviewBox.w
119
- if frac == 1.0:
120
- # shift rightmost label to be readable
121
- xmin -= max(len(txt)*2 - self.rightMarginPixels+1, 0)
122
-
123
- self.plotlabel(xmin, self.plotviewBox.ymax+4, txt, attr)
174
+ x = self.plotviewBox.xmin + frac*self.plotviewBox.w
175
+
176
+ if frac < 1.0:
177
+ txt = tick + txt
178
+ else:
179
+ right_margin = self.plotwidth - 1 - self.plotviewBox.xmax
180
+ if (len(txt)+len(tick))*2 <= right_margin:
181
+ txt = tick + txt
182
+ else:
183
+ # shift rightmost label to be left of its tick
184
+ x -= len(txt)*2
185
+ if len(tick) == 0:
186
+ x += 1
187
+ txt = txt + tick
188
+
189
+ self.plotlabel(x, self.plotviewBox.ymax+4, txt, 'graph_axis')
124
190
 
125
191
  def createLabels(self):
126
192
  self.gridlabels = []
193
+ self.ylabel_maxw = self.leftMarginPixels
127
194
 
128
195
  # y-axis
129
196
  self.add_y_axis_label(1.00)
@@ -143,18 +210,18 @@ class GraphSheet(InvertedCanvas):
143
210
  # TODO: grid lines corresponding to axis labels
144
211
 
145
212
  xname = ','.join(xcol.name for xcol in self.xcols if vd.isNumeric(xcol)) or 'row#'
146
- xname, _ = clipstr(xname, self.leftMarginPixels//2-2)
147
- self.plotlabel(0, self.plotviewBox.ymax+4, xname+'»', colors.color_graph_axis)
213
+ xname, _ = clipstr(xname, self.left_margin//2-2)
214
+ self.plotlabel(0, self.plotviewBox.ymax+4, xname+'»', 'graph_axis')
148
215
 
149
216
 
150
217
  Sheet.addCommand('.', 'plot-column', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols([cursorCol])))', 'plot current numeric column vs key columns; numeric key column is used for x-axis, while categorical key columns determine color')
151
218
  Sheet.addCommand('g.', 'plot-numerics', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols(nonKeyVisibleCols)))', 'plot a graph of all visible numeric columns vs key columns')
152
219
 
153
220
  # swap directions of up/down
154
- InvertedCanvas.addCommand(None, 'go-up', 'sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height')
155
- InvertedCanvas.addCommand(None, 'go-down', 'sheet.cursorBox.ymin -= cursorBox.h', 'move cursor down by its height')
156
- InvertedCanvas.addCommand(None, 'go-top', 'sheet.cursorBox.ymin = visibleBox.ymax', 'move cursor to top edge of visible canvas')
157
- InvertedCanvas.addCommand(None, 'go-bottom', 'sheet.cursorBox.ymin = visibleBox.ymin', 'move cursor to bottom edge of visible canvas')
221
+ InvertedCanvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height')
222
+ InvertedCanvas.addCommand(None, 'go-down', 'if cursorBox: sheet.cursorBox.ymin -= cursorBox.h', 'move cursor down by its height')
223
+ InvertedCanvas.addCommand(None, 'go-top', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcTopCursorY()', 'move cursor to top edge of visible canvas')
224
+ InvertedCanvas.addCommand(None, 'go-bottom', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcBottomCursorY()', 'move cursor to bottom edge of visible canvas')
158
225
  InvertedCanvas.addCommand(None, 'go-pagedown', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin -= t; sheet.visibleBox.ymin -= t; sheet.refresh()', 'move cursor down to next visible page')
159
226
  InvertedCanvas.addCommand(None, 'go-pageup', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin += t; sheet.visibleBox.ymin += t; sheet.refresh()', 'move cursor up to previous visible page')
160
227
 
@@ -185,3 +252,8 @@ vd.addGlobals({
185
252
  'GraphSheet': GraphSheet,
186
253
  'InvertedCanvas': InvertedCanvas,
187
254
  })
255
+
256
+ vd.addMenuItems('''
257
+ Plot > Graph > current column > plot-column
258
+ Plot > Graph > all numeric columns > plot-numerics
259
+ ''')
visidata/guide.py ADDED
@@ -0,0 +1,180 @@
1
+ '''
2
+ # A Guide to VisiData Guides
3
+ Each guide shows you how to use a particular feature in VisiData. Gray guides have not been written yet. We love contributions: [:onclick https://visidata.org/docs/api/guides]https://visidata.org/docs/api/guides[/].
4
+
5
+ - [:keystrokes]Up/Down[/] to move the row cursor
6
+ - [:keystrokes]Enter[/] to view a topic
7
+ '''
8
+ import re
9
+
10
+ from visidata import vd, BaseSheet, Sheet, ItemColumn, Column, VisiData, ENTER, RowColorizer, AttrDict, MissingAttrFormatter
11
+ from visidata import wraptext
12
+
13
+ guides_list = '''
14
+ GuideIndex ("A Guide to VisiData Guides (you are here)")
15
+ HelpGuide ("Where to Start and How to Quit") # manpage; ask for patreon
16
+ MenuGuide ("The VisiData Menu System")
17
+ CommandsSheet ("How to find the command you want run")
18
+
19
+ # real barebones basics
20
+ MovementGuide ("Movement and Search")
21
+ SortGuide ("Sorting")
22
+ TypesSheet ("The basic type system")
23
+ CommandLog ("Undo and Replay")
24
+
25
+ # rev this thing up
26
+
27
+ SelectionGuide ("Selecting and filtering") # stu|, and variants; filtering with dup-sheet; g prefix often refers to selected rowset
28
+ SheetsSheet ("The Sheet Stack")
29
+ ColumnsSheet ("Columns: the only way to fly")
30
+ StatusesSheet ("Revisit old status messages")
31
+ SidebarSheet ("Dive into the sidebar")
32
+ SaversGuide ("Saving Data") # talk about options.overwrite + ro here
33
+
34
+ ErrorsSheet ("What was that error?")
35
+ ModifyGuide ("Adding, Editing, Deleting Rows")
36
+
37
+ # the varieties of data experience
38
+
39
+ SlideGuide ("Sliding rows and columns around")
40
+ ExprGuide ("Compute Python over every row")
41
+ JoinGuide ("Joining multiple sheets together")
42
+ DescribeSheet ("Basic Statistics (min/max/mode/median/mean)")
43
+ AggregatorsSheet ("Aggregations like sum, mean, and distinct")
44
+ FrequencyTable ("Frequency Tables are how you GROUP BY")
45
+ PivotGuide ("Pivot Tables are just Frequency Tables with more columns")
46
+ MeltGuide ("Melt is just Unpivot")
47
+ JsonGuide ("Some special features for JSON") # with expand/contract, unfurl
48
+ RegexGuide ("Matching and Transforming Strings with Regex")
49
+ GraphSheet ("Basic scatterplots and other graphs")
50
+ WindowFunctionGuide ("Perform operations on groups of rows")
51
+
52
+ # for the frequent user
53
+ OptionsSheet ("Options and Settings")
54
+ ClipboardGuide ("Copy and Paste Data via the Clipboard")
55
+ DirSheet ("Browsing the local filesystem")
56
+ FormatsSheet ("What can you open with VisiData?")
57
+ SplitpaneGuide ("Split VisiData into two panes")
58
+ ThemesSheet ("Change Interface Theme")
59
+ ColorSheet ("See available colors")
60
+ MacrosSheet ("Recording macros")
61
+ MemorySheet ("Making note of certain values")
62
+
63
+ # advanced usage and developers
64
+
65
+ ThreadsSheet ("Threads past and present")
66
+ PyobjSheet ("Inspecting internal Python objects")
67
+
68
+ # appendices
69
+
70
+ InputEditorGuide ("Using the builtin line editor")
71
+ '''
72
+
73
+ vd.guides = {} # name -> guidecls
74
+
75
+ @VisiData.api
76
+ def addGuide(vd, name, guidecls):
77
+ vd.guides[name] = guidecls
78
+
79
+ @VisiData.api
80
+ class GuideIndex(Sheet):
81
+ guide = __doc__
82
+
83
+ rowtype = 'guides' # rowdef: list(guide number, guide name, topic description, points, max_points)
84
+ columns = [
85
+ ItemColumn('n', 0, type=int),
86
+ ItemColumn('name', 1, width=0),
87
+ ItemColumn('topic', 2, width=60),
88
+ ]
89
+ colorizers = [
90
+ RowColorizer(7, 'color_guide_unwritten', lambda s,c,r,v: r and r[1] not in vd.guides)
91
+ ]
92
+ def iterload(self):
93
+ i = 0
94
+ for line in guides_list.splitlines():
95
+ m = re.search(r'(\w+?) \("(.*)"\)', line)
96
+ if m:
97
+ yield [i] + list(m.groups())
98
+ i += 1
99
+
100
+ def openRow(self, row):
101
+ name = row[1]
102
+ return vd.getGuide(name)
103
+
104
+ class OptionHelpGetter:
105
+ 'For easy and consistent formatting in sidebars and helpstrings, use {vd.options.help.opt_name}.'
106
+ def __getattr__(self, optname):
107
+ opt = vd.options._get(optname, 'default')
108
+ return f'[:onclick options-sheet {optname}]`{optname}`[/]: to {opt.helpstr} (default: {opt.value})'
109
+
110
+
111
+ class CommandHelpGetter:
112
+ 'For easy and consistent formatting in sidebars and helpstrings, use {vd.commands.help.long_name}.'
113
+ def __init__(self, cls):
114
+ self.cls = cls
115
+ self.helpsheet = vd.HelpSheet()
116
+ self.helpsheet.ensureLoaded()
117
+
118
+ def __getattr__(self, k):
119
+ longname = k.replace('_', '-')
120
+ binding = self.helpsheet.revbinds.get(longname, [None])[0]
121
+ # cmddict has a SheetClass associated with each command
122
+ # go through all the parents of the Sheet type, to look for the command
123
+ for cls in self.cls.superclasses():
124
+ cmd = self.helpsheet.cmddict.get((cls.__name__, longname), None)
125
+ if cmd:
126
+ break
127
+ if not cmd:
128
+ return ''
129
+ if 'input' in cmd.execstr.lower():
130
+ inputtype = 'input'
131
+ m = re.search(r'type="(\w*)"', cmd.execstr, re.IGNORECASE)
132
+ if not m:
133
+ m = re.search(r'input(\w*)\("', cmd.execstr, re.IGNORECASE)
134
+ if m:
135
+ inputtype = m.groups()[0].lower()
136
+ binding += f'[:45] {inputtype}[/]'
137
+
138
+ helpstr = cmd.helpstr
139
+ return f'`{binding}` (`{longname}`) to {helpstr}'
140
+
141
+
142
+ class GuideSheet(Sheet):
143
+ rowtype = 'lines'
144
+ filetype = 'guide'
145
+ columns = [
146
+ ItemColumn('linenum', 0, type=int, width=0),
147
+ ItemColumn('guide', 1, width=80, displayer='full'),
148
+ ]
149
+ precious = False
150
+ guide_text = ''
151
+ sheettype = Sheet
152
+
153
+ def iterload(self):
154
+ winWidth = 78
155
+ helper = AttrDict(commands=CommandHelpGetter(self.sheettype),
156
+ options=OptionHelpGetter())
157
+ guidetext = MissingAttrFormatter().format(self.guide_text, help=helper, vd=vd)
158
+ for startingLine, text in enumerate(guidetext.splitlines()):
159
+ text = text.strip()
160
+ if text:
161
+ for i, (L, _) in enumerate(wraptext(str(text), width=winWidth)):
162
+ yield [startingLine+i+1, L]
163
+ else:
164
+ yield [startingLine+1, text]
165
+
166
+
167
+
168
+ @VisiData.api
169
+ def getGuide(vd, name): # -> GuideSheet()
170
+ if name in vd.guides:
171
+ return vd.guides[name]()
172
+ vd.warning(f'no guide named {name}')
173
+
174
+ BaseSheet.addCommand('', 'open-guide-index', 'vd.push(GuideIndex("VisiData_Guide"))', 'open VisiData guides table of contents')
175
+
176
+ vd.addMenuItems('''
177
+ Help > VisiData Feature Guides > open-guide-index
178
+ ''')
179
+
180
+ vd.addGlobals({'GuideSheet':GuideSheet, "CommandHelpGetter": CommandHelpGetter, "OptionHelpGetter": OptionHelpGetter})
visidata/help.py CHANGED
@@ -1,10 +1,35 @@
1
1
  import functools
2
2
  import collections
3
- from pkg_resources import resource_filename
4
3
 
5
- from visidata import *
4
+ from visidata import VisiData, MetaSheet, ColumnAttr, Column, BaseSheet, VisiDataMetaSheet, SuspendCurses
5
+ from visidata import vd, asyncthread, ENTER, drawcache, AttrDict
6
6
 
7
- vd.show_help = False
7
+ vd.option('disp_help', 2, 'show help panel during input')
8
+
9
+ @BaseSheet.api
10
+ def hint_basichelp(sheet):
11
+ return 0, '`Alt+[:underline]H[/]` to open the [:underline]H[/]elp menu'
12
+
13
+
14
+ @VisiData.api
15
+ def iterMenuPaths(vd, item=None, menupath=[]):
16
+ 'Generate (longname, menupath).'
17
+ if item is None:
18
+ item = vd.menus
19
+
20
+ if isinstance(item, (list, tuple)):
21
+ for m in item:
22
+ yield from vd.iterMenuPaths(m, menupath)
23
+ elif item.longname:
24
+ yield item.longname, ' > '.join(menupath+[item.title])
25
+ else:
26
+ yield from vd.iterMenuPaths(item.menus, menupath+[item.title])
27
+
28
+
29
+ @VisiData.property
30
+ @drawcache
31
+ def menuPathsByLongname(vd):
32
+ return dict(vd.iterMenuPaths())
8
33
 
9
34
 
10
35
  @VisiData.api
@@ -16,10 +41,12 @@ class HelpSheet(MetaSheet):
16
41
 
17
42
  columns = [
18
43
  ColumnAttr('sheet'),
44
+ ColumnAttr('module'),
19
45
  ColumnAttr('longname'),
46
+ Column('menupath', width=0, cache=True, getter=lambda col,row: vd.menuPathsByLongname[row.longname]),
20
47
  Column('keystrokes', getter=lambda col,row: col.sheet.revbinds.get(row.longname, [None])[0]),
21
- Column('all_bindings', width=0, getter=lambda col,row: list(set(col.sheet.revbinds.get(row.longname, [])))),
22
- Column('description', getter=lambda col,row: col.sheet.cmddict[(row.sheet, row.longname)].helpstr),
48
+ Column('all_bindings', width=0, cache=True, getter=lambda col,row: list(set(col.sheet.revbinds.get(row.longname, [])))),
49
+ Column('description', width=40, getter=lambda col,row: col.sheet.cmddict[(row.sheet, row.longname)].helpstr),
23
50
  ColumnAttr('execstr', width=0),
24
51
  Column('logged', width=0, getter=lambda col,row: vd.isLoggableCommand(row.longname)),
25
52
  ]
@@ -54,73 +81,69 @@ class HelpSheet(MetaSheet):
54
81
  return revbinds
55
82
 
56
83
 
57
- @VisiData.api
58
- @asyncthread
59
- def help_search(vd, sheet, regex):
60
- vs = HelpSheet(source=None)
61
- vs.rows = [] # do not trigger push reload
62
- vd.push(vs) # push first, then reload
63
- vd.sync(vs.reload())
64
-
65
- # find rows matching regex on original HelpSheet
66
- rowidxs = list(vd.searchRegex(vs, regex=regex, columns="visibleCols"))
67
-
68
- # add only matching rows
69
- allrows = vs.rows
70
- vs.rows = []
71
- for rowidx in rowidxs:
72
- vs.addRow(allrows[rowidx])
73
-
74
-
75
84
  class HelpPane:
76
85
  def __init__(self, name):
86
+ import visidata
77
87
  self.name = name
78
88
  self.scr = None
79
89
  self.parentscr = None
80
90
  self.amgr = visidata.AnimationMgr()
81
91
 
82
- def draw(self, scr, x=None, y=None):
92
+ @property
93
+ def width(self):
94
+ return self.amgr.maxWidth
95
+
96
+ @property
97
+ def height(self):
98
+ return self.amgr.maxHeight
99
+
100
+ def draw(self, scr, x=None, y=None, **kwargs):
83
101
  if not scr: return
84
- if not vd.show_help:
85
- if self.scr:
86
- self.scr.erase()
87
- self.scr.refresh()
88
- self.scr = None
89
- return
102
+ # if vd.options.disp_help <= 0:
103
+ # if self.scr:
104
+ # self.scr.erase()
105
+ # self.scr.refresh()
106
+ # self.scr = None
107
+ # return
90
108
  if y is None: y=0 # show at top of screen by default
91
109
  if x is None: x=0
110
+ hneeded = self.amgr.maxHeight+3
111
+ wneeded = self.amgr.maxWidth+4
112
+ scrh, scrw = scr.getmaxyx()
92
113
  if not self.scr or scr is not self.parentscr: # (re)allocate help pane scr
93
114
  if y >= 0:
94
- if y+self.amgr.maxHeight+3 < scr.getmaxyx()[0]:
115
+ if y+hneeded < scrh:
95
116
  yhelp = y+1
96
117
  else:
97
- yhelp = y-self.amgr.maxHeight-3
118
+ hneeded = max(0, min(hneeded, y-1))
119
+ yhelp = y-hneeded
98
120
  else: # y<0
99
- yhelp = scr.getmaxyx()[0]-self.amgr.maxHeight-4
121
+ yhelp = max(0, scrh-hneeded-1)
100
122
 
101
123
  if x >= 0:
102
- if x+self.amgr.maxWidth+4 < scr.getmaxyx()[1]:
124
+ if x+wneeded < scrw:
103
125
  xhelp = x+1
104
126
  else:
105
- xhelp = x-self.amgr.maxWidth-4
127
+ wneeded = max(0, min(wneeded, x-1))
128
+ xhelp = x-wneeded
106
129
  else: # x<0
107
- xhelp = scr.getmaxyx()[1]-self.amgr.maxWidth-5
130
+ xhelp = max(0, scrh-wneeded-1)
108
131
 
109
- self.scr = scr.derwin(self.amgr.maxHeight+3, self.amgr.maxWidth+4, yhelp, xhelp)
132
+ self.scr = vd.subwindow(scr, xhelp, yhelp, wneeded, hneeded)
110
133
  self.parentscr = scr
111
134
 
112
135
  self.scr.erase()
113
136
  self.scr.box()
114
- self.amgr.draw(self.scr, y=1, x=2)
137
+ self.amgr.draw(self.scr, y=1, x=2, **kwargs)
115
138
  self.scr.refresh()
116
139
 
117
140
 
118
141
  @VisiData.api
119
142
  @functools.lru_cache(maxsize=None)
120
- def getHelpPane(vd, name, module='vdplus'):
143
+ def getHelpPane(vd, name, module='visidata') -> HelpPane:
121
144
  ret = HelpPane(name)
122
145
  try:
123
- ret.amgr.load(name, Path(resource_filename(module,'ddw/'+name+'.ddw')).open_text(encoding='utf-8'))
146
+ ret.amgr.load(name, (vd.pkg_resources_files(module)/f'ddw/{name}.ddw').open(encoding='utf-8'))
124
147
  ret.amgr.trigger(name, loop=True)
125
148
  except FileNotFoundError as e:
126
149
  vd.debug(str(e))
@@ -135,23 +158,31 @@ def getHelpPane(vd, name, module='vdplus'):
135
158
  def openManPage(vd):
136
159
  import os
137
160
  with SuspendCurses():
138
- if os.system(' '.join(['man', resource_filename(__name__, 'man/vd.1')])) != 0:
139
- vd.push(TextSheet('man_vd', source=Path(resource_filename(__name__, 'man/vd.txt'))))
161
+ module_path = vd.pkg_resources_files(__name__.split('.')[0])
162
+ if os.system(' '.join(['man', str(module_path/'man/vd.1')])) != 0:
163
+ vd.push(TextSheet('man_vd', source=module_path/'man/vd.txt'))
140
164
 
141
165
 
142
166
  # in VisiData, g^H refers to the man page
143
167
  BaseSheet.addCommand('g^H', 'sysopen-help', 'openManPage()', 'Show the UNIX man page for VisiData')
144
168
  BaseSheet.addCommand('z^H', 'help-commands', 'vd.push(HelpSheet(name + "_commands", source=sheet, revbinds={}))', 'list commands and keybindings available on current sheet')
145
169
  BaseSheet.addCommand('gz^H', 'help-commands-all', 'vd.push(HelpSheet("all_commands", source=None, revbinds={}))', 'list commands and keybindings for all sheet types')
146
- BaseSheet.addCommand(None, 'help-search', 'help_search(sheet, input("help: "))', 'search through command longnames with search terms')
147
170
 
148
171
  BaseSheet.bindkey('KEY_F(1)', 'sysopen-help')
149
172
  BaseSheet.bindkey('zKEY_F(1)', 'help-commands')
150
173
  BaseSheet.bindkey('zKEY_BACKSPACE', 'help-commands')
174
+ BaseSheet.bindkey('gKEY_BACKSPACE', 'sysopen-help')
151
175
 
152
- HelpSheet.addCommand(ENTER, 'exec-command', 'quit(sheet); draw_all(); activeStack[0].execCommand(cursorRow.longname)', 'execute command on undersheet')
176
+ HelpSheet.addCommand(None, 'exec-command', 'quit(sheet); draw_all(); activeStack[0].execCommand(cursorRow.longname)', 'execute command on undersheet')
153
177
  BaseSheet.addCommand(None, 'open-tutorial-visidata', 'launchBrowser("https://jsvine.github.io/intro-to-visidata/")', 'open https://jsvine.github.io/intro-to-visidata/')
154
178
 
155
179
  vd.addMenuItem("Help", "VisiData tutorial", 'open-tutorial-visidata')
156
180
  vd.addMenuItem("Help", 'Sheet commands', 'help-commands')
157
181
  vd.addMenuItem("Help", 'All commands', 'help-commands-all')
182
+
183
+ vd.addGlobals(HelpSheet=HelpSheet)
184
+
185
+ vd.addMenuItems('''
186
+ Help > Quick reference > sysopen-help
187
+ Help > Command list > help-commands
188
+ ''')
visidata/hint.py ADDED
@@ -0,0 +1,39 @@
1
+ import collections
2
+
3
+ from visidata import vd, BaseSheet
4
+
5
+
6
+ @BaseSheet.lazy_property
7
+ def prevHints(sheet):
8
+ return collections.defaultdict(int)
9
+
10
+
11
+ @BaseSheet.api
12
+ def getHint(sheet, *args, **kwargs) -> str:
13
+ funcs = [getattr(sheet, x) for x in dir(sheet) if x.startswith('hint_')]
14
+ results = []
15
+ hints = sheet.prevHints
16
+ for f in funcs:
17
+ try:
18
+ r = f(*args, **kwargs)
19
+ if r:
20
+ if isinstance(r, dict):
21
+ n = r.get('_relevance', 1)
22
+ v = r
23
+ elif isinstance(r, tuple):
24
+ n, v = r
25
+ else:
26
+ n = 1
27
+ v = r
28
+
29
+ if v not in sheet.prevHints:
30
+ results.append((n, v))
31
+ sheet.prevHints[v] += 1
32
+ except Exception as e:
33
+ vd.debug(f'{f.__name__}: {e}')
34
+
35
+ if results:
36
+ return sorted(results, reverse=True)[0][1]
37
+
38
+
39
+ vd.addCommand('', 'help-hint', 'status(getHint() or pressMenu("Help"))', 'get context-dependent hint on what to do next')