visidata 2.11.dev0__py3-none-any.whl → 3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ from visidata import vd, VisiData, ItemColumn, AttrDict
2
+
3
+
4
+ from .gitsheet import GitSheet
5
+ from .diff import GitDiffSheet
6
+
7
+
8
+ @VisiData.api
9
+ def git_stash(vd, p, args):
10
+ if 'list' in args:
11
+ return GitStashes('git-stash-list', source=p, gitargs=args)
12
+
13
+
14
+ class GitStashes(GitSheet):
15
+ guide = '''
16
+ # git stash
17
+ This is the list of changes that have been stashed previously.
18
+
19
+ `a` to apply this stashed change (without removing it)
20
+ `d` to drop this stashed change
21
+ `b` to create a branch from this stashed change'),
22
+ '''
23
+ rowtype = 'stashed change' # rowdef: AttrDict(stashid=, branched_from=, sha1=, msg=)
24
+ columns = [
25
+ ItemColumn('stashid'),
26
+ ItemColumn('branched_from'),
27
+ ItemColumn('sha1'),
28
+ ItemColumn('msg'),
29
+ ItemColumn('line', width=0),
30
+ ]
31
+
32
+ def iterload(self):
33
+ for line in self.git_lines('stash', *self.gitargs):
34
+ stashid, ctx, rest = line.split(': ', 2)
35
+ if ctx.startswith('WIP on '):
36
+ branched_from = ctx[len('WIP on '):]
37
+ sha1, msg = rest.split(' ', 1)
38
+ elif ctx.startswith('On '):
39
+ branched_from = ctx[len('On '):]
40
+ sha1 = ''
41
+ msg = rest
42
+ yield AttrDict(
43
+ line=line,
44
+ stashid=stashid,
45
+ branched_from=branched_from,
46
+ sha1=sha1,
47
+ msg=msg.strip(),
48
+ )
49
+
50
+ def openRow(self, row):
51
+ 'open this stashed change'
52
+ return GitDiffSheet(row.stashid, "diffs", gitargs=['stash show --no-color --patch', row.stashid], source=self.source)
53
+
54
+
55
+ GitSheet.addCommand('', 'git-open-stashes', 'vd.push(git_stash(source, ["list"]))', 'push stashes sheet')
56
+
57
+ GitStashes.addCommand('a', 'git-stash-apply', 'loggit("stash", "apply", cursorRow[0])', 'apply this stashed change without removing')
58
+ GitStashes.addCommand('', 'git-stash-pop', 'loggit("stash", "pop", cursorRow[0])', 'apply this stashed change and drop it')
59
+ GitStashes.addCommand('d', 'git-stash-drop', 'loggit("stash", "drop", cursorRow[0])', 'drop this stashed change')
60
+ GitStashes.addCommand('b', 'git-stash-branch', 'loggit("stash", "branch", input("create branch from stash named: "), cursorRow[0])', 'create branch from stash')
61
+
62
+
63
+ vd.addMenuItems('''
64
+ Git > Open > stashes > git-open-stashes
65
+ Git > Stash > apply > git-stash-apply
66
+ Git > Stash > drop > git-stash-drop
67
+ Git > Stash > apply then drop > git-stash-pop
68
+ Git > Stash > create branch > git-stash-branch
69
+ ''')
@@ -0,0 +1,204 @@
1
+ from visidata import vd, Column, VisiData, ItemColumn, Path, AttrDict, BaseSheet, IndexSheet
2
+ from visidata import RowColorizer, CellColorizer
3
+ from visidata import filesize, modtime, date
4
+
5
+ from .gitsheet import GitSheet
6
+ #from .diff import DifferSheet
7
+
8
+ vd.option('vgit_show_ignored', False, 'show ignored files on git status')
9
+ vd.theme_option('color_git_staged_mod', 'green', 'color of files staged with modifications')
10
+ vd.theme_option('color_git_staged_add', 'green', 'color of files staged for addition')
11
+ vd.theme_option('color_git_staged_del', 'red', 'color of files staged for deletion')
12
+ vd.theme_option('color_git_unstaged_del', 'on 88', 'color of files deleted but unstaged')
13
+ vd.theme_option('color_git_untracked', '243 blue', 'color of ignored/untracked files')
14
+
15
+
16
+ @VisiData.api
17
+ def git_status(vd, p, args, **kwargs):
18
+ vs = GitStatus('/'.join(p.parts[-2:]), source=p)
19
+ if not vs.gitRootPath:
20
+ return vd.git_repos(p, [])
21
+ return vs
22
+
23
+ class GitFile:
24
+ def __init__(self, path, gitsrc):
25
+ self.path = path
26
+ self.filename = path.relative_to(gitsrc)
27
+ self.is_dir = self.path.is_dir()
28
+
29
+ def __str__(self):
30
+ return str(self.filename) + (self.is_dir and '/' or '')
31
+
32
+ class GitStatus(GitSheet):
33
+ rowtype = 'files' # rowdef: GitFile
34
+ guide = '''
35
+ # git status
36
+ An overview of the local git checkout.
37
+
38
+ - `Enter` to open diff of file (`git diff`)
39
+ - `a` to stage changes in file (`git add`)
40
+ - `r` to unstage changes in file (`git reset`)
41
+ - `c` to revert all unstaged changes in file (`git checkout`)
42
+ - `d` to stage the entire file for deletion (`git rm`)
43
+ - `z Ctrl+S` to commit staged changes (`git commit`)
44
+ '''
45
+
46
+ columns = [
47
+ Column('path', width=40, getter=lambda c,r: str(r)),
48
+ Column('status', getter=lambda c,r: c.sheet.statusText(c.sheet.git_status(r)), width=8),
49
+ Column('status_raw', getter=lambda c,r: c.sheet.git_status(r), width=0),
50
+ Column('staged', getter=lambda c,r: c.sheet.git_status(r).dels),
51
+ Column('unstaged', getter=lambda c,r: c.sheet.git_status(r).adds),
52
+ Column('type', getter=lambda c,r: r.is_dir() and '/' or r.suffix, width=0),
53
+ Column('size', type=int, getter=lambda c,r: filesize(r)),
54
+ Column('modtime', type=date, getter=lambda c,r: modtime(r)),
55
+ ]
56
+ nKeys = 1
57
+
58
+ colorizers = [
59
+ CellColorizer(3, 'color_git_staged_mod', lambda s,c,r,v: r and c and c.name == 'staged' and s.git_status(r).status[0] == 'M'), # staged mod
60
+ CellColorizer(1, 'color_git_staged_del', lambda s,c,r,v: r and c and c.name == 'staged' and s.git_status(r).status == 'D '), # staged delete
61
+ RowColorizer(1, 'color_git_staged_add', lambda s,c,r,v: r and s.git_status(r).status in ['A ', 'M ']), # staged add/mod
62
+ RowColorizer(1, 'color_git_unstaged_del', lambda s,c,r,v: r and s.git_status(r).status[1] == 'D'), # unstaged delete
63
+ RowColorizer(3, 'color_git_untracked', lambda s,c,r,v: r and s.git_status(r).status == '!!'), # ignored
64
+ RowColorizer(1, 'color_git_untracked', lambda s,c,r,v: r and s.git_status(r).status == '??'), # untracked
65
+ ]
66
+
67
+ def statusText(self, st):
68
+ vmod = {'A': 'add', 'D': 'rm', 'M': 'mod', 'T': 'chmod', '?': '', '!': 'ignored', 'U': 'unmerged'}
69
+ x, y = st.status
70
+ if st == '??': # untracked
71
+ return 'new'
72
+ elif st == '!!': # ignored
73
+ return 'ignored'
74
+ elif x != ' ' and y == ' ': # staged
75
+ return vmod.get(x, x)
76
+ elif y != ' ': # unstaged
77
+ return vmod.get(y, y)
78
+ else:
79
+ return ''
80
+
81
+ @property
82
+ def workdir(self):
83
+ return str(self.source)
84
+
85
+ def git_status(self, r):
86
+ '''return tuple of (status, adds, dels).
87
+ status like !! ??
88
+ adds and dels are lists of additions and deletions.
89
+ '''
90
+ if not r:
91
+ return None
92
+
93
+ fn = str(r)
94
+ ret = self._cachedStatus.get(fn, None)
95
+ if not ret:
96
+ ret = AttrDict(status='??')
97
+ self._cachedStatus[fn] = ret
98
+
99
+ return ret
100
+
101
+ def ignored(self, fn):
102
+ if self.options.vgit_show_ignored:
103
+ return False
104
+
105
+ if fn in self._cachedStatus:
106
+ return self._cachedStatus[fn].status == '!!'
107
+
108
+ return False
109
+
110
+ @property
111
+ def remotediff(self):
112
+ return self.gitBranchStatuses.get(self.branch, 'no branch')
113
+
114
+ def iterload(self):
115
+ files = [GitFile(p, self.source) for p in self.source.iterdir() if p.base_stem not in ('.git')] # files in working dir
116
+
117
+ filenames = dict((gf.filename, gf) for gf in files)
118
+
119
+ self._cachedStatus.clear()
120
+ for fn in self.git_iter('ls-files', '-z'):
121
+ self._cachedStatus[fn] = AttrDict(status=' ')
122
+
123
+ for line in self.git_iter('status', '-z', '-unormal', '--ignored'):
124
+ if not line: continue
125
+
126
+ if line[2:3] == ' ':
127
+ st, fn = line[:2], line[3:]
128
+ else:
129
+ fn = line
130
+ st = '??' # untracked file
131
+
132
+ self._cachedStatus[fn] = AttrDict(status=st)
133
+ if not self.ignored(fn):
134
+ yield Path(fn)
135
+
136
+ for line in self.git_iter('diff-files', '--numstat', '-z'):
137
+ if not line: continue
138
+ adds, dels, fn = line.split('\t')
139
+ if fn not in self._cachedStatus:
140
+ self._cachedStatus[fn] = AttrDict(status='##')
141
+ cs = self._cachedStatus[fn]
142
+ cs.adds = '+%s/-%s' % (adds, dels)
143
+
144
+ for line in self.git_iter('diff-index', '--cached', '--numstat', '-z', 'HEAD'):
145
+ if not line: continue
146
+ adds, dels, fn = line.split('\t')
147
+ if fn not in self._cachedStatus:
148
+ self._cachedStatus[fn] = AttrDict(status='$$')
149
+ cs = self._cachedStatus[fn]
150
+ cs.dels = '+%s/-%s' % (adds, dels)
151
+
152
+ self.orderBy(None, self.columns[-1], reverse=True)
153
+
154
+ self.recalc() # erase column caches
155
+
156
+ def openRow(self, row):
157
+ 'Open unstaged diffs for this file, or dive into directory'
158
+ if row.is_dir:
159
+ return GitStatus(row.path)
160
+ else:
161
+ return DifferSheet(row, "HEAD", "index", "working", source=sheet)
162
+
163
+ def openRows(self, rows):
164
+ 'Open unstaged hunks for selected rows'
165
+ return getHunksSheet(sheet, *rows)
166
+
167
+
168
+ @GitStatus.lazy_property
169
+ def _cachedStatus(self):
170
+ return {} # [filename] -> AttrDict(status='xx', adds=, dels=)
171
+
172
+
173
+ GitStatus.addCommand('a', 'git-add', 'loggit("add", cursorRow.filename)', 'add this new file or modified file to staging')
174
+ #GitStatus.addCommand('m', 'git-mv', 'loggit("mv", cursorRow.filename, input("rename file to: ", value=cursorRow.filename))', 'rename this file')
175
+ GitStatus.addCommand('d', 'git-rm', 'loggit("rm", cursorRow.filename)', 'stage this file for deletion')
176
+ GitStatus.addCommand('r', 'git-reset', 'loggit("reset", "HEAD", cursorRow.filename)', 'reset/unstage this file')
177
+ GitStatus.addCommand('c', 'git-checkout', 'loggit("checkout", cursorRow.filename)', 'checkout this file')
178
+ GitStatus.addCommand('ga', 'git-add-selected', 'loggit("add", *[r for r in selectedRows])', 'add all selected files to staging')
179
+ GitStatus.addCommand('gd', 'git-rm-selected', 'loggit("rm", *[r for r in selectedRows])', 'delete all selected files')
180
+ GitStatus.addCommand(None, 'git-commit', 'loggit("commit", "-m", input("commit message: "))', 'commit changes')
181
+ GitStatus.addCommand(None, 'git-ignore-file', 'open(rootPath/".gitignore", "a").write(cursorRow.filename+"\\n"); reload()', 'add file to toplevel .gitignore')
182
+ GitStatus.addCommand(None, 'git-ignore-wildcard', 'open(rootPath/.gitignore, "a").write(input("add wildcard to .gitignore: "))', 'add input line to toplevel .gitignore')
183
+
184
+
185
+ #GitStatus.addCommand('z^J', 'diff-file-staged', 'vd.push(getStagedHunksSheet(sheet, cursorRow))', 'push staged diffs for this file')
186
+ #GitStatus.addCommand('gz^J', 'diff-selected-staged', 'vd.push(getStagedHunksSheet(sheet, *(selectedRows or rows)))', 'push staged diffs for selected files or all files')
187
+ #GitStatus.addCommand('^O', 'sysopen-row', 'launchExternalEditorPath(Path(cursorRow.path))', 'open this file in $EDITOR')
188
+
189
+
190
+ vd.addMenuItems('''
191
+ Git > View staged changes > current file > diff-file-staged
192
+ Git > View staged changes > selected files > staged changes > diff-selected-staged
193
+ Git > Stage > current file > git-add
194
+ Git > Stage > selected files > git-add-selected
195
+ Git > Unstage > current file > git-reset
196
+ Git > Unstage > selected files > git-reset-selected
197
+ Git > Rename file > git-mv
198
+ Git > Delete > file > git-rm
199
+ Git > Delete > selected files > git-rm-selected
200
+ Git > Ignore > file > ignore-file
201
+ Git > Ignore > wildcard > ignore-wildcard
202
+ Git > Commit staged changes > git-commit
203
+ Git > Revert unstaged changes > current file > git-checkout
204
+ ''')
@@ -0,0 +1,34 @@
1
+ from .gitsheet import vd, GitSheet
2
+
3
+
4
+ GitSheet.options.disp_status_fmt = '{sheet.progressStatus}‹{sheet.branchStatus}› {sheet.name}| '
5
+
6
+ @GitSheet.property
7
+ def progressStatus(sheet):
8
+ inp = sheet.gitInProgress()
9
+ return ('[%s] ' % inp) if inp else ''
10
+
11
+
12
+ @GitSheet.property
13
+ def branchStatus(sheet):
14
+ if hasattr(sheet.gitRootSheet, 'branch'):
15
+ return '%s%s' % (sheet.rootSheet.branch, sheet.rootSheet.remotediff)
16
+ return ''
17
+
18
+
19
+ @GitSheet.api
20
+ def gitInProgress(sheet):
21
+ p = sheet.gitPath
22
+ if not p:
23
+ return 'no repo'
24
+ if (p/'rebase-merge').exists() or (p/'rebase-apply/rebasing').exists():
25
+ return 'rebasing'
26
+ elif p/'rebase-apply'.exists():
27
+ return 'applying'
28
+ elif p/'CHERRY_PICK_HEAD'.exists():
29
+ return 'cherry-picking'
30
+ elif p/'MERGE_HEAD'.exists():
31
+ return 'merging'
32
+ elif p/'BISECT_LOG'.exists():
33
+ return 'bisecting'
34
+ return ''
visidata/basesheet.py CHANGED
@@ -1,8 +1,7 @@
1
1
  import os
2
2
 
3
3
  import visidata
4
- from visidata import Extensible, VisiData, vd, EscapeException, cleanName
5
- from unittest import mock
4
+ from visidata import Extensible, VisiData, vd, EscapeException, MissingAttrFormatter, AttrDict
6
5
 
7
6
 
8
7
  UNLOADED = tuple() # sentinel for a sheet not yet loaded for the first time
@@ -98,6 +97,7 @@ class BaseSheet(DrawablePane):
98
97
  rowtype = 'objects' # one word, plural, describing the items
99
98
  precious = True # False for a few discardable metasheets
100
99
  defer = False # False for not deferring changes until save
100
+ guide = '' # default to show in sidebar
101
101
 
102
102
  def _obj_options(self):
103
103
  return vd.OptionsObject(vd._options, obj=self)
@@ -107,15 +107,14 @@ class BaseSheet(DrawablePane):
107
107
 
108
108
  class_options = options = _dualproperty(_obj_options, _class_options)
109
109
 
110
- def __init__(self, *names, **kwargs):
110
+ def __init__(self, *names, rows=UNLOADED, **kwargs):
111
111
  self._name = None # initial cache value necessary for self.options
112
- self.names = names
112
+ self.loading = False
113
+ self.names = list(names)
113
114
  self.name = self.options.name_joiner.join(str(x) for x in self.names if x)
