visidata 3.1__py3-none-any.whl → 3.2__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 (99) hide show
  1. visidata/__init__.py +2 -2
  2. visidata/_input.py +70 -36
  3. visidata/_open.py +9 -6
  4. visidata/_types.py +2 -2
  5. visidata/aggregators.py +125 -16
  6. visidata/apps/vdsql/_ibis.py +8 -13
  7. visidata/basesheet.py +4 -1
  8. visidata/canvas.py +11 -7
  9. visidata/clipboard.py +11 -2
  10. visidata/cliptext.py +65 -23
  11. visidata/cmdlog.py +5 -1
  12. visidata/column.py +6 -2
  13. visidata/ddwplay.py +2 -2
  14. visidata/deprecated.py +91 -63
  15. visidata/errors.py +41 -5
  16. visidata/{features → experimental}/helloworld.py +1 -1
  17. visidata/expr.py +1 -0
  18. visidata/extensible.py +4 -0
  19. visidata/features/cmdpalette.py +3 -3
  20. visidata/features/describe.py +2 -2
  21. visidata/features/expand_cols.py +8 -5
  22. visidata/features/freeze.py +14 -2
  23. visidata/features/go_col.py +2 -1
  24. visidata/features/graph_zoom_y.py +47 -0
  25. visidata/features/incr.py +7 -3
  26. visidata/features/join.py +23 -12
  27. visidata/features/layout.py +8 -3
  28. visidata/features/melt.py +1 -0
  29. visidata/features/rank.py +103 -0
  30. visidata/features/reload_every.py +9 -6
  31. visidata/features/sysedit.py +14 -4
  32. visidata/features/transpose.py +1 -0
  33. visidata/features/window.py +12 -0
  34. visidata/form.py +4 -4
  35. visidata/freqtbl.py +47 -3
  36. visidata/fuzzymatch.py +8 -5
  37. visidata/graph.py +5 -3
  38. visidata/guides/AggregatorsSheet.md +84 -0
  39. visidata/guides/MacrosSheet.md +1 -1
  40. visidata/guides/RankGuide.md +51 -0
  41. visidata/guides/TypesSheet.md +1 -1
  42. visidata/guides/WindowFunctionGuide.md +49 -0
  43. visidata/help.py +3 -4
  44. visidata/indexsheet.py +1 -1
  45. visidata/loaders/_pandas.py +3 -1
  46. visidata/loaders/archive.py +6 -3
  47. visidata/loaders/csv.py +5 -1
  48. visidata/loaders/eml.py +2 -0
  49. visidata/loaders/f5log.py +2 -2
  50. visidata/loaders/fec.py +6 -9
  51. visidata/loaders/fixed_width.py +2 -0
  52. visidata/loaders/hdf5.py +34 -10
  53. visidata/loaders/npy.py +54 -23
  54. visidata/loaders/orgmode.py +3 -2
  55. visidata/loaders/pandas_freqtbl.py +4 -0
  56. visidata/loaders/psv.py +13 -0
  57. visidata/loaders/sqlite.py +1 -1
  58. visidata/loaders/vds.py +3 -4
  59. visidata/macros.py +4 -3
  60. visidata/main.py +11 -5
  61. visidata/mainloop.py +7 -4
  62. visidata/man/parse_options.py +3 -2
  63. visidata/man/vd.1 +26 -14
  64. visidata/man/vd.txt +25 -14
  65. visidata/menu.py +9 -9
  66. visidata/metasheets.py +3 -3
  67. visidata/mouse.py +1 -0
  68. visidata/pyobj.py +17 -9
  69. visidata/save.py +5 -1
  70. visidata/selection.py +29 -18
  71. visidata/settings.py +2 -2
  72. visidata/sheets.py +52 -24
  73. visidata/shell.py +2 -2
  74. visidata/sidebar.py +4 -2
  75. visidata/sort.py +89 -11
  76. visidata/statusbar.py +10 -9
  77. visidata/tests/test_cliptext.py +151 -0
  78. visidata/tests/test_commands.py +5 -2
  79. visidata/tests/test_menu.py +1 -1
  80. visidata/textsheet.py +34 -8
  81. visidata/themes/ascii8.py +2 -2
  82. visidata/themes/light.py +5 -0
  83. visidata/threads.py +16 -8
  84. visidata/undo.py +1 -1
  85. {visidata-3.1.data → visidata-3.2.data}/data/share/man/man1/vd.1 +26 -14
  86. {visidata-3.1.data → visidata-3.2.data}/data/share/man/man1/visidata.1 +26 -14
  87. {visidata-3.1.dist-info → visidata-3.2.dist-info}/METADATA +62 -15
  88. {visidata-3.1.dist-info → visidata-3.2.dist-info}/RECORD +95 -93
  89. {visidata-3.1.dist-info → visidata-3.2.dist-info}/WHEEL +1 -1
  90. {visidata-3.1.dist-info → visidata-3.2.dist-info}/entry_points.txt +1 -0
  91. visidata/features/errors_guide.py +0 -26
  92. visidata/loaders/api_bitio.py +0 -102
  93. visidata/stored_prop.py +0 -38
  94. visidata-3.1.data/scripts/vd +0 -6
  95. /visidata/{guides/SortGuide.md → vendor/__init__.py} +0 -0
  96. {visidata-3.1.data → visidata-3.2.data}/data/share/applications/visidata.desktop +0 -0
  97. {visidata-3.1.data → visidata-3.2.data}/scripts/vd2to3.vdx +0 -0
  98. {visidata-3.1.dist-info → visidata-3.2.dist-info}/LICENSE.gpl3 +0 -0
  99. {visidata-3.1.dist-info → visidata-3.2.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@ from copy import copy
2
2
  from statistics import mode, median, mean, stdev
3
3
 
4
4
  from visidata import vd, Column, ColumnAttr, vlen, RowColorizer, asyncthread, Progress, wrapply
5
- from visidata import BaseSheet, TableSheet, ColumnsSheet, SheetsSheet
5
+ from visidata import BaseSheet, TableSheet, ColumnsSheet, IndexSheet
6
6
 
7
7
 
8
8
  vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggregators)
