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/movement.py CHANGED
@@ -6,6 +6,7 @@ from visidata import vd, VisiData, BaseSheet, Sheet, Column, Progress, ALT, asyn
6
6
 
7
7
  def rotateRange(n, idx, reverse=False):
8
8
  'Wraps an iter starting from idx. Yields indices from idx to n and then 0 to idx.'
9
+ if n == 0: return []
9
10
  if reverse:
10
11
  rng = range(idx-1, -1, -1)
11
12
  rng2 = range(n-1, idx-1, -1)
@@ -96,47 +97,35 @@ def nextColRegex(sheet, colregex):
96
97
  def visibleWidth(self):
97
98
  'Width of column as is displayed in terminal'
98
99
  vcolidx = self.sheet.visibleCols.index(self)
100
+ if vcolidx not in self.sheet._visibleColLayout:
101
+ self.sheet.calcSingleColLayout(vcolidx)
99
102
  return self.sheet._visibleColLayout[vcolidx][1]
100
103
 
101
104
 
102
- Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left'),
103
- Sheet.addCommand(None, 'go-down', 'cursorDown(+1)', 'go down'),
104
- Sheet.addCommand(None, 'go-up', 'cursorDown(-1)', 'go up'),
105
- Sheet.addCommand(None, 'go-right', 'cursorRight(+1)', 'go right'),
106
- Sheet.addCommand(None, 'go-pagedown', 'cursorDown(nScreenRows-1); sheet.topRowIndex = bottomRowIndex', 'scroll one page forward'),
107
- Sheet.addCommand(None, 'go-pageup', 'cursorDown(-nScreenRows+1); sheet.bottomRowIndex = topRowIndex', 'scroll one page backward'),
105
+ Sheet.addCommand(None, 'go-left', 'cursorRight(-1)', 'go left')
106
+ Sheet.addCommand(None, 'go-down', 'cursorDown(+1)', 'go down')
107
+ Sheet.addCommand(None, 'go-up', 'cursorDown(-1)', 'go up')
108
+ Sheet.addCommand(None, 'go-right', 'cursorRight(+1)', 'go right')
109
+ Sheet.addCommand(None, 'go-pagedown', 'cursorDown(nScreenRows-1); sheet.topRowIndex = bottomRowIndex', 'scroll one page forward')
110
+ Sheet.addCommand(None, 'go-pageup', 'cursorDown(-nScreenRows+1); sheet.bottomRowIndex = topRowIndex', 'scroll one page backward')
108
111
 
