We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
save_xlsx: null values become the string "None"
**Small description**

Setting `--null-value` in either direction doesn't help, so I suspect it isn't just that `options.null_value` is set to `None`.

I found this during the batch conversion. There's code below.

**Expected result**

An empty string (or the `options.null_value`) is more reasonable than `None`, for this conversion. But I can't set an empty string, with `--null-value`.

**Actual result with screenshot**

In lieu of a screenshot, I have console output.

```console
> vd -f json -b --save-filetype=xlsx -o nones.xlsx <<< '[{"foo":"None","bar":null}]'
opening - as json
saving 1 sheets to nones.xlsx as xlsx
Pay attention.
nones.xlsx save finished
> vd -f xlsx -b --save-filetype=json -o - nones.xlsx +:-:: | jq
opening nones.xlsx as xlsx
Let your best be for your friend.
saving 1 sheets to - as json
[
  {
    "foo": "None",
    "bar": "None"
  }
]
```

<details>

<summary>Testing with `--null-value`</summary>

```
> vd -f json -b --save-filetype=xlsx --cmdlog-histfile=vd.log --null-value "None" -o nones.xlsx <<< '[{"foo":"None","bar":null}]'
opening - as json
saving 1 sheets to nones.xlsx as xlsx
Stop this moment, I tell you!
nones.xlsx save finished
> vd -f xlsx -b --save-filetype=json --null-value "" -o - nones.xlsx +:-:: | jq
opening nones.xlsx as xlsx
Listen.
saving 1 sheets to - as json
[
  {
    "foo": "None",
    "bar": "None"
  }
]
> vd -f xlsx -b --save-filetype=json --null-value "None" -o - nones.xlsx +:-:: | jq
opening nones.xlsx as xlsx
Was I the same when I got up this morning?
saving 1 sheets to - as json
[
  {
    "foo": "None",
    "bar": "None"
  }
]
> vd -f json -b --save-filetype=xlsx --cmdlog-histfile=vd.log --null-value "" -o nones.xlsx <<< '[{"foo":"None","bar":null}]'
opening - as json
saving 1 sheets to nones.xlsx as xlsx
Listen.
nones.xlsx save finished
> vd -f xlsx -b --save-filetype=json --null-value "" -o - nones.xlsx +:-:: | jq
opening nones.xlsx as xlsx
I wonder what they'll do next!
saving 1 sheets to - as json
[
  {
    "foo": "None",
    "bar": "None"
  }
]
> vd -f xlsx -b --save-filetype=json --null-value "None" -o - nones.xlsx +:-:: | jq
opening nones.xlsx as xlsx
What are you thinking of?
saving 1 sheets to - as json
[
  {
    "foo": "None",
    "bar": "None"
  }
]
```

</details>

**Steps to reproduce with sample data and a .vd**

This was all done within `--batch` mode (and setting `--cmdlog-histfile` resulted in no output).

**Additional context**

I'm pretty sure this is due to naive serialization of the python value.

```python
>>> f"{None}"
'None'
```

Version

```
saul.pw/VisiData v2.9.1
```

As it happens, I'm interested in extending the `save_xlsx` functionality to create Tables (there is support in `openpyxl`). If I get round to that sooner rather than later, I'll look to fix this first.

