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/shell.py CHANGED
@@ -41,6 +41,12 @@ def exec_shell(*args):
41
41
 
42
42
  @VisiData.api
43
43
  def open_dir(vd, p):
44
+ if p.is_dir():
45
+ return DirSheet(p.base_stem, source=p)
46
+ if p.is_file():
47
+ vd.status(f'opening {p.given} as txt')
48
+ return vd.open_txt(p)
49
+ vd.warning(f'could not determine file type for {p.given}')
44
50
  return DirSheet(p.base_stem, source=p)
45
51
 
46
52
  @VisiData.api
@@ -48,8 +54,8 @@ def open_fdir(vd, p):
48
54
  return FileListSheet(p.base_stem, source=p)
49
55
 
50
56
  @VisiData.api
51
- def addShellColumns(vd, cmd, sheet):
52
- shellcol = ColumnShell(cmd, source=sheet, width=0)
57
+ def addShellColumns(vd, cmd, sheet, curcol=None):
58
+ shellcol = ColumnShell(cmd, source=sheet, width=0, curcol=curcol)
53
59
  sheet.addColumnAtCursor(
54
60
  Column(cmd+'_stdout', type=bytes.rstrip, srccol=shellcol, getter=lambda col,row: col.srccol.getValue(row)[0]),
55
61
  Column(cmd+'_stderr', type=bytes.rstrip, srccol=shellcol, getter=lambda col,row: col.srccol.getValue(row)[1]),
@@ -57,23 +63,23 @@ def addShellColumns(vd, cmd, sheet):
57
63
 
58
64
 
59
65
  class ColumnShell(Column):
60
- def __init__(self, name, cmd=None, **kwargs):
66
+ def __init__(self, name, cmd=None, curcol=None, **kwargs):
61
67
  super().__init__(name, **kwargs)
62
68
  self.expr = cmd or name
69
+ self.curcol = curcol
63
70
 
64
71
  @asynccache(lambda col,row: (col, col.sheet.rowid(row)))
65
72
  def calcValue(self, row):
66
73
  try:
67
74
  import shlex
68
75
  args = []
69
- context = LazyComputeRow(self.source, row)
76
+ context = LazyComputeRow(self.source, row, curcol=self.curcol)
70
77
  for arg in shlex.split(self.expr):
71
78
  if arg.startswith('$'):
72
- args.append(shlex.quote(str(context[arg[1:]])))
73
- else:
74
- args.append(arg)
79
+ arg = shlex.quote(str(context[arg[1:]]))
80
+ args.append(arg)
75
81
 
76
- p = subprocess.Popen([os.getenv('SHELL', 'bash'), '-c', ' '.join(args)],
82
+ p = subprocess.Popen([os.getenv('SHELL', 'bash'), '-c', shlex.join(args)],
77
83
  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
78
84
  return p.communicate()
79
85
  except Exception as e:
@@ -88,20 +94,19 @@ class DirSheet(Sheet):
88
94
 
89
95
  - {help.commands.open_row_file}
90
96
  - {help.commands.open_rows}
91
- - (`open-dir-parent`) to open parent directory
97
+ - {help.commands.open_dir_parent}
92
98
  - {help.commands.sysopen_row}
93
99
 
94
100
  ## Options (must reload to take effect)
95
101
 
96
102
  - {help.options.dir_depth}
97
- - [CLI] `-r` to include all files in all subfolders
98
103
  - {help.options.dir_hidden}
99
104
  '''
100
105
  rowtype = 'files' # rowdef: Path
101
106
  defer = True
102
107
  columns = [
103
108
  Column('directory',
104
- getter=lambda col,row: str(row.parent) if str(row.parent) == '.' else str(row.parent) + '/',
109
+ getter=lambda col,row: str(row.parent) if str(row.parent) in ('.', '/') else str(row.parent) + '/',
105
110
  setter=lambda col,row,val: col.sheet.moveFile(row, val)),
106
111
  Column('filename',
107
112
  getter=lambda col,row: row._path.name,
@@ -248,15 +253,18 @@ def inputShell(vd):
248
253
  DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source.parent if source.resolve()!=Path(".").resolve() else os.path.dirname(source.resolve())))', 'open parent directory') #1801
249
254
  BaseSheet.addCommand('', 'open-dir-current', 'vd.push(vd.currentDirSheet)', 'open Directory Sheet: browse properties of files in current directory')
250
255
 
251
- Sheet.addCommand('z;', 'addcol-shell', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
256
+ Sheet.addCommand('z;', 'addcol-shell', 'cmd=inputShell(); addShellColumns(cmd, sheet, curcol=cursorCol)', 'create new column from bash expression, with $columnNames as variables')
252
257
 
253
258
  DirSheet.addCommand(ENTER, 'open-row-file', 'vd.push(openSource(cursorRow or fail("no row"), filetype="dir" if cursorRow.is_dir() else LazyComputeRow(sheet, cursorRow).ext))', 'open current file as a new sheet')
254
259
  DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets')
255
260
  DirSheet.addCommand('^O', 'sysopen-row', 'launchEditor(cursorRow)', 'open current file in external $EDITOR')
256
261
  DirSheet.addCommand('g^O', 'sysopen-rows', 'launchEditor(*selectedRows)', 'open selected files in external $EDITOR')
257
262
 
258
- DirSheet.addCommand('y', 'copy-row', 'copy_files([cursorRow], inputPath("copy to dest: "))', 'copy file to given directory')
259
- DirSheet.addCommand('gy', 'copy-selected', 'copy_files(selectedRows, inputPath("copy to dest: ", value=cursorRow.given))', 'copy selected files to given directory')
263
+ DirSheet.addCommand('y', 'copy-row', 'copy_files([cursorRow], inputPath("copy to dest: "))', 'copy file to given directory *path*')
264
+ DirSheet.addCommand('gy', 'copy-selected', 'copy_files(selectedRows, inputPath("copy to dest: ", value=cursorRow.given))', 'copy selected files to given directory *path*')
265
+
266
+ DirSheet.addCommand('z'+ENTER, 'open-row-filetype', 'ft = input("filetype: ", type="filetype", value=options.filetype or LazyComputeRow(sheet, cursorRow).ext); vd.push(openSource(cursorRow, filetype=ft) or fail(f"file {cursorDisplay} does not exist"))', 'open file in current row as input filetype')
267
+
260
268
 
261
269
  @DirSheet.api
262
270
  @asyncthread
@@ -282,4 +290,5 @@ vd.addGlobals({
282
290
 
283
291
  vd.addMenuItems('''
284
292
  Column > Add column > shell > addcol-shell
293
+ Open > file in row > open-row-filetype
285
294
  ''')
visidata/sidebar.py CHANGED
@@ -6,25 +6,81 @@ from visidata import CommandHelpGetter, OptionHelpGetter
6
6
 
7
7
 
8
8
  vd.option('disp_sidebar', True, 'whether to display sidebar')
9
- vd.option('disp_sidebar_fmt', '{guide}', 'format string for default sidebar')
9
+ vd.option('disp_sidebar_fmt', '', 'format string for default sidebar')
10
10
  vd.theme_option('disp_sidebar_width', 0, 'max width for sidebar')
11
11
  vd.theme_option('disp_sidebar_height', 0, 'max height for sidebar')
12
12
  vd.theme_option('color_sidebar', 'black on 114 blue', 'base color of sidebar')
13
13
  vd.theme_option('color_sidebar_title', 'black on yellow', 'color of sidebar title')
14
14
 
15
+ @VisiData.api
16
+ class AddedHelp:
17
+ '''Context manager to add help text/screen to list of available sidebars.'''
18
+ def __init__(self, text:Union[str,'HelpPane'], title=''):
19
+ if text:
20
+ self.helpfunc = lambda: (text, title)
21
+ else:
22
+ self.helpfunc = None
23
+
24
+ def __enter__(self):
25
+ if self.helpfunc:
26
+ vd._help_sidebars.insert(0, self.helpfunc)
27
+ vd.clearCaches()
28
+ return self
29
+
30
+ def __exit__(self, *args):
31
+ if self.helpfunc:
32
+ vd._help_sidebars.remove(self.helpfunc)
33
+ vd.clearCaches()
34
+
35
+
15
36
  @BaseSheet.property
16
37
  def formatter_helpstr(sheet):
17
38
  return AttrDict(commands=CommandHelpGetter(type(sheet)),
18
39
  options=OptionHelpGetter())
19
40
 
20
41
 
21
- @BaseSheet.property
42
+ @BaseSheet.cached_property
22
43
  def default_sidebar(sheet):
23
44
  'Default to format options.disp_sidebar_fmt. Overridable.'
24
45
  fmt = sheet.options.disp_sidebar_fmt
46
+ if not fmt: return ''
25
47
  return sheet.formatString(fmt, help=sheet.formatter_helpstr)
26
48
 
27
49
 
50
+ @VisiData.api
51
+ def cycleSidebar(vd, n:int=1):
52
+ if vd.sheet.help_sidebars:
53
+ vd.disp_help += n
54
+ if vd.disp_help >= len(vd.sheet.help_sidebars):
55
+ vd.disp_help = -1
56
+
57
+ vd.options.disp_sidebar = (vd.disp_help >= 0)
58
+ else:
59
+ vd.disp_help = -1
60
+ vd.clearCaches()
61
+
62
+
63
+ @BaseSheet.cached_property
64
+ def help_sidebars(sheet) -> 'list[Callable[[], tuple[str,str]]]':
65
+ r = []
66
+ if sheet.default_sidebar:
67
+ r.append(lambda: (sheet.default_sidebar, ''))
68
+ if sheet.guide:
69
+ r.append(lambda: (sheet.formatString(sheet.guide, help=sheet.formatter_helpstr), ''))
70
+ return r + vd._help_sidebars
71
+
72
+
73
+ @VisiData.cached_property
74
+ def sidebarStatus(vd) -> str:
75
+ if vd.sheet.help_sidebars:
76
+ if vd.options.disp_sidebar and vd.disp_help >= 0:
77
+ n = vd.disp_help+1
78
+ return f'[:onclick sidebar-toggle][:sidebar][{n}/{len(vd.sheet.help_sidebars)}][/]'
79
+ else:
80
+ return f'[:onclick sidebar-toggle][:sidebar][{len(vd.sheet.help_sidebars)}][/]'
81
+
82
+ return ''
83
+
28
84
  @VisiData.property
29
85
  def recentStatusMessages(vd) -> str:
30
86
  r = ''
@@ -51,27 +107,24 @@ def recentStatusMessages(vd) -> str:
51
107
  @VisiData.api
52
108
  def drawSidebar(vd, scr, sheet):
53
109
  sidebar = vd.recentStatusMessages
110
+ title = ''
54
111
  bottommsg = ''
55
112
  overflowmsg = '[:reverse] Ctrl+P to view all status messages [/]'
56
113
  try:
57
- if not sidebar and sheet.options.disp_sidebar:
58
- sidebar = sheet.default_sidebar
59
- if not sidebar and sheet.options.disp_help > 0:
60
- sidebar = sheet.formatString(sheet.guide, help=sheet.formatter_helpstr)
61
-
62
- if sheet.options.disp_help < 0:
63
- bottommsg = '[:onclick sidebar-toggle][:reverse][x][:]'
64
- overflowmsg = '[:onclick open-sidebar]…↓…[/]'
65
- else:
66
- bottommsg = '[:onclick sidebar-toggle][:reverse] b to toggle sidebar [:]'
67
- overflowmsg = '[:reverse] see full sidebar with [:code]gb[/] [:]'
114
+ if not sidebar and vd.options.disp_sidebar and vd.disp_help >= 0:
115
+ sidebar, title = sheet.help_sidebars[vd.disp_help%len(sheet.help_sidebars)]()
116
+
117
+ # bottommsg = sheet.formatString('[:onclick sidebar-toggle][:reverse] {help.commands.sidebar_toggle} [:]', help=sheet.formatter_helpstr)
118
+ # overflowmsg = '[:reverse] see full sidebar with [:code]gb[/] [:]'
119
+
120
+ overflowmsg = '[:onclick open-sidebar]…↓…[/]'
68
121
  except Exception as e:
69
122
  vd.exceptionCaught(e)
70
123
  sidebar = f'# error\n{e}'
71
124
 
72
125
  sheet.current_sidebar = sidebar
73
126
 
74
- return sheet.drawSidebarText(scr, text=sheet.current_sidebar, overflowmsg=overflowmsg, bottommsg=bottommsg)
127
+ return sheet.drawSidebarText(scr, text=sheet.current_sidebar, title=title, overflowmsg=overflowmsg, bottommsg=bottommsg)
75
128
 
76
129
  @BaseSheet.api
77
130
  def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
@@ -136,7 +189,7 @@ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', o
136
189
  x = max(0, w-titlew-6)
137
190
  clipdraw(sidebarscr, 0, x, f"|[:sidebar_title] {title} [:]|", cattr, w=titlew+4)
138
191
  if bottommsg:
139
- clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|[:]', cattr)
192
+ clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|', cattr)
140
193
 
141
194
  sidebarscr.refresh()
142
195
 
@@ -152,11 +205,13 @@ class SidebarSheet(TextSheet):
152
205
  - `b` to toggle the sidebar on/off for the current sheet
153
206
  '''
154
207
 
155
- BaseSheet.addCommand('b', 'sidebar-toggle', 'sheet.options.disp_sidebar = not sheet.options.disp_sidebar', 'toggle sidebar on/off')
208
+ BaseSheet.addCommand('b', 'sidebar-toggle', 'vd.options.disp_sidebar = not vd.options.disp_sidebar', 'toggle sidebar')
156
209
  BaseSheet.addCommand('gb', 'open-sidebar', 'sheet.current_sidebar = "" if not hasattr(sheet, "current_sidebar") else sheet.current_sidebar; vd.push(SidebarSheet(name, options.disp_sidebar_fmt, source=sheet.current_sidebar.splitlines()))', 'open sidebar in new sheet')
210
+ BaseSheet.addCommand('^G', 'sidebar-cycle', 'vd.cycleSidebar()', 'cycle through available sidebar panels')
157
211
 
158
212
 
159
213
  vd.addMenuItems('''
160
214
  View > Sidebar > toggle > sidebar-toggle
215
+ View > Sidebar > cycle > sidebar-cycle
161
216
  View > Sidebar > open in new sheet > open-sidebar
162
217
  ''')
visidata/sort.py CHANGED
@@ -35,18 +35,27 @@ class Reversor:
35
35
  return other.obj < self.obj
36
36
 
37
37
 
38
- @Sheet.api
39
- def sortkey(self, r):
38
+
39
+ @Sheet.cached_property
40
+ def ordering(sheet) -> 'list[tuple[Column, bool]]':
40
41
  ret = []
41
- for col, reverse in self._ordering:
42
+ for col, reverse in sheet._ordering:
42
43
  if isinstance(col, str):
43
- col = self.column(col)
44
+ col = sheet.column(col)
45
+ ret.append((col, reverse))
46
+ return ret
47
+
48
+
49
+ @Sheet.api
50
+ def sortkey(sheet, r, ordering:'list[tuple[Column, bool]]'=[]):
51
+ ret = []
52
+ for col, reverse in (ordering or sheet.ordering):
44
53
  val = col.getTypedValue(r)
45
54
  ret.append(Reversor(val) if reverse else val)
46
55
 
47
-
48
56
  return ret
49
57
 
58
+
50
59
  @Sheet.api
51
60
  @asyncthread
52
61
  def sort(self):
@@ -55,8 +64,14 @@ def sort(self):
55
64
  return
56
65
  try:
57
66
  with Progress(gerund='sorting', total=self.nRows) as prog:
67
+ # replace ambiguous colname strings with unambiguous Column objects #2494
68
+ self._ordering = self.ordering
69
+ def _sortkey(r):
70
+ prog.addProgress(1)
71
+ return self.sortkey(r, ordering=self._ordering)
72
+
58
73
  # must not reassign self.rows: use .sort() instead of sorted()
59
- self.rows.sort(key=lambda r,self=self,prog=prog: (prog.addProgress(1), self.sortkey(r))[1])
74
+ self.rows.sort(key=_sortkey)
60
75
  except TypeError as e:
61
76
  vd.warning('sort incomplete due to TypeError; change column type')
62
77
  vd.exceptionCaught(e, status=False)
visidata/statusbar.py CHANGED
@@ -11,14 +11,16 @@ import visidata
11
11
  from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr
12
12
 
13
13
 
14
- vd.option('disp_rstatus_fmt', '{sheet.threadStatus} {sheet.keystrokeStatus} [:longname]{sheet.longname}[/] {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus}{sheet.selectedStatus}{vd.replayStatus}', 'right-side status format string')
15
- vd.option('disp_status_fmt', '[:onclick sheets-stack]{sheet.shortcut} {sheet.name}[/]| ', 'status line prefix')
14
+
15
+ vd.option('disp_rstatus_fmt', '{sheet.threadStatus} {sheet.keystrokeStatus} [:longname_status]{sheet.longname}[/] {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus}{sheet.selectedStatus}{vd.replayStatus}{vd.sidebarStatus}', 'right-side status format string')
16
+ vd.option('disp_status_fmt', '{sheet.sheetlist}| ', 'left-side status format string')
16
17
  vd.theme_option('disp_lstatus_max', 0, 'maximum length of left status line')
17
18
  vd.theme_option('disp_status_sep', '│', 'separator between statuses')
18
19
 
19
20
  vd.theme_option('color_keystrokes', 'bold white on 237', 'color of input keystrokes')
20
- vd.theme_option('color_longname', '6', 'color of command longnames')
21
- vd.theme_option('color_keys', 'bold', 'color of keystrokes in help')
21
+ vd.theme_option('color_longname_guide', '237', 'color of command longnames')
22
+ vd.theme_option('color_longname_status', 'white', 'color of command longnames')
23
+ vd.theme_option('color_keys', 'bold reverse', 'color of keystrokes in help')
22
24
  vd.theme_option('color_status', 'bold on 238', 'status line color')
23
25
  vd.theme_option('color_error', '202 1', 'error message color')
24
26
  vd.theme_option('color_warning', '166 15', 'warning message color')
@@ -29,6 +31,41 @@ vd.theme_option('color_highlight_status', 'black on green', 'color of highlighte
29
31
 
30
32
  BaseSheet.init('longname', lambda: '')
31
33
 
34
+ def fitWithin(s, n=10):
35
+ if len(s) > n:
36
+ return s[:n//2-1] + '…' + s[-n//2+1:]
37
+ return s
38
+
39
+ @BaseSheet.property
40
+ def ancestors(sheet):
41
+ if isinstance(sheet.source, BaseSheet):
42
+ return sheet.source.ancestors + [sheet.source]
43
+ else:
44
+ return []
45
+
46
+ @BaseSheet.property
47
+ def sheetlist(sheet):
48
+ leafsheets = []
49
+ parents = set()
50
+
51
+ sheetstack = vd.sheetstack(sheet.pane)
52
+ sheets = [x for x in vd.allSheets if x in sheetstack]+ [x for x in sheetstack if x not in vd.allSheets]
53
+
54
+ sheetnames = []
55
+ for vs in sheets:
56
+ if isinstance(vs, BaseSheet):
57
+ shortcut = ' '
58
+ if vs.shortcut in '1 2 3 4 5 6 7 8 9 10'.split():
59
+ shortcut = vs.shortcut[-1] + '›'
60
+ if vs is vd.sheet:
61
+ sheetnames.append(f'[:menu_active]{shortcut}{vs.name}[:]')
62
+ else:
63
+ sheetnames.append(f'[:onclick jump-sheet-{vs.shortcut}]' + fitWithin(f'{shortcut}{vs.name}', 20) + '[:]')
64
+ else:
65
+ sheetnames.append(vs)
66
+
67
+ return ' | '.join(sheetnames)
68
+
32
69
  @BaseSheet.api
33
70
  def _updateStatusBeforeExec(sheet, cmd, args, ks):
34
71
  sheet.longname = cmd.longname
@@ -223,7 +260,7 @@ class StatusSheet(Sheet):
223
260
  ColumnItem('nrepeats', 2, type=int, width=0),
224
261
  ColumnItem('args', 1, width=0),
225
262
  Column('message', width=50, getter=lambda col,row: composeStatus(row[1], row[2])),
226
- ColumnItem('source', 3, max_help=1),
263
+ ColumnItem('source', 3, width=0),
227
264
  ]
228
265
  colorizers = [
229
266
  RowColorizer(1, 'color_error', lambda s,c,r,v: r and r[0] == 3),
visidata/stored_list.py CHANGED
@@ -13,8 +13,11 @@ class StoredList(list):
13
13
  @property
14
14
  def path(self):
15
15
  vdpath = Path(vd.options.visidata_dir)
16
- if vdpath.exists():
17
- return vdpath/(self.name + '.jsonl')
16
+ if not vdpath.exists():
17
+ if vd.options.nothing:
18
+ return
19
+ vdpath.mkdir(parents=True)
20
+ return vdpath/(self.name + '.jsonl')
18
21
 
19
22
  def reload(self):
20
23
  p = self.path
@@ -10,6 +10,7 @@ def curses_setup():
10
10
  import visidata
11
11
 
12
12
  curses.curs_set = lambda v: None
13
+ curses.doupdate = lambda: None
13
14
  visidata.options.overwrite = 'always'
14
15
 
15
16
 
@@ -56,6 +56,7 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
56
56
  'exec-python': 'import time',
57
57
  'unselect-cols-regex': '.',
58
58
  'go-col-regex': 'Units', # column name in sample
59
+ 'go-col-name': 'Units', # column name in sample
59
60
  'go-col-number': '2',
60
61
  'go-row-number': '5', # go to row 5
61
62
  'addcol-bulk': '1',
@@ -67,6 +68,7 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
67
68
  'setcol-incr-step': '2',
68
69
  'setcol-iter': 'range(1, 100)',
69
70
  'setcol-format-enum': '1=cat',
71
+ 'open-ping': 'github.com',
70
72
  'setcol-input': '5',
71
73
  'show-expr': 'OrderDate',
72
74
  'setcol-expr': 'OrderDate',
@@ -92,7 +94,8 @@ inputLines = { 'save-sheet': 'jetsam.csv', # save to some tmp file
92
94
  'expand-cols-depth': '0',
93
95
  'save-cmdlog': 'test_commands.vdj',
94
96
  'aggregate-col': 'mean',
95
- 'memo-aggregate': 'mean',
97
+ 'memo-aggregate': 'count',
98
+ 'memo-cell': 'memoname',
96
99
  'addcol-shell': '',
97
100
  'theme-input': 'light',
98
101
  'add-rows': '1',
@@ -170,12 +173,17 @@ class TestCommands:
170
173
 
171
174
  sample_file = vd.pkg_resources_files(visidata) / 'tests/sample.tsv'
172
175
  vs = visidata.TsvSheet('test_commands', source=visidata.Path(sample_file))
176
+ cmd = vs.getCommand(longname)
177
+ if not cmd:
178
+ vd.warning(f'command cannot be tested on TsvSheet, skipping: {longname}')
179
+ return
173
180
  vs.reload.__wrapped__(vs)
174
181
  vs.vd = vd
175
182
  vd.sheets = [vs]
176
183
  vd.allSheets = [vs]
177
184
  vs.mouseX, vs.mouseY = (4, 4)
178
185
  vs.draw(mock_screen)
186
+ vs._scr = mock_screen
179
187
  if longname in inputLines:
180
188
  vd.currentReplayRow = vd.cmdlog.newRow(longname=longname, input=inputLines[longname])
181
189
  else:
@@ -0,0 +1,18 @@
1
+ import pytest
2
+ import visidata
3
+
4
+ class TestCompleteExpr:
5
+ def test_completer(self):
6
+ vs = visidata.DirSheet('test', source=visidata.Path('.'))
7
+ vs.reload()
8
+ cexpr = visidata.CompleteExpr(vs)
9
+ assert cexpr('fi', 0) == 'filename' # visible column first
10
+ assert cexpr('fi', 1) == 'filetype' # hidden column second
11
+ assert cexpr('logn', 0) == 'lognormvariate' # global from math
12
+ assert cexpr('a+logn', 0) == 'a+lognormvariate'
13
+
14
+ assert cexpr('testv', 0) == 'testv' # no match returns same
15
+
16
+ visidata.vd.memoValue('testvalue', 42, '42')
17
+ cexpr = visidata.CompleteExpr(vs)
18
+ assert cexpr('testv', 0) == 'testvalue'
@@ -46,9 +46,10 @@ class TestEditText:
46
46
  self.chars.extend(keys.split())
47
47
 
48
48
  exception = kwargs.pop('exception', None)
49
+ widget = visidata.InputWidget(**kwargs)
49
50
  if exception:
50
51
  with pytest.raises(exception):
51
- visidata.vd.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr(), **kwargs)
52
+ widget.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr())
52
53
  else:
53
- r = visidata.vd.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr(), **kwargs)
54
+ r = widget.editline(mock_screen, 0, 0, 0, attr=visidata.ColorAttr())
54
55
  assert r == result
visidata/text_source.py CHANGED
@@ -2,7 +2,7 @@ import re
2
2
 
3
3
  from visidata import vd, BaseSheet
4
4
 
5
- vd.option('regex_skip', '', 'regex of lines to skip in text sources', help='regex')
5
+ vd.option('regex_skip', '', 'regex of lines to skip in text sources', help='regex', replay=True)
6
6
  vd.option('regex_flags', 'I', 'flags to pass to re.compile() [AILMSUX]', replay=True)
7
7
 
8
8
  @BaseSheet.api
@@ -44,9 +44,12 @@ class FilterFile:
44
44
 
45
45
 
46
46
  @BaseSheet.api
47
- def open_text_source(sheet):
48
- 'Open sheet source as text, using sheet options for encoding and regex_skip.'
49
- fp = sheet.source.open(encoding=sheet.options.encoding, encoding_errors=sheet.options.encoding_errors)
47
+ def open_text_source(sheet, **kwargs):
48
+ 'Open sheet source as text, passing **kwargs to .open() (default to sheet options for encoding and regex_skip).'
49
+ openkwargs = dict(encoding=sheet.options.encoding,
50
+ encoding_errors=sheet.options.encoding_errors)
51
+ openkwargs.update(kwargs)
52
+ fp = sheet.source.open(**openkwargs)
50
53
  regex_skip = sheet.options.regex_skip
51
54
  if regex_skip:
52
55
  return FilterFile(fp, regex_skip, sheet.regex_flags())
visidata/textsheet.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import textwrap
2
2
 
3
3
  from visidata import vd, BaseSheet, options, Sheet, ColumnItem, asyncthread
4
- from visidata import Column, ColumnItem, vlen
4
+ from visidata import Column, vlen
5
5
  from visidata import globalCommand, VisiData
6
6
  import visidata
7
7
 
8
8
 
9
- vd.option('wrap', False, 'wrap text to fit window width on TextSheet', max_help=0)
9
+ vd.option('wrap', False, 'wrap text to fit window width on TextSheet')
10
10
  vd.option('save_filetype', 'tsv', 'specify default file type to save as', replay=True)
11
11
 
12
12
 
@@ -32,7 +32,7 @@ class TextSheet(Sheet):
32
32
  for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)):
33
33
  yield [startingLine+i+1, L]
34
34
  else:
35
- yield [startingLine+1, text.strip()]
35
+ yield [startingLine+1, text]
36
36
 
37
37
  def sysopen(sheet, linenum=0):
38
38
  @asyncthread
@@ -55,8 +55,23 @@ class TextSheet(Sheet):
55
55
  # .source is list of source text lines to 'load'
56
56
  # .sourceSheet is Sheet error came from
57
57
  class ErrorSheet(TextSheet):
58
+ columns = [
59
+ ColumnItem('linenum', 0, type=int, width=0),
60
+ ColumnItem('error', 1),
61
+ ]
62
+ guide = '''# Error Sheet'''
58
63
  precious = False
59
64
 
65
+ class ErrorCellSheet(ErrorSheet):
66
+ columns = [
67
+ ColumnItem('linenum', 0, type=int, width=0),
68
+ ColumnItem('cell_error', 1),
69
+ ]
70
+ guide = '''# Error Cell Sheet
71
+ This sheet shows the error that occurred when calculating a cell.
72
+ - `q` to quit this error sheet.
73
+ '''
74
+
60
75
 
61
76
  class ErrorsSheet(Sheet):
62
77
  columns = [
@@ -83,15 +98,14 @@ def recentErrorsSheet(self):
83
98
  BaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status("no error")', 'view traceback for most recent error')
84
99
  BaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')
85
100
 
86
- Sheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet("%s[%s].%s" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet')
87
- Sheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+"_cell_error", sourceSheet=sheet, source=getattr(cursorCell, "error", None) or fail("no error this cell")))', 'view traceback for error in current cell')
101
+ 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')
88
102
 
89
103
  TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')
90
104
 
91
105
 
92
106
  TextSheet.options.save_filetype = 'txt'
93
107
 
94
- vd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})
108
+ vd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet, 'ErrorCellSheet': ErrorCellSheet})
95
109
 