109
- Sheet.addCommand(None, 'go-leftmost', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = 0', 'go all the way to the left of sheet'),
110
- Sheet.addCommand(None, 'go-top', 'sheet.cursorRowIndex = sheet.topRowIndex = 0', 'go all the way to the top of sheet'),
111
- Sheet.addCommand(None, 'go-bottom', 'sheet.cursorRowIndex = len(rows); sheet.topRowIndex = cursorRowIndex-nScreenRows', 'go all the way to the bottom of sheet'),
112
- Sheet.addCommand(None, 'go-rightmost', 'sheet.leftVisibleColIndex = len(visibleCols)-1; pageLeft(); sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go all the way to the right of sheet'),
112
+ Sheet.addCommand(None, 'go-leftmost', 'sheet.cursorVisibleColIndex = sheet.leftVisibleColIndex = 0', 'go all the way to the left of sheet')
113
+ Sheet.addCommand(None, 'go-top', 'sheet.cursorRowIndex = sheet.topRowIndex = 0', 'go all the way to the top of sheet')
114
+ Sheet.addCommand(None, 'go-bottom', 'sheet.cursorRowIndex = len(rows); sheet.topRowIndex = cursorRowIndex-nScreenRows', 'go all the way to the bottom of sheet')
115
+ Sheet.addCommand(None, 'go-rightmost', 'sheet.leftVisibleColIndex = len(visibleCols)-1; pageLeft(); sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go all the way to the right of sheet')
113
116
 
114
- @Sheet.command('BUTTON1_PRESSED', 'go-mouse', 'set cursor to row and column where mouse was clicked')
115
- def go_mouse(sheet):
116
- ridx = sheet.visibleRowAtY(sheet.mouseY)
117
- if ridx is not None:
118
- sheet.cursorRowIndex = ridx
119
- cidx = sheet.visibleColAtX(sheet.mouseX)
120
- if cidx is not None:
121
- sheet.cursorVisibleColIndex = cidx
122
-
123
- Sheet.addCommand(None, 'scroll-mouse', 'sheet.topRowIndex=cursorRowIndex-mouseY+1', 'scroll to mouse cursor location'),
124
-
125
- Sheet.addCommand('ScrollwheelUp', 'scroll-up', 'cursorDown(options.scroll_incr); sheet.topRowIndex += options.scroll_incr', 'scroll one row up'),
126
- Sheet.addCommand('ScrollwheelDown', 'scroll-down', 'cursorDown(-options.scroll_incr); sheet.topRowIndex -= options.scroll_incr', 'scroll one row down'),
127
-
128
- Sheet.addCommand('c', 'go-col-regex', 'sheet.cursorVisibleColIndex=nextColRegex(input("column name regex: ", type="regex-col", defaultLast=True))', 'go to next column with name matching regex')
117
+ Sheet.addCommand('c', 'go-col-regex', 'sheet.cursorVisibleColIndex=nextColRegex(inputRegex("column name regex: ", type="regex-col", defaultLast=True))', 'go to next column with name matching regex')
129
118
  Sheet.addCommand('zc', 'go-col-number', 'sheet.cursorVisibleColIndex = int(input("move to column number: "))', 'go to given column number (0-based)')
130
119
  Sheet.addCommand('zr', 'go-row-number', 'sheet.cursorRowIndex = int(input("move to row number: "))', 'go to the given row number (0-based)')
131
120
 
132
121
 
133
- Sheet.addCommand('<', 'go-prev-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val, reverse=True, msg="no different value up this column")', 'go up current column to next value'),
134
- Sheet.addCommand('>', 'go-next-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val, msg="no different value down this column")', 'go down current column to next value'),
135
- Sheet.addCommand('{', 'go-prev-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row), reverse=True, msg="no previous selected row")', 'go up current column to previous selected row'),
136
- Sheet.addCommand('}', 'go-next-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row), msg="no next selected row") ', 'go down current column to next selected row'),
122
+ Sheet.addCommand('<', 'go-prev-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val, reverse=True, msg="no different value up this column")', 'go up current column to next value')
123
+ Sheet.addCommand('>', 'go-next-value', 'moveToNextRow(lambda row,sheet=sheet,col=cursorCol,val=cursorTypedValue: col.getTypedValue(row) != val, msg="no different value down this column")', 'go down current column to next value')
124
+ Sheet.addCommand('{', 'go-prev-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row), reverse=True, msg="no previous selected row")', 'go up current column to previous selected row')
125
+ Sheet.addCommand('}', 'go-next-selected', 'moveToNextRow(lambda row,sheet=sheet: sheet.isSelected(row), msg="no next selected row") ', 'go down current column to next selected row')
137
126
 
138
- Sheet.addCommand('z<', 'go-prev-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row)), reverse=True, msg="no null up this column")', 'go up current column to next null value'),
139
- Sheet.addCommand('z>', 'go-next-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row)), msg="no null down this column")', 'go down current column to next null value'),
127
+ Sheet.addCommand('z<', 'go-prev-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row)), reverse=True, msg="no null up this column")', 'go up current column to next null value')
128
+ Sheet.addCommand('z>', 'go-next-null', 'moveToNextRow(lambda row,col=cursorCol,isnull=isNullFunc(): isnull(col.getValue(row)), msg="no null down this column")', 'go down current column to next null value')
140
129
 
141
130
  for i in range(1, 11):
142
131
  BaseSheet.addCommand(ALT+str(i)[-1], 'jump-sheet-'+str(i), f'vd.push(*(list(s for s in allSheets if s.shortcut==str({i})) or fail("no sheet")))', f'jump to sheet {i}')
@@ -181,9 +170,8 @@ Sheet.addCommand('gzk', 'scroll-cells-top', 'cursorCol.voffset = 0', 'scroll dis
181
170
  Sheet.addCommand(None, 'go-end', 'sheet.cursorRowIndex = len(rows)-1; sheet.cursorVisibleColIndex = len(visibleCols)-1', 'go to last row and last column')
182
171
  Sheet.addCommand(None, 'go-home', 'sheet.topRowIndex = sheet.cursorRowIndex = 0; sheet.leftVisibleColIndex = sheet.cursorVisibleColIndex = 0', 'go to first row and first column')
183
172
 
184
- BaseSheet.bindkey('CTRL-BUTTON4_PRESSED', 'scroll-left')
185
- BaseSheet.bindkey('CTRL-REPORT_MOUSE_POSITION', 'scroll-right')
186
- BaseSheet.bindkey('CTRL-2097152', 'scroll-right')
173
+ BaseSheet.bindkey('Ctrl+ScrollUp', 'scroll-left')
174
+ BaseSheet.bindkey('Ctrl+ScrollDown', 'scroll-right')
187
175
 
188
176
  BaseSheet.bindkey('zKEY_UP', 'scroll-up')
189
177
  BaseSheet.bindkey('zKEY_DOWN', 'scroll-down')
@@ -208,10 +196,30 @@ BaseSheet.bindkey('gl', 'go-rightmost')
208
196
  BaseSheet.addCommand('^^', 'jump-prev', 'vd.activeStack[1:] or fail("no previous sheet"); vd.push(vd.activeStack[1])', 'jump to previous sheet in this pane')
209
197
  BaseSheet.addCommand('g^^', 'jump-first', 'vd.push(vd.activeStack[-1])', 'jump to first sheet')
210
198
 
211
- BaseSheet.addCommand('BUTTON1_RELEASED', 'no-op', 'pass')
199
+ BaseSheet.addCommand('BUTTON1_RELEASED', 'no-op', 'pass', 'do nothing')
212
200
 
213
201
  BaseSheet.addCommand(None, 'mouse-enable', 'mm, _ = curses.mousemask(-1); status("mouse "+("ON" if mm else "OFF"))', 'enable mouse events')
214
202
  BaseSheet.addCommand(None, 'mouse-disable', 'mm, _ = curses.mousemask(0); status("mouse "+("ON" if mm else "OFF"))', 'disable mouse events')
215
203
 
216
204
 
217
205
  vd.addGlobals({'rotateRange': rotateRange})
206
+
207
+ vd.addMenuItems('''
208
+ View > Other sheet > previous sheet > jump-prev
209
+ View > Other sheet > first sheet > jump-first
210
+ Column > Goto > by number > go-col-number
211
+ Column > Goto > by name > go-col-regex
212
+ Row > Goto > top > go-top
213
+ Row > Goto > bottom > go-bottom
214
+ Row > Goto > previous > page > go-pageup
215
+ Row > Goto > previous > null > go-prev-null
216
+ Row > Goto > previous > value > go-prev-value
217
+ Row > Goto > previous > selected > go-prev-selected
218
+ Row > Goto > next > page > go-pagedown
219
+ Row > Goto > next > null > go-next-null
220
+ Row > Goto > next > value > go-next-value
221
+ Row > Goto > next > selected > go-next-selected
222
+ Row > Goto > by number > go-row-number
223
+ View > Other sheet > previous sheet > jump-prev
224
+ View > Other sheet > first sheet > jump-first
225
+ ''')
@@ -0,0 +1,99 @@
1
+ from visidata import vd, VisiData, BaseSheet, Sheet, Column, AttrColumn, CellColorizer, Option
2
+
3
+
4
+ @BaseSheet.lazy_property
5
+ def optionsSheet(sheet):
6
+ return OptionsSheet(sheet.name+"_options", source=sheet)
7
+
8
+ @VisiData.lazy_property
9
+ def globalOptionsSheet(vd):
10
+ return OptionsSheet('global_options', source='global')
11
+
12
+
13
+ @VisiData.api
14
+ class OptionsSheet(Sheet):
15
+ _rowtype = Option # rowdef: Option
16
+ rowtype = 'options'
17
+ precious = False
18
+ columns = (
19
+ Column('option', getter=lambda col,row: row.name),
20
+ Column('module', getter=lambda col,row: row.module, max_help=1),
21
+ Column('value',
22
+ getter=lambda col,row: col.sheet.diffOption(row.name),
23
+ setter=lambda col,row,val: col.sheet.source.options.set(row.name, val)
24
+ ),
25
+ Column('default', getter=lambda col,row: vd.options.getdefault(row.name)),
26
+ Column('description', width=40, getter=lambda col,row: vd.options._get(row.name, 'default').helpstr),
27
+ AttrColumn('replayable', max_help=1),
28
+ )
29
+ colorizers = [
30
+ CellColorizer(3, None, lambda s,c,r,v: v.value if r and c in s.columns[2:4] and r.name.startswith('color_') else None),
31
+ ]
32
+ nKeys = 2
33
+
34
+ @property
35
+ def guide(self):
36
+ if self.source == 'global':
37
+ r = '# Global Options\nThis is a list of global option settings.'
38
+ else:
39
+ r = '# Sheet Options\nThis is a list of option settings specifically for the current sheet.'
40
+
41
+ r += f'\n\n- `e` to edit/toggle the current option value'
42
+ r += '\n- `d` to restore option to builtin default'
43
+ return r
44
+
45
+ def diffOption(self, optname):
46
+ return vd.options.getonly(optname, self.source, '')
47
+
48
+ def editOption(self, row):
49
+ currentValue = vd.options.getobj(row.name, self.source)
50
+ vd.addUndo(vd.options.set, row.name, currentValue, self.source)
51
+ if isinstance(row.value, bool):
52
+ vd.options.set(row.name, not currentValue, self.source)
53
+ else:
54
+ helpstr = f'# options.{self.cursorRow.name}'
55
+ opt = vd.options._get(self.cursorRow.name, 'default')
56
+ if opt.helpstr:
57
+ x = getattr(vd, 'help_'+opt.helpstr, opt.helpstr or '')
58
+ helpstr += (x or '').strip()
59
+ if opt.extrahelp:
60
+ helpstr += '\n'+opt.extrahelp.strip()
61
+ valcolidx = self.visibleCols.index(self.column(self.valueColName))
62
+ v = self.editCell(valcolidx, value=currentValue, help=helpstr)
63
+ vd.options.set(row.name, v, self.source)
64
+
65
+ @property
66
+ def valueColName(self):
67
+ return 'global_value' if self.source == 'global' else 'sheet_value'
68
+
69
+ def beforeLoad(self):
70
+ super().beforeLoad()
71
+ self.columns[2].name = self.valueColName
72
+
73
+ def iterload(self):
74
+ for k in vd.options.keys():
75
+ v = vd.options._get(k)
76
+ # if vd.options.disp_help > v.max_help:
77
+ # continue
78
+ if v.sheettype in [None, BaseSheet]:
79
+ yield v
80
+ elif self.source != 'global' and v.sheettype in self.source.superclasses():
81
+ yield v
82
+
83
+ def newRow(self):
84
+ vd.fail('adding rows to the options sheet is not supported.')
85
+
86
+
87
+ BaseSheet.addCommand('O', 'options-global', 'vd.push(vd.globalOptionsSheet)', 'open Options Sheet: edit global options (apply to all sheets)')
88
+
89
+ BaseSheet.addCommand('zO', 'options-sheet', 'vd.push(sheet.optionsSheet)', 'open Options Sheet: edit sheet options (apply to current sheet only)')
90
+
91
+ OptionsSheet.addCommand('d', 'unset-option', 'options.unset(cursorRow.name, str(source))', 'remove option override for this context')
92
+ OptionsSheet.addCommand(None, 'edit-option', 'editOption(cursorRow)', 'edit option at current row')
93
+ OptionsSheet.bindkey('e', 'edit-option')
94
+ OptionsSheet.bindkey('Enter', 'edit-option')
95
+
96
+ vd.addMenuItems('''
97
+ File > Options > all sheets > options-global
98
+ File > Options > this sheet > options-sheet
99
+ ''')
visidata/path.py CHANGED
@@ -7,10 +7,46 @@ import pathlib
7
7
  from urllib.parse import urlparse, urlunparse
8
8
  from functools import wraps, lru_cache
9
9
 
10
- from visidata import *
10
+ from visidata import vd
11
+ from visidata import VisiData, Progress
11
12
 
12
- vd.option('encoding', 'utf-8', 'encoding passed to codecs.open', replay=True)
13
- vd.option('encoding_errors', 'surrogateescape', 'encoding_errors passed to codecs.open', replay=True)
13
+ vd.help_encoding = '''Common Encodings:
14
+
15
+ - `utf-8`: Unicode (ASCII compatible, most common)
16
+ - `utf-8-sig`: Unicode as above, but saves/skips leading BOM
17
+ - `ascii`: 7-bit ASCII
18
+ - `latin1`: also known as `iso-8859-1`
19
+ - `cp437`: original IBM PC character set
20
+ - `shift_jis`: Japanese
21
+
22
+ See [:onclick https://docs.python.org/3/library/codecs.html#standard-encodings]https://docs.python.org/3/library/codecs.html#standard-encodings[/]
23
+ '''
24
+
25
+ vd.help_encoding_errors = '''Encoding Error Handlers:
26
+
27
+ - `strict`: raise error
28
+ - `ignore`: discard
29
+ - `replace`: replacement marker
30
+ - `backslashreplace`: use "\\uxxxxxx"
31
+ - `surrogateescape`: use surrogate characters
32
+
33
+ See [:onclick https://docs.python.org/3/library/codecs.html#error-handlers]https://docs.python.org/3/library/codecs.html#error-handlers[/]
34
+ '''
35
+
36
+ vd.option('encoding', 'utf-8-sig', 'encoding passed to codecs.open when reading a file', replay=True, help=vd.help_encoding)
37
+ vd.option('encoding_errors', 'surrogateescape', 'encoding_errors passed to codecs.open', replay=True, help=vd.help_encoding_errors)
38
+
39
+ @VisiData.api
40
+ def pkg_resources_files(vd, package):
41
+ '''
42
+ Returns a Traversable object (Path-like), based on the location of the package.
43
+ importlib.resources.files exists in Python >= 3.9; use importlib_resources for the rest.
44
+ '''
45
+ try:
46
+ from importlib.resources import files
47
+ except ImportError: #1968
48
+ from importlib_resources import files
49
+ return files(package)
14
50
 
15
51
  @lru_cache()
16
52
  def vstat(path, force=False):
@@ -22,7 +58,9 @@ def vstat(path, force=False):
22
58
  def filesize(path):
23
59
  if hasattr(path, 'filesize') and path.filesize is not None:
24
60
  return path.filesize
25
- if path.has_fp() or path.is_url():
61
+ if hasattr(path, 'is_url') and path.is_url():
62
+ return 0
63
+ if hasattr(path, 'has_fp') and path.has_fp():
26
64
  return 0
27
65
  st = path.stat() # vstat(path)
28
66
  return st and st.st_size
@@ -38,8 +76,8 @@ class BytesIOWrapper(io.BufferedReader):
38
76
 
39
77
  def __init__(self, text_io_buffer, encoding=None, errors=None, **kwargs):
40
78
  super(BytesIOWrapper, self).__init__(text_io_buffer, **kwargs)
41
- self.encoding = encoding or text_io_buffer.encoding or options.encoding
42
- self.errors = errors or text_io_buffer.errors or options.encoding_errors
79
+ self.encoding = encoding or text_io_buffer.encoding or vd.options.encoding
80
+ self.errors = errors or text_io_buffer.errors or vd.options.encoding_errors
43
81
 
44
82
  def _encoding_call(self, method_name, *args, **kwargs):
45
83
  raw_method = getattr(self.raw, method_name)
@@ -74,11 +112,9 @@ class FileProgress:
74
112
  # track Progress on original fp
75
113
  self.fp_orig_read = self.fp.read
76
114
  self.fp_orig_close = self.fp.close
77
- # These two lines result in bug #1159, a corrupted save of corruption formats
78
- # for now we are reverting by commenting out, and opened #1175 to investigate
79
- # Progress bars for compression formats might not work in the meanwhile
80
- #self.fp.read = self.read
81
- #self.fp.close = self.close
115
+
116
+ self.fp.read = self.read
117
+ self.fp.close = self.close
82
118
 
83
119
  if self.prog:
84
120
  self.prog.__enter__()
@@ -131,6 +167,11 @@ class Path(os.PathLike):
131
167
  self.filesize = filesize
132
168
  self.rfile = None
133
169
 
170
+ @property
171
+ def name(self):
172
+ 'Filename without any extensions. Not the same as pathlib.Path.'
173
+ return self.base_stem
174
+
134
175
  @lru_cache()
135
176
  def stat(self, force=False):
136
177
  return self._path.stat()
@@ -150,15 +191,17 @@ class Path(os.PathLike):
150
191
 
151
192
  self.ext = self.suffix[1:]
152
193
  if self.suffix: #1450 don't make this a oneliner; [:-0] doesn't work
153
- self.name = self._path.name[:-len(self.suffix)]
194
+ self.base_stem = self._path.name[:-len(self.suffix)]
195
+ elif self._given == '.': #1768
196
+ self.base_stem = self._path.absolute().name
154
197
  else:
155
- self.name = self._path.name
198
+ self.base_stem = self._path.name
156
199
 
157
200
  # check if file is compressed
158
201
  if self.suffix in ['.gz', '.bz2', '.xz', '.lzma', '.zst']:
159
202
  self.compression = self.ext
160
203
  uncompressedpath = Path(self.given[:-len(self.suffix)]) # strip suffix
161
- self.name = uncompressedpath.name
204
+ self.base_stem = uncompressedpath.base_stem
162
205
  self.ext = uncompressedpath.ext
163
206
  else:
164
207
  self.compression = None
@@ -190,23 +233,26 @@ class Path(os.PathLike):
190
233
  'Return True if this is a virtual Path to an already open file.'
191
234
  return bool(self.fp or self.fptext)
192
235
 
193
- def open_text(self, mode='rt', encoding=None, encoding_errors=None, newline=None):
194
- 'Open path in text mode, using options.encoding and options.encoding_errors. Return open file-pointer or file-pointer-like.'
236
+ def open(self, mode='rt', encoding=None, encoding_errors=None, newline=None):
237
+ 'Open path in text or binary mode, using options.encoding and options.encoding_errors. Return open file-pointer or file-pointer-like.'
195
238
  # rfile makes a single-access fp reusable
196
239
 
197
240
  if self.rfile:
198
- return self.rfile
241
+ if 'b' in mode:
242
+ raise ValueError('a RepeatFile holds text and cannot be reopened in binary mode')
243
+ return self.rfile.reopen()
199
244
 
200
245
  if self.fp:
201
- self.fptext = codecs.iterdecode(self.fp,
202
- encoding=encoding or options.encoding,
203
- errors=encoding_errors or options.encoding_errors)
246
+ if 'b' not in mode:
247
+ self.fptext = codecs.iterdecode(self.fp,
248
+ encoding=encoding or vd.options.encoding,
249
+ errors=encoding_errors or vd.options.encoding_errors)
204
250
 
205
251
  if self.fptext:
206
252
  self.rfile = RepeatFile(self.fptext)
207
253
  return self.rfile
208
254
 
209
- if 't' not in mode:
255
+ if 't' not in mode and 'b' not in mode:
210
256
  mode += 't'
211
257
 
212
258
  if self.given == '-':
@@ -216,18 +262,21 @@ class Path(os.PathLike):
216
262
  # convert 'a' to 'w' for stdout: https://bugs.python.org/issue27805
217
263
  return open(os.dup(vd._stdout.fileno()), 'wt')
218
264
  else:
219
- vd.error('invalid mode "%s" for Path.open_text()' % mode)
265
+ vd.error('invalid mode "%s" for Path.open()' % mode)
220
266
  return sys.stderr
221
267
 
222
- return self.open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors, newline=newline)
268
+ if 'b' in mode:
269
+ return self._open(mode=mode)
270
+ else:
271
+ return self._open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors, newline=newline)
223
272
 
