We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bugs with save-all
        I've a question about saving.

1. I load a two sheet notebook "responses-grades-cda.xlsx".
2. `g ctrl+s`
3. it gives me a blank line -- why not default to name of file without xlsx extension?
4. I type in file name without extension + `vds`: "responses-grades-cda.vds".
5. I get an error: "AttributeError: 'X1sxIndexSheet' object has no attribute 'join'"

_Originally posted by @reagle in https://github.com/saulpw/visidata/discussions/1266#discussioncomment-4870711_
      

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `visidata/save.py`
Content:
```
1 import collections
2 
3 from visidata import *
4 
5 
6 vd.option('confirm_overwrite', True, 'whether to prompt for overwrite confirmation on save')
7 vd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)
8 
9 @Sheet.api
10 def safe_trdict(vs):
11     'returns string.translate dictionary for replacing tabs and newlines'
12     if options.safety_first:
13         delim = vs.options.delimiter
14         return {
15              0: '', #  strip NUL completely
16     ord(delim): vs.options.tsv_safe_tab, # \t
17             10: vs.options.tsv_safe_newline,  # \n
18             13: vs.options.tsv_safe_newline,  # \r
19         }
20     return {}
21 
22 
23 @Sheet.api
24 def iterdispvals(sheet, *cols, format=False):
25     'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'
26     if not cols:
27         cols = sheet.visibleCols
28 
29     transformers = collections.OrderedDict()  # list of transformers for each column in order
30     for col in cols:
31         transformers[col] = [ col.type ]
32         if format:
33             formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))
34             transformers[col].append(formatMaker(col._formatdict))
35         trdict = sheet.safe_trdict()
36         if trdict:
37             transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))
38 
39     options_safe_error = options.safe_error
40     for r in Progress(sheet.rows):
41         dispvals = collections.OrderedDict()  # [col] -> value
42         for col, transforms in transformers.items():
43             try:
44                 dispval = col.getValue(r)
45 
46             except Exception as e:
47                 vd.exceptionCaught(e)
48                 dispval = options_safe_error or str(e)
49 
50             try:
51                 for t in transforms:
52                     if dispval is None:
53                         break
54                     elif isinstance(dispval, TypedExceptionWrapper):
55                         dispval = options_safe_error or str(dispval)
56                         break
57                     else:
58                         dispval = t(dispval)
59 
60                 if dispval is None and format:
61                     dispval = ''
62             except Exception as e:
63                 dispval = str(dispval)
64 
65             dispvals[col] = dispval
66 
67         yield dispvals
68 
69 
70 @Sheet.api
71 def itervals(sheet, *cols, format=False):
72     for row in sheet.iterdispvals(*cols, format=format):
73         yield [row[c] for c in cols]
74 
75 @BaseSheet.api
76 def getDefaultSaveName(sheet):
77     src = getattr(sheet, 'source', None)
78     if hasattr(src, 'scheme') and src.scheme:
79         return src.name + src.suffix
80     if isinstance(src, Path):
81         if sheet.options.is_set('save_filetype', sheet):
82             # if save_filetype is over-ridden from default, use it as the extension
83             return str(src.with_suffix('')) + '.' + sheet.options.save_filetype
84         return str(src)
85     else:
86         return sheet.name+'.'+getattr(sheet, 'filetype', options.save_filetype)
87 
88 
89 @VisiData.api
90 def save_cols(vd, cols):
91     sheet = cols[0].sheet
92     vs = copy(sheet)
93     vs.columns = list(cols)
94     vs.rows = sheet.rows
95     if len(cols) == 1:
96         savedcoltxt = cols[0].name + ' column'
97     else:
98         savedcoltxt = '%s columns' % len(cols)
99     path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())
100     vd.saveSheets(path, vs, confirm_overwrite=options.confirm_overwrite)
101 
102 
103 @VisiData.api
104 def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):
105     'Save all *vsheets* to *givenpath*.'
106 
107     filetype = givenpath.ext or options.save_filetype
108 
109     vd.clearCaches()
110 
111     savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)
112 
113     if savefunc is None:
114         vd.fail(f'no function to save as {filetype}')
115 
116     if givenpath.exists() and confirm_overwrite:
117         vd.confirm("%s already exists. overwrite? " % givenpath.given)
118 
119     vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))
120 
121     if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /
122         for vs in vsheets:
123             vs.hasBeenModified = False
124         # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)
125         # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)
126         if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:
127             vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
128         return vd.execAsync(savefunc, givenpath, *vsheets)
129 
130     # path is a dir
131 
132     # save as individual files in the givenpath directory
133     try:
134         os.makedirs(givenpath, exist_ok=True)
135     except FileExistsError:
136         pass
137 
138     if not givenpath.is_dir():
139         vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
140 
141     def _savefiles(vsheets, givenpath, savefunc, filetype):
142         for vs in vsheets:
143             p = Path((givenpath / vs.name).with_suffix('.'+filetype))
144             savefunc(p, vs)
145             vs.hasBeenModified = False
146     return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)
147 
148 
149 @VisiData.api
150 def save_zip(vd, p, *vsheets):
151     vd.clearCaches()
152 
153     import tempfile
154     import zipfile
155     with tempfile.TemporaryDirectory() as tmpdir:
156         with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:
157             for vs in Progress(vsheets):
158                 filetype = vs.options.save_filetype
159                 tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')
160                 savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)
161                 savefunc(tmpp, vs)
162                 zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')
163     vd.status('%s save finished' % p)
164 
165 
166 @VisiData.api
167 def save_txt(vd, p, *vsheets):
168     with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:
169         for vs in vsheets:
170             unitsep = vs.options.delimiter
171             rowsep = vs.options.row_delimiter
172             for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):
173                 fp.write(unitsep.join(dispvals.values()))
174                 fp.write(rowsep)
175     vd.status('%s save finished' % p)
176 
177 
178 @BaseSheet.api
179 def rootSheet(sheet):
180     r = sheet
181     while isinstance(r.source, BaseSheet):
182         r = r.source
183 
184     return r
185 
186 BaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=getDefaultSaveName()), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .tsv)')
187 BaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')
188 BaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet(), confirm_overwrite=options.confirm_overwrite)', 'save root sheet to its source')
189 BaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath("save all sheets to: "), *vd.stackedSheets, confirm_overwrite=options.confirm_overwrite)', 'save all sheets to given file or directory)')
190 IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value="_".join(getattr(vs, "name", None) or "blank" for vs in selectedRows)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')
191 Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
192 Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
193
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/visidata/save.py b/visidata/save.py
--- a/visidata/save.py
+++ b/visidata/save.py
@@ -104,6 +104,10 @@
 def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):
     'Save all *vsheets* to *givenpath*.'
 
+    if not vsheets: # blank tuple
+        vd.warning('no sheets to save')
+        return
+
     filetype = givenpath.ext or options.save_filetype
 
     vd.clearCaches()

{"golden_diff": "diff --git a/visidata/save.py b/visidata/save.py\n--- a/visidata/save.py\n+++ b/visidata/save.py\n@@ -104,6 +104,10 @@\n def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):\n     'Save all *vsheets* to *givenpath*.'\n \n+    if not vsheets: # blank tuple\n+        vd.warning('no sheets to save')\n+        return\n+\n     filetype = givenpath.ext or options.save_filetype\n \n     vd.clearCaches()\n", "issue": "Bugs with save-all\n        I've a question about saving.\r\n\r\n1. I load a two sheet notebook \"responses-grades-cda.xlsx\".\r\n2. `g ctrl+s`\r\n3. it gives me a blank line -- why not default to name of file without xlsx extension?\r\n4. I type in file name without extension + `vds`: \"responses-grades-cda.vds\".\r\n5. I get an error: \"AttributeError: 'X1sxIndexSheet' object has no attribute 'join'\"\r\n\r\n_Originally posted by @reagle in https://github.com/saulpw/visidata/discussions/1266#discussioncomment-4870711_\r\n      \n", "before_files": [{"content": "import collections\n\nfrom visidata import *\n\n\nvd.option('confirm_overwrite', True, 'whether to prompt for overwrite confirmation on save')\nvd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)\n\n@Sheet.api\ndef safe_trdict(vs):\n    'returns string.translate dictionary for replacing tabs and newlines'\n    if options.safety_first:\n        delim = vs.options.delimiter\n        return {\n             0: '', #  strip NUL completely\n    ord(delim): vs.options.tsv_safe_tab, # \\t\n            10: vs.options.tsv_safe_newline,  # \\n\n            13: vs.options.tsv_safe_newline,  # \\r\n        }\n    return {}\n\n\n@Sheet.api\ndef iterdispvals(sheet, *cols, format=False):\n    'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'\n    if not cols:\n        cols = sheet.visibleCols\n\n    transformers = collections.OrderedDict()  # list of transformers for each column in order\n    for col in cols:\n        transformers[col] = [ col.type ]\n        if format:\n            formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))\n            transformers[col].append(formatMaker(col._formatdict))\n        trdict = sheet.safe_trdict()\n        if trdict:\n            transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))\n\n    options_safe_error = options.safe_error\n    for r in Progress(sheet.rows):\n        dispvals = collections.OrderedDict()  # [col] -> value\n        for col, transforms in transformers.items():\n            try:\n                dispval = col.getValue(r)\n\n            except Exception as e:\n                vd.exceptionCaught(e)\n                dispval = options_safe_error or str(e)\n\n            try:\n                for t in transforms:\n                    if dispval is None:\n                        break\n                    elif isinstance(dispval, TypedExceptionWrapper):\n                        dispval = options_safe_error or str(dispval)\n                        break\n                    else:\n                        dispval = t(dispval)\n\n                if dispval is None and format:\n                    dispval = ''\n            except Exception as e:\n                dispval = str(dispval)\n\n            dispvals[col] = dispval\n\n        yield dispvals\n\n\n@Sheet.api\ndef itervals(sheet, *cols, format=False):\n    for row in sheet.iterdispvals(*cols, format=format):\n        yield [row[c] for c in cols]\n\n@BaseSheet.api\ndef getDefaultSaveName(sheet):\n    src = getattr(sheet, 'source', None)\n    if hasattr(src, 'scheme') and src.scheme:\n        return src.name + src.suffix\n    if isinstance(src, Path):\n        if sheet.options.is_set('save_filetype', sheet):\n            # if save_filetype is over-ridden from default, use it as the extension\n            return str(src.with_suffix('')) + '.' + sheet.options.save_filetype\n        return str(src)\n    else:\n        return sheet.name+'.'+getattr(sheet, 'filetype', options.save_filetype)\n\n\n@VisiData.api\ndef save_cols(vd, cols):\n    sheet = cols[0].sheet\n    vs = copy(sheet)\n    vs.columns = list(cols)\n    vs.rows = sheet.rows\n    if len(cols) == 1:\n        savedcoltxt = cols[0].name + ' column'\n    else:\n        savedcoltxt = '%s columns' % len(cols)\n    path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())\n    vd.saveSheets(path, vs, confirm_overwrite=options.confirm_overwrite)\n\n\n@VisiData.api\ndef saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):\n    'Save all *vsheets* to *givenpath*.'\n\n    filetype = givenpath.ext or options.save_filetype\n\n    vd.clearCaches()\n\n    savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n\n    if savefunc is None:\n        vd.fail(f'no function to save as {filetype}')\n\n    if givenpath.exists() and confirm_overwrite:\n        vd.confirm(\"%s already exists. overwrite? \" % givenpath.given)\n\n    vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))\n\n    if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /\n        for vs in vsheets:\n            vs.hasBeenModified = False\n        # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)\n        # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)\n        if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:\n            vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n        return vd.execAsync(savefunc, givenpath, *vsheets)\n\n    # path is a dir\n\n    # save as individual files in the givenpath directory\n    try:\n        os.makedirs(givenpath, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    if not givenpath.is_dir():\n        vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n\n    def _savefiles(vsheets, givenpath, savefunc, filetype):\n        for vs in vsheets:\n            p = Path((givenpath / vs.name).with_suffix('.'+filetype))\n            savefunc(p, vs)\n            vs.hasBeenModified = False\n    return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)\n\n\n@VisiData.api\ndef save_zip(vd, p, *vsheets):\n    vd.clearCaches()\n\n    import tempfile\n    import zipfile\n    with tempfile.TemporaryDirectory() as tmpdir:\n        with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:\n            for vs in Progress(vsheets):\n                filetype = vs.options.save_filetype\n                tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')\n                savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n                savefunc(tmpp, vs)\n                zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')\n    vd.status('%s save finished' % p)\n\n\n@VisiData.api\ndef save_txt(vd, p, *vsheets):\n    with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:\n        for vs in vsheets:\n            unitsep = vs.options.delimiter\n            rowsep = vs.options.row_delimiter\n            for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):\n                fp.write(unitsep.join(dispvals.values()))\n                fp.write(rowsep)\n    vd.status('%s save finished' % p)\n\n\n@BaseSheet.api\ndef rootSheet(sheet):\n    r = sheet\n    while isinstance(r.source, BaseSheet):\n        r = r.source\n\n    return r\n\nBaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath(\"save to: \", value=getDefaultSaveName()), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .tsv)')\nBaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')\nBaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet(), confirm_overwrite=options.confirm_overwrite)', 'save root sheet to its source')\nBaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath(\"save all sheets to: \"), *vd.stackedSheets, confirm_overwrite=options.confirm_overwrite)', 'save all sheets to given file or directory)')\nIndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath(\"save %d sheets to: \" % nSelectedRows, value=\"_\".join(getattr(vs, \"name\", None) or \"blank\" for vs in selectedRows)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')\nSheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')\nSheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')\n", "path": "visidata/save.py"}], "after_files": [{"content": "import collections\n\nfrom visidata import *\n\n\nvd.option('confirm_overwrite', True, 'whether to prompt for overwrite confirmation on save')\nvd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)\n\n@Sheet.api\ndef safe_trdict(vs):\n    'returns string.translate dictionary for replacing tabs and newlines'\n    if options.safety_first:\n        delim = vs.options.delimiter\n        return {\n             0: '', #  strip NUL completely\n    ord(delim): vs.options.tsv_safe_tab, # \\t\n            10: vs.options.tsv_safe_newline,  # \\n\n            13: vs.options.tsv_safe_newline,  # \\r\n        }\n    return {}\n\n\n@Sheet.api\ndef iterdispvals(sheet, *cols, format=False):\n    'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'\n    if not cols:\n        cols = sheet.visibleCols\n\n    transformers = collections.OrderedDict()  # list of transformers for each column in order\n    for col in cols:\n        transformers[col] = [ col.type ]\n        if format:\n            formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))\n            transformers[col].append(formatMaker(col._formatdict))\n        trdict = sheet.safe_trdict()\n        if trdict:\n            transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))\n\n    options_safe_error = options.safe_error\n    for r in Progress(sheet.rows):\n        dispvals = collections.OrderedDict()  # [col] -> value\n        for col, transforms in transformers.items():\n            try:\n                dispval = col.getValue(r)\n\n            except Exception as e:\n                vd.exceptionCaught(e)\n                dispval = options_safe_error or str(e)\n\n            try:\n                for t in transforms:\n                    if dispval is None:\n                        break\n                    elif isinstance(dispval, TypedExceptionWrapper):\n                        dispval = options_safe_error or str(dispval)\n                        break\n                    else:\n                        dispval = t(dispval)\n\n                if dispval is None and format:\n                    dispval = ''\n            except Exception as e:\n                dispval = str(dispval)\n\n            dispvals[col] = dispval\n\n        yield dispvals\n\n\n@Sheet.api\ndef itervals(sheet, *cols, format=False):\n    for row in sheet.iterdispvals(*cols, format=format):\n        yield [row[c] for c in cols]\n\n@BaseSheet.api\ndef getDefaultSaveName(sheet):\n    src = getattr(sheet, 'source', None)\n    if hasattr(src, 'scheme') and src.scheme:\n        return src.name + src.suffix\n    if isinstance(src, Path):\n        if sheet.options.is_set('save_filetype', sheet):\n            # if save_filetype is over-ridden from default, use it as the extension\n            return str(src.with_suffix('')) + '.' + sheet.options.save_filetype\n        return str(src)\n    else:\n        return sheet.name+'.'+getattr(sheet, 'filetype', options.save_filetype)\n\n\n@VisiData.api\ndef save_cols(vd, cols):\n    sheet = cols[0].sheet\n    vs = copy(sheet)\n    vs.columns = list(cols)\n    vs.rows = sheet.rows\n    if len(cols) == 1:\n        savedcoltxt = cols[0].name + ' column'\n    else:\n        savedcoltxt = '%s columns' % len(cols)\n    path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())\n    vd.saveSheets(path, vs, confirm_overwrite=options.confirm_overwrite)\n\n\n@VisiData.api\ndef saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):\n    'Save all *vsheets* to *givenpath*.'\n\n    if not vsheets: # blank tuple\n        vd.warning('no sheets to save')\n        return\n\n    filetype = givenpath.ext or options.save_filetype\n\n    vd.clearCaches()\n\n    savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n\n    if savefunc is None:\n        vd.fail(f'no function to save as {filetype}')\n\n    if givenpath.exists() and confirm_overwrite:\n        vd.confirm(\"%s already exists. overwrite? \" % givenpath.given)\n\n    vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))\n\n    if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /\n        for vs in vsheets:\n            vs.hasBeenModified = False\n        # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)\n        # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)\n        if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:\n            vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n        return vd.execAsync(savefunc, givenpath, *vsheets)\n\n    # path is a dir\n\n    # save as individual files in the givenpath directory\n    try:\n        os.makedirs(givenpath, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    if not givenpath.is_dir():\n        vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n\n    def _savefiles(vsheets, givenpath, savefunc, filetype):\n        for vs in vsheets:\n            p = Path((givenpath / vs.name).with_suffix('.'+filetype))\n            savefunc(p, vs)\n            vs.hasBeenModified = False\n    return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)\n\n\n@VisiData.api\ndef save_zip(vd, p, *vsheets):\n    vd.clearCaches()\n\n    import tempfile\n    import zipfile\n    with tempfile.TemporaryDirectory() as tmpdir:\n        with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:\n            for vs in Progress(vsheets):\n                filetype = vs.options.save_filetype\n                tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')\n                savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n                savefunc(tmpp, vs)\n                zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')\n    vd.status('%s save finished' % p)\n\n\n@VisiData.api\ndef save_txt(vd, p, *vsheets):\n    with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:\n        for vs in vsheets:\n            unitsep = vs.options.delimiter\n            rowsep = vs.options.row_delimiter\n            for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):\n                fp.write(unitsep.join(dispvals.values()))\n                fp.write(rowsep)\n    vd.status('%s save finished' % p)\n\n\n@BaseSheet.api\ndef rootSheet(sheet):\n    r = sheet\n    while isinstance(r.source, BaseSheet):\n        r = r.source\n\n    return r\n\nBaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath(\"save to: \", value=getDefaultSaveName()), sheet, confirm_overwrite=options.confirm_overwrite)', 'save current sheet to filename in format determined by extension (default .tsv)')\nBaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')\nBaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet(), confirm_overwrite=options.confirm_overwrite)', 'save root sheet to its source')\nBaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath(\"save all sheets to: \"), *vd.stackedSheets, confirm_overwrite=options.confirm_overwrite)', 'save all sheets to given file or directory)')\nIndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath(\"save %d sheets to: \" % nSelectedRows, value=\"_\".join(getattr(vs, \"name\", None) or \"blank\" for vs in selectedRows)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')\nSheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')\nSheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')\n", "path": "visidata/save.py"}]}