visidata 2.11.1__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 +259 -42
- visidata/_open.py +84 -29
- visidata/_types.py +21 -3
- 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
- {vgit → visidata/apps/vgit}/blame.py +5 -2
- {vgit → visidata/apps/vgit}/branch.py +31 -16
- {vgit → visidata/apps/vgit}/config.py +3 -3
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- {vgit → visidata/apps/vgit}/grep.py +6 -5
- visidata/apps/vgit/log.py +81 -0
- {vgit → visidata/apps/vgit}/main.py +18 -5
- {vgit → visidata/apps/vgit}/remote.py +8 -4
- visidata/apps/vgit/repos.py +71 -0
- {vgit → visidata/apps/vgit}/setup.py +6 -4
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- {vgit → visidata/apps/vgit}/statusbar.py +2 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +208 -93
- visidata/choose.py +6 -6
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +73 -17
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +88 -114
- visidata/color.py +142 -56
- visidata/column.py +121 -129
- 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 +30 -5
- 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} +3 -1
- 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} +32 -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 +2 -4
- 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/macros.py +129 -42
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -155
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +301 -148
- visidata/man/vd.txt +290 -153
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +78 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +113 -32
- visidata/pivot.py +73 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +37 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -25
- 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 +17 -5
- 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.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- vgit/__init__.py +0 -1
- vgit/gitsheet.py +0 -164
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.1.data/scripts/vgit +0 -9
- visidata-2.11.1.dist-info/RECORD +0 -155
- {vgit → visidata/apps/vgit}/__main__.py +0 -0
- {vgit → visidata/apps/vgit}/abort.py +0 -0
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,306 @@
|
|
1
|
+
'''# RedditSheet
|
2
|
+
|
3
|
+
- [:keys]Ctrl+O[/] to open a browser tab to [:code]{sheet.cursorRow.display_name_prefixed}[/]
|
4
|
+
- [:keys]g Ctrl+O[/] to open browser windows for {sheet.nSelectedRows} selected subreddits
|
5
|
+
|
6
|
+
- [:keys]Enter[/] to open sheet with top ~1000 submissions for [:code]{sheet.cursorRow.display_name_prefixed}[/]
|
7
|
+
- [:keys]g Enter[/] to open sheet with top ~1000 submissions for {sheet.nSelectedRows} selected subreddits
|
8
|
+
|
9
|
+
- [:keys]ga[/] to append more subreddits matching input by name or description
|
10
|
+
'''
|
11
|
+
|
12
|
+
import visidata
|
13
|
+
from visidata import vd, VisiData, Sheet, AttrColumn, asyncthread, ENTER, anytype, date
|
14
|
+
|
15
|
+
|
16
|
+
vd.option('reddit_client_id', '', 'client_id for reddit api')
|
17
|
+
vd.option('reddit_client_secret', '', 'client_secret for reddit api')
|
18
|
+
vd.option('reddit_user_agent', visidata.__version__, 'user_agent for reddit api')
|
19
|
+
|
20
|
+
|
21
|
+
@VisiData.api
|
22
|
+
def open_reddit(vd, p):
|
23
|
+
vd.importExternal('praw')
|
24
|
+
vd.enable_requests_cache()
|
25
|
+
|
26
|
+
if not vd.options.reddit_client_id:
|
27
|
+
return RedditGuide('reddit_guide')
|
28
|
+
|
29
|
+
if p.given.startswith('r/') or p.given.startswith('/r/'):
|
30
|
+
return SubredditSheet(p.base_stem, source=p.base_stem.split('+'), search=(p.given[0]=='/'))
|
31
|
+
|
32
|
+
if p.given.startswith('u/') or p.given.startswith('/u/'):
|
33
|
+
return RedditorsSheet(p.base_stem, source=p.base_stem.split('+'), search=(p.given[0]=='/'))
|
34
|
+
|
35
|
+
return SubredditSheet(p.base_stem, source=p)
|
36
|
+
|
37
|
+
vd.new_reddit = vd.open_reddit
|
38
|
+
|
39
|
+
@VisiData.cached_property
|
40
|
+
def reddit(vd):
|
41
|
+
import praw
|
42
|
+
return praw.Reddit(check_for_updates=False, **vd.options.getall('reddit_'))
|
43
|
+
|
44
|
+
|
45
|
+
subreddit_hidden_attrs='''
|
46
|
+
name #accounts_active accounts_active_is_fuzzed advertiser_category
|
47
|
+
all_original_content allow_chat_post_creation allow_discovery
|
48
|
+
allow_galleries allow_images allow_polls allow_predictions
|
49
|
+
allow_predictions_tournament allow_videogifs allow_videos
|
50
|
+
banner_background_color banner_background_image banner_img banner_size
|
51
|
+
can_assign_link_flair can_assign_user_flair collapse_deleted_comments
|
52
|
+
comment_score_hide_mins community_icon community_reviewed @created
|
53
|
+
@created_utc description_html disable_contributor_requests
|
54
|
+
display_name display_name_prefixed emoji emojis_custom_size
|
55
|
+
emojis_enabled filters free_form_reports fullname has_menu_widget
|
56
|
+
header_img header_size header_title hide_ads icon_img icon_size
|
57
|
+
is_chat_post_feature_enabled is_crosspostable_subreddit
|
58
|
+
is_enrolled_in_new_modmail key_color lang link_flair_enabled
|
59
|
+
link_flair_position mobile_banner_image mod notification_level
|
60
|
+
original_content_tag_enabled over18 prediction_leaderboard_entry_type
|
61
|
+
primary_color public_description public_description_html public_traffic
|
62
|
+
quaran quarantine restrict_commenting restrict_posting show_media
|
63
|
+
show_media_preview spoilers_enabled submission_type submit_link_label
|
64
|
+
submit_text submit_text_html submit_text_label suggested_comment_sort
|
65
|
+
user_can_flair_in_sr user_flair_background_color user_flair_css_class
|
66
|
+
user_flair_enabled_in_sr user_flair_position user_flair_richtext
|
67
|
+
user_flair_template_id user_flair_text user_flair_text_color
|
68
|
+
user_flair_type user_has_favorited user_is_banned user_is_contributor
|
69
|
+
user_is_moderator user_is_muted user_is_subscriber user_sr_flair_enabled
|
70
|
+
user_sr_theme_enabled #videostream_links_count whitelist_status widgets
|
71
|
+
wiki wiki_enabled wls
|
72
|
+
'''
|
73
|
+
|
74
|
+
post_hidden_attrs='''
|
75
|
+
all_awardings allow_live_comments @approved_at_utc approved_by archived
|
76
|
+
author_flair_background_color author_flair_css_class author_flair_richtext
|
77
|
+
author_flair_template_id author_flair_text author_flair_text_color
|
78
|
+
author_flair_type author_fullname author_patreon_flair author_premium
|
79
|
+
awarders @banned_at_utc banned_by can_gild can_mod_post category clicked
|
80
|
+
comment_limit comment_sort content_categories contest_mode @created_utc
|
81
|
+
discussion_type distinguished domain edited flair fullname gilded
|
82
|
+
gildings hidden hide_score is_crosspostable is_meta is_original_content
|
83
|
+
is_reddit_media_domain is_robot_indexable is_self is_video likes
|
84
|
+
link_flair_background_color link_flair_css_class link_flair_richtext
|
85
|
+
link_flair_text link_flair_text_color link_flair_type locked media
|
86
|
+
media_embed media_only mod mod_note mod_reason_by mod_reason_title
|
87
|
+
mod_reports name no_follow num_crossposts num_duplicates num_reports
|
88
|
+
over_18 parent_whitelist_status permalink pinned pwls quarantine
|
89
|
+
removal_reason removed_by removed_by_category report_reasons saved
|
90
|
+
score secure_media secure_media_embed selftext_html send_replies
|
91
|
+
shortlink spoiler stickied subreddit_id subreddit_name_prefixed
|
92
|
+
subreddit_subscribers subreddit_type suggested_sort thumbnail
|
93
|
+
thumbnail_height thumbnail_width top_awarded_type total_awards_received
|
94
|
+
treatment_tags upvote_ratio user_reports #view_count visited
|
95
|
+
whitelist_status wls
|
96
|
+
'''
|
97
|
+
|
98
|
+
comment_hidden_attrs='''
|
99
|
+
all_awardings @approved_at_utc approved_by archived associated_award
|
100
|
+
author_flair_background_color author_flair_css_class author_flair_richtext
|
101
|
+
author_flair_template_id author_flair_text author_flair_text_color
|
102
|
+
author_flair_type author_fullname author_patreon_flair author_premium
|
103
|
+
awarders @banned_at_utc banned_by body_html can_gild can_mod_post
|
104
|
+
collapsed collapsed_because_crowd_control collapsed_reason comment_type
|
105
|
+
controversiality @created_utc distinguished fullname gilded gildings
|
106
|
+
is_root is_submitter likes link_id locked mod mod_note mod_reason_by
|
107
|
+
mod_reason_title mod_reports name no_follow num_reports parent_id
|
108
|
+
permalink removal_reason report_reasons saved #score #score_hidden
|
109
|
+
send_replies stickied submission subreddit_id subreddit_name_prefixed
|
110
|
+
subreddit_type top_awarded_type total_awards_received treatment_tags
|
111
|
+
user_reports
|
112
|
+
'''
|
113
|
+
|
114
|
+
redditor_hidden_attrs='''
|
115
|
+
#awardee_karma #awarder_karma @created @created_utc
|
116
|
+
fullname has_subscribed has_verified_email hide_from_robots icon_img id
|
117
|
+
is_employee is_friend is_gold is_mod pref_show_snoovatar
|
118
|
+
snoovatar_img snoovatar_size stream #total_karma verified
|
119
|
+
subreddit.banner_img subreddit.name subreddit.over_18 subreddit.public_description #subreddit.subscribers subreddit.title
|
120
|
+
'''
|
121
|
+
|
122
|
+
def hiddenCols(hidden_attrs):
|
123
|
+
coltypes = { t.icon:t.typetype for t in vd.typemap.values() if not t.icon.isalpha() }
|
124
|
+
for attr in hidden_attrs.split():
|
125
|
+
coltype = anytype
|
126
|
+
if attr[0] in coltypes:
|
127
|
+
coltype = coltypes.get(attr[0])
|
128
|
+
attr = attr[1:]
|
129
|
+
yield AttrColumn(attr, type=coltype, width=0)
|
130
|
+
|
131
|
+
|
132
|
+
class SubredditSheet(Sheet):
|
133
|
+
guide = __doc__
|
134
|
+
# source is a text list of subreddits
|
135
|
+
rowtype = 'subreddits' # rowdef: praw.Subreddit
|
136
|
+
nKeys=1
|
137
|
+
search=False
|
138
|
+
columns = [
|
139
|
+
AttrColumn('display_name_prefixed', width=15),
|
140
|
+
AttrColumn('active_user_count', type=int),
|
141
|
+
AttrColumn('subscribers', type=int),
|
142
|
+
AttrColumn('subreddit_type'),
|
143
|
+
AttrColumn('title'),
|
144
|
+
AttrColumn('description', width=50),
|
145
|
+
AttrColumn('url', width=10),
|
146
|
+
] + list(hiddenCols(subreddit_hidden_attrs))
|
147
|
+
|
148
|
+
def iterload(self):
|
149
|
+
for name in self.source:
|
150
|
+
name = name.strip()
|
151
|
+
if self.search:
|
152
|
+
yield from vd.reddit.subreddits.search(name)
|
153
|
+
else:
|
154
|
+
try:
|
155
|
+
r = vd.reddit.subreddit(name)
|
156
|
+
r.display_name_prefixed
|
157
|
+
yield r
|
158
|
+
except Exception as e:
|
159
|
+
vd.exceptionCaught(e)
|
160
|
+
|
161
|
+
def openRow(self, row):
|
162
|
+
return RedditSubmissions(row.display_name_prefixed, source=row)
|
163
|
+
|
164
|
+
def openRows(self, rows):
|
165
|
+
comboname = '+'.join(row.display_name for row in rows)
|
166
|
+
return RedditSubmissions(comboname, source=vd.reddit.subreddit(comboname))
|
167
|
+
|
168
|
+
|
169
|
+
class RedditorsSheet(Sheet):
|
170
|
+
# source is a text list of usernames
|
171
|
+
rowtype = 'redditors' # rowdef: praw.Subreddit
|
172
|
+
nKeys=1
|
173
|
+
columns = [
|
174
|
+
AttrColumn('name', width=15),
|
175
|
+
AttrColumn('comment_karma', type=int),
|
176
|
+
AttrColumn('link_karma', type=int),
|
177
|
+
AttrColumn('comments'),
|
178
|
+
AttrColumn('submissions'),
|
179
|
+
] + list(hiddenCols(redditor_hidden_attrs))
|
180
|
+
|
181
|
+
def iterload(self):
|
182
|
+
for name in self.source:
|
183
|
+
if self.search:
|
184
|
+
yield from vd.reddit.redditors.popular(name)
|
185
|
+
else:
|
186
|
+
yield vd.reddit.redditor(name)
|
187
|
+
|
188
|
+
def openRow(self, row):
|
189
|
+
return RedditSubmissions(row.fullname, source=row.submissions)
|
190
|
+
|
191
|
+
def openRows(self, rows):
|
192
|
+
comboname = '+'.join(row.name for row in rows)
|
193
|
+
return RedditSubmissions(comboname, source=vd.reddit.redditor(comboname).submissions)
|
194
|
+
|
195
|
+
|
196
|
+
class RedditSubmissions(Sheet):
|
197
|
+
guide = '''# Reddit Submissions
|
198
|
+
|
199
|
+
[:keys]Enter[/] to open sheet with comments for the current post
|
200
|
+
[:keys]ga[/] to add posts in this subreddit matching input'''
|
201
|
+
|
202
|
+
# source=ListingGenerator
|
203
|
+
rowtype='reddit posts' # rowdef: praw.Submission
|
204
|
+
nKeys=2
|
205
|
+
columns = [
|
206
|
+
AttrColumn('subreddit'),
|
207
|
+
AttrColumn('id', width=0),
|
208
|
+
AttrColumn('created', width=12, type=date),
|
209
|
+
AttrColumn('author'),
|
210
|
+
AttrColumn('ups', width=8, type=int),
|
211
|
+
AttrColumn('downs', width=8, type=int),
|
212
|
+
AttrColumn('num_comments', width=8, type=int),
|
213
|
+
AttrColumn('title', width=50),
|
214
|
+
AttrColumn('selftext', width=60),
|
215
|
+
AttrColumn('url'),
|
216
|
+
AttrColumn('comments', width=0),
|
217
|
+
] + list(hiddenCols(post_hidden_attrs))
|
218
|
+
|
219
|
+
def iterload(self):
|
220
|
+
kind = 'new' # 'top'
|
221
|
+
f = getattr(self.source, kind, None)
|
222
|
+
if f:
|
223
|
+
yield from f(limit=10000)
|
224
|
+
|
225
|
+
def openRow(self, row):
|
226
|
+
return RedditComments(row.id, source=row.comments.list())
|
227
|
+
|
228
|
+
|
229
|
+
class RedditComments(Sheet):
|
230
|
+
# source=list of comments
|
231
|
+
rowtype='comments' # rowdef: praw.Comment
|
232
|
+
nKeys=2
|
233
|
+
columns=[
|
234
|
+
AttrColumn('subreddit', width=0),
|
235
|
+
AttrColumn('id', width=0),
|
236
|
+
AttrColumn('ups', width=4, type=int),
|
237
|
+
AttrColumn('downs', width=4, type=int),
|
238
|
+
AttrColumn('replies', type=list),
|
239
|
+
AttrColumn('created', type=date),
|
240
|
+
AttrColumn('author'),
|
241
|
+
AttrColumn('depth', type=int),
|
242
|
+
AttrColumn('body', width=60),
|
243
|
+
AttrColumn('edited', width=0),
|
244
|
+
] + list(hiddenCols(comment_hidden_attrs))
|
245
|
+
|
246
|
+
def iterload(self):
|
247
|
+
yield from self.source
|
248
|
+
|
249
|
+
def openRow(self, row):
|
250
|
+
return RedditComments(row.id, source=row.replies)
|
251
|
+
|
252
|
+
|
253
|
+
class RedditGuide(RedditSubmissions):
|
254
|
+
guide = '''# Authenticate Reddit
|
255
|
+
The Reddit API must be configured before use.
|
256
|
+
|
257
|
+
1. Login to Reddit and go to [:underline]https://www.reddit.com/prefs/apps[/].
|
258
|
+
2. Create a "script" app. (Use "[:underline]http://localhost:8000[/]" for the redirect uri)
|
259
|
+
3. Add credentials to visidatarc:
|
260
|
+
|
261
|
+
options.reddit_client_id = '...' # below the description in the upper left
|
262
|
+
options.reddit_client_secret = '...'
|
263
|
+
|
264
|
+
## Use [:code]reddit[/] filetype for subreddits or users
|
265
|
+
|
266
|
+
Multiple may be specified, joined with "+".
|
267
|
+
|
268
|
+
vd r/commandline.reddit
|
269
|
+
vd u/gallowboob.reddit
|
270
|
+
vd r/rust+golang+python.reddit
|
271
|
+
vd u/spez+kn0thing.reddit
|
272
|
+
'''
|
273
|
+
|
274
|
+
@SubredditSheet.api
|
275
|
+
@asyncthread
|
276
|
+
def addRowsFromQuery(sheet, q):
|
277
|
+
for r in vd.reddit.subreddits.search(q):
|
278
|
+
sheet.addRow(r, index=sheet.cursorRowIndex+1)
|
279
|
+
|
280
|
+
|
281
|
+
@RedditSubmissions.api
|
282
|
+
@asyncthread
|
283
|
+
def addRowsFromQuery(sheet, q):
|
284
|
+
for r in sheet.source.search(q, limit=None):
|
285
|
+
sheet.addRow(r, index=sheet.cursorRowIndex+1)
|
286
|
+
|
287
|
+
|
288
|
+
@VisiData.api
|
289
|
+
def sysopen_subreddits(vd, *subreddits):
|
290
|
+
url = "https://www.reddit.com/r/"+"+".join(subreddits)
|
291
|
+
vd.launchBrowser(url)
|
292
|
+
|
293
|
+
|
294
|
+
SubredditSheet.addCommand('^O', 'sysopen-subreddit', 'sysopen_subreddits(cursorRow.display_name)', 'open browser window with subreddit')
|
295
|
+
SubredditSheet.addCommand('g^O', 'sysopen-subreddits', 'sysopen_subreddits(*(row.display_name for row in selectedRows))', 'open browser window with messages from selected subreddits')
|
296
|
+
SubredditSheet.addCommand('g'+ENTER, 'open-subreddits', 'vd.push(openRows(selectedRows))', 'open sheet with top ~1000 submissions for each selected subreddit')
|
297
|
+
SubredditSheet.addCommand('ga', 'add-subreddits-match', 'addRowsFromQuery(input("add subreddits matching: "))', 'add subreddits matching input by name or description')
|
298
|
+
RedditSubmissions.addCommand('ga', 'add-submissions-match', 'addRowsFromQuery(input("add posts matching: "))', 'add posts in this subreddit matching input')
|
299
|
+
|
300
|
+
vd.addMenuItems('''
|
301
|
+
File > Reddit > open selected subreddits > open-subreddits
|
302
|
+
File > Reddit > add > matching subreddits > add-subreddits-match
|
303
|
+
File > Reddit > add > matching submissions > add-submissions-match
|
304
|
+
File > Reddit > open in browser > subreddit in current row > sysopen-subreddit
|
305
|
+
File > Reddit > open in browser > selected subreddits > sysopen-subreddits
|
306
|
+
''')
|
@@ -0,0 +1,249 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from visidata import vd, VisiData, BaseSheet, Sheet, TextSheet, PyobjSheet
|
4
|
+
from visidata import ItemColumn, Column, vlen, date, asyncsingle, ENTER, AttrDict
|
5
|
+
|
6
|
+
vd.option('zulip_batch_size', -100, 'number of messages to fetch per call (<0 to fetch before anchor)')
|
7
|
+
vd.option('zulip_anchor', 1000000000, 'message id to start fetching from')
|
8
|
+
vd.option('zulip_delay_s', 0.00001, 'seconds to wait between calls (0 to stop after first)')
|
9
|
+
vd.option('zulip_api_key', '', 'Zulip API key')
|
10
|
+
vd.option('zulip_email', '', 'Email for use with Zulip API key')
|
11
|
+
|
12
|
+
|
13
|
+
@VisiData.api
|
14
|
+
def open_zulip(vd, p):
|
15
|
+
vd.importExternal('zulip')
|
16
|
+
import zulip
|
17
|
+
|
18
|
+
if not vd.options.zulip_api_key:
|
19
|
+
vd.warning('zulip_api_key must be set first')
|
20
|
+
vd.status('Enter your login email and Zulip API key (see _https://zulip.com/api/api-keys_).')
|
21
|
+
email = vd.input(f'Login email for {p.given}: ', record=False)
|
22
|
+
api_key = vd.input(f'Zulip API key: ', record=False)
|
23
|
+
|
24
|
+
vd.setPersistentOptions(zulip_email=email, zulip_api_key=api_key)
|
25
|
+
|
26
|
+
vd.z_client = zulip.Client(site=p.given, api_key=vd.options.zulip_api_key, email=vd.options.zulip_email)
|
27
|
+
|
28
|
+
return vd.subscribedStreams
|
29
|
+
|
30
|
+
|
31
|
+
VisiData.openhttp_zulip = VisiData.open_zulip
|
32
|
+
|
33
|
+
|
34
|
+
@VisiData.api
|
35
|
+
def z_rpc(vd, r, result_field_name=None):
|
36
|
+
if r['result'] != 'success':
|
37
|
+
return PyobjSheet(result_field_name+'_error', source=r)
|
38
|
+
elif result_field_name:
|
39
|
+
return PyobjSheet(result_field_name, source=r[result_field_name])
|
40
|
+
|
41
|
+
|
42
|
+
@VisiData.lazy_property
|
43
|
+
def allStreams(vd):
|
44
|
+
return ZulipStreamsSheet("all_streams", zulip_func='get_streams', zulip_result_key="streams", zulip_kwargs=dict(include_public=True, include_subscribed=True))
|
45
|
+
|
46
|
+
|
47
|
+
@VisiData.lazy_property
|
48
|
+
def subscribedStreams(vd):
|
49
|
+
return ZulipStreamsSheet("subscriptions", zulip_func='get_subscriptions', zulip_result_key="subscriptions")
|
50
|
+
|
51
|
+
|
52
|
+
@VisiData.lazy_property
|
53
|
+
def allMessages(vd):
|
54
|
+
return ZulipMessagesSheet("all_messages")
|
55
|
+
|
56
|
+
|
57
|
+
@VisiData.api
|
58
|
+
def parseColumns(vd, fieldlist):
|
59
|
+
for cname in fieldlist:
|
60
|
+
kwargs = {}
|
61
|
+
while not cname[0].isalpha():
|
62
|
+
if cname[0] == '#': kwargs['type'] = int
|
63
|
+
elif cname[0] == '@': kwargs['type'] = date
|
64
|
+
elif cname[0] == '-': kwargs['width'] = 0
|
65
|
+
else: break
|
66
|
+
cname = cname[1:]
|
67
|
+
yield ItemColumn(cname, **kwargs)
|
68
|
+
|
69
|
+
|
70
|
+
class ZulipAPISheet(Sheet):
|
71
|
+
zulip_func = None
|
72
|
+
zulip_result_key = ''
|
73
|
+
zulip_args = []
|
74
|
+
zulip_kwargs = {}
|
75
|
+
fields = ''
|
76
|
+
|
77
|
+
def iterload(self):
|
78
|
+
self.columns = []
|
79
|
+
for c in vd.parseColumns(self.fields.split()):
|
80
|
+
self.addColumn(c)
|
81
|
+
|
82
|
+
zulip_func = self.zulip_func
|
83
|
+
if isinstance(zulip_func, str): # allow later binding for startup perf
|
84
|
+
zulip_func = getattr(vd.z_client, zulip_func)
|
85
|
+
r = zulip_func(*self.zulip_args, **self.zulip_kwargs)
|
86
|
+
if r['result'] != 'success':
|
87
|
+
vd.push(PyobjSheet(self.zulip_result_key+'_error', source=r))
|
88
|
+
return
|
89
|
+
yield from r[self.zulip_result_key]
|
90
|
+
|
91
|
+
def addRow(self, r, **kwargs):
|
92
|
+
return super().addRow(AttrDict(r), **kwargs)
|
93
|
+
|
94
|
+
|
95
|
+
class ZulipStreamsSheet(ZulipAPISheet):
|
96
|
+
guide = '''# Zulip Streams
|
97
|
+
|
98
|
+
- `Enter` to open recent messages from the stream
|
99
|
+
- `z Enter` to open list of topics from the stream
|
100
|
+
'''
|
101
|
+
rowtype = 'streams' # rowdef: dict of stream from server
|
102
|
+
fields = '-#stream_id name @date_created description -rendered_description -invite_only -is_web_public -stream_post_policy -history_public_to_subscribers -#first_message_id -#message_retention_days -is_announcement_only'
|
103
|
+
|
104
|
+
def openRow(self, r):
|
105
|
+
return ZulipMessagesSheet(r.name, filters=dict(stream=r.name))
|
106
|
+
|
107
|
+
def openCell(self, c, r):
|
108
|
+
return ZulipTopicsSheet(r.name+'_topics',
|
109
|
+
zulip_func=vd.z_client.get_stream_topics,
|
110
|
+
zulip_args=[r.stream_id],
|
111
|
+
zulip_result_key='topics')
|
112
|
+
|
113
|
+
|
114
|
+
class ZulipTopicsSheet(ZulipAPISheet):
|
115
|
+
rowtype = 'topics' # rowdef: dict of topic from server
|
116
|
+
fields='name #max_id'
|
117
|
+
def openRow(self, r):
|
118
|
+
return ZulipMessagesSheet(f'{r.name}:{r.subject}', filters=dict(stream=r.name, topic=r.subject))
|
119
|
+
|
120
|
+
|
121
|
+
class ZulipMembersSheet(ZulipAPISheet):
|
122
|
+
guide = '''# Zulip Members
|
123
|
+
- `Enter` to open list of messages from this member
|
124
|
+
'''
|
125
|
+
rowtype = 'members' # rowdef: dict of member from server
|
126
|
+
fields = '''-#user_id full_name email timezone @date_joined -#avatar_version -is_admin -is_owner -is_guest -is_bot -#role -is_active -avatar_url -bot_type -#bot_owner_id'''
|
127
|
+
def openRow(self, r):
|
128
|
+
return ZulipMessagesSheet(r.display_recipient, filters=dict(stream=r.display_recipient))
|
129
|
+
|
130
|
+
|
131
|
+
class ZulipMessagesSheet(Sheet):
|
132
|
+
guide = '''# Zulip Messages Sheet
|
133
|
+
Loads continuously starting with most recent, until all messages have been read.
|
134
|
+
|
135
|
+
- `Ctrl+C` to cancel loading.
|
136
|
+
- `Enter` to open message in word-wrapped text sheet
|
137
|
+
'''
|
138
|
+
rowtype = 'messages' # rowdef: dict of message from server
|
139
|
+
# fields = ''
|
140
|
+
columns = [
|
141
|
+
ItemColumn('timestamp', type=date, fmtstr='%Y-%m-%d %H:%M'),
|
142
|
+
ItemColumn('sender', 'sender_full_name'),
|
143
|
+
ItemColumn('sender_email', width=0),
|
144
|
+
ItemColumn('recipient', 'display_recipient'),
|
145
|
+
ItemColumn('subject'),
|
146
|
+
ItemColumn('content'),
|
147
|
+
ItemColumn('client', width=0),
|
148
|
+
ItemColumn('reactions', type=vlen),
|
149
|
+
ItemColumn('submessages', type=vlen),
|
150
|
+
ItemColumn('flags', width=0),
|
151
|
+
]
|
152
|
+
filters={}
|
153
|
+
|
154
|
+
@asyncsingle # kill previous thread
|
155
|
+
def reload(self):
|
156
|
+
self.rows = []
|
157
|
+
narrow = list(self.filters.items())
|
158
|
+
n = self.options.zulip_batch_size
|
159
|
+
req = AttrDict(
|
160
|
+
num_before = -n if n < 0 else 0,
|
161
|
+
num_after = n if n > 0 else 0,
|
162
|
+
anchor = self.options.zulip_anchor,
|
163
|
+
apply_markdown = False,
|
164
|
+
narrow = narrow)
|
165
|
+
|
166
|
+
while True:
|
167
|
+
r = vd.z_client.call_endpoint(url='messages', method='GET', request=req)
|
168
|
+
if r['result'] == 'success':
|
169
|
+
if not r['messages']: break
|
170
|
+
for i, msg in enumerate(r['messages']):
|
171
|
+
self.addRow(msg, index=i)
|
172
|
+
req['anchor'] = min(msg['id'] for msg in r['messages'])-1
|
173
|
+
s = self.options.zulip_delay_s
|
174
|
+
if s <= 0:
|
175
|
+
break
|
176
|
+
time.sleep(s)
|
177
|
+
|
178
|
+
# vd.status('finished loading, waiting for new messages')
|
179
|
+
# vd.z_client.call_on_each_event(self.received_event, ['message'], narrow=narrow)
|
180
|
+
|
181
|
+
def get_channel_name(self, r):
|
182
|
+
recp = r['display_recipient']
|
183
|
+
if isinstance(recp, list): # private message
|
184
|
+
return '[%s]' % recp[0]['full_name']
|
185
|
+
else:
|
186
|
+
return '%s:%s' % (recp, r['subject'])
|
187
|
+
|
188
|
+
def update_message(self, msgid, content):
|
189
|
+
req = {
|
190
|
+
"message_id": msgid,
|
191
|
+
"content": content
|
192
|
+
}
|
193
|
+
vd.z_rpc(vd.z_client.update_message(req))
|
194
|
+
|
195
|
+
def openRow(self, r):
|
196
|
+
vs = TextSheet(self.get_channel_name(r), source=[r["content"]])
|
197
|
+
vs.options.wrap = True
|
198
|
+
return vs
|
199
|
+
|
200
|
+
def received_event(self, event):
|
201
|
+
if event['type'] == 'message':
|
202
|
+
self.addRow(event['message'])
|
203
|
+
|
204
|
+
def reply_message(self, msg, row):
|
205
|
+
recp = row['display_recipient']
|
206
|
+
if isinstance(recp, list):
|
207
|
+
for dest in recp:
|
208
|
+
self.send_message(msg, row['subject'], dest['email'], 'private')
|
209
|
+
else:
|
210
|
+
self.send_message(msg, row['subject'], dest, 'stream')
|
211
|
+
|
212
|
+
def send_message(self, msg, subject, dest, msgtype='stream'):
|
213
|
+
req = {
|
214
|
+
'type': msgtype,
|
215
|
+
'content': msg,
|
216
|
+
'subject': subject,
|
217
|
+
'to': dest,
|
218
|
+
}
|
219
|
+
r = vd.z_client.send_message(req)
|
220
|
+
|
221
|
+
if r['result'] != 'success':
|
222
|
+
vd.push(PyobjSheet('send_message_result', source=r))
|
223
|
+
|
224
|
+
|
225
|
+
vd.addGlobals({
|
226
|
+
'ZulipMembersSheet': ZulipMembersSheet,
|
227
|
+
'ZulipStreamsSheet': ZulipStreamsSheet,
|
228
|
+
'ZulipAPISheet': ZulipAPISheet,
|
229
|
+
'ZulipMessagesSheet': ZulipMessagesSheet,
|
230
|
+
})
|
231
|
+
|
232
|
+
ZulipAPISheet.addCommand('', 'open-zulip-profile', 'vd.push(PyobjSheet("profile", source=z_client.get_profile()))', 'open connected user\'s profile')
|
233
|
+
ZulipAPISheet.addCommand('', 'open-zulip-members', 'vd.push(ZulipMembersSheet("members", zulip_func=z_client.get_users, zulip_result_key="members"))', 'open list of all members')
|
234
|
+
ZulipAPISheet.addCommand('', 'open-zulip-streams', 'vd.push(vd.allStreams)', 'open list of all streams')
|
235
|
+
ZulipAPISheet.addCommand('', 'open-zulip-subs', 'vd.push(vd.subscribedStreams)', 'open list of subscribed streams')
|
236
|
+
ZulipAPISheet.addCommand('', 'open-zulip-msgs', 'vd.push(vd.allMessages)', 'open list of all messages')
|
237
|
+
|
238
|
+
ZulipMessagesSheet.addCommand('', 'reply-zulip-msg', 'reply_message(input(cursorRow["display_recipient"][1]["short_name"]+"> ", "message"), cursorRow)', 'reply to current topic')
|
239
|
+
ZulipMessagesSheet.addCommand('', 'edit-zulip-msg', 'update_message(cursorRow["id"], editCell(3, cursorRowIndex))', 'edit message content')
|
240
|
+
|
241
|
+
vd.addMenuItems('''
|
242
|
+
File > Zulip > profile > open-zulip-profile
|
243
|
+
File > Zulip > member list > open-zulip-members
|
244
|
+
File > Zulip > streams > open-zulip-streams
|
245
|
+
File > Zulip > subscriptions > open-zulip-subs
|
246
|
+
File > Zulip > messages > open-zulip-subs
|
247
|
+
File > Zulip > reply > reply-zulip-msg
|
248
|
+
File > Zulip > edit message > edit-zulip-msg
|
249
|
+
''')
|
visidata/loaders/archive.py
CHANGED
@@ -8,13 +8,23 @@ from visidata import vd, VisiData, asyncthread, Sheet, Progress, Menu, options
|
|
8
8
|
from visidata import ColumnAttr, Column, Path
|
9
9
|
from visidata.type_date import date
|
10
10
|
|
11
|
+
@VisiData.api
|
12
|
+
def guess_zip(vd, p):
|
13
|
+
if not p.is_url() and zipfile.is_zipfile(p.open_bytes()):
|
14
|
+
return dict(filetype='zip')
|
15
|
+
|
16
|
+
@VisiData.api
|
17
|
+
def guess_tar(vd, p):
|
18
|
+
if tarfile.is_tarfile(p.fp):
|
19
|
+
return dict(filetype='tar')
|
20
|
+
|
11
21
|
@VisiData.api
|
12
22
|
def open_zip(vd, p):
|
13
|
-
return vd.ZipSheet(p.
|
23
|
+
return vd.ZipSheet(p.base_stem, source=p)
|
14
24
|
|
15
25
|
@VisiData.api
|
16
26
|
def open_tar(vd, p):
|
17
|
-
return TarSheet(p.
|
27
|
+
return TarSheet(p.base_stem, source=p)
|
18
28
|
|
19
29
|
VisiData.open_tgz = VisiData.open_tar
|
20
30
|
VisiData.open_txz = VisiData.open_tar
|
@@ -37,16 +47,33 @@ class ZipSheet(Sheet):
|
|
37
47
|
getter=lambda col, row: datetime.datetime(*row[0].date_time)),
|
38
48
|
]
|
39
49
|
nKeys = 2
|
50
|
+
guide = '''# Zip Sheet
|
51
|
+
This is a list of files contained in the zipfile {sheet.displaySource}.
|
52
|
+
|
53
|
+
Once extracted, files can be loaded with `ENTER`.
|
54
|
+
|
55
|
+
Commands:
|
56
|
+
|
57
|
+
- `x` to extract current file to current directory
|
58
|
+
- `gx` to extract selected files to current directory
|
59
|
+
- `zx` to extract current file to a given pathname
|
60
|
+
- `gzx` to extract selected files to given directory
|
61
|
+
|
62
|
+
'''
|
40
63
|
|
41
64
|
def openZipFile(self, fp, *args, **kwargs):
|
42
65
|
'''Use VisiData input to handle password-protected zip files.'''
|
66
|
+
if isinstance(fp, zipfile.ZipFile):
|
67
|
+
zip_open = fp.open
|
68
|
+
elif isinstance(fp, unzip_http.RemoteZipFile):
|
69
|
+
zip_open = fp._open
|
43
70
|
try:
|
44
|
-
return
|
71
|
+
return zip_open(*args, **kwargs)
|
45
72
|
except RuntimeError as err:
|
46
73
|
if 'password required' in err.args[0]:
|
47
74
|
pwd = vd.input(f'{args[0].filename} is encrypted, enter password: ', display=False)
|
48
|
-
return
|
49
|
-
vd.
|
75
|
+
return zip_open(*args, **kwargs, pwd=pwd.encode('utf-8'))
|
76
|
+
vd.exceptionCaught(err)
|
50
77
|
|
51
78
|
def openRow(self, row):
|
52
79
|
fi, zpath = row
|
@@ -59,10 +86,16 @@ class ZipSheet(Sheet):
|
|
59
86
|
files = []
|
60
87
|
for row in rows:
|
61
88
|
r, _ = row
|
62
|
-
|
63
|
-
vd.confirm(f'{r.filename} exists, overwrite? ') #1452
|
89
|
+
vd.confirmOverwrite(path/r.filename) #1452
|
64
90
|
self.extract_async(row)
|
65
91
|
|
92
|
+
def sysopen_row(self, row):
|
93
|
+
'Extract file in row to tempdir and launch $EDITOR. Modifications will be discarded.'
|
94
|
+
import tempfile
|
95
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
96
|
+
self.zfp.extract(member=row[0], path=tempdir)
|
97
|
+
vd.launchExternalEditorPath(Path(tempdir)/row[0].filename)
|
98
|
+
|
66
99
|
@asyncthread
|
67
100
|
def extract_async(self, *rows, path=None):
|
68
101
|
'Extract rows to *path*, without confirmation.'
|
@@ -115,6 +148,7 @@ ZipSheet.addCommand('x', 'extract-file', 'extract(cursorRow)', 'extract current
|
|
115
148
|
ZipSheet.addCommand('gx', 'extract-selected', 'extract(*onlySelectedRows)', 'extract selected files to current directory')
|
116
149
|
ZipSheet.addCommand('zx', 'extract-file-to', 'extract(cursorRow, path=inputPath("extract to: "))', 'extract current file to given pathname')
|
117
150
|
ZipSheet.addCommand('gzx', 'extract-selected-to', 'extract(*onlySelectedRows, path=inputPath("extract %d files to: " % nSelectedRows))', 'extract selected files to given directory')
|
151
|
+
ZipSheet.addCommand('Ctrl+O', 'sysopen-row', 'sysopen_row(cursorRow)', 'open $EDITOR with current file (modifications will be discarded)')
|
118
152
|
|
119
153
|
vd.addMenu(Menu('File', Menu('Extract',
|
120
154
|
Menu('current file', 'extract-file'),
|