visidata 2.11.1__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 (255) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  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. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +59 -50
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +5 -1
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +30 -5
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +163 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +75 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +180 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +17 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +48 -10
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +301 -148
  187. visidata/man/vd.txt +290 -153
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +50 -201
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +229 -257
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +113 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/conftest.py +3 -3
  213. visidata/tests/test_cliptext.py +39 -0
  214. visidata/tests/test_commands.py +62 -7
  215. visidata/tests/test_edittext.py +2 -2
  216. visidata/tests/test_features.py +17 -0
  217. visidata/tests/test_menu.py +14 -0
  218. visidata/tests/test_path.py +13 -4
  219. visidata/text_source.py +53 -0
  220. visidata/textsheet.py +10 -3
  221. visidata/theme.py +44 -0
  222. visidata/themes/__init__.py +0 -0
  223. visidata/themes/ascii8.py +84 -0
  224. visidata/themes/asciimono.py +84 -0
  225. visidata/themes/light.py +17 -0
  226. visidata/threads.py +87 -39
  227. visidata/tuiwin.py +22 -0
  228. visidata/type_currency.py +22 -3
  229. visidata/type_date.py +31 -9
  230. visidata/type_floatsi.py +5 -1
  231. visidata/undo.py +17 -5
  232. visidata/utils.py +106 -23
  233. visidata/vdobj.py +28 -17
  234. visidata/windows.py +10 -0
  235. visidata/wrappers.py +9 -3
  236. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  237. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
  238. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
  239. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  240. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
  241. visidata-3.0.dist-info/RECORD +257 -0
  242. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  243. vgit/__init__.py +0 -1
  244. vgit/gitsheet.py +0 -164
  245. visidata/layout.py +0 -44
  246. visidata/misc.py +0 -5
  247. visidata-2.11.1.data/scripts/vgit +0 -9
  248. visidata-2.11.1.dist-info/RECORD +0 -155
  249. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  250. {vgit → visidata/apps/vgit}/abort.py +0 -0
  251. /visidata/{repeat.py → features/repeat.py} +0 -0
  252. {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
  253. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
  255. {visidata-2.11.1.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)
@@ -129,6 +167,11 @@ class Path(os.PathLike):
129
167
  self.filesize = filesize
130
168
  self.rfile = None
131
169
 
170
+ @property
171
+ def name(self):
172
+ 'Filename without any extensions. Not the same as pathlib.Path.'
173
+ return self.base_stem
174
+
132
175
  @lru_cache()
133
176
  def stat(self, force=False):
134
177
  return self._path.stat()
@@ -148,17 +191,17 @@ class Path(os.PathLike):
148
191
 
149
192
  self.ext = self.suffix[1:]
150
193
  if self.suffix: #1450 don't make this a oneliner; [:-0] doesn't work
151
- self.name = self._path.name[:-len(self.suffix)]
194
+ self.base_stem = self._path.name[:-len(self.suffix)]
152
195
  elif self._given == '.': #1768
153
- self.name = '.'
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
 
@@ -256,7 +305,7 @@ class Path(os.PathLike):
256
305
  import lzma
257
306
  zopen = lzma.open
258
307
  elif self.compression == 'zst':
259
- import zstandard
308
+ zstandard = vd.importExternal('zstandard')
260
309
  zopen = zstandard.open
261
310
  else:
262
311
  return FileProgress(path, fp=self._path.open(*args, **kwargs), **kwargs)
@@ -270,7 +319,7 @@ class Path(os.PathLike):
270
319
 
271
320
  def __iter__(self):
272
321
  with Progress(total=filesize(self)) as prog:
273
- with self.open_text(encoding=vd.options.encoding) as fd:
322
+ with self.open(encoding=vd.options.encoding) as fd:
274
323
  for i, line in enumerate(fd):
275
324
  prog.addProgress(len(line))
276
325
  yield line.rstrip('\n')
@@ -279,13 +328,22 @@ class Path(os.PathLike):
279
328
  'Open the file pointed by this path and return a file object in binary mode.'
280
329
  if 'b' not in mode:
281
330
  mode += 'b'
282
- return self.open(mode=mode)
331
+ return self.open(mode=mode) #1880
283
332
 
284
333
  def read_bytes(self):
285
334
  'Return the entire binary contents of the pointed-to file as a bytes object.'
286
335
  with self.open(mode='rb') as fp:
287
336
  return fp.read()
288
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
+
289
347
  def is_url(self):
290
348
  'Return True if the given path appears to be a URL.'
291
349
  return '://' in self.given
@@ -338,22 +396,31 @@ class RepeatFile:
338
396
 
339
397
  def __enter__(self):
340
398
  '''Returns a new independent file-like object, sharing the same line cache.'''
341
- return RepeatFile(self.iter_lines, lines=self.lines)
399
+ return self.reopen()
342
400
 
343
401
  def __exit__(self, a,b,c):
344
402
  pass
345
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
+
346
408
  def read(self, n=None):
347
- r = ''
409
+ r = None
348
410
  if n is None:
349
411
  n = 10**12 # some too huge number
350
- while len(r) < n:
412
+ while r is None or len(r) < n:
351
413
  try:
352
414
  s = next(self.iter)
353
- 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'
354
421
  except StopIteration:
355
422
  break # end of file
356
- return r
423
+ return r or ''
357
424
 
358
425
  def write(self, s):
359
426
  return self.iter_lines.write(s)
@@ -362,9 +429,16 @@ class RepeatFile:
362
429
  '''Tells the current position as an opaque line marker.'''
363
430
  return self.iter.nextIndex
364
431
 
365
- def seek(self, n):
432
+ def seek(self, offset, whence=io.SEEK_SET):
366
433
  '''Seek to an already seen opaque line position marker only.'''
367
- 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
368
442
 
369
443
  def readline(self, size=-1):
370
444
  if size != -1:
@@ -408,3 +482,10 @@ class RepeatFileIter:
408
482
 
409
483
  self.nextIndex += 1
410
484
  return r
485
+
486
+
487
+ vd.addGlobals(RepeatFile=RepeatFile,
488
+ Path=Path,
489
+ modtime=modtime,
490
+ filesize=filesize,
491
+ vstat=vstat)