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
@@ -2,6 +2,7 @@ import pytest
2
2
  from unittest.mock import Mock, call
3
3
 
4
4
  import visidata
5
+ from visidata import iterchars
5
6
 
6
7
 
7
8
  class TestClipText:
@@ -19,12 +20,175 @@ class TestClipText:
19
20
  (' jsonl', 5, ' jso…', 5),
20
21
  ('abcdで', 6, 'abcdで', 6),
21
22
  ('abcdで', 5, 'abcd…', 5),
23
+ ('a', 1, 'a', 1),
24
+ ('ab', 1, '…', 1),
25
+ ('abc', 2, 'a…', 2),
26
+ ('で', 1, '…', 1),
27
+ ('でで', 1, '…', 1),
28
+ ('でで', 2, '…', 1),
29
+ ('でで', 3, 'で…', 3),
30
+ ('ででで', 4, 'で…', 3),
31
+ ('ででで', 5, 'でで…', 5),
32
+ ('', 1, '', 0),
33
+ ('', None, '', 0),
34
+ ('abcdef', None, 'abcdef', 6),
35
+ ('ででで', None, 'ででで', 6),
36
+ ('で'*100, None, 'で'*100, 2*100),
37
+ (iterchars([1,2,3]), 4, '[3]…', 4),
38
+ (iterchars([1,2,3]), 7, '[3] 1;…', 7),
39
+ (iterchars([1,2,3]), 9, '[3] 1; 2…', 9),
40
+ (iterchars([1,2,3]), 11, '[3] 1; 2; 3', 11),
41
+ (iterchars([1,2,3]), 12, '[3] 1; 2; 3', 11),
42
+ (iterchars({'a':1, 'b':2, 'c':3}), 7, '{3} a=…', 7),
43
+ (iterchars({'a':1, 'b':2, 'c':3}), 15, '{3} a=1 b=2 c=3', 15),
44
+ (iterchars({'a':1, 'b':2, 'で':3}), 13, '{3} a=1 b=2 …', 13),
45
+ (iterchars({'a':1, 'b':2, 'で':3}), 16, '{3} a=1 b=2 で=3', 16),
22
46
  ])
23
47
  def test_clipstr(self, s, w, clippeds, clippedw):
24
48
  clips, clipw = visidata.clipstr(s, w)
25
49
  assert clips == clippeds
26
50
  assert clipw == clippedw
27
51
 
52
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
53
+ ('b to', 4, 'b to', 4),
54
+ ('abcde', 8, 'abcde', 5),
55
+ (' jsonl', 5, ' jsあ', 5),
56
+ ('abcdで', 6, 'abcdで', 6),
57
+ ('abcdで', 5, 'abcあ', 5),
58
+ ('a', 1, 'a', 1),
59
+ ('ab', 1, 'a', 1),
60
+ ('abc', 2, 'あ', 2),
61
+ ('で', 1, '', 0),
62
+ ('でで', 1, '', 0),
63
+ ('でで', 2, 'あ', 2),
64
+ ('でで', 3, 'あ', 2),
65
+ ('ででで', 4, 'であ', 4),
66
+ ('ででで', 5, 'であ', 4),
67
+ ('', 1, '', 0),
68
+ ('', None, '', 0),
69
+ ('abcdef', None, 'abcdef', 6),
70
+ ('ででで', None, 'ででで', 6),
71
+ ('で'*100, None, 'で'*100, 2*100),
72
+ (iterchars([1,2,3]), 1, '[', 1),
73
+ (iterchars({'a':1, 'b':2, 'c':3}), 1, '{', 1),
74
+ ])
75
+ def test_clipstr_wide_truncator(self, s, w, clippeds, clippedw):
76
+ clips, clipw = visidata.clipstr(s, w, truncator='あ')
77
+ assert clips == clippeds
78
+ assert clipw == clippedw
79
+
80
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
81
+ ('b to', 4, 'b to', 4),
82
+ ('abcde', 8, 'abcde', 5),
83
+ (' jsonl', 5, ' json', 5),
84
+ ('abcdで', 6, 'abcdで', 6),
85
+ ('abcdで', 5, 'abcd', 4),
86
+ ('a', 1, 'a', 1),
87
+ ('ab', 1, 'a', 1),
88
+ ('abc', 2, 'ab', 2),
89
+ ('で', 1, '', 0),
90
+ ('でで', 1, '', 0),
91
+ ('でで', 2, 'で', 2),
92
+ ('でで', 3, 'で', 2),
93
+ ('ででで', 4, 'でで', 4),
94
+ ('ででで', 5, 'でで', 4),
95
+ ('', 1, '', 0),
96
+ ('', None, '', 0),
97
+ ('abcdef', None, 'abcdef', 6),
98
+ ('ででで', None, 'ででで', 6),
99
+ ('で'*100, None, 'で'*100, 2*100),
100
+ ])
101
+ def test_clipstr_empty_truncator(self, s, w, clippeds, clippedw):
102
+ clips, clipw = visidata.clipstr(s, w, truncator='')
103
+ assert clips == clippeds
104
+ assert clipw == clippedw
105
+
106
+ @pytest.mark.parametrize('s, w, truncator, clippeds, clippedw', [
107
+ ('first\nsecond\n\nthird\n\n\n', 22, '', 'first·second··third···', 22),
108
+ ('first\nsecond\n\nthird\n\n\n', 22, '…', 'first·second··third···', 22),
109
+ ('first\nsecond\n\nthird\n\n\n', 21, '', 'first·second··third··', 21),
110
+ ('first\nsecond\n\nthird\n\n\n', 21, '…', 'first·second··third·…', 21),
111
+ (''.join([chr(i) for i in range(256)]), 256, '',
112
+ '································ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~··································¡¢£¤¥¦§¨©ª«¬\xad®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ', 256),
113
+ ])
114
+ def test_clipstr_unprintable(self, s, w, truncator, clippeds, clippedw):
115
+ clips, clipw = visidata.clipstr(s, w, truncator=truncator, oddspace='·')
116
+ assert clips == clippeds
117
+ assert clipw == clippedw
118
+
119
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
120
+ ('b to', 4, 'b to', 4),
121
+ ('abcde', 8, 'abcde', 5),
122
+ (' jsonl', 5, 'jsonl', 5),
123
+ ('abcdで', 6, 'abcdで', 6),
124
+ ('abcdで', 5, 'bcdで', 5),
125
+ ('でbcdで', 6, 'bcdで', 5),
126
+ ('でbcdefghiで', 10, 'bcdefghiで', 10),
127
+ ('でbcdefghiで', 3, 'iで', 3),
128
+ ('でbcdで', 2, 'で', 2),
129
+ ('でbcdで', 1, '', 0),
130
+ ('でbcdで', 0, '', 0),
131
+ ('でbcdで', -1, '', 0),
132
+ ])
133
+ def test_clipstr_start(self, s, w, clippeds, clippedw):
134
+ clips, clipw = visidata.clipstr_start(s, w)
135
+ assert clips == clippeds
136
+ assert clipw == clippedw
137
+
138
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
139
+ ('aAbcで', 6, 'aAbcで', 6),
140
+ ('aAbcで', 7, 'aAbcで', 6),
141
+ ('aAbcで', 1000, 'aAbcで', 6),
142
+ ('aAbcで', 5, '…bcで', 5),
143
+ ('でbcで', 5, '…bcで', 5),
144
+ ('でででででbcで', 5, '…bcで', 5),
145
+ ('でbcで', 3, '…で', 3),
146
+ ('でbcで', 2, '…', 1),
147
+ ('でbcで', 1, '…', 1),
148
+ ('でbcで', 0, '', 0),
149
+ ('でbcで', -1, '', 0),
150
+ ])
151
+ def test_clipstr_start_truncator(self, s, w, clippeds, clippedw):
152
+ clips, clipw = visidata.clipstr_start(s, w, truncator='…')
153
+ assert clips == clippeds
154
+ assert clipw == clippedw
155
+
156
+ @pytest.mark.parametrize('s, w, clippeds, clippedw', [
157
+ ('1234567890', 6, '12…890', 6),
158
+ ('1234567890', 7, '123…890', 7),
159
+ ('1234567890', 8, '123…7890', 8),
160
+ ('1234567890', 9, '1234…7890', 9),
161
+ ('1234567890', 10, '1234567890', 10),
162
+ ('1234567890', 11, '1234567890', 10),
163
+ ('1234567890', 99, '1234567890', 10),
164
+ # all full-width characters
165
+ ('ででででで', 0, '', 0),
166
+ ('ででででで', 1, '…', 1),
167
+ ('ででででで', 2, '…', 1),
168
+ ('ででででで', 3, '…で', 3),
169
+ ('ででででで', 4, '…で', 3),
170
+ ('ででででで', 5, 'で…で', 5),
171
+ ('ででででで', 6, 'で…で', 5),
172
+ ('ででででで', 7, 'で…でで', 7),
173
+ ('ででででで', 8, 'で…でで', 7),
174
+ ('ででででで', 9, 'でで…でで', 9),
175
+ ('ででででで', 10, 'ででででで', 10),
176
+ ('ででででで', 11, 'ででででで', 10),
177
+ ('ででででで', 99, 'ででででで', 10),
178
+ # odd string length, with mix of full-width characters
179
+ ('ででaaでa', 0, '', 0),
180
+ ('ででaaでa', 1, '…', 1),
181
+ ('ででaaでa', 2, '…a', 2),
182
+ ('ででaaででa', 3, '…a', 2),
183
+ ('ででaaででa', 4, '…でa', 4),
184
+ ('ででaaででa', 5, 'で…a', 4),
185
+ ('ででaaででa', 6, 'で…でa', 6),
186
+ ])
187
+ def test_clipstr_middle(self, s, w, clippeds, clippedw):
188
+ clips, clipw = visidata.clipstr_middle(s, w)
189
+ assert clips == clippeds
190
+ assert clipw == clippedw
191
+
28
192
  def test_clipdraw_chunks(self):
