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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -199
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +9 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +66 -24
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +23 -4
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +18 -5
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/slide.py +15 -17
  34. visidata/features/status_source.py +3 -1
  35. visidata/features/sysedit.py +1 -1
  36. visidata/features/transpose.py +2 -1
  37. visidata/features/type_ipaddr.py +2 -4
  38. visidata/features/unfurl.py +1 -0
  39. visidata/form.py +2 -2
  40. visidata/freqtbl.py +16 -11
  41. visidata/fuzzymatch.py +1 -0
  42. visidata/graph.py +173 -12
  43. visidata/guide.py +61 -25
  44. visidata/guides/ClipboardGuide.md +48 -0
  45. visidata/guides/ColumnsGuide.md +52 -0
  46. visidata/guides/CommandsSheet.md +28 -0
  47. visidata/guides/DirSheet.md +34 -0
  48. visidata/guides/ErrorsSheet.md +17 -0
  49. visidata/guides/FrequencyTable.md +42 -0
  50. visidata/guides/GrepSheet.md +28 -0
  51. visidata/guides/JsonSheet.md +38 -0
  52. visidata/guides/MacrosSheet.md +19 -0
  53. visidata/guides/MeltGuide.md +52 -0
  54. visidata/guides/MemorySheet.md +7 -0
  55. visidata/guides/MenuGuide.md +26 -0
  56. visidata/guides/ModifyGuide.md +38 -0
  57. visidata/guides/PivotGuide.md +71 -0
  58. visidata/guides/RegexGuide.md +107 -0
  59. visidata/guides/SelectionGuide.md +44 -0
  60. visidata/guides/SlideGuide.md +26 -0
  61. visidata/guides/SortGuide.md +0 -0
  62. visidata/guides/SplitpaneGuide.md +15 -0
  63. visidata/guides/TypesSheet.md +43 -0
  64. visidata/guides/XsvGuide.md +36 -0
  65. visidata/help.py +6 -6
  66. visidata/hint.py +2 -1
  67. visidata/indexsheet.py +2 -2
  68. visidata/interface.py +13 -14
  69. visidata/keys.py +4 -1
  70. visidata/loaders/api_airtable.py +1 -1
  71. visidata/loaders/archive.py +1 -1
  72. visidata/loaders/csv.py +9 -5
  73. visidata/loaders/eml.py +11 -6
  74. visidata/loaders/f5log.py +1 -0
  75. visidata/loaders/fec.py +18 -42
  76. visidata/loaders/fixed_width.py +19 -3
  77. visidata/loaders/grep.py +121 -0
  78. visidata/loaders/html.py +1 -0
  79. visidata/loaders/http.py +6 -1
  80. visidata/loaders/json.py +22 -4
  81. visidata/loaders/jsonla.py +8 -2
  82. visidata/loaders/mailbox.py +1 -0
  83. visidata/loaders/markdown.py +25 -6
  84. visidata/loaders/msgpack.py +19 -0
  85. visidata/loaders/npy.py +0 -1
  86. visidata/loaders/odf.py +18 -4
  87. visidata/loaders/orgmode.py +1 -1
  88. visidata/loaders/rec.py +6 -4
  89. visidata/loaders/sas.py +11 -4
  90. visidata/loaders/scrape.py +0 -1
  91. visidata/loaders/texttables.py +2 -0
  92. visidata/loaders/tsv.py +24 -7
  93. visidata/loaders/unzip_http.py +127 -3
  94. visidata/loaders/vds.py +4 -0
  95. visidata/loaders/vdx.py +1 -1
  96. visidata/loaders/xlsx.py +5 -0
  97. visidata/loaders/xml.py +2 -1
  98. visidata/macros.py +14 -31
  99. visidata/main.py +20 -15
  100. visidata/mainloop.py +17 -6
  101. visidata/man/vd.1 +74 -39
  102. visidata/man/vd.txt +73 -41
  103. visidata/memory.py +16 -5
  104. visidata/menu.py +14 -3
  105. visidata/metasheets.py +5 -6
  106. visidata/modify.py +4 -4
  107. visidata/mouse.py +2 -0
  108. visidata/movement.py +14 -28
  109. visidata/optionssheet.py +3 -5
  110. visidata/path.py +59 -37
  111. visidata/pivot.py +8 -5
  112. visidata/pyobj.py +63 -9
  113. visidata/rename_col.py +18 -1
  114. visidata/save.py +16 -9
  115. visidata/search.py +4 -4
  116. visidata/selection.py +10 -56
  117. visidata/settings.py +37 -35
  118. visidata/sheets.py +189 -118
  119. visidata/shell.py +23 -14
  120. visidata/sidebar.py +71 -16
  121. visidata/sort.py +21 -6
  122. visidata/statusbar.py +42 -5
  123. visidata/stored_list.py +5 -2
  124. visidata/tests/conftest.py +1 -0
  125. visidata/tests/test_commands.py +9 -1
  126. visidata/tests/test_completer.py +18 -0
  127. visidata/tests/test_edittext.py +3 -2
  128. visidata/text_source.py +7 -4
  129. visidata/textsheet.py +20 -6
  130. visidata/themes/ascii8.py +9 -6
  131. visidata/themes/asciimono.py +14 -4
  132. visidata/threads.py +13 -3
  133. visidata/tuiwin.py +5 -1
  134. visidata/type_currency.py +1 -2
  135. visidata/type_date.py +6 -1
  136. visidata/undo.py +10 -13
  137. visidata/utils.py +9 -3
  138. visidata/vdobj.py +21 -1
  139. visidata/wrappers.py +9 -1
  140. {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  141. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
  142. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
  143. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
  144. visidata-3.1.dist-info/RECORD +284 -0
  145. visidata-3.0.1.dist-info/RECORD +0 -258
  146. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
  147. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  148. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  149. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  150. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  151. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
@@ -32,9 +32,7 @@ def setcol_precision(col, amount:int):
32
32
  if not precision_str is None:
33
33
  col.fmtstr = f'%.{max(0, int(precision_str[1]) + amount)}f'
34
34
  else:
35
- col.type = float
36
- if col.fmtstr == '':
37
- col.fmtstr = '%.2f'
35
+ vd.fail('column type must be numeric or date')
38
36
 
39
37
 
40
38
  vd.addMenuItems('''
@@ -58,12 +58,18 @@ def inputPalette(sheet, prompt, items,
58
58
  formatter=lambda m, item, trigger_key: f'{trigger_key} {item}',
59
59
  multiple=False,
60
60
  **kwargs):
61
+ if sheet.options.disp_expert >= 5:
62
+ return vd.input(prompt,
63
+ completer=CompleteKey(sorted(item[value_key] for item in items)),
64
+ **kwargs)
65
+
61
66
  bindings = dict()
62
67
 
63
68
  tabitem = -1
64
69
 
65
70
  def tab(n, nitems):
66
71
  nonlocal tabitem
72
+ if not nitems: return None
67
73
  tabitem = (tabitem + n) % nitems
68
74
 
69
75
  def _draw_palette(value):
@@ -109,22 +115,28 @@ def inputPalette(sheet, prompt, items,
109
115
  for i in range(nitems-len(palrows)):
110
116
  palrows.append((None, None))
111
117
 
118
+ used_triggers = set()
112
119
  for i, (m, item) in enumerate(palrows):
113
120
  trigger_key = ''
114
121
  if tabitem >= 0 and item:
115
- trigger_key = f'{i+1}'[-1]
116
- bindings[trigger_key] = partial(add_to_input if multiple else accept_input, value=item[value_key])
122
+ tkey = f'{i+1}'[-1]
123
+ if tkey not in used_triggers:
124
+ trigger_key = tkey
125
+ bindings[trigger_key] = partial(add_to_input if multiple else accept_input, value=item[value_key])
126
+ used_triggers.add(trigger_key)
117
127
 
118
128
  attr = colors.color_cmdpalette
119
129
 
120
130
  if tabitem < 0 and palrows:
121
131
  _ , topitem = palrows[0]
132
+ if not topitem: return
122
133
  if multiple:
123
134
  bindings[' '] = partial(add_to_input, value=topitem[value_key])
124
135
  bindings['^J'] = partial(accept_input_if_subset, value=topitem[value_key])
125
136
  else:
126
137
  bindings['^J'] = partial(accept_input, value=topitem[value_key])
127
138
  elif item and i == tabitem:
139
+ if not item: return
128
140
  if multiple:
129
141
  bindings['^J'] = partial(accept_input_if_subset, value=item[value_key])
130
142
  bindings[' '] = partial(add_to_input, value=item[value_key])
@@ -160,12 +172,12 @@ def inputLongname(sheet):
160
172
  prompt = 'command name: '
161
173
  # get set of commands possible in the sheet
162
174
  this_sheets_help = HelpSheet('', source=sheet)
163
- this_sheets_help.ensureLoaded()
175
+ vd.sync(this_sheets_help.ensureLoaded())
164
176
 
165
177
  def _fmt_cmdpal_summary(match, row, trigger_key):
166
178
  keystrokes = this_sheets_help.revbinds.get(row.longname, [None])[0] or ' '
167
179
  formatted_longname = match.formatted.get('longname', row.longname) if match else row.longname
168
- formatted_name = f'[:longname][:onclick {row.longname}]{formatted_longname}[/][/]'
180
+ formatted_name = f'[:bold][:onclick {row.longname}]{formatted_longname}[/][/]'
169
181
  if vd.options.debug and match:
170
182
  keystrokes = f'[{match.score}]'
171
183
  r = f' [:keystrokes]{keystrokes.rjust(len(prompt)-5)}[/] '
@@ -186,6 +198,12 @@ def inputLongname(sheet):
186
198
  help=vd.help_longname,
187
199
  type='longname')
188
200
 
201
+ @BaseSheet.api
202
+ def inputLongnameSimple(sheet):
203
+ 'Input a command longname without using the command palette.'
204
+ longnames = set(k for (k, obj), v in vd.commands.iter(sheet))
205
+ return vd.input("command name: ", completer=CompleteKey(sorted(longnames)), type='longname')
206
+
189
207
 
190
208
  @BaseSheet.api
191
209
  def exec_longname(sheet, longname):
@@ -195,3 +213,4 @@ def exec_longname(sheet, longname):
195
213
 
196
214
 
197
215
  vd.addCommand('Space', 'exec-longname', 'exec_longname(inputLongname())', 'execute command by its longname')
216
+ vd.addCommand('zSpace', 'exec-longname-simple', 'exec_longname(inputLongnameSimple())', 'execute command by its longname')
@@ -44,4 +44,4 @@ BaseSheet.addCommand(None, 'open-colors', 'vd.push(vd.colorsSheet)', 'open Color
44
44
 
45
45
  @VisiData.lazy_property
46
46
  def colorsSheet(vd):
47
- return ColorSheet()
47
+ return ColorSheet('colors')
@@ -24,7 +24,7 @@ __author__ = "Jeremy Singer-Vine <jsvine@gmail.com>"
24
24
 
25
25
  from copy import copy
26
26
 
27
- from visidata import Sheet, BaseSheet, asyncthread, Progress, vd
27
+ from visidata import Sheet, TableSheet, asyncthread, Progress, vd
28
28
 
29
29
 
30
30
  def gen_identify_duplicates(sheet):
@@ -103,8 +103,8 @@ def dedupe_rows(sheet):
103
103
 
104
104
 
105
105
  # Add longname-commands to VisiData to execute these methods
106
- BaseSheet.addCommand(None, "select-duplicate-rows", "sheet.select_duplicate_rows()", "select each row that is a duplicate of a prior row")
107
- BaseSheet.addCommand(None, "dedupe-rows", "vd.push(sheet.dedupe_rows())", "open new sheet in which only non-duplicate rows in the active sheet are included")
106
+ TableSheet.addCommand(None, "select-duplicate-rows", "sheet.select_duplicate_rows()", "select each row that is a duplicate of a prior row")
107
+ TableSheet.addCommand(None, "dedupe-rows", "vd.push(sheet.dedupe_rows())", "open new sheet in which only non-duplicate rows in the active sheet are included")
108
108
 
109
109
  vd.addMenuItems('''
110
110
  Row > Select > duplicate rows > select-duplicate-rows
@@ -0,0 +1,71 @@
1
+ import itertools
2
+ import re
3
+ from visidata import vd, Sheet, AttrDict, dispwidth
4
+
5
+
6
+ @Sheet.api
7
+ def nextColRegex(sheet, colregex):
8
+ 'Go to first visible column after the cursor matching `colregex`.'
9
+ pivot = sheet.cursorVisibleColIndex
10
+ for i in itertools.chain(range(pivot+1, len(sheet.visibleCols)), range(0, pivot+1)):
11
+ c = sheet.visibleCols[i]
12
+ if re.search(colregex, c.name, sheet.regex_flags()):
13
+ return i
14
+
15
+ vd.fail('no column name matches /%s/' % colregex)
16
+
17
+ @Sheet.api
18
+ def nextColName(sheet, show_cells=True):
19
+ if len(sheet.visibleCols) == 0: vd.fail('no columns to choose from')
20
+
21
+ prompt = 'choose column name: '
22
+ colnames = []
23
+ for c in sheet.visibleCols:
24
+ dv = None
25
+ if show_cells and len(sheet.rows) > 0:
26
+ dv = c.getDisplayValue(sheet.cursorRow)
27
+ #the underscore that starts _cursor_cell excludes it from being fuzzy matched
28
+ item = AttrDict(name_lower=c.name.lower(),
29
+ name=c.name,
30
+ _cursor_cell=dv)
31
+ colnames.append(item)
32
+
33
+ def _fmt_colname(match, row, trigger_key):
34
+ name = match.formatted.get('name', row.name) if match else row.name
35
+ r = ' '*(len(prompt)-3)
36
+ r += f'[:keystrokes]{trigger_key}[/] '
37
+ if show_cells and len(sheet.rows) > 0:
38
+ # pad the right side with spaces
39
+ # use row.name, because name from match contains
40
+ # extra formatting characters that change its length
41
+ n_spaces = max(20 - dispwidth(row.name), 0)
42
+ r += name + n_spaces*' '
43
+ r += ' '
44
+ #todo: does not show disp_note_none for None
45
+ r += row._cursor_cell
46
+ else:
47
+ r += name
48
+ return r
49
+ name = vd.activeSheet.inputPalette(prompt,
50
+ colnames,
51
+ value_key='name',
52
+ type='column_name',
53
+ formatter=_fmt_colname)
54
+
55
+ pivot = sheet.cursorVisibleColIndex
56
+ for i in itertools.chain(range(pivot+1, len(sheet.visibleCols)), range(0, pivot+1)):
57
+ if name == sheet.visibleCols[i].name:
58
+ return i
59
+
60
+ vd.warning(f'found no column with name: {name}')
61
+
62
+
63
+ Sheet.addCommand('c', 'go-col-regex', 'sheet.cursorVisibleColIndex=nextColRegex(inputRegex("column name regex: ", type="regex-col", defaultLast=True))', 'go to next column with name matching regex')
64
+ Sheet.addCommand('zc', 'go-col-number', 'sheet.cursorVisibleColIndex = int(input("move to column number: "))', 'go to given column number (0-based)')
65
+ Sheet.addCommand('', 'go-col-name', 'sheet.cursorVisibleColIndex=nextColName()', 'go to next column with name matching string, case-insensitive')
66
+
67
+ vd.addMenuItems('''
68
+ Column > Goto > by regex > go-col-regex
69
+ Column > Goto > by number > go-col-number
70
+ Column > Goto > by name > go-col-name
71
+ ''')
@@ -54,7 +54,7 @@ def ext_plot_seaborn(rows, xcols, ycols):
54
54
  nplotted += 1
55
55
  except Exception:
56
56
  nerrors += 1
57
- if options.debug:
57
+ if vd.options.debug:
58
58
  raise
59
59
 
60
60
  sns.scatterplot(
visidata/features/join.py CHANGED
@@ -18,7 +18,8 @@ def ensureLoaded(vd, sheets):
18
18
 
19
19
  @asyncthread
20
20
  def _appendRowsAfterLoading(joinsheet, origsheets):
21
- if vd.ensureLoaded(origsheets):
21
+ with Progress(gerund='loading'):
22
+ vd.ensureLoaded(origsheets)
22
23
  vd.sync()
23
24
 
24
25
  colnames = {c.name:c for c in joinsheet.visibleCols}
@@ -95,7 +96,7 @@ vd.jointypes = [AttrDict(key=k, desc=v) for k, v in {
95
96
  'append': 'all rows from all sheets; columns from all sheets',
96
97
  'concat': 'all rows from all sheets; columns and type from first sheet',
97
98
  'extend': 'only rows from first sheet; type from first sheet; columns from all sheets',
98
- 'merge': 'merge differences from other sheets into first sheet (including new rows)',
99
+ 'merge': 'all rows from all sheets; where keys match, use latest truthy value',
99
100
  }.items()]
100
101
 
101
102
  def joinkey(sheetKeyCols, row):
@@ -154,7 +155,9 @@ class JoinKeyColumn(Column):
154
155
 
155
156
 
156
157
  class MergeColumn(Column):
158
+ # .cols is { sheet: col, ... } in sheet-join order
157
159
  def calcValue(self, row):
160
+ 'Return value from last joined sheet with truth-y value in this column for the given row.'
158
161
  for vs, c in reversed(list(self.cols.items())):
159
162
  if c:
160
163
  v = c.getTypedValue(row[vs])
@@ -167,7 +170,11 @@ class MergeColumn(Column):
167
170
 
168
171
  def isDiff(self, row, value):
169
172
  col = list(self.cols.values())[0]
170
- return col and value != col.getValue(row[col.sheet])
173
+ if not col:
174
+ return False
175
+ if row[col.sheet] is None:
176
+ return True
177
+ return value != col.getValue(row[col.sheet])
171
178
 
172
179
 
173
180
  #### slicing and dicing
@@ -184,8 +191,9 @@ class JoinSheet(Sheet):
184
191
  def loader(self):
185
192
  sheets = self.sources
186
193
 
187
- vd.ensureLoaded(sheets)
188
- vd.sync()
194
+ with Progress(gerund='loading'):
195
+ vd.ensureLoaded(sheets)
196
+ vd.sync()
189
197
 
190
198
  # first item in joined row is the key tuple from the first sheet.
191
199
  # first columns are the key columns from the first sheet, using its row (0)
@@ -196,6 +204,7 @@ class JoinSheet(Sheet):
196
204
  self.setKeys(self.columns)
197
205
 
198
206
  allcols = collections.defaultdict(dict) # colname: { sheet: origcol, ... }
207
+ # MergeColumn relies on allcols having sheets in this specific order
199
208
  for sheetnum, vs in enumerate(sheets):
200
209
  for c in vs.visibleCols:
201
210
  if c not in self.sheetKeyCols[vs]:
@@ -266,8 +275,9 @@ def ExtendedSheet_reload(self, sheets):
266
275
  # first item in joined row is the key tuple from the first sheet.
267
276
  # first columns are the key columns from the first sheet, using its row (0)
268
277
 
269
- vd.ensureLoaded(sheets)
270
- vd.sync()
278
+ with Progress(gerund='loading'):
279
+ vd.ensureLoaded(sheets)
280
+ vd.sync()
271
281
 
272
282
  self.columns = []
273
283
  for i, c in enumerate(sheets[0].keyCols):
@@ -331,11 +341,11 @@ class ConcatSheet(Sheet):
331
341
  # only one column with each name allowed per sheet
332
342
  keyedcols = collections.defaultdict(dict) # name -> { sheet -> col }
333
343
 
344
+ with Progress(gerund='loading'):
345
+ vd.ensureLoaded(self.source)
346
+ vd.sync()
334
347
  with Progress(gerund='joining', sheet=self, total=sum(vs.nRows for vs in self.source)) as prog:
335
348
  for sheet in self.source:
336
- if sheet.ensureLoaded():
337
- vd.sync()
338
-
339
349
  for r in sheet.rows:
340
350
  yield (sheet, r)
341
351
  prog.addProgress(1)
@@ -1,4 +1,4 @@
1
- from visidata import VisiData, vd, Column, Sheet, Fanout
1
+ from visidata import VisiData, vd, Column, Sheet, Fanout, asyncthread
2
2
 
3
3
  @Column.api
4
4
  def setWidth(self, w):
@@ -35,22 +35,35 @@ def hide_col(vd, col):
35
35
  if not col: vd.fail("no columns to hide")
36
36
  col.hide()
37
37
 
38
+ @Sheet.api
39
+ @asyncthread
40
+ def hide_uniform_cols(sheet):
41
+ if len(sheet.rows) < 2:
42
+ return
43
+ for col in sheet.visibleCols:
44
+ vals = (col.getTypedValue(r) for r in sheet.rows)
45
+ first = next(vals)
46
+ if all(v == first for v in vals):
47
+ col.hide()
48
+
38
49
  Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
39
50
  Sheet.addCommand('z_', 'resize-col-input', 'width = int(input("set width= ", value=cursorCol.width)); cursorCol.setWidth(width)', 'adjust width of current column to N')
40
51
  Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.setWidth(c.getMaxWidth(visibleRows))', 'toggle widths of all visible columns between full and default width')
41
52
  Sheet.addCommand('gz_', 'resize-cols-input', 'width = int(input("set width= ", value=cursorCol.width)); Fanout(visibleCols).setWidth(width)', 'adjust widths of all visible columns to N')
42
53
 
43
- Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'Hide current column')
54
+ Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
44
55
  Sheet.addCommand('z-', 'resize-col-half', 'cursorCol.setWidth(cursorCol.width//2)', 'reduce width of current column by half')
56
+ Sheet.addCommand(None, 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
45
57
 
46
- Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'Unhide all hidden columns')
58
+ Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
47
59
  Sheet.addCommand('v', 'toggle-multiline', 'for c in visibleCols: c.toggleMultiline()', 'toggle multiline display')
48
60
  Sheet.addCommand('zv', 'resize-height-input', 'Fanout(visibleCols).height=int(input("set height for all columns to: ", value=max(c.height for c in sheet.visibleCols)))', 'resize row height to N')
49
61
  Sheet.addCommand('gzv', 'resize-height-max', 'h=calc_height(cursorRow, {}, maxheight=windowHeight-1); vd.status(f"set height for all columns to {h}"); Fanout(visibleCols).height=h', 'resize row height to max height needed to see this row')
50
62
 
51
63
  vd.addMenuItems('''
52
- Column > Hide > hide-col
53
- Column > Unhide all > unhide-cols
64
+ Column > Hide > Hide > hide-col
65
+ Column > Hide > Hide uniform > hide-uniform-cols
66
+ Column > Hide > Unhide all > unhide-cols
54
67
  Column > Resize > half width > resize-col-half
55
68
  Column > Resize > current column width to max > resize-col-max
56
69
  Column > Resize > current column width to N > resize-col-input
visidata/features/ping.py CHANGED
@@ -7,23 +7,23 @@ from statistics import mean
7
7
 
8
8
  from visidata import vd, VisiData, BaseSheet, Sheet, Column, AttrColumn, Progress
9
9
 
10
- vd.theme_option('color_shellcmd', '21 on 114 green', '')
11
- vd.theme_option('color_colname', 'underline', '')
12
- vd.theme_option('color_longname', 'bold 52 on 114 green', '')
13
-
14
-
15
10
  @VisiData.api
16
11
  def new_ping(vd, p):
17
12
  'Open a sheet with the round-trip time for each hop along the path to the given host.'
18
- pingsheet = PingSheet(p.given, source=p.given)
19
- return PingStatsSheet("traceroute_"+pingsheet.name, source=pingsheet)
13
+ return makePingStats(p.given)
20
14
 
15
+ # open_ping() is called when a local file exists matching the path p, otherwise new_ping() is called
16
+ vd.open_ping = vd.new_ping
17
+
18
+ def makePingStats(ip):
19
+ pingsheet = PingSheet(ip, source=ip)
20
+ return PingStatsSheet("traceroute_"+pingsheet.name, source=pingsheet)
21
21
 
22
22
  class PingStatsSheet(Sheet):
23
23
  help='''# ping/traceroute
24
- This sheet runs {shellcmd}traceroute{} to generate intermediate hops, then runs {shellcmd}ping{} against each hop N times to get the {colname}avg_ms{} and {colname}max_ms{} ping time.
24
+ This sheet runs `traceroute` to generate intermediate hops, then runs `ping` against each hop N times to get the `avg_ms` and `max_ms` ping time.
25
25
 
26
- {longname}open-source{} to open the raw ping data.
26
+ - {help.commands.open_source} (for raw ping data)
27
27
  '''
28
28
  rowtype='hosts' # rowdef: PingColumn
29
29
  columns = [
@@ -36,7 +36,7 @@ This sheet runs {shellcmd}traceroute{} to generate intermediate hops, then runs
36
36
  ]
37
37
  def loader(self):
38
38
  sheet = self.source
39
- sheet.ensureLoaded()
39
+ vd.sync(sheet.ensureLoaded())
40
40
 
41
41
  self.rows = sheet.columns
42
42
 
@@ -76,7 +76,7 @@ class PingSheet(Sheet):
76
76
  def ping_error(self, ip, data):
77
77
  if ip in self.sources:
78
78
  self.sources.remove(ip)
79
- vd.warning("%s removed: %s" % (ip, data))
79
+ vd.warning("%s removed: %s" % (ip, data.rstrip()))
80
80
 
81
81
  def update_traces(self, row, ip):
82
82
  rtes = self.routes.get(ip)
@@ -150,7 +150,11 @@ vd.option('ping_count', 3, 'send this many pings to each host', sheettype=PingSh
150
150
  vd.option('ping_interval', 0.1, 'wait between ping rounds, in seconds', sheettype=PingSheet)
151
151
 
152
152
  PingSheet.options.null_value = False
153
- BaseSheet.addCommand('', 'open-ping', 'vd.push(openSource(input("ping: ", type="hostip"), filetype="ping"))', 'open sheet to ping input IP Address')
153
+ BaseSheet.addCommand('', 'open-ping', 'vd.push(makePingStats(vd.input("ping: ", type="hostip")))', 'open sheet to ping input IP Address')
154
+
155
+ vd.addGlobals(
156
+ makePingStats=makePingStats,
157
+ )
154
158
 
155
159
  vd.addMenuItems('''
156
160
  System > Ping IP/hostname > open-ping
@@ -126,12 +126,12 @@ def inputRegexSubst(vd, prompt):
126
126
  after=dict(type='regex-replace', prompt='replace: ', help=prompt))
127
127
 
128
128
 
129
- Sheet.addCommand(':', 'addcol-split', 'addColumnAtCursor(RegexColumn(makeRegexSplitter, cursorCol, inputRegex("split regex: ", type="regex-split")))', 'Add column split by regex')
130
- Sheet.addCommand(';', 'addcol-capture', 'addColumnAtCursor(RegexColumn(makeRegexMatcher, cursorCol, inputRegex("capture regex: ", type="regex-capture")))', 'Add column captured by regex')
129
+ Sheet.addCommand(':', 'addcol-split', 'addColumnAtCursor(RegexColumn(makeRegexSplitter, cursorCol, inputRegex("split regex: ", type="regex-split")))', 'add column split by regex')
130
+ Sheet.addCommand(';', 'addcol-capture', 'addColumnAtCursor(RegexColumn(makeRegexMatcher, cursorCol, inputRegex("capture regex: ", type="regex-capture")))', 'add column captured by regex')
131
131
 
132
- Sheet.addCommand('*', 'addcol-regex-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, **inputRegexSubst("regex transform column"))))', 'add column derived from current column, replacing regex with subst (may include \1 backrefs)')
133
- Sheet.addCommand('g*', 'setcol-regex-subst', 'setValuesFromRegex([cursorCol], someSelectedRows, **inputRegexSubst("regex transform column"))', 'regex/subst - modify selected rows in current column, replacing regex with subst, (may include backreferences \\1 etc)')
134
- Sheet.addCommand('gz*', 'setcol-regex-subst-all', 'setValuesFromRegex(visibleCols, someSelectedRows, **inputRegexSubst(f"regex transform {nVisibleCols} columns"))', 'modify selected rows in all visible columns, replacing regex with subst (may include \\1 backrefs)')
132
+ Sheet.addCommand('*', 'addcol-regex-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, **inputRegexSubst("regex transform column"))))', 'add column derived from current column, replacing `search` regex with `replace` (may include \\1 backrefs)')
133
+ Sheet.addCommand('g*', 'setcol-regex-subst', 'setValuesFromRegex([cursorCol], someSelectedRows, **inputRegexSubst("regex transform column"))', 'modify selected rows in current column, replacing `search` regex with `replace`, (may include backreferences \\1 etc)')
134
+ Sheet.addCommand('gz*', 'setcol-regex-subst-all', 'setValuesFromRegex(visibleCols, someSelectedRows, **inputRegexSubst(f"regex transform {nVisibleCols} columns"))', 'modify selected rows in all visible columns, replacing `search` regex with `replace` (may include \\1 backrefs)')
135
135
 
136
136
 
137
137
  vd.addMenuItems('''
@@ -58,30 +58,28 @@ def moveVisibleCol(sheet, fromVisColIdx, toVisColIdx):
58
58
  return fromVisColIdx
59
59
 
60
60
 
61
- Sheet.addCommand('H', 'slide-left', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol-1)', 'slide current column left')
62
- Sheet.addCommand('L', 'slide-right', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex+1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol+1)', 'slide current column right')
61
+ Sheet.addCommand('H', 'slide-left', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol-1)', 'slide the current column left **one position**')
62
+ Sheet.addCommand('L', 'slide-right', 'sheet.cursorVisibleColIndex = slide_col(cursorVisibleColIndex, cursorVisibleColIndex+1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, cursorCol.keycol+1)', 'slide the current column right **one position**')
63
63
  Sheet.addCommand('J', 'slide-down', 'sheet.cursorRowIndex = slide_row(cursorRowIndex, cursorRowIndex+1)', 'slide current row down')
64
64
  Sheet.addCommand('K', 'slide-up', 'sheet.cursorRowIndex = slide_row(cursorRowIndex, cursorRowIndex-1)', 'slide current row up')
65
- Sheet.addCommand('gH', 'slide-leftmost', 'slide_col(cursorVisibleColIndex, len(keyCols) + 0) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, 1)', 'slide current column all the way to the left of sheet')
66
- Sheet.addCommand('gL', 'slide-rightmost', 'slide_col(cursorVisibleColIndex, nVisibleCols-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, len(keyCols))', 'slide current column all the way to the right of sheet')
65
+ Sheet.addCommand('gH', 'slide-leftmost', 'slide_col(cursorVisibleColIndex, len(keyCols) + 0) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, 1)', 'slide the current column **all the way** to the left of its section')
66
+ Sheet.addCommand('gL', 'slide-rightmost', 'slide_col(cursorVisibleColIndex, nVisibleCols-1) if not cursorCol.keycol else slide_keycol(cursorCol.keycol, len(keyCols))', 'slide the current column **all the way** to the right of its section')
67
67
  Sheet.addCommand('gJ', 'slide-bottom', 'slide_row(cursorRowIndex, nRows)', 'slide current row all the way to the bottom of sheet')
68
68
  Sheet.addCommand('gK', 'slide-top', 'slide_row(cursorRowIndex, 0)', 'slide current row to top of sheet')
69
- Sheet.addCommand('zH', 'slide-left-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex-int(input("slide col left n=", value=1)))', 'slide current column N positions to the left')
70
- Sheet.addCommand('zL', 'slide-right-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex+int(input("slide col left n=", value=1)))', 'slide current column N positions to the right')
69
+ Sheet.addCommand('zH', 'slide-left-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex-int(input("slide col left n=", value=1)))', 'slide current column **N positions** to the left')
70
+ Sheet.addCommand('zL', 'slide-right-n', 'slide_col(cursorVisibleColIndex, cursorVisibleColIndex+int(input("slide col left n=", value=1)))', 'slide current column **N positions** to the right')
71
71
  Sheet.addCommand('zJ', 'slide-down-n', 'slide_row(cursorRowIndex, cursorRowIndex+int(input("slide row down n=", value=1)))', 'slide current row N positions down')
72
72
  Sheet.addCommand('zK', 'slide-up-n', 'slide_row(cursorRowIndex, cursorRowIndex-int(input("slide row up n=", value=1)))', 'slide current row N positions up')
73
73
 
74
- Sheet.bindkey('KEY_SLEFT', 'slide-left')
75
- Sheet.bindkey('KEY_SR', 'slide-up')
76
- Sheet.bindkey('kDN', 'slide-down')
77
- Sheet.bindkey('kUP', 'slide-up')
78
- Sheet.bindkey('KEY_SRIGHT', 'slide-right')
79
- Sheet.bindkey('KEY_SF', 'slide-down')
80
-
81
- Sheet.bindkey('gKEY_SLEFT', 'slide-leftmost')
82
- Sheet.bindkey('gkDN', 'slide-bottom')
83
- Sheet.bindkey('gkUP', 'slide-top')
84
- Sheet.bindkey('gKEY_SRIGHT', 'slide-rightmost')
74
+ Sheet.bindkey('Shift+Left', 'slide-left')
75
+ Sheet.bindkey('Shift+Up', 'slide-up')
76
+ Sheet.bindkey('Shift+Down', 'slide-down')
77
+ Sheet.bindkey('Shift+Right', 'slide-right')
78
+
79
+ Sheet.bindkey('g Shift+Left', 'slide-leftmost')
80
+ Sheet.bindkey('g Shift+Down', 'slide-bottom')
81
+ Sheet.bindkey('g Shift+Up', 'slide-top')
82
+ Sheet.bindkey('g Shift+Right', 'slide-rightmost')
85
83
 
86
84
  vd.addMenuItems('''
87
85
  Edit > Slide > Row > up > slide-up
@@ -5,7 +5,9 @@ from visidata import VisiData
5
5
 
6
6
  @VisiData.api
7
7
  def getStatusSource(vd):
8
- stack = inspect.stack()
8
+ if not vd.options.debug:
9
+ return ''
10
+ stack = inspect.stack(context=0) #2370
9
11
  for i, sf in enumerate(stack):
10
12
  if sf.function in 'status aside'.split():
11
13
  if stack[i+1].function in 'error fail warning debug'.split():
@@ -30,7 +30,7 @@ def syseditCells_async(sheet, cols, rows, filetype=None):
30
30
  vd.sync(tempvs.ensureLoaded())
31
31
 
32
32
  while isinstance(tempvs, IndexSheet):
33
- tempvs.ensureLoaded()
33
+ vd.sync(tempvs.ensureLoaded())
34
34
  tempvs = tempvs.rows[0]
35
35
 
36
36
  for col in sheet.visibleCols:
@@ -16,7 +16,8 @@ class TransposeSheet(Sheet):
16
16
  # rows become columns
17
17
  for row in Progress(self.source.rows, 'transposing'):
18
18
  self.addColumn(Column('_'.join(map(str, self.source.rowkey(row))),
19
- getter=lambda c,origcol,row=row: origcol.getValue(row)))
19
+ getter=lambda c,origcol,row=row: origcol.getValue(row),
20
+ setter=lambda c,origcol,v,row=row: origcol.setValue(row, v)))
20
21
 
21
22
  # columns become rows
22
23
  self.rows = list(self.source.nonKeyVisibleCols)
@@ -7,8 +7,8 @@ from visidata import vd
7
7
  from visidata.sheets import Column, TableSheet
8
8
 
9
9
 
10
- vd.addType(ip_address, icon=":", formatter=lambda fmt, ip: str(ip))
11
- vd.addType(ip_network, icon="/", formatter=lambda fmt, ip: str(ip))
10
+ vd.addType(ip_address, icon=":", formatter=lambda fmt, ip: str(ip), name='ip_address')
11
+ vd.addType(ip_network, icon="/", formatter=lambda fmt, ip: str(ip), name='ip_network')
12
12
 
13
13
 
14
14
  def isSupernet(cell, network, isNull):
@@ -69,5 +69,3 @@ TableSheet.addCommand(
69
69
  'cursorCol.selectSupernets(input("ip or cidr block: "))',
70
70
  "select rows where the CIDR block value includes the input address space",
71
71
  )
72
-
73
- vd.addGlobals(ip_address=ip_address, ip_network=ip_network)
@@ -25,6 +25,7 @@ class UnfurledSheet(Sheet):
25
25
  self.cursorVisibleColIndex = len(self.columns)-1
26
26
  self.addColumn(ColumnItem(col.name + "_key", 1))
27
27
  self.addColumn(ColumnItem(col.name + "_value", 2))
28
+ self.cursorVisibleColIndex = len(self.columns)-1
28
29
  else:
29
30
  self.addColumn(SubColumnFunc(col.name, col, 0, keycol=col.keycol))
30
31
 
visidata/form.py CHANGED
@@ -13,8 +13,8 @@ class FormSheet(VisiDataMetaSheet):
13
13
  rowtype='labels' # rowdef: { .x .y .text .color .command .input .key}
14
14
 
15
15
  @VisiData.api
16
- def replayCommand(vd, longname, sheet=None, col='', row=''):
17
- vd.replayOne(vd.cmdlog.newRow(sheet=self.name, col=col, row=row, longname=r.command, input=r.input))
16
+ def replayCommand(vd, longname, input=None, sheet=None, col='', row=''):
17
+ vd.replayOne(vd.cmdlog.newRow(sheet=sheet, col=col, row=row, longname=longname, input=input))
18
18
 
19
19
 
20
20
  class FormCanvas(BaseSheet):
visidata/freqtbl.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from copy import copy
2
2
  import itertools
3
3
 
4
- from visidata import vd, asyncthread, vlen, VisiData, Column, AttrColumn, Sheet, ColumnsSheet, ENTER
4
+ from visidata import vd, asyncthread, vlen, VisiData, Column, AttrColumn, Sheet, ColumnsSheet, ENTER, Fanout
5
5
  from visidata.pivot import PivotSheet, PivotGroupRow
6
6
 
7
7
 
@@ -19,13 +19,19 @@ def valueNames(vd, discrete_vals, numeric_vals):
19
19
  return '+'.join(ret)
20
20
 
21
21
  class HistogramColumn(Column):
22
+ '.sourceCol is the column to be histogrammed'
22
23
  def calcValue(col, row):
23
24
  histogram = col.sheet.options.disp_histogram
24
25
  histolen = col.width-2
25
- return histogram*(histolen*len(row.sourcerows)//col.sheet.largest)
26
+ return histogram*(histolen*col.sourceCol.getTypedValue(row)//col.largest)
27
+
28
+ def updateLargest(col, row):
29
+ col.largest = max(col.largest, col.sourceCol.getTypedValue(row))
26
30
 
27
31
 
28
32
  def makeFreqTable(sheet, *groupByCols):
33
+ if not any(groupByCols):
34
+ vd.fail('FreqTableSheet requires at least 1 column for grouping')
29
35
  return FreqTableSheet(sheet.name,
30
36
  '%s_freq' % '-'.join(col.name for col in groupByCols),
31
37
  groupByCols=groupByCols,
@@ -61,32 +67,31 @@ Each row on this sheet corresponds to a *bin* of rows on the source sheet that h
61
67
  self.source.unselect(row.sourcerows)
62
68
  return super().unselectRow(row)
63
69
 
64
- def updateLargest(self, grouprow):
65
- self.largest = max(self.largest, len(grouprow.sourcerows))
66
-
67
70
  def resetCols(self):
68
71
  super().resetCols()
69
72
 
70
73
  # add default bonus columns
74
+ countCol = AttrColumn('count', 'sourcerows', type=vlen)
71
75
  for c in [
72
- AttrColumn('count', 'sourcerows', type=vlen),
76
+ countCol,
73
77
  Column('percent', type=float, getter=lambda col,row: len(row.sourcerows)*100/col.sheet.source.nRows),
74
78
  ]:
75
79
  self.addColumn(c)
76
80
 
77
81
  if self.options.disp_histogram:
78
- c = HistogramColumn('histogram', type=str, width=self.options.default_width*2)
82
+ c = HistogramColumn('histogram', type=str, width=self.options.default_width*2, sourceCol=countCol)
79
83
  self.addColumn(c)
80
84
 
81
85
  # if non-numeric grouping, reverse sort by count at end of load
82
86
  if not any(vd.isNumeric(c) for c in self.groupByCols):
83
- self._ordering = [('count', True)]
87
+ self._ordering = [(countCol, True)]
84
88
 
85
89
  def loader(self):
86
90
  'Generate frequency table.'
87
91
  # two more threads
92
+ histcols = [col for col in self.visibleCols if isinstance(col, HistogramColumn)]
88
93
  vd.sync(self.addAggregateCols(),
89
- self.groupRows(self.updateLargest))
94
+ self.groupRows(lambda row, cols=Fanout(histcols): cols.updateLargest(row)))
90
95
 
91
96
  def afterLoad(self):
92
97
  super().afterLoad()
@@ -138,7 +143,7 @@ class FreqTablePreviewSheet(Sheet):
138
143
  FreqTableSheet.addCommand('', 'open-preview', 'vd.push(FreqTablePreviewSheet(sheet.name, "preview", source=sheet, columns=source.columns), pane=2); vd.options.disp_splitwin_pct=50', 'open split preview of source rows at cursor')
139
144
 
140
145
  Sheet.addCommand('F', 'freq-col', 'vd.push(makeFreqTable(sheet, cursorCol))', 'open Frequency Table grouped on current column, with aggregations of other columns')
141
- Sheet.addCommand('gF', 'freq-keys', 'vd.push(makeFreqTable(sheet, *keyCols))', 'open Frequency Table grouped by all key columns on source sheet, with aggregations of other columns')
146
+ Sheet.addCommand('gF', 'freq-keys', 'vd.push(makeFreqTable(sheet, *keyCols)) if keyCols else vd.fail("there are no key columns to group by")', 'open Frequency Table grouped by all key columns on source sheet, with aggregations of other columns')
142
147
  Sheet.addCommand('zF', 'freq-summary', 'vd.push(makeFreqTableSheetSummary(sheet, Column("Total", sheet=sheet, getter=lambda col, row: "Total")))', 'open one-line summary for all rows and selected rows')
143
148
 
144
149
  ColumnsSheet.addCommand(ENTER, 'freq-row', 'vd.push(makeFreqTable(source[0], cursorRow))', 'open a Frequency Table sheet grouped on column referenced in current row')
@@ -148,7 +153,7 @@ FreqTableSheet.addCommand('gu', 'unselect-rows', 'unselect(selectedRows)', 'unse
148
153
  FreqTableSheet.addCommand('g'+ENTER, 'dive-selected', 'vd.push(openRows(selectedRows))', 'open copy of source sheet with rows that are grouped in selected rows')
149
154
  FreqTableSheet.addCommand('', 'select-first', 'for r in rows: source.select([r.sourcerows[0]])', 'select first source row in each bin')
150
155
 
151
- FreqTableSheet.init('largest', lambda: 1)
156
+ HistogramColumn.init('largest', lambda: 1)
152
157
 
153
158
  vd.addGlobals(
154
159
  makeFreqTable=makeFreqTable,
visidata/fuzzymatch.py CHANGED
@@ -374,6 +374,7 @@ def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[
374
374
  match = {}
375
375
  formatted_hay = {}
376
376
  for k, v in h.items():
377
+ if k[0] == '_': continue
377
378
  for p in needles:
378
379
  mr = _fuzzymatch(v, p)
379
380
  if mr.score > 0: