We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[save-selected] default save filename is not valid
**Small description**
When doing a `save-selected` the default sheet name is invalid, see the screenshot below

**Expected result**
For the suggested file name to be valid and useable. Maybe something like '`sheet1_sheet2_.csv`'.

**Actual result with screenshot**
![image](https://user-images.githubusercontent.com/11225502/140711890-afa742c8-0927-4d79-ab51-c06a54dcdb89.png)

**Steps to reproduce with sample data and a .vd**
Open a number of sheets, go to `sheets-stack` select a few and do `save-selected`. This gives a stringified list rather than a proper name.

**Additional context**
Commit: https://github.com/saulpw/visidata/commit/9ce048d0378de0211e53f1591d79c8fbed39dff1

This seems to work:
`IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value="_".join(vs.name or "blank" for vs in source)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')`


--- 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/textsheet.py`
Content:
```
1 import textwrap
2 
3 from visidata import vd, BaseSheet, options, Sheet, ColumnItem, asyncthread
4 from visidata import Column, ColumnItem, vlen
5 from visidata import globalCommand, VisiData
6 import visidata
7 
8 
9 vd.option('wrap', False, 'wrap text to fit window width on TextSheet')
10 vd.option('save_filetype', '', 'specify default file type to save as', replay=True)
11 
12 
13 ## text viewer
14 # rowdef: (linenum, str)
15 class TextSheet(Sheet):
16     'Displays any iterable source, with linewrap if ``options.wrap`` is set.'
17     rowtype = 'lines'  # rowdef: [linenum, text]
18     filetype = 'txt'
19     columns = [
20         ColumnItem('linenum', 0, type=int, width=0),
21         ColumnItem('text', 1),
22     ]
23 
24     def iterload(self):
25         yield from self.readlines(self.source)
26 
27     def readlines(self, source):
28         winWidth = min(self.columns[1].width or 78, self.windowWidth-2)
29         wrap = self.options.wrap
30         for startingLine, text in enumerate(source):
31             if wrap and text:
32                 for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)):
33                     yield [startingLine+i+1, L]
34             else:
35                 yield [startingLine+1, text]
36 
37     def sysopen(sheet, linenum=0):
38         @asyncthread
39         def writelines(sheet, fn):
40             with open(fn, 'w') as fp:
41                 for row in sheet.rows:
42                     fp.write(row[1])
43                     fp.write('\n')
44 
45         import tempfile
46         with tempfile.NamedTemporaryFile() as temp:
47             writelines(sheet, temp.name)
48             vd.launchEditor(temp.name, '+%s' % linenum)
49             sheet.rows = []
50             for r in sheet.readlines(visidata.Path(temp.name)):
51                 sheet.addRow(r)
52 
53 
54 # .source is list of source text lines to 'load'
55 # .sourceSheet is Sheet error came from
56 class ErrorSheet(TextSheet):
57     precious = False
58 
59 
60 class ErrorsSheet(Sheet):
61     columns = [
62         Column('nlines', type=vlen),
63         ColumnItem('lastline', -1)
64     ]
65     def reload(self):
66         self.rows = self.source
67 
68     def openRow(self, row):
69         return ErrorSheet(source=self.cursorRow)
70 
71 @VisiData.property
72 def allErrorsSheet(self):
73     return ErrorsSheet("errors_all", source=vd.lastErrors)
74 
75 @VisiData.property
76 def recentErrorsSheet(self):
77     error = vd.lastErrors[-1] if vd.lastErrors else ''
78     return ErrorSheet("errors_recent", source=error)
79 
80 
81 
82 BaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status("no error")', 'view traceback for most recent error')
83 BaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')
84 
85 Sheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet("%s[%s].%s" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet'),
86 Sheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+"_cell_error", sourceSheet=sheet, source=getattr(cursorCell, "error", None) or fail("no error this cell")))', 'view traceback for error in current cell')
87 
88 TextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')
89 
90 
91 TextSheet.class_options.save_filetype = 'txt'
92 
93 vd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})
94
```

Path: `visidata/save.py`
Content:
```
1 from visidata import *
2 
3 
4 vd.option('confirm_overwrite', True, 'whether to prompt for overwrite confirmation on save')
5 vd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)
6 
7 @Sheet.api
8 def safe_trdict(vs):
9     'returns string.translate dictionary for replacing tabs and newlines'
10     if options.safety_first:
11         delim = vs.options.delimiter
12         return {
13              0: '', #  strip NUL completely
14     ord(delim): vs.options.tsv_safe_tab, # \t
15             10: vs.options.tsv_safe_newline,  # \n
16             13: vs.options.tsv_safe_newline,  # \r
17         }
18     return {}
19 
20 
21 @Sheet.api
22 def iterdispvals(sheet, *cols, format=False):
23     '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.'
24     if not cols:
25         cols = sheet.visibleCols
26 
27     transformers = collections.OrderedDict()  # list of transformers for each column in order
28     for col in cols:
29         transformers[col] = [ col.type ]
30         if format:
31             transformers[col].append(col.format)
32         trdict = sheet.safe_trdict()
33         if trdict:
34             transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))
35 
36     options_safe_error = options.safe_error
37     for r in Progress(sheet.rows):
38         dispvals = collections.OrderedDict()  # [col] -> value
39         for col, transforms in transformers.items():
40             try:
41                 dispval = col.getValue(r)
42 
43             except Exception as e:
44                 vd.exceptionCaught(e)
45                 dispval = options_safe_error or str(e)
46 
47             try:
48                 for t in transforms:
49                     if dispval is None:
50                         break
51                     elif isinstance(dispval, TypedExceptionWrapper):
52                         dispval = options_safe_error or str(dispval)
53                         break
54                     else:
55                         dispval = t(dispval)
56 
57                 if dispval is None and format:
58                     dispval = ''
59             except Exception as e:
60                 dispval = str(dispval)
61 
62             dispvals[col] = dispval
63 
64         yield dispvals
65 
66 
67 @Sheet.api
68 def itervals(sheet, *cols, format=False):
69     for row in sheet.iterdispvals(*cols, format=format):
70         yield [row[c] for c in cols]
71 
72 @BaseSheet.api
73 def getDefaultSaveName(sheet):
74     src = getattr(sheet, 'source', None)
75     if hasattr(src, 'scheme') and src.scheme:
76         return src.name + src.suffix
77     if isinstance(src, Path):
78         if sheet.options.is_set('save_filetype', sheet):
79             # if save_filetype is over-ridden from default, use it as the extension
80             return str(src.with_suffix('')) + '.' + sheet.options.save_filetype
81         return str(src)
82     else:
83         return sheet.name+'.'+getattr(sheet, 'filetype', options.save_filetype)
84 
85 
86 @VisiData.api
87 def save_cols(vd, cols):
88     sheet = cols[0].sheet
89     vs = copy(sheet)
90     vs.columns = list(cols)
91     vs.rows = sheet.rows
92     if len(cols) == 1:
93         savedcoltxt = cols[0].name + ' column'
94     else:
95         savedcoltxt = '%s columns' % len(cols)
96     path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())
97     vd.saveSheets(path, vs, confirm_overwrite=options.confirm_overwrite)
98 
99 
100 @VisiData.api
101 def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=False):
102     'Save all *vsheets* to *givenpath*.'
103 
104     filetype = givenpath.ext or options.save_filetype
105 
106     vd.clearCaches()
107 
108     savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)
109 
110     if savefunc is None:
111         savefunc = getattr(vd, 'save_' + options.save_filetype, None) or vd.fail('no function to save as type %s, set options.save_filetype' % filetype)
112         vd.warning(f'save for {filetype} unavailable, using {options.save_filetype}')
113 
114     if givenpath.exists() and confirm_overwrite:
115         vd.confirm("%s already exists. overwrite? " % givenpath.given)
116 
117     vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))
118 
119     if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /
120         for vs in vsheets:
121             vs.hasBeenModified = False
122         return vd.execAsync(savefunc, givenpath, *vsheets)
123 
124     # more than one sheet; either no specific multisave for save filetype, or path ends with /
125 
126     # save as individual files in the givenpath directory
127     try:
128         os.makedirs(givenpath, exist_ok=True)
129     except FileExistsError:
130         pass
131 
132     if not givenpath.is_dir():
133         vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
134 
135     def _savefiles(vsheets, givenpath, savefunc, filetype):
136         for vs in vsheets:
137             p = Path((givenpath / vs.name).with_suffix('.'+filetype))
138             savefunc(p, vs)
139             vs.hasBeenModified = False
140     return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)
141 
142 
143 @VisiData.api
144 def save_txt(vd, p, *vsheets):
145     with p.open_text(mode='w', encoding=vsheets[0].options.encoding) as fp:
146         for vs in vsheets:
147             unitsep = vs.options.delimiter
148             rowsep = vs.options.row_delimiter
149             for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):
150                 fp.write(unitsep.join(dispvals.values()))
151                 fp.write(rowsep)
152     vd.status('%s save finished' % p)
153 
154 
155 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)')
156 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)')
157 IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value=str(source)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')
158 Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
159 Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
160
```
--- 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
@@ -107,11 +107,16 @@
 
     savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)
 
+    using_save_filetype = False
+
     if savefunc is None:
         savefunc = getattr(vd, 'save_' + options.save_filetype, None) or vd.fail('no function to save as type %s, set options.save_filetype' % filetype)
         vd.warning(f'save for {filetype} unavailable, using {options.save_filetype}')
+        using_save_filetype = True
 
-    if givenpath.exists() and confirm_overwrite:
+    if using_save_filetype and givenpath.exists() and confirm_overwrite:
+        vd.confirm("%s already exists. overwrite with %s? " % (givenpath.given, options.save_filetype))
+    elif givenpath.exists() and confirm_overwrite:
         vd.confirm("%s already exists. overwrite? " % givenpath.given)
 
     vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))
@@ -119,9 +124,13 @@
     if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /
         for vs in vsheets:
             vs.hasBeenModified = False
+        # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)
+        # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)
+        if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:
+            vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
         return vd.execAsync(savefunc, givenpath, *vsheets)
 
-    # more than one sheet; either no specific multisave for save filetype, or path ends with /
+    # path is a dir
 
     # save as individual files in the givenpath directory
     try:
diff --git a/visidata/textsheet.py b/visidata/textsheet.py
--- a/visidata/textsheet.py
+++ b/visidata/textsheet.py
@@ -7,7 +7,7 @@
 
 
 vd.option('wrap', False, 'wrap text to fit window width on TextSheet')
-vd.option('save_filetype', '', 'specify default file type to save as', replay=True)
+vd.option('save_filetype', 'tsv', 'specify default file type to save as', replay=True)
 
 
 ## text viewer

{"golden_diff": "diff --git a/visidata/save.py b/visidata/save.py\n--- a/visidata/save.py\n+++ b/visidata/save.py\n@@ -107,11 +107,16 @@\n \n     savefunc = getattr(vsheets[0], 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n \n+    using_save_filetype = False\n+\n     if savefunc is None:\n         savefunc = getattr(vd, 'save_' + options.save_filetype, None) or vd.fail('no function to save as type %s, set options.save_filetype' % filetype)\n         vd.warning(f'save for {filetype} unavailable, using {options.save_filetype}')\n+        using_save_filetype = True\n \n-    if givenpath.exists() and confirm_overwrite:\n+    if using_save_filetype and givenpath.exists() and confirm_overwrite:\n+        vd.confirm(\"%s already exists. overwrite with %s? \" % (givenpath.given, options.save_filetype))\n+    elif 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@@ -119,9 +124,13 @@\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-    # more than one sheet; either no specific multisave for save filetype, or path ends with /\n+    # path is a dir\n \n     # save as individual files in the givenpath directory\n     try:\ndiff --git a/visidata/textsheet.py b/visidata/textsheet.py\n--- a/visidata/textsheet.py\n+++ b/visidata/textsheet.py\n@@ -7,7 +7,7 @@\n \n \n vd.option('wrap', False, 'wrap text to fit window width on TextSheet')\n-vd.option('save_filetype', '', 'specify default file type to save as', replay=True)\n+vd.option('save_filetype', 'tsv', 'specify default file type to save as', replay=True)\n \n \n ## text viewer\n", "issue": "[save-selected] default save filename is not valid\n**Small description**\r\nWhen doing a `save-selected` the default sheet name is invalid, see the screenshot below\r\n\r\n**Expected result**\r\nFor the suggested file name to be valid and useable. Maybe something like '`sheet1_sheet2_.csv`'.\r\n\r\n**Actual result with screenshot**\r\n![image](https://user-images.githubusercontent.com/11225502/140711890-afa742c8-0927-4d79-ab51-c06a54dcdb89.png)\r\n\r\n**Steps to reproduce with sample data and a .vd**\r\nOpen a number of sheets, go to `sheets-stack` select a few and do `save-selected`. This gives a stringified list rather than a proper name.\r\n\r\n**Additional context**\r\nCommit: https://github.com/saulpw/visidata/commit/9ce048d0378de0211e53f1591d79c8fbed39dff1\r\n\r\nThis seems to work:\r\n`IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath(\"save %d sheets to: \" % nSelectedRows, value=\"_\".join(vs.name or \"blank\" for vs in source)), *selectedRows, confirm_overwrite=options.confirm_overwrite)', 'save all selected sheets to given file or directory')`\r\n\n", "before_files": [{"content": "import textwrap\n\nfrom visidata import vd, BaseSheet, options, Sheet, ColumnItem, asyncthread\nfrom visidata import Column, ColumnItem, vlen\nfrom visidata import globalCommand, VisiData\nimport visidata\n\n\nvd.option('wrap', False, 'wrap text to fit window width on TextSheet')\nvd.option('save_filetype', '', 'specify default file type to save as', replay=True)\n\n\n## text viewer\n# rowdef: (linenum, str)\nclass TextSheet(Sheet):\n    'Displays any iterable source, with linewrap if ``options.wrap`` is set.'\n    rowtype = 'lines'  # rowdef: [linenum, text]\n    filetype = 'txt'\n    columns = [\n        ColumnItem('linenum', 0, type=int, width=0),\n        ColumnItem('text', 1),\n    ]\n\n    def iterload(self):\n        yield from self.readlines(self.source)\n\n    def readlines(self, source):\n        winWidth = min(self.columns[1].width or 78, self.windowWidth-2)\n        wrap = self.options.wrap\n        for startingLine, text in enumerate(source):\n            if wrap and text:\n                for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)):\n                    yield [startingLine+i+1, L]\n            else:\n                yield [startingLine+1, text]\n\n    def sysopen(sheet, linenum=0):\n        @asyncthread\n        def writelines(sheet, fn):\n            with open(fn, 'w') as fp:\n                for row in sheet.rows:\n                    fp.write(row[1])\n                    fp.write('\\n')\n\n        import tempfile\n        with tempfile.NamedTemporaryFile() as temp:\n            writelines(sheet, temp.name)\n            vd.launchEditor(temp.name, '+%s' % linenum)\n            sheet.rows = []\n            for r in sheet.readlines(visidata.Path(temp.name)):\n                sheet.addRow(r)\n\n\n# .source is list of source text lines to 'load'\n# .sourceSheet is Sheet error came from\nclass ErrorSheet(TextSheet):\n    precious = False\n\n\nclass ErrorsSheet(Sheet):\n    columns = [\n        Column('nlines', type=vlen),\n        ColumnItem('lastline', -1)\n    ]\n    def reload(self):\n        self.rows = self.source\n\n    def openRow(self, row):\n        return ErrorSheet(source=self.cursorRow)\n\n@VisiData.property\ndef allErrorsSheet(self):\n    return ErrorsSheet(\"errors_all\", source=vd.lastErrors)\n\n@VisiData.property\ndef recentErrorsSheet(self):\n    error = vd.lastErrors[-1] if vd.lastErrors else ''\n    return ErrorSheet(\"errors_recent\", source=error)\n\n\n\nBaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status(\"no error\")', 'view traceback for most recent error')\nBaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')\n\nSheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet(\"%s[%s].%s\" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet'),\nSheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+\"_cell_error\", sourceSheet=sheet, source=getattr(cursorCell, \"error\", None) or fail(\"no error this cell\")))', 'view traceback for error in current cell')\n\nTextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')\n\n\nTextSheet.class_options.save_filetype = 'txt'\n\nvd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})\n", "path": "visidata/textsheet.py"}, {"content": "from 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            transformers[col].append(col.format)\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        savefunc = getattr(vd, 'save_' + options.save_filetype, None) or vd.fail('no function to save as type %s, set options.save_filetype' % filetype)\n        vd.warning(f'save for {filetype} unavailable, using {options.save_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        return vd.execAsync(savefunc, givenpath, *vsheets)\n\n    # more than one sheet; either no specific multisave for save filetype, or path ends with /\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_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\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('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=str(source)), *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 textwrap\n\nfrom visidata import vd, BaseSheet, options, Sheet, ColumnItem, asyncthread\nfrom visidata import Column, ColumnItem, vlen\nfrom visidata import globalCommand, VisiData\nimport visidata\n\n\nvd.option('wrap', False, 'wrap text to fit window width on TextSheet')\nvd.option('save_filetype', 'tsv', 'specify default file type to save as', replay=True)\n\n\n## text viewer\n# rowdef: (linenum, str)\nclass TextSheet(Sheet):\n    'Displays any iterable source, with linewrap if ``options.wrap`` is set.'\n    rowtype = 'lines'  # rowdef: [linenum, text]\n    filetype = 'txt'\n    columns = [\n        ColumnItem('linenum', 0, type=int, width=0),\n        ColumnItem('text', 1),\n    ]\n\n    def iterload(self):\n        yield from self.readlines(self.source)\n\n    def readlines(self, source):\n        winWidth = min(self.columns[1].width or 78, self.windowWidth-2)\n        wrap = self.options.wrap\n        for startingLine, text in enumerate(source):\n            if wrap and text:\n                for i, L in enumerate(textwrap.wrap(str(text), width=winWidth)):\n                    yield [startingLine+i+1, L]\n            else:\n                yield [startingLine+1, text]\n\n    def sysopen(sheet, linenum=0):\n        @asyncthread\n        def writelines(sheet, fn):\n            with open(fn, 'w') as fp:\n                for row in sheet.rows:\n                    fp.write(row[1])\n                    fp.write('\\n')\n\n        import tempfile\n        with tempfile.NamedTemporaryFile() as temp:\n            writelines(sheet, temp.name)\n            vd.launchEditor(temp.name, '+%s' % linenum)\n            sheet.rows = []\n            for r in sheet.readlines(visidata.Path(temp.name)):\n                sheet.addRow(r)\n\n\n# .source is list of source text lines to 'load'\n# .sourceSheet is Sheet error came from\nclass ErrorSheet(TextSheet):\n    precious = False\n\n\nclass ErrorsSheet(Sheet):\n    columns = [\n        Column('nlines', type=vlen),\n        ColumnItem('lastline', -1)\n    ]\n    def reload(self):\n        self.rows = self.source\n\n    def openRow(self, row):\n        return ErrorSheet(source=self.cursorRow)\n\n@VisiData.property\ndef allErrorsSheet(self):\n    return ErrorsSheet(\"errors_all\", source=vd.lastErrors)\n\n@VisiData.property\ndef recentErrorsSheet(self):\n    error = vd.lastErrors[-1] if vd.lastErrors else ''\n    return ErrorSheet(\"errors_recent\", source=error)\n\n\n\nBaseSheet.addCommand('^E', 'error-recent', 'vd.lastErrors and vd.push(recentErrorsSheet) or status(\"no error\")', 'view traceback for most recent error')\nBaseSheet.addCommand('g^E', 'errors-all', 'vd.push(vd.allErrorsSheet)', 'view traceback for most recent errors')\n\nSheet.addCommand(None, 'view-cell', 'vd.push(ErrorSheet(\"%s[%s].%s\" % (name, cursorRowIndex, cursorCol.name), sourceSheet=sheet, source=cursorDisplay.splitlines()))', 'view contents of current cell in a new sheet'),\nSheet.addCommand('z^E', 'error-cell', 'vd.push(ErrorSheet(sheet.name+\"_cell_error\", sourceSheet=sheet, source=getattr(cursorCell, \"error\", None) or fail(\"no error this cell\")))', 'view traceback for error in current cell')\n\nTextSheet.addCommand('^O', 'sysopen-sheet', 'sheet.sysopen(sheet.cursorRowIndex)', 'open copy of text sheet in $EDITOR and reload on exit')\n\n\nTextSheet.class_options.save_filetype = 'txt'\n\nvd.addGlobals({'TextSheet': TextSheet, 'ErrorSheet': ErrorSheet})\n", "path": "visidata/textsheet.py"}, {"content": "from 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            transformers[col].append(col.format)\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    using_save_filetype = False\n\n    if savefunc is None:\n        savefunc = getattr(vd, 'save_' + options.save_filetype, None) or vd.fail('no function to save as type %s, set options.save_filetype' % filetype)\n        vd.warning(f'save for {filetype} unavailable, using {options.save_filetype}')\n        using_save_filetype = True\n\n    if using_save_filetype and givenpath.exists() and confirm_overwrite:\n        vd.confirm(\"%s already exists. overwrite with %s? \" % (givenpath.given, options.save_filetype))\n    elif 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_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\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('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=str(source)), *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"}]}