29
193
  prechunks = [
30
194
  ('', 'x'),
@@ -30,6 +30,7 @@ nonTested = (
30
30
  'breakpoint',
31
31
  'redraw',
32
32
  'menu',
33
+ 'sysedit',
33
34
  'sysopen',
34
35
  'open-memusage',
35
36
  )
@@ -67,6 +68,7 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
67
68
  'addcol-incr-step': '2',
68
69
  'setcol-incr-step': '2',
69
70
  'setcol-iter': 'range(1, 100)',
71
+ 'addcol-iter': 'range(1, 100)',
70
72
  'setcol-format-enum': '1=cat',
71
73
  'open-ping': 'github.com',
72
74
  'setcol-input': '5',
@@ -116,6 +118,7 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
116
118
  'sheet': '',
117
119
  'col': 'Units',
118
120
  'row': '5',
121
+ 'addcol-aggregate': 'max',
119
122
  }
120
123
 
121
124
  @pytest.mark.usefixtures('curses_setup')
@@ -156,7 +159,8 @@ class TestCommands:
156
159
  # cleanup
157
160
  for f in ['flotsam.csv', 'debris.csv', 'jetsam.csv', 'lagan.csv', 'test_commands.vdj']:
158
161
  pf = Path(f)
159
- if pf.exists: pf.unlink()
162
+ if pf.exists:
163
+ pf.unlink(missing_ok=True)
160
164
 
161
165
 
162
166
  def runOneTest(self, mock_screen, longname):
@@ -172,7 +176,7 @@ class TestCommands:
172
176
  vd.getkeystroke = Mock(side_effect=['^J'])
173
177
 
174
178
  sample_file = vd.pkg_resources_files(visidata) / 'tests/sample.tsv'
175
- vs = visidata.TsvSheet('test_commands', source=visidata.Path(sample_file))
179
+ vs = visidata.TsvSheet('sample', source=visidata.Path(sample_file))
176
180
  cmd = vs.getCommand(longname)
177
181
  if not cmd:
178
182
  vd.warning(f'command cannot be tested on TsvSheet, skipping: {longname}')
@@ -5,7 +5,7 @@ import pytest
5
5
 
6
6
  class TestMenu:
7
7
  def test_menuitems(self):
8
- vd.addMenuItems('''Column > Add column > foobar > hello-world''')
8
+ vd.addMenuItems('''Column > Add column > foobar > addcol-sparkline''')
9
9
 
10
10
  m = TableSheet().getMenuItem(['Column', 'Add column', 'foobar'])
11
11
  assert m
visidata/textsheet.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import textwrap
2
+ import re
2
3
 
3
4
  from visidata import vd, BaseSheet, options, Sheet, ColumnItem, asyncthread
4
5
  from visidata import Column, vlen
@@ -59,9 +60,33 @@ class ErrorSheet(TextSheet):
59
60
  ColumnItem('linenum', 0, type=int, width=0),
60
61
  ColumnItem('error', 1),
61
62
  ]
62
- guide = '''# Error Sheet'''
63
+ guide = '''
64
+ # Error Sheet
65
+ This is the traceback for an error.
66
+ - move cursor then {help.commands.sysopen_error}
67
+ - `q` to quit this error sheet.
68
+ '''
63
69
  precious = False
64
70
 
71
+ def sysopen_error(self, col, row):
72
+ '''Open an external editor for the file relevant to the cursor line
73
+ in the Error Sheet. If the cursor is on the first line, use the file
74
+ mentioned at the end of the stack trace'''
75
+ if self.rows and self.cursorRowIndex == 0:
76
+ searchidx = len(self.rows) - 1
77
+ else:
78
+ searchidx = self.cursorRowIndex
79
+ pat = re.compile(r'^ +File "(.*)", line (\d+), in ')
80
+ for _, text in self.rows[searchidx::-1]: # rowdef: [linenum, text]
81
+ match = pat.search(text)
82
+ if match:
83
+ vd.launchEditor(match.group(1), f'+{match.group(2)}')
84
+ return
85
+
86
+ def reload(self):
87
+ src = self.source or (vd.lastErrors[-1] if vd.lastErrors else [])
88
+ self.rows = list(enumerate(src))
89
+
65
90
  class ErrorCellSheet(ErrorSheet):
66
91
  columns = [
67
92
  ColumnItem('linenum', 0, type=int, width=0),
@@ -69,6 +94,7 @@ class ErrorCellSheet(ErrorSheet):
69
94
  ]
70
95
  guide = '''# Error Cell Sheet
71
96
  This sheet shows the error that occurred when calculating a cell.
97
+ - move cursor then {help.commands.sysopen_error}
72
98
  - `q` to quit this error sheet.
73
99
  '''
74
100
 
@@ -79,29 +105,29 @@ class ErrorsSheet(Sheet):
79
105
  ColumnItem('lastline', -1)
80
106
  ]
81
107
  def reload(self):
82
- self.rows = self.source
108
+ self.rows = self.source or vd.lastErrors
83
109
 
84
110
  def openRow(self, row):
85
111
  return ErrorSheet(source=self.cursorRow)
86
112
 
87
- @VisiData.property
113
+ @VisiData.lazy_property
88
114
  def allErrorsSheet(self):
89
- return ErrorsSheet("errors_all", source=vd.lastErrors)
115
+ return ErrorsSheet("errors_all")
90
116
 
