diff --git a/CHANGELOG.md b/CHANGELOG.md index 4091e78d8..6ae404831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # VisiData version history +# v2.10.2 (2022-10-XX) + +- add .vdx, a simplified new cmdlog format +- add `-N`/`--nothing` command to disable loading .visidatarc and plugin addons +- add `addcol-aggr` to add an aggregator column to the **FreqTable** without needing to + regenerate it (requested by @geekscrapy #1541) + +## Improvements + +- [cli] load commandline file arguments from the start (requested by @reagle #1471) +- [cli] `--config=''` now does not try to load any config +- [open] rename `zo` `open-cell` command to `open-cell-file` +- [loaders whl] load python .whl (reported by @frosencrantz #1539) + +## Bugfixes + +- [cli] fix for empty arg +- [DirSheet] fix bug where `Enter` no longer opened a file from the **DirSheet** (reported by @frosencrantz #1527) +- [input paste] fix pasting via a Path via `Ctrl+Y` into input (reported by @frosencrantz #1546) +- [menu] allow VisiData to run without menu +- [mouse] catch any curses.getmouse() errors (reported by @geekscrapy #1553) +- [performance] allow vd to be truly idle (reported by WizzardUU #1532) +- [plugins_autoload] catch error for environment having invalid package metadata (reported by @jsdealy #1529) +- [plugins_autoload] catch exception if plugin fails to load +- [plugins-autoload] fix check for if plugins_autoload is set in args +- [plugins-autoload] update for importlib-metadata 5.0 API (reported by @jkerhin #1550) +- [pyobj] undo rename of `open-row`/`open-cell` (were renamed to `open-X-pyobj`) (revert of eff9833e6A) +- [sheets] ensure IndexSheets are precious, and that **SheetsSheet** is not (reported by @frosencrantz #1547) +- [unzip-http] extracting a file now checks for overwrite (reported by @frosencrantz #1452) +- [windows clipboard] fix piping to clip command through stdin (thanks @daviewales for the fix; reported by @pshangov #1431) + +## API + +- expose `CommandLogBase` (was `_CommandLog`) +- [options] allow FooSheet.options instead of .class_options +- add seperate non-async `select_row`, `toggle_row`, and `unselect_row` for selection of single rows +- the before/after decorators now do not fail if api functions they are decorating do not already exist + # v2.10.1 (2022-09-14) ## Improvements diff --git a/README.md b/README.md index 083e15e22..1b25f7e84 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# VisiData v2.10.1 [![twitter @VisiData][1.1]][1] [![CircleCI](https://circleci.com/gh/saulpw/visidata/tree/stable.svg?style=svg)](https://circleci.com/gh/saulpw/visidata/tree/stable) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata) +# VisiData v2.10.2 [![twitter @VisiData][1.1]][1] [![CircleCI](https://circleci.com/gh/saulpw/visidata/tree/stable.svg?style=svg)](https://circleci.com/gh/saulpw/visidata/tree/stable) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/saulpw/visidata) A terminal interface for exploring and arranging tabular data. diff --git a/dev/checklists/release.md b/dev/checklists/release.md index 461d820d6..db15f3b42 100644 --- a/dev/checklists/release.md +++ b/dev/checklists/release.md @@ -30,14 +30,25 @@ b. update the date in the manpage; - c. update version number on README and front page of website (update dates here too); + c. update version number on README d. bump version in `__version__` in source code (visidata/main.py, visidata/__init__.py) and setup.py; 6. Run dev/mkman.sh to build the manpage and updated website - Run ./mkmanhtml.sh, and move that to visidata.org:site/docs/man, and to visidata:docs/man.md -7. Push `develop` to testpypi +7. Merge `develop` to stable + +8. Merge `stable` back into other branches + + a. if the branch works with minimal conflicts, keep the branch + + b. otherwise, clean out the branch + + +9. Push code to stable + +10. Push `develop` to pypi a. set up a ~/.pypirc @@ -57,69 +68,38 @@ password: ``` - b. push to testpypi + Push to pypi ``` python3 setup.py sdist bdist_wheel --universal - twine upload dist/* -r testpypi + twine upload dist/* ``` -8. Test install from testpypi - - a. on virgin instance - - b. on mac and linux - - c. See if windows works - - d. from git clone - - ``` - pip3 install -i https://test.pypi.org/simple/ visidata - ``` - -9. Merge `develop` to stable - -10. Merge `stable` back into other branches - - a. if the branch works with minimal conflicts, keep the branch - - b. otherwise, clean out the branch - - -11. Push code to stable - -12. Push stable to pypi - -``` -twine upload dist/* -``` - -13. Test install/upgrade from pypi +11. Test install/upgrade from pypi a. Build and deploy the website b. Ask someone else to test install -14. Create a tag `v#.#.#` for that commit +12. Create a tag `v#.#.#` for that commit ``` git tag v#.#.# git push --tags ``` -15. Write up the release notes and add it to `www/releases.md`. Add it to index.md. +13. Write up the release notes and add it to `www/releases.md`. Add it to index.md. -16. Upload new motd +14. Upload new motd -17. Update the website by pushing to master. Update with new manpage. Update redirect to point to new manpage. +15. Update the website by pushing to master. Update with new manpage. Update redirect to point to new manpage. - release notes -18. Comb through issues and close the ones that have been solved, referencing the version number +16. Comb through issues and close the ones that have been solved, referencing the version number -19. Post github release notes on patreon. +17. Post github release notes on patreon. -20. Update the other distributions. +18. Update the other distributions. # conda diff --git a/docs/api/options.rst b/docs/api/options.rst index a8856c8c3..4a4eb9d8b 100644 --- a/docs/api/options.rst +++ b/docs/api/options.rst @@ -35,16 +35,24 @@ Options can be overridden globally, or for all sheets of a specific type, or onl The options context should be referenced directly when setting: - ``sheet.options`` to *set* an option on a specific sheet instance (**sheet override**). - - ``.class_options`` to *set* a option default for a particular type of Sheet (**class override**). + - ``.options`` to *set* a option default for a particular type of Sheet (**class override**). - ``vd.options`` (or plain ``options``) to *set* an option globally for all sheets, except for sheets that have a sheet override (**global override**). +.. note:: + + Class overrides prior to v2.10 + ------------------------------ + + In VisiData v2.10 and earlier, class overrides had to be specified using ``.class_options``. + In subsequent versions, ``.options`` can be used for getting and setting options at all levels. + Use ``sheet.options`` to *get* an option within the context of a specific sheet. This is strongly preferred, so the user can override the option setting on a sheet-specific basis. However, some options and situations are truly sheet-agnostic, and so ``vd.options`` (or plain ``options``) will *get* an option using the context of the **top sheet**. When getting an option value, VisiData will look for a sheet override first, then class overrides next (from most specific subclass all the way up to BaseSheet), then a global override, before returning the default value from the option definition itself. -In general, plugins should use ``FooSheet.class_options`` to override values for the plugin-specific sheet type. +In general, plugins should use ``FooSheet.options`` to override values for the plugin-specific sheet type. .. note:: @@ -118,4 +126,4 @@ Examples sheet.options.color_current_row = 'bold blue' # option set for all DirSheets - DirSheet.class_options.color_current_row = 'reverse green' + DirSheet.options.color_current_row = 'reverse green' diff --git a/setup.py b/setup.py index e441bbafe..ee73dfa81 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup # tox can't actually run python3 setup.py: https://github.com/tox-dev/tox/issues/96 #from visidata import __version__ -__version__ = '2.10.1' +__version__ = '2.10.2' setup(name='visidata', version=__version__, diff --git a/tests/diff-join.vd b/tests/diff-join.vd index e62a81872..5ce90add6 100644 --- a/tests/diff-join.vd +++ b/tests/diff-join.vd @@ -3,7 +3,7 @@ sheet col row longname input keystrokes comment open-file tests/data2.tsv o data1 Key key-col ! data1 sheets-stack S -sheets キdata2 open-row-pyobj ^J +sheets キdata2 open-row ^J data2 Key key-col ! data2 sheets-stack S sheets キdata2 select-row s diff --git a/tests/edit-joinkey-2.vd b/tests/edit-joinkey-2.vd index 39d7da314..8a58587f0 100644 --- a/tests/edit-joinkey-2.vd +++ b/tests/edit-joinkey-2.vd @@ -16,4 +16,4 @@ data2+data1 A key-col ! data2+data1 Key キ2 edit-cell 4 e data2+data1 reload-sheet ^R data2 sheets-stack S -sheets キdata1 open-row-pyobj ^J +sheets キdata1 open-row ^J diff --git a/tests/edit-joinregular-2.vd b/tests/edit-joinregular-2.vd index b908118e2..abc93cb86 100644 --- a/tests/edit-joinregular-2.vd +++ b/tests/edit-joinregular-2.vd @@ -15,4 +15,4 @@ data2+data1 sort-keys-asc g[ data2+data1 A key-col ! data2+data1 C キ2 edit-cell a3 e data2+data1 sheets-stack S -sheets キdata2 open-row-pyobj ^J +sheets キdata2 open-row ^J diff --git a/tests/invalid_unicode_sqlite.vd b/tests/invalid_unicode_sqlite.vd index effd66ef3..47f959bdf 100644 --- a/tests/invalid_unicode_sqlite.vd +++ b/tests/invalid_unicode_sqlite.vd @@ -2,4 +2,4 @@ sheet col row longname input keystrokes comment global encoding set-option latin-1 invalid_unicode encoding set-option latin-1 open-file tests/invalid_unicode.sqlite o -invalid_unicode キTest open-row-pyobj Enter open sheet with copies of rows referenced in current row +invalid_unicode キTest open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/join-cols-single-sheet.vd b/tests/join-cols-single-sheet.vd index f83fbde56..88d3fc12e 100644 --- a/tests/join-cols-single-sheet.vd +++ b/tests/join-cols-single-sheet.vd @@ -6,4 +6,4 @@ sample_columns キRegion select-row s select current row sample_columns キRep select-row s select current row sample_columns join-cols & add column from concatenating selected source columns sample_columns sheets-stack S open Sheets Stack: join or jump between the active sheets on the current stack -sheets キsample open-row-pyobj ^J open sheet with copies of rows referenced in current row +sheets キsample open-row ^J open sheet with copies of rows referenced in current row diff --git a/tests/join-different-types.vd b/tests/join-different-types.vd index 50f649108..df1733611 100644 --- a/tests/join-different-types.vd +++ b/tests/join-different-types.vd @@ -1,16 +1,16 @@ sheet col row longname input keystrokes comment open-file tests/joining_error.xlsx o open-file tests/joining_error_interesting_records.csv o -joining_error キrecords open-row-pyobj ^J open sheet with copies of rows referenced in current row +joining_error キrecords open-row ^J open sheet with copies of rows referenced in current row joining_error_records sheets-stack S open Sheets Stack: join or jump between the active sheets on the current stack sheets キjoining_error_interesting_records slide-up K slide current row up sheets キjoining_error_interesting_records slide-up K slide current row up sheets キjoining_error_interesting_records select-row s select current row sheets キjoining_error_records select-row s select current row -sheets キjoining_error_interesting_records open-row-pyobj ^J open sheet referenced in current row +sheets キjoining_error_interesting_records open-row ^J open sheet referenced in current row joining_error_interesting_records Row key-col ! toggle current column as a key column joining_error_interesting_records sheets-stack S open Sheets Stack: join or jump between the active sheets on the current stack -sheets キjoining_error_records open-row-pyobj ^J open sheet referenced in current row +sheets キjoining_error_records open-row ^J open sheet referenced in current row joining_error_records Row key-col ! toggle current column as a key column joining_error_records sheets-stack S open Sheets Stack: join or jump between the active sheets on the current stack sheets キjoining_error_interesting_records slide-up K slide current row up diff --git a/tests/join-non-unique-cols.vd b/tests/join-non-unique-cols.vd index 64a2f6cf4..5a00badea 100644 --- a/tests/join-non-unique-cols.vd +++ b/tests/join-non-unique-cols.vd @@ -14,4 +14,4 @@ sheets join-sheets outer & benchmark+sample columns-sheet C benchmark+sample_columns name sort-desc ] benchmark+sample_columns sheets-stack S -sheets キbenchmark+sample open-row-pyobj ^J +sheets キbenchmark+sample open-row ^J diff --git a/tests/listofdictobj.vd b/tests/listofdictobj.vd index 5ca963857..e322ea3f7 100644 --- a/tests/listofdictobj.vd +++ b/tests/listofdictobj.vd @@ -1,4 +1,4 @@ sheet col row longname input keystrokes comment open-file sample_data/y77d-th95.json.gz o y77d-th95 geolocation expand-col-depth 1 z( expand current column of containers to given depth (0=fully) - geolocation.coordinates 0 open-cell-pyobj z^J open sheet with copies of rows referenced in current cell + geolocation.coordinates 0 open-cell z^J open sheet with copies of rows referenced in current cell diff --git a/tests/load-ods.vd b/tests/load-ods.vd index 6514d1008..c41baff51 100644 --- a/tests/load-ods.vd +++ b/tests/load-ods.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file sample_data/benchmark.ods o -benchmark キbenchmark open-row-pyobj Enter open sheet with copies of rows referenced in current row +benchmark キbenchmark open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/load-sqlite-view.vd b/tests/load-sqlite-view.vd index a6872f034..3936e1a27 100644 --- a/tests/load-sqlite-view.vd +++ b/tests/load-sqlite-view.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file sample_data/employees.sqlite o -employees キemp_view open-row-pyobj Enter open sheet with copies of rows referenced in current row +employees キemp_view open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/load-zip.vd b/tests/load-zip.vd index 373689340..e35f36b16 100644 --- a/tests/load-zip.vd +++ b/tests/load-zip.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file sample_data/benchmark.zip o -benchmark キsample_data/,benchmark.csv open-row-pyobj Enter open sheet with copies of rows referenced in current row +benchmark キsample_data/,benchmark.csv open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/sqlite_withoutrowid.vd b/tests/sqlite_withoutrowid.vd index 0f1b519b5..f9a9b0f0f 100644 --- a/tests/sqlite_withoutrowid.vd +++ b/tests/sqlite_withoutrowid.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file tests/without_rowid.db o -without_rowid キwithoutrowid open-row-pyobj Enter open sheet with copies of rows referenced in current row +without_rowid キwithoutrowid open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/xlsx-color-cells.vd b/tests/xlsx-color-cells.vd index e63d9a5cf..eb1ab82aa 100644 --- a/tests/xlsx-color-cells.vd +++ b/tests/xlsx-color-cells.vd @@ -1,4 +1,4 @@ sheet col row longname input keystrokes comment global xlsx_meta_columns set-option True open-file sample_data/color-merged-cells.xlsx o -color-merged-cells キSheet1 open-row-pyobj Enter open sheet with copies of rows referenced in current row +color-merged-cells キSheet1 open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/xlsx-empty-cell.vd b/tests/xlsx-empty-cell.vd index ecbbe79fe..f8e01a75c 100644 --- a/tests/xlsx-empty-cell.vd +++ b/tests/xlsx-empty-cell.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file sample_data/empty-cell.xlsx o -empty-cell キSheet1 open-row-pyobj Enter open sheet with copies of rows referenced in current row +empty-cell キSheet1 open-row Enter open sheet with copies of rows referenced in current row diff --git a/tests/xlsx-merged-cells.vd b/tests/xlsx-merged-cells.vd index 80be7c545..1bf53b7ab 100644 --- a/tests/xlsx-merged-cells.vd +++ b/tests/xlsx-merged-cells.vd @@ -1,3 +1,3 @@ sheet col row longname input keystrokes comment open-file sample_data/color-merged-cells.xlsx o -color-merged-cells キSheet1 open-row-pyobj Enter open sheet with copies of rows referenced in current row +color-merged-cells キSheet1 open-row Enter open sheet with copies of rows referenced in current row diff --git a/visidata/__init__.py b/visidata/__init__.py index 48a6603e2..c1f5538ea 100644 --- a/visidata/__init__.py +++ b/visidata/__init__.py @@ -1,6 +1,6 @@ 'VisiData: a curses interface for exploring and arranging tabular data' -__version__ = '2.10.1' +__version__ = '2.10.2' __version_info__ = 'VisiData v' + __version__ __author__ = 'Saul Pwanson ' __status__ = 'Production/Stable' @@ -67,6 +67,8 @@ def getGlobals(): import visidata.pyobj import visidata.loaders.json import visidata._open +import visidata.metasheets +import visidata.cmdlog import visidata.save import visidata.clipboard import visidata.slide @@ -75,14 +77,12 @@ def getGlobals(): import visidata.menu import visidata.choose -import visidata.metasheets import visidata.join import visidata.aggregators import visidata.describe import visidata.pivot import visidata.freqtbl import visidata.melt -import visidata.cmdlog import visidata.freeze import visidata.regex import visidata.canvas @@ -105,6 +105,7 @@ def getGlobals(): import visidata.memory import visidata.macros import visidata.macos +import visidata.repeat import visidata.loaders.csv import visidata.loaders.archive @@ -148,6 +149,8 @@ def getGlobals(): import visidata.loaders.arrow import visidata.loaders.parquet +import visidata.loaders.vdx + import visidata.form import visidata.ddwplay diff --git a/visidata/_input.py b/visidata/_input.py index 4206b8d33..30a45a355 100644 --- a/visidata/_input.py +++ b/visidata/_input.py @@ -60,7 +60,7 @@ def until_get_wch(scr): return ret -def splice(v, i, s): +def splice(v:str, i:int, s:str): 'Insert `s` into string `v` at `i` (such that v[i] == s[0]).' return v if i < 0 else v[:i] + s + v[i:] @@ -227,7 +227,7 @@ def find_nonword(s, a, b, incr): elif ch == '^U': v = v[i:]; i = 0 # clear to beginning elif ch == '^V': v = splice(v, i, until_get_wch(scr)); i += 1 # literal character elif ch == '^W': j = find_nonword(v, 0, i-1, -1); v = v[:j+1] + v[i:]; i = j+1 # erase word - elif ch == '^Y': v = splice(v, i, vd.memory.clipval) + elif ch == '^Y': v = splice(v, i, str(vd.memory.clipval)) elif ch == '^Z': suspend() # CTRL+arrow elif ch == 'kLFT5': i = find_nonword(v, 0, i-1, -1)+1; # word left diff --git a/visidata/_open.py b/visidata/_open.py index f1662a6f5..6bbc293e8 100644 --- a/visidata/_open.py +++ b/visidata/_open.py @@ -132,4 +132,4 @@ def loadInternalSheet(vd, cls, p, **kwargs): BaseSheet.addCommand('o', 'open-file', 'vd.push(openSource(inputFilename("open: "), create=True))', 'Open file or URL') -TableSheet.addCommand('zo', 'open-cell', 'vd.push(openSource(cursorDisplay))', 'Open file or URL from path in current cell') +TableSheet.addCommand('zo', 'open-cell-file', 'vd.push(openSource(cursorDisplay))', 'Open file or URL from path in current cell') diff --git a/visidata/basesheet.py b/visidata/basesheet.py index 049357479..80b649461 100644 --- a/visidata/basesheet.py +++ b/visidata/basesheet.py @@ -73,6 +73,19 @@ def execCommand2(self, cmd, vdglobals=None): return True +class _dualproperty: + 'Return *obj_method* or *cls_method* depending on whether property is on instance or class.' + def __init__(self, obj_method, cls_method): + self._obj_method = obj_method + self._cls_method = cls_method + + def __get__(self, obj, objtype=None): + if obj is None: + return self._cls_method(objtype) + else: + return self._obj_method(obj) + + class BaseSheet(DrawablePane): 'Base class for all sheet types.' _rowtype = object # callable (no parms) that returns new empty item @@ -81,13 +94,13 @@ class BaseSheet(DrawablePane): precious = True # False for a few discardable metasheets defer = False # False for not deferring changes until save - @visidata.classproperty - def class_options(cls): + def _obj_options(self): + return vd.OptionsObject(vd._options, obj=self) + + def _class_options(cls): return vd.OptionsObject(vd._options, obj=cls) - @property - def options(self): - return vd.OptionsObject(vd._options, obj=self) + class_options = options = _dualproperty(_obj_options, _class_options) def __init__(self, *names, **kwargs): self._name = None # initial cache value necessary for self.options diff --git a/visidata/clipboard.py b/visidata/clipboard.py index 927736770..41fb1090b 100644 --- a/visidata/clipboard.py +++ b/visidata/clipboard.py @@ -1,6 +1,7 @@ from copy import copy, deepcopy import shutil import subprocess +import io import sys import tempfile import functools @@ -42,17 +43,13 @@ def copyCells(sheet, col, rows): @Sheet.api def syscopyValue(sheet, val): - # use NTF to generate filename and delete file on context exit - with tempfile.NamedTemporaryFile(suffix='.txt') as temp: - with open(temp.name, "w", encoding=sheet.options.encoding) as fp: - fp.write(val) + # pipe val to stdin of clipboard command - p = subprocess.Popen( - sheet.options.clipboard_copy_cmd.split(), - stdin=open(temp.name, 'r', encoding=sheet.options.encoding), - stdout=subprocess.DEVNULL, - close_fds=True) - p.communicate() + p = subprocess.run( + sheet.options.clipboard_copy_cmd.split(), + input=val, + encoding=sheet.options.encoding, + stdout=subprocess.DEVNULL) vd.status('copied value to system clipboard') @@ -72,15 +69,13 @@ def syscopyCells_async(sheet, cols, rows, filetype): vd.status(f'copying {vs.nRows} {vs.rowtype} to system clipboard as {filetype}') - # use NTF to generate filename and delete file on context exit - with tempfile.NamedTemporaryFile(suffix='.'+filetype) as temp: - vd.sync(vd.saveSheets(Path(temp.name), vs)) - p = subprocess.Popen( + with io.StringIO() as buf: + vd.sync(vd.saveSheets(Path(sheet.name+'.'+filetype, fptext=buf), vs)) + subprocess.run( sheet.options.clipboard_copy_cmd.split(), - stdin=open(temp.name, 'r', encoding=sheet.options.encoding), - stdout=subprocess.DEVNULL, - close_fds=True) - p.communicate() + input=buf.getvalue(), + encoding=sheet.options.encoding, + stdout=subprocess.DEVNULL) @VisiData.api diff --git a/visidata/cmdlog.py b/visidata/cmdlog.py index 6d484cb7f..e1e363178 100644 --- a/visidata/cmdlog.py +++ b/visidata/cmdlog.py @@ -35,7 +35,7 @@ def open_vdj(vd, p): @VisiData.api def save_vdj(vd, p, *vsheets): with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp: - fp.write("#!/usr/bin/env vd -p\n") + fp.write("#!vd -p\n") for vs in vsheets: vs.write_jsonl(fp) @@ -137,7 +137,7 @@ def moveToCol(vs, col): return True -@TableSheet.api +@BaseSheet.api def commandCursor(sheet, execstr): 'Return (col, row) of cursor suitable for cmdlog replay of execstr.' colname, rowname = '', '' @@ -152,7 +152,7 @@ def commandCursor(sheet, execstr): # rowdef: namedlist (like TsvSheet) -class _CommandLog: +class CommandLogBase: 'Log of commands for current session.' rowtype = 'logged commands' precious = False @@ -178,7 +178,7 @@ def beforeExecHook(self, sheet, cmd, args, keystrokes): self.afterExecSheet(sheet, False, '') colname, rowname, sheetname = '', '', None - if sheet and not (cmd.longname.startswith('open-') and not cmd.longname in ('open-row-pyobj', 'open-cell-pyobj')): + if sheet and not (cmd.longname.startswith('open-') and not cmd.longname in ('open-row', 'open-cell')): sheetname = sheet.name colname, rowname = sheet.commandCursor(cmd.execstr) @@ -233,10 +233,10 @@ def openHook(self, vs, src): vs.cmdlog_sheet.addRow(r) self.addRow(r) -class CommandLog(_CommandLog, VisiDataMetaSheet): +class CommandLog(CommandLogBase, VisiDataMetaSheet): pass -class CommandLogJsonl(_CommandLog, JsonLinesSheet): +class CommandLogJsonl(CommandLogBase, JsonLinesSheet): filetype = 'vdj' @@ -286,7 +286,7 @@ def replay_cancel(vd): @VisiData.api def moveToReplayContext(vd, r, vs): - 'set the sheet/row/col to the values in the replay row. return sheet' + 'set the sheet/row/col to the values in the replay row' if r.row not in [None, '']: vs.moveToRow(r.row) or vd.error('no "%s" row' % r.row) @@ -507,15 +507,8 @@ def repeat_for_selected(cmdlog, r): CommandLogJsonl.addCommand('gx', 'replay-all', 'vd.replay(sheet)', 'replay contents of entire CommandLog') CommandLogJsonl.addCommand('^C', 'replay-stop', 'sheet.cursorRowIndex = sheet.nRows', 'abort replay') -BaseSheet.addCommand('', 'repeat-last', 'execCommand(vd.cmdlog.rows[-1].longname) if vd.cmdlog.rows else fail("no recent command to repeat")', 'run most recent command with an empty, queried input') -BaseSheet.addCommand('', 'repeat-input', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_n(r, 1)', 'run previous modifying command (incl input)') -BaseSheet.addCommand('', 'repeat-input-n', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_n(r, input("# times to repeat prev command:", value=1))', 'run previous command (incl its input) N times') -BaseSheet.addCommand('', 'repeat-input-selected', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_selected(r)', 'run previous command (incl its input) for each selected row') - -vd.addMenuItem('Edit', 'Repeat', 'last command', 'repeat-input') -vd.addMenuItem('Edit', 'Repeat', 'last command N times', 'repeat-input-n') -vd.addMenuItem('Edit', 'Repeat', 'last command for all selected rows', 'repeat-input-selected') +CommandLog.options.json_sort_keys = False +CommandLog.options.encoding = 'utf-8' +CommandLogJsonl.options.json_sort_keys = False -CommandLog.class_options.json_sort_keys = False -CommandLog.class_options.encoding = 'utf-8' -CommandLogJsonl.class_options.json_sort_keys = False +vd.addGlobals({"CommandLogBase": CommandLogBase}) diff --git a/visidata/deprecated.py b/visidata/deprecated.py index 7cca11d86..859ebb96c 100644 --- a/visidata/deprecated.py +++ b/visidata/deprecated.py @@ -115,9 +115,8 @@ def isNumeric(col): alias('next-null', 'go-next-null') alias('page-right', 'go-right-page') alias('page-left', 'go-left-page') -alias('dive-cell', 'open-cell-pyobj') -alias('dive-row', 'open-row-pyobj') -alias('open-row', 'open-row-pyobj') +alias('dive-cell', 'open-cell') +alias('dive-row', 'open-row') alias('add-sheet', 'open-new') alias('save-sheets-selected', 'save-selected') alias('join-sheets', 'join-selected') diff --git a/visidata/extensible.py b/visidata/extensible.py index 1d2e807fb..249c76939 100644 --- a/visidata/extensible.py +++ b/visidata/extensible.py @@ -47,7 +47,7 @@ def before(cls, beforefunc): funcname = beforefunc.__name__ oldfunc = getattr(cls, funcname, None) if not oldfunc: - vd.fail('@before on non-existing func {cls.__name__}.{funcname}') + setattr(cls, funcname, beforefunc) @wraps(oldfunc) def wrappedfunc(*args, **kwargs): @@ -58,16 +58,16 @@ def wrappedfunc(*args, **kwargs): return wrappedfunc @classmethod - def after(cls, beforefunc): - funcname = beforefunc.__name__ + def after(cls, afterfunc): + funcname = afterfunc.__name__ oldfunc = getattr(cls, funcname, None) if not oldfunc: - vd.fail('@after on non-existing func {cls.__name__}.{funcname}') + setattr(cls, funcname, afterfunc) @wraps(oldfunc) def wrappedfunc(*args, **kwargs): r = oldfunc(*args, **kwargs) - beforefunc(*args, **kwargs) + afterfunc(*args, **kwargs) return r setattr(cls, funcname, wrappedfunc) diff --git a/visidata/loaders/archive.py b/visidata/loaders/archive.py index c8c27974e..6e6073dda 100644 --- a/visidata/loaders/archive.py +++ b/visidata/loaders/archive.py @@ -1,3 +1,4 @@ +import pathlib import tarfile import zipfile from visidata.loaders import unzip_http @@ -16,6 +17,7 @@ def open_tar(vd, p): VisiData.open_tgz = VisiData.open_tar VisiData.open_txz = VisiData.open_tar VisiData.open_tbz2 = VisiData.open_tar +VisiData.open_whl = VisiData.open_zip @VisiData.api class ZipSheet(Sheet): @@ -49,10 +51,21 @@ def openRow(self, row): fp = self.openZipFile(self.zfp, fi) return vd.openSource(Path(fi.filename, fp=fp, filesize=fi.file_size), filetype=options.filetype) - @asyncthread def extract(self, *rows, path=None): + path = path or pathlib.Path('.') + + files = [] + for row in rows: + r, _ = row + if (path/r.filename).exists(): + vd.confirm(f'{r.filename} exists, overwrite? ') #1452 + self.extract_async(row) + + @asyncthread + def extract_async(self, *rows, path=None): + 'Extract rows to *path*, without confirmation.' for r, _ in Progress(rows): - self.zfp.extractall(members=[r.filename], path=path) + self.zfp.extract(member=r.filename, path=path) vd.status(f'extracted {r.filename}') @property diff --git a/visidata/loaders/json.py b/visidata/loaders/json.py index 8f3df35ea..1e4dc350c 100644 --- a/visidata/loaders/json.py +++ b/visidata/loaders/json.py @@ -135,7 +135,7 @@ def save_jsonl(vd, p, *vsheets): vs.write_jsonl(fp) -JsonSheet.class_options.encoding = 'utf-8' +JsonSheet.options.encoding = 'utf-8' VisiData.save_ndjson = VisiData.save_jsonl VisiData.save_ldjson = VisiData.save_jsonl diff --git a/visidata/loaders/pandas_freqtbl.py b/visidata/loaders/pandas_freqtbl.py index c0dbf1c02..89bf31088 100644 --- a/visidata/loaders/pandas_freqtbl.py +++ b/visidata/loaders/pandas_freqtbl.py @@ -164,6 +164,9 @@ def reload(self): {} )) + def openRow(self, row): + return self.source.expand_source_rows(row) + @Sheet.api def expand_source_rows(sheet, row): """Support for expanding a row of frequency table to underlying rows""" @@ -174,12 +177,7 @@ def expand_source_rows(sheet, row): PandasSheet.addCommand('F', 'freq-col', 'vd.push(PandasFreqTableSheet(sheet, cursorCol))', 'open Frequency Table grouped on current column, with aggregations of other columns') PandasSheet.addCommand('gF', 'freq-keys', 'vd.push(PandasFreqTableSheet(sheet, *keyCols))', 'open Frequency Table grouped by all key columns on source sheet, with aggregations of other columns') -PandasFreqTableSheet.addCommand('t', 'stoggle-row', 'toggle([cursorRow]); cursorDown(1)', 'toggle selection of rows grouped in current row in source sheet') -PandasFreqTableSheet.addCommand('s', 'select-row', 'select([cursorRow]); cursorDown(1)', 'select rows grouped in current row in source sheet') -PandasFreqTableSheet.addCommand('u', 'unselect-row', 'unselect([cursorRow]); cursorDown(1)', 'unselect rows grouped in current row in source sheet') -PandasFreqTableSheet.addCommand(ENTER, 'open-row', 'vd.push(source.expand_source_rows(cursorRow))', 'open copy of source sheet with rows that are grouped in current row') - -PandasFreqTableSheet.class_options.numeric_binning = False +PandasFreqTableSheet.options.numeric_binning = False vd.addGlobals({ 'PandasFreqTableSheet': PandasFreqTableSheet, diff --git a/visidata/loaders/sqlite.py b/visidata/loaders/sqlite.py index 94f837c79..6891d7f6f 100644 --- a/visidata/loaders/sqlite.py +++ b/visidata/loaders/sqlite.py @@ -236,7 +236,11 @@ def save_sqlite(vd, p, *vsheets): SqliteIndexSheet.addCommand('a', 'add-table', 'fail("create a new table by saving a sheet to this database file")', 'stub; add table by saving a sheet to the db file instead') SqliteIndexSheet.bindkey('ga', 'add-table') -SqliteSheet.class_options.header = 0 +SqliteSheet.options.header = 0 VisiData.save_db = VisiData.save_sqlite -vd.addGlobals({'SqliteQuerySheet': SqliteQuerySheet}) +vd.addGlobals({ + 'SqliteIndexSheet': SqliteIndexSheet, + 'SqliteSheet': SqliteSheet, + 'SqliteQuerySheet': SqliteQuerySheet +}) diff --git a/visidata/loaders/vdx.py b/visidata/loaders/vdx.py new file mode 100644 index 000000000..3316ce77c --- /dev/null +++ b/visidata/loaders/vdx.py @@ -0,0 +1,45 @@ +import visidata +from visidata import VisiData, CommandLogBase, BaseSheet, Sheet, AttrDict + + +@VisiData.api +def open_vdx(vd, p): + return CommandLogSimple(p.name, source=p, precious=True) + + +class CommandLogSimple(CommandLogBase, Sheet): + filetype = 'vdx' + def iterload(self): + for line in self.source: + if not line or line[0] == '#': + continue + longname, *rest = line.split(' ', maxsplit=1) + yield AttrDict(longname=longname, + input=rest[0] if rest else '') + + +@VisiData.api +def save_vdx(vd, p, *vsheets): + with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp: + fp.write(f"# {visidata.__version_info__}\n") + for vs in vsheets: + prevrow = None + for r in vs.rows: + if prevrow is not None and r.sheet and prevrow.sheet != r.sheet: + fp.write(f'sheet {r.sheet}\n') + if r.col and (prevrow.col != r.col or prevrow is None): + fp.write(f'col {r.col}\n') + if r.row and (prevrow.row != r.row or prevrow is None): + fp.write(f'row {r.row}\n') + + line = r.longname + if r.input: + line += ' ' + str(r.input) + fp.write(line + '\n') + + prevrow = r + + +BaseSheet.addCommand('', 'sheet', 'n=input("sheet to jump to: "); vd.push(vd.getSheet(n) or fail(f"no such sheet {n}"))', 'jump to named sheet') +BaseSheet.addCommand('', 'col', 'n=input("column to go to: "); moveToCol(n) or fail(f"no such column {n}")', 'move to named/numbered col') +BaseSheet.addCommand('', 'row', 'n=input("row to go to: "); moveToRow(n) or fail(f"no such row {n}")', 'move to named/numbered row') diff --git a/visidata/loaders/xword.py b/visidata/loaders/xword.py index 2becebd62..624ea8829 100755 --- a/visidata/loaders/xword.py +++ b/visidata/loaders/xword.py @@ -103,4 +103,4 @@ def save_xd(vd, p, vs): CrosswordsSheet.addCommand(ENTER, 'open-clues', 'vd.push(CrosswordSheet("clues_"+cursorRow.title, source=cursorRow))', 'open CrosswordSheet: clue answer pair for crossword') CrosswordSheet.addCommand(ENTER, 'open-grid', 'vd.push(GridSheet("grid", source=sheet, pos=cursorRow[0]))', 'open GridSheet: grid for crossword') -GridSheet.class_options.disp_column_sep = '' +GridSheet.options.disp_column_sep = '' diff --git a/visidata/main.py b/visidata/main.py index b8eef1517..cba4d7478 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -2,7 +2,7 @@ # Usage: $0 [] [ ...] # $0 [] --play [--batch] [-w ] [-o ] [field=value ...] -__version__ = '2.10.1' +__version__ = '2.10.2' __version_info__ = 'saul.pw/VisiData v' + __version__ from copy import copy @@ -31,6 +31,7 @@ vd.option('output', None, 'save the final visible sheet to output at the end of replay') vd.option('preplay', '', 'longnames to preplay before replay') vd.option('imports', 'plugins', 'imports to preload before .visidatarc (command-line only)') +vd.option('nothing', False, 'no config, no plugins, nothing extra') # for --play def eval_vd(logpath, *args, **kwargs): @@ -75,6 +76,7 @@ def optalias(abbr, name, val=None): option_aliases[abbr] = (name, val) +optalias('N', 'nothing') optalias('f', 'filetype') optalias('p', 'play') optalias('b', 'batch') @@ -146,7 +148,7 @@ def main_vd(): flGlobal = True elif arg in ['-n', '--nonglobal']: flGlobal = False - elif arg[0] == '-': + elif arg.startswith('-'): optname = arg.lstrip('-') optval = None try: @@ -206,7 +208,8 @@ def main_vd(): args = AttrDict(current_args) - vd.loadConfigAndPlugins(args) + if not args.nothing: + vd.loadConfigAndPlugins(args) for k, v in global_args.items(): options.set(k, v, obj='global') @@ -247,7 +250,8 @@ def main_vd(): vd.cmdlog.openHook(vs, vs.source) sources.append(vs) - vd.sheets.extend(sources) # purposefully do not load everything + for vs in reversed(sources): + vd.push(vs, load=False) #1471, 1555 if not vd.sheets and not args.play and not args.batch: if 'filetype' in current_args: diff --git a/visidata/mainloop.py b/visidata/mainloop.py index 6f0f0a70a..cb89daf69 100644 --- a/visidata/mainloop.py +++ b/visidata/mainloop.py @@ -88,7 +88,7 @@ def setWindows(vd, scr, pct=None): 'Assign winTop, winBottom, win1 and win2 according to options.disp_splitwin_pct.' if pct is None: pct = options.disp_splitwin_pct # percent of window for secondary sheet (negative means bottom) - disp_menu = vd.menuRunning or vd.options.disp_menu + disp_menu = getattr(vd, 'menuRunning', None) or vd.options.disp_menu topmenulines = 1 if disp_menu else 0 h, w = scr.getmaxyx() @@ -214,6 +214,8 @@ def parseMouse(vd, **kwargs): @VisiData.api def mainloop(self, scr): 'Manage execution of keystrokes and subsequent redrawing of screen.' + nonidle_timeout = vd.curses_timeout + scr.timeout(vd.curses_timeout) with contextlib.suppress(curses.error): curses.curs_set(0) @@ -258,18 +260,18 @@ def mainloop(self, scr): self.statuses.clear() if keystroke == 'KEY_MOUSE': - self.keystrokes = '' - keystroke, y, x, winname, winscr = vd.parseMouse(top=vd.winTop, bot=vd.winBottom, menu=vd.scrMenu) + try: + self.keystrokes = '' + keystroke, y, x, winname, winscr = vd.parseMouse(top=vd.winTop, bot=vd.winBottom, menu=vd.scrMenu) - pct = vd.windowConfig['pct'] - topPaneActive = ((vd.activePane == 2 and pct < 0) or (vd.activePane == 1 and pct > 0)) - bottomPaneActive = ((vd.activePane == 1 and pct < 0) or (vd.activePane == 2 and pct > 0)) + pct = vd.windowConfig['pct'] + topPaneActive = ((vd.activePane == 2 and pct < 0) or (vd.activePane == 1 and pct > 0)) + bottomPaneActive = ((vd.activePane == 1 and pct < 0) or (vd.activePane == 2 and pct > 0)) - if (bottomPaneActive and winname == 'top') or (topPaneActive and winname == 'bot'): - self.activePane = 1 if self.activePane == 2 else 2 - sheet = self.activeSheet + if (bottomPaneActive and winname == 'top') or (topPaneActive and winname == 'bot'): + self.activePane = 1 if self.activePane == 2 else 2 + sheet = self.activeSheet - try: f = self.getMouse(winscr, x, y, keystroke) sheet.mouseX, sheet.mouseY = x, y if f: @@ -313,13 +315,15 @@ def mainloop(self, scr): # no idle redraw unless background threads are running time.sleep(0) # yield to other threads which may not have started yet if vd.unfinishedThreads: - scr.timeout(vd.curses_timeout) + vd.curses_timeout = nonidle_timeout else: numTimeouts += 1 if vd.timeouts_before_idle >= 0 and numTimeouts > vd.timeouts_before_idle: - scr.timeout(-1) + vd.curses_timeout = -1 else: - scr.timeout(vd.curses_timeout) + vd.curses_timeout = nonidle_timeout + + scr.timeout(vd.curses_timeout) def initCurses(): @@ -369,7 +373,7 @@ def run(vd, *sheetlist): try: # Populate VisiData object with sheets from a given list. for vs in sheetlist: - vd.push(vs) + vd.push(vs, load=False) scr = initCurses() ret = vd.mainloop(scr) diff --git a/visidata/man/vd.1 b/visidata/man/vd.1 index 3fde20884..0a6e2df77 100644 --- a/visidata/man/vd.1 +++ b/visidata/man/vd.1 @@ -1,4 +1,4 @@ -.Dd September 13, 2022 +.Dd October 06, 2022 .Dt vd \&1 "Quick Reference Guide" .Os Linux/MacOS . @@ -874,6 +874,9 @@ field delimiter to use for tsv/usv filetype .No "True " overwrite existing files without confirmation . +.Lo N nothing T +.No "False " +disable loading .visidatarc and plugin addons . .El .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact @@ -964,6 +967,14 @@ sort object keys when saving to json column name to use for non-dict rows .It Sy --filetype Ns = Ns Ar "str " No "" specify file type +.It Sy --replay-wait Ns = Ns Ar "float " No "0.0" +time to wait between replayed commands, in seconds +.It Sy --replay-movement No " False" +insert movements during replay +.It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" +string prefix for rowkey in the cmdlog +.It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" +file to autorecord each cmdlog action to .It Sy --confirm-overwrite Ns = Ns Ar "bool " No "True" whether to prompt for overwrite confirmation on save .It Sy --safe-error Ns = Ns Ar "str " No "#ERR" @@ -980,14 +991,6 @@ numeric aggregators to calculate on Describe sheet number of bins for histogram of numeric columns .It Sy --numeric-binning No " False" bin numeric columns into ranges -.It Sy --replay-wait Ns = Ns Ar "float " No "0.0" -time to wait between replayed commands, in seconds -.It Sy --replay-movement No " False" -insert movements during replay -.It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" -string prefix for rowkey in the cmdlog -.It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" -file to autorecord each cmdlog action to .It Sy --regex-flags Ns = Ns Ar "str " No "I" flags to pass to re.compile() [AILMSUX] .It Sy --regex-maxsplit Ns = Ns Ar "int " No "0" @@ -1016,6 +1019,8 @@ save the final visible sheet to output at the end of replay longnames to preplay before replay .It Sy --imports Ns = Ns Ar "str " No "plugins" imports to preload before .visidatarc (command-line only) +.It Sy --nothing No " False" +no config, no plugins, nothing extra .It Sy --unfurl-empty No " False" if unfurl includes rows for empty containers .It Sy --incr-base Ns = Ns Ar "float " No "1.0" @@ -1182,6 +1187,12 @@ cell color to use when editing cell edit field fill character .It Sy "disp_unprintable " No "\[u00B7]" substitute character for unprintables +.It Sy "disp_replay_play " No "\[u25B6]" +status indicator for active replay +.It Sy "disp_replay_pause " No "\[u2016]" +status indicator for paused replay +.It Sy "color_status_replay" No "green" +color of replay status indicator .It Sy "disp_formatter " No "generic" formatter to use for display and saving .It Sy "disp_menu " No "True" @@ -1210,12 +1221,6 @@ right-side menu format string histogram element character .It Sy "disp_histolen " No "50" width of histogram column -.It Sy "disp_replay_play " No "\[u25B6]" -status indicator for active replay -.It Sy "disp_replay_pause " No "\[u2016]" -status indicator for paused replay -.It Sy "color_status_replay" No "green" -color of replay status indicator .It Sy "disp_canvas_charset" No "\[u2800]\[u2801]\[u2802]\[u2803]\[u2804]\[u2805]\[u2806]\[u2807]\[u2808]\[u2809]\[u280A]\[u280B]\[u280C]\[u280D]\[u280E]\[u280F]\[u2810]\[u2811]\[u2812]\[u2813]\[u2814]\[u2815]\[u2816]\[u2817]\[u2818]\[u2819]\[u281A]\[u281B]\[u281C]\[u281D]\[u281E]\[u281F]\[u2820]\[u2821]\[u2822]\[u2823]\[u2824]\[u2825]\[u2826]\[u2827]\[u2828]\[u2829]\[u282A]\[u282B]\[u282C]\[u282D]\[u282E]\[u282F]\[u2830]\[u2831]\[u2832]\[u2833]\[u2834]\[u2835]\[u2836]\[u2837]\[u2838]\[u2839]\[u283A]\[u283B]\[u283C]\[u283D]\[u283E]\[u283F]\[u2840]\[u2841]\[u2842]\[u2843]\[u2844]\[u2845]\[u2846]\[u2847]\[u2848]\[u2849]\[u284A]\[u284B]\[u284C]\[u284D]\[u284E]\[u284F]\[u2850]\[u2851]\[u2852]\[u2853]\[u2854]\[u2855]\[u2856]\[u2857]\[u2858]\[u2859]\[u285A]\[u285B]\[u285C]\[u285D]\[u285E]\[u285F]\[u2860]\[u2861]\[u2862]\[u2863]\[u2864]\[u2865]\[u2866]\[u2867]\[u2868]\[u2869]\[u286A]\[u286B]\[u286C]\[u286D]\[u286E]\[u286F]\[u2870]\[u2871]\[u2872]\[u2873]\[u2874]\[u2875]\[u2876]\[u2877]\[u2878]\[u2879]\[u287A]\[u287B]\[u287C]\[u287D]\[u287E]\[u287F]\[u2880]\[u2881]\[u2882]\[u2883]\[u2884]\[u2885]\[u2886]\[u2887]\[u2888]\[u2889]\[u288A]\[u288B]\[u288C]\[u288D]\[u288E]\[u288F]\[u2890]\[u2891]\[u2892]\[u2893]\[u2894]\[u2895]\[u2896]\[u2897]\[u2898]\[u2899]\[u289A]\[u289B]\[u289C]\[u289D]\[u289E]\[u289F]\[u28A0]\[u28A1]\[u28A2]\[u28A3]\[u28A4]\[u28A5]\[u28A6]\[u28A7]\[u28A8]\[u28A9]\[u28AA]\[u28AB]\[u28AC]\[u28AD]\[u28AE]\[u28AF]\[u28B0]\[u28B1]\[u28B2]\[u28B3]\[u28B4]\[u28B5]\[u28B6]\[u28B7]\[u28B8]\[u28B9]\[u28BA]\[u28BB]\[u28BC]\[u28BD]\[u28BE]\[u28BF]\[u28C0]\[u28C1]\[u28C2]\[u28C3]\[u28C4]\[u28C5]\[u28C6]\[u28C7]\[u28C8]\[u28C9]\[u28CA]\[u28CB]\[u28CC]\[u28CD]\[u28CE]\[u28CF]\[u28D0]\[u28D1]\[u28D2]\[u28D3]\[u28D4]\[u28D5]\[u28D6]\[u28D7]\[u28D8]\[u28D9]\[u28DA]\[u28DB]\[u28DC]\[u28DD]\[u28DE]\[u28DF]\[u28E0]\[u28E1]\[u28E2]\[u28E3]\[u28E4]\[u28E5]\[u28E6]\[u28E7]\[u28E8]\[u28E9]\[u28EA]\[u28EB]\[u28EC]\[u28ED]\[u28EE]\[u28EF]\[u28F0]\[u28F1]\[u28F2]\[u28F3]\[u28F4]\[u28F5]\[u28F6]\[u28F7]\[u28F8]\[u28F9]\[u28FA]\[u28FB]\[u28FC]\[u28FD]\[u28FE]\[u28FF]" charset to render 2x4 blocks on canvas .It Sy "disp_pixel_random " No "False" diff --git a/visidata/man/vd.inc b/visidata/man/vd.inc index 9b29d63af..1cd26d0c6 100644 --- a/visidata/man/vd.inc +++ b/visidata/man/vd.inc @@ -1,4 +1,4 @@ -.Dd September 13, 2022 +.Dd October 06, 2022 .Dt vd \&1 "Quick Reference Guide" .Os Linux/MacOS . @@ -874,6 +874,9 @@ field delimiter to use for tsv/usv filetype .No "True " overwrite existing files without confirmation . +.Lo N nothing T +.No "False " +disable loading .visidatarc and plugin addons . .El .so vd-cli.inc diff --git a/visidata/man/vd.txt b/visidata/man/vd.txt index 0b1b0b08a..02e3db6b6 100644 --- a/visidata/man/vd.txt +++ b/visidata/man/vd.txt @@ -562,6 +562,8 @@ COMMANDLINE OPTIONS for tsv/usv filetype -y, --confirm-overwrite=F True overwrite existing files without confirmation + -N, --nothing=T False disable loading + .visidatarc and plugin addons --visidata-dir=str ~/.visidata/ directory to load and store additional files --mouse-interval=int 1 max time between @@ -654,6 +656,15 @@ COMMANDLINE OPTIONS --default-colname=str column name to use for non-dict rows --filetype=str specify file type + --replay-wait=float 0.0 time to wait between re‐ + played commands, in sec‐ + onds + --replay-movement False insert movements during + replay + --rowkey-prefix=str キ string prefix for rowkey + in the cmdlog + --cmdlog-histfile=str file to autorecord each + cmdlog action to --confirm-overwrite=bool True whether to prompt for overwrite confirmation on save @@ -676,15 +687,6 @@ COMMANDLINE OPTIONS togram of numeric columns --numeric-binning False bin numeric columns into ranges - --replay-wait=float 0.0 time to wait between re‐ - played commands, in sec‐ - onds - --replay-movement False insert movements during - replay - --rowkey-prefix=str キ string prefix for rowkey - in the cmdlog - --cmdlog-histfile=str file to autorecord each - cmdlog action to --regex-flags=str I flags to pass to re.com‐ pile() [AILMSUX] --regex-maxsplit=int 0 maxsplit to pass to @@ -719,6 +721,8 @@ COMMANDLINE OPTIONS --imports=str plugins imports to preload before .visidatarc (command-line only) + --nothing False no config, no plugins, + nothing extra --unfurl-empty False if unfurl includes rows for empty containers --incr-base=float 1.0 start value for column @@ -859,6 +863,11 @@ COMMANDLINE OPTIONS disp_edit_fill _ edit field fill character disp_unprintable · substitute character for unprint‐ ables + disp_replay_play ▶ status indicator for active re‐ + play + disp_replay_pause ‖ status indicator for paused re‐ + play + color_status_replay green color of replay status indicator disp_formatter generic formatter to use for display and saving disp_menu True show menu on top line when not @@ -882,11 +891,6 @@ COMMANDLINE OPTIONS right-side menu format string disp_histogram * histogram element character disp_histolen 50 width of histogram column - disp_replay_play ▶ status indicator for active re‐ - play - disp_replay_pause ‖ status indicator for paused re‐ - play - color_status_replay green color of replay status indicator disp_canvas_charset ⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿ charset to render 2x4 blocks on @@ -1008,4 +1012,4 @@ SUPPORTED SOURCES AUTHOR VisiData was made by Saul Pwanson . -Linux/MacOS September 13, 2022 Linux/MacOS +Linux/MacOS October 06, 2022 Linux/MacOS diff --git a/visidata/man/visidata.1 b/visidata/man/visidata.1 index 3fde20884..0a6e2df77 100644 --- a/visidata/man/visidata.1 +++ b/visidata/man/visidata.1 @@ -1,4 +1,4 @@ -.Dd September 13, 2022 +.Dd October 06, 2022 .Dt vd \&1 "Quick Reference Guide" .Os Linux/MacOS . @@ -874,6 +874,9 @@ field delimiter to use for tsv/usv filetype .No "True " overwrite existing files without confirmation . +.Lo N nothing T +.No "False " +disable loading .visidatarc and plugin addons . .El .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -compact @@ -964,6 +967,14 @@ sort object keys when saving to json column name to use for non-dict rows .It Sy --filetype Ns = Ns Ar "str " No "" specify file type +.It Sy --replay-wait Ns = Ns Ar "float " No "0.0" +time to wait between replayed commands, in seconds +.It Sy --replay-movement No " False" +insert movements during replay +.It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" +string prefix for rowkey in the cmdlog +.It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" +file to autorecord each cmdlog action to .It Sy --confirm-overwrite Ns = Ns Ar "bool " No "True" whether to prompt for overwrite confirmation on save .It Sy --safe-error Ns = Ns Ar "str " No "#ERR" @@ -980,14 +991,6 @@ numeric aggregators to calculate on Describe sheet number of bins for histogram of numeric columns .It Sy --numeric-binning No " False" bin numeric columns into ranges -.It Sy --replay-wait Ns = Ns Ar "float " No "0.0" -time to wait between replayed commands, in seconds -.It Sy --replay-movement No " False" -insert movements during replay -.It Sy --rowkey-prefix Ns = Ns Ar "str " No "\[u30AD]" -string prefix for rowkey in the cmdlog -.It Sy --cmdlog-histfile Ns = Ns Ar "str " No "" -file to autorecord each cmdlog action to .It Sy --regex-flags Ns = Ns Ar "str " No "I" flags to pass to re.compile() [AILMSUX] .It Sy --regex-maxsplit Ns = Ns Ar "int " No "0" @@ -1016,6 +1019,8 @@ save the final visible sheet to output at the end of replay longnames to preplay before replay .It Sy --imports Ns = Ns Ar "str " No "plugins" imports to preload before .visidatarc (command-line only) +.It Sy --nothing No " False" +no config, no plugins, nothing extra .It Sy --unfurl-empty No " False" if unfurl includes rows for empty containers .It Sy --incr-base Ns = Ns Ar "float " No "1.0" @@ -1182,6 +1187,12 @@ cell color to use when editing cell edit field fill character .It Sy "disp_unprintable " No "\[u00B7]" substitute character for unprintables +.It Sy "disp_replay_play " No "\[u25B6]" +status indicator for active replay +.It Sy "disp_replay_pause " No "\[u2016]" +status indicator for paused replay +.It Sy "color_status_replay" No "green" +color of replay status indicator .It Sy "disp_formatter " No "generic" formatter to use for display and saving .It Sy "disp_menu " No "True" @@ -1210,12 +1221,6 @@ right-side menu format string histogram element character .It Sy "disp_histolen " No "50" width of histogram column -.It Sy "disp_replay_play " No "\[u25B6]" -status indicator for active replay -.It Sy "disp_replay_pause " No "\[u2016]" -status indicator for paused replay -.It Sy "color_status_replay" No "green" -color of replay status indicator .It Sy "disp_canvas_charset" No "\[u2800]\[u2801]\[u2802]\[u2803]\[u2804]\[u2805]\[u2806]\[u2807]\[u2808]\[u2809]\[u280A]\[u280B]\[u280C]\[u280D]\[u280E]\[u280F]\[u2810]\[u2811]\[u2812]\[u2813]\[u2814]\[u2815]\[u2816]\[u2817]\[u2818]\[u2819]\[u281A]\[u281B]\[u281C]\[u281D]\[u281E]\[u281F]\[u2820]\[u2821]\[u2822]\[u2823]\[u2824]\[u2825]\[u2826]\[u2827]\[u2828]\[u2829]\[u282A]\[u282B]\[u282C]\[u282D]\[u282E]\[u282F]\[u2830]\[u2831]\[u2832]\[u2833]\[u2834]\[u2835]\[u2836]\[u2837]\[u2838]\[u2839]\[u283A]\[u283B]\[u283C]\[u283D]\[u283E]\[u283F]\[u2840]\[u2841]\[u2842]\[u2843]\[u2844]\[u2845]\[u2846]\[u2847]\[u2848]\[u2849]\[u284A]\[u284B]\[u284C]\[u284D]\[u284E]\[u284F]\[u2850]\[u2851]\[u2852]\[u2853]\[u2854]\[u2855]\[u2856]\[u2857]\[u2858]\[u2859]\[u285A]\[u285B]\[u285C]\[u285D]\[u285E]\[u285F]\[u2860]\[u2861]\[u2862]\[u2863]\[u2864]\[u2865]\[u2866]\[u2867]\[u2868]\[u2869]\[u286A]\[u286B]\[u286C]\[u286D]\[u286E]\[u286F]\[u2870]\[u2871]\[u2872]\[u2873]\[u2874]\[u2875]\[u2876]\[u2877]\[u2878]\[u2879]\[u287A]\[u287B]\[u287C]\[u287D]\[u287E]\[u287F]\[u2880]\[u2881]\[u2882]\[u2883]\[u2884]\[u2885]\[u2886]\[u2887]\[u2888]\[u2889]\[u288A]\[u288B]\[u288C]\[u288D]\[u288E]\[u288F]\[u2890]\[u2891]\[u2892]\[u2893]\[u2894]\[u2895]\[u2896]\[u2897]\[u2898]\[u2899]\[u289A]\[u289B]\[u289C]\[u289D]\[u289E]\[u289F]\[u28A0]\[u28A1]\[u28A2]\[u28A3]\[u28A4]\[u28A5]\[u28A6]\[u28A7]\[u28A8]\[u28A9]\[u28AA]\[u28AB]\[u28AC]\[u28AD]\[u28AE]\[u28AF]\[u28B0]\[u28B1]\[u28B2]\[u28B3]\[u28B4]\[u28B5]\[u28B6]\[u28B7]\[u28B8]\[u28B9]\[u28BA]\[u28BB]\[u28BC]\[u28BD]\[u28BE]\[u28BF]\[u28C0]\[u28C1]\[u28C2]\[u28C3]\[u28C4]\[u28C5]\[u28C6]\[u28C7]\[u28C8]\[u28C9]\[u28CA]\[u28CB]\[u28CC]\[u28CD]\[u28CE]\[u28CF]\[u28D0]\[u28D1]\[u28D2]\[u28D3]\[u28D4]\[u28D5]\[u28D6]\[u28D7]\[u28D8]\[u28D9]\[u28DA]\[u28DB]\[u28DC]\[u28DD]\[u28DE]\[u28DF]\[u28E0]\[u28E1]\[u28E2]\[u28E3]\[u28E4]\[u28E5]\[u28E6]\[u28E7]\[u28E8]\[u28E9]\[u28EA]\[u28EB]\[u28EC]\[u28ED]\[u28EE]\[u28EF]\[u28F0]\[u28F1]\[u28F2]\[u28F3]\[u28F4]\[u28F5]\[u28F6]\[u28F7]\[u28F8]\[u28F9]\[u28FA]\[u28FB]\[u28FC]\[u28FD]\[u28FE]\[u28FF]" charset to render 2x4 blocks on canvas .It Sy "disp_pixel_random " No "False" diff --git a/visidata/metasheets.py b/visidata/metasheets.py index 4ad779f79..049ebd6ee 100644 --- a/visidata/metasheets.py +++ b/visidata/metasheets.py @@ -74,11 +74,11 @@ class VisiDataMetaSheet(TsvSheet): pass # commandline must not override these for internal sheets -VisiDataMetaSheet.class_options.delimiter = vd_system_sep -VisiDataMetaSheet.class_options.header = 1 -VisiDataMetaSheet.class_options.skip = 0 -VisiDataMetaSheet.class_options.row_delimiter = '\n' -VisiDataMetaSheet.class_options.encoding = 'utf-8' +VisiDataMetaSheet.options.delimiter = vd_system_sep +VisiDataMetaSheet.options.header = 1 +VisiDataMetaSheet.options.skip = 0 +VisiDataMetaSheet.options.row_delimiter = '\n' +VisiDataMetaSheet.options.encoding = 'utf-8' class OptionsSheet(Sheet): @@ -237,7 +237,7 @@ def join_cols(sheet): OptionsSheet.addCommand(None, 'edit-option', 'editOption(cursorRow)', 'edit option at current row') OptionsSheet.bindkey('e', 'edit-option') OptionsSheet.bindkey(ENTER, 'edit-option') -MetaSheet.class_options.header = 0 +MetaSheet.options.header = 0 vd.addGlobals({ diff --git a/visidata/path.py b/visidata/path.py index 4c76a9bec..bf6a12bd7 100644 --- a/visidata/path.py +++ b/visidata/path.py @@ -348,6 +348,9 @@ def read(self, n=None): break # end of file return r + def write(self, s): + return self.iter_lines.write(s) + def tell(self): '''Tells the current position as an opaque line marker.''' return self.iter.nextIndex diff --git a/visidata/pivot.py b/visidata/pivot.py index d72b63569..dbda705fd 100644 --- a/visidata/pivot.py +++ b/visidata/pivot.py @@ -29,6 +29,7 @@ def formatRange(col, numeric_key): return col.format(a) return ' - '.join(col.format(x) for x in numeric_key) + class RangeColumn(Column): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -42,6 +43,18 @@ def _format(self, typedval, *args, **kwargs): return None return formatRange(self.origcol, typedval) + +def AggrColumn(aggcol, aggregator): + aggname = '%s_%s' % (aggcol.name, aggregator.name) + + return Column(aggname, + type=aggregator.type or aggcol.type, + fmtstr=aggcol.fmtstr, + getter=lambda col,row,agg=aggregator: agg(col.origCol, row.sourcerows), + origCol=aggcol, + ) + + class PivotSheet(Sheet): 'Summarize key columns in pivot table and display as new sheet.' rowtype = 'grouped rows' # rowdef: PivotGroupRow @@ -116,12 +129,7 @@ def addAggregateCols(self): if not self.pivotCols: for aggcol, aggregatorlist in aggcols.items(): for aggregator in aggregatorlist: - aggname = '%s_%s' % (aggcol.name, aggregator.name) - - c = Column(aggname, - type=aggregator.type or aggcol.type, - fmtstr=aggcol.fmtstr, - getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows)) + c = AggrColumn(aggcol, aggregator) self.addColumn(c) # add pivoted columns @@ -258,8 +266,18 @@ def groupRows(self, rowfunc=None): c.setCache(True) +@PivotSheet.api +def addcol_aggr(sheet, col): + hasattr(col, 'origCol') or vd.fail('not an aggregation column') + for agg in vd.chooseMany(vd.aggregator_choices): + sheet.addColumnAtCursor(AggrColumn(col.origCol, vd.aggregators[agg])) + + Sheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column') +PivotSheet.addCommand('', 'addcol-aggr', 'addcol_aggr(cursorCol)', 'add aggregation column from source of current column') +vd.addMenuItem('Column', 'Add column', 'aggregator', 'addcol-aggr') + vd.addGlobals({ 'Pivot': Pivot, 'PivotGroupRow': PivotGroupRow, diff --git a/visidata/pyobj.py b/visidata/pyobj.py index 3055d894e..c3a97eab9 100644 --- a/visidata/pyobj.py +++ b/visidata/pyobj.py @@ -405,8 +405,9 @@ def launch_repl(v, i): Sheet.addCommand(')', 'contract-col', 'closeColumn(sheet, cursorCol)', 'unexpand current column; restore original column and remove other columns at this level') -Sheet.addCommand(ENTER, 'open-row-pyobj', 'vd.push(openRow(cursorRow))', 'open sheet with copies of rows referenced in current row') -Sheet.addCommand('z'+ENTER, 'open-cell-pyobj', 'vd.push(openCell(cursorCol, cursorRow))', 'open sheet with copies of rows referenced in current cell') +Sheet.addCommand('', 'open-row-basic', 'vd.push(TableSheet.openRow(sheet, cursorRow))', 'open sheet with open sheet with copies of rows referenced in current row') +Sheet.addCommand(ENTER, 'open-row', 'vd.push(openRow(cursorRow))', 'open current row with sheet-specific dive') +Sheet.addCommand('z'+ENTER, 'open-cell', 'vd.push(openCell(cursorCol, cursorRow))', 'open sheet with copies of rows referenced in current cell') Sheet.addCommand('g'+ENTER, 'dive-selected', 'for r in selectedRows: vd.push(openRow(r))', 'open sheet with copies of rows referenced in selected rows') Sheet.addCommand('gz'+ENTER, 'dive-selected-cells', 'for r in selectedRows: vd.push(openCell(cursorCol, r))', 'open sheet with copies of rows referenced in selected rows') diff --git a/visidata/repeat.py b/visidata/repeat.py new file mode 100644 index 000000000..f1fac1ea8 --- /dev/null +++ b/visidata/repeat.py @@ -0,0 +1,10 @@ +from visidata import vd, BaseSheet + +BaseSheet.addCommand('', 'repeat-last', 'execCommand(vd.cmdlog.rows[-1].longname) if vd.cmdlog.rows else fail("no recent command to repeat")', 'run most recent command with an empty, queried input') +BaseSheet.addCommand('', 'repeat-input', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_n(r, 1)', 'run previous modifying command (incl input)') +BaseSheet.addCommand('', 'repeat-input-n', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_n(r, input("# times to repeat prev command:", value=1))', 'run previous command (incl its input) N times') +BaseSheet.addCommand('', 'repeat-input-selected', 'r = copy(vd.cmdlog.rows[-1]) if vd.cmdlog.rows else fail("no recent command to repeat"); vd.cmdlog.repeat_for_selected(r)', 'run previous command (incl its input) for each selected row') + +vd.addMenuItem('Edit', 'Repeat', 'last command', 'repeat-input') +vd.addMenuItem('Edit', 'Repeat', 'last command N times', 'repeat-input-n') +vd.addMenuItem('Edit', 'Repeat', 'last command for all selected rows', 'repeat-input-selected') diff --git a/visidata/selection.py b/visidata/selection.py index 7e1a66422..9c51ed510 100644 --- a/visidata/selection.py +++ b/visidata/selection.py @@ -19,11 +19,35 @@ def toggle(self, rows): if not self.unselectRow(r): self.selectRow(r) + +@Sheet.api +def select_row(self, row): + 'Add single *row* to set of selected rows.' + self.addUndoSelection() + self.selectRow(row) + + +@Sheet.api +def toggle_row(self, row): + 'Toggle selection of given *row*.' + self.addUndoSelection() + if not self.unselectRow(row): + self.selectRow(row) + + +@Sheet.api +def unselect_row(self, row): + 'Remove single *row* from set of selected rows.' + self.addUndoSelection() + self.unselectRow(row) or vd.warning('row not selected') + + @Sheet.api def selectRow(self, row): - 'Add *row* to set of selected rows. Overrideable.' + 'Add *row* to set of selected rows. May be called multiple times in one command. Overrideable.' self._selectedRows[self.rowid(row)] = row + @Sheet.api def unselectRow(self, row): 'Remove *row* from set of selected rows. Return True if row was previously selected. Overrideable.' @@ -45,7 +69,7 @@ def select(self, rows, status=True, progress=True): "Add *rows* to set of selected rows. Async. Don't show progress if *progress* is False; don't show status if *status* is False." self.addUndoSelection() before = self.nSelectedRows - if options.bulk_select_clear: + if self.options.bulk_select_clear: self.clearSelected() for r in (Progress(rows, 'selecting') if progress else rows): self.selectRow(r) @@ -135,9 +159,9 @@ def addUndoSelection(sheet): vd.addUndo(undoAttrCopyFunc([sheet], '_selectedRows')) -Sheet.addCommand('t', 'stoggle-row', 'toggle([cursorRow]); cursorDown(1)', 'toggle selection of current row') -Sheet.addCommand('s', 'select-row', 'select([cursorRow]); cursorDown(1)', 'select current row') -Sheet.addCommand('u', 'unselect-row', 'unselect([cursorRow]); cursorDown(1)', 'unselect current row') +Sheet.addCommand('t', 'stoggle-row', 'toggle_row(cursorRow); cursorDown(1)', 'toggle selection of current row') +Sheet.addCommand('s', 'select-row', 'select_row(cursorRow); cursorDown(1)', 'select current row') +Sheet.addCommand('u', 'unselect-row', 'unselect_row(cursorRow); cursorDown(1)', 'unselect current row') Sheet.addCommand('gt', 'stoggle-rows', 'toggle(rows)', 'toggle selection of all rows') Sheet.addCommand('gs', 'select-rows', 'select(rows)', 'select all rows') diff --git a/visidata/settings.py b/visidata/settings.py index f151b95e9..6db415718 100644 --- a/visidata/settings.py +++ b/visidata/settings.py @@ -372,29 +372,41 @@ def loadConfigAndPlugins(vd, args=AttrDict()): sys.path.append(str(visidata.Path(vd.options.visidata_dir)/"plugins-deps")) # autoload installed plugins first - if vd.options.plugins_autoload: + args_plugins_autoload = args.plugins_autoload if 'plugins_autoload' in args else True + if not args.nothing and args_plugins_autoload and vd.options.plugins_autoload: from importlib_metadata import entry_points # a backport which supports < 3.8 https://github.com/pypa/twine/pull/732 - - for ep in entry_points().get('visidata.plugins', []): - plug = ep.load() - sys.modules[f'visidata.plugins.{ep.name}'] = plug - vd.status(f'Plugin {ep.name} loaded') - - # import plugins from .visidata/plugins before .visidatarc, so plugin options can be overridden - for modname in (args.imports or vd.options.imports or '').split(): try: - vd.addGlobals(importlib.import_module(modname).__dict__) - except ModuleNotFoundError as e: - # issue #1131 - if 'plugins' in e.args[0]: - continue - vd.exceptionCaught(e) + eps = entry_points() + eps_visidata = eps.select(group='visidata.plugins') if 'visidata.plugins' in eps.groups else [] except Exception as e: - vd.exceptionCaught(e) - continue + eps_visidata = [] + vd.warning('plugin autoload failed; see issue #1529') + + for ep in eps_visidata: + try: + plug = ep.load() + sys.modules[f'visidata.plugins.{ep.name}'] = plug + vd.debug(f'Plugin {ep.name} loaded') + except Exception as e: + vd.warning(f'Plugin {ep.name} failed to load') + vd.exceptionCaught(e) + continue + + # import plugins from .visidata/plugins before .visidatarc, so plugin options can be overridden + for modname in (args.imports or vd.options.imports or '').split(): + try: + vd.addGlobals(importlib.import_module(modname).__dict__) + except ModuleNotFoundError as e: #1131 + if 'plugins' in e.args[0]: + continue + vd.exceptionCaught(e) + except Exception as e: + vd.exceptionCaught(e) + continue # user customisations in config file in standard location - vd.loadConfigFile(vd.options.config, vd.getGlobals()) + if vd.options.config: + vd.loadConfigFile(vd.options.config, vd.getGlobals()) vd.option('visidata_dir', '~/.visidata/', 'directory to load and store additional files', sheettype=None) diff --git a/visidata/sheets.py b/visidata/sheets.py index f793e659a..5d2fcf509 100644 --- a/visidata/sheets.py +++ b/visidata/sheets.py @@ -956,7 +956,6 @@ def reload(self): class IndexSheet(Sheet): 'Base class for tabular sheets with rows that are Sheets.' rowtype = 'sheets' # rowdef: Sheet - precious = False columns = [ Column('name', getter=lambda c,r: r.names[-1], setter=lambda c,r,v: setitem(r.names, -1, v)), @@ -1004,6 +1003,7 @@ class SheetsSheet(IndexSheet): ColumnAttr('progressPct'), # ColumnAttr('threads', 'currentThreads', type=vlen), ] + precious = False nKeys = 1 def reload(self): self.rows = self.source @@ -1038,7 +1038,7 @@ def remove(vd, vs): @VisiData.api -def push(vd, vs, pane=0): +def push(vd, vs, pane=0, load=True): 'Push Sheet *vs* onto ``vd.sheets`` stack for *pane* (0 for active pane, -1 for inactive pane). Remove from other position if already on sheets stack.' if not isinstance(vs, BaseSheet): return # return instead of raise, some commands need this @@ -1061,7 +1061,8 @@ def push(vd, vs, pane=0): if vs.precious and vs not in vd.allSheets: vd.allSheets.append(vs) - vs.ensureLoaded() + if load: + vs.ensureLoaded() @VisiData.lazy_property @@ -1148,11 +1149,13 @@ def _async_deepcopy(newlist, oldlist): return ret -IndexSheet.class_options.header = 0 -IndexSheet.class_options.skip = 0 +IndexSheet.options.header = 0 +IndexSheet.options.skip = 0 BaseSheet.init('pane', lambda: 1) +Sheet.init('_ordering', list, copy=True) # (col:Column, reverse:bool) + globalCommand('S', 'sheets-stack', 'vd.push(vd.sheetsSheet)', 'open Sheets Stack: join or jump between the active sheets on the current stack') globalCommand('gS', 'sheets-all', 'vd.push(vd.allSheetsSheet)', 'open Sheets Sheet: join or jump between all sheets from current session') diff --git a/visidata/shell.py b/visidata/shell.py index 68fbe3095..449b4a9e4 100644 --- a/visidata/shell.py +++ b/visidata/shell.py @@ -222,7 +222,7 @@ def inputShell(vd): Sheet.addCommand('z;', 'addcol-sh', 'cmd=inputShell(); addShellColumns(cmd, sheet)', 'create new column from bash expression, with $columnNames as variables') -DirSheet.addCommand(ENTER, 'open-row', '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') +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') DirSheet.addCommand('g'+ENTER, 'open-rows', 'for r in selectedRows: vd.push(openSource(r))', 'open selected files as new sheets') DirSheet.addCommand('^O', 'sysopen-row', 'launchEditor(cursorRow)', 'open current file in external $EDITOR') DirSheet.addCommand('g^O', 'sysopen-rows', 'launchEditor(*selectedRows)', 'open selected files in external $EDITOR') diff --git a/visidata/sort.py b/visidata/sort.py index 0a305e2b0..8678b30b4 100644 --- a/visidata/sort.py +++ b/visidata/sort.py @@ -1,9 +1,6 @@ from copy import copy from visidata import vd, asyncthread, Progress, Sheet, options, UNLOADED -Sheet.init('_ordering', list, copy=True) # (col:Column, reverse:bool) - - @Sheet.api def orderBy(sheet, *cols, reverse=False): 'Add *cols* to internal ordering and re-sort the rows accordingly. Pass *reverse* as True to order these *cols* descending. Pass empty *cols* (or cols[0] of None) to clear internal ordering.' diff --git a/visidata/textsheet.py b/visidata/textsheet.py index 1432ddd6d..880cdee29 100644 --- a/visidata/textsheet.py +++ b/visidata/textsheet.py @@ -88,6 +88,6 @@ def recentErrorsSheet(self): TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit') -TextSheet.class_options.save_filetype = 'txt' +TextSheet.options.save_filetype = 'txt' vd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})