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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. visidata/__init__.py +12 -10
  2. visidata/_input.py +208 -199
  3. visidata/_open.py +4 -1
  4. visidata/_types.py +4 -3
  5. visidata/aggregators.py +88 -39
  6. visidata/apps/vdsql/_ibis.py +9 -11
  7. visidata/apps/vdsql/clickhouse.py +2 -2
  8. visidata/apps/vdsql/snowflake.py +1 -1
  9. visidata/apps/vgit/status.py +1 -1
  10. visidata/basesheet.py +11 -4
  11. visidata/canvas.py +66 -24
  12. visidata/clipboard.py +13 -6
  13. visidata/cliptext.py +7 -6
  14. visidata/cmdlog.py +40 -27
  15. visidata/column.py +14 -49
  16. visidata/ddw/regex.ddw +3 -2
  17. visidata/deprecated.py +14 -2
  18. visidata/desktop/visidata.desktop +2 -2
  19. visidata/editor.py +1 -0
  20. visidata/errors.py +1 -1
  21. visidata/experimental/sort_selected.py +54 -0
  22. visidata/expr.py +69 -18
  23. visidata/features/change_precision.py +1 -3
  24. visidata/features/cmdpalette.py +23 -4
  25. visidata/features/colorsheet.py +1 -1
  26. visidata/features/dedupe.py +3 -3
  27. visidata/features/go_col.py +71 -0
  28. visidata/features/graph_seaborn.py +1 -1
  29. visidata/features/join.py +20 -10
  30. visidata/features/layout.py +18 -5
  31. visidata/features/ping.py +16 -12
  32. visidata/features/regex.py +5 -5
  33. visidata/features/slide.py +15 -17
  34. visidata/features/status_source.py +3 -1
  35. visidata/features/sysedit.py +1 -1
  36. visidata/features/transpose.py +2 -1
  37. visidata/features/type_ipaddr.py +2 -4
  38. visidata/features/unfurl.py +1 -0
  39. visidata/form.py +2 -2
  40. visidata/freqtbl.py +16 -11
  41. visidata/fuzzymatch.py +1 -0
  42. visidata/graph.py +173 -12
  43. visidata/guide.py +61 -25
  44. visidata/guides/ClipboardGuide.md +48 -0
  45. visidata/guides/ColumnsGuide.md +52 -0
  46. visidata/guides/CommandsSheet.md +28 -0
  47. visidata/guides/DirSheet.md +34 -0
  48. visidata/guides/ErrorsSheet.md +17 -0
  49. visidata/guides/FrequencyTable.md +42 -0
  50. visidata/guides/GrepSheet.md +28 -0
  51. visidata/guides/JsonSheet.md +38 -0
  52. visidata/guides/MacrosSheet.md +19 -0
  53. visidata/guides/MeltGuide.md +52 -0
  54. visidata/guides/MemorySheet.md +7 -0
  55. visidata/guides/MenuGuide.md +26 -0
  56. visidata/guides/ModifyGuide.md +38 -0
  57. visidata/guides/PivotGuide.md +71 -0
  58. visidata/guides/RegexGuide.md +107 -0
  59. visidata/guides/SelectionGuide.md +44 -0
  60. visidata/guides/SlideGuide.md +26 -0
  61. visidata/guides/SortGuide.md +0 -0
  62. visidata/guides/SplitpaneGuide.md +15 -0
  63. visidata/guides/TypesSheet.md +43 -0
  64. visidata/guides/XsvGuide.md +36 -0
  65. visidata/help.py +6 -6
  66. visidata/hint.py +2 -1
  67. visidata/indexsheet.py +2 -2
  68. visidata/interface.py +13 -14
  69. visidata/keys.py +4 -1
  70. visidata/loaders/api_airtable.py +1 -1
  71. visidata/loaders/archive.py +1 -1
  72. visidata/loaders/csv.py +9 -5
  73. visidata/loaders/eml.py +11 -6
  74. visidata/loaders/f5log.py +1 -0
  75. visidata/loaders/fec.py +18 -42
  76. visidata/loaders/fixed_width.py +19 -3
  77. visidata/loaders/grep.py +121 -0
  78. visidata/loaders/html.py +1 -0
  79. visidata/loaders/http.py +6 -1
  80. visidata/loaders/json.py +22 -4
  81. visidata/loaders/jsonla.py +8 -2
  82. visidata/loaders/mailbox.py +1 -0
  83. visidata/loaders/markdown.py +25 -6
  84. visidata/loaders/msgpack.py +19 -0
  85. visidata/loaders/npy.py +0 -1
  86. visidata/loaders/odf.py +18 -4
  87. visidata/loaders/orgmode.py +1 -1
  88. visidata/loaders/rec.py +6 -4
  89. visidata/loaders/sas.py +11 -4
  90. visidata/loaders/scrape.py +0 -1
  91. visidata/loaders/texttables.py +2 -0
  92. visidata/loaders/tsv.py +24 -7
  93. visidata/loaders/unzip_http.py +127 -3
  94. visidata/loaders/vds.py +4 -0
  95. visidata/loaders/vdx.py +1 -1
  96. visidata/loaders/xlsx.py +5 -0
  97. visidata/loaders/xml.py +2 -1
  98. visidata/macros.py +14 -31
  99. visidata/main.py +20 -15
  100. visidata/mainloop.py +17 -6
  101. visidata/man/vd.1 +74 -39
  102. visidata/man/vd.txt +73 -41
  103. visidata/memory.py +16 -5
  104. visidata/menu.py +14 -3
  105. visidata/metasheets.py +5 -6
  106. visidata/modify.py +4 -4
  107. visidata/mouse.py +2 -0
  108. visidata/movement.py +14 -28
  109. visidata/optionssheet.py +3 -5
  110. visidata/path.py +59 -37
  111. visidata/pivot.py +8 -5
  112. visidata/pyobj.py +63 -9
  113. visidata/rename_col.py +18 -1
  114. visidata/save.py +16 -9
  115. visidata/search.py +4 -4
  116. visidata/selection.py +10 -56
  117. visidata/settings.py +37 -35
  118. visidata/sheets.py +189 -118
  119. visidata/shell.py +23 -14
  120. visidata/sidebar.py +71 -16
  121. visidata/sort.py +21 -6
  122. visidata/statusbar.py +42 -5
  123. visidata/stored_list.py +5 -2
  124. visidata/tests/conftest.py +1 -0
  125. visidata/tests/test_commands.py +9 -1
  126. visidata/tests/test_completer.py +18 -0
  127. visidata/tests/test_edittext.py +3 -2
  128. visidata/text_source.py +7 -4
  129. visidata/textsheet.py +20 -6
  130. visidata/themes/ascii8.py +9 -6
  131. visidata/themes/asciimono.py +14 -4
  132. visidata/threads.py +13 -3
  133. visidata/tuiwin.py +5 -1
  134. visidata/type_currency.py +1 -2
  135. visidata/type_date.py +6 -1
  136. visidata/undo.py +10 -13
  137. visidata/utils.py +9 -3
  138. visidata/vdobj.py +21 -1
  139. visidata/wrappers.py +9 -1
  140. {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
  141. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
  142. {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
  143. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
  144. visidata-3.1.dist-info/RECORD +284 -0
  145. visidata-3.0.1.dist-info/RECORD +0 -258
  146. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
  147. {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
  148. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
  149. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
  150. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
  151. {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/rename_col.py CHANGED
@@ -1,4 +1,4 @@
1
- from visidata import vd, Sheet
1
+ from visidata import vd, VisiData, Sheet
2
2
 
3
3
  @Sheet.api
4
4
  def hint_rename_col(sheet):
@@ -6,6 +6,23 @@ def hint_rename_col(sheet):
6
6
  return 5, f"[:hint]The current column can't be used in an expression because [:code]{sheet.cursorCol.name}[/] is not a valid Python identifier. [:onclick rename-col]Rename the column[/] with `^`.[/]"
7
7
 
8
8
 
9
+ @VisiData.api
10
+ def addUndoColNames(vd, cols):
11
+ oldnames = [(c, c.name) for c in cols]
12
+ def _undo():
13
+ for c, name in oldnames:
14
+ c.name = name
15
+ vd.addUndo(_undo)
16
+
17
+
18
+ @Sheet.api
19
+ def updateColNames(sheet, rows, cols, overwrite=False):
20
+ vd.addUndoColNames(cols)
21
+ for c in cols:
22
+ if not c._name or overwrite:
23
+ c.name = "\n".join(c.getDisplayValue(r) for r in rows)
24
+
25
+
9
26
  Sheet.addCommand('^', 'rename-col', 'vd.addUndoColNames([cursorCol]); cursorCol.name = editCell(cursorVisibleColIndex, -1, value=cleanName(cursorCol.name))', 'rename current column')
10
27
  Sheet.addCommand('z^', 'rename-col-selected', 'updateColNames(selectedRows or [cursorRow], [sheet.cursorCol], overwrite=True)', 'rename current column to combined contents of current cell in selected rows (or current row)')
11
28
  Sheet.addCommand('g^', 'rename-cols-row', 'updateColNames(selectedRows or [cursorRow], sheet.visibleCols)', 'rename all unnamed visible columns to contents of selected rows (or current row)')
visidata/save.py CHANGED
@@ -3,7 +3,7 @@ import os
3
3
  from copy import copy
4
4
 
5
5
  from visidata import vd
6
- from visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper
6
+ from visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper, TypedWrapper, UNLOADED
7
7
 
8
8
  vd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)
9
9
  vd.option('save_encoding', 'utf-8', 'encoding passed to codecs.open when saving a file', replay=True, help=vd.help_encoding)
@@ -13,12 +13,15 @@ def safe_trdict(vs):
13
13
  'returns string.translate dictionary for replacing tabs and newlines'
14
14
  if vs.options.safety_first:
15
15
  delim = vs.options.delimiter
16
- return {
16
+ trdict = {
17
17
  0: '', # strip NUL completely
18
- ord(delim): vs.options.tsv_safe_tab, # \t
19
18
  10: vs.options.tsv_safe_newline, # \n
20
19
  13: vs.options.tsv_safe_newline, # \r
21
20
  }
21
+ if not delim or ord(delim) in trdict:
22
+ vd.fail(f'cannot use delimiter {repr(delim)} with safety_first')
23
+ trdict[ord(delim)] = vs.options.tsv_safe_tab # \t
24
+ return trdict
22
25
  return {}
23
26
 
24
27
 
@@ -29,12 +32,12 @@ def iterdispvals(sheet, *cols, format=False):
29
32
  cols = sheet.visibleCols
30
33
 
31
34
  transformers = collections.OrderedDict() # list of transformers for each column in order
35
+ trdict = sheet.safe_trdict()
32
36
  for col in cols:
33
37
  transformers[col] = [ col.type ]
34
38
  if format:
35
39
  formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))
36
40
  transformers[col].append(formatMaker(col._formatdict))
37
- trdict = sheet.safe_trdict()
38
41
  if trdict:
39
42
  transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))