91
- @VisiData.property
117
+ @VisiData.lazy_property
92
118
  def recentErrorsSheet(self):
93
- error = vd.lastErrors[-1] if vd.lastErrors else ''
94
- return ErrorSheet("errors_recent", source=error)
119
+ return ErrorSheet("errors_recent")
95
120
 
96
121
 
97
122
 
98
- BaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status("no error")', 'view traceback for most recent error')
123
+ BaseSheet.addCommand('^E', 'error-recent', 'recentErrorsSheet.reload(); vd.push(recentErrorsSheet) if vd.lastErrors else status("no error")', 'view traceback for most recent error')
99
124
  BaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')
100
125
 
101
126
  Sheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorCellSheet(sheet.name+"_cell_error", sourceSheet=sheet, source=getattr(cursorCell, "error", None) or fail("no error this cell")))', 'view traceback for error in current cell')
102
127
 
103
128
  TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')
104
129
 
130
+ ErrorSheet.addCommand('Enter', 'sysopen-error', 'sysopen_error(cursorCol, cursorRow)', 'open traceback line in $EDITOR')
105
131
 
106
132
  TextSheet.options.save_filetype = 'txt'
107
133
 
visidata/themes/ascii8.py CHANGED
@@ -82,6 +82,6 @@ vd.themes['ascii8'] = dict(
82
82
  disp_menu_fmt='7-bit ASCII 3-bit color',
83
83
  plot_colors = 'white',
84
84
  disp_histogram='*',
85
- disp_graph_lines_x_charset='||||',
86
- disp_graph_lines_y_charset='----'
85
+ disp_graph_reflines_x_charset='||||',
86
+ disp_graph_reflines_y_charset='----'
87
87
  )
visidata/themes/light.py CHANGED
@@ -4,9 +4,14 @@ from visidata import vd
4
4
 
5
5
  vd.themes['light'] = dict(
6
6
  color_default = 'black on white', # the default fg and bg colors
7
+ color_default_hdr = 'bold black on white',
8
+ color_bottom_hdr = 'underline black on white',
9
+ color_current_col = 'bold on white',
10
+ color_column_sep = 'black on white',
7
11
  color_key_col = '20 blue', # color of key columns
8
12
  color_edit_cell = '234 black', # cell color to use when editing cell
9
13
  color_selected_row = '164 magenta', # color of selected rows
14
+ color_selected_col = 'bold on white',
10
15
  color_note_row = '164 magenta', # color of row note on left edge
11
16
  color_note_type = '88 red', # color of cell note for non-str types in anytype columns
12
17
  color_warning = '202 11 yellow',
visidata/threads.py CHANGED
@@ -270,6 +270,27 @@ def asyncsingle(func):
270
270
  _execAsync.searchThread = None
271
271
  return _execAsync
272
272
 
273
+ def asyncsingle_queue(func):
274
+ '''Function decorator like `@asyncthread` but as a singleton. When called, `func(...)` spawns a new thread, and waits for the end of any previous thread still running *func*.
275
+ ``vd.sync()`` does wait for unfinished asyncsingle_queue threads, which is an important difference from asyncsingle.
276
+ '''
277
+ @functools.wraps(func)
278
+ def _execAsync(*args, **kwargs):
279
+ def _func(*args, **kwargs):
280
+ func(*args, **kwargs)
281
+ _execAsync.searchThread = None
282
+ # end of thread
283
+
284
+ # cancel previous thread if running
285
+ if _execAsync.searchThread:
286
+ vd.sync(_execAsync.searchThread)
287
+
288
+ _func.__name__ = func.__name__ # otherwise, the the thread's name is '_func'
289
+
290
+ _execAsync.searchThread = vd.execAsync(_func, *args, **kwargs)
291
+ _execAsync.searchThread = None
292
+ return _execAsync
293
+
273
294
  @VisiData.property
274
295
  def unfinishedThreads(self):
275
296
  'A list of unfinished threads (those without a recorded `endTime`).'
@@ -319,13 +340,14 @@ def open_pyprof(vd, p):
319
340
  @VisiData.api
320
341
  def toggleProfiling(vd):
321
342
  t = threading.current_thread()
322
- if not t.profile:
323
- t.profile = cProfile.Profile()
343
+ if not vd.options.profile:
344
+ if not t.profile:
345
+ t.profile = cProfile.Profile()
324
346
  t.profile.enable()
325
- if not vd.options.profile:
326
- vd.options.set('profile', True)
347
+ vd.options.set('profile', True)
327
348
  else:
328
- t.profile.disable()
349
+ if t.profile:
350
+ t.profile.disable()
329
351
  vd.options.set('profile', False)
330
352
  vd.status('profiling ' + ('ON' if vd.options.profile else 'OFF'))
331
353
 
@@ -337,7 +359,10 @@ class ThreadProfiler:
337
359
 
338
360
  def __enter__(self):
339
361
  if vd.options.profile:
340
- self.thread.profile.enable()
362
+ try:
363
+ self.thread.profile.enable()
364
+ except ValueError: #"ValueError: Another profiling tool is already active"
365
+ pass
341
366
  return self
342
367
 
343
368
  def __exit__(self, exc_type, exc_val, tb):
@@ -381,6 +406,7 @@ class ProfileSheet(Sheet):
381
406
  ]