--- 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/loaders/xlsx.py`
Content:
```
1 import itertools
2 import copy
3 
4 from visidata import VisiData, vd, Sheet, Column, Progress, IndexSheet, ColumnAttr, SequenceSheet, AttrDict, AttrColumn, date, datetime
5 
6 
7 vd.option('xlsx_meta_columns', False, 'include columns for cell objects, font colors, and fill colors', replay=True)
8 
9 @VisiData.api
10 def open_xls(vd, p):
11     return XlsIndexSheet(p.name, source=p)
12 
13 @VisiData.api
14 def open_xlsx(vd, p):
15     return XlsxIndexSheet(p.name, source=p)
16 
17 class XlsxIndexSheet(IndexSheet):
18     'Load XLSX file (in Excel Open XML format).'
19     rowtype = 'sheets'  # rowdef: xlsxSheet
20     columns = [
21         Column('sheet', getter=lambda col,row: row.source.title),  # xlsx sheet title
22         ColumnAttr('name', width=0),  # visidata Sheet name
23         ColumnAttr('nRows', type=int),
24         ColumnAttr('nCols', type=int),
25         Column('active', getter=lambda col,row: row.source is col.sheet.workbook.active),
26     ]
27     nKeys = 1
28 
29     def iterload(self):
30         import openpyxl
31         self.workbook = openpyxl.load_workbook(str(self.source), data_only=True, read_only=True)
32         for sheetname in self.workbook.sheetnames:
33             src = self.workbook[sheetname]
34             yield XlsxSheet(self.name, sheetname, source=src)
35 
36 
37 class XlsxSheet(SequenceSheet):
38     # rowdef: AttrDict of column_letter to cell
39     def setCols(self, headerrows):
40         from openpyxl.utils.cell import get_column_letter
41         self.columns = []
42         self._rowtype = AttrDict
43 
44         if not headerrows:
45             return
46 
47         headers = [[cell.value for cell in row.values()] for row in headerrows]
48         column_letters = [
49                 x.column_letter if 'column_letter' in dir(x)
50                 else get_column_letter(i+1)
51                 for i, x in enumerate(headerrows[0].values())]
52 
53         for i, colnamelines in enumerate(itertools.zip_longest(*headers, fillvalue='')):
54             colnamelines = ['' if c is None else c for c in colnamelines]
55             column_name = ''.join(map(str, colnamelines))
56             self.addColumn(AttrColumn(column_name, column_letters[i] + '.value'))
57             self.addXlsxMetaColumns(column_letters[i], column_name)
58 
59     def addRow(self, row, index=None):
60         Sheet.addRow(self, row, index=index)  # skip SequenceSheet
61         for column_letter, v in list(row.items())[len(self.columns):len(row)]:  # no-op if already done
62             self.addColumn(AttrColumn('', column_letter + '.value'))
63             self.addXlsxMetaColumns(column_letter, column_letter)
64 
65     def iterload(self):
66         from openpyxl.utils.cell import get_column_letter
67         worksheet = self.source
68         for row in Progress(worksheet.iter_rows(), total=worksheet.max_row or 0):
69             yield AttrDict({get_column_letter(i+1): cell for i, cell in enumerate(row)})
70 
71     def addXlsxMetaColumns(self, column_letter, column_name):
72         if self.options.xlsx_meta_columns:
73             self.addColumn(
74                     AttrColumn(column_name + '_cellPyObj', column_letter))
75             self.addColumn(
76                     AttrColumn(column_name + '_fontColor',
77                         column_letter + '.font.color.value'))
78             self.addColumn(
79                     AttrColumn(column_name + '_fillColor', column_letter +
80                         '.fill.start_color.value'))
81 
82     def paste_after(self, rowidx):
83         to_paste = list(copy.copy(r) for r in reversed(vd.memory.cliprows))
84         self.addRows(to_paste, index=rowidx)
85 
86 
87 class XlsIndexSheet(IndexSheet):
88     'Load XLS file (in Excel format).'
89     rowtype = 'sheets'  # rowdef: xlsSheet
90     columns = [
91         Column('sheet', getter=lambda col,row: row.source.name),  # xls sheet name
92         ColumnAttr('name', width=0),  # visidata sheet name
93         ColumnAttr('nRows', type=int),
94         ColumnAttr('nCols', type=int),
95     ]
96     nKeys = 1
97     def iterload(self):
98         import xlrd
99         self.workbook = xlrd.open_workbook(str(self.source))
100         for sheetname in self.workbook.sheet_names():
101             yield XlsSheet(self.name, sheetname, source=self.workbook.sheet_by_name(sheetname))
102 
103 
104 class XlsSheet(SequenceSheet):
105     def iterload(self):
106         worksheet = self.source
107         for rownum in Progress(range(worksheet.nrows)):
108             yield list(worksheet.cell(rownum, colnum).value for colnum in range(worksheet.ncols))
109 
110 
111 @Sheet.property
112 def xls_name(vs):
113     name = vs.names[-1]
114     if vs.options.clean_names:
115         cleaned_name = ''.join('_' if ch in ':[]*?/\\' else ch for ch in vs.name) #1122
116         name = cleaned_name[:31] #1122  #594
117         name = name.strip('_')
118 
119     return name
120 
121 
122 @VisiData.api
123 def save_xlsx(vd, p, *sheets):
124     import openpyxl
125 
126     wb = openpyxl.Workbook()
127     wb.remove_sheet(wb['Sheet'])
128 
129     for vs in sheets:
130         if vs.xls_name != vs.names[-1]:
131             vd.warning(f'saving {vs.name} as {vs.xls_name}')
132         ws = wb.create_sheet(title=vs.xls_name)
133 
134         headers = [col.name for col in vs.visibleCols]
135         ws.append(headers)
136 
137         for dispvals in vs.iterdispvals(format=False):
138 
139             row = []
140             for col, v in dispvals.items():
141                 if col.type == date:
142                     v = datetime.datetime.fromtimestamp(int(v.timestamp()))
143                 elif not vd.isNumeric(col):
144                     v = str(v)
145                 row.append(v)
146 
147             ws.append(row)
148 
149     wb.active = ws
150 
151     wb.save(filename=p)
152     vd.status(f'{p} save finished')
153 
154 
155 @VisiData.api
156 def save_xls(vd, p, *sheets):
157     import xlwt
158 
159     wb = xlwt.Workbook()
160 
161     for vs in sheets:
162         if vs.xls_name != vs.name:
163             vd.warning(f'saving {vs.name} as {vs.xls_name}')
164         ws1 = wb.add_sheet(vs.xls_name)
165         for col_i, col in enumerate(vs.visibleCols):
166             ws1.write(0, col_i, col.name)
167 
168         for r_i, dispvals in enumerate(vs.iterdispvals(format=True)):
169             r_i += 1
170             for c_i, v in enumerate(dispvals.values()):
171                 ws1.write(r_i, c_i, v)
172 
173     wb.save(p)
174     vd.status(f'{p} save finished')
175
```
--- 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/loaders/xlsx.py b/visidata/loaders/xlsx.py
--- a/visidata/loaders/xlsx.py
+++ b/visidata/loaders/xlsx.py
@@ -138,7 +138,9 @@
 
             row = []
             for col, v in dispvals.items():
-                if col.type == date:
+                if v is None:
+                    v = ""
+                elif col.type == date:
                     v = datetime.datetime.fromtimestamp(int(v.timestamp()))
                 elif not vd.isNumeric(col):
                     v = str(v)

{"golden_diff": "diff --git a/visidata/loaders/xlsx.py b/visidata/loaders/xlsx.py\n--- a/visidata/loaders/xlsx.py\n+++ b/visidata/loaders/xlsx.py\n@@ -138,7 +138,9 @@\n \n             row = []\n             for col, v in dispvals.items():\n-                if col.type == date:\n+                if v is None:\n+                    v = \"\"\n+                elif col.type == date:\n                     v = datetime.datetime.fromtimestamp(int(v.timestamp()))\n                 elif not vd.isNumeric(col):\n                     v = str(v)\n", "issue": "save_xlsx: null values become the string \"None\"\n**Small description**\r\n\r\nSetting `--null-value` in either direction doesn't help, so I suspect it isn't just that `options.null_value` is set to `None`.\r\n\r\nI found this during the batch conversion. There's code below.\r\n\r\n**Expected result**\r\n\r\nAn empty string (or the `options.null_value`) is more reasonable than `None`, for this conversion. But I can't set an empty string, with `--null-value`.\r\n\r\n**Actual result with screenshot**\r\n\r\nIn lieu of a screenshot, I have console output.\r\n\r\n```console\r\n> vd -f json -b --save-filetype=xlsx -o nones.xlsx <<< '[{\"foo\":\"None\",\"bar\":null}]'\r\nopening - as json\r\nsaving 1 sheets to nones.xlsx as xlsx\r\nPay attention.\r\nnones.xlsx save finished\r\n> vd -f xlsx -b --save-filetype=json -o - nones.xlsx +:-:: | jq\r\nopening nones.xlsx as xlsx\r\nLet your best be for your friend.\r\nsaving 1 sheets to - as json\r\n[\r\n  {\r\n    \"foo\": \"None\",\r\n    \"bar\": \"None\"\r\n  }\r\n]\r\n```\r\n\r\n<details>\r\n\r\n<summary>Testing with `--null-value`</summary>\r\n\r\n```\r\n> vd -f json -b --save-filetype=xlsx --cmdlog-histfile=vd.log --null-value \"None\" -o nones.xlsx <<< '[{\"foo\":\"None\",\"bar\":null}]'\r\nopening - as json\r\nsaving 1 sheets to nones.xlsx as xlsx\r\nStop this moment, I tell you!\r\nnones.xlsx save finished\r\n> vd -f xlsx -b --save-filetype=json --null-value \"\" -o - nones.xlsx +:-:: | jq\r\nopening nones.xlsx as xlsx\r\nListen.\r\nsaving 1 sheets to - as json\r\n[\r\n  {\r\n    \"foo\": \"None\",\r\n    \"bar\": \"None\"\r\n  }\r\n]\r\n> vd -f xlsx -b --save-filetype=json --null-value \"None\" -o - nones.xlsx +:-:: | jq\r\nopening nones.xlsx as xlsx\r\nWas I the same when I got up this morning?\r\nsaving 1 sheets to - as json\r\n[\r\n  {\r\n    \"foo\": \"None\",\r\n    \"bar\": \"None\"\r\n  }\r\n]\r\n> vd -f json -b --save-filetype=xlsx --cmdlog-histfile=vd.log --null-value \"\" -o nones.xlsx <<< '[{\"foo\":\"None\",\"bar\":null}]'\r\nopening - as json\r\nsaving 1 sheets to nones.xlsx as xlsx\r\nListen.\r\nnones.xlsx save finished\r\n> vd -f xlsx -b --save-filetype=json --null-value \"\" -o - nones.xlsx +:-:: | jq\r\nopening nones.xlsx as xlsx\r\nI wonder what they'll do next!\r\nsaving 1 sheets to - as json\r\n[\r\n  {\r\n    \"foo\": \"None\",\r\n    \"bar\": \"None\"\r\n  }\r\n]\r\n> vd -f xlsx -b --save-filetype=json --null-value \"None\" -o - nones.xlsx +:-:: | jq\r\nopening nones.xlsx as xlsx\r\nWhat are you thinking of?\r\nsaving 1 sheets to - as json\r\n[\r\n  {\r\n    \"foo\": \"None\",\r\n    \"bar\": \"None\"\r\n  }\r\n]\r\n```\r\n\r\n</details>\r\n\r\n**Steps to reproduce with sample data and a .vd**\r\n\r\nThis was all done within `--batch` mode (and setting `--cmdlog-histfile` resulted in no output).\r\n\r\n**Additional context**\r\n\r\nI'm pretty sure this is due to naive serialization of the python value.\r\n\r\n```python\r\n>>> f\"{None}\"\r\n'None'\r\n```\r\n\r\nVersion\r\n\r\n```\r\nsaul.pw/VisiData v2.9.1\r\n```\r\n\r\nAs it happens, I'm interested in extending the `save_xlsx` functionality to create Tables (there is support in `openpyxl`). If I get round to that sooner rather than later, I'll look to fix this first.\n", "before_files": [{"content": "import itertools\nimport copy\n\nfrom visidata import VisiData, vd, Sheet, Column, Progress, IndexSheet, ColumnAttr, SequenceSheet, AttrDict, AttrColumn, date, datetime\n\n\nvd.option('xlsx_meta_columns', False, 'include columns for cell objects, font colors, and fill colors', replay=True)\n\n@VisiData.api\ndef open_xls(vd, p):\n    return XlsIndexSheet(p.name, source=p)\n\n@VisiData.api\ndef open_xlsx(vd, p):\n    return XlsxIndexSheet(p.name, source=p)\n\nclass XlsxIndexSheet(IndexSheet):\n    'Load XLSX file (in Excel Open XML format).'\n    rowtype = 'sheets'  # rowdef: xlsxSheet\n    columns = [\n        Column('sheet', getter=lambda col,row: row.source.title),  # xlsx sheet title\n        ColumnAttr('name', width=0),  # visidata Sheet name\n        ColumnAttr('nRows', type=int),\n        ColumnAttr('nCols', type=int),\n        Column('active', getter=lambda col,row: row.source is col.sheet.workbook.active),\n    ]\n    nKeys = 1\n\n    def iterload(self):\n        import openpyxl\n        self.workbook = openpyxl.load_workbook(str(self.source), data_only=True, read_only=True)\n        for sheetname in self.workbook.sheetnames:\n            src = self.workbook[sheetname]\n            yield XlsxSheet(self.name, sheetname, source=src)\n\n\nclass XlsxSheet(SequenceSheet):\n    # rowdef: AttrDict of column_letter to cell\n    def setCols(self, headerrows):\n        from openpyxl.utils.cell import get_column_letter\n        self.columns = []\n        self._rowtype = AttrDict\n\n        if not headerrows:\n            return\n\n        headers = [[cell.value for cell in row.values()] for row in headerrows]\n        column_letters = [\n                x.column_letter if 'column_letter' in dir(x)\n                else get_column_letter(i+1)\n                for i, x in enumerate(headerrows[0].values())]\n\n        for i, colnamelines in enumerate(itertools.zip_longest(*headers, fillvalue='')):\n            colnamelines = ['' if c is None else c for c in colnamelines]\n            column_name = ''.join(map(str, colnamelines))\n            self.addColumn(AttrColumn(column_name, column_letters[i] + '.value'))\n            self.addXlsxMetaColumns(column_letters[i], column_name)\n\n    def addRow(self, row, index=None):\n        Sheet.addRow(self, row, index=index)  # skip SequenceSheet\n        for column_letter, v in list(row.items())[len(self.columns):len(row)]:  # no-op if already done\n            self.addColumn(AttrColumn('', column_letter + '.value'))\n            self.addXlsxMetaColumns(column_letter, column_letter)\n\n    def iterload(self):\n        from openpyxl.utils.cell import get_column_letter\n        worksheet = self.source\n        for row in Progress(worksheet.iter_rows(), total=worksheet.max_row or 0):\n            yield AttrDict({get_column_letter(i+1): cell for i, cell in enumerate(row)})\n\n    def addXlsxMetaColumns(self, column_letter, column_name):\n        if self.options.xlsx_meta_columns:\n            self.addColumn(\n                    AttrColumn(column_name + '_cellPyObj', column_letter))\n            self.addColumn(\n                    AttrColumn(column_name + '_fontColor',\n                        column_letter + '.font.color.value'))\n            self.addColumn(\n                    AttrColumn(column_name + '_fillColor', column_letter +\n                        '.fill.start_color.value'))\n\n    def paste_after(self, rowidx):\n        to_paste = list(copy.copy(r) for r in reversed(vd.memory.cliprows))\n        self.addRows(to_paste, index=rowidx)\n\n\nclass XlsIndexSheet(IndexSheet):\n    'Load XLS file (in Excel format).'\n    rowtype = 'sheets'  # rowdef: xlsSheet\n    columns = [\n        Column('sheet', getter=lambda col,row: row.source.name),  # xls sheet name\n        ColumnAttr('name', width=0),  # visidata sheet name\n        ColumnAttr('nRows', type=int),\n        ColumnAttr('nCols', type=int),\n    ]\n    nKeys = 1\n    def iterload(self):\n        import xlrd\n        self.workbook = xlrd.open_workbook(str(self.source))\n        for sheetname in self.workbook.sheet_names():\n            yield XlsSheet(self.name, sheetname, source=self.workbook.sheet_by_name(sheetname))\n\n\nclass XlsSheet(SequenceSheet):\n    def iterload(self):\n        worksheet = self.source\n        for rownum in Progress(range(worksheet.nrows)):\n            yield list(worksheet.cell(rownum, colnum).value for colnum in range(worksheet.ncols))\n\n\n@Sheet.property\ndef xls_name(vs):\n    name = vs.names[-1]\n    if vs.options.clean_names:\n        cleaned_name = ''.join('_' if ch in ':[]*?/\\\\' else ch for ch in vs.name) #1122\n        name = cleaned_name[:31] #1122  #594\n        name = name.strip('_')\n\n    return name\n\n\n@VisiData.api\ndef save_xlsx(vd, p, *sheets):\n    import openpyxl\n\n    wb = openpyxl.Workbook()\n    wb.remove_sheet(wb['Sheet'])\n\n    for vs in sheets:\n        if vs.xls_name != vs.names[-1]:\n            vd.warning(f'saving {vs.name} as {vs.xls_name}')\n        ws = wb.create_sheet(title=vs.xls_name)\n\n        headers = [col.name for col in vs.visibleCols]\n        ws.append(headers)\n\n        for dispvals in vs.iterdispvals(format=False):\n\n            row = []\n            for col, v in dispvals.items():\n                if col.type == date:\n                    v = datetime.datetime.fromtimestamp(int(v.timestamp()))\n                elif not vd.isNumeric(col):\n                    v = str(v)\n                row.append(v)\n\n            ws.append(row)\n\n    wb.active = ws\n\n    wb.save(filename=p)\n    vd.status(f'{p} save finished')\n\n\n@VisiData.api\ndef save_xls(vd, p, *sheets):\n    import xlwt\n\n    wb = xlwt.Workbook()\n\n    for vs in sheets:\n        if vs.xls_name != vs.name:\n            vd.warning(f'saving {vs.name} as {vs.xls_name}')\n        ws1 = wb.add_sheet(vs.xls_name)\n        for col_i, col in enumerate(vs.visibleCols):\n            ws1.write(0, col_i, col.name)\n\n        for r_i, dispvals in enumerate(vs.iterdispvals(format=True)):\n            r_i += 1\n            for c_i, v in enumerate(dispvals.values()):\n                ws1.write(r_i, c_i, v)\n\n    wb.save(p)\n    vd.status(f'{p} save finished')\n", "path": "visidata/loaders/xlsx.py"}], "after_files": [{"content": "import itertools\nimport copy\n\nfrom visidata import VisiData, vd, Sheet, Column, Progress, IndexSheet, ColumnAttr, SequenceSheet, AttrDict, AttrColumn, date, datetime\n\n\nvd.option('xlsx_meta_columns', False, 'include columns for cell objects, font colors, and fill colors', replay=True)\n\n@VisiData.api\ndef open_xls(vd, p):\n    return XlsIndexSheet(p.name, source=p)\n\n@VisiData.api\ndef open_xlsx(vd, p):\n    return XlsxIndexSheet(p.name, source=p)\n\nclass XlsxIndexSheet(IndexSheet):\n    'Load XLSX file (in Excel Open XML format).'\n    rowtype = 'sheets'  # rowdef: xlsxSheet\n    columns = [\n        Column('sheet', getter=lambda col,row: row.source.title),  # xlsx sheet title\n        ColumnAttr('name', width=0),  # visidata Sheet name\n        ColumnAttr('nRows', type=int),\n        ColumnAttr('nCols', type=int),\n        Column('active', getter=lambda col,row: row.source is col.sheet.workbook.active),\n    ]\n    nKeys = 1\n\n    def iterload(self):\n        import openpyxl\n        self.workbook = openpyxl.load_workbook(str(self.source), data_only=True, read_only=True)\n        for sheetname in self.workbook.sheetnames:\n            src = self.workbook[sheetname]\n            yield XlsxSheet(self.name, sheetname, source=src)\n\n\nclass XlsxSheet(SequenceSheet):\n    # rowdef: AttrDict of column_letter to cell\n    def setCols(self, headerrows):\n        from openpyxl.utils.cell import get_column_letter\n        self.columns = []\n        self._rowtype = AttrDict\n\n        if not headerrows:\n            return\n\n        headers = [[cell.value for cell in row.values()] for row in headerrows]\n        column_letters = [\n                x.column_letter if 'column_letter' in dir(x)\n                else get_column_letter(i+1)\n                for i, x in enumerate(headerrows[0].values())]\n\n        for i, colnamelines in enumerate(itertools.zip_longest(*headers, fillvalue='')):\n            colnamelines = ['' if c is None else c for c in colnamelines]\n            column_name = ''.join(map(str, colnamelines))\n            self.addColumn(AttrColumn(column_name, column_letters[i] + '.value'))\n            self.addXlsxMetaColumns(column_letters[i], column_name)\n\n    def addRow(self, row, index=None):\n        Sheet.addRow(self, row, index=index)  # skip SequenceSheet\n        for column_letter, v in list(row.items())[len(self.columns):len(row)]:  # no-op if already done\n            self.addColumn(AttrColumn('', column_letter + '.value'))\n            self.addXlsxMetaColumns(column_letter, column_letter)\n\n    def iterload(self):\n        from openpyxl.utils.cell import get_column_letter\n        worksheet = self.source\n        for row in Progress(worksheet.iter_rows(), total=worksheet.max_row or 0):\n            yield AttrDict({get_column_letter(i+1): cell for i, cell in enumerate(row)})\n\n    def addXlsxMetaColumns(self, column_letter, column_name):\n        if self.options.xlsx_meta_columns:\n            self.addColumn(\n                    AttrColumn(column_name + '_cellPyObj', column_letter))\n            self.addColumn(\n                    AttrColumn(column_name + '_fontColor',\n                        column_letter + '.font.color.value'))\n            self.addColumn(\n                    AttrColumn(column_name + '_fillColor', column_letter +\n                        '.fill.start_color.value'))\n\n    def paste_after(self, rowidx):\n        to_paste = list(copy.copy(r) for r in reversed(vd.memory.cliprows))\n        self.addRows(to_paste, index=rowidx)\n\n\nclass XlsIndexSheet(IndexSheet):\n    'Load XLS file (in Excel format).'\n    rowtype = 'sheets'  # rowdef: xlsSheet\n    columns = [\n        Column('sheet', getter=lambda col,row: row.source.name),  # xls sheet name\n        ColumnAttr('name', width=0),  # visidata sheet name\n        ColumnAttr('nRows', type=int),\n        ColumnAttr('nCols', type=int),\n    ]\n    nKeys = 1\n    def iterload(self):\n        import xlrd\n        self.workbook = xlrd.open_workbook(str(self.source))\n        for sheetname in self.workbook.sheet_names():\n            yield XlsSheet(self.name, sheetname, source=self.workbook.sheet_by_name(sheetname))\n\n\nclass XlsSheet(SequenceSheet):\n    def iterload(self):\n        worksheet = self.source\n        for rownum in Progress(range(worksheet.nrows)):\n            yield list(worksheet.cell(rownum, colnum).value for colnum in range(worksheet.ncols))\n\n\n@Sheet.property\ndef xls_name(vs):\n    name = vs.names[-1]\n    if vs.options.clean_names:\n        cleaned_name = ''.join('_' if ch in ':[]*?/\\\\' else ch for ch in vs.name) #1122\n        name = cleaned_name[:31] #1122  #594\n        name = name.strip('_')\n\n    return name\n\n\n@VisiData.api\ndef save_xlsx(vd, p, *sheets):\n    import openpyxl\n\n    wb = openpyxl.Workbook()\n    wb.remove_sheet(wb['Sheet'])\n\n    for vs in sheets:\n        if vs.xls_name != vs.names[-1]:\n            vd.warning(f'saving {vs.name} as {vs.xls_name}')\n        ws = wb.create_sheet(title=vs.xls_name)\n\n        headers = [col.name for col in vs.visibleCols]\n        ws.append(headers)\n\n        for dispvals in vs.iterdispvals(format=False):\n\n            row = []\n            for col, v in dispvals.items():\n                if v is None:\n                    v = \"\"\n                elif col.type == date:\n                    v = datetime.datetime.fromtimestamp(int(v.timestamp()))\n                elif not vd.isNumeric(col):\n                    v = str(v)\n                row.append(v)\n\n            ws.append(row)\n\n    wb.active = ws\n\n    wb.save(filename=p)\n    vd.status(f'{p} save finished')\n\n\n@VisiData.api\ndef save_xls(vd, p, *sheets):\n    import xlwt\n\n    wb = xlwt.Workbook()\n\n    for vs in sheets:\n        if vs.xls_name != vs.name:\n            vd.warning(f'saving {vs.name} as {vs.xls_name}')\n        ws1 = wb.add_sheet(vs.xls_name)\n        for col_i, col in enumerate(vs.visibleCols):\n            ws1.write(0, col_i, col.name)\n\n        for r_i, dispvals in enumerate(vs.iterdispvals(format=True)):\n            r_i += 1\n            for c_i, v in enumerate(dispvals.values()):\n                ws1.write(r_i, c_i, v)\n\n    wb.save(p)\n    vd.status(f'{p} save finished')\n", "path": "visidata/loaders/xlsx.py"}]}