@@ -114,7 +114,7 @@ class DescribeSheet(ColumnsSheet):
114
114
 
115
115
  TableSheet.addCommand('I', 'describe-sheet', 'vd.push(DescribeSheet(sheet.name+"_describe", source=[sheet]))', 'open Describe Sheet with descriptive statistics for all visible columns')
116
116
  BaseSheet.addCommand('gI', 'describe-all', 'vd.push(DescribeSheet("describe_all", source=vd.stackedSheets))', 'open Describe Sheet with description statistics for all visible columns from all sheets')
117
- SheetsSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visible columns from selected sheets')
117
+ IndexSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visible columns from selected sheets')
118
118
 
119
119
  DescribeSheet.addCommand('zs', 'select-cell', 'cursorRow.sheet.select(cursorValue)', 'select rows on source sheet which are being described in current cell')
120
120
  DescribeSheet.addCommand('zu', 'unselect-cell', 'cursorRow.sheet.unselect(cursorValue)', 'unselect rows on source sheet which are being described in current cell')
@@ -121,8 +121,10 @@ class ExpandedColumn(Column):
121
121
  def calcValue(self, row):
122
122
  return getitemdef(self.origCol.getValue(row), self.expr)
123
123
 
124
- def setValue(self, row, value):
124
+ def setValue(self, row, value, setModified=True):
125
125
  self.origCol.getValue(row)[self.expr] = value
126
+ if setModified:
127
+ self.origCol.sheet.setModified()
126
128
 
127
129
 
128
130
  @Sheet.api
@@ -141,6 +143,7 @@ def contract_cols(sheet, cols, depth=1): # depth == 0 means contract all the wa
141
143
  col.width = sheet.options.default_width
142
144
 
143
145
  sheet.columns = [col for col in sheet.columns if getattr(col, 'origCol', None) not in origCols]
146
+ sheet.calcColLayout()
144
147
 
145
148
 
146
149
  @Sheet.api