114
115
  self.source = None
115
- self.rows = UNLOADED # list of opaque objects
116
- self._scr = mock.MagicMock(__bool__=mock.Mock(return_value=False)) # disable curses in batch mode
117
- self.mouseX = 0
118
- self.mouseY = 0
116
+ self.rows = rows # list of opaque objects
117
+ self._scr = None
119
118
  self.hasBeenModified = False
120
119
 
121
120
  super().__init__(**kwargs)
@@ -153,6 +152,14 @@ class BaseSheet(DrawablePane):
153
152
  def __str__(self):
154
153
  return self.name
155
154
 
155
+ @property
156
+ def rows(self):
157
+ return self._rows
158
+
159
+ @rows.setter
160
+ def rows(self, rows):
161
+ self._rows = rows
162
+
156
163
  @property
157
164
  def nRows(self):
158
165
  'Number of rows on this sheet. Override in subclass.'
@@ -165,11 +172,26 @@ class BaseSheet(DrawablePane):
165
172
  return vs in self.source
166
173
  return False
167
174
 
168
- def execCommand(self, cmd, vdglobals=None, keystrokes=None):
169
- cmd = self.getCommand(cmd or keystrokes)
175
+ @property
176
+ def displaySource(self):
177
+ if isinstance(self.source, BaseSheet):
178
+ return f'the *{self.source[0]}* sheet'
179
+
180
+ if isinstance(self.source, (list, tuple)):
181
+ if len(self.source) == 1:
182
+ return f'the **{self.source[0]}** sheet'
183
+ return f'{len(self.source)} sheets'
184
+
185
+ return f'**{self.source}**'
186
+
187
+ def execCommand(self, longname, vdglobals=None, keystrokes=None):
188
+ if ' ' in longname:
189
+ cmd, arg = longname.split(' ', maxsplit=1)
190
+ vd.injectInput(arg)
191
+
192
+ cmd = self.getCommand(longname or keystrokes)
170
193
  if not cmd:
171
- if keystrokes:
172
- vd.status('no command for %s' % keystrokes)
194
+ vd.warning('no command for %s' % (longname or keystrokes))
173
195
  return False
174
196
 
175
197
  escaped = False
@@ -183,25 +205,30 @@ class BaseSheet(DrawablePane):
183
205
  try:
184
206
  for hookfunc in vd.beforeExecHooks:
185
207
  hookfunc(self, cmd, '', keystrokes)
186
- vd.debug(cmd.longname)
187
208
  escaped = super().execCommand2(cmd, vdglobals=vdglobals)
188
209
  except Exception as e:
189
210
  vd.debug(cmd.execstr)
190
211
  err = vd.exceptionCaught(e)
191
212
  escaped = True
192
213
 
193
- try:
194
- if vd.cmdlog:
195
- # sheet may have changed
196
- vd.cmdlog.afterExecSheet(vd.activeSheet, escaped, err)
197
- except Exception as e:
198
- vd.exceptionCaught(e)
214
+ if vd.cmdlog:
215
+ # sheet may have changed
216
+ vd.callNoExceptions(vd.cmdlog.afterExecSheet, vd.activeSheet, escaped, err)
199
217
 
200
- self.checkCursorNoExceptions()
218
+ vd.callNoExceptions(self.checkCursor)
201
219
 