224
273
  @wraps(pathlib.Path.read_text)
225
274
  def read_text(self, *args, **kwargs):
226
275
  'Open the file in text mode and return its entire decoded contents.'
227
276
  if 'encoding' not in kwargs:
228
- kwargs['encoding'] = options.encoding
277
+ kwargs['encoding'] = vd.options.encoding
229
278
  if 'errors' not in kwargs:
230
- kwargs['errors'] = kwargs.get('encoding_errors', options.encoding_errors)
279
+ kwargs['errors'] = kwargs.get('encoding_errors', vd.options.encoding_errors)
231
280
 
232
281
  if self.lines:
233
282
  return RepeatFile(self.lines).read()
@@ -237,7 +286,7 @@ class Path(os.PathLike):
237
286
  return self._path.read_text(*args, **kwargs)
238
287
 
239
288
  @wraps(pathlib.Path.open)
240
- def open(self, *args, **kwargs):
289
+ def _open(self, *args, **kwargs):
241
290
  if self.fp:
242
291
  return FileProgress(self, fp=self.fp, **kwargs)
243
292
 
@@ -245,25 +294,32 @@ class Path(os.PathLike):
245
294
  return FileProgress(self, fp=BytesIOWrapper(self.fptext), **kwargs)
246
295
 
247
296
  path = self
