visidata 2.11.dev0__py3-none-any.whl → 3.0__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 (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
visidata/shell.py CHANGED
@@ -10,19 +10,25 @@ except ImportError:
10
10
  pass # pwd,grp modules not available on Windows
11
11
 
12
12
  from visidata import Column, Sheet, LazyComputeRow, asynccache, BaseSheet, vd
13
- from visidata import Path, ENTER, asyncthread, FileExistsError, VisiData
13
+ from visidata import Path, ENTER, asyncthread, VisiData
14
14
  from visidata import modtime, filesize, vstat, Progress, TextSheet
15
15
  from visidata.type_date import date
16
16
 
17
17
 
18
- vd.option('dir_recurse', False, 'walk source path recursively on DirSheet')
18
+ vd.option('dir_depth', 0, 'folder recursion depth on DirSheet')
19
19
  vd.option('dir_hidden', False, 'load hidden files on DirSheet')
20
20
 
21
21
 
22
+ @VisiData.api
23
+ def guess_dir(vd, p):
24
+ if p.is_dir():
25
+ return dict(filetype='dir')
26
+
27
+
22
28
  @VisiData.lazy_property
23
29
  def currentDirSheet(p):
24
30
  'Support opening the current DirSheet from the vdmenu'
25
- return DirSheet('.', source=Path('.'))
31
+ return DirSheet(Path('.').absolute().name, source=Path('.'))
26
32
 
27
33
  @asyncthread
28
34
  def exec_shell(*args):
@@ -35,11 +41,11 @@ def exec_shell(*args):
35
41
 
36
42
  @VisiData.api
37
43
  def open_dir(vd, p):
38
- return DirSheet(p.name, source=p)
44
+ return DirSheet(p.base_stem, source=p)
39
45
 
40
46
  @VisiData.api
41
47
  def open_fdir(vd, p):
42
- return FileListSheet(p.name, source=p)
48
+ return FileListSheet(p.base_stem, source=p)
43
49
 
44
50
  @VisiData.api
45
51
  def addShellColumns(vd, cmd, sheet):
@@ -76,6 +82,21 @@ class ColumnShell(Column):
76
82
 
77
83
  class DirSheet(Sheet):
78
84
  'Sheet displaying directory, using ENTER to open a particular file. Edited fields are applied to the filesystem.'
85
+ guide = '''
86
+ # Directory Sheet
87
+ This is a list of files in the {sheet.displaySource} folder.
88
+
89
+ - {help.commands.open_row_file}
90
+ - {help.commands.open_rows}
91
+ - (`open-dir-parent`) to open parent directory
92
+ - {help.commands.sysopen_row}
93
+
94
+ ## Options (must reload to take effect)
95
+
96
+ - {help.options.dir_depth}
97
+ - [CLI] `-r` to include all files in all subfolders
98
+ - {help.options.dir_hidden}
99
+ '''
79
100
  rowtype = 'files' # rowdef: Path
80
101
  defer = True
81
102
  columns = [
@@ -145,11 +166,14 @@ class DirSheet(Sheet):
145
166
 
146
167
  def removeFile(self, path):
147
168
  if path.is_dir():
148
- os.rmdir(path)
169
+ if self.options.safety_first:
170
+ os.rmdir(path)
171
+ else:
172
+ shutil.rmtree(path) #1965
149
173
  else:
150
174
  path.unlink()
151
175
 
152
- def deleteSourceRow(self, r):
176
+ def commitDeleteRow(self, r):
153
177
  self.removeFile(r)
154
178
 
155
179
  def newRow(self):
@@ -158,7 +182,7 @@ class DirSheet(Sheet):
158
182
  def iterload(self):
159
183
  hidden_files = self.options.dir_hidden
160
184
 
161
- def _walkfiles(p):
185
+ def _walkfiles(p, dir_depth:int=0):
162
186
  basepath = str(p)
163
187
  for folder, subdirs, files in os.walk(basepath):
164
188
  subfolder = folder[len(basepath)+1:]
@@ -166,24 +190,23 @@ class DirSheet(Sheet):
166
190
  if subfolder in ['.', '..']: continue
167
191
 
168
192
  fpath = Path(folder)
169
- yield fpath
193
+ if str(fpath) != str(p):
194
+ yield fpath
170
195
 
171
196
  for fn in files:
172
197
  yield fpath/fn
173
198
 
174
- def _listfiles(p):
175
- basepath = str(p)
176
- for fn in os.listdir(basepath):
177
- yield p/fn
178
-
199
+ if dir_depth < len(fpath.parents)-len(p.parents)+1:
200
+ for d in subdirs:
201
+ yield fpath/d
202
+ subdirs.clear()
179
203
 
180
204
  basepath = str(self.source)
181
205
 
182
206
  folders = set()
183
- f = _walkfiles if self.options.dir_recurse else _listfiles
184
207
 
185
- for p in f(self.source):
186
- if not hidden_files and p.name.startswith('.'):
208
+ for p in _walkfiles(self.source, self.options.dir_depth):
209
+ if not hidden_files and str(p).startswith('.') and not str(p).startswith('..'):
187
210
  continue
188
211
 
189
212
  yield p
@@ -204,11 +227,14 @@ class DirSheet(Sheet):
204
227
  self._deferredDels.clear()
205
228
  self.reload()
206
229
 
230
+ def getDefaultSaveName(sheet):
231
+ return sheet.name + '.' + sheet.options.save_filetype
232
+
207
233
 
208
234
  class FileListSheet(DirSheet):
209
235
  _ordering = []
210
236
  def iterload(self):
211
- for fn in self.source.open_text():
237
+ for fn in self.source.open():
212
238
  yield Path(fn.rstrip())
213
239
 
214
240
 
@@ -219,10 +245,10 @@ def inputShell(vd):
219
245
  vd.warning('no $column in command')
220
246
  return cmd
221
247
 
222
- DirSheet.addCommand('`', 'open-dir-parent', 'vd.push(openSource(source/".."))', 'open parent directory')
248
+ 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
223
249
  BaseSheet.addCommand('', 'open-dir-current', 'vd.push(vd.currentDirSheet)', 'open Directory Sheet: browse properties of files in current directory')
224
250
 
225
- Sheet.addCommand('z;', 'addcol-sh', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
251
+ Sheet.addCommand('z;', 'addcol-shell', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables')
226
252
 
227
253
  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')
228
254
  DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets')
@@ -243,7 +269,7 @@ def copy_files(sheet, paths, dest):
243
269
  try:
244
270
  destpath = destdir/str(srcpath._path.name)
245
271
  if srcpath.is_dir():
246
- shutil.copy_tree(srcpath, destpath)
272
+ shutil.copytree(srcpath, destpath)
247
273
  else:
248
274
  shutil.copyfile(srcpath, destpath)
249
275
  except Exception as e:
@@ -253,3 +279,7 @@ def copy_files(sheet, paths, dest):
253
279
  vd.addGlobals({
254
280
  'DirSheet': DirSheet
255
281
  })
282
+
283
+ vd.addMenuItems('''
284
+ Column > Add column > shell > addcol-shell
285
+ ''')
visidata/sidebar.py ADDED
@@ -0,0 +1,162 @@
1
+ from typing import Optional, Union
2
+ import textwrap
3
+
4
+ from visidata import vd, VisiData, BaseSheet, colors, TextSheet, clipdraw, wraptext, dispwidth, AttrDict, wrmap
5
+ from visidata import CommandHelpGetter, OptionHelpGetter
6
+
7
+
8
+ vd.option('disp_sidebar', True, 'whether to display sidebar')
9
+ vd.option('disp_sidebar_fmt', '{guide}', 'format string for default sidebar')
10
+ vd.theme_option('disp_sidebar_width', 0, 'max width for sidebar')
11
+ vd.theme_option('disp_sidebar_height', 0, 'max height for sidebar')
12
+ vd.theme_option('color_sidebar', 'black on 114 blue', 'base color of sidebar')
13
+ vd.theme_option('color_sidebar_title', 'black on yellow', 'color of sidebar title')
14
+
15
+ @BaseSheet.property
16
+ def formatter_helpstr(sheet):
17
+ return AttrDict(commands=CommandHelpGetter(type(sheet)),
18
+ options=OptionHelpGetter())
19
+
20
+
21
+ @BaseSheet.property
22
+ def default_sidebar(sheet):
23
+ 'Default to format options.disp_sidebar_fmt. Overridable.'
24
+ fmt = sheet.options.disp_sidebar_fmt
25
+ return sheet.formatString(fmt, help=sheet.formatter_helpstr)
26
+
27
+
28
+ @VisiData.property
29
+ def recentStatusMessages(vd) -> str:
30
+ r = ''
31
+ for (pri, msgparts), n in vd.statuses.items():
32
+ msg = '; '.join(wrmap(str, msgparts))
33
+ msg = f'[{n}x] {msg}' if n > 1 else msg
34
+
35
+ if pri == 3: msgattr = '[:error]'
36
+ elif pri == 2: msgattr = '[:warning]'
37
+ elif pri == 1: msgattr = '[:warning]'
38
+ else: msgattr = ''
39
+
40
+ if msgattr:
41
+ r += '\n' + f'{msgattr}{msg}[/]'
42
+ else:
43
+ r += '\n' + msg
44
+
45
+ if r:
46
+ return '# statuses' + r
47
+
48
+ return ''
49
+
50
+
51
+ @VisiData.api
52
+ def drawSidebar(vd, scr, sheet):
53
+ sidebar = vd.recentStatusMessages
54
+ bottommsg = ''
55
+ overflowmsg = '[:reverse] Ctrl+P to view all status messages [/]'
56
+ 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[/] [:]'
68
+ except Exception as e:
69
+ vd.exceptionCaught(e)
70
+ sidebar = f'# error\n{e}'
71
+
72
+ sheet.current_sidebar = sidebar
73
+
74
+ return sheet.drawSidebarText(scr, text=sheet.current_sidebar, overflowmsg=overflowmsg, bottommsg=bottommsg)
75
+
76
+ @BaseSheet.api
77
+ def drawSidebarText(sheet, scr, text:Union[None,str,'HelpPane'], title:str='', overflowmsg:str='', bottommsg:str=''):
78
+ scrh, scrw = scr.getmaxyx()
79
+ maxw = sheet.options.disp_sidebar_width or scrw//2
80
+ maxh = sheet.options.disp_sidebar_height or scrh-2
81
+
82
+ cattr = colors.get_color('color_sidebar')
83
+
84
+ text = text or ''
85
+
86
+ if hasattr(text, 'draw'): # like a HelpPane
87
+ maxlinew = text.width
88
+ winh = min(maxh, text.height+2)+1
89
+ else:
90
+ text = textwrap.dedent(text.strip('\n'))
91
+
92
+ if not text:
93
+ return
94
+
95
+ lines = text.splitlines()
96
+ if not title and lines and lines[0].strip().startswith('# '):
97
+ title = lines[0].strip()[2:]
98
+ text = '\n'.join(lines[1:])
99
+
100
+ if not text:
101
+ return
102
+
103
+ lines = list(wraptext(text, width=maxw-4))
104
+ maxlinew = 0
105
+ if lines:
106
+ maxlinew = max(maxlinew, max(dispwidth(textonly, maxwidth=maxw) for line, textonly in lines))
107
+ winh = min(maxh, len(lines)+2)
108
+
109
+ titlew = dispwidth(title)
110
+
111
+ maxlinew = max(maxlinew, dispwidth(overflowmsg)+4)
112
+ maxlinew = max(maxlinew, dispwidth(bottommsg)+4)
113
+ maxlinew = max(maxlinew, titlew)
114
+ winw = min(maxw, maxlinew+4)
115
+ x, y, w, h = scrw-winw-1, scrh-winh-1, winw, winh
116
+
117
+ sidebarscr = vd.subwindow(scr, x, y, w, h)
118
+
119
+ sidebarscr.erase()
120
+ sidebarscr.bkgd(' ', cattr.attr)
121
+ sidebarscr.border()
122
+ vd.onMouse(sidebarscr, 0, 0, w, h, BUTTON1_RELEASED='no-op', BUTTON1_PRESSED='no-op')
123
+
124
+ if hasattr(text, 'draw'): # like a HelpPane
125
+ text.draw(sidebarscr, attr=cattr)
126
+ else:
127
+ i = 0
128
+ for line, _ in lines:
129
+ if i >= h-2:
130
+ bottommsg = overflowmsg
131
+ break
132
+
133
+ x += clipdraw(sidebarscr, i+1, 2, line, cattr, w=w-3)
134
+ i += 1
135
+
136
+ x = max(0, w-titlew-6)
137
+ clipdraw(sidebarscr, 0, x, f"|[:sidebar_title] {title} [:]|", cattr, w=titlew+4)
138
+ if bottommsg:
139
+ clipdraw(sidebarscr, h-1, winw-dispwidth(bottommsg)-4, '|'+bottommsg+'|[:]', cattr)
140
+
141
+ sidebarscr.refresh()
142
+
143
+
144
+ @VisiData.api
145
+ class SidebarSheet(TextSheet):
146
+ guide = '''
147
+ # Sidebar Guide
148
+ The sidebar provides additional information about the current sheet, defaulting to a basic guide to the current sheet type.
149
+ It can be configured to show many useful attributes via `options.disp_sidebar_fmt`.
150
+
151
+ - `gb` to open the sidebar in a new sheet
152
+ - `b` to toggle the sidebar on/off for the current sheet
153
+ '''
154
+
155
+ BaseSheet.addCommand('b', 'sidebar-toggle', 'sheet.options.disp_sidebar = not sheet.options.disp_sidebar', 'toggle sidebar on/off')
156
+ 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')
157
+
158
+
159
+ vd.addMenuItems('''
160
+ View > Sidebar > toggle > sidebar-toggle
161
+ View > Sidebar > open in new sheet > open-sidebar
162
+ ''')
visidata/sort.py CHANGED
@@ -36,7 +36,7 @@ class Reversor:
36
36
 
37
37
 
38
38
  @Sheet.api
39
- def sortkey(self, r, prog=None):
39
+ def sortkey(self, r):
40
40
  ret = []
41
41
  for col, reverse in self._ordering:
42
42
  if isinstance(col, str):
@@ -44,8 +44,6 @@ def sortkey(self, r, prog=None):
44
44
  val = col.getTypedValue(r)
45
45
  ret.append(Reversor(val) if reverse else val)
46
46
 
47
- if prog:
48
- prog.addProgress(1)
49
47
 
50
48
  return ret
51
49
 
@@ -58,7 +56,7 @@ def sort(self):
58
56
  try:
59
57
  with Progress(gerund='sorting', total=self.nRows) as prog:
60
58
  # must not reassign self.rows: use .sort() instead of sorted()
61
- self.rows.sort(key=lambda r,self=self,prog=prog: self.sortkey(r, prog=prog))
59
+ self.rows.sort(key=lambda r,self=self,prog=prog: (prog.addProgress(1), self.sortkey(r))[1])
62
60
  except TypeError as e:
63
61
  vd.warning('sort incomplete due to TypeError; change column type')
64
62
  vd.exceptionCaught(e, status=False)
@@ -75,3 +73,12 @@ Sheet.addCommand('z[', 'sort-asc-add', 'orderBy(cursorCol)', 'sort ascending by
75
73
  Sheet.addCommand('z]', 'sort-desc-add', 'orderBy(cursorCol, reverse=True)', 'sort descending by current column; add to existing sort criteria')
76
74
  Sheet.addCommand('gz[', 'sort-keys-asc-add', 'orderBy(*keyCols)', 'sort ascending by all key columns; add to existing sort criteria')
77
75
  Sheet.addCommand('gz]', 'sort-keys-desc-add', 'orderBy(*keyCols, reverse=True)', 'sort descending by all key columns; add to existing sort criteria')
76
+
77
+ vd.addMenuItems('''
78
+ Column > Sort by > current column only > ascending > sort-asc
79
+ Column > Sort by > current column only > descending > sort-desc
80
+ Column > Sort by > current column also > ascending > sort-asc-add
81
+ Column > Sort by > current column also > descending > sort-desc-add
82
+ Column > Sort by > key columns > ascending > sort-keys-asc
83
+ Column > Sort by > key columns > descending > sort-keys-desc
84
+ ''')
visidata/statusbar.py CHANGED
@@ -1,30 +1,42 @@
1
+ '''
2
+ Status messages get added with vd.{debug/aside/status/warning/fail/error}(), and cleared in mainloop
3
+ '''
4
+
5
+ import builtins
1
6
  import collections
2
7
  import curses
8
+ import sys
3
9
 
4
- from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, MissingAttrFormatter
10
+ import visidata
11
+ from visidata import vd, VisiData, BaseSheet, Sheet, ColumnItem, Column, RowColorizer, options, colors, wrmap, clipdraw, ExpectedException, update_attr, dispwidth, ColorAttr
5
12
 
6
13
 
7
- vd.option('disp_rstatus_fmt', ' {sheet.longname} {sheet.nRows:9d} {sheet.rowtype} {sheet.modifiedStatus} {sheet.options.disp_selected_note}{sheet.nSelectedRows}', 'right-side status format string')
8
- vd.option('disp_status_fmt', '{sheet.shortcut}› {sheet.name}| ', 'status line prefix')
9
- vd.option('disp_lstatus_max', 0, 'maximum length of left status line')
10
- vd.option('disp_status_sep', ' ', 'separator between statuses')
14
+ vd.option('disp_rstatus_fmt', '{sheet.threadStatus} {sheet.keystrokeStatus} {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')
16
+ vd.theme_option('disp_lstatus_max', 0, 'maximum length of left status line')
17
+ vd.theme_option('disp_status_sep', '│', 'separator between statuses')
11
18
 
12
- vd.option('color_keystrokes', 'bold 233 black on 110 cyan', 'color of input keystrokes on status line')
13
- vd.option('color_status', 'bold black on 110 cyan', 'status line color')
14
- vd.option('color_error', 'red', 'error message color')
15
- vd.option('color_warning', 'yellow', 'warning message color')
16
- vd.option('color_top_status', 'underline', 'top window status bar color')
17
- vd.option('color_active_status', 'black on 110 cyan', ' active window status bar color')
18
- vd.option('color_inactive_status', '8 on black', 'inactive window status bar color')
19
+ vd.theme_option('color_keystrokes', 'bold white on 237', 'color of input keystrokes on status line')
20
+ vd.theme_option('color_keys', 'bold', 'color of keystrokes in help')
21
+ vd.theme_option('color_status', 'bold on 238', 'status line color')
22
+ vd.theme_option('color_error', '202 1', 'error message color')
23
+ vd.theme_option('color_warning', '166 15', 'warning message color')
24
+ vd.theme_option('color_top_status', 'underline', 'top window status bar color')
25
+ vd.theme_option('color_active_status', 'black on 68 blue', ' active window status bar color')
26
+ vd.theme_option('color_inactive_status', '8 on black', 'inactive window status bar color')
27
+ vd.theme_option('color_highlight_status', 'black on green', 'color of highlighted elements in statusbar')
19
28
 
20
29
  BaseSheet.init('longname', lambda: '')
21
30
 
22
- vd.beforeExecHooks.append(lambda sheet, cmd, args, ks: setattr(sheet, 'longname', cmd.longname))
31
+ @BaseSheet.api
32
+ def _updateStatusBeforeExec(sheet, cmd, args, ks):
33
+ sheet.longname = cmd.longname
34
+ if sheet._scr:
35
+ vd.drawRightStatus(sheet._scr, sheet) #996 show longname during commands
36
+ sheet._scr.refresh()
23
37
 
24
38
 
25
- @BaseSheet.property
26
- def modifiedStatus(sheet):
27
- return ' [M]' if sheet.hasBeenModified else ''
39
+ vd.beforeExecHooks.append(BaseSheet._updateStatusBeforeExec)
28
40
 
29
41
 
30
42
  @VisiData.lazy_property
@@ -36,6 +48,11 @@ def statuses(vd):
36
48
  def statusHistory(vd):
37
49
  return list() # list of [priority, statusmsg, repeats] for all status messages ever
38
50
 
51
+ @VisiData.api
52
+ def getStatusSource(vd):
53
+ return None
54
+
55
+
39
56
  @VisiData.api
40
57
  def status(vd, *args, priority=0):
41
58
  'Display *args* on status until next action.'
@@ -45,17 +62,25 @@ def status(vd, *args, priority=0):
45
62
  k = (priority, tuple(map(str, args)))
46
63
  vd.statuses[k] = vd.statuses.get(k, 0) + 1
47
64
 
48
- return vd.addToStatusHistory(*args, priority=priority)
65
+ source = vd.getStatusSource()
66
+
67
+ if not vd.cursesEnabled:
68
+ msg = '\r' + composeStatus(args)
69
+ if vd.options.debug:
70
+ msg += f' [{source}]'
71
+ builtins.print(msg, file=sys.stderr)
72
+
73
+ return vd.addToStatusHistory(*args, priority=priority, source=source)
49
74
 
50
75
  @VisiData.api
51
- def addToStatusHistory(vd, *args, priority=0):
76
+ def addToStatusHistory(vd, *args, priority=0, source=None):
52
77
  if vd.statusHistory:
53
- prevpri, prevargs, prevn = vd.statusHistory[-1]
78
+ prevpri, prevargs, _, _ = vd.statusHistory[-1]
54
79
  if prevpri == priority and prevargs == args:
55
80
  vd.statusHistory[-1][2] += 1
56
81
  return True
57
82
 
58
- vd.statusHistory.append([priority, args, 1])
83
+ vd.statusHistory.append([priority, args, 1, source])
59
84
  return True
60
85
 
61
86
  @VisiData.api
@@ -75,6 +100,11 @@ def warning(vd, *args):
75
100
  'Display *args* on status as a warning.'
76
101
  vd.status(*args, priority=1)
77
102
 
103
+ @VisiData.api
104
+ def aside(vd, *args, priority=0):
105
+ 'Add a message to statuses without showing the message proactively.'
106
+ return vd.addToStatusHistory(*args, priority=priority, source=vd.getStatusSource())
107
+
78
108
  @VisiData.api
79
109
  def debug(vd, *args, **kwargs):
80
110
  'Display *args* on status if options.debug is set.'
@@ -87,7 +117,7 @@ def middleTruncate(s, w):
87
117
  return s[:w] + options.disp_truncator + s[-w:]
88
118
 
89
119
 
90
- def composeStatus(msgparts, n):
120
+ def composeStatus(msgparts, n=1):
91
121
  msg = '; '.join(wrmap(str, msgparts))
92
122
  if n > 1:
93
123
  msg = '[%sx] %s' % (n, msg)
@@ -97,13 +127,13 @@ def composeStatus(msgparts, n):
97
127
  @BaseSheet.api
98
128
  def leftStatus(sheet):
99
129
  'Return left side of status bar for this sheet. Overridable.'
100
- return options.disp_status_fmt.format(sheet=sheet, vd=vd)
130
+ return sheet.formatString(sheet.options.disp_status_fmt)
101
131
 
102
132
 
103
133
  @VisiData.api
104
134
  def drawLeftStatus(vd, scr, vs):
105
135
  'Draw left side of status bar.'
106
- cattr = colors.get_color('color_status')
136
+ cattr = colors.get_color('color_active_status')
107
137
  active = (vs is vd.activeSheet)
108
138
  if active:
109
139
  cattr = update_attr(cattr, colors.color_active_status, 1)
@@ -113,103 +143,77 @@ def drawLeftStatus(vd, scr, vs):
113
143
  if scr is vd.winTop:
114
144
  cattr = update_attr(cattr, colors.color_top_status, 1)
115
145
 
116
- attr = cattr.attr
117
- error_attr = update_attr(cattr, colors.color_error, 1).attr
118
- warn_attr = update_attr(cattr, colors.color_warning, 2).attr
119
- sep = options.disp_status_sep
120
-
121
146
  x = 0
122
147
  y = vs.windowHeight-1 # status for each window
123
- try:
124
- lstatus = vs.leftStatus()
125
- maxwidth = options.disp_lstatus_max
126
- if maxwidth > 0:
127
- lstatus = middleTruncate(lstatus, maxwidth//2)
128
-
129
- x = clipdraw(scr, y, 0, lstatus, attr, w=vs.windowWidth-1)
130
-
131
- vd.onMouse(scr, y, 0, 1, x,
132
- BUTTON1_PRESSED='sheets-stack',
133
- BUTTON1_RELEASED='sheets-stack',
134
- BUTTON3_PRESSED='rename-sheet',
135
- BUTTON3_CLICKED='rename-sheet')
136
- except Exception as e:
137
- vd.exceptionCaught(e)
138
-
139
- if not active:
140
- return
141
-
142
- one = False
143
- for (pri, msgparts), n in sorted(vd.statuses.items(), key=lambda k: -k[0][0]):
144
- try:
145
- if x > vs.windowWidth:
146
- break
147
- if one: # any messages already:
148
- x += clipdraw(scr, y, x, sep, attr, w=vs.windowWidth-x)
149
- one = True
150
- msg = composeStatus(msgparts, n)
151
-
152
- if pri == 3: msgattr = error_attr
153
- elif pri == 2: msgattr = warn_attr
154
- elif pri == 1: msgattr = warn_attr
155
- else: msgattr = attr
156
- x += clipdraw(scr, y, x, msg, msgattr, w=vs.windowWidth-x)
157
- except Exception as e:
158
- vd.exceptionCaught(e)
148
+ lstatus = vs.leftStatus()
149
+ maxwidth = options.disp_lstatus_max
150
+ if maxwidth > 0:
151
+ lstatus = middleTruncate(lstatus, maxwidth//2)
152
+
153
+ x = clipdraw(scr, y, 0, lstatus, cattr, w=vs.windowWidth-1)
154
+
155
+ vd.onMouse(scr, 0, y, x, 1,
156
+ BUTTON3_PRESSED='rename-sheet',
157
+ BUTTON3_CLICKED='rename-sheet')
159
158
 
160
159
 
161
160
  @VisiData.api
162
161
  def rightStatus(vd, sheet):
163
162
  'Return right side of status bar. Overridable.'
164
- return MissingAttrFormatter().format(sheet.options.disp_rstatus_fmt, sheet=sheet, vd=vd)
163
+ return sheet.formatString(sheet.options.disp_rstatus_fmt)
165
164
 
166
165
 
167
- @VisiData.api
168
- def drawRightStatus(vd, scr, vs):
169
- 'Draw right side of status bar. Return length displayed.'
170
- rightx = vs.windowWidth
171
-
172
- ret = 0
173
- statcolors = [
174
- (vd.rightStatus(vs), 'color_status'),
175
- ]
166
+ @BaseSheet.property
167
+ def keystrokeStatus(vs):
168
+ if vs is vd.activeSheet:
169
+ return f'[:keystrokes]{vd.keystrokes}[/]'
176
170
 
177
- active = vs is vd.activeSheet
171
+ return ''
178
172
 
179
- if active:
180
- statcolors.append((f'{vd.prettykeys(vd.keystrokes)} ' or '', 'color_keystrokes'))
181
173
 
174
+ @BaseSheet.property
175
+ def threadStatus(vs) -> str:
182
176
  if vs.currentThreads:
183
- statcolors.insert(0, vd.checkMemoryUsage())
177
+ ret = str(vd.checkMemoryUsage())
184
178
  gerunds = [p.gerund for p in vs.progresses if p.gerund] or ['processing']
185
- statcolors.insert(1, (' %s %s…' % (vs.progressPct, gerunds[0]), 'color_working'))
186
-
187
- if active and vd.currentReplay:
188
- statcolors.insert(0, (vd.replayStatus, 'color_status_replay'))
189
-
190
- for rstatcolor in statcolors:
191
- if rstatcolor:
192
- try:
193
- rstatus, coloropt = rstatcolor
194
- rstatus = ' '+rstatus
195
- cattr = colors.get_color(coloropt)
196
- if scr is vd.winTop:
197
- cattr = update_attr(cattr, colors.color_top_status, 0)
198
- if active:
199
- cattr = update_attr(cattr, colors.color_active_status, 0)
200
- else:
201
- cattr = update_attr(cattr, colors.color_inactive_status, 0)
202
- statuslen = clipdraw(scr, vs.windowHeight-1, rightx, rstatus, cattr.attr, w=vs.windowWidth-1, rtl=True)
203
- rightx -= statuslen
204
- ret += statuslen
205
- except Exception as e:
206
- vd.exceptionCaught(e)
207
-
208
- if scr:
209
- curses.doupdate()
179
+ ret += f' [:working]{vs.progressPct} {gerunds[0]}…[/]'
180
+ return ret
181
+ return ''
182
+
183
+ @BaseSheet.property
184
+ def modifiedStatus(sheet):
185
+ ret = ' [M]' if sheet.hasBeenModified else ''
186
+ if not vd.couldOverwrite():
187
+ ret += ' [:highlight_status][RO][/] '
210
188
  return ret
211
189
 
212
190
 
191
+ @Sheet.property
192
+ def selectedStatus(sheet):
193
+ if sheet.nSelectedRows:
194
+ return f' [:selected_row][:onclick dup-selected]{sheet.options.disp_selected_note}{sheet.nSelectedRows}[/][/] '
195
+
196
+
197
+ @VisiData.api
198
+ def drawRightStatus(vd, scr, vs):
199
+ 'Draw right side of status bar. Return length displayed.'
200
+ rightx = vs.windowWidth
201
+
202
+ statuslen = 0
203
+ try:
204
+ cattr = ColorAttr()
205
+ if scr is vd.winTop:
206
+ cattr = update_attr(cattr, colors.color_top_status, 0)
207
+ cattr = update_attr(cattr, colors.color_active_status if vs is vd.activeSheet else colors.color_inactive_status, 0)
208
+ rstat = vd.rightStatus(vs)
209
+ x = max(2, rightx-dispwidth(rstat)-1)
210
+ statuslen = clipdraw(scr, vs.windowHeight-1, x, rstat, cattr, w=vs.windowWidth-1)
211
+ finally:
212
+ if scr:
213
+ curses.doupdate()
214
+ return statuslen
215
+
216
+
213
217
  class StatusSheet(Sheet):
214
218
  precious = False
215
219
  rowtype = 'statuses' # rowdef: (priority, args, nrepeats)
@@ -217,7 +221,8 @@ class StatusSheet(Sheet):
217
221
  ColumnItem('priority', 0, type=int, width=0),
218
222
  ColumnItem('nrepeats', 2, type=int, width=0),
219
223
  ColumnItem('args', 1, width=0),
220
- Column('message', getter=lambda col,row: composeStatus(row[1], row[2])),
224
+ Column('message', width=50, getter=lambda col,row: composeStatus(row[1], row[2])),
225
+ ColumnItem('source', 3, max_help=1),
221
226
  ]
222
227
  colorizers = [
223
228
  RowColorizer(1, 'color_error', lambda s,c,r,v: r and r[0] == 3),
@@ -234,3 +239,7 @@ def statusHistorySheet(vd):
234
239
 
235
240
 
236
241
  BaseSheet.addCommand('^P', 'open-statuses', 'vd.push(vd.statusHistorySheet)', 'open Status History')
242
+
243
+ vd.addMenuItems('''
244
+ View > Statuses > open-statuses
245
+ ''')