40
43
 
@@ -46,7 +49,6 @@ def iterdispvals(sheet, *cols, format=False):
46
49
  dispval = col.getValue(r)
47
50
 
48
51
  except Exception as e:
49
- vd.exceptionCaught(e)
50
52
  dispval = options_safe_error or str(e)
51
53
 
52
54
  try:
@@ -56,6 +58,9 @@ def iterdispvals(sheet, *cols, format=False):
56
58
  elif isinstance(dispval, TypedExceptionWrapper):
57
59
  dispval = options_safe_error or str(dispval)
58
60
  break
61
+ elif isinstance(dispval, TypedWrapper):
62
+ dispval = ''
63
+ break
59
64
  else:
60
65
  dispval = t(dispval)
61
66
 
@@ -89,7 +94,7 @@ def getDefaultSaveName(sheet):
89
94
 
90
95
 
91
96
  @VisiData.api
92
- def save_cols(vd, cols):
97
+ def saveCols(vd, cols):
93
98
  sheet = cols[0].sheet
94
99
  vs = copy(sheet)
95
100
  vs.columns = list(cols)
@@ -109,8 +114,10 @@ def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):
109
114
  if not vsheets: # blank tuple
110
115
  vd.warning('no sheets to save')
111
116
  return
117
+ unloaded = [ vs for vs in vsheets if vs.rows is UNLOADED ]
118
+ vd.sync(*vd.ensureLoaded(unloaded))
112
119
 