202
220
  vd.clearCaches()
221
+
222
+ for t in self.currentThreads:
223
+ if not hasattr(t, 'lastCommand'):
224
+ t.lastCommand = True
225
+
203
226
  return escaped
204
227
 
228
+ @property
229
+ def lastCommandThreads(self):
230
+ return [t for t in self.currentThreads if getattr(t, 'lastCommand', None)]
231
+
205
232
  @property
206
233
  def names(self):
207
234
  return self._names
@@ -224,8 +251,7 @@ class BaseSheet(DrawablePane):
224
251
  self._name = self.maybeClean(str(name))
225
252
 
226
253
  def maybeClean(self, s):
227
- if self.options.clean_names:
228
- s = cleanName(s)
254
+ 'stub'
229
255
  return s
230
256
 
231
257
  def recalc(self):
@@ -233,10 +259,8 @@ class BaseSheet(DrawablePane):
233
259
  pass
234
260
 
235
261
  def refresh(self):
236
- 'Clear the terminal screen and let the next draw cycle redraw everything.'
237
- if self._scr:
238
- self._scr.clear()
239
- self._scr.refresh()
262
+ 'Recalculate any internal state needed for `draw()`. Overridable.'
263
+ pass
240
264
 
241
265
  def ensureLoaded(self):