382
407
 
383
408
  nKeys=3
409
+ _ordering = [('inlinetime_us', True)] # initially sort by inlinetime descending
384
410
 
385
411
  def reload(self):
386
412
  if isinstance(self.source, cProfile.Profile):
@@ -388,7 +414,6 @@ class ProfileSheet(Sheet):
388
414
  else:
389
415
  self.rows = self.source
390
416
 
391
- self.orderBy(None, self.column('inlinetime_us'), reverse=True)
392
417
  self.callers = collections.defaultdict(list) # [row.code] -> list(code)
393
418
 
394
419
  for r in self.rows:
@@ -435,6 +460,10 @@ def codestr(code):
435
460
  return code.co_name
436
461
 
437
462
 
463
+ @VisiData.lazy_property
464
+ def allThreadsSheet(self):
465
+ return ThreadsSheet("threads", source=vd.threads)
466
+
438
467
  ThreadsSheet.addCommand('^C', 'cancel-thread', 'cancelThread(cursorRow)', 'abort thread at current row')
439
468
  ThreadsSheet.addCommand('g^C', 'cancel-all', 'cancelThread(*sheet.rows)', 'abort all threads on this threads sheet')
440
469
  ThreadsSheet.addCommand(None, 'add-row', 'fail("cannot add new rows on Threads Sheet")', 'invalid command')
