From 85d06f8c2197f1e7441e613460ac9c0478caa130 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:44:24 -0800 Subject: [PATCH 01/11] [main-] parse +a:b arg as col:row instead of row:col Removes unused code that passed wrong type to Path: "Path(inputs[-1])" --- visidata/features/slide.py | 34 +++++++++++++++---------------- visidata/main.py | 41 ++++++++++++++++++++------------------ visidata/man/vd.inc | 6 +++--- visidata/man/vd.txt | 4 ++-- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/visidata/features/slide.py b/visidata/features/slide.py index a7cbb27b2..69b24bd79 100644 --- a/visidata/features/slide.py +++ b/visidata/features/slide.py @@ -116,20 +116,20 @@ def t(vdx, golden): def test_slide_keycol_1(vd): t = make_tester(f''' open-file {sample_path} - +::OrderDate key-col - +::Region key-col - +::Rep key-col + +:OrderDate: key-col + +:Region: key-col + +:Rep: key-col ''') t('', 'OrderDate Region Rep Item Units Unit_Cost Total') - t('+::Rep slide-leftmost', 'Rep OrderDate Region Item Units Unit_Cost Total') - t('+::OrderDate slide-rightmost', 'Region Rep OrderDate Item Units Unit_Cost Total') - t('+::Rep slide-left', 'OrderDate Rep Region Item Units Unit_Cost Total') - t('+::OrderDate slide-right', 'Region OrderDate Rep Item Units Unit_Cost Total') + t('+:Rep: slide-leftmost', 'Rep OrderDate Region Item Units Unit_Cost Total') + t('+:OrderDate: slide-rightmost', 'Region Rep OrderDate Item Units Unit_Cost Total') + t('+:Rep: slide-left', 'OrderDate Rep Region Item Units Unit_Cost Total') + t('+:OrderDate: slide-right', 'Region OrderDate Rep Item Units Unit_Cost Total') t(''' - +::Item key-col - +::Item slide-left + +:Item: key-col + +:Item: slide-left slide-left slide-right slide-right @@ -141,17 +141,17 @@ def test_slide_keycol_1(vd): def test_slide_leftmost(vd): t = make_tester(f'''open-file {benchmark_path}''') - t('+::Paid slide-leftmost', 'Paid Date Customer SKU Item Quantity Unit') + t('+:Paid: slide-leftmost', 'Paid Date Customer SKU Item Quantity Unit') t = make_tester(f''' open-file {benchmark_path} - +::Date key-col + +:Date: key-col ''') t('', 'Date Customer SKU Item Quantity Unit Paid') - t('''+::Item slide-leftmost''', 'Date Item Customer SKU Quantity Unit Paid') - t('''+::SKU key-col - +::Quantity slide-leftmost''', 'Date SKU Quantity Customer Item Unit Paid') - t('''+::Date slide-leftmost''', 'Date Customer SKU Item Quantity Unit Paid') - t('''+::Item slide-leftmost - +::SKU slide-leftmost''', 'Date SKU Item Customer Quantity Unit Paid') + t('''+:Item: slide-leftmost''', 'Date Item Customer SKU Quantity Unit Paid') + t('''+:SKU: key-col + +:Quantity: slide-leftmost''', 'Date SKU Quantity Customer Item Unit Paid') + t('''+:Date: slide-leftmost''', 'Date Customer SKU Item Quantity Unit Paid') + t('''+:Item: slide-leftmost + +:SKU: slide-leftmost''', 'Date SKU Item Customer Quantity Unit Paid') diff --git a/visidata/main.py b/visidata/main.py index c3db16c60..73d669c4a 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -90,25 +90,27 @@ def duptty(): @visidata.VisiData.api -def parsePos(vd, arg:str, inputs=None): - 'Return (startsheets:list, startrow:str, startcol:str) from *arg* like "+sheet:subsheet:col:row". Empty sheetstr in startsheets means the starting pos applies to all sheets.' - startsheets, startrow, startcol = [], None, None - - if ':' not in arg: - return (None, arg, None) +def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): + '''Return (startsheets:list, startcol:str, startrow:str) from *arg* like "+sheet:subsheet:col:row". + Returns an empty list for *startsheets* when the starting pos applies to all sheets. + Returns None for *startsheets* when the position expression did not specify a sheet. + *inputs* is a list of (path, options) tuples. + ''' + startsheets, startcol, startrow = None, None, None pos = arg.split(':') if len(pos) == 1: - startsheet = [Path(inputs[-1]).base_stem] if inputs else None - start_pos = (startsheet, pos[0], None) + startrow = arg elif len(pos) == 2: - startsheet = [Path(inputs[-1]).base_stem] if inputs else None - startrow, startcol = pos - start_pos = (None, startrow, startcol) - else: # if len(pos) >= 3: + startcol, startrow = pos + else: + # the first element of pos is the startsheet, + # the later elements (if present) describe the branch to a subsheet startsheets = pos[:-2] - startrow, startcol = pos[-2:] - start_pos = (startsheets, startrow, startcol) + startcol, startrow = pos[-2:] + if startcol == '': startcol = None + if startrow == '': startrow = None + start_pos = (startsheets, startcol, startrow) # index subsheets need to be loaded *after* the cursor indexing vd.options.set('load_lazy', True, obj=start_pos[0]) @@ -133,8 +135,8 @@ def outputProgressEvery(vd, sheet, seconds:float=0.5): time.sleep(seconds) @visidata.VisiData.api -def moveToPos(vd, sources, startsheets, startrow, startcol): - sheets = [] # sheets to apply startrow:startcol to +def moveToPos(vd, sources, startsheets, startcol, startrow): + sheets = [] # sheets to apply startcol:startrow to if not startsheets: sheets = sources # apply row/col to all sheets else: @@ -146,10 +148,11 @@ def moveToPos(vd, sources, startsheets, startrow, startcol): vd.sync(vs.ensureLoaded()) vd.clearCaches() - for startsheet in startsheets[1:]: - rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + startsheet) + # descend the tree of subsheets + for subsheet in startsheets[1:]: + rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) if rowidx is None: - vd.warning(f'{vs.name} has no subsheet "{startsheet}"') + vd.warning(f'{vs.name} has no subsheet "{subsheet}"') vs = None break vs = vs.rows[rowidx] diff --git a/visidata/man/vd.inc b/visidata/man/vd.inc index 3feabe280..8cf1e8b47 100644 --- a/visidata/man/vd.inc +++ b/visidata/man/vd.inc @@ -37,7 +37,7 @@ .Nm vd .Op Ar options .Op Ar input No ... -.Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar row Ns : Ns Ar col +.Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row . .Sh DESCRIPTION .Nm VisiData No is an easy-to-use multipurpose tool to explore, clean, edit, and restructure data. @@ -880,8 +880,8 @@ show/hide methods and hidden properties .Bl -tag -width XXXXXXXXXXXXXXXXXXXXXXXXXXX -compact .It Cm -P Ns = Ns Ar longname .No preplay Ar longname No before replay or regular launch; limited to Sy Base Sheet No bound commands -.It Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar row Ns : Ns Ar col -.No launch vd with Ar subsheet No of Ar toplevel No at top-of-stack, and cursor at Ar row No and Ar col Ns ; all arguments are optional +.It Cm + Ns Ar toplevel Ns : Ns Ar subsheet Ns : Ns Ar col Ns : Ns Ar row +.No launch vd with Ar subsheet No of Ar toplevel No at top-of-stack, and cursor at Ar col No and Ar row Ns ; all arguments are optional .It Cm --overwrite Ns = Ns Ar c .No Overwrite with confirmation .It Cm --guides diff --git a/visidata/man/vd.txt b/visidata/man/vd.txt index a21307b38..07180dcc0 100644 --- a/visidata/man/vd.txt +++ b/visidata/man/vd.txt @@ -566,8 +566,8 @@ COMMANDLINE OPTIONS -P=longname preplay longname before replay or regular launch; limited to Base Sheet bound commands - +toplevel:subsheet:row:col launch vd with subsheet of toplevel at - top-of-stack, and cursor at row and col; all + +toplevel:subsheet:col:row launch vd with subsheet of toplevel at + top-of-stack, and cursor at col and row; all arguments are optional --overwrite=c Overwrite with confirmation --guides open Guide Index From 5ea41f6e217f41e76f96bdf0d5427f6fd1844a8e Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:34:02 -0700 Subject: [PATCH 02/11] [main-] apply +:col:row arg to all sheets --- visidata/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/visidata/main.py b/visidata/main.py index 73d669c4a..ad0da68ab 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -100,13 +100,16 @@ def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): pos = arg.split(':') if len(pos) == 1: + startsheets = [Path(inputs[-1][0]).base_stem] if inputs else None startrow = arg elif len(pos) == 2: + startsheets = [Path(inputs[-1][0]).base_stem] if inputs else None startcol, startrow = pos else: # the first element of pos is the startsheet, # the later elements (if present) describe the branch to a subsheet startsheets = pos[:-2] + if startsheets == ['']: startsheets = [] startcol, startrow = pos[-2:] if startcol == '': startcol = None if startrow == '': startrow = None From b5540200b9a2044b531ed58af00ef8002d075aa1 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:23:40 -0800 Subject: [PATCH 03/11] [main-] +startpos: improve error messages for invalid start sheets --- visidata/basesheet.py | 2 +- visidata/main.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/visidata/basesheet.py b/visidata/basesheet.py index e2552c87f..7d3c9424b 100644 --- a/visidata/basesheet.py +++ b/visidata/basesheet.py @@ -337,7 +337,7 @@ def getSheet(vd, sheetname): try: sheetidx = int(sheetname) return vd.sheets[sheetidx] - except ValueError: + except (ValueError, IndexError): pass if sheetname == 'options': diff --git a/visidata/main.py b/visidata/main.py index ad0da68ab..f443b86ee 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -156,9 +156,11 @@ def moveToPos(vd, sources, startsheets, startcol, startrow): rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) if rowidx is None: vd.warning(f'{vs.name} has no subsheet "{subsheet}"') - vs = None - break + return vs = vs.rows[rowidx] + if not isinstance(vs, BaseSheet): + vd.warning(f'row {subsheet} for subsheet is not a sheet') + return vd.sync(vs.ensureLoaded()) vd.clearCaches() if vs: From dd26b719b23d608076edb100c59fa5ce3b004540 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Wed, 29 May 2024 01:22:03 -0700 Subject: [PATCH 04/11] [main-] +startpos: treat numeric subsheet/col/row as idx, not name --- visidata/main.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/visidata/main.py b/visidata/main.py index f443b86ee..d211277db 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -92,6 +92,9 @@ def duptty(): @visidata.VisiData.api def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): '''Return (startsheets:list, startcol:str, startrow:str) from *arg* like "+sheet:subsheet:col:row". + The elements of *startsheets* are identifiers that pick out a sheet, either + a) a string that is a sheet name, or + b) a list of strings that are sheet names or numbers (for row indices). For example ['1', 'sales', '3']. Returns an empty list for *startsheets* when the starting pos applies to all sheets. Returns None for *startsheets* when the position expression did not specify a sheet. *inputs* is a list of (path, options) tuples. @@ -153,11 +156,18 @@ def moveToPos(vd, sources, startsheets, startcol, startrow): vd.clearCaches() # descend the tree of subsheets for subsheet in startsheets[1:]: - rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) - if rowidx is None: - vd.warning(f'{vs.name} has no subsheet "{subsheet}"') + if subsheet and subsheet.isdigit(): + rowidx = int(subsheet) + else: + rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) + if rowidx is None: + vd.warning(f'{vs.name} has no subsheet "{subsheet}"') + return + try: + vs = vs.rows[rowidx] + except IndexError: + vd.warning(f'{vs.name} has no subsheet "{rowidx}"') return - vs = vs.rows[rowidx] if not isinstance(vs, BaseSheet): vd.warning(f'row {subsheet} for subsheet is not a sheet') return @@ -170,16 +180,17 @@ def moveToPos(vd, sources, startsheets, startcol, startrow): if startrow: for vs in sheets: if vs: + if startrow.isdigit(): # treat strings that look like integers as indices, never row keys + startrow = int(startrow) vs.moveToRow(startrow) or vd.warning(f'{vs} has no row "{startrow}"') if startcol: for vs in sheets: if vs: + if startcol.isdigit(): # treat strings that look like integers as indices, never column names + startcol = int(startcol) if not vs.moveToCol(startcol): - if startcol.isdigit(): - vs.moveToCol(int(startcol)) # handle indexing by column number - else: - vd.warning(f'{vs} has no column "{startcol}"') + vd.warning(f'{vs} has no column "{startcol}"') def main_vd(): 'Open the given sources using the VisiData interface.' From a08eb3a664eed9500d0e9ec8f588f048e93c75c6 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 23 Mar 2024 00:34:53 -0700 Subject: [PATCH 05/11] [main-] +startpos: load subsheets before moving cursor --- visidata/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/visidata/main.py b/visidata/main.py index d211277db..f80cbffb0 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -93,8 +93,9 @@ def duptty(): def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): '''Return (startsheets:list, startcol:str, startrow:str) from *arg* like "+sheet:subsheet:col:row". The elements of *startsheets* are identifiers that pick out a sheet, either - a) a string that is a sheet name, or - b) a list of strings that are sheet names or numbers (for row indices). For example ['1', 'sales', '3']. + a) a string that is the name of a sheet or subsheet + b) a string that is numbers (which are indices of a row). + For example ['1', 'sales', '3']. Returns an empty list for *startsheets* when the starting pos applies to all sheets. Returns None for *startsheets* when the position expression did not specify a sheet. *inputs* is a list of (path, options) tuples. @@ -118,9 +119,6 @@ def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): if startrow == '': startrow = None start_pos = (startsheets, startcol, startrow) - # index subsheets need to be loaded *after* the cursor indexing - vd.options.set('load_lazy', True, obj=start_pos[0]) - return start_pos @@ -148,6 +146,9 @@ def moveToPos(vd, sources, startsheets, startcol, startrow): else: startsheet = startsheets[0] or sources[-1] vs = vd.getSheet(startsheet) + # Prevent the sheet from doing automatic ensureLoaded() on its subsheets when it + # loads, so that we can call ensureLoaded() ourselves and sync() on it. + vd.options.set('load_lazy', True, obj=vs) if not vs: vd.warning(f'no sheet "{startsheet}"') return @@ -171,6 +172,7 @@ def moveToPos(vd, sources, startsheets, startcol, startrow): if not isinstance(vs, BaseSheet): vd.warning(f'row {subsheet} for subsheet is not a sheet') return + vd.options.set('load_lazy', True, obj=vs) vd.sync(vs.ensureLoaded()) vd.clearCaches() if vs: From 6f852a452f3be5f494d5b65275ed572a90c4e3ee Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:18:44 -0800 Subject: [PATCH 06/11] [main-] +startpos: retry moves until they succeed --- visidata/main.py | 146 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 46 deletions(-) diff --git a/visidata/main.py b/visidata/main.py index f80cbffb0..778394470 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -15,9 +15,10 @@ import signal import warnings import builtins # to override print +import time from visidata import vd, options, run, BaseSheet, AttrDict -from visidata import Path +from visidata import Path, asyncthread from visidata.settings import _get_config_file import visidata @@ -100,6 +101,7 @@ def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): Returns None for *startsheets* when the position expression did not specify a sheet. *inputs* is a list of (path, options) tuples. ''' + if arg == '': return None startsheets, startcol, startrow = None, None, None pos = arg.split(':') @@ -139,60 +141,110 @@ def outputProgressEvery(vd, sheet, seconds:float=0.5): time.sleep(seconds) @visidata.VisiData.api -def moveToPos(vd, sources, startsheets, startcol, startrow): - sheets = [] # sheets to apply startcol:startrow to - if not startsheets: - sheets = sources # apply row/col to all sheets +def moveToPos(vd, sources, sheet_desc, startcol, startrow): + if sheet_desc == []: + # the moves list must have each of move refer only 1 specific sheet, + # so expand an "all sheets" descriptor into individual sheet descriptors + sheet_descs = [[str(i)] for i in range(len(sources))] else: - startsheet = startsheets[0] or sources[-1] - vs = vd.getSheet(startsheet) - # Prevent the sheet from doing automatic ensureLoaded() on its subsheets when it - # loads, so that we can call ensureLoaded() ourselves and sync() on it. - vd.options.set('load_lazy', True, obj=vs) - if not vs: - vd.warning(f'no sheet "{startsheet}"') - return - - vd.sync(vs.ensureLoaded()) - vd.clearCaches() - # descend the tree of subsheets - for subsheet in startsheets[1:]: + sheet_descs = [sheet_desc] + # for each sheet, attempt column moves first, then rows + if startcol is not None or startrow is not None: + moves = [(d, startcol, None) for d in sheet_descs] + \ + [(d, None, startrow) for d in sheet_descs] + else: + moves = [(d, None, None) for d in sheet_descs] + # start a thread to keep attempting the moves till they all succeed, for sheets that are slow to load + retry_move_to_pos(vd, sources, moves) + +def sheet_from_description(vd, sources, sheet_desc): + '''Return a Sheet to apply col/row to, given a list *sheet_desc* that refers to one specific sheet. + The *sheet_desc* is either a Sheet, or a list of strings similar to the return value of parsePos(), + with the difference that *sheet_desc* will not ever be the empty list that denotes "all sheets". + Return None if no matching sheet was found; if sheets are loading, a subsequent call may return + a matching sheet. + Raise ValueError to indicate that a move failed, and should not be retried.''' + if isinstance(sheet_desc, BaseSheet): + vd.push(sheet_desc) + return sheet_desc + + # descend the tree of subsheets + for desc_lvl, subsheet in enumerate(sheet_desc): + if desc_lvl == 0: + vs = None + #try subsheets as numbers first, then as names + if subsheet.isdigit(): + try: + vs = sources[int(subsheet)] + except IndexError: + pass + else: + vs = vd.getSheet(subsheet) + if not vs: + raise ValueError(f'no sheet "{subsheet}"') + else: if subsheet and subsheet.isdigit(): rowidx = int(subsheet) else: rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) - if rowidx is None: - vd.warning(f'{vs.name} has no subsheet "{subsheet}"') - return try: - vs = vs.rows[rowidx] + if rowidx is None: raise IndexError + vs_subsheet = vs.rows[rowidx] except IndexError: - vd.warning(f'{vs.name} has no subsheet "{rowidx}"') - return - if not isinstance(vs, BaseSheet): - vd.warning(f'row {subsheet} for subsheet is not a sheet') - return + vd.warning(f'sheet {vs.name} has no subsheet "{subsheet}"') + return None + if not isinstance(vs_subsheet, BaseSheet): + raise ValueError(f'row "{subsheet}" is not a sheet in {vs.name}') + vs = vs_subsheet + # if we have any more levels of subsheets to look at, load the current sheet fully + if desc_lvl < len(sheet_desc) - 1: + # Prevent the sheet from doing automatic ensureLoaded() on its subsheets when it + # loads, so that we can call ensureLoaded() ourselves and sync() on it. vd.options.set('load_lazy', True, obj=vs) vd.sync(vs.ensureLoaded()) vd.clearCaches() - if vs: - vd.push(vs) - sheets = [vs] - - if startrow: - for vs in sheets: - if vs: - if startrow.isdigit(): # treat strings that look like integers as indices, never row keys - startrow = int(startrow) - vs.moveToRow(startrow) or vd.warning(f'{vs} has no row "{startrow}"') - - if startcol: - for vs in sheets: - if vs: - if startcol.isdigit(): # treat strings that look like integers as indices, never column names - startcol = int(startcol) - if not vs.moveToCol(startcol): - vd.warning(f'{vs} has no column "{startcol}"') + vd.push(vs) + return vs + +@asyncthread +def retry_move_to_pos(vd, sources, moves, retry_interval=0.1): + while moves: + unmoved = [] + for move in moves: + try: + move_succeeded = attempt_move_to_pos(vd, sources, *move) + except ValueError as e: + # skip failed moves if they can't ever succeed + vd.warning(e) + continue + if not move_succeeded: + unmoved.append(move) + moves = unmoved + if moves: + time.sleep(retry_interval) + +def attempt_move_to_pos(vd, sources, sheet_desc, startcol, startrow): + '''Return True if the move succeeded in moving to the row and column, on the described sheet.''' + vs = sheet_from_description(vd, sources, sheet_desc) + if not vs: + return False + success = True + if startrow is not None: + if startrow.isdigit(): # treat strings that look like integers as indices, never row keys + startrow = int(startrow) + if not vs.moveToRow(startrow): + if vs.nRows > 0: # avoid uninformative warnings early in startup + vd.warning(f'{vs} has no row {startrow}: n_rows={len(vs.rows)}"') + success = False + + if startcol is not None: + if startcol.isdigit(): # treat strings that look like integers as indices, never column names + startcol = int(startcol) + if not vs.moveToCol(startcol): + if vs.nRows > 0: + vd.warning(f'{vs} has no column {startcol}') + success = False + return success def main_vd(): 'Open the given sources using the VisiData interface.' @@ -280,7 +332,9 @@ def main_vd(): if flGlobal: global_args[optname] = optval elif arg.startswith('+'): # position cursor at start - after_config.append((vd.moveToPos, *vd.parsePos(arg[1:], inputs=inputs))) + parsed_pos = vd.parsePos(arg[1:], inputs=inputs) + if parsed_pos: + after_config.append((vd.moveToPos, *parsed_pos)) elif current_args.get('play', None) and '=' in arg: # parse 'key=value' pairs for formatting cmdlog template in replay mode k, v = arg.split('=', maxsplit=1) From c498b227f3bfaedc911f5e6aec80f5b0172c70b6 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:56:04 -0700 Subject: [PATCH 07/11] [main-] make arg +sheetname:: switch to sheetname --- visidata/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/visidata/main.py b/visidata/main.py index 778394470..0deedb87f 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -228,6 +228,12 @@ def attempt_move_to_pos(vd, sources, sheet_desc, startcol, startrow): vs = sheet_from_description(vd, sources, sheet_desc) if not vs: return False + # switch the active sheet, for command line args like +s:: + if vs and startrow is None and startcol is None: + vd.push(vs) + return True + + # try cursor moves success = True if startrow is not None: if startrow.isdigit(): # treat strings that look like integers as indices, never row keys From 69ccfb642b67cb65619b3c8b2ca7234cd6f35e3b Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Fri, 22 Nov 2024 02:41:41 -0800 Subject: [PATCH 08/11] [main-] apply +:subsheet:col:row to all sheets This handles the case where the sheet is the empty string, and a subsheet is specified. --- visidata/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/visidata/main.py b/visidata/main.py index 0deedb87f..0d712818a 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -142,10 +142,10 @@ def outputProgressEvery(vd, sheet, seconds:float=0.5): @visidata.VisiData.api def moveToPos(vd, sources, sheet_desc, startcol, startrow): - if sheet_desc == []: - # the moves list must have each of move refer only 1 specific sheet, - # so expand an "all sheets" descriptor into individual sheet descriptors - sheet_descs = [[str(i)] for i in range(len(sources))] + if sheet_desc == [] or sheet_desc[0] == '': + ## the list moves must have each of its elements refer only 1 + # sheet, so expand the "all sheets" sheet descriptor into individual sheets + sheet_descs = [[str(i)] + sheet_desc[1:] for i, sheet in enumerate(sources)] else: sheet_descs = [sheet_desc] # for each sheet, attempt column moves first, then rows From f65db0b5e28dc3561f38bef2c827b20a5ecc7043 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Sat, 23 Nov 2024 03:23:26 -0800 Subject: [PATCH 09/11] [main-] fail moveToRow if row idx is past the sheet end Previously, moveToRow() would return True, and set the cursor row index to the nonexistent row, with no visible errors. For slow loading sheets, checkCursor() could then move the cursor to an earlier row. --- visidata/cmdlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visidata/cmdlog.py b/visidata/cmdlog.py index 2b2bf9a17..35b9491a8 100644 --- a/visidata/cmdlog.py +++ b/visidata/cmdlog.py @@ -87,7 +87,7 @@ def isLoggableSheet(sheet): def moveToRow(vs, rowstr): 'Move cursor to row given by *rowstr*, which can be either the row number or keystr.' rowidx = vs.getRowIndexFromStr(rowstr) - if rowidx is None: + if rowidx is None or rowidx >= vs.nRows: return False vs.cursorRowIndex = rowidx From 74ccc8b1bf5c843e5f0e46f9c59327ba50df4dbd Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Fri, 1 Nov 2024 02:07:33 -0700 Subject: [PATCH 10/11] [man-] add comprehensive examples for +startpos syntax --- visidata/man/vd.inc | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/visidata/man/vd.inc b/visidata/man/vd.inc index 8cf1e8b47..b9f55ccf0 100644 --- a/visidata/man/vd.inc +++ b/visidata/man/vd.inc @@ -959,11 +959,36 @@ disable loading .visidatarc and plugin addons .Dl Ic vd newfile.tsv .No open a blank sheet named Ar newfile No if file does not exist .Pp -.Dl Ic vd sample.xlsx +:sheet1:2:3 -.No launch with Sy sheet1 No at top-of-stack, and cursor at column Sy 2 No and row Sy 3 -.Pp .Dl Ic vd -P open-plugins .No preplay longname Sy open-plugins No before starting the session +.Pp +.Dl Ic vd a.xlsx b.tsv c.tsv +0:3:1:2 +.No launch with cursor for sheet Sy 0 No (a.xlsx) at subsheet Sy 3 No in sheet Sy 0 Ns , col Sy 1 Ns , row Sy 2 +.Dl Ic vd a.xlsx b.tsv c.tsv +0:1:2 +.No launch with cursor for sheet Sy 0 No (a.xlsx) at col Sy 1 Ns , row Sy 2 +.Dl Ic vd a.tsv b.tsv c.tsv +1:2 +.No launch with cursor for last sheet (c.tsv) at col Sy 1 Ns , row Sy 2 +.Dl Ic vd a.tsv b.tsv c.tsv +1: +.No launch with cursor for last sheet (c.tsv) at col Sy 1 +.Dl Ic vd a.tsv b.tsv c.tsv +:2 +.No launch with cursor for last sheet (c.tsv) at row Sy 2 +.Pp +.Dl Ic vd a.xlsx b.xlsx c.xlsx +0:annual:1:2 +1:sales:monthly:3:4 +.No launch with cursor for sheet Sy 0 No (a.xlsx) in subsheet Sy annual No at col Sy 1 Ns , row Sy 2 Ns , and cursor for sheet Sy 1 No (b.xlsx), subsheet Sy sales No in subsheet monthly; col Sy 3 Ns , row Sy 4 +.Pp +.Dl Ic vd a.tsv b.tsv c.tsv +1:: +.No launch in sheet Sy 1 No (b.tsv), with cursor at col 0, row 0 +.Dl Ic vd a.tsv b.tsv c.tsv +-2:: +.No launch in Sy 2nd No from last sheet (b.tsv), with cursor at col 0, row 0 +.Pp +.Dl Ic vd a.tsv b.tsv c.tsv +:1: +.No launch with cursors for all sheets at col Sy 1 +.Dl Ic vd a.tsv b.tsv c.tsv +::2 +.No launch with cursors for all sheets at row Sy 2 +.Dl Ic vd a.tsv b.tsv c.tsv +:1:2 +.No launch with cursors for all sheets at col Sy 1 Ns , row Sy 2 +.Dl Ic vd a.xlsx b.xslx c.xlsx +:a:1:2 +.No launch with cursors for all sheets at subsheet Sy a Ns , col Sy 1 Ns , row Sy 2 .Sh FILES At the start of every session, .Sy VisiData No looks for Pa $HOME/.visidatarc Ns , and calls Python exec() on its contents if it exists. From db6207c11a95bf188891ebf4213127855efa0f52 Mon Sep 17 00:00:00 2001 From: midichef <67946319+midichef@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:39:07 -0800 Subject: [PATCH 11/11] [main-] convert int strings for sheet/subsheet/col/row to ints Using int indices internally is better than using sheet names, because sheet names were ambiguous when sheets had the same name, for example, the name "sample" in: vd sample.tsv b.tsv sample.json +1:2 --- visidata/main.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/visidata/main.py b/visidata/main.py index 0d712818a..50b4a41bb 100755 --- a/visidata/main.py +++ b/visidata/main.py @@ -95,8 +95,8 @@ def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): '''Return (startsheets:list, startcol:str, startrow:str) from *arg* like "+sheet:subsheet:col:row". The elements of *startsheets* are identifiers that pick out a sheet, either a) a string that is the name of a sheet or subsheet - b) a string that is numbers (which are indices of a row). - For example ['1', 'sales', '3']. + b) integers (which are indices of a row or column, or a sheet number). + For example [1, 'sales', 3]. Returns an empty list for *startsheets* when the starting pos applies to all sheets. Returns None for *startsheets* when the position expression did not specify a sheet. *inputs* is a list of (path, options) tuples. @@ -104,12 +104,20 @@ def parsePos(vd, arg:str, inputs:'list[tuple[str, dict]]'=None): if arg == '': return None startsheets, startcol, startrow = None, None, None - pos = arg.split(':') + pos = [] + # convert any numeric index strings to ints + for idx in arg.split(':'): + if idx: + if idx.isdigit() or (idx[0] == '-' and idx[1:].isdigit()): + idx = int(idx) + pos.append(idx) + if len(pos) == 1: - startsheets = [Path(inputs[-1][0]).base_stem] if inputs else None + # -1 means the last sheet in the list of open sheets + startsheets = [-1] if inputs else None startrow = arg elif len(pos) == 2: - startsheets = [Path(inputs[-1][0]).base_stem] if inputs else None + startsheets = [-1] if inputs else None startcol, startrow = pos else: # the first element of pos is the startsheet, @@ -145,7 +153,7 @@ def moveToPos(vd, sources, sheet_desc, startcol, startrow): if sheet_desc == [] or sheet_desc[0] == '': ## the list moves must have each of its elements refer only 1 # sheet, so expand the "all sheets" sheet descriptor into individual sheets - sheet_descs = [[str(i)] + sheet_desc[1:] for i, sheet in enumerate(sources)] + sheet_descs = [[i] + sheet_desc[1:] for i, sheet in enumerate(sources)] else: sheet_descs = [sheet_desc] # for each sheet, attempt column moves first, then rows @@ -159,7 +167,7 @@ def moveToPos(vd, sources, sheet_desc, startcol, startrow): def sheet_from_description(vd, sources, sheet_desc): '''Return a Sheet to apply col/row to, given a list *sheet_desc* that refers to one specific sheet. - The *sheet_desc* is either a Sheet, or a list of strings similar to the return value of parsePos(), + The *sheet_desc* is either a Sheet, or a list of strings/ints similar to the return value of parsePos(), with the difference that *sheet_desc* will not ever be the empty list that denotes "all sheets". Return None if no matching sheet was found; if sheets are loading, a subsequent call may return a matching sheet. @@ -173,9 +181,9 @@ def sheet_from_description(vd, sources, sheet_desc): if desc_lvl == 0: vs = None #try subsheets as numbers first, then as names - if subsheet.isdigit(): + if isinstance(subsheet, int): try: - vs = sources[int(subsheet)] + vs = sources[subsheet] except IndexError: pass else: @@ -183,8 +191,8 @@ def sheet_from_description(vd, sources, sheet_desc): if not vs: raise ValueError(f'no sheet "{subsheet}"') else: - if subsheet and subsheet.isdigit(): - rowidx = int(subsheet) + if isinstance(subsheet, int): + rowidx = subsheet else: rowidx = vs.getRowIndexFromStr(vd.options.rowkey_prefix + subsheet) try: @@ -219,12 +227,13 @@ def retry_move_to_pos(vd, sources, moves, retry_interval=0.1): continue if not move_succeeded: unmoved.append(move) - moves = unmoved - if moves: + if unmoved: time.sleep(retry_interval) + moves = unmoved def attempt_move_to_pos(vd, sources, sheet_desc, startcol, startrow): - '''Return True if the move succeeded in moving to the row and column, on the described sheet.''' + '''Return True if the move succeeded in moving to the row and column, on the described sheet. + Raise ValueError to indicate that a move failed, and should not be retried.''' vs = sheet_from_description(vd, sources, sheet_desc) if not vs: return False @@ -236,16 +245,12 @@ def attempt_move_to_pos(vd, sources, sheet_desc, startcol, startrow): # try cursor moves success = True if startrow is not None: - if startrow.isdigit(): # treat strings that look like integers as indices, never row keys - startrow = int(startrow) if not vs.moveToRow(startrow): if vs.nRows > 0: # avoid uninformative warnings early in startup vd.warning(f'{vs} has no row {startrow}: n_rows={len(vs.rows)}"') success = False if startcol is not None: - if startcol.isdigit(): # treat strings that look like integers as indices, never column names - startcol = int(startcol) if not vs.moveToCol(startcol): if vs.nRows > 0: vd.warning(f'{vs} has no column {startcol}')