visidata 3.0.2__py3-none-any.whl → 3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -202
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +7 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +54 -20
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +17 -2
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +16 -3
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/status_source.py +3 -1
  34. visidata/features/sysedit.py +1 -1
  35. visidata/features/transpose.py +2 -1
  36. visidata/features/type_ipaddr.py +2 -4
  37. visidata/features/unfurl.py +1 -0
  38. visidata/form.py +2 -2
  39. visidata/freqtbl.py +16 -11
  40. visidata/fuzzymatch.py +1 -0
  41. visidata/graph.py +163 -12
  42. visidata/guide.py +57 -24
  43. visidata/guides/ClipboardGuide.md +48 -0
  44. visidata/guides/ColumnsGuide.md +52 -0
  45. visidata/guides/CommandsSheet.md +28 -0
  46. visidata/guides/DirSheet.md +34 -0
  47. visidata/guides/ErrorsSheet.md +17 -0
  48. visidata/guides/FrequencyTable.md +42 -0
  49. visidata/guides/GrepSheet.md +28 -0
  50. visidata/guides/JsonSheet.md +38 -0
  51. visidata/guides/MacrosSheet.md +19 -0
  52. visidata/guides/MeltGuide.md +52 -0
  53. visidata/guides/MemorySheet.md +7 -0
  54. visidata/guides/MenuGuide.md +26 -0
  55. visidata/guides/ModifyGuide.md +38 -0
  56. visidata/guides/PivotGuide.md +71 -0
  57. visidata/guides/RegexGuide.md +107 -0
  58. visidata/guides/SelectionGuide.md +44 -0
  59. visidata/guides/SlideGuide.md +26 -0
  60. visidata/guides/SortGuide.md +0 -0
  61. visidata/guides/SplitpaneGuide.md +15 -0
  62. visidata/guides/TypesSheet.md +43 -0
  63. visidata/guides/XsvGuide.md +36 -0
  64. visidata/help.py +6 -6
  65. visidata/hint.py +2 -1
  66. visidata/indexsheet.py +2 -2
  67. visidata/interface.py +13 -14
  68. visidata/keys.py +4 -1
  69. visidata/loaders/api_airtable.py +1 -1
  70. visidata/loaders/archive.py +1 -1
  71. visidata/loaders/csv.py +9 -5
  72. visidata/loaders/eml.py +11 -6
  73. visidata/loaders/f5log.py +1 -0
  74. visidata/loaders/fec.py +18 -42
  75. visidata/loaders/fixed_width.py +19 -3
  76. visidata/loaders/grep.py +121 -0
  77. visidata/loaders/html.py +1 -0
  78. visidata/loaders/http.py +6 -1
  79. visidata/loaders/json.py +22 -4
  80. visidata/loaders/jsonla.py +8 -2
  81. visidata/loaders/mailbox.py +1 -0
  82. visidata/loaders/markdown.py +25 -6
  83. visidata/loaders/msgpack.py +19 -0
  84. visidata/loaders/npy.py +0 -1
  85. visidata/loaders/odf.py +18 -4
  86. visidata/loaders/orgmode.py +1 -1
  87. visidata/loaders/rec.py +6 -4
  88. visidata/loaders/sas.py +11 -4
  89. visidata/loaders/scrape.py +0 -1
  90. visidata/loaders/texttables.py +2 -0
  91. visidata/loaders/tsv.py +24 -7
  92. visidata/loaders/unzip_http.py +127 -3
  93. visidata/loaders/vds.py +4 -0
  94. visidata/loaders/vdx.py +1 -1
  95. visidata/loaders/xlsx.py +5 -0
  96. visidata/loaders/xml.py +2 -1
  97. visidata/macros.py +14 -31
  98. visidata/main.py +14 -13
  99. visidata/mainloop.py +14 -6
  100. visidata/man/vd.1 +72 -39
  101. visidata/man/vd.txt +72 -41
  102. visidata/memory.py +15 -4
  103. visidata/menu.py +14 -3
  104. visidata/metasheets.py +5 -6
  105. visidata/modify.py +4 -4
  106. visidata/mouse.py +2 -0
  107. visidata/movement.py +14 -28
  108. visidata/optionssheet.py +3 -5
  109. visidata/path.py +59 -37
  110. visidata/pivot.py +8 -5
  111. visidata/pyobj.py +63 -9
  112. visidata/save.py +16 -9
  113. visidata/search.py +4 -4
  114. visidata/selection.py +10 -56
  115. visidata/settings.py +37 -35
  116. visidata/sheets.py +186 -108
  117. visidata/shell.py +22 -12
  118. visidata/sidebar.py +71 -16
  119. visidata/sort.py +21 -6
  120. visidata/statusbar.py +42 -5
  121. visidata/stored_list.py +5 -2
  122. visidata/tests/conftest.py +1 -0
  123. visidata/tests/test_commands.py +9 -1
  124. visidata/tests/test_completer.py +18 -0
  125. visidata/tests/test_edittext.py +3 -2
  126. visidata/text_source.py +7 -4
  127. visidata/textsheet.py +20 -6
  128. visidata/themes/ascii8.py +9 -6
  129. visidata/themes/asciimono.py +14 -4
  130. visidata/threads.py +13 -3
  131. visidata/tuiwin.py +5 -1
  132. visidata/type_currency.py +1 -2
  133. visidata/type_date.py +6 -1
  134. visidata/undo.py +10 -5
  135. visidata/utils.py +9 -3
  136. visidata/vdobj.py +21 -1
  137. visidata/wrappers.py +9 -1
  138. {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  139. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
  140. {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
  141. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
  142. visidata-3.1.dist-info/RECORD +284 -0
  143. visidata-3.0.2.dist-info/RECORD +0 -258
  144. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
  145. {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  146. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  147. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  148. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  149. {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/graph.py CHANGED
@@ -1,10 +1,16 @@
1
1
  import math
2
2
 
3
- from visidata import VisiData, Canvas, Sheet, Progress, BoundingBox, Point
4
- from visidata import vd, asyncthread, dispwidth, colors, clipstr
3
+ from visidata import VisiData, Canvas, Sheet, Progress, BoundingBox, Point, ColumnsSheet
4
+ from visidata import vd, asyncthread, dispwidth, colors, clipstr, ColorAttr, update_attr
5
+ from visidata.type_date import date
6
+ from statistics import median
5
7
 
6
8
  vd.theme_option('color_graph_axis', 'bold', 'color for graph axis labels')
7
9
  vd.theme_option('disp_graph_tick_x', '╵', 'character for graph x-axis ticks')
10
+ vd.theme_option('color_graph_refline', '', 'color for graph reference value lines')
11
+ vd.theme_option('disp_graph_reflines_x_charset', '▏││▕', 'charset to render vertical reference lines on graph')
12
+ vd.theme_option('disp_graph_reflines_y_charset', '▔──▁', 'charset to render horizontal reference lines on graph')
13
+ vd.theme_option('disp_graph_multiple_reflines_char', '▒', 'char to render multiple parallel reflines')
8
14
 
9
15
 
10
16
  @VisiData.api
@@ -21,7 +27,7 @@ class InvertedCanvas(Canvas):
21
27
  'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
22
28
  self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
23
29
  self.visibleBox.ymin = canvasPoint.y - self.canvasH(self.plotviewBox.ymax-plotterPoint.y)
24
- self.refresh()
30
+ self.resetBounds()
25
31
 
26
32
  def rowsWithin(self, plotter_bbox):
27
33
  return super().rowsWithin(plotter_bbox, invert_y=True)
@@ -29,9 +35,10 @@ class InvertedCanvas(Canvas):
29
35
  def zoomTo(self, bbox):
30
36
  super().zoomTo(bbox)
31
37
  self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymin),
32
- Point(bbox.xmin, bbox.ymax + 1/4*self.canvasCharHeight))
38
+ Point(bbox.xmin, bbox.ymax))
39
+ self.resetBounds()
33
40
 
34
- def scaleY(self, canvasY):
41
+ def scaleY(self, canvasY) -> int:
35
42
  'returns a plotter y coordinate for a canvas y coordinate, with the y direction inverted'
36
43
  return self.plotviewBox.ymax-round((canvasY-self.visibleBox.ymin)*self.yScaler)
37
44
 
@@ -69,6 +76,11 @@ class GraphSheet(InvertedCanvas):
69
76
  self.ylabel_maxw = 0
70
77
  super().__init__(*names, **kwargs)
71
78
 
79
+ self.reflines_x = []
80
+ self.reflines_y = []
81
+ self.reflines_char_x = {} # { x value in character coordinates -> character to use to draw that vertical line }
82
+ self.reflines_char_y = {} # { y value in character coordinates -> character to use to draw that horizontal line }
83
+
72
84
  vd.numericCols(self.xcols) or vd.fail('at least one numeric key col necessary for x-axis')
73
85
  self.ycols or vd.fail('%s is non-numeric' % '/'.join(yc.name for yc in kwargs.get('ycols')))
74
86
 
@@ -111,11 +123,45 @@ class GraphSheet(InvertedCanvas):
111
123
 
112
124
  self.xzoomlevel=self.yzoomlevel=1.0
113
125
  self.resetBounds()
114
- self.refresh()
115
126
 
116
- def resetBounds(self):
117
- super().resetBounds()
127
+ def draw(self, scr):
128
+ windowHeight, windowWidth = scr.getmaxyx()
129
+ if self.needsRefresh:
130
+ self.render(windowHeight, windowWidth)
131
+
132
+ # required because we use clear_empty_squares=False for draw_pixels()
133
+ self.draw_empty(scr)
134
+ # draw reflines first so pixels draw over them
135
+ self.draw_reflines(scr)
136
+ # use clear_empty_squares to keep reflines
137
+ self.draw_pixels(scr, clear_empty_squares=False)
138
+ self.draw_labels(scr)
139
+
140
+ def draw_reflines(self, scr):
141
+ cursorBBox = self.plotterCursorBox
142
+ # draws only on character cells that have reflines, leaves other cells unaffected
143
+ for char_y in range(0, self.plotheight//4):
144
+ has_y_line = char_y in self.reflines_char_y.keys()
145
+ for char_x in range(0, self.plotwidth//2):
146
+ has_x_line = char_x in self.reflines_char_x.keys()
147
+ if has_x_line or has_y_line:
148
+ cattr = colors.color_refline
149
+ if has_x_line:
150
+ ch = self.reflines_char_x[char_x]
151
+ # where two lines cross, draw the vertical line, not the horizontal one
152
+ elif has_y_line:
153
+ ch = self.reflines_char_y[char_y]
154
+ # draw cursor
155
+ if cursorBBox.contains(char_x*2, char_y*4) or \
156
+ cursorBBox.contains(char_x*2+1, char_y*4+3):
157
+ cattr = update_attr(cattr, colors.color_current_row)
158
+ scr.addstr(char_y, char_x, ch, cattr.attr)
159
+
160
+ def resetBounds(self, refresh=True):
161
+ super().resetBounds(refresh=False)
118
162
  self.createLabels()
163
+ if refresh:
164
+ self.refresh()
119
165
 
120
166
  def moveToRow(self, rowstr):
121
167
  ymin, ymax = map(float, map(self.parseY, rowstr.split()))
@@ -123,6 +169,43 @@ class GraphSheet(InvertedCanvas):
123
169
  self.cursorBox.h = ymax-ymin
124
170
  return True
125
171
 
172
+ def plot_elements(self, invert_y=True):
173
+ self.plot_reflines()
174
+ super().plot_elements(invert_y=True)
175
+
176
+ def plot_reflines(self):
177
+ self.reflines_char_x = {}
178
+ self.reflines_char_y = {}
179
+
180
+ bb = self.visibleBox
181
+ xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
182
+
183
+ for data_y in self.reflines_y:
184
+ data_y = float(data_y)
185
+ if data_y >= ymin and data_y <= ymax:
186
+ char_y, offset = divmod(self.scaleY(data_y), 4)
187
+ chars = self.options.disp_graph_reflines_y_charset
188
+ # if we're drawing two different reflines in the same square, fill it with a different char
189
+ if char_y in self.reflines_char_y and self.reflines_char_y[char_y] != chars[offset]:
190
+ self.reflines_char_y[char_y] = vd.options.disp_graph_multiple_reflines_char
191
+ else:
192
+ self.reflines_char_y[char_y] = chars[offset]
193
+
194
+ for data_x in self.reflines_x:
195
+ data_x = float(data_x)
196
+ if data_x >= xmin and data_x <= xmax:
197
+ plot_x = self.scaleX(data_x)
198
+ # plot_x is an integer count of plotter pixels, and each character box has 2 plotter pixels
199
+ char_x = plot_x // 2
200
+ # To subdivide the 2 plotter pixels per square into 4 zones, we have to first multiply by 2.
201
+ offset = 2*plot_x % 4
202
+ chars = self.options.disp_graph_reflines_x_charset
203
+ # if we're drawing two different reflines in the same square, fill it with a different char
204
+ if char_x in self.reflines_char_x and self.reflines_char_x[char_x] != chars[offset]:
205
+ self.reflines_char_y[char_x] = vd.options.disp_graph_multiple_reflines_char
206
+ else:
207
+ self.reflines_char_x[char_x] = chars[offset]
208
+
126
209
  def moveToCol(self, colstr):
127
210
  xmin, xmax = map(float, map(self.parseX, colstr.split()))
128
211
  self.cursorBox.xmin = xmin
@@ -163,20 +246,23 @@ class GraphSheet(InvertedCanvas):
163
246
  return self.ycols[0].type(txt)
164
247
 
165
248
  def add_y_axis_label(self, frac):
166
- txt = self.formatYLabel(self.visibleBox.ymin + frac*self.visibleBox.h)
249
+ label_data_y = self.visibleBox.ymin + frac*self.visibleBox.h
250
+ txt = self.formatYLabel(label_data_y)
167
251
  w = (dispwidth(txt)+1)*2
168
252
  if self.ylabel_maxw < w:
169
253
  self.ylabel_maxw = w
254
+ y = self.scaleY(label_data_y)
170
255
 
171
256
  # plot y-axis labels on the far left of the canvas, but within the plotview height-wise
172
- self.plotlabel(0, self.plotviewBox.ymin + (1.0-frac)*self.plotviewBox.h, txt, 'graph_axis')
257
+ self.plotlabel(0, y, txt, 'graph_axis')
173
258
 
174
259
  def add_x_axis_label(self, frac):
175
- txt = self.formatXLabel(self.visibleBox.xmin + frac*self.visibleBox.w)
260
+ label_data_x = self.visibleBox.xmin + frac*self.visibleBox.w
261
+ txt = self.formatXLabel(label_data_x)
176
262
  tick = vd.options.disp_graph_tick_x or ''
177
263
 
178
264
  # plot x-axis labels below the plotviewBox.ymax, but within the plotview width-wise
179
- x = self.plotviewBox.xmin + frac*self.plotviewBox.w
265
+ x = self.scaleX(label_data_x)
180
266
 
181
267
  if frac < 1.0:
182
268
  txt = tick + txt
@@ -223,9 +309,62 @@ class GraphSheet(InvertedCanvas):
223
309
  rows = super().rowsWithin(plotter_bbox)
224
310
  return sorted(rows, key=lambda r: self.row_order[self.source.rowid(r)])
225
311
 
312
+ def draw_refline_x(self):
313
+ xcol = vd.numericCols(self.xcols)[0]
314
+ xtype = xcol.type
315
+ val = median(xcol.getValues(self.sourceRows))
316
+ suggested = format_input_value(val, xtype)
317
+ xstrs = vd.input("add line(s) at x = ", type="reflinex", value=suggested, defaultLast=True).split()
318
+
319
+ for xstr in xstrs:
320
+ vals = [ v.strip() for v in xstr.split(',') ]
321
+ if len(vals) != len(self.xcols):
322
+ vd.fail(f'must have {len(self.xcols)} x values, had {len(vals)} values: {xstr}')
323
+ self.reflines_x += [xtype(val) for xcol, val in zip(self.xcols, vals) if xtype(val) not in self.reflines_x ]
324
+ self.refresh()
325
+
326
+ def draw_refline_y(self):
327
+ ytype = self.ycols[0].type
328
+ val = median(self.ycols[0].getValues(self.sourceRows))
329
+ suggested = format_input_value(val, ytype)
330
+ ystrs = vd.input("add line(s) at y = ", type="refliney", value=suggested, defaultLast=True).split()
331
+
332
+ self.reflines_y += [ ytype(y) for y in ystrs if ytype(y) not in self.reflines_y ]
333
+ self.refresh()
334
+
335
+ def erase_refline_x(self):
336
+ if len(self.reflines_x) == 0:
337
+ vd.fail(f'no x refline to erase')
338
+ xtype = vd.numericCols(self.xcols)[0].type
339
+ suggested = format_input_value(self.reflines_x[0], xtype)
340
+
341
+ xstrs = vd.input('remove line(s) at x = ', value=suggested, type='reflinex', defaultLast=True).split()
342
+ for input_x in xstrs:
343
+ self.reflines_x.remove(xtype(input_x))
344
+ self.refresh()
345
+
346
+ def erase_refline_y(self):
347
+ ytype = self.ycols[0].type
348
+ suggested = format_input_value(self.reflines_y[0], ytype) if self.reflines_y else ''
349
+ ystrs = vd.input('remove line(s) at y = ', value=suggested, type='refliney', defaultLast=True).split()
350
+ for y in ystrs:
351
+ try:
352
+ self.reflines_y.remove(ytype(y))
353
+ except ValueError:
354
+ vd.fail(f'value {y} not in reflines_y')
355
+ self.refresh()
356
+
357
+ def format_input_value(val, type):
358
+ '''format a value for entry into vd.input(), so its representation has no spaces and no commas'''
359
+ if type is date:
360
+ return val.strftime('%Y-%m-%d')
361
+ else:
362
+ return str(val)
363
+
226
364
 
227
365
  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')
228
366
  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')
367
+ ColumnsSheet.addCommand('g.', 'plot-source-selected', 'vd.push(GraphSheet(sheet.source[0].name, "graph", source=source[0], sourceRows=source[0].rows, xcols=source[0].keyCols, ycols=numericCols(selectedRows)))', 'plot a graph of all selected columns vs key columns on source sheet')
229
368
 
230
369
  # swap directions of up/down
231
370
  InvertedCanvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height')
@@ -257,6 +396,12 @@ def set_x(sheet, s):
257
396
  Canvas.addCommand('y', 'resize-y-input', 'sheet.set_y(input("set ymin ymax="))', 'set ymin/ymax on graph axes')
258
397
  Canvas.addCommand('x', 'resize-x-input', 'sheet.set_x(input("set xmin xmax="))', 'set xmin/xmax on graph axes')
259
398
 
399
+ GraphSheet.addCommand('gx', 'draw-refline-x', 'sheet.draw_refline_x()', 'draw a vertical line at x-values (space-separated)')
400
+ GraphSheet.addCommand('gy', 'draw-refline-y', 'sheet.draw_refline_y()', 'draw a horizontal line at y-values (space-separated)')
401
+ GraphSheet.addCommand('zx', 'erase-refline-x', 'sheet.erase_refline_x()', 'remove a horizontal line at x-values (space-separated)')
402
+ GraphSheet.addCommand('zy', 'erase-refline-y', 'sheet.erase_refline_y()', 'remove a vertical line at y-values (space-separated)')
403
+ GraphSheet.addCommand('gzx', 'erase-reflines-x', 'sheet.reflines_x = []; sheet.refresh()', 'erase all vertical x-value lines')
404
+ GraphSheet.addCommand('gzy', 'erase-reflines-y', 'sheet.reflines_y = []; sheet.refresh()', 'erase any horizontal y-value lines')
260
405
 
261
406
  vd.addGlobals({
262
407
  'GraphSheet': GraphSheet,
@@ -266,4 +411,10 @@ vd.addGlobals({
266
411
  vd.addMenuItems('''
267
412
  Plot > Graph > current column > plot-column
268
413
  Plot > Graph > all numeric columns > plot-numerics
414
+ Plot > Refline > draw at x coord (vert) > draw-refline-x
415
+ Plot > Refline > draw at y coord (horiz) > draw-refline-y
416
+ Plot > Refline > erase at x coord (vert) > erase-refline-x
417
+ Plot > Refline > erase at y coord (horiz) > erase-refline-y
418
+ Plot > Refline > erase all x (vert) > erase-reflines-x
419
+ Plot > Refline > erase all y (horiz) > erase-reflines-y
269
420
  ''')
visidata/guide.py CHANGED
@@ -8,7 +8,8 @@ Each guide shows you how to use a particular feature in VisiData. Gray guides ha
8
8
  import re
9
9
 
10
10
  from visidata import vd, BaseSheet, Sheet, ItemColumn, Column, VisiData, ENTER, RowColorizer, AttrDict, MissingAttrFormatter
11
- from visidata import wraptext
11
+ from visidata import wraptext, Path, CellColorizer
12
+ import visidata
12
13
 
13
14
  guides_list = '''
14
15
  GuideIndex ("A Guide to VisiData Guides (you are here)")
@@ -20,7 +21,7 @@ CommandsSheet ("How to find the command you want run")
20
21
  MovementGuide ("Movement and search")
21
22
  InputGuide ("Input keystrokes")
22
23
  SortGuide ("Sorting")
23
- ColumnsGuide ("Rename, hide, and resize columns")
24
+ ColumnsGuide ("Resize, hide, and rename columns")
24
25
  TypesSheet ("Column types")
25
26
  CommandLog ("Undo and Replay")
26
27
 
@@ -46,7 +47,7 @@ AggregatorsSheet ("Aggregations like sum, mean, and distinct")
46
47
  FrequencyTable ("Frequency Tables are how you GROUP BY")
47
48
  PivotGuide ("Pivot Tables are just Frequency Tables with more columns")
48
49
  MeltGuide ("Melt is just Unpivot")
49
- JsonGuide ("Some special features for JSON") # with expand/contract, unfurl
50
+ JsonSheet ("Some special features for JSON") # with expand/contract, unfurl
50
51
  RegexGuide ("Matching and Transforming Strings with Regex")
51
52
  GraphSheet ("Basic scatterplots and other graphs")
52
53
  WindowFunctionGuide ("Perform operations on groups of rows")
@@ -62,10 +63,15 @@ ColorSheet ("See available colors")
62
63
  MacrosSheet ("Recording macros")
63
64
  MemorySheet ("Making note of certain values")
64
65
 
66
+ # Specific use cases
67
+
68
+ XsvGuide ("CSV/TSV and other text-delimited formats")
69
+ GrepSheet ("Load output of grep-like tools")
70
+
65
71
  # advanced usage and developers
66
72
 
67
73
  ThreadsSheet ("Threads past and present")
68
- PyobjSheet ("Inspecting internal Python objects")
74
+ DeveloperGuide ("Inspecting internal Python objects")
69
75
 
70
76
  # appendices
71
77
 
@@ -75,8 +81,10 @@ InputEditorGuide ("Using the builtin line editor")
75
81
  vd.guides = {} # name -> guidecls
76
82
 
77
83
  @VisiData.api
78
- def addGuide(vd, name, guidecls):
79
- vd.guides[name] = guidecls
84
+ def addGuide(vd, name):
85
+ guideSource = Path(vd.pkg_resources_files(visidata)/f'guides/{name}.md')
86
+ if guideSource.exists():
87
+ vd.guides[name] = GuideSheet(name, source=guideSource)
80
88
 
81
89
  @VisiData.api
82
90
  class GuideIndex(Sheet):
@@ -84,7 +92,7 @@ class GuideIndex(Sheet):
84
92
 
85
93
  rowtype = 'guides' # rowdef: list(guide number, guide name, topic description, points, max_points)
86
94
  columns = [
87
- ItemColumn('n', 0, type=int),
95
+ ItemColumn('n', 0, width=0, type=int),
88
96
  ItemColumn('name', 1, width=0),
89
97
  ItemColumn('topic', 2, width=60),
90
98
  ]
@@ -96,7 +104,9 @@ class GuideIndex(Sheet):
96
104
  for line in guides_list.splitlines():
97
105
  m = re.search(r'(\w+?) \("(.*)"\)', line)
98
106
  if m:
99
- yield [i] + list(m.groups())
107
+ guidename, description = list(m.groups())
108
+ vd.addGuide(guidename)
109
+ yield [i, guidename, description]
100
110
  i += 1
101
111
 
102
112
  def openRow(self, row):
@@ -104,25 +114,25 @@ class GuideIndex(Sheet):
104
114
  return vd.getGuide(name)
105
115
 
106
116
  class OptionHelpGetter:
107
- 'For easy and consistent formatting in sidebars and helpstrings, use {vd.options.help.opt_name}.'
117
+ 'For easy and consistent formatting in sidebars and helpstrings, use {help.options.opt_name}.'
108
118
  def __getattr__(self, optname):
109
119
  opt = vd.options._get(optname, 'default')
110
- return f'[:onclick options-sheet {optname}][:longname]{optname}[/][/]: to {opt.helpstr} (default: {opt.value})'
120
+ return f'[:onclick options-sheet {optname}][:longname_guide]{optname}[/][/]: {opt.helpstr} (default: {opt.value})'
111
121
 
112
122
 
113
123
  class CommandHelpGetter:
114
- 'For easy and consistent formatting in sidebars and helpstrings, use {vd.commands.help.long_name}.'
124
+ 'For easy and consistent formatting in sidebars and helpstrings, use {help.commands.long_name}.'
115
125
  def __init__(self, cls):
116
126
  self.cls = cls
117
127
  self.helpsheet = vd.HelpSheet()
118
- self.helpsheet.ensureLoaded()
128
+ vd.sync(self.helpsheet.ensureLoaded())
119
129
 
120
130
  def __getattr__(self, k):
121
131
  return self.__getitem__(k)
122
132
 
123
133
  def __getitem__(self, k):
124
134
  longname = k.replace('_', '-')
125
- binding = self.helpsheet.revbinds.get(longname, [None])[0]
135
+ binding = self.helpsheet.revbinds.get(longname, [None])[0] or '<unbound>'
126
136
  # cmddict has a SheetClass associated with each command
127
137
  # go through all the parents of the Sheet type, to look for the command
128
138
  for cls in self.cls.superclasses():
@@ -137,11 +147,11 @@ class CommandHelpGetter:
137
147
  if not m:
138
148
  m = re.search(r'input(\w*)\("', cmd.execstr, re.IGNORECASE)
139
149
  if m:
140
- inputtype = m.groups()[0].lower()
141
- binding += f'[:45] {inputtype}[/]'
150
+ inputtype = m.groups()[0].lower() or 'input'
151
+ binding += f' <{inputtype}>'
142
152
 
143
153
  helpstr = cmd.helpstr
144
- return f'[:keys]{binding}[/] ([:longname]{longname}[/]) to {helpstr}'
154
+ return f'[:code]{binding}[/] ([:longname_guide]{longname}[/]) to {helpstr}'
145
155
 
146
156
 
147
157
  class GuideSheet(Sheet):
@@ -152,14 +162,31 @@ class GuideSheet(Sheet):
152
162
  ItemColumn('guide', 1, width=80, displayer='full'),
153
163
  ]
154
164
  precious = False
155
- guide_text = ''
156
- sheettype = Sheet
165
+ colorizers = [CellColorizer(4, 'color_sidebar', lambda s,c,r,v: True)]
157
166
 
158
167
  def iterload(self):
168
+ self.metadata = AttrDict(sheettype='Sheet')
169
+ text = self.source.open(mode='r').read()
159
170
  winWidth = 78
160
- helper = AttrDict(commands=CommandHelpGetter(self.sheettype),
171
+
172
+ # parsing front matter
173
+ sections = text.split('---\n', maxsplit=2)
174
+ for section in sections[:-1]:
175
+ for config in section.splitlines():
176
+ config = config.strip()
177
+ if config:
178
+ try:
179
+ key, val = config.split(': ', maxsplit=1)
180
+ except ValueError:
181
+ vd.fail('incorrect front matter syntax')
182
+ self.metadata[key] = val
183
+
184
+ # formatting text
185
+ helper = AttrDict(commands=CommandHelpGetter(vd.getGlobals()[self.metadata.sheettype]),
161
186
  options=OptionHelpGetter())
162
- guidetext = MissingAttrFormatter().format(self.guide_text, help=helper, vd=vd)
187
+ guidetext = MissingAttrFormatter().format(sections[-1], help=helper, vd=vd)
188
+
189
+ # parsing guide
163
190
  for startingLine, text in enumerate(guidetext.splitlines()):
164
191
  text = text.strip()
165
192
  if text:
@@ -169,11 +196,10 @@ class GuideSheet(Sheet):
169
196
  yield [startingLine+1, text]
170
197
 
171
198
 
172
-
173
199
  @VisiData.api
174
200
  def getGuide(vd, name): # -> GuideSheet()
175
201
  if name in vd.guides:
176
- return vd.guides[name]()
202
+ return vd.guides[name]
177
203
  vd.warning(f'no guide named {name}')
178
204
 
179
205
  BaseSheet.addCommand('', 'open-guide-index', 'vd.push(GuideIndex("VisiData_Guide"))', 'open VisiData guides table of contents')
@@ -189,8 +215,15 @@ def inputKeys(vd, prompt):
189
215
 
190
216
  @BaseSheet.api
191
217
  def getCommandInfo(sheet, keys):
218
+ if not keys:
219
+ return ''
192
220
  cmd = sheet.getCommand(keys)
193
- return CommandHelpGetter(type(sheet))[cmd.longname]
221
+ if cmd:
222
+ return CommandHelpGetter(type(sheet))[cmd.longname]
223
+ else:
224
+ vd.warning(f'no command bound to {keys} on {sheet}')
225
+
226
+ GuideSheet.options.color_current_row = "underline"
194
227
 
195
228
  vd.addCommand('', 'show-command-info', 'status(getCommandInfo(inputKeys("get command for keystrokes: ")))', 'show longname and helpstring for keybinding')
196
229
 
@@ -200,4 +233,4 @@ vd.addMenuItems('''
200
233
 
201
234
  vd.optalias('guides', 'preplay', 'open-guide-index')
202
235
 
203
- vd.addGlobals({'GuideSheet':GuideSheet, "CommandHelpGetter": CommandHelpGetter, "OptionHelpGetter": OptionHelpGetter})
236
+ vd.addGlobals({"CommandHelpGetter": CommandHelpGetter, "OptionHelpGetter": OptionHelpGetter})
@@ -0,0 +1,48 @@
1
+ # Copy, Cut and Paste...
2
+
3
+ ## ...rows to/from the internal clipboard
4
+
5
+ - {help.commands.copy_row}
6
+ - {help.commands.copy_selected}
7
+
8
+ - {help.commands.cut_row}
9
+ - {help.commands.cut_selected}
10
+
11
+ - {help.commands.paste_after}
12
+ - {help.commands.paste_before}
13
+
14
+ ## ...cells to/from the internal clipboard
15
+
16
+ - {help.commands.copy_cell}
17
+ - {help.commands.cut_cell}
18
+ - {help.commands.paste_cell}
19
+
20
+ ## ...columns from the intenral clipboard
21
+
22
+ - {help.commands.copy_cells}
23
+ - {help.commands.cut_cells}
24
+ - {help.commands.setcol_clipboard}
25
+
26
+ If there are fewer values in the clipboard than in the selected target column, the clipboard values will be repeated in sequence.
27
+
28
+ ## ....to/from the system clipboard
29
+
30
+ - {help.commands.syscopy_cell}
31
+ - {help.commands.syscopy_cells}
32
+
33
+ - {help.commands.syscopy_row}
34
+ - {help.commands.syscopy_selected}
35
+
36
+ - {help.commands.syspaste_cells}
37
+ - {help.commands.syspaste_cells_selected}
38
+
39
+ ## Paste a new sheet from the system clipboard
40
+
41
+ - {help.commands.open_syspaste}
42
+
43
+ The paste command will prompt for the filetype, so the input data (from the clipboard) can be in any supported format.
44
+
45
+ ## Relevant options
46
+
47
+ - {help.options.clipboard_copy_cmd}
48
+ - {help.options.clipboard_paste_cmd}
@@ -0,0 +1,52 @@
1
+ # Resize, hide, and rename columns
2
+
3
+ ## Resize the current column
4
+
5
+ - {help.commands.resize_col_max}
6
+ - {help.commands.resize_col_input}
7
+ - {help.commands.resize_col_half}
8
+
9
+ ## Resize multiple columns
10
+
11
+ - {help.commands.resize_cols_input}
12
+
13
+ ## Hide and Unhide columns
14
+
15
+ Clean up the sheet by hiding unwanted columns. Hidden columns are still present, but not visible in the main set of columns.
16
+ (They are minimized on the right side and can be unhidden with {help.commands.resize_col_max}.
17
+
18
+ - {help.commands.hide_col}
19
+ - {help.commands.unhide_cols}
20
+
21
+ Use {help.commands.columns_sheet} to control visibility as well.
22
+
23
+ ## Rename columns
24
+
25
+ - {help.commands.rename_col}
26
+
27
+ ## Rename one or more columns using data
28
+
29
+ Rename one or more columns using data from the sheet:
30
+
31
+ - {help.commands.rename_col_selected}
32
+ - {help.commands.rename_cols_row}
33
+ - {help.commands.rename_cols_selected}
34
+
35
+ ## Clean column names
36
+
37
+ Rename columns to be valid Python identifiers that can be used in column
38
+ expressions.
39
+
40
+ - {help.commands.normalize_col_names}
41
+ - {help.commands.clean_names}
42
+
43
+ ## Change row height
44
+
45
+ Sometimes the data for a cell will not fit within the current width. If the
46
+ data is large, or spans multiple lines, change the row height to show more for
47
+ the current column width. Row height adjustments can also be combined with
48
+ column width changes.
49
+
50
+ - {help.commands.toggle_multiline}
51
+ - {help.commands.resize_height_input}
52
+ - {help.commands.resize_height_max}
@@ -0,0 +1,28 @@
1
+ # How to find commands
2
+
3
+ ## Command Palette
4
+
5
+ - {help.commands.exec_longname}
6
+
7
+ Start typing a command longname or keyword in its helpstring.
8
+
9
+ - [:code]Enter[/] to execute top command.
10
+ - [:code]Tab[/] to highlight top command and provide a numeric jumplist.
11
+
12
+ When a command is highlighted:
13
+
14
+ - [:code]Tab[/]/[:code]Shift+Tab[/] to cycle highlighted command.
15
+ - [:code]Enter[/] to execute highlighted command.
16
+ - [:code]0-9[/] to execute numbered command.
17
+
18
+ ## Command Sheet
19
+
20
+ The Command Sheet lists all of the available commands, their keyboard bindings, function names, and other information.
21
+
22
+ - {help.commands.help_commands_all}
23
+ - {help.commands.help_commands}
24
+
25
+ 'description' and 'longname' column: useful to find rows that share a longname suffix or prefix. E.g, `save` or `selected`.
26
+
27
+ 'keystrokes' and 'all_bindings' column: useful to search for keybindings. E.g, `Ctrl` or `F`.
28
+
@@ -0,0 +1,34 @@
1
+ ---
2
+ sheettype: DirSheet
3
+ ---
4
+ # Directory Sheet
5
+
6
+ The **DirSheet** is a display of files and folders in the *source* directory.
7
+
8
+ To load a **DirSheet**:
9
+
10
+ - provide a directory as a source on the CLI (e.g. [:code]vd sample_data/[/])
11
+ - {help.commands.open_dir_current} (equivalent to [:code]vd .[/])
12
+
13
+ Use the **DirSheet** to find and open files:
14
+
15
+ - {help.commands.open_row_file}
16
+ - {help.commands.open_rows}
17
+ - {help.commands.open_dir_parent}
18
+ - {help.commands.sysopen_row}
19
+
20
+ Use the **DirSheet** as a file manager:
21
+
22
+ - {help.commands.copy_row}
23
+ - {help.commands.copy_selected}
24
+ - [:keys]d[/] ([:longname]delete-row[/]) to delete file from filesystem
25
+ - [:keys]e[/] ([:longname]edit-cell[/]) to change file metadata (all of the file metadata, except for *filetype*, is modifiable)
26
+
27
+ Modifications on the **DirSheet** are deferred - they do not take effect on the filesystem itself until they are confirmed.
28
+ - {help.commands.commit_sheet}
29
+
30
+ ## Options of Interest (must reload to take effect)
31
+
32
+ - pass the flag [:code]-r[/] ([:code]--recursive[/]) on the CLI to display all of the files in all subfolders
33
+ - {help.options.dir_depth}
34
+ - {help.options.dir_hidden}
@@ -0,0 +1,17 @@
1
+ # What was that error?
2
+
3
+ Status messages include [:warning]warnings[/] and [:error]errors[/].
4
+
5
+ A command may issue a [:warning]warning[/] status and continue running.
6
+ A command that [:warning]fails[/] or [:error]errors[/] is aborted.
7
+
8
+ ## Investigating errors further
9
+
10
+ If a Python Exception like [:error]RuntimeError[/] appears in the sidebar:
11
+
12
+ - {help.commands.error_recent}
13
+ - {help.commands.errors_all}
14
+
15
+ If [:note_type]{vd.options.disp_note_fmtexc}[/] or [:error]{vd.options.disp_note_getexc}[/] appear inside a cell, it indicates an error happened during calculation, type-conversion, or formatting. When the cursor is on an error cell:
16
+
17
+ - {help.commands.error_cell}
@@ -0,0 +1,42 @@
1
+ # Frequency Tables are how you GROUP BY
2
+
3
+ Frequency Tables group rows into bins by column value, and includes summary columns for source columns with aggregators.
4
+
5
+ Set `--numeric-binning` to bin numeric rows into ranges instead of discrete values.
6
+
7
+ - {help.commands.freq_col}
8
+
9
+ - {help.commands.freq_keys}
10
+
11
+ - {help.commands.freq_summary}
12
+
13
+ ## Aggregators
14
+
15
+ A **Frequency Table** contains a summary columns for each aggregator added to a source column.
16
+ These aggregators need to be added before creating the Frequency Table.
17
+ Examples of aggregators include min, max, sum, distinct, count, and list.
18
+
19
+ - {help.commands.aggregate-col}
20
+
21
+ Note: set an appropriate type for the aggregator target column, for example {help.commands.type_float}.
22
+
23
+ ## Explore the data
24
+
25
+ Dive into a group to see the underlying row(s) using the **Frequency Table**:
26
+
27
+ - {help.commands.open_row}
28
+ - {help.commands.dive_selected}
29
+
30
+ Select a group to select all of its underlying rows in the source sheet.
31
+
32
+ ## Using Split Panes with Frequency Tables
33
+
34
+ Press `Shift+Z` to open a split pane, and then `Shift+F` to create a **Frequency Table**. The **Frequency Table** will automatically open in the other pane.
35
+
36
+ See the `SplitpanesGuide` for more.
37
+
38
+ #### Options
39
+
40
+ - {help.options.disp_histogram}
41
+ - {help.options.histogram_bins}
42
+ - {help.options.numeric_binning}