@@ -449,7 +478,7 @@ BaseSheet.addCommand('^C', 'cancel-sheet', 'cancelThread(*sheet.currentThreads o
449
478
  BaseSheet.addCommand('g^C', 'cancel-all', 'liveThreads=list(t for vs in vd.sheets for t in vs.currentThreads); cancelThread(*liveThreads); status("canceled %s threads" % len(liveThreads))', 'abort all spawned threads')
450
479
 
451
480
 
452
- BaseSheet.addCommand('^T', 'threads-all', 'vd.push(ThreadsSheet("threads", source=vd.threads))', 'open Threads for all sheets')
481
+ BaseSheet.addCommand('^T', 'threads-all', 'vd.push(vd.allThreadsSheet)', 'open Threads for all sheets')
453
482
  BaseSheet.addCommand('z^T', 'threads-sheet', 'vd.push(ThreadsSheet("threads", source=sheet.currentThreads))', 'open Threads for this sheet')
454
483
 
455
484
  vd.addGlobals({
@@ -457,6 +486,7 @@ vd.addGlobals({
457
486
  'Progress': Progress,
458
487
  'asynccache': asynccache,
459
488
  'asyncsingle': asyncsingle,
489
+ 'asyncsingle_queue': asyncsingle_queue,
460
490
  'asyncignore': asyncignore,
461
491
  })
462
492
 
visidata/utils.py CHANGED
@@ -5,7 +5,7 @@ import re
5
5
 
6
6
  'Various helper classes and functions.'
7
7
 
8
- __all__ = ['AlwaysDict', 'AttrDict', 'DefaultAttrDict', 'moveListItem', 'namedlist', 'classproperty', 'MissingAttrFormatter', 'getitem', 'setitem', 'getitemdef', 'getitemdeep', 'setitemdeep', 'getattrdeep', 'setattrdeep', 'ExplodingMock', 'ScopedSetattr']
8
+ __all__ = ['AlwaysDict', 'AttrDict', 'DefaultAttrDict', 'moveListItem', 'namedlist', 'classproperty', 'MissingAttrFormatter', 'getitem', 'setitem', 'getitemdef', 'getitemdeep', 'setitemdeep', 'getattrdeep', 'setattrdeep', 'ExplodingMock', 'ScopedSetattr', 'colname_letters']
9
9
 
10
10
 
11
11
  class AlwaysDict(dict):
@@ -215,3 +215,17 @@ def ScopedSetattr(obj, attrname, val):
215
215
  yield
216
216
  finally:
217
217
  setattr(obj, attrname, oldval)
218
+
219
+ def colname_letters(num):
220
+ '''*num* is a 1-based integer: 1, 2, 3... gives A B C .. Z AA AB .. ZZ AAA .. to infinity; *num* of 0 returns the empty string'''
221
+ # credit to https://stackoverflow.com/questions/48983939/convert-a-number-to-excel-s-base-26/48984697#48984697
222
+ def divmod_excel(n):
223
+ a, b = divmod(n, 26)
224
+ if b == 0:
225
+ return a - 1, b + 26
226
+ return a, b
227
+ chars = []
228
+ while num > 0:
229
+ num, d = divmod_excel(num)
230
+ chars.append('-ABCDEFGHIJKLMNOPQRSTUVWXYZ'[d])
231
+ return ''.join(reversed(chars))
File without changes
@@ -1,4 +1,4 @@
1
- .Dd October 13, 2024
1
+ .Dd June 13, 2025
2
2
  .Dt vd \&1 "Quick Reference Guide"
3
3
  .Os Linux/MacOS
4
4
  .
@@ -193,6 +193,8 @@ adjust widths of all visible columns to Ar number
193
193
  .Pp
194
194
  .It Ic " -" Ns " (hyphen)"
195
195
  hide current column
196
+ .It Ic "g-" Ns " (hyphen)"
197
+ hide any column that has multiple rows but only one distinct value
196
198
  .It Ic "z-" Ns
197
199
  reduce width of current column by half
198
200
  .It Ic "gv" Ns
@@ -247,10 +249,10 @@ add/reset cache for current/all visible column(s)
247
249
  .No add new columns from capture groups of Ar regex No (also requires example row)
248
250
  .It Ic "z" Ns Ic "\&;" Ar expr
249
251
  .No create new column from bash Ar expr Ns , with Sy $ Ns columnNames as variables
250
- .It Ic " *" Ar regex Ns Sy / Ns Ar subst
251
- .No add column derived from current column, replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs)
252
- .It Ic "g* gz*" Ar regex Ns Sy / Ns Ar subst
253
- .No modify selected rows in current/all visible column(s), replacing Ar regex No with Ar subst No (may include Sy \e1 No backrefs)
252
+ .It Ic " *" Ar search No Sy Tab No Ar replace
253
+ .No add column derived from current column, replacing Ar search No regex with Ar replace No (may include Sy \e1 No backrefs)
254
+ .It Ic "g* gz*" Ar search No Sy Tab No Ar replace
255
+ .No modify selected rows in current/all visible column(s), replacing Ar search No with Ar replace No (may include Sy \e1 No backrefs)
254
256
  .Pp
255
257
  .It Ic " ( g("
256
258
  .No expand current/all visible column(s) of lists (e.g. Sy [3] Ns ) or dicts (e.g. Sy {3} Ns ) one level
@@ -300,7 +302,7 @@ sort ascending/descending by current column; replace any existing sort criteria
300
302
  .It Ic " g[ g]"
301
303
  sort ascending/descending by all key columns; replace any existing sort criteria
302
304
  .It Ic " z[ z]"
303
- sort ascending/descending by current column; add to existing sort criteria
305
+ sort ascending/descending by current column; keep higher priority sort criteria
304
306
  .It Ic "gz[ gz]"
305
307
  sort ascending/descending by all key columns; add to existing sort criteria
306
308
  .It Ic " \&""
@@ -316,6 +318,8 @@ open duplicate sheet with deepcopy of selected rows
316
318
  The rows in these duplicated sheets (except deepcopy) are references to rows on the original source sheets, and so edits to the filtered rows will naturally be reflected in the original rows. Use
317
319
  .Ic "g'"
318
320
  to freeze sheet contents in a deliberate copy.
321
+ .Ic "z'"
322
+ replace current column with a frozen copy, with all cells evaluated
319
323
  .
320
324
  .Ss Editing Rows and Cells
321
325
  .
@@ -352,6 +356,8 @@ fill null cells in current column with contents of non-null cells up the current
352
356
  .
353
357
  .It Ic " e" Ar text
354
358
  edit contents of current cell
359
+ .It Ic " ^O"
360
+ .No edit contents of current cell in external Sy EDITOR
355
361
  .It Ic " ge" Ar text
356
362
  .No set contents of current column for selected rows to Ar text
357
363
  .
@@ -443,6 +449,8 @@ increase/decrease zoom level, centered on cursor
443
449
  zoom to fit full extent
444
450
  .It Ic "z_" No (underbar)
445
451
  set aspect ratio
452
+ .It Ic "g_" No (underbar)
453
+ Zoom y-axis to fit all visible data points
446
454
  .It Ic " x" Ar xmin xmax
447
455
  .No set Ar xmin Ns / Ns Ar xmax No on graph
448
456
  .It Ic " y" Ar ymin ymax
@@ -782,7 +790,7 @@ abort replay
782
790
  .It Ic ^T
783
791
  .No open global Sy Threads Sheet No for all asynchronous threads running
784
792
  .It Ic z^T
785
- .No open current sheet's Sy Threads Sheet No
793
+ .No open current sheet's Sy Threads Sheet
786
794
  .El
787
795
  .Bl -inset -compact
788
796
  .It (sheet-specific commands)
@@ -791,7 +799,7 @@ abort replay
791
799
  .It Ic " ^C"
792
800
  abort thread at current row
793
801
  .It Ic "g^C"
794
- .No abort all threads on current Sy Threads Sheet No
802
+ .No abort all threads on current Sy Threads Sheet
795
803
  .El
796
804
  .
797
805
  .Ss DERIVED SHEETS
@@ -912,7 +920,7 @@ disable loading .visidatarc and plugin addons
912
920
  .
913
921
  .El
914
922
  .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact
915
- .It Sy --visidata-dir Ns = Ns Ar "str " No "~/.visidata/"
923
+ .It Sy --visidata-dir Ns = Ns Ar "str " No "/home/anja/.config/visidata"
916
924
  directory to load and store additional files
917
925
  .It Sy --debug No " False"
918
926
  exit on error and display stacktrace
@@ -1021,7 +1029,7 @@ source of randomized startup messages
1021
1029
  folder recursion depth on DirSheet
1022
1030
  .It Sy --dir-hidden No " False"
1023
1031
  load hidden files on DirSheet
1024
- .It Sy --config Ns = Ns Ar "Path " No "/home/saul/.visidatarc"
1032
+ .It Sy --config Ns = Ns Ar "Path " No "/home/anja/.visidatarc"
1025
1033
  config file to exec in Python
1026
1034
  .It Sy --play Ns = Ns Ar "str " No ""
1027
1035
  file.vdj to replay
@@ -1055,7 +1063,7 @@ device ID associated with matrix login
1055
1063
  client_id for reddit api
1056
1064
  .It Sy --reddit-client-secret Ns = Ns Ar "str " No ""
1057
1065
  client_secret for reddit api
1058
- .It Sy --reddit-user-agent Ns = Ns Ar "str " No "3.1dev"
1066
+ .It Sy --reddit-user-agent Ns = Ns Ar "str " No "3.3"
1059
1067
  user_agent for reddit api
1060
1068
  .It Sy --zulip-batch-size Ns = Ns Ar "int " No "-100"
1061
1069
  number of messages to fetch per call (<0 to fetch before anchor)
@@ -1100,6 +1108,8 @@ max number of fixed-width columns to create (0 is no max)
1100
1108
  whether to include edge labels on graphviz diagrams
1101
1109
  .It Sy --grep-base-dir Ns = Ns Ar "NoneType " No "None"
1102
1110
  base directory for relative paths opened with sysopen-row
1111
+ .It Sy --hdf5-matrix-enumerate No " False"
1112
+ enumerate matrix rows and columns
1103
1113
  .It Sy --html-title Ns = Ns Ar "str " No "<h2>{sheet.name}</h2>"
1104
1114
  table header when saving to html
1105
1115
  .It Sy --http-max-next Ns = Ns Ar "int " No "0"
@@ -1110,6 +1120,8 @@ http headers to send to requests
1110
1120
  verify host and certificates for https
1111
1121
  .It Sy --npy-allow-pickle No " False"
1112
1122
  numpy allow unpickling objects (unsafe)
1123
+ .It Sy --npy-matrix-enumerate No " False"
1124
+ enumerate matrix rows and columns
1113
1125
  .It Sy --pcap-internet Ns = Ns Ar "str " No "n"
1114
1126
  (y/s/n) if save_dot includes all internet hosts separately (y), combined (s), or does not include the internet (n)
1115
1127
  .It Sy --pdf-tables No " False"
@@ -1144,8 +1156,6 @@ API Key for api.apilayer.com/fixer
1144
1156
  Cache days for currency conversions
1145
1157
  .It Sy --describe-aggrs Ns = Ns Ar "str " No "mean stdev"
1146
1158
  numeric aggregators to calculate on Describe sheet
1147
- .It Sy --hello-world Ns = Ns Ar "str " No "\[u00A1]Hola mundo!"
1148
- shown by the hello-world command
1149
1159
  .It Sy --incr-base Ns = Ns Ar "float " No "1.0"
1150
1160
  start value for column increments
1151
1161
  .It Sy --ping-count Ns = Ns Ar "int " No "3"
@@ -1193,7 +1203,7 @@ command submenu indicator
1193
1203
  indicator if command pushes sheet onto sheet stack
1194
1204
  .It Sy "disp_menu_input " No "\[u2026]"
1195
1205
  indicator if input required for command
1196
- .It Sy "disp_menu_fmt " No "| VisiData {vd.version} | {vd.hintStatus}"
1206
+ .It Sy "disp_menu_fmt " No "| VisiData {vd.version} | {vd.motd}"
1197
1207
  right-side menu format string
1198
1208
  .It Sy "disp_float_fmt " No "{:.02f}"
1199
1209
  default fmtstr to format float values
@@ -1305,6 +1315,8 @@ replace whitespace with spaces in multiline
1305
1315
  multiline string to indicate truncation
1306
1316
  .It Sy "disp_multiline_focus" No "True"
1307
1317
  only multiline cursor row
1318
+ .It Sy "color_multiline_bottom" No ""
1319
+ color of bottom line of multiline rows
1308
1320
  .It Sy "color_aggregator " No "bold 255 white on 234 black"
1309
1321
  color of aggregator summary on bottom row
1310
1322
  .It Sy "disp_rstatus_fmt " No "{sheet.threadStatus} {sheet.keystrokeStatus} [:longname_status]{sheet.longname}[/] {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus}{sheet.selectedStatus}{vd.replayStatus}{vd.sidebarStatus}"
@@ -1365,7 +1377,7 @@ histogram element character
1365
1377
  show axes and legend on graph
1366
1378
  .It Sy "disp_canvas_charset" No "\[u2800]\[u2801]\[u2802]\[u2803]\[u2804]\[u2805]\[u2806]\[u2807]\[u2808]\[u2809]\[u280A]\[u280B]\[u280C]\[u280D]\[u280E]\[u280F]\[u2810]\[u2811]\[u2812]\[u2813]\[u2814]\[u2815]\[u2816]\[u2817]\[u2818]\[u2819]\[u281A]\[u281B]\[u281C]\[u281D]\[u281E]\[u281F]\[u2820]\[u2821]\[u2822]\[u2823]\[u2824]\[u2825]\[u2826]\[u2827]\[u2828]\[u2829]\[u282A]\[u282B]\[u282C]\[u282D]\[u282E]\[u282F]\[u2830]\[u2831]\[u2832]\[u2833]\[u2834]\[u2835]\[u2836]\[u2837]\[u2838]\[u2839]\[u283A]\[u283B]\[u283C]\[u283D]\[u283E]\[u283F]\[u2840]\[u2841]\[u2842]\[u2843]\[u2844]\[u2845]\[u2846]\[u2847]\[u2848]\[u2849]\[u284A]\[u284B]\[u284C]\[u284D]\[u284E]\[u284F]\[u2850]\[u2851]\[u2852]\[u2853]\[u2854]\[u2855]\[u2856]\[u2857]\[u2858]\[u2859]\[u285A]\[u285B]\[u285C]\[u285D]\[u285E]\[u285F]\[u2860]\[u2861]\[u2862]\[u2863]\[u2864]\[u2865]\[u2866]\[u2867]\[u2868]\[u2869]\[u286A]\[u286B]\[u286C]\[u286D]\[u286E]\[u286F]\[u2870]\[u2871]\[u2872]\[u2873]\[u2874]\[u2875]\[u2876]\[u2877]\[u2878]\[u2879]\[u287A]\[u287B]\[u287C]\[u287D]\[u287E]\[u287F]\[u2880]\[u2881]\[u2882]\[u2883]\[u2884]\[u2885]\[u2886]\[u2887]\[u2888]\[u2889]\[u288A]\[u288B]\[u288C]\[u288D]\[u288E]\[u288F]\[u2890]\[u2891]\[u2892]\[u2893]\[u2894]\[u2895]\[u2896]\[u2897]\[u2898]\[u2899]\[u289A]\[u289B]\[u289C]\[u289D]\[u289E]\[u289F]\[u28A0]\[u28A1]\[u28A2]\[u28A3]\[u28A4]\[u28A5]\[u28A6]\[u28A7]\[u28A8]\[u28A9]\[u28AA]\[u28AB]\[u28AC]\[u28AD]\[u28AE]\[u28AF]\[u28B0]\[u28B1]\[u28B2]\[u28B3]\[u28B4]\[u28B5]\[u28B6]\[u28B7]\[u28B8]\[u28B9]\[u28BA]\[u28BB]\[u28BC]\[u28BD]\[u28BE]\[u28BF]\[u28C0]\[u28C1]\[u28C2]\[u28C3]\[u28C4]\[u28C5]\[u28C6]\[u28C7]\[u28C8]\[u28C9]\[u28CA]\[u28CB]\[u28CC]\[u28CD]\[u28CE]\[u28CF]\[u28D0]\[u28D1]\[u28D2]\[u28D3]\[u28D4]\[u28D5]\[u28D6]\[u28D7]\[u28D8]\[u28D9]\[u28DA]\[u28DB]\[u28DC]\[u28DD]\[u28DE]\[u28DF]\[u28E0]\[u28E1]\[u28E2]\[u28E3]\[u28E4]\[u28E5]\[u28E6]\[u28E7]\[u28E8]\[u28E9]\[u28EA]\[u28EB]\[u28EC]\[u28ED]\[u28EE]\[u28EF]\[u28F0]\[u28F1]\[u28F2]\[u28F3]\[u28F4]\[u28F5]\[u28F6]\[u28F7]\[u28F8]\[u28F9]\[u28FA]\[u28FB]\[u28FC]\[u28FD]\[u28FE]\[u28FF]"
1367
1379
  charset to render 2x4 blocks on canvas
1368
- .It Sy "disp_pixel_random " No "False"
1380
+ .It Sy "disp_graph_pixel_random" No "False"
1369
1381
  randomly choose attr from set of pixels instead of most common
1370
1382
  .It Sy "disp_zoom_incr " No "2.0"
1371
1383
  amount to multiply current zoomlevel when zooming
@@ -1385,8 +1397,17 @@ charset to render vertical reference lines on graph
1385
1397
  charset to render horizontal reference lines on graph
1386
1398
  .It Sy "disp_graph_multiple_reflines_char" No "\[u2592]"
1387
1399
  char to render multiple parallel reflines
1388
- .It Sy "disp_expert " No "0"
1389
- max level of options and columns to include
1400
+ .It Sy "disp_help_flags " No "cmdpalette guides help hints inputfield inputkeys nometacols sidebar"
1401
+ list of helper features to enable (space-separated):
1402
+ - "cmdpalette": exec-longname suggestions
1403
+ - "guides": guides in sidebar
1404
+ - "help": help sidebar collapsed by default
1405
+ - "hints": context-sensitive hints on menu line
1406
+ - "inputfield": context-sensitive help for each input field
1407
+ - "inputkeys": input quick reference in sidebar
1408
+ - "nometacols": hide expert columns on metasheets
1409
+ - "sidebar": context-sensitive sheet help in sidebar
1410
+ - "all": enable all helper features
1390
1411
  .It Sy "color_add_pending " No "green"
1391
1412
  color for rows pending add
1392
1413
  .It Sy "color_change_pending" No "reverse yellow"