96
110
  vd.addMenuItems('''
97
111
  View > Errors > recent > error-recent
visidata/themes/ascii8.py CHANGED
@@ -13,10 +13,10 @@ vd.themes['ascii8'] = dict(
13
13
  disp_ambig_width=1,
14
14
 
15
15
  disp_pending='',
16
- note_pending=':',
17
- note_format_exc='?',
18
- note_getter_exc='!',
19
- note_type_exc='!',
16
+ disp_note_pending=':',
17
+ disp_note_fmtexc='?',
18
+ disp_note_getexc='!',
19
+ disp_note_typeexc='!',
20
20
 
21
21
  color_note_pending='bold magenta',
22
22
  color_note_type='yellow',
@@ -68,7 +68,8 @@ vd.themes['ascii8'] = dict(
68
68
  color_top_status='underline',
69
69
  color_active_status='black on cyan',
70
70
  color_inactive_status='8 on black',
71
- color_working='green',
71
+ color_working='bold white',
72
+ color_longname_status='black on cyan',
72
73
 
73
74
  color_menu='black on cyan',
74
75
  color_menu_active='yellow on black',
@@ -80,5 +81,7 @@ vd.themes['ascii8'] = dict(
80
81
  disp_menu_input='_',
81
82
  disp_menu_fmt='7-bit ASCII 3-bit color',
82
83
  plot_colors = 'white',
83
- disp_histogram='*'
84
+ disp_histogram='*',
85
+ disp_graph_lines_x_charset='||||',
86
+ disp_graph_lines_y_charset='----'
84
87
  )
@@ -13,10 +13,10 @@ vd.themes['asciimono'] = dict(
13
13
  disp_ambig_width=1,
14
14
 
15
15
  disp_pending='',
16
- note_pending=':',
17
- note_format_exc='?',
18
- note_getter_exc='!',
19
- note_type_exc='!',
16
+ disp_note_pending=':',
17
+ disp_note_fmtexc='?',
18
+ disp_note_getexc='!',
19
+ disp_note_typeexc='!',
20
20
 
21
21
  color_note_pending='bold',
22
22
  color_note_type='',
@@ -48,9 +48,13 @@ vd.themes['asciimono'] = dict(
48
48
  color_hidden_col='8',
49
49
  color_selected_row='',
50
50
  color_edit_cell='',
51
+ color_edit_unfocused='',
51
52
  color_graph_hidden='',
52
53
  color_graph_selected='bold',
53
54
  color_status_replay='',
55
+ color_currency_neg='',
56
+ color_match='',
57
+ color_cmdpalette='',
54
58
 
55
59
  color_graph_axis='bold',
56
60
  color_sidebar='reverse',
@@ -69,6 +73,12 @@ vd.themes['asciimono'] = dict(
69
73
  color_active_status='reverse',
70
74
  color_inactive_status='8',
71
75
  color_working='',
76
+ color_longname='',
77
+ color_highlight_status='',
78
+ color_sidebar_title='',
79
+ color_heading='',
80
+ color_guide_unwritten='',
81
+ color_code='',
72
82
 
73
83
  color_menu='reverse',
74
84
  color_menu_active='',