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.
- visidata/__init__.py +72 -91
- visidata/_input.py +263 -44
- visidata/_open.py +84 -29
- visidata/_types.py +22 -4
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- visidata/apps/vgit/__main__.py +3 -0
- visidata/apps/vgit/abort.py +23 -0
- visidata/apps/vgit/blame.py +76 -0
- visidata/apps/vgit/branch.py +153 -0
- visidata/apps/vgit/config.py +95 -0
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- visidata/apps/vgit/grep.py +37 -0
- visidata/apps/vgit/log.py +81 -0
- visidata/apps/vgit/main.py +55 -0
- visidata/apps/vgit/remote.py +57 -0
- visidata/apps/vgit/repos.py +71 -0
- visidata/apps/vgit/setup.py +37 -0
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- visidata/apps/vgit/statusbar.py +34 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +251 -99
- visidata/choose.py +15 -11
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +84 -18
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +89 -114
- visidata/color.py +142 -56
- visidata/column.py +134 -131
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +32 -6
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +4 -2
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +33 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +3 -5
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macos.py +1 -1
- visidata/macros.py +130 -41
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -154
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +302 -147
- visidata/man/vd.txt +291 -151
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +79 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +131 -43
- visidata/pivot.py +74 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +42 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -24
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +18 -6
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.dev0.dist-info/RECORD +0 -142
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
# requires shell tools: ping traceroute
|
2
|
+
# these tools aren't inlined as they require root privs
|
3
|
+
|
4
|
+
import re
|
5
|
+
import time
|
6
|
+
from statistics import mean
|
7
|
+
|
8
|
+
from visidata import vd, VisiData, BaseSheet, Sheet, Column, AttrColumn, Progress
|
9
|
+
|
10
|
+
vd.theme_option('color_shellcmd', '21 on 114 green', '')
|
11
|
+
vd.theme_option('color_colname', 'underline', '')
|
12
|
+
vd.theme_option('color_longname', 'bold 52 on 114 green', '')
|
13
|
+
|
14
|
+
|
15
|
+
@VisiData.api
|
16
|
+
def new_ping(vd, p):
|
17
|
+
'Open a sheet with the round-trip time for each hop along the path to the given host.'
|
18
|
+
pingsheet = PingSheet(p.given, source=p.given)
|
19
|
+
return PingStatsSheet("traceroute_"+pingsheet.name, source=pingsheet)
|
20
|
+
|
21
|
+
|
22
|
+
class PingStatsSheet(Sheet):
|
23
|
+
help='''# ping/traceroute
|
24
|
+
This sheet runs {shellcmd}traceroute{} to generate intermediate hops, then runs {shellcmd}ping{} against each hop N times to get the {colname}avg_ms{} and {colname}max_ms{} ping time.
|
25
|
+
|
26
|
+
{longname}open-source{} to open the raw ping data.
|
27
|
+
'''
|
28
|
+
rowtype='hosts' # rowdef: PingColumn
|
29
|
+
columns = [
|
30
|
+
Column('hop', type=int, width=5, getter=lambda col,r: col.sheet.source.sources.index(r.ip)+1),
|
31
|
+
Column('hostname', getter=lambda col,r: r.name, width=40),
|
32
|
+
Column('ip', getter=lambda col,r: r.ip, width=17),
|
33
|
+
Column('avg_ms', type=float, fmtstr='%.1f', getter=lambda col,r: mean(r.getValues(col.sheet.source.rows))),
|
34
|
+
Column('max_ms', type=float, fmtstr='%.1f', getter=lambda col,r: max(r.getValues(col.sheet.source.rows))),
|
35
|
+
Column('count', type=int, getter=lambda col,r: len(list(r.getValues(col.sheet.source.rows)))),
|
36
|
+
]
|
37
|
+
def loader(self):
|
38
|
+
sheet = self.source
|
39
|
+
sheet.ensureLoaded()
|
40
|
+
|
41
|
+
self.rows = sheet.columns
|
42
|
+
|
43
|
+
|
44
|
+
def PingColumn(name, ip):
|
45
|
+
return Column(name, ip=ip, getter=lambda c,r: r.get(c.ip), type=float, fmtstr='%0.1f')
|
46
|
+
|
47
|
+
|
48
|
+
class PingSheet(Sheet):
|
49
|
+
rowtype = 'pings' # rowdef: {'time':time.time(), 'hostname': pingtime}
|
50
|
+
def __init__(self, *names, source=None, **kwargs):
|
51
|
+
super().__init__(*names, source=source, **kwargs)
|
52
|
+
self.sources = [source]
|
53
|
+
|
54
|
+
def ping_response(self, row, ip, data):
|
55
|
+
for line in data.splitlines():
|
56
|
+
m = re.search(r'from ([0-9\.]+).* time=(.*) ms', line)
|
57
|
+
if m:
|
58
|
+
row[ip] = m.group(2)
|
59
|
+
# assert ip == m.group(1)
|
60
|
+
|
61
|
+
def traceroute_response(self, ip, data):
|
62
|
+
for line in data.splitlines():
|
63
|
+
m = re.search(r'(\d+) (\S+) \((\S+)\) (.*) ms', line)
|
64
|
+
if m:
|
65
|
+
ttl, hostname, inner_ip, latency_ms = m.groups()
|
66
|
+
self.routes[ip][int(ttl)-1] = inner_ip
|
67
|
+
if inner_ip == ip:
|
68
|
+
return # traceroute work is done, let ping do the rest
|
69
|
+
|
70
|
+
if inner_ip not in self.sources:
|
71
|
+
self.sources.insert(-1, inner_ip)
|
72
|
+
self.addColumn(PingColumn(hostname, inner_ip), index=-1)
|
73
|
+
self.send_trace(ip, int(ttl)+1) # get next hop
|
74
|
+
break
|
75
|
+
|
76
|
+
def ping_error(self, ip, data):
|
77
|
+
if ip in self.sources:
|
78
|
+
self.sources.remove(ip)
|
79
|
+
vd.warning("%s removed: %s" % (ip, data))
|
80
|
+
|
81
|
+
def update_traces(self, row, ip):
|
82
|
+
rtes = self.routes.get(ip)
|
83
|
+
if rtes is None:
|
84
|
+
rtes = []
|
85
|
+
self.routes[ip] = rtes
|
86
|
+
|
87
|
+
if ip in rtes:
|
88
|
+
return
|
89
|
+
|
90
|
+
for i, inner_ip in enumerate(rtes):
|
91
|
+
if inner_ip is None:
|
92
|
+
self.send_trace(ip, i+1)
|
93
|
+
|
94
|
+
self.send_trace(ip, len(rtes)+1) # and one more for the road
|
95
|
+
|
96
|
+
def send_trace(self, ip, n):
|
97
|
+
sh = vd.importExternal('sh')
|
98
|
+
|
99
|
+
while n > len(self.routes[ip]):
|
100
|
+
self.routes[ip].append(None)
|
101
|
+
|
102
|
+
sh.traceroute('--sim-queries=1',
|
103
|
+
'--queries=1',
|
104
|
+
'--first=%s' % n,
|
105
|
+
'--max-hops=%s' % n,
|
106
|
+
ip,
|
107
|
+
_bg=True, _bg_exc=False,
|
108
|
+
_out=lambda data,self=self,ip=ip,a=1: self.traceroute_response(ip, data),
|
109
|
+
_err=lambda data,self=self,ip=ip,a=1: self.ping_error(ip, data))
|
110
|
+
|
111
|
+
def iterload(self):
|
112
|
+
sh = vd.importExternal('sh')
|
113
|
+
|
114
|
+
self.stop = False
|
115
|
+
self.start_time = time.time()
|
116
|
+
self.columns.clear()
|
117
|
+
|
118
|
+
for ip in self.sources:
|
119
|
+
self.addColumn(PingColumn(ip, ip))
|
120
|
+
|
121
|
+
self.routes = {}
|
122
|
+
self.rows = []
|
123
|
+
pings_sent = {}
|
124
|
+
ping_count = self.options.ping_count
|
125
|
+
with Progress(total=ping_count*len(self.sources), gerund='pinging') as prog:
|
126
|
+
while not self.stop:
|
127
|
+
r = {'time':time.time()}
|
128
|
+
yield r
|
129
|
+
|
130
|
+
npings = 0 # this loop
|
131
|
+
for ip in self.sources:
|
132
|
+
self.update_traces(r, ip)
|
133
|
+
pings_sent[ip] = pings_sent.get(ip, 0)+1
|
134
|
+
if ping_count and pings_sent[ip] <= ping_count:
|
135
|
+
sh.ping('-c', '1', ip, _bg=True, _bg_exc=False, _timeout=30,
|
136
|
+
_out=lambda data,self=self,r=r,ip=ip: self.ping_response(r,ip,data),
|
137
|
+
_err=lambda data,self=self,ip=ip,a=1: self.ping_error(ip, data))
|
138
|
+
npings += 1
|
139
|
+
else:
|
140
|
+
r[ip] = None
|
141
|
+
|
142
|
+
if npings == 0:
|
143
|
+
vd.status('no more pings to send')
|
144
|
+
break
|
145
|
+
|
146
|
+
time.sleep(self.options.ping_interval)
|
147
|
+
|
148
|
+
|
149
|
+
vd.option('ping_count', 3, 'send this many pings to each host', sheettype=PingSheet)
|
150
|
+
vd.option('ping_interval', 0.1, 'wait between ping rounds, in seconds', sheettype=PingSheet)
|
151
|
+
|
152
|
+
PingSheet.options.null_value = False
|
153
|
+
BaseSheet.addCommand('', 'open-ping', 'vd.push(openSource(input("ping: ", type="hostip"), filetype="ping"))', 'open sheet to ping input IP Address')
|
154
|
+
|
155
|
+
vd.addMenuItems('''
|
156
|
+
System > Ping IP/hostname > open-ping
|
157
|
+
''')
|
@@ -0,0 +1,208 @@
|
|
1
|
+
from visidata import *
|
2
|
+
|
3
|
+
|
4
|
+
@VisiData.api
|
5
|
+
def new_top(vd, p):
|
6
|
+
return vd.processes
|
7
|
+
|
8
|
+
|
9
|
+
class CPUStatsSheet(Sheet):
|
10
|
+
rowtype='CPUs' # rowdef = (count, perfect, times, freq) from psutil (see below)
|
11
|
+
columns = [
|
12
|
+
ColumnItem('cpu', 0, type=int, keycol=0),
|
13
|
+
ColumnItem('cpu_pct', 1, type=float)
|
14
|
+
]
|
15
|
+
def iterload(self):
|
16
|
+
psutil = vd.importExternal('psutil')
|
17
|
+
|
18
|
+
for i, k in enumerate(psutil.cpu_times()._fields):
|
19
|
+
self.addColumn(SubColumnItem(2, ColumnItem(k+'_s', i, type=float, sheet=self)))
|
20
|
+
|
21
|
+
for i, k in enumerate(psutil.cpu_freq()._fields):
|
22
|
+
self.addColumn(SubColumnItem(3, ColumnItem(k+'_MHz', i, type=float, sheet=self)))
|
23
|
+
|
24
|
+
for r in zip(range(psutil.cpu_count()),
|
25
|
+
psutil.cpu_percent(percpu=True),
|
26
|
+
psutil.cpu_times(percpu=True),
|
27
|
+
psutil.cpu_freq(percpu=True)):
|
28
|
+
yield r
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
class MemStatsSheet(Sheet):
|
33
|
+
rowtype = '' # rowdef: (name, value)
|
34
|
+
columns = [
|
35
|
+
AttrColumn('t', type=date, fmtstr='%H:%M:%S'),
|
36
|
+
AttrColumn('meminfo'),
|
37
|
+
AttrColumn('virtmem'),
|
38
|
+
AttrColumn('swapmem'),
|
39
|
+
]
|
40
|
+
nKeys = 1
|
41
|
+
|
42
|
+
def iterload(self):
|
43
|
+
psutil = vd.importExternal('psutil')
|
44
|
+
import time
|
45
|
+
|
46
|
+
proc = psutil.Process()
|
47
|
+
|
48
|
+
while True:
|
49
|
+
yield AttrDict(t=time.time(),
|
50
|
+
virtmem=psutil.virtual_memory(),
|
51
|
+
swapmem=psutil.swap_memory(),
|
52
|
+
meminfo=proc.memory_info(),
|
53
|
+
)
|
54
|
+
|
55
|
+
if self.nRows == 1: # first time through
|
56
|
+
for n in 'meminfo virtmem swapmem'.split():
|
57
|
+
c = self.column(n)
|
58
|
+
if not c.hidden:
|
59
|
+
c.expand([self.rows[0]])
|
60
|
+
|
61
|
+
time.sleep(1)
|
62
|
+
|
63
|
+
|
64
|
+
class UsefulProcessesSheet(Sheet):
|
65
|
+
columns = [
|
66
|
+
Column('pid', type=int, getter=lambda c,r: r[0].pid),
|
67
|
+
Column('name', getter=lambda c,r: r[0].name()),
|
68
|
+
Column('status', getter=lambda c,r: r[0].status()),
|
69
|
+
Column('cmdline', getter=lambda c,r: ' '.join(r[0].cmdline())),
|
70
|
+
Column('user', getter=lambda c,r: r[0].username()),
|
71
|
+
Column('real_uid', type=int, width=0, getter=lambda c,r: r[0].uids()[0]),
|
72
|
+
Column('effective_uid', type=int, width=0, getter=lambda c,r: r[0].uids()[1]),
|
73
|
+
Column('mem_uss', type=int, getter=lambda c,r: r[1].mem_uss),
|
74
|
+
Column('user_time_s', type=float, getter=lambda c,r: r[0].cpu_times()[0]),
|
75
|
+
Column('system_time_s', type=float, getter=lambda c,r: r[0].cpu_times()[1]),
|
76
|
+
]
|
77
|
+
def iterload(self):
|
78
|
+
psutil = vd.importExternal('psutil')
|
79
|
+
for pr in psutil.process_iter():
|
80
|
+
yield (pr, pr.memory_full_info())
|
81
|
+
|
82
|
+
|
83
|
+
class ProcessesSheet(Sheet):
|
84
|
+
columns = [
|
85
|
+
Column('pid', type=int, getter=lambda c,r: r.pid),
|
86
|
+
Column('name', getter=lambda c,r: r.name()),
|
87
|
+
Column('status', getter=lambda c,r: r.status()),
|
88
|
+
Column('parent_pid', type=int, getter=lambda c,r: r.ppid()),
|
89
|
+
Column('exe', getter=lambda c,r: r.exe()),
|
90
|
+
Column('cmdline', getter=lambda c,r: ' '.join(r.cmdline())),
|
91
|
+
Column('cwd', getter=lambda c,r: r.cwd()),
|
92
|
+
Column('user', getter=lambda c,r: r.username()),
|
93
|
+
|
94
|
+
Column('real_uid', type=int, width=0, getter=lambda c,r: r.uids()[0]),
|
95
|
+
Column('effective_uid', type=int, width=0, getter=lambda c,r: r.uids()[1]),
|
96
|
+
Column('saved_uid', type=int, width=0, getter=lambda c,r: r.uids()[2]),
|
97
|
+
|
98
|
+
Column('real_gid', type=int, width=0, getter=lambda c,r: r.gids()[0]),
|
99
|
+
Column('effective_gid', type=int, width=0, getter=lambda c,r: r.gids()[1]),
|
100
|
+
Column('saved_gid', type=int, width=0, getter=lambda c,r: r.gids()[2]),
|
101
|
+
|
102
|
+
Column('create_time', type=date, getter=lambda c,r: r.create_time()),
|
103
|
+
Column('cpu_num', type=int, getter=lambda c,r: r.cpu_num()),
|
104
|
+
Column('cpu_percent', type=float, getter=lambda c,r: r.cpu_percent()),
|
105
|
+
|
106
|
+
Column('tty', getter=lambda c,r: r.terminal()),
|
107
|
+
Column('nice', getter=lambda c,r: r.nice()),
|
108
|
+
Column('ioclass', getter=lambda c,r: r.ionice()[0]),
|
109
|
+
Column('ionice', getter=lambda c,r: r.ionice()[1]),
|
110
|
+
|
111
|
+
Column('user_time_s', type=float, getter=lambda c,r: r.cpu_times()[0]),
|
112
|
+
Column('system_time_s', type=float, getter=lambda c,r: r.cpu_times()[1]),
|
113
|
+
Column('children_user_time_s', type=float, width=0, getter=lambda c,r: r.cpu_times()[2]),
|
114
|
+
Column('children_system_time_s', type=float, width=0, getter=lambda c,r: r.cpu_times()[3]),
|
115
|
+
|
116
|
+
Column('read_ops', type=int, getter=lambda c,r: r.io_counters()[0]),
|
117
|
+
Column('write_ops', type=int, getter=lambda c,r: r.io_counters()[1]),
|
118
|
+
Column('read_bytes', type=int, getter=lambda c,r: r.io_counters()[2]),
|
119
|
+
Column('write_bytes', type=int, getter=lambda c,r: r.io_counters()[3]),
|
120
|
+
|
121
|
+
Column('voluntary_ctx_switches', type=int, getter=lambda c,r: r.num_ctx_switches()[0]),
|
122
|
+
Column('involuntary_ctx_switches', type=int, getter=lambda c,r: r.num_ctx_switches()[1]),
|
123
|
+
Column('num_fds', type=int, getter=lambda c,r: r.num_fds()),
|
124
|
+
Column('num_threads', type=int, getter=lambda c,r: r.num_threads()),
|
125
|
+
]
|
126
|
+
nKeys = 2
|
127
|
+
|
128
|
+
def iterload(self):
|
129
|
+
psutil = vd.importExternal('psutil')
|
130
|
+
# self.columns = []
|
131
|
+
# for c in ProcessesSheet.columns:
|
132
|
+
# self.addColumn(c)
|
133
|
+
|
134
|
+
for i,k in enumerate(psutil.Process().memory_full_info()._fields):
|
135
|
+
self.addColumn(Column('mem_'+k, type=int, getter=lambda c,r,i=i: r.memory_full_info()[i], cache=True))
|
136
|
+
# mem_uss is probably the most representative metric for determining how much memory is actually being used by a process. It represents the amount of memory that would be freed if the process was terminated right now.
|
137
|
+
|
138
|
+
yield from psutil.process_iter()
|
139
|
+
# try:
|
140
|
+
# self.addRow((pr, pr.memory_full_info()))
|
141
|
+
# except:
|
142
|
+
# self.addRow((pr, None))
|
143
|
+
|
144
|
+
class RlimitsSheet(Sheet):
|
145
|
+
columns = [
|
146
|
+
ColumnItem('rlimit', 0),
|
147
|
+
Column('soft', type=int, getter=lambda c,r: c.sheet.soft(r), setter=lambda c,r,v: c.sheet.set_soft(r, v)),
|
148
|
+
Column('hard', type=int, getter=lambda c,r: c.sheet.hard(r), setter=lambda c,r,v: c.sheet.set_hard(r, v))
|
149
|
+
]
|
150
|
+
|
151
|
+
def soft(self, r):
|
152
|
+
return self.source.rlimit(r[1])[0]
|
153
|
+
def hard(self, r):
|
154
|
+
return self.source.rlimit(r[1])[1]
|
155
|
+
def set_soft(self, r, v):
|
156
|
+
self.source.rlimit(r[1], (v, self.hard(r)))
|
157
|
+
def set_hard(self, r, v):
|
158
|
+
self.source.rlimit(r[1], (self.soft(r), v))
|
159
|
+
|
160
|
+
def iterload(self):
|
161
|
+
psutil = vd.importExternal('psutil')
|
162
|
+
for r in dir(psutil):
|
163
|
+
if r.startswith('RLIMIT'):
|
164
|
+
yield (r[7:], getattr(psutil, r))
|
165
|
+
|
166
|
+
|
167
|
+
@VisiData.lazy_property
|
168
|
+
def cpuStats(vd):
|
169
|
+
return CPUStatsSheet('cpu_stats')
|
170
|
+
|
171
|
+
@VisiData.lazy_property
|
172
|
+
def memStats(vd):
|
173
|
+
return MemStatsSheet('memory_stats')
|
174
|
+
|
175
|
+
@VisiData.lazy_property
|
176
|
+
def processes(vd):
|
177
|
+
return ProcessesSheet('processes')
|
178
|
+
|
179
|
+
|
180
|
+
BaseSheet.addCommand('', 'open-cpustats', 'vd.push(vd.cpuStats)', 'open CPU usage stats' )
|
181
|
+
BaseSheet.addCommand('', 'open-memstats', 'vd.push(vd.memStats)', 'open memory usage stats')
|
182
|
+
BaseSheet.addCommand('', 'open-processes', 'vd.push(vd.processes)', 'open system process stats')
|
183
|
+
|
184
|
+
@VisiData.api
|
185
|
+
def chooseSignal(vd):
|
186
|
+
import signal
|
187
|
+
d = [{'key': attr[3:]} for attr in dir(signal) if attr.startswith('SIG') and not attr.startswith('SIG_')]
|
188
|
+
return getattr(signal, 'SIG'+vd.chooseOne(d, type='signal'))
|
189
|
+
|
190
|
+
ProcessesSheet.addCommand('d', 'term-process', 'os.kill(cursorRow.pid, signal.SIGTERM)', 'send SIGTERM to process')
|
191
|
+
ProcessesSheet.addCommand('gd', 'term-selected', 'for r in someSelectedRows: os.kill(r.pid, signal.SIGTERM)', 'send SIGTERM to selected processes')
|
192
|
+
ProcessesSheet.addCommand('zd', 'kill-process', 'os.kill(cursorRow.pid, signal.SIGKILL)', 'send SIGKILL to process')
|
193
|
+
ProcessesSheet.addCommand('gzd', 'kill-selected', 'for r in someSelectedRows: os.kill(r.pid, signal.SIGKILL)', 'send SIGKILL to selected processes')
|
194
|
+
|
195
|
+
ProcessesSheet.addCommand('^K', 'signal-process', 'os.kill(cursorRow.pid, chooseSignal())', 'send chosen signal to process')
|
196
|
+
UsefulProcessesSheet.addCommand('^K', 'signal-selected', 'os.kill(cursorRow.pid, chooseSignal())', 'kill(2) send chosen signal to process')
|
197
|
+
UsefulProcessesSheet.addCommand('', 'open-rlimits', 'vd.push(RlimitsSheet(cursorRow.name() + "_rlimits", cursorRow))', 'push rlimits for this process')
|
198
|
+
|
199
|
+
vd.addMenuItem('System', 'Signal', 'current process', 'TERMinate', 'term-process')
|
200
|
+
vd.addMenuItem('System', 'Signal', 'current process', 'KILL', 'kill-process')
|
201
|
+
vd.addMenuItem('System', 'Signal', 'current process', 'choose signal', 'signal-process')
|
202
|
+
vd.addMenuItem('System', 'Signal', 'selected processes', 'TERMinate', 'term-selected')
|
203
|
+
vd.addMenuItem('System', 'Signal', 'selected processes', 'KILL', 'kill-selected')
|
204
|
+
vd.addMenuItem('System', 'Signal', 'selected processes', 'choose signal', 'signal-selected')
|
205
|
+
vd.addMenuItem('System', 'Stats', 'CPU', 'open-cpustats')
|
206
|
+
vd.addMenuItem('System', 'Stats', 'memory', 'open-memstats')
|
207
|
+
vd.addMenuItem('System', 'Stats', 'processes', 'open-processes')
|
208
|
+
vd.addMenuItem('System', 'Stats', 'resource limits', 'open-rlimits')
|
@@ -0,0 +1,6 @@
|
|
1
|
+
from visidata import vd, Sheet
|
2
|
+
|
3
|
+
Sheet.addCommand(None, 'random-rows', 'nrows=int(input("random number to filter: ", value=nRows)); vs=copy(sheet); vs.name=name+"_sample"; vs.rows=sample(rows, nrows or nRows); vd.push(vs)', 'open duplicate sheet with a random population subset of N rows')
|
4
|
+
Sheet.addCommand(None, 'select-random', 'nrows=int(input("random number to select: ", value=nRows)); select(sample(rows, nrows or nRows))', 'select random sample of N rows')
|
5
|
+
|
6
|
+
vd.addMenuItems('Row > Select > random sample > select-random')
|
@@ -5,17 +5,11 @@ from visidata import asyncthread, options, vd
|
|
5
5
|
from visidata import VisiData, BaseSheet, Sheet, Column, Progress
|
6
6
|
|
7
7
|
|
8
|
-
@
|
9
|
-
def
|
10
|
-
|
11
|
-
vd.warning('no %s selected' % sheet.rowtype)
|
12
|
-
return
|
13
|
-
modified = 'column' if len(cols) == 1 else 'columns'
|
14
|
-
rex = vd.input("transform %s by regex: " % modified, type="regex-subst")
|
15
|
-
setValuesFromRegex(cols, rows, rex)
|
8
|
+
@VisiData.lazy_property
|
9
|
+
def help_regex(vd):
|
10
|
+
return vd.getHelpPane('regex', module='visidata')
|
16
11
|
|
17
12
|
|
18
|
-
vd.option('regex_flags', 'I', 'flags to pass to re.compile() [AILMSUX]', replay=True)
|
19
13
|
vd.option('regex_maxsplit', 0, 'maxsplit to pass to regex.split', replay=True)
|
20
14
|
|
21
15
|
@VisiData.api
|
@@ -24,6 +18,8 @@ def makeRegexSplitter(vd, regex, origcol):
|
|
24
18
|
|
25
19
|
@VisiData.api
|
26
20
|
def makeRegexMatcher(vd, regex, origcol):
|
21
|
+
if not regex.groups:
|
22
|
+
vd.fail('specify a capture group') #1778
|
27
23
|
def _regexMatcher(row):
|
28
24
|
m = regex.search(origcol.getDisplayValue(row))
|
29
25
|
if m:
|
@@ -52,12 +48,9 @@ def addRegexColumns(vs, regexMaker, origcol, regexstr):
|
|
52
48
|
cols = {}
|
53
49
|
ncols = 0 # number of new columns added already
|
54
50
|
for r in Progress(vs.getSampleRows()):
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
continue
|
59
|
-
except Exception as e:
|
60
|
-
vd.exceptionCaught(e)
|
51
|
+
m = vd.callNoExceptions(func, r)
|
52
|
+
if not m:
|
53
|
+
continue
|
61
54
|
|
62
55
|
if isinstance(m, dict):
|
63
56
|
for name in m:
|
@@ -74,12 +67,15 @@ def addRegexColumns(vs, regexMaker, origcol, regexstr):
|
|
74
67
|
else:
|
75
68
|
raise TypeError("addRegexColumns() expects a dict, list, or tuple from regexMaker, but got a "+type(m).__name__)
|
76
69
|
|
70
|
+
if not cols:
|
71
|
+
vd.warning("no regex matches found, didn't add column")
|
72
|
+
return
|
73
|
+
|
77
74
|
vs.addColumnAtCursor(*cols.values())
|
78
75
|
|
79
76
|
|
80
77
|
@VisiData.api
|
81
|
-
def regexTransform(vd, origcol,
|
82
|
-
before, after = vd.parse_sed_transform(instr)
|
78
|
+
def regexTransform(vd, origcol, before='', after=''):
|
83
79
|
return lambda col,row,origcol=origcol,before=before,after=after,flags=origcol.sheet.regex_flags(): re.sub(before, after, origcol.getDisplayValue(row), flags=flags)
|
84
80
|
|
85
81
|
|
@@ -104,27 +100,47 @@ def indexWithEscape(s, char, escape_char='\\'):
|
|
104
100
|
return None
|
105
101
|
|
106
102
|
|
103
|
+
@Sheet.api
|
107
104
|
@asyncthread
|
108
|
-
def setValuesFromRegex(cols, rows,
|
109
|
-
transforms = [vd.regexTransform(col,
|
105
|
+
def setValuesFromRegex(sheet, cols, rows, before='', after=''):
|
106
|
+
transforms = [vd.regexTransform(col, before=before, after=after) for col in cols]
|
110
107
|
vd.addUndoSetValues(cols, rows)
|
111
108
|
for r in Progress(rows, 'replacing'):
|
112
109
|
for col, transform in zip(cols, transforms):
|
113
|
-
col.
|
110
|
+
vd.callNoExceptions(col.setValue, r, transform(col, r))
|
111
|
+
|
114
112
|
for col in cols:
|
115
113
|
col.recalc()
|
116
114
|
|
117
115
|
|
118
|
-
@
|
119
|
-
def
|
120
|
-
|
121
|
-
|
116
|
+
@VisiData.api
|
117
|
+
def inputRegex(vd, prompt, type='regex', **kwargs):
|
118
|
+
return vd.input(prompt, type=type, help=vd.help_regex, **kwargs)
|
119
|
+
|
122
120
|
|
123
121
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
122
|
+
@VisiData.api
|
123
|
+
def inputRegexSubst(vd, prompt):
|
124
|
+
'Input regex transform via oneliner (separated with `/`). Return parsed transformer as dict(before=, after=).'
|
125
|
+
return vd.inputMultiple(before=dict(type='regex', prompt='search: ', help=prompt),
|
126
|
+
after=dict(type='regex-replace', prompt='replace: ', help=prompt))
|
127
|
+
|
128
|
+
|
129
|
+
Sheet.addCommand(':', 'addcol-split', 'addColumnAtCursor(RegexColumn(makeRegexSplitter, cursorCol, inputRegex("split regex: ", type="regex-split")))', 'Add column split by regex')
|
130
|
+
Sheet.addCommand(';', 'addcol-capture', 'addColumnAtCursor(RegexColumn(makeRegexMatcher, cursorCol, inputRegex("capture regex: ", type="regex-capture")))', 'Add column captured by regex')
|
131
|
+
|
132
|
+
Sheet.addCommand('*', 'addcol-regex-subst', 'addColumnAtCursor(Column(cursorCol.name + "_re", getter=regexTransform(cursorCol, **inputRegexSubst("regex transform column"))))', 'add column derived from current column, replacing regex with subst (may include \1 backrefs)')
|
133
|
+
Sheet.addCommand('g*', 'setcol-regex-subst', 'setValuesFromRegex([cursorCol], someSelectedRows, **inputRegexSubst("regex transform column"))', 'regex/subst - modify selected rows in current column, replacing regex with subst, (may include backreferences \\1 etc)')
|
134
|
+
Sheet.addCommand('gz*', 'setcol-regex-subst-all', 'setValuesFromRegex(visibleCols, someSelectedRows, **inputRegexSubst(f"regex transform {nVisibleCols} columns"))', 'modify selected rows in all visible columns, replacing regex with subst (may include \\1 backrefs)')
|
135
|
+
|
136
|
+
|
137
|
+
vd.addMenuItems('''
|
138
|
+
Edit > Modify > selected cells > regex substitution > setcol-regex-subst
|
139
|
+
Column > Add column > capture by regex > addcol-capture
|
140
|
+
Column > Add column > split by regex > addcol-split
|
141
|
+
Column > Add column > subst by regex > addcol-regex-subst
|
142
|
+
Row > Select > by regex > current column > select-col-regex
|
143
|
+
Row > Select > by regex > all columns > select-cols-regex
|
144
|
+
Row > Unselect > by regex > current column > unselect-col-regex
|
145
|
+
Row > Unselect > by regex > all columns > unselect-cols-regex
|
146
|
+
''')
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
|
4
|
+
from visidata import vd, BaseSheet, Sheet, asyncthread, Path, ScopedSetattr
|
5
|
+
|
6
|
+
|
7
|
+
@BaseSheet.api
|
8
|
+
@asyncthread
|
9
|
+
def reload_every(sheet, seconds:int):
|
10
|
+
while True:
|
11
|
+
sheet.reload()
|
12
|
+
time.sleep(seconds)
|
13
|
+
|
14
|
+
|
15
|
+
@BaseSheet.api
|
16
|
+
@asyncthread
|
17
|
+
def reload_modified(sheet):
|
18
|
+
'Spawn thread to call sheet.reload_rows when sheet.source mtime has changed.'
|
19
|
+
p = sheet.source
|
20
|
+
assert isinstance(p, Path)
|
21
|
+
assert not p.is_url()
|
22
|
+
|
23
|
+
mtime = os.stat(p).st_mtime
|
24
|
+
while True:
|
25
|
+
time.sleep(1)
|
26
|
+
t = os.stat(p).st_mtime
|
27
|
+
if t != mtime:
|
28
|
+
mtime = t
|
29
|
+
sheet.reload_rows()
|
30
|
+
|
31
|
+
|
32
|
+
@Sheet.api
|
33
|
+
@asyncthread
|
34
|
+
def reload_rows(self):
|
35
|
+
'Reload rows from ``self.source``, keeping current columns intact. Async.'
|
36
|
+
with (ScopedSetattr(self, 'loading', True),
|
37
|
+
ScopedSetattr(self, 'checkCursor', lambda: True),
|
38
|
+
ScopedSetattr(self, 'cursorRowIndex', self.cursorRowIndex)):
|
39
|
+
self.beforeLoad()
|
40
|
+
try:
|
41
|
+
self.loader()
|
42
|
+
vd.status("finished loading rows")
|
43
|
+
finally:
|
44
|
+
self.afterLoad()
|
45
|
+
|
46
|
+
|
47
|
+
BaseSheet.addCommand('', 'reload-every', 'sheet.reload_every(input("reload interval (sec): ", value=1))', 'schedule sheet reload every N seconds') #683
|
48
|
+
BaseSheet.addCommand('', 'reload-modified', 'sheet.reload_modified()', 'reload sheet when source file modified (tail-like behavior)') #1686
|
49
|
+
BaseSheet.addCommand('z^R', 'reload-rows', 'preloadHook(); reload_rows(); status("reloaded")', 'Reload current sheet')
|
50
|
+
|
51
|
+
vd.addMenuItems('''
|
52
|
+
File > Reload > rows only > reload-rows
|
53
|
+
File > Reload > every N seconds > reload-every
|
54
|
+
File > Reload > when source modified > reload-modified
|
55
|
+
''')
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import ast
|
2
|
+
|
3
|
+
from visidata import vd, Column, ExprColumn, Sheet
|
4
|
+
|
5
|
+
vd.option('rename_cascade', False, 'cascade column renames into expressions') #2088
|
6
|
+
|
7
|
+
|
8
|
+
class Renamer(ast.NodeTransformer):
|
9
|
+
def __init__(self, find, replace):
|
10
|
+
self.find = find
|
11
|
+
self.replace = replace
|
12
|
+
|
13
|
+
def visit_Name(self, node):
|
14
|
+
if node.id == self.find:
|
15
|
+
node.id = self.replace
|
16
|
+
|
17
|
+
return node
|
18
|
+
|
19
|
+
|
20
|
+
@Column.before
|
21
|
+
def setName(col, newname):
|
22
|
+
if col.sheet and col.sheet.options.rename_cascade:
|
23
|
+
for c in col.sheet.columns:
|
24
|
+
if isinstance(c, ExprColumn):
|
25
|
+
parsed_expr = ast.parse(c.expr)
|
26
|
+
canon_expr = ast.unparse(parsed_expr)
|
27
|
+
new_expr = ast.unparse(Renamer(col.name, newname).visit(parsed_expr))
|
28
|
+
if new_expr != canon_expr:
|
29
|
+
vd.addUndo(setattr, c, 'expr', c.expr)
|
30
|
+
c.expr = new_expr
|
@@ -0,0 +1,60 @@
|
|
1
|
+
'''
|
2
|
+
Enables two functions:
|
3
|
+
1. options.disp_scroll_context: the minimum number of lines to keep visible above and below the cursor when scrolling
|
4
|
+
2. toggle-scrollfix: Lock the cursor to a particular row number in the view
|
5
|
+
|
6
|
+
Allows the user to fix the position on the page where they would like the
|
7
|
+
cursor to "stick". Helps to provide context about surrounding rows when
|
8
|
+
near the top and bottom of the page.
|
9
|
+
|
10
|
+
Usage: scroll a few lines down, `toggle-scrollfix' and scroll again!
|
11
|
+
|
12
|
+
NOTE:
|
13
|
+
- disables scroll-middle command (zz)
|
14
|
+
'''
|
15
|
+
|
16
|
+
__author__ = 'Geekscrapy'
|
17
|
+
__version__ = '1.2'
|
18
|
+
|
19
|
+
from visidata import vd, Sheet
|
20
|
+
|
21
|
+
vd.option('disp_scroll_context', 0, 'minimum number of lines to keep visible above/below cursor when scrolling')
|
22
|
+
vd.optalias('disp_scrolloff', 'disp_scroll_context')
|
23
|
+
|
24
|
+
@Sheet.after
|
25
|
+
def checkCursor(sheet):
|
26
|
+
nctx = sheet.options.disp_scroll_context
|
27
|
+
if nctx:
|
28
|
+
if nctx >= int(sheet.nScreenRows/2):
|
29
|
+
if sheet.cursorRowIndex+1 > int(sheet.nScreenRows/2):
|
30
|
+
sheet.topRowIndex = sheet.cursorRowIndex - int(sheet.nScreenRows/2)
|
31
|
+
else:
|
32
|
+
if sheet.cursorRowIndex-sheet.topRowIndex < nctx:
|
33
|
+
sheet.topRowIndex = max(0, sheet.cursorRowIndex-nctx)
|
34
|
+
|
35
|
+
if sheet.bottomRowIndex-sheet.cursorRowIndex < nctx:
|
36
|
+
sheet.bottomRowIndex = sheet.cursorRowIndex+nctx
|
37
|
+
|
38
|
+
nfix = getattr(sheet, 'disp_scrollfix', 0)
|
39
|
+
if nfix > 0:
|
40
|
+
cursorViewIndex = sheet.cursorRowIndex - sheet.topRowIndex
|
41
|
+
if cursorViewIndex > nfix:
|
42
|
+
sheet.topRowIndex += abs(nfix - cursorViewIndex)
|
43
|
+
if cursorViewIndex < nfix and sheet.topRowIndex > 0:
|
44
|
+
sheet.topRowIndex -= abs(nfix - cursorViewIndex)
|
45
|
+
|
46
|
+
|
47
|
+
@Sheet.api
|
48
|
+
def toggle_scrollfix(sheet):
|
49
|
+
if getattr(sheet, 'disp_scrollfix', -1) >= 0:
|
50
|
+
sheet.disp_scrollfix = -1
|
51
|
+
vd.status("cursor unlocked")
|
52
|
+
else:
|
53
|
+
sheet.disp_scrollfix = sheet.cursorRowIndex - sheet.topRowIndex
|
54
|
+
vd.status(f"cursor locked to screen row {sheet.disp_scrollfix}")
|
55
|
+
|
56
|
+
|
57
|
+
Sheet.addCommand('', 'toggle-scrollfix', 'toggle_scrollfix()', helpstr='toggle cursor lock to current screen row')
|
58
|
+
|
59
|
+
|
60
|
+
vd.addMenuItems('''View > Toggle display > lock cursor to screen row > toggle-scrollfix''')
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from visidata import Sheet, asyncthread, Progress
|
2
|
+
|
3
|
+
|
4
|
+
@Sheet.api
|
5
|
+
@asyncthread
|
6
|
+
def select_equal_selected(sheet, col):
|
7
|
+
selectedVals = set(col.getDisplayValue(row) for row in Progress(sheet.selectedRows))
|
8
|
+
sheet.select(sheet.gatherBy(lambda r,c=col,vals=selectedVals: c.getDisplayValue(r) in vals), progress=False)
|
9
|
+
|
10
|
+
|
11
|
+
Sheet.addCommand('', 'select-equal-selected', 'select_equal_selected(cursorCol)', 'select rows with values in current column in already selected rows')
|