242
266
  'Call ``reload()`` if not already loaded.'
@@ -257,30 +281,14 @@ class BaseSheet(DrawablePane):
257
281
  'Check cursor and fix if out-of-bounds. Overridable.'
258
282
  pass
259
283
 
260
- def checkCursorNoExceptions(self):
261
- try:
262
- return self.checkCursor()
263
- except Exception as e:
264
- vd.exceptionCaught(e)
265
-
266
284
  def evalExpr(self, expr, **kwargs):
267
285
  'Evaluate Python expression *expr* in the context of *kwargs* (may vary by sheet type).'
268
286
  return eval(expr, vd.getGlobals(), None)
269
287
 
270
- @property
271
- def sidebar(self):
272
- 'Default implementation just returns set value. Overridable.'
273
- return self._sidebar
288
+ def formatString(self, fmt, **kwargs):
289
+ 'Return formatted string with *sheet* and *vd* accessible to expressions. Missing expressions return empty strings instead of error.'
290
+ return MissingAttrFormatter().format(fmt, sheet=self, vd=vd, **kwargs)
274
291
 
275
- @sidebar.setter
276
- def sidebar(self, v):
277
- 'Default implementation just sets value. Overridable.'
278
- self._sidebar = v
279
-
280
- @property
281
- def sidebar_title(self):
282
- 'Default implementation returns fixed value. Overridable.'
283
- return 'sidebar'
284
292
 
285
293
 
286
294
  @VisiData.api
@@ -288,10 +296,11 @@ def redraw(vd):
288
296
  'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.'
289
297
  for vs in vd.sheets:
290
298
  vs._scr = None
291
- vd.scrFull.clear()
292
- vd.win1.clear()
293
- vd.win2.clear()
294
- vd.setWindows(vd.scrFull)
299
+ if vd.win1: vd.win1.clear()
300
+ if vd.win2: vd.win2.clear()
301
+ if vd.scrFull:
302
+ vd.scrFull.clear()
303
+ vd.setWindows(vd.scrFull)
295
304
 
296
305
 
297
306
  @VisiData.property
@@ -299,7 +308,7 @@ def sheet(self):
299
308
  return self.activeSheet
300
309
 
301
310
  @VisiData.api
302
- def isLongname(self, ks):
311
+ def isLongname(self, ks:str):
303
312
  'Return True if *ks* is a longname.'
304
313
  return ('-' in ks) and (ks[-1] != '-') or (len(ks) > 3 and ks.islower())
305
314