113
- filetypes = [givenpath.ext, vd.options.save_filetype]
120
+ filetypes = [givenpath.ext.lower(), vd.options.save_filetype.lower()]
114
121
 
115
122
  vd.clearCaches()
116
123
 
@@ -203,8 +210,8 @@ BaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSave
203
210
  BaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet())', 'save root sheet to its source')
204
211
  BaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath("save all sheets to: "), *vd.stackedSheets)', 'save all sheets to given file or directory)')
205
212
  IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value="_".join(getattr(vs, "name", None) or "blank" for vs in selectedRows)), *selectedRows)', 'save all selected sheets to given file or directory')
206
- Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
207
- Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
213
+ Sheet.addCommand('', 'save-col', 'saveCols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
214
+ Sheet.addCommand('', 'save-col-keys', 'saveCols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
208
215
 
209
216
  vd.addMenuItems('''
210
217
  File > Save > current sheet > save-sheet
visidata/search.py CHANGED
@@ -91,10 +91,10 @@ def moveInputRegex(sheet, action:str, type="regex", **kwargs):
91
91
 
92
92
  @Sheet.api
93
93
  @asyncthread
94
- def search_expr(sheet, expr, reverse=False):
94
+ def search_expr(sheet, expr, reverse=False, curcol=None):
95
95
  for i in rotateRange(len(sheet.rows), sheet.cursorRowIndex, reverse=reverse):
96
96
  try:
97
- if sheet.evalExpr(expr, sheet.rows[i]):
97
+ if sheet.evalExpr(expr, sheet.rows[i], curcol=curcol):
98
98
  sheet.cursorRowIndex=i
99
99
  return
100
100
  except Exception as e:
@@ -111,8 +111,8 @@ Sheet.addCommand('N', 'searchr-next', 'vd.moveRegex(sheet, reverse=True)', 'go t
111
111
 
112
112
  Sheet.addCommand('g/', 'search-cols', 'moveInputRegex("g/", backward=False, columns="visibleCols")', 'search for regex forwards over all visible columns')
113
113
  Sheet.addCommand('g?', 'searchr-cols', 'moveInputRegex("g?", backward=True, columns="visibleCols")', 'search for regex backwards over all visible columns')
114
- Sheet.addCommand('z/', 'search-expr', 'search_expr(inputExpr("search by expr: ") or fail("no expr"))', 'search by Python expression forwards in current column (with column names as variables)')
115
- Sheet.addCommand('z?', 'searchr-expr', 'search_expr(inputExpr("searchr by expr: ") or fail("no expr"), reverse=True)', 'search by Python expression backwards in current column (with column names as variables)')
114
+ Sheet.addCommand('z/', 'search-expr', 'search_expr(inputExpr("search by expr: ") or fail("no expr"), curcol=cursorCol)', 'search by Python expression forwards in current column (with column names as variables)')
115
+ Sheet.addCommand('z?', 'searchr-expr', 'search_expr(inputExpr("searchr by expr: ") or fail("no expr"), curcol=cursorCol, reverse=True)', 'search by Python expression backwards in current column (with column names as variables)')
116
116
 
117
117
  vd.addMenuItems('''
118
118
  View > Search > current column > search-col
visidata/selection.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from copy import copy
2
- from visidata import vd, Sheet, Progress, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, RowColorizer, GuideSheet
2
+ from visidata import vd, Sheet, Progress, asyncthread, options, rotateRange, Fanout, undoAttrCopyFunc, RowColorizer
3
3
 
4
4
  vd.option('bulk_select_clear', False, 'clear selected rows before new bulk selections', replay=True)
5
5
  vd.option('some_selected_rows', False, 'if no rows selected, if True, someSelectedRows returns all rows; if False, fails')
@@ -13,54 +13,6 @@ Sheet.colorizers.append( RowColorizer(2, 'color_selected_row', lambda s,c,r,v:
13
13
  r is not None and s.isSelected(r))
14
14
  )
15
15
 
16
- class SelectionGuide(GuideSheet):
17
- sheettype = Sheet
18
- guide_text ='''# Selecting and filtering
19
-
20
- Some commands operate only on "selected rows". For instance, a common command to filter is {help.commands.dup_selected}.
21
-
22
- Many g-prefixed commands are like this. For example, use {help.commands.edit_cell}, but use {help.commands.setcol_input}. Search for "selected rows" in the [:onclick help-commands-all]commands list[/] or the [:onclick sysopen-help]manpage[/] for a full list.
23
-
24
- Rows on the **Frequency Table** or **Pivot Table** reference a group of rows from the source sheet. Selecting a row on those sheets also selects the referenced rows on the underlying source sheet.
25
-
26
- Select and unselect rows with these commands:
27
-
28
- ## One row at a time
29
-
30
- - {help.commands.select_row}
31
- - {help.commands.unselect_row}
32
- - {help.commands.stoggle_row}
33
-
34
- ## All rows at the same time
35
-
36
- - {help.commands.select_rows}
37
- - {help.commands.unselect_rows}
38
- - {help.commands.stoggle_rows}
39
-
40
- ## By matching patterns
41
-
42
- - {help.commands.select_col_regex}
43
- - {help.commands.unselect_col_regex}
44
- - {help.commands.select_cols_regex}
45
- - {help.commands.unselect_cols_regex}
46
-
47
- - {help.commands.select_equal_cell}
48
- - {help.commands.select_equal_row}
49
-
50
- ## Select by Python expression
51
-
52
- Python expressions can use a column value by the column name, if the
53
- column name is a valid Python identifier (with only letters, digits, and underscores).
54
-
55
- - {help.commands.select_expr}
56
- - {help.commands.unselect_expr}
57
-
58
- ## Options
59
-
60
- - {help.options.bulk_select_clear}
61
- - {help.options.some_selected_rows}
62
- '''
63
-
64
16
  @Sheet.api
65
17
  def isSelected(self, row):
66
18
  'Return True if *row* is selected.'
@@ -71,8 +23,10 @@ def isSelected(self, row):
71
23
  def toggle(self, rows):
72
24
  'Toggle selection of given *rows*. Async.'
73
25
  self.addUndoSelection()
74
- for r in Progress(rows, 'toggling', total=len(self.rows)):
75
- if not self.unselectRow(r):
26
+ for r in Progress(rows, 'toggling', total=len(rows)):
27
+ if self.isSelected(r): #1671
28
+ self.unselectRow(r)
29
+ else:
76
30
  self.selectRow(r)
77
31
 
78
32
 
@@ -87,7 +41,9 @@ def select_row(self, row):
87
41
  def toggle_row(self, row):
88
42
  'Toggle selection of given *row*.'
89
43
  self.addUndoSelection()
90
- if not self.unselectRow(row):
44
+ if self.isSelected(row):
45
+ self.unselectRow(row)
46
+ else:
91
47
  self.selectRow(row)
92
48
 
93
49
 
@@ -240,8 +196,8 @@ Sheet.addCommand('g,', 'select-equal-row', 'select(gatherBy(lambda r,currow=curs
240
196
  Sheet.addCommand('z,', 'select-exact-cell', 'select(gatherBy(lambda r,c=cursorCol,v=cursorTypedValue: c.getTypedValue(r) == v), progress=False)', 'select rows matching current cell in current column')
241
197
  Sheet.addCommand('gz,', 'select-exact-row', 'select(gatherBy(lambda r,currow=cursorRow,vcols=visibleCols: all([c.getTypedValue(r) == c.getTypedValue(currow) for c in vcols])), progress=False)', 'select rows matching current row in all visible columns')
242
198
 
243
- Sheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'select rows matching Python expression in any visible column')
244
- Sheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr: sheet.evalExpr(expr, r)), progress=False)', 'unselect rows matching Python expression in any visible column')
199
+ Sheet.addCommand('z|', 'select-expr', 'expr=inputExpr("select by expr: "); select(gatherBy(lambda r, sheet=sheet, expr=expr, curcol=cursorCol: sheet.evalExpr(expr, r, curcol=curcol)), progress=False)', 'select rows matching Python expression in any visible column')
200
+ Sheet.addCommand('z\\', 'unselect-expr', 'expr=inputExpr("unselect by expr: "); unselect(gatherBy(lambda r, sheet=sheet, expr=expr, curcol=cursorCol: sheet.evalExpr(expr, r, curcol=curcol)), progress=False)', 'unselect rows matching Python expression in any visible column')
245
201
 
246
202
  Sheet.addCommand(None, 'select-error-col', 'select(gatherBy(lambda r,c=cursorCol: c.isError(r)), progress=False)', 'select rows with errors in current column')
247
203
  Sheet.addCommand(None, 'select-error', 'select(gatherBy(lambda r,vcols=visibleCols: isinstance(r, TypedExceptionWrapper) or any([c.isError(r) for c in vcols])), progress=False)', 'select rows with errors in any column')
@@ -266,5 +222,3 @@ vd.addMenuItems('''
266
222
  Row > Toggle select > from top > stoggle-before
267
223
  Row > Toggle select > to bottom > stoggle-after
268
224
  ''')
269
-
270
- vd.addGuide('SelectionGuide', SelectionGuide)
visidata/settings.py CHANGED
@@ -84,7 +84,7 @@ class SettingsMgr(collections.OrderedDict):
84
84
  if d:
85
85
  for m in self._mappings(obj or vd.activeSheet):
86
86
  v = d.get(self.objname(m))
87
- if v:
87
+ if v is not None:
88
88
  return v
89
89
 
90
90
  def iter(self, obj=None):
@@ -107,16 +107,17 @@ class SettingsMgr(collections.OrderedDict):
107
107
 
108
108
 
109
109
  class Command:
110
- def __init__(self, longname, execstr, helpstr='', module='', deprecated=False):
110
+ def __init__(self, longname, execstr, helpstr='', module='', replay=True, deprecated=False):
111
111
  self.longname = longname
112
112
  self.execstr = execstr
113
113
  self.helpstr = helpstr
114
114
  self.module = module
115
115
  self.deprecated = deprecated
116
+ self.replayable = replay
116
117
 
117
118
 
118
119
  class Option:
119
- def __init__(self, name, value, description='', module='', help='', max_help=10):
120
+ def __init__(self, name, value, description='', module='', help=''):
120
121
  # description gets shows on the manpage and the optionssheet; help is shown on the sidebar while editing
121
122
  self.name = name
122
123
  self.value = value
@@ -125,7 +126,6 @@ class Option:
125
126
  self.replayable = False
126
127
  self.sheettype = BaseSheet
127
128
  self.module = module
128
- self.max_help = max_help
129
129
 
130
130
  def __str__(self):
131
131
  return str(self.value)
@@ -299,7 +299,7 @@ def _resolve_optalias(vd, optname, optval):
299
299
 
300
300
 
301
301
  @VisiData.api
302
- def option(vd, name, default, description, replay=False, sheettype=BaseSheet, help:str='', max_help=10):
302
+ def option(vd, name, default, description, replay=False, sheettype=BaseSheet, help:str=''):
303
303
  '''Declare a new option.
304
304
 
305
305
  - `name`: name of option
@@ -312,7 +312,6 @@ def option(vd, name, default, description, replay=False, sheettype=BaseSheet, he
312
312
  opt.replayable = replay
313
313
  opt.sheettype=sheettype
314
314
  opt.extrahelp = help
315
- opt.max_help = max_help
316
315
  return opt
317
316
 
318
317
 
@@ -320,12 +319,12 @@ def option(vd, name, default, description, replay=False, sheettype=BaseSheet, he
320
319
  def theme_option(vd, name, *args, **kwargs):
321
320
  if name.startswith('color_'):
322
321
  kwargs.setdefault('help', vd.help_color)
323
- return vd.option(name, *args, **kwargs, max_help=-1)
322
+ return vd.option(name, *args, **kwargs)
324
323
 
325
324
 
326
325
  @BaseSheet.class_api
327
326
  @classmethod
328
- def addCommand(cls, keystrokes, longname, execstr, helpstr='', **kwargs):
327
+ def addCommand(cls, keystrokes, longname, execstr, helpstr='', replay=True, **kwargs):
329
328
  '''Add a new command to *cls* sheet type.
330
329
 
331
330
  - *keystrokes*: default keybinding, including **prefixes**.
@@ -333,9 +332,10 @@ def addCommand(cls, keystrokes, longname, execstr, helpstr='', **kwargs):
333
332
  - *execstr*: Python statement to pass to `exec()`'ed when the command is executed.
334
333
  - *helpstr*: help string shown in the **Commands Sheet**.
335
334
  '''
336
- vd.commands.set(longname, Command(longname, execstr, helpstr=helpstr, module=vd.importingModule, **kwargs), cls)
335
+ cmd = Command(longname, execstr, helpstr=helpstr, module=vd.importingModule, replay=replay, **kwargs)
336
+ vd.commands.set(longname, cmd, cls)
337
337
  if keystrokes:
338
- vd.bindkeys.set(vd.prettykeys(keystrokes.replace(' ', '')), longname, cls)
338
+ vd.bindkey(keystrokes, longname, cls)
339
339
  return longname
340
340
 
341
341
  def _command(cls, binding, longname, helpstr, **kwargs):
@@ -346,12 +346,19 @@ def _command(cls, binding, longname, helpstr, **kwargs):
346
346
  return decorator
347
347
 
348
348
  BaseSheet.command = classmethod(_command)
349
- globalCommand = BaseSheet.addCommand
349
+ globalCommand = BaseSheet.addCommand # to be deprecated
350
+
351
+
352
+ @VisiData.api
353
+ def bindkey(vd, keystrokes, longname, obj='BaseSheet'):
354
+ 'Bind *keystrokes* to *longname* on BaseSheet and unbind more-specific bindings of keystrokes.'
355
+ vd.bindkeys.set(vd.prettykeys(keystrokes.replace(' ', '')), longname, obj)
350
356
 
351
357
  @VisiData.api
352
- def bindkey(vd, keystrokes, longname):
358
+ def unbindkey(vd, keystrokes, obj='BaseSheet'):
353
359
  'Bind *keystrokes* to *longname* on BaseSheet and unbind more-specific bindings of keystrokes.'
354
- vd.bindkeys[vd.prettykeys(keystrokes)] = {'BaseSheet': longname}
360
+ vd.bindkeys.unset(vd.prettykeys(keystrokes.replace(' ', '')), obj)
361
+
355
362
 
356
363
  @BaseSheet.class_api
357
364
  @classmethod
@@ -360,14 +367,14 @@ def bindkey(cls, keystrokes, longname):
360
367
  oldlongname = vd.bindkeys._get(keystrokes, cls)
361
368
  if oldlongname:
362
369
  vd.warning('%s was already bound to %s' % (keystrokes, oldlongname))
363
- vd.bindkeys.set(vd.prettykeys(keystrokes), longname, cls)
370
+ vd.bindkey(keystrokes, longname, cls)
364
371
 
365
372
  @BaseSheet.class_api
366
373
  @classmethod
367
374
  def unbindkey(cls, keystrokes):
368
375
  '''Unbind `keystrokes` on a `<SheetType>`.
369
376
  May be necessary to avoid a warning when overriding a binding on the same exact class.'''
370
- vd.bindkeys.unset(vd.prettykeys(keystrokes), cls)
377
+ vd.unbindkey(keystrokes, cls)
371
378
 
372
379
  @BaseSheet.api
373
380
  def getCommand(sheet, cmd):
@@ -376,29 +383,32 @@ def getCommand(sheet, cmd):
376
383
  return cmd
377
384
 
378
385
  longname = cmd
379
- while vd.bindkeys._get(longname, obj=sheet):
386
+ while vd.bindkeys._get(longname, obj=sheet) is not None:
380
387
  longname = vd.bindkeys._get(longname, obj=sheet)
381
388
 
382
389
  return vd.commands._get(longname, obj=sheet)
383
390
 
384
391
 
385
392
  @VisiData.api
386
- def loadConfigFile(vd, fn='', _globals=None):
393
+ def loadConfigFile(vd, fn=''):
387
394
  p = visidata.Path(fn or vd.options.config)
388
- if _globals is None:
389
- _globals = vd.getGlobals()
390
395
  if p.exists():
396
+ newdefs = {}
391
397
  try:
392
398
  with open(p) as fd:
393
399
  code = compile(fd.read(), str(p), 'exec')
394
400
  vd.importingModule = 'visidatarc'
395
- exec(code, _globals)
401
+ exec(code, vd.getGlobals(), newdefs)
396
402
  except Exception as e:
397
403
  vd.exceptionCaught(e)
398
404
  finally:
399
405
  vd.importingModule = None
400
406
 
401
- vd.addGlobals(_globals)
407
+ keys = newdefs.get('__all__', None)
408
+ if keys:
409
+ vd.addGlobals({k:newdefs[k] for k in keys})
410
+ else:
411
+ vd.addGlobals(newdefs)
402
412
 
403
413
 
404
414
  def addOptions(parser):
@@ -470,17 +480,21 @@ def loadConfigAndPlugins(vd, args=AttrDict()):
470
480
 
471
481
  # user customisations in config file in standard location
472
482
  if vd.options.config:
473
- vd.loadConfigFile(vd.options.config, vd.getGlobals())
483
+ vd.loadConfigFile(vd.options.config)
474
484
 
475
485
 
476
486
  @VisiData.api
477
- def importModule(vd, pkgname):
487
+ def importModule(vd, pkgname, symbols=[]):
478
488
  'Import the given *pkgname*, setting vd.importingModule to *pkgname* before import and resetting to None after.'
479
489
  modparts = pkgname.split('.')
480
490
  vd.importingModule = modparts[-1]
481
491
  r = importlib.import_module(pkgname)
482
492
  vd.importingModule = None
483
493
  vd.importedModules.append(r)
494
+ vd.addGlobals({pkgname:r})
495
+ if symbols:
496
+ vd.addGlobals({k:getattr(r, k) for k in symbols if hasattr(r, k)})
497
+
484
498
  return r
485
499
 
486
500
 
@@ -488,24 +502,12 @@ def importModule(vd, pkgname):
488
502
  def importSubmodules(vd, pkgname):
489
503
  'Import all files below the given *pkgname*'
490
504
  import pkgutil
491
- import os.path
492
505
 
493
506
  m = vd.importModule(pkgname)
494
507
  for module in pkgutil.walk_packages(m.__path__):
495
508
  vd.importModule(pkgname + '.' + module.name)
496
509
 
497
510
 
498
- @VisiData.api
499
- def importStar(vd, pkgname):
500
- 'Add all symbols from *pkgname* into visidata globals.'
501
- import pkgutil
502
- import os.path
503
-
504
- m = vd.importModule(pkgname)
505
- vd.addGlobals({pkgname:m})
506
- vd.addGlobals(m.__dict__)
507
-
508
-
509
511
  @VisiData.api
510
512
  def importExternal(vd, modname, pipmodname=''):
511
513
  pipmodname = pipmodname or modname