248
- binmode = 'wb' if 'w' in kwargs.get('mode', '') else 'rb'
297
+
249
298
  if self.compression == 'gz':
250
299
  import gzip
251
- return gzip.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs)
300
+ zopen = gzip.open
252
301
  elif self.compression == 'bz2':
253
302
  import bz2
254
- return bz2.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs)
303
+ zopen = bz2.open
255
304
  elif self.compression in ['xz', 'lzma']:
256
305
  import lzma
257
- return lzma.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs)
306
+ zopen = lzma.open
258
307
  elif self.compression == 'zst':
259
- import zstandard
260
- return zstandard.open(FileProgress(path, fp=open(path, mode=binmode), **kwargs), *args, **kwargs)
308
+ zstandard = vd.importExternal('zstandard')
309
+ zopen = zstandard.open
261
310
  else:
262
311
  return FileProgress(path, fp=self._path.open(*args, **kwargs), **kwargs)
263
312
 
313
+ if 'w' in kwargs.get('mode', ''):
314
+ #1159 FileProgress on the outside to close properly when writing
315
+ return FileProgress(path, fp=zopen(path, **kwargs), **kwargs)
316
+
317
+ #1255 FileProgress on the inside to track uncompressed bytes when reading
318
+ return zopen(FileProgress(path, fp=open(path, mode='rb'), **kwargs), **kwargs)
319
+
264
320
  def __iter__(self):