@@ -181,10 +184,10 @@ Sheet.addCommand('g(', 'expand-cols', 'expand_cols_deep(visibleCols, depth=1)',
181
184
  Sheet.addCommand('z(', 'expand-col-depth', 'expand_cols_deep([cursorCol], depth=int(input("expand depth=", value=0)))', 'expand current column of containers to given depth (0=fully)')
182
185
  Sheet.addCommand('gz(', 'expand-cols-depth', 'expand_cols_deep(visibleCols, depth=int(input("expand depth=", value=0)))', 'expand all visible columns of containers to given depth (0=fully)')
183
186
 
184
- Sheet.addCommand(')', 'contract-col', 'contract_cols([cursorCol])', 'remove current column and siblings from sheet columns and unhide parent')
185
- Sheet.addCommand('g)', 'contract-cols', 'contract_cols(visibleCols)', 'remove all child columns and unhide toplevel parents')
186
- Sheet.addCommand('z)', 'contract-col-depth', 'contract_cols([cursorCol], depth=int(input("contract depth=", value=0)))', 'remove current column and siblings from sheet columns and unhide parent')
187
- Sheet.addCommand('gz)', 'contract-cols-depth', 'contract_cols(visibleCols, depth=int(input("contract depth=", value=0)))', 'remove all child columns and unhide toplevel parents')
187
+ Sheet.addCommand(')', 'contract-col', 'contract_cols([cursorCol])', 'remove current column and siblings from sheet columns and unhide parent (1 level)')
188
+ Sheet.addCommand('g)', 'contract-cols', 'contract_cols(visibleCols)', 'remove all child columns and unhide toplevel parents (1 level)')
189
+ Sheet.addCommand('z)', 'contract-col-depth', 'contract_cols([cursorCol], depth=int(input("contract depth=", value=0)))', 'remove current column and siblings from sheet columns and unhide parent, prompting for depth')
190
+ Sheet.addCommand('gz)', 'contract-cols-depth', 'contract_cols(visibleCols, depth=int(input("contract depth=", value=0)))', 'remove all child columns and unhide toplevel parents, prompting for depth')
188
191
 
189
192
  ColumnsSheet.addCommand(')', 'contract-source-cols', 'source[0].addColumn(contract_source_cols(someSelectedRows), index=cursorRowIndex)', 'contract selected columns into column group') #1702
190
193
 
@@ -60,10 +60,22 @@ class StaticSheet(Sheet):
60
60
  row.append(val)
61
61
 
62
62
 
63
+ @Sheet.api
64
+ def setcol_freeze(sheet, unfrozen): #2660 Contributed by @midichef
65
+ frozen = sheet.freeze_col(unfrozen)
66
+ frozen.name = unfrozen.name
67
+ unfrozen.hide()
68
+ vd.addUndoColNames([unfrozen])
69
+ unfrozen.name = frozen.name + '_unfrozen'
70
+ sheet.addColumnAtCursor(frozen)
71
+ vd.status(f'replaced {frozen.name} with frozen copy')
72
+
73
+
74
+ Sheet.addCommand("z'", 'setcol-freeze', 'setcol_freeze(cursorCol)', 'replace current column with a frozen copy, with all cells evaluated')
63
75
  Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(freeze_col(cursorCol))', 'add a frozen copy of current column with all cells evaluated')
64
76
  Sheet.addCommand("g'", 'freeze-sheet', 'vd.push(StaticSheet(sheet)); status("pushed frozen copy of "+name)', 'open a frozen copy of current sheet with all visible columns evaluated')
65
- Sheet.addCommand("z'", 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column')
77
+ Sheet.addCommand(None, 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column')
66
78
  Sheet.addCommand("gz'", 'cache-cols', 'for c in visibleCols: c.resetCache()', 'add/reset cache for all visible columns')
67
79
 
68
- vd.addMenuItem('Column', 'Freeze', 'freeze-col')
80
+ vd.addMenuItem('Column', 'Freeze', 'setcol-freeze')
69
81
  vd.addMenuItem('File', 'Freeze', 'freeze-sheet')
@@ -32,7 +32,7 @@ def nextColName(sheet, show_cells=True):
32
32
 
33
33
  def _fmt_colname(match, row, trigger_key):
34
34
  name = match.formatted.get('name', row.name) if match else row.name
35
- r = ' '*(len(prompt)-3)
35
+ r = ' '*(dispwidth(prompt)-3)
36
36
  r += f'[:keystrokes]{trigger_key}[/] '
37
37
  if show_cells and len(sheet.rows) > 0:
38
38
  # pad the right side with spaces
@@ -46,6 +46,7 @@ def nextColName(sheet, show_cells=True):
46
46
  else:
47
47
  r += name
48
48
  return r
49
+
49
50
  name = vd.activeSheet.inputPalette(prompt,
50
51
  colnames,
51
52
  value_key='name',
@@ -0,0 +1,47 @@
1
+ # contributed by Ram Rachum (@cool-RR) via ChatGPT #2751
2
+
3
+ from visidata import GraphSheet
4
+
5
+ @GraphSheet.api
6
+ def zoom_all_y(sheet):
7
+ """Find the lowest and highest y values in the current visible plot and set the y range to that."""
8
+ ymin, ymax = None, None
9
+
10
+ # Find the min and max y values of all points within the current x range
11
+ xmin, xmax = sheet.visibleBox.xmin, sheet.visibleBox.xmax
12
+
13
+ for vertexes, attr, row in sheet.polylines:
14
+ if attr in sheet.hiddenAttrs:
15
+ continue
16
+
17
+ for x, y in vertexes:
18
+ # Check if the point is within the current x range
19
+ if xmin <= x <= xmax:
20
+ if ymin is None or y < ymin:
21
+ ymin = y
22
+ if ymax is None or y > ymax:
23
+ ymax = y
24
+
25
+ if ymin is None or ymax is None:
26
+ # No visible points found
27
+ return
28
+
29
+ # Add a 5% margin on both top and bottom
30
+ y_range = ymax - ymin
31
+ margin = sheet.ycols[0].type(y_range * 0.05)
32
+
33
+ # Calculate adjusted min/max with margin
34
+ adj_ymin = ymin - margin
35
+ adj_ymax = ymax + margin
36
+
37
+ # Create 5 equally spaced y ticks from real min to real max (not adjusted with margin)
38
+ step = (ymax - ymin) / 4 # 5 ticks means 4 intervals
39
+ y_ticks = tuple(ymin + step * i for i in range(5))
40
+
41
+ # Set the y range with margin
42
+ sheet.set_y(f"{adj_ymin} {adj_ymax}")
43
+
44
+ # Set custom y ticks (using the real min/max, not the adjusted ones)
45
+ sheet.forced_y_ticks = y_ticks
46
+
47
+ GraphSheet.addCommand('g_', 'zoom-all-y', 'zoom_all_y()', 'Zoom y-axis to fit all visible data points')
visidata/features/incr.py CHANGED
@@ -1,4 +1,4 @@
1
- from visidata import VisiData, Sheet, vd, options
1
+ from visidata import VisiData, Sheet, vd
2
2
 
3
3
 
4
4
  vd.option('incr_base', 1.0, 'start value for column increments', replay=True)
@@ -7,10 +7,14 @@ vd.option('incr_base', 1.0, 'start value for column increments', replay=True)
7
7
  @VisiData.api
8
8
  def numrange(vd, n, step=1):
9
9
  'Generate n values, starting from options.incr_base and increasing by step for each number.'
10
- base = type(step)(options.incr_base)
11
- yield from (base+x*step for x in range(n))
10
+ base = type(step)(vd.options.incr_base)
11
+ yield from ((base+x)*step for x in range(n))
12
12
 
13
13
 
14
+ def test_numrange(vd=None):
15
+ assert list(vd.numrange(5)) == [1,2,3,4,5]
16
+ assert list(vd.numrange(5, step=5)) == [5,10,15,20,25] #2769
17
+
14
18
  @VisiData.api
15
19
  def num(vd, *args):
16
20
  'Return parsed string as number, preferring int to float.'
visidata/features/join.py CHANGED
@@ -3,7 +3,7 @@ import itertools
3
3
  import functools
4
4
  from copy import copy
5
5
 
6
- from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet, AttrDict
6
+ from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet, AttrDict, dispwidth
7
7
 
8
8
  vd.help_join = '# Join Help\nHELPTODO'
9
9
 
@@ -166,7 +166,8 @@ class MergeColumn(Column):
166
166
 
167
167
  def putValue(self, row, value):
168
168
  for vs, c in reversed(list(self.cols.items())):
169
- c.setValue(row[vs], value)
169
+ if row[vs] is not None:
170
+ c.setValue(row[vs], value)
170
171
 
171
172
  def isDiff(self, row, value):
172
173
  col = list(self.cols.values())[0]
@@ -362,11 +363,11 @@ class ConcatSheet(Sheet):
362
363
 
363
364
 
364
365
  @VisiData.api
365
- def chooseJointype(vd):
366
+ def inputJointype(vd):
366
367
  prompt = 'choose jointype: '
367
368
  def _fmt_aggr_summary(match, row, trigger_key):
368
369
  formatted_jointype = match.formatted.get('key', row.key) if match else row.key
369
- r = ' '*(len(prompt)-3)
370
+ r = ' '*(dispwidth(prompt)-3)
370
371
  r += f'[:keystrokes]{trigger_key}[/] '
371
372
  r += formatted_jointype
372
373
  if row.desc:
@@ -382,19 +383,29 @@ def chooseJointype(vd):
382
383
  type='jointype')
383
384
 
384
385
 
385
- IndexSheet.addCommand('&', 'join-selected', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype=chooseJointype()))', 'merge selected sheets with visible columns from all, keeping rows according to jointype')
386
+ IndexSheet.addCommand('&', 'join-selected', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype=inputJointype()))', 'join selected sheets with visible columns from all, keeping rows according to jointype')
386
387
  IndexSheet.bindkey('g&', 'join-selected')
387
- Sheet.addCommand('&', 'join-sheets-top2', 'vd.push(openJoin(vd.sheets[1:2], jointype=chooseJointype()))', 'concatenate top two sheets in Sheets Stack')
388
- Sheet.addCommand('g&', 'join-sheets-all', 'vd.push(openJoin(vd.sheets[1:], jointype=chooseJointype()))', 'concatenate all sheets in Sheets Stack')
389
-
390
- ColumnsSheet.addCommand('&', 'join-sheets-cols', 'vd.push(join_sheets_cols(selectedRows, jointype=chooseJointype()))', '')
388
+ Sheet.addCommand('&', 'join-sheets-top2', 'vd.push(openJoin(vd.sheets[1:2], jointype=inputJointype()))', 'join top two sheets on Sheets Stack')
389
+ Sheet.addCommand('g&', 'join-sheets-all', 'vd.push(openJoin(vd.sheets[1:], jointype=inputJointype()))', 'join all sheets on Sheets Stack')
390
+ ColumnsSheet.addCommand('&', 'join-sheets-cols', 'vd.push(join_sheets_cols(selectedRows, jointype=inputJointype()))', 'join sheets for selected columns')
391
391
 
392
392
  vd.addMenuItems('''
393
- Data > Join > selected sheets > join-selected
394
- Data > Join > top two sheets > join-sheets-top2
395
- Data > Join > all sheets > join-sheets-all
393
+ Data > Join > Selected Sheets > choose jointype > join-selected
394
+ Data > Join > Top Two Sheets > choose jointype > join-sheets-top2
395
+ Data > Join > All Sheets > choose jointype > join-sheets-all
396
396
  ''')
397
397
 
398
+ for d in vd.jointypes:
399
+ jointype, joinhelp = d.key, d.desc
400
+ IndexSheet.addCommand('', f'join-selected-{jointype}', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype="{jointype}))', f'join selected sheets, keeping {joinhelp}')
401
+ Sheet.addCommand('', f'join-sheets-top2-{jointype}', f'vd.push(openJoin(vd.sheets[1:2], jointype="{jointype}"))', f'join top two sheets on Sheets Stack, keeping {joinhelp}')
402
+ Sheet.addCommand('', f'join-sheets-all-{jointype}', f'vd.push(openJoin(vd.sheets[1:], jointype="{jointype}"))', f'join all sheets on Sheets Stack, keeping {joinhelp}')
403
+ ColumnsSheet.addCommand('', 'join-cols-{jointype}', 'vd.push(join_sheets_cols(selectedRows, jointype=inputJointype()))', f'join sheets for selected columns, keeping {joinhelp}')
404
+
405
+ vd.addMenuItems(f'''Data > Join > Selected Sheets > {jointype} > join-selected-{jointype}''')
406
+ vd.addMenuItems(f'''Data > Join > Top Two Sheets > {jointype} > join-sheets-top2-{jointype}''')
407
+ vd.addMenuItems(f'''Data > Join > All Sheets > {jointype} > join-selected-{jointype}''')
408
+
398
409
  IndexSheet.guide += '''
399
410
  - `&` to join the selected sheets together
400
411
  '''
@@ -40,20 +40,25 @@ def hide_col(vd, col):
40
40
  def hide_uniform_cols(sheet):
41
41
  if len(sheet.rows) < 2:
42
42
  return
43
- for col in sheet.visibleCols:
43
+ idx = sheet.cursorVisibleColIndex
44
+ for i, col in enumerate(sheet.visibleCols):
44
45
  vals = (col.getTypedValue(r) for r in sheet.rows)
45
46
  first = next(vals)
46
47
  if all(v == first for v in vals):
48
+ vd.status(f'hide {col.name} with value: {col.getDisplayValue(sheet.rows[0])}')
49
+ if i <= idx:
50
+ sheet.cursorRight(-1)
47
51
  col.hide()
52
+ Sheet.clear_all_caches() #2578
48
53
 
49
54
  Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
50
55
  Sheet.addCommand('z_', 'resize-col-input', 'width = int(input("set width= ", value=cursorCol.width)); cursorCol.setWidth(width)', 'adjust width of current column to N')
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')
56
+ Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.toggleWidth(c.getMaxWidth(visibleRows))', 'toggle widths of all visible columns between full and default width')
52
57
  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')
53
58
 
54
59
  Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
55
60
  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')
61
+ Sheet.addCommand('g-', 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
57
62
 
58
63
  Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
59
64
  Sheet.addCommand('v', 'toggle-multiline', 'for c in visibleCols: c.toggleMultiline()', 'toggle multiline display')
visidata/features/melt.py CHANGED
@@ -104,3 +104,4 @@ vd.addMenuItems('''
104
104
  Data > Melt > nonkey columns > melt
105
105
  Data > Melt > nonkey columns by regex > melt-regex
106
106
  ''')
107
+ vd.addGlobals(MeltedSheet=MeltedSheet)
@@ -0,0 +1,103 @@
1
+ import itertools
2
+
3
+ from visidata import Sheet, ListAggregator, SettableColumn
4
+ from visidata import vd, anytype, asyncthread, Progress
5
+
6
+ class RankAggregator(ListAggregator):
7
+ '''
8
+ Ranks start at 1, and each group's rank is 1 higher than the previous group.
9
+ When elements are tied in ranking, each of them gets the same rank.
10
+ '''
11
+ def aggregate(self, col, rows) -> [int]:
12
+ return self.aggregate_list(col, rows)
13
+
14
+ def aggregate_list(self, col, rows) -> [int]:
15
+ if not col.sheet.keyCols:
16
+ vd.error('ranking requires one or more key columns')
17
+ return self.rank(col, rows)
18
+
19
+ def rank(self, col, rows):
20
+ if col.keycol:
21
+ vd.warning('rank aggregator is uninformative for key columns')
22
+ def _key_progress(prog):
23
+ def identity(val):
24
+ prog.addProgress(1)
25
+ return val
26
+ return identity
27
+ with Progress(gerund='ranking', total=4*col.sheet.nRows) as prog:
28
+ p = _key_progress(prog) # increment progress every time p() is called
29
+ # compile row data, for each row a list of tuples: (group_key, rank_key, rownum)
30
+ rowdata = [(col.sheet.rowkey(r), col.getTypedValue(r), p(rownum)) for rownum, r in enumerate(rows)]
31
+ # sort by row key and column value to prepare for grouping
32
+ # If the column is in descending order, use descending order for within-group ranking.
33
+ reverse = next((r for (c, r) in col.sheet.ordering if c == col or c == col.name), False)
34
+ try:
35
+ rowdata.sort(reverse=reverse, key=p)
36
+ if reverse:
37
+ vd.status('ranking {col.name} in descending order')
38
+ except TypeError as e:
39
+ vd.fail(f'elements in a ranking column must be comparable: {e.args[0]}')
40
+ rowvals = []
41
+ #group by row key
42
+ for _, group in itertools.groupby(rowdata, key=lambda v: v[0]):
43
+ # within a group, the rows have already been sorted by col_val
44
+ group = list(group)
45
+ # rank each group individually
46
+ group_ranks = rank_sorted_iterable([p(col_val) for _, col_val, rownum in group])
47
+ rowvals += [(rownum, rank) for (_, _, rownum), rank in zip(group, group_ranks)]
48
+ # sort by unique rownum, to make rank results match the original row order
49
+ rowvals.sort(key=p)
50
+ rowvals = [ rank for rownum, rank in rowvals ]
51
+ return rowvals
52
+
53
+ vd.aggregators['rank'] = RankAggregator('rank', anytype, helpstr='list of ranks, when grouping by key columns', listtype=int)
54
+
55
+ def rank_sorted_iterable(vals_sorted) -> [int]:
56
+ '''*vals_sorted* is an iterable whose elements form one or more groups.
57
+ The iterable must already be sorted.'''
58
+
59
+ ranks = []
60
+ val_groups = itertools.groupby(vals_sorted)
61
+ for rank, (_, val_group) in enumerate(val_groups, 1):
62
+ for _ in val_group:
63
+ ranks.append(rank)
64
+ return ranks
65
+
66
+ @Sheet.api
67
+ @asyncthread
68
+ def addcol_sheetrank(sheet, rows):
69
+ '''
70
+ Each row is ranked within its sheet. Rows are ordered by the
71
+ value of their key columns.
72
+ '''
73
+ if not sheet.keyCols:
74
+ vd.error('ranking requires one or more key columns')
75
+ colname = f'{sheet.name}_sheetrank'
76
+ c = SettableColumn(name=colname, type=int)
77
+ sheet.addColumnAtCursor(c)
78
+ def _key_progress(prog):
79
+ def identity(val):
80
+ prog.addProgress(1)
81
+ return val
82
+ return identity
83
+ with Progress(gerund='ranking', total=5*sheet.nRows) as prog:
84
+ p = _key_progress(prog) # increment progress every time p() is called
85
+ ordering = [(col, reverse) for (col, reverse) in sheet.ordering if col.keycol]
86
+ rowkeys = [(sheet.rowkey(r), p(rownum), r) for rownum, r in enumerate(rows)]
87
+ if ordering:
88
+ vd.status('using custom ordering for keycol sort')
89
+ keycols_ordered = [col for (col, reverse) in ordering]
90
+ keycols_unordered = [keycol for keycol in sheet.keyCols if not keycol in keycols_ordered]
91
+ ordering += [(keycol, False) for keycol in keycols_unordered]
92
+ def _sortkey(e): # sort the rows by using the column
93
+ p(None)
94
+ return sheet.sortkey(e[2], ordering=ordering)
95
+ rowkeys.sort(key=_sortkey)
96
+ else:
97
+ rowkeys.sort(key=p)
98
+ ranks = rank_sorted_iterable([p(rowkey) for rowkey, _, _ in rowkeys])
99
+ row_ranks = sorted(zip((rownum for _, rownum, _ in rowkeys), ranks), key=p)
100
+ row_ranks = [rank for rownum, rank in row_ranks]
101
+ c.setValues(sheet.rows, *[p(row_rank) for row_rank in row_ranks])
102
+
103
+ Sheet.addCommand('', 'addcol-rank-sheet', 'sheet.addcol_sheetrank(rows)', 'add column with the rank of each row based on its key columns')
@@ -1,14 +1,17 @@
1
1
  import os
2
2
  import time
3
3
 
4
- from visidata import vd, BaseSheet, Sheet, asyncthread, Path, ScopedSetattr
4
+ from visidata import vd, BaseSheet, Sheet, asyncignore, asyncthread, Path, ScopedSetattr
5
5
 
6
6
 
7
7
  @BaseSheet.api
8
- @asyncthread
8
+ @asyncignore
9
9
  def reload_every(sheet, seconds:int):
10
10
  while True:
11
- sheet.reload()
11
+ # continue reloading till vd.remove() runs, like when the sheet is quit
12
+ if not sheet in vd.sheets:
13
+ break
14
+ vd.sync(sheet.reload())
12
15
  time.sleep(seconds)
13
16
 
14
17
 
@@ -20,13 +23,13 @@ def reload_modified(sheet):
20
23
  assert isinstance(p, Path)
21
24
  assert not p.is_url()
22
25
 
23
- mtime = os.stat(p).st_mtime
26
+ mtime = 0
24
27
  while True:
25
- time.sleep(1)
26
28
  t = os.stat(p).st_mtime
27
29
  if t != mtime:
28
30
  mtime = t
29
31
  sheet.reload_rows()
32
+ time.sleep(1)
30
33
 
31
34
 
32
35
  @Sheet.api
@@ -46,7 +49,7 @@ def reload_rows(self):
46
49
 
47
50
  BaseSheet.addCommand('', 'reload-every', 'sheet.reload_every(input("reload interval (sec): ", value=1))', 'schedule sheet reload every N seconds') #683
48
51
  BaseSheet.addCommand('', 'reload-modified', 'sheet.reload_modified()', 'reload sheet when source file modified (tail-like behavior)') #1686
49
- BaseSheet.addCommand('z^R', 'reload-rows', 'preloadHook(); reload_rows(); status("reloaded")', 'Reload current sheet')
52
+ Sheet.addCommand('z^R', 'reload-rows', 'preloadHook(); reload_rows(); status("reloaded")', 'Reload current sheet leaving current columns intact')
50
53
 
51
54
  vd.addMenuItems('''
52
55
  File > Reload > rows only > reload-rows
@@ -1,5 +1,5 @@
1
1
  from copy import copy
2
- from visidata import vd, asyncthread, Path, Sheet, IndexSheet
2
+ from visidata import vd, asyncthread, Path, Sheet, IndexSheet, TableSheet
3
3
 
4
4
 
5
5
  @Sheet.api
@@ -37,7 +37,17 @@ def syseditCells_async(sheet, cols, rows, filetype=None):
37
37
  tempcol = tempvs.colsByName.get(col.name)
38
38
  if not tempcol: # column not in edited version
39
39
  continue
40
- col.setValuesTyped(rows, *[tempcol.getTypedValue(r) for r in tempvs.rows])
41
-
42
-
40
+ # only assign values that were changed by the editor
41
+ edited_rows = []
42
+ edited_vals = []
43
+ for r, r_edited in zip(rows, tempvs.rows):
44
+ v = tempcol.getDisplayValue(r_edited)
45
+ if col.getDisplayValue(r) != v:
46
+ edited_rows.append(r)
47
+ edited_vals.append(v)
48
+ if edited_rows:
49
+ col.setValuesTyped(edited_rows, *edited_vals)
50
+
51
+
52
+ TableSheet.addCommand('^O', 'sysedit-cell', 'cd = cursorDisplay; e = vd.launchExternalEditor(cd); cursorCol.setValues([cursorRow], e) if e != cd else None', 'edit current cell in external $EDITOR')
43
53
  Sheet.addCommand('g^O', 'sysedit-selected', 'syseditCells(visibleCols, onlySelectedRows)', 'edit rows in $EDITOR')
@@ -25,3 +25,4 @@ class TransposeSheet(Sheet):
25
25
  Sheet.addCommand('T', 'transpose', 'vd.push(TransposeSheet(name+"_T", source=sheet))', 'open new sheet with rows and columns transposed')
26
26
 
27
27
  vd.addMenuItems('Data > Transpose > transpose')
28
+ vd.addGlobals(TransposeSheet=TransposeSheet)
@@ -36,6 +36,16 @@ class WindowColumn(Column):
36
36
 
37
37
  return self._windowrows
38
38
 
39
+ def __getstate__(self):
40
+ r = super().__getstate__()
41
+ r['before'] = self.before
42
+ r['after'] = self.after
43
+ r['sourcecol'] = self.sourcecol.name
44
+ return r
45
+
46
+ def __setstate__(self, r):
47
+ self.sourcecol = self.sheet.column(r.pop('sourcecol', None))
48
+ return super().__setstate__(r)
39
49
 
40
50
  @Sheet.api
41
51
  def addcol_window(sheet, curcol):
@@ -54,3 +64,5 @@ Sheet.addCommand('w', 'addcol-window', 'addcol_window(cursorCol)', 'add column w
54
64
  Sheet.addCommand('', 'select-around-n', 'select_around(input("select rows around selected: ", value=1))', 'select additional N rows before/after each selected row')
55
65
 
56
66
  vd.addMenuItem('Row', 'Select', 'N rows around each selected row', 'select-around-n')
67
+
68
+ vd.addGlobals(WindowColumn=WindowColumn)
visidata/form.py CHANGED
@@ -40,7 +40,7 @@ class FormCanvas(BaseSheet):
40
40
  continue
41
41
  x, y = r.x, r.y
42
42
  if isinstance(y, float) and (0 < y < 1) or (-1 < y < 0): y = h*y
43
- if isinstance(x, float) and (0 < x < 1) or (-1 < x < 0): x = w*x-(len(r.text)/2)
43
+ if isinstance(x, float) and (0 < x < 1) or (-1 < x < 0): x = w*x-(dispwidth(r.text)/2)
44
44
  x = int(x)
45
45
  y = int(y)
46
46
  if y < 0: y += h
@@ -52,7 +52,7 @@ class FormCanvas(BaseSheet):
52
52
  # underline first occurrence of r.key in r.text
53
53
  if hasattr(r, 'key') and r.key:
54
54
  index = r.text.find(r.key)
55
- clipdraw(scr, y, x+index, r.text[index:len(r.key)+1], colors[color + " underline"])
55
+ clipdraw(scr, y, x+index, r.text[index:index+len(r.key)], colors[color + " underline"])
56
56
  vd.onMouse(scr, x, y, dispwidth(r.text), 1,
57
57
  BUTTON1_PRESSED=lambda y,x,key,r=r,sheet=self: sheet.onPressed(r),
58
58
  BUTTON1_RELEASED=lambda y,x,key,r=r,sheet=self: sheet.onReleased(r))
@@ -61,7 +61,7 @@ class FormCanvas(BaseSheet):
61
61
  vd.setWindows(vd.scrFull)
62
62
  drawnrows = [r for r in self.source.rows if r.text]
63
63
  inputs = [r for r in self.source.rows if r.input]
64
- maxw = max(int(r.x)+len(r.text) for r in drawnrows)
64
+ maxw = max(int(r.x)+dispwidth(r.text) for r in drawnrows)
65
65
  maxh = max(int(r.y) for r in drawnrows)
66
66
  h, w = vd.scrFull.getmaxyx()
67
67
  y, x = max(0, (h-maxh)//2-1), max(0, (w-maxw)//2-1)
@@ -103,7 +103,7 @@ class FormCanvas(BaseSheet):
103
103
  @VisiData.api
104
104
  def confirm(vd, prompt, exc=EscapeException):
105
105
  'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
106
- if vd.options.batch and not vd.options.interactive:
106
+ if vd.options.batch:
107
107
  return vd.fail('cannot confirm in batch mode: ' + prompt)
108
108
 
109
109
  form = FormSheet('confirm', rows=[
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, Fanout
4
+ from visidata import vd, vlen, VisiData, Column, AttrColumn, Sheet, ColumnsSheet, ENTER, Fanout
5
5
  from visidata.pivot import PivotSheet, PivotGroupRow
6
6
 
7
7
 
@@ -60,13 +60,57 @@ Each row on this sheet corresponds to a *bin* of rows on the source sheet that h
60
60
  return '+'.join(c.name for c in self.groupByCols)
61
61
 
62
62
  def selectRow(self, row):
63
- self.source.select(row.sourcerows) # select all entries in the bin on the source sheet
63
+ # Does not create an undo-operation for the select on the source rows. The caller should create undo-information itself.
64
+ self.source.select(row.sourcerows, add_undo=False) # select all entries in the bin on the source sheet
64
65
  return super().selectRow(row) # then select the bin itself on this sheet
65
66
 
66
67
  def unselectRow(self, row):
67
- self.source.unselect(row.sourcerows)
68
+ self.source.unselect(row.sourcerows, add_undo=False)
68
69
  return super().unselectRow(row)
69
70
 
71
+ def addUndoSelection(self):
72
+ self.source.addUndoSelection()
73
+ super().addUndoSelection()
74
+
75
+ # override Sheet operations that handle multiple rows:
76
+ # select(), unselect(), and toggle()
77
+ # to make undo more efficient. Without this optimization, the memory
78
+ # use for the undo-tracking on the source sheet is O(n^2) in the number
79
+ # of bins selected, which can easily exceed all available memory.
80
+ def select(self, rows, status=True, progress=True, add_undo=True):
81
+ if add_undo:
82
+ self.addUndoSelection()
83
+ super().select(rows, status, progress, add_undo=False)
84
+
85
+ def unselect(self, rows, status=True, progress=True, add_undo=True):
86
+ if add_undo:
87
+ self.addUndoSelection()
88
+ super().unselect(rows, status, progress, add_undo=False)
89
+
90
+ def toggle(self, rows, add_undo=True):
91
+ 'Toggle selection of given *rows* and corresponding rows in source sheet.'
92
+ if add_undo:
93
+ self.addUndoSelection()
94
+ super().toggle(rows, add_undo=False)
95
+
96
+ def select_row(self, row, add_undo=True):
97
+ 'Add single *row* to set of selected rows, and corresponding rows in source sheet.'
98
+ if add_undo:
99
+ self.addUndoSelection()
100
+ super().select_row(row, add_undo=False)
101
+
102
+ def unselect_row(self, row, add_undo=True):
103
+ 'Remove single *row* from set of selected rows, and remove corresponding rows in source sheet.'
104
+ if add_undo:
105
+ self.addUndoSelection()
106
+ super().unselect_row(row, add_undo=False)
107
+
108
+ def toggle_row(self, row, add_undo=True):
109
+ 'Toggle selection of given *row* and of corresponding rows in source sheet.'
110
+ if add_undo:
111
+ self.addUndoSelection()
112
+ super().toggle_row(row, add_undo=False)
113
+
70
114
  def resetCols(self):
71
115
  super().resetCols()
72
116
 
visidata/fuzzymatch.py CHANGED
@@ -367,23 +367,26 @@ CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')
367
367
 
368
368
  @VisiData.api
369
369
  def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]"):
370
- 'Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict.'
371
-
370
+ '''Perform case-insensitive matching. Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict.'''
371
+ needles = [ p.lower() for p in needles]
372
372
  matches = []
373
373
  for h in haystack:
374
374
  match = {}
375
375
  formatted_hay = {}
376
376
  for k, v in h.items():
377
377
  if k[0] == '_': continue
378
+ positions = set()
379
+ v = v.lower()
378
380
  for p in needles:
379
381
  mr = _fuzzymatch(v, p)
380
382
  if mr.score > 0:
381
- match[k] = mr
382
- formatted_hay[k] = _format_match(v, mr.positions)
383
+ match.setdefault(k, []).append(mr)
384
+ positions |= set(mr.positions)
385
+ formatted_hay[k] = _format_match(v, positions)
383
386
 
384
387
  if match:
385
388
  # square to prefer larger scores in a single haystack
386
- score = int(sum(mr.score**2 for mr in match.values()))
389
+ score = int(sum([mr.score**2 for mrs in match.values() for mr in mrs]))
387
390
  matches.append(CombinedMatch(score=score, formatted=formatted_hay, match=h))
388
391
 
389
392
  return sorted(matches, key=lambda m: -m.score)