visidata 3.1.1__py3-none-any.whl → 3.3__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 +106 -58
  3. visidata/_open.py +10 -7
  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 -3
  8. visidata/canvas.py +11 -7
  9. visidata/clipboard.py +11 -2
  10. visidata/cliptext.py +68 -23
  11. visidata/cmdlog.py +5 -1
  12. visidata/column.py +48 -33
  13. visidata/ddwplay.py +2 -2
  14. visidata/deprecated.py +96 -63
  15. visidata/errors.py +41 -5
  16. visidata/{features → experimental}/helloworld.py +1 -1
  17. visidata/experimental/liveupdate.py +1 -1
  18. visidata/expr.py +1 -0
  19. visidata/extensible.py +4 -0
  20. visidata/features/cmdpalette.py +64 -25
  21. visidata/features/describe.py +2 -2
  22. visidata/features/expand_cols.py +7 -5
  23. visidata/features/freeze.py +14 -2
  24. visidata/features/go_col.py +3 -3
  25. visidata/features/graph_zoom_y.py +47 -0
  26. visidata/features/incr.py +7 -3
  27. visidata/features/join.py +23 -12
  28. visidata/features/layout.py +8 -4
  29. visidata/features/melt.py +1 -0
  30. visidata/features/rank.py +103 -0
  31. visidata/features/reload_every.py +11 -8
  32. visidata/features/sysedit.py +14 -4
  33. visidata/features/transpose.py +1 -0
  34. visidata/features/window.py +12 -0
  35. visidata/form.py +10 -9
  36. visidata/freqtbl.py +47 -3
  37. visidata/fuzzymatch.py +11 -7
  38. visidata/graph.py +5 -3
  39. visidata/guides/AggregatorsSheet.md +84 -0
  40. visidata/guides/CommandsSheet.md +1 -0
  41. visidata/guides/MacrosSheet.md +1 -1
  42. visidata/guides/RankGuide.md +51 -0
  43. visidata/guides/TypesSheet.md +1 -1
  44. visidata/guides/WindowFunctionGuide.md +49 -0
  45. visidata/help.py +23 -6
  46. visidata/indexsheet.py +1 -1
  47. visidata/loaders/_pandas.py +3 -1
  48. visidata/loaders/archive.py +33 -6
  49. visidata/loaders/csv.py +12 -1
  50. visidata/loaders/eml.py +2 -0
  51. visidata/loaders/f5log.py +2 -2
  52. visidata/loaders/fec.py +6 -9
  53. visidata/loaders/fixed_width.py +2 -0
  54. visidata/loaders/hdf5.py +34 -10
  55. visidata/loaders/npy.py +54 -23
  56. visidata/loaders/orgmode.py +3 -2
  57. visidata/loaders/pandas_freqtbl.py +4 -0
  58. visidata/loaders/psv.py +13 -0
  59. visidata/loaders/sqlite.py +1 -1
  60. visidata/loaders/vds.py +3 -4
  61. visidata/macros.py +5 -4
  62. visidata/main.py +21 -11
  63. visidata/mainloop.py +8 -5
  64. visidata/man/parse_options.py +3 -2
  65. visidata/man/vd.1 +38 -17
  66. visidata/man/vd.txt +47 -17
  67. visidata/menu.py +10 -10
  68. visidata/metasheets.py +3 -3
  69. visidata/mouse.py +3 -0
  70. visidata/movement.py +6 -3
  71. visidata/pyobj.py +17 -9
  72. visidata/save.py +10 -2
  73. visidata/selection.py +29 -18
  74. visidata/settings.py +9 -5
  75. visidata/sheets.py +124 -48
  76. visidata/shell.py +2 -2
  77. visidata/sidebar.py +11 -8
  78. visidata/sort.py +89 -11
  79. visidata/statusbar.py +10 -9
  80. visidata/tests/test_cliptext.py +164 -0
  81. visidata/tests/test_commands.py +6 -2
  82. visidata/tests/test_menu.py +1 -1
  83. visidata/textsheet.py +34 -8
  84. visidata/themes/ascii8.py +2 -2
  85. visidata/themes/light.py +5 -0
  86. visidata/threads.py +38 -8
  87. visidata/utils.py +15 -1
  88. visidata/vendor/__init__.py +0 -0
  89. {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/vd.1 +38 -17
  90. {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/visidata.1 +38 -17
  91. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/METADATA +62 -15
  92. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/RECORD +98 -92
  93. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/WHEEL +1 -1
  94. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/entry_points.txt +1 -0
  95. visidata-3.1.1.data/scripts/vd +0 -6
  96. {visidata-3.1.1.data → visidata-3.3.data}/data/share/applications/visidata.desktop +0 -0
  97. {visidata-3.1.1.data → visidata-3.3.data}/scripts/vd2to3.vdx +0 -0
  98. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/LICENSE.gpl3 +0 -0
  99. {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/top_level.txt +0 -0
@@ -25,14 +25,13 @@ def nextColName(sheet, show_cells=True):
25
25
  if show_cells and len(sheet.rows) > 0:
26
26
  dv = c.getDisplayValue(sheet.cursorRow)
27
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,
28
+ item = AttrDict(name=c.name,
30
29
  _cursor_cell=dv)
31
30
  colnames.append(item)
32
31
 
33
32
  def _fmt_colname(match, row, trigger_key):
34
33
  name = match.formatted.get('name', row.name) if match else row.name
35
- r = ' '*(len(prompt)-3)
34
+ r = ' '*(dispwidth(prompt)-3)
36
35
  r += f'[:keystrokes]{trigger_key}[/] '
37
36
  if show_cells and len(sheet.rows) > 0:
38
37
  # pad the right side with spaces
@@ -46,6 +45,7 @@ def nextColName(sheet, show_cells=True):
46
45
  else:
47
46
  r += name
48
47
  return r
48
+
49
49
  name = vd.activeSheet.inputPalette(prompt,
50
50
  colnames,
51
51
  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,24 @@ 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()
48
52
 
49
53
  Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
50
54
  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')
55
+ 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
56
  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
57
 
54
58
  Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
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')
59
+ Sheet.addCommand('z-', 'resize-col-half', 'width = cursorCol.width if cursorCol.width is not None else options.default_width; cursorCol.setWidth(width//2)', 'reduce width of current column by half')
60
+ Sheet.addCommand('g-', 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
57
61
 
58
62
  Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
59
63
  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, asyncsingle_queue, 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,19 +23,19 @@ 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
33
- @asyncthread
36
+ @asyncsingle_queue
34
37
  def reload_rows(self):
35
- 'Reload rows from ``self.source``, keeping current columns intact. Async.'
38
+ '''Reload rows from ``self.source``, keeping current columns intact. Async. If previous calls are running, waits for them to finish.'''
36
39
  with (ScopedSetattr(self, 'loading', True),
37
40
  ScopedSetattr(self, 'checkCursor', lambda: True),
38
41
  ScopedSetattr(self, 'cursorRowIndex', self.cursorRowIndex)):
@@ -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)
@@ -102,8 +102,8 @@ class FormCanvas(BaseSheet):
102
102
  @functools.wraps(VisiData.confirm)
103
103
  @VisiData.api
104
104
  def confirm(vd, prompt, exc=EscapeException):
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:
105
+ 'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Return True when proceeding, otherwise raise *exc*, or if *exc* is falsy, return False'
106
+ if vd.options.batch:
107
107
  return vd.fail('cannot confirm in batch mode: ' + prompt)
108
108
 
109
109
  form = FormSheet('confirm', rows=[
@@ -115,10 +115,11 @@ def confirm(vd, prompt, exc=EscapeException):
115
115
  ])
116
116
 
117
117
  ret = FormCanvas(source=form).run(vd.scrFull)
118
- if not ret:
119
- raise exc('')
120
- yn = ret['yn'][:1]
121
- if not yn or yn not in 'Yy':
118
+ confirmed = False # default is to disconfirm, if user exited the confirmation via: Esc ^C ^Q q
119
+ if ret:
120
+ yn = ret['yn'][:1]
121
+ confirmed = yn and yn in 'Yy'
122
+ if not confirmed:
122
123
  msg = 'disconfirmed: ' + prompt
123
124
  if exc:
124
125
  raise exc(msg)
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, status=False, 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, status=False, 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
@@ -366,24 +366,28 @@ CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')
366
366
 
367
367
 
368
368
  @VisiData.api
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
-
369
+ def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]", case_sensitive=False):
370
+ '''Perform matching that is case-insensitive by default. 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. Set *case_sensitive* to match case.'''
371
+ if not case_sensitive:
372
+ needles = [ p.lower() for p in needles]
372
373
  matches = []
373
374
  for h in haystack:
374
375
  match = {}
375
376
  formatted_hay = {}
376
377
  for k, v in h.items():
377
378
  if k[0] == '_': continue
379
+ positions = set()
380
+ v_match = v if case_sensitive else v.lower()
378
381
  for p in needles:
379
- mr = _fuzzymatch(v, p)
382
+ mr = _fuzzymatch(v_match, p)
380
383
  if mr.score > 0:
381
- match[k] = mr
382
- formatted_hay[k] = _format_match(v, mr.positions)
384
+ match.setdefault(k, []).append(mr)
385
+ positions |= set(mr.positions)
386
+ formatted_hay[k] = _format_match(v, positions)
383
387
 
384
388
  if match:
385
389
  # square to prefer larger scores in a single haystack
386
- score = int(sum(mr.score**2 for mr in match.values()))
390
+ score = int(sum([mr.score**2 for mrs in match.values() for mr in mrs]))
387
391
  matches.append(CombinedMatch(score=score, formatted=formatted_hay, match=h))
388
392
 
389
393
  return sorted(matches, key=lambda m: -m.score)
visidata/graph.py CHANGED
@@ -72,6 +72,8 @@ class InvertedCanvas(Canvas):
72
72
 
73
73
  # provides axis labels, legend
74
74
  class GraphSheet(InvertedCanvas):
75
+ rowtype = 'points'
76
+
75
77
  def __init__(self, *names, **kwargs):
76
78
  self.ylabel_maxw = 0
77
79
  super().__init__(*names, **kwargs)
@@ -145,7 +147,7 @@ class GraphSheet(InvertedCanvas):
145
147
  for char_x in range(0, self.plotwidth//2):
146
148
  has_x_line = char_x in self.reflines_char_x.keys()
147
149
  if has_x_line or has_y_line:
148
- cattr = colors.color_refline
150
+ cattr = colors.color_graph_refline
149
151
  if has_x_line:
150
152
  ch = self.reflines_char_x[char_x]
151
153
  # where two lines cross, draw the vertical line, not the horizontal one
@@ -268,11 +270,11 @@ class GraphSheet(InvertedCanvas):
268
270
  txt = tick + txt
269
271
  else:
270
272
  right_margin = self.plotwidth - 1 - self.plotviewBox.xmax
271
- if (len(txt)+len(tick))*2 <= right_margin:
273
+ if (dispwidth(txt)+dispwidth(tick))*2 <= right_margin:
272
274
  txt = tick + txt
273
275
  else:
274
276
  # shift rightmost label to be left of its tick
275
- x -= len(txt)*2
277
+ x -= dispwidth(txt)*2
276
278
  if len(tick) == 0:
277
279
  x += 1
278
280
  txt = txt + tick