265
321
  with Progress(total=filesize(self)) as prog:
266
- with self.open_text(encoding=vd.options.encoding) as fd:
322
+ with self.open(encoding=vd.options.encoding) as fd:
267
323
  for i, line in enumerate(fd):
268
324
  prog.addProgress(len(line))
269
325
  yield line.rstrip('\n')
@@ -272,13 +328,22 @@ class Path(os.PathLike):
272
328
  'Open the file pointed by this path and return a file object in binary mode.'
273
329
  if 'b' not in mode:
274
330
  mode += 'b'
275
- return self.open(mode=mode)
331
+ return self.open(mode=mode) #1880
276
332
 
277
333
  def read_bytes(self):
278
334
  'Return the entire binary contents of the pointed-to file as a bytes object.'
279
335
  with self.open(mode='rb') as fp:
280
336
  return fp.read()
281
337
 
338
+ @wraps(pathlib.Path.is_fifo)
339
+ def is_fifo(self):
340
+ 'Return True if the path is a file.'
341
+ return self._path.is_fifo()
342
+
343
+ def is_local(self):
344
+ 'Return True if self.filename refers to a file on the local disk.'
345
+ return not bool(self.fp) and not bool(self.fptext)
346
+
282
347
  def is_url(self):
283
348
  'Return True if the given path appears to be a URL.'
284
349
  return '://' in self.given
@@ -315,10 +380,10 @@ class Path(os.PathLike):
315
380
  'Return a sibling Path with *name* as a filename in the same directory.'
316
381
  if self.is_url():
317
382
  urlparts = list(urlparse(self.given))
318
- urlparts[2] = '/'.join(Path(urlparts[2])._parts[1:-1] + [name])
383
+ urlparts[2] = '/'.join(list(Path(urlparts[2]).parts[1:-1]) + [name])
319
384
  return Path(urlunparse(urlparts))
320
385
  else:
321
- return Path(self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]))
386
+ return Path(self._from_parsed_parts(self._drv, self._root, list(self.parts[:-1]) + [name]))
322
387
 
323
388
 
324
389
  class RepeatFile:
@@ -331,22 +396,31 @@ class RepeatFile:
331
396
 
332
397
  def __enter__(self):
333
398
  '''Returns a new independent file-like object, sharing the same line cache.'''
334
- return RepeatFile(self.iter_lines, lines=self.lines)
399
+ return self.reopen()
335
400
 
336
401
  def __exit__(self, a,b,c):
337
402
  pass
338
403
 
404
+ def reopen(self):
405
+ 'Return copy of file-like with internal iterator reset.'
406
+ return RepeatFile(self.iter_lines, lines=self.lines)
407
+
339
408
  def read(self, n=None):
340
- r = ''
409
+ r = None
341
410
  if n is None:
342
411
  n = 10**12 # some too huge number
343
- while len(r) < n:
412
+ while r is None or len(r) < n:
344
413
  try:
345
414
  s = next(self.iter)
346
- r += s + '\n'
415
+ if r is None:
416
+ r = '' if isinstance(s, str) else b''
417
+ else:
418
+ assert isinstance(r, type(s)), (r, type(s))
419
+
420
+ r += s + '\n' if isinstance(s, str) else b'\n'
347
421
  except StopIteration:
348
422
  break # end of file
349
- return r
423
+ return r or ''
350
424
 
351
425
  def write(self, s):
352
426
  return self.iter_lines.write(s)
@@ -355,9 +429,16 @@ class RepeatFile:
355
429
  '''Tells the current position as an opaque line marker.'''
356
430
  return self.iter.nextIndex
357
431
 
358
- def seek(self, n):
432
+ def seek(self, offset, whence=io.SEEK_SET):
359
433
  '''Seek to an already seen opaque line position marker only.'''
360
- self.iter.nextIndex = n
434
+ if whence != io.SEEK_SET and offset != 0:
435
+ if whence == io.SEEK_CUR:
436
+ raise io.UnsupportedOperation("can't do nonzero cur-relative seeks")
437
+ elif whence == io.SEEK_END:
438
+ raise io.UnsupportedOperation("can't do nonzero end-relative seeks")
439
+ else:
440
+ raise ValueError('invalid whence (%s, should be %s, %s or %s)' % (whence, io.SEEK_SET, io.SEEK_CUR, io.SEEK_END))
441
+ self.iter.nextIndex = offset
361
442
 
362
443
  def readline(self, size=-1):
363
444
  if size != -1:
@@ -401,3 +482,10 @@ class RepeatFileIter:
401
482
 
402
483
  self.nextIndex += 1
403
484
  return r
485
+
486
+
487
+ vd.addGlobals(RepeatFile=RepeatFile,
488
+ Path=Path,
489
+ modtime=modtime,
490
+ filesize=filesize,
491
+ vstat=vstat)