offpunk 2.1__tar.gz → 2.2__tar.gz
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.
- {offpunk-2.1 → offpunk-2.2}/CHANGELOG +18 -0
- {offpunk-2.1 → offpunk-2.2}/PKG-INFO +1 -1
- {offpunk-2.1 → offpunk-2.2}/ansicat.py +95 -25
- {offpunk-2.1 → offpunk-2.2}/man/ansicat.1 +2 -0
- {offpunk-2.1 → offpunk-2.2}/man/netcache.1 +11 -0
- {offpunk-2.1 → offpunk-2.2}/man/opnk.1 +4 -0
- {offpunk-2.1 → offpunk-2.2}/netcache.py +8 -7
- {offpunk-2.1 → offpunk-2.2}/offpunk.py +56 -44
- {offpunk-2.1 → offpunk-2.2}/offutils.py +7 -1
- {offpunk-2.1 → offpunk-2.2}/opnk.py +16 -10
- {offpunk-2.1 → offpunk-2.2}/.gitignore +0 -0
- {offpunk-2.1 → offpunk-2.2}/CONTRIBUTORS +0 -0
- {offpunk-2.1 → offpunk-2.2}/LICENSE +0 -0
- {offpunk-2.1 → offpunk-2.2}/README.md +0 -0
- {offpunk-2.1 → offpunk-2.2}/debug.sh +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/config.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/dev.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/index.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/install.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/lists.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/offline.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/shell.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/doc/tutorial.gmi +0 -0
- {offpunk-2.1 → offpunk-2.2}/man/offpunk.1 +0 -0
- {offpunk-2.1 → offpunk-2.2}/netcache_migration.py +0 -0
- {offpunk-2.1 → offpunk-2.2}/offblocklist.py +0 -0
- {offpunk-2.1 → offpunk-2.2}/offthemes.py +0 -0
- {offpunk-2.1 → offpunk-2.2}/pyproject.toml +0 -0
- {offpunk-2.1 → offpunk-2.2}/requirements.txt +0 -0
- {offpunk-2.1 → offpunk-2.2}/screenshot_offpunk1.png +0 -0
- {offpunk-2.1 → offpunk-2.2}/screenshot_offpunk2.png +0 -0
- {offpunk-2.1 → offpunk-2.2}/ubuntu_dependencies.txt +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Offpunk History
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
## 2.2 - February 13th 2023
|
|
5
|
+
- cache folder is now configurable through $OFFPUNK_CACHE_PATH environment variable (by prx)
|
|
6
|
+
- offpunk: adding an URL to a list now update the view mode if url already present
|
|
7
|
+
- netcache: solve an infinite gemini loop with code 6X (see also bug #31)
|
|
8
|
+
- ansicat: added support for <video> HTML-element
|
|
9
|
+
- ansicat: if chafa fails to load an image, fallback to timg if available
|
|
10
|
+
- offpunk: add list autocompletion to "tour"
|
|
11
|
+
- offpunk: removed "blackbox", which has not been used nor maintained
|
|
12
|
+
- offpunk: "gus" was broken, it is functionnal again
|
|
13
|
+
- opnk/offpunk: more informative prompt in less
|
|
14
|
+
- ansicat: added support for HTML description elements <dt> and <dd> (by Bert Livens)
|
|
15
|
+
- opnk: added "--mode" command-line argument (bug #39)
|
|
16
|
+
- offpunk: support for "preformatted" theming (bug #38)
|
|
17
|
+
- opnk/netcache: added "--cache-validity" command-line argument (bug #37)
|
|
18
|
+
- ansicat: consider files as XML, not SVG, if they don’t have .svg extension
|
|
19
|
+
- offpunk: fix "view link" crashing with link to empty files
|
|
20
|
+
|
|
3
21
|
## 2.1 - December 15th 2023
|
|
4
22
|
- freshly updated gemtext/rss links are highlighted ("new_link" theme option)
|
|
5
23
|
- offpunk : new "copy title" and "copy link" function
|
|
@@ -106,7 +106,8 @@ def inline_image(img_file,width):
|
|
|
106
106
|
if not os.path.exists(img_file):
|
|
107
107
|
return ""
|
|
108
108
|
#Chafa is faster than timg inline. Let use that one by default
|
|
109
|
-
|
|
109
|
+
#But we keep a list of "inlines" in case chafa fails
|
|
110
|
+
inlines = []
|
|
110
111
|
ansi_img = ""
|
|
111
112
|
#We avoid errors by not trying to render non-image files
|
|
112
113
|
if shutil.which("file"):
|
|
@@ -120,32 +121,39 @@ def inline_image(img_file,width):
|
|
|
120
121
|
if hasattr(img_obj,"n_frames") and img_obj.n_frames > 1:
|
|
121
122
|
# we remove all frames but the first one
|
|
122
123
|
img_obj.save(img_file,format="gif",save_all=False)
|
|
123
|
-
|
|
124
|
+
inlines.append("chafa --bg white -s %s -f symbols")
|
|
124
125
|
elif _NEW_CHAFA:
|
|
125
|
-
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
inlines.append("chafa --bg white -t 1 -s %s -f symbols --animate=off")
|
|
127
|
+
if _NEW_TIMG:
|
|
128
|
+
inlines.append("timg --frames=1 -p q -g %sx1000")
|
|
129
|
+
image_success = False
|
|
130
|
+
while not image_success and len(inlines)>0:
|
|
131
|
+
cmd = inlines.pop(0)%width + " %s"
|
|
130
132
|
try:
|
|
131
133
|
ansi_img = run(cmd, parameter=img_file)
|
|
134
|
+
image_success = True
|
|
132
135
|
except Exception as err:
|
|
133
|
-
ansi_img = "***
|
|
136
|
+
ansi_img = "***IMAGE ERROR***\n%s…\n…%s" %(str(err)[:50],str(err)[-50:])
|
|
134
137
|
return ansi_img
|
|
135
138
|
|
|
136
139
|
def terminal_image(img_file):
|
|
137
140
|
#Render by timg is better than old chafa.
|
|
138
141
|
# it is also centered
|
|
139
|
-
|
|
142
|
+
cmds = []
|
|
140
143
|
if _NEW_CHAFA:
|
|
141
|
-
|
|
142
|
-
elif _NEW_TIMG:
|
|
143
|
-
cmd = "timg --loops=1 -C"
|
|
144
|
+
cmds.append("chafa -C on -d 0 --bg white -t 1 -w 1")
|
|
144
145
|
elif _HAS_CHAFA:
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
cmds.append("chafa -d 0 --bg white -t 1 -w 1")
|
|
147
|
+
if _NEW_TIMG:
|
|
148
|
+
cmds.append("timg --loops=1 -C")
|
|
149
|
+
image_success = False
|
|
150
|
+
while not image_success and len(cmds) > 0:
|
|
151
|
+
cmd = cmds.pop(0) + " %s"
|
|
152
|
+
try:
|
|
153
|
+
run(cmd, parameter=img_file, direct_output=True)
|
|
154
|
+
image_success = True
|
|
155
|
+
except Exception as err:
|
|
156
|
+
print(err)
|
|
149
157
|
|
|
150
158
|
|
|
151
159
|
# First, we define the different content->text renderers, outside of the rest
|
|
@@ -357,15 +365,28 @@ class AbstractRenderer():
|
|
|
357
365
|
|
|
358
366
|
# Beware, blocks are not wrapped nor indented and left untouched!
|
|
359
367
|
# They are mostly useful for pictures and preformatted text.
|
|
360
|
-
def add_block(self,intext):
|
|
368
|
+
def add_block(self,intext,theme=None):
|
|
361
369
|
# If necessary, we add the title before a block
|
|
362
370
|
self._title_first()
|
|
363
371
|
# we don’t want to indent blocks
|
|
364
372
|
self._endline()
|
|
365
373
|
self._disable_indents()
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
374
|
+
#we have to apply the theme for every line in the intext
|
|
375
|
+
#applying theme to preformatted is controversial as it could change it
|
|
376
|
+
if theme:
|
|
377
|
+
block = ""
|
|
378
|
+
lines = intext.split("\n")
|
|
379
|
+
for l in lines:
|
|
380
|
+
self.open_theme(theme)
|
|
381
|
+
self.last_line += self.current_indent + l
|
|
382
|
+
self.close_theme(theme)
|
|
383
|
+
self._endline()
|
|
384
|
+
self.last_line += "\n"
|
|
385
|
+
#one thing is sure : we need to keep unthemed blocks for images!
|
|
386
|
+
else:
|
|
387
|
+
self.final_text += self.current_indent + intext
|
|
388
|
+
self.new_paragraph = False
|
|
389
|
+
self._endline()
|
|
369
390
|
self._enable_indents()
|
|
370
391
|
|
|
371
392
|
def add_text(self,intext):
|
|
@@ -633,7 +654,7 @@ class GemtextRenderer(AbstractRenderer):
|
|
|
633
654
|
r.close_theme("preformatted")
|
|
634
655
|
elif preformatted:
|
|
635
656
|
# infinite line to not wrap preformated
|
|
636
|
-
r.add_block(line+"\n")
|
|
657
|
+
r.add_block(line+"\n",theme="preformatted")
|
|
637
658
|
elif len(line.strip()) == 0:
|
|
638
659
|
r.newparagraph(force=True)
|
|
639
660
|
elif line.startswith("=>"):
|
|
@@ -1077,7 +1098,7 @@ class HtmlRenderer(AbstractRenderer):
|
|
|
1077
1098
|
toreturn = " " + toreturn
|
|
1078
1099
|
return toreturn
|
|
1079
1100
|
def recursive_render(element,indent="",preformatted=False):
|
|
1080
|
-
if element.name
|
|
1101
|
+
if element.name in ["blockquote", "dd"]:
|
|
1081
1102
|
r.newparagraph()
|
|
1082
1103
|
r.startindent(" ",reverse=" ")
|
|
1083
1104
|
for child in element.children:
|
|
@@ -1085,7 +1106,7 @@ class HtmlRenderer(AbstractRenderer):
|
|
|
1085
1106
|
recursive_render(child,indent="\t")
|
|
1086
1107
|
r.close_theme("blockquote")
|
|
1087
1108
|
r.endindent()
|
|
1088
|
-
elif element.name in ["div","p"]:
|
|
1109
|
+
elif element.name in ["div","p","dt"]:
|
|
1089
1110
|
r.newparagraph()
|
|
1090
1111
|
for child in element.children:
|
|
1091
1112
|
recursive_render(child,indent=indent)
|
|
@@ -1114,8 +1135,8 @@ class HtmlRenderer(AbstractRenderer):
|
|
|
1114
1135
|
recursive_render(child,indent=indent,preformatted=True)
|
|
1115
1136
|
elif element.name in ["pre"]:
|
|
1116
1137
|
r.newparagraph()
|
|
1117
|
-
r.add_block(element.text)
|
|
1118
|
-
r.newparagraph()
|
|
1138
|
+
r.add_block(element.text,theme="preformatted")
|
|
1139
|
+
r.newparagraph(force=True)
|
|
1119
1140
|
elif element.name in ["li"]:
|
|
1120
1141
|
r.startindent(" • ",sub=" ")
|
|
1121
1142
|
for child in element.children:
|
|
@@ -1196,6 +1217,52 @@ class HtmlRenderer(AbstractRenderer):
|
|
|
1196
1217
|
r.add_text(text + link_id)
|
|
1197
1218
|
r.close_theme("image_link")
|
|
1198
1219
|
r.newline()
|
|
1220
|
+
|
|
1221
|
+
elif element.name == "video":
|
|
1222
|
+
poster = element.get("poster")
|
|
1223
|
+
src = element.get("src")
|
|
1224
|
+
for child in element.children:
|
|
1225
|
+
if not src:
|
|
1226
|
+
if child.name == "source":
|
|
1227
|
+
src = child.get("src")
|
|
1228
|
+
text = ""
|
|
1229
|
+
if poster:
|
|
1230
|
+
ansi_img = render_image(poster,width=width,mode=mode)
|
|
1231
|
+
alt = element.get("alt")
|
|
1232
|
+
if alt:
|
|
1233
|
+
alt = sanitize_string(alt)
|
|
1234
|
+
text += "[VIDEO] %s"%alt
|
|
1235
|
+
else:
|
|
1236
|
+
text += "[VIDEO]"
|
|
1237
|
+
|
|
1238
|
+
if poster:
|
|
1239
|
+
if not mode in self.images:
|
|
1240
|
+
self.images[mode] = []
|
|
1241
|
+
poster_url,d = looks_like_base64(poster,self.url)
|
|
1242
|
+
if poster_url:
|
|
1243
|
+
vid_url,d2 = looks_like_base64(src,self.url)
|
|
1244
|
+
self.images[mode].append(poster_url)
|
|
1245
|
+
r.add_block(ansi_img)
|
|
1246
|
+
r.open_theme("image_link")
|
|
1247
|
+
r.center_line()
|
|
1248
|
+
if vid_url and src:
|
|
1249
|
+
links.append(vid_url+" "+text)
|
|
1250
|
+
link_id = " [%s]"%(len(links)+startlinks)
|
|
1251
|
+
r.add_text(text + link_id)
|
|
1252
|
+
else:
|
|
1253
|
+
r.add_text(text)
|
|
1254
|
+
r.close_theme("image_link")
|
|
1255
|
+
r.newline()
|
|
1256
|
+
elif src:
|
|
1257
|
+
vid_url,d = looks_like_base64(src,self.url)
|
|
1258
|
+
links.append(vid_url+" "+text)
|
|
1259
|
+
link_id = " [%s]"%(len(links)+startlinks)
|
|
1260
|
+
r.open_theme("image_link")
|
|
1261
|
+
r.center_line()
|
|
1262
|
+
r.add_text(text + link_id)
|
|
1263
|
+
r.close_theme("image_link")
|
|
1264
|
+
r.newline()
|
|
1265
|
+
|
|
1199
1266
|
elif element.name == "br":
|
|
1200
1267
|
r.newline()
|
|
1201
1268
|
elif element.name not in ["script","style","template"] and type(element) != Comment:
|
|
@@ -1288,6 +1355,9 @@ def get_mime(path,url=None):
|
|
|
1288
1355
|
# If it’s a xml file, consider it as such, regardless of what file thinks
|
|
1289
1356
|
elif path.endswith(".xml"):
|
|
1290
1357
|
mime = "text/xml"
|
|
1358
|
+
# If it doesn’t end with .svg, it is probably an xml, not a SVG file
|
|
1359
|
+
elif "svg" in mime and not path.endswith(".svg"):
|
|
1360
|
+
mime = "text/xml"
|
|
1291
1361
|
#Some xml/html document are considered as octet-stream
|
|
1292
1362
|
if mime == "application/octet-stream":
|
|
1293
1363
|
mime = "text/xml"
|
|
@@ -54,6 +54,8 @@ either thanks to the MIME type,
|
|
|
54
54
|
or from the file being rendered itself.
|
|
55
55
|
.It Fl \-mime Ar MIME
|
|
56
56
|
MIME type of the content to parse.
|
|
57
|
+
.It Fl \-mode Ar MODE
|
|
58
|
+
MODE to use to render to choose between normal (default), full or source
|
|
57
59
|
.It Fl \-url Ar URL ...
|
|
58
60
|
original URL of the content.
|
|
59
61
|
.El
|
|
@@ -27,6 +27,15 @@ otherwise it would always refresh it from the version available online.
|
|
|
27
27
|
It is also useful for mapping a given URL to its location in the cache,
|
|
28
28
|
independently of whether it has been downloaded first.
|
|
29
29
|
.Pp
|
|
30
|
+
Default cache path is
|
|
31
|
+
.Pa ~/.cache/offpunk .
|
|
32
|
+
Set
|
|
33
|
+
.Ev OFFPUNK_CACHE_PATH
|
|
34
|
+
environment variable to use another location.
|
|
35
|
+
.Bd -literal
|
|
36
|
+
OFFPUNK_CACHE_PATH=/home/ploum/custom-cache netcache.py gemini://some.url
|
|
37
|
+
.Ed
|
|
38
|
+
.Pp
|
|
30
39
|
.Xr Offpunk 1
|
|
31
40
|
is a command-line browser and feed reader dedicated to browsing the Web,
|
|
32
41
|
Gemini, Gopher and Spartan.
|
|
@@ -47,6 +56,8 @@ The value is expressed in megabytes.
|
|
|
47
56
|
.It Fl \-timeout Ar TIMEOUT
|
|
48
57
|
time to wait before cancelling connection.
|
|
49
58
|
The value is expressed in seconds.
|
|
59
|
+
.It Fl \-cache-validity CACHE_VALIDITY
|
|
60
|
+
Maximum age (in second) of the cached version before redownloading a new version.
|
|
50
61
|
.El
|
|
51
62
|
.
|
|
52
63
|
.Sh EXIT STATUS
|
|
@@ -37,6 +37,10 @@ path to the file or URL to open.
|
|
|
37
37
|
.Bl -tag -width Ds -offset indent
|
|
38
38
|
.It Fl h , \-help
|
|
39
39
|
Show a help message and exit
|
|
40
|
+
.It Fl \-mode Ar MODE
|
|
41
|
+
MODE to use to render to choose between normal (default), full or source
|
|
42
|
+
.It Fl \-cache-validity CACHE_VALIDITY
|
|
43
|
+
Maximum age (in second) of the cached version before redownloading a new version.
|
|
40
44
|
.El
|
|
41
45
|
.
|
|
42
46
|
.Sh EXIT STATUS
|
|
@@ -731,9 +731,9 @@ def _fetch_gemini(url,timeout=DEFAULT_TIMEOUT,interactive=True,accept_bad_ssl_ce
|
|
|
731
731
|
raise RuntimeError(meta)
|
|
732
732
|
# Client cert
|
|
733
733
|
elif status.startswith("6"):
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
734
|
+
error = "Handling certificates for status 6X are not supported by offpunk\n"
|
|
735
|
+
error += "See bug #31 for discussion about the problem"
|
|
736
|
+
raise RuntimeError(error)
|
|
737
737
|
# Invalid status
|
|
738
738
|
elif not status.startswith("2"):
|
|
739
739
|
raise RuntimeError("Server returned undefined status code %s!" % status)
|
|
@@ -878,11 +878,12 @@ def main():
|
|
|
878
878
|
help="Cancel download of items above that size (value in Mb).")
|
|
879
879
|
parser.add_argument("--timeout", type=int,
|
|
880
880
|
help="Time to wait before cancelling connection (in second).")
|
|
881
|
+
parser.add_argument("--cache-validity",type=int, default=0,
|
|
882
|
+
help="maximum age, in second, of the cached version before \
|
|
883
|
+
redownloading a new version")
|
|
881
884
|
# No argument: write help
|
|
882
885
|
parser.add_argument('url', metavar='URL', nargs='*',
|
|
883
886
|
help='download URL and returns the content or the path to a cached version')
|
|
884
|
-
# arg = URL: download and returns cached URI
|
|
885
|
-
# --cache-validity : do not download if cache is valid
|
|
886
887
|
# --validity : returns the date of the cached version, Null if no version
|
|
887
888
|
# --force-download : download and replace cache, even if valid
|
|
888
889
|
args = parser.parse_args()
|
|
@@ -893,8 +894,8 @@ def main():
|
|
|
893
894
|
if args.offline:
|
|
894
895
|
path = get_cache_path(u)
|
|
895
896
|
else:
|
|
896
|
-
|
|
897
|
-
|
|
897
|
+
path,url = fetch(u,max_size=args.max_size,timeout=args.timeout,\
|
|
898
|
+
validity=args.cache_validity)
|
|
898
899
|
if args.path:
|
|
899
900
|
print(path)
|
|
900
901
|
else:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Offline-First Gemini/Web/Gopher/RSS reader and browser
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.2"
|
|
8
8
|
|
|
9
9
|
## Initial imports and conditional imports {{{
|
|
10
10
|
import argparse
|
|
@@ -206,6 +206,8 @@ class GeminiClient(cmd.Cmd):
|
|
|
206
206
|
return [i+" " for i in allowed if i.startswith(text)]
|
|
207
207
|
def complete_move(self,text,line,begidx,endidx):
|
|
208
208
|
return self.complete_add(text,line,begidx,endidx)
|
|
209
|
+
def complete_tour(self,text,line,begidx,endidx):
|
|
210
|
+
return self.complete_add(text,line,begidx,endidx)
|
|
209
211
|
|
|
210
212
|
def complete_theme(self,text,line,begidx,endidx):
|
|
211
213
|
elements = offthemes.default
|
|
@@ -299,7 +301,7 @@ class GeminiClient(cmd.Cmd):
|
|
|
299
301
|
else:
|
|
300
302
|
self.page_index = 0
|
|
301
303
|
# Update state (external files are not added to history)
|
|
302
|
-
self.current_url =
|
|
304
|
+
self.current_url = modedurl
|
|
303
305
|
if update_hist and not self.sync_only:
|
|
304
306
|
self._update_history(modedurl)
|
|
305
307
|
else:
|
|
@@ -920,7 +922,8 @@ Use 'ls -l' to see URLs."""
|
|
|
920
922
|
if not line:
|
|
921
923
|
print("What?")
|
|
922
924
|
return
|
|
923
|
-
|
|
925
|
+
search = line.replace(" ","%20")
|
|
926
|
+
self._go_to_url("gemini://geminispace.info/search?%s"%search)
|
|
924
927
|
|
|
925
928
|
def do_history(self, *args):
|
|
926
929
|
"""Display history."""
|
|
@@ -988,8 +991,11 @@ Use "view XX" where XX is a number to view information about link XX.
|
|
|
988
991
|
if netcache.is_cache_valid(link_url):
|
|
989
992
|
last_modified = netcache.cache_last_modified(link_url)
|
|
990
993
|
link_renderer = self.get_renderer(link_url)
|
|
991
|
-
|
|
992
|
-
|
|
994
|
+
if link_renderer:
|
|
995
|
+
link_title = link_renderer.get_page_title()
|
|
996
|
+
print(link_title)
|
|
997
|
+
else:
|
|
998
|
+
print("Empty cached version")
|
|
993
999
|
print("Last cached on %s"%time.ctime(last_modified))
|
|
994
1000
|
else:
|
|
995
1001
|
print("No cached version for this link")
|
|
@@ -1215,8 +1221,6 @@ archives, which is a special historical list limited in size. It is similar to `
|
|
|
1215
1221
|
url = self.current_url
|
|
1216
1222
|
r = self.get_renderer(url)
|
|
1217
1223
|
if r:
|
|
1218
|
-
mode = r.get_mode()
|
|
1219
|
-
url = mode_url(url,mode)
|
|
1220
1224
|
title = r.get_page_title()
|
|
1221
1225
|
else:
|
|
1222
1226
|
title = ""
|
|
@@ -1233,23 +1237,26 @@ archives, which is a special historical list limited in size. It is similar to `
|
|
|
1233
1237
|
return False
|
|
1234
1238
|
else:
|
|
1235
1239
|
if not url:
|
|
1236
|
-
url
|
|
1240
|
+
url = self.current_url
|
|
1241
|
+
unmoded_url,mode = unmode_url(url)
|
|
1237
1242
|
# first we check if url already exists in the file
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1243
|
+
if self.list_has_url(url,list,exact_mode=True):
|
|
1244
|
+
if verbose:
|
|
1245
|
+
print("%s already in %s."%(url,list))
|
|
1246
|
+
return False
|
|
1247
|
+
# If the URL already exists but without a mode, we update the mode
|
|
1248
|
+
# FIXME: this doesn’t take into account the case where you want to remove the mode
|
|
1249
|
+
elif url != unmoded_url and self.list_has_url(unmoded_url,list):
|
|
1250
|
+
self.list_update_url_mode(unmoded_url,list,mode)
|
|
1251
|
+
if verbose:
|
|
1252
|
+
print("%s has updated mode in %s to %s"%(url,list,mode))
|
|
1253
|
+
else:
|
|
1254
|
+
with open(list_path,"a") as l_file:
|
|
1255
|
+
l_file.write(self.to_map_line(url))
|
|
1256
|
+
l_file.close()
|
|
1257
|
+
if verbose:
|
|
1258
|
+
print("%s added to %s" %(url,list))
|
|
1259
|
+
return True
|
|
1253
1260
|
|
|
1254
1261
|
@needs_gi
|
|
1255
1262
|
def list_add_top(self,list,limit=0,truncate_lines=0):
|
|
@@ -1288,8 +1295,14 @@ archives, which is a special historical list limited in size. It is similar to `
|
|
|
1288
1295
|
def list_rm_url(self,url,list):
|
|
1289
1296
|
return self.list_has_url(url,list,deletion=True)
|
|
1290
1297
|
|
|
1298
|
+
def list_update_url_mode(self,url,list,mode):
|
|
1299
|
+
return self.list_has_url(url,list,update_mode = mode)
|
|
1300
|
+
|
|
1291
1301
|
# deletion and has_url are so similar, I made them the same method
|
|
1292
|
-
|
|
1302
|
+
# deletion : true or false if you want to delete the URL
|
|
1303
|
+
# exact_mode : True if you want to check only for the exact url, not the canonical one
|
|
1304
|
+
# update_mode : a new mode to update the URL
|
|
1305
|
+
def list_has_url(self,url,list,deletion=False, exact_mode=False, update_mode = None):
|
|
1293
1306
|
list_path = self.list_path(list)
|
|
1294
1307
|
if list_path:
|
|
1295
1308
|
to_return = False
|
|
@@ -1298,7 +1311,8 @@ archives, which is a special historical list limited in size. It is similar to `
|
|
|
1298
1311
|
lf.close()
|
|
1299
1312
|
to_write = []
|
|
1300
1313
|
# let’s remove the mode
|
|
1301
|
-
|
|
1314
|
+
if not exact_mode:
|
|
1315
|
+
url=unmode_url(url)[0]
|
|
1302
1316
|
for l in lines:
|
|
1303
1317
|
# we separate components of the line
|
|
1304
1318
|
# to ensure we identify a complete URL, not a part of it
|
|
@@ -1306,15 +1320,27 @@ archives, which is a special historical list limited in size. It is similar to `
|
|
|
1306
1320
|
if url not in splitted and len(splitted) > 1:
|
|
1307
1321
|
current = unmode_url(splitted[1])[0]
|
|
1308
1322
|
#sometimes, we must remove the ending "/"
|
|
1309
|
-
if url == current:
|
|
1310
|
-
to_return = True
|
|
1311
|
-
elif url.endswith("/") and url[:-1] == current:
|
|
1323
|
+
if url == current or (url.endswith("/") and url[:-1] == current):
|
|
1312
1324
|
to_return = True
|
|
1325
|
+
if update_mode:
|
|
1326
|
+
new_line = l.replace(current,mode_url(url,update_mode))
|
|
1327
|
+
to_write.append(new_line)
|
|
1328
|
+
elif not deletion:
|
|
1329
|
+
to_write.append(l)
|
|
1313
1330
|
else:
|
|
1314
1331
|
to_write.append(l)
|
|
1315
|
-
|
|
1332
|
+
elif url in splitted:
|
|
1316
1333
|
to_return = True
|
|
1317
|
-
|
|
1334
|
+
# We update the mode if asked by replacing the old url
|
|
1335
|
+
# by a moded one in the same line
|
|
1336
|
+
if update_mode:
|
|
1337
|
+
new_line = l.replace(url,mode_url(url,update_mode))
|
|
1338
|
+
to_write.append(new_line)
|
|
1339
|
+
elif not deletion:
|
|
1340
|
+
to_write.append(l)
|
|
1341
|
+
else:
|
|
1342
|
+
to_write.append(l)
|
|
1343
|
+
if deletion or update_mode:
|
|
1318
1344
|
with open(list_path,"w") as lf:
|
|
1319
1345
|
for l in to_write:
|
|
1320
1346
|
lf.write(l)
|
|
@@ -1588,19 +1614,6 @@ The following lists cannot be removed or frozen but can be edited with "list edi
|
|
|
1588
1614
|
else:
|
|
1589
1615
|
cmd.Cmd.do_help(self, arg)
|
|
1590
1616
|
|
|
1591
|
-
### Flight recorder
|
|
1592
|
-
def do_blackbox(self, *args):
|
|
1593
|
-
"""Display contents of flight recorder, showing statistics for the
|
|
1594
|
-
current gemini browsing session."""
|
|
1595
|
-
lines = []
|
|
1596
|
-
# Compute flight time
|
|
1597
|
-
now = time.time()
|
|
1598
|
-
delta = now - self.log["start_time"]
|
|
1599
|
-
hours, remainder = divmod(delta, 3600)
|
|
1600
|
-
minutes, seconds = divmod(remainder, 60)
|
|
1601
|
-
# Assemble lines
|
|
1602
|
-
lines.append(("Patrol duration", "%02d:%02d:%02d" % (hours, minutes, seconds)))
|
|
1603
|
-
|
|
1604
1617
|
def do_sync(self, line):
|
|
1605
1618
|
"""Synchronize all bookmarks lists and URLs from the to_fetch list.
|
|
1606
1619
|
- New elements in pages in subscribed lists will be added to tour
|
|
@@ -1879,7 +1892,6 @@ def main():
|
|
|
1879
1892
|
gc.onecmd(line)
|
|
1880
1893
|
lists = None
|
|
1881
1894
|
gc.call_sync(refresh_time=refresh_time,depth=depth,lists=args.url)
|
|
1882
|
-
gc.onecmd("blackbox")
|
|
1883
1895
|
else:
|
|
1884
1896
|
# We are in the normal mode. First process config file
|
|
1885
1897
|
torun_queue = read_config(torun_queue,rcfile=args.config_file,interactive=True)
|
|
@@ -62,9 +62,15 @@ def xdg(folder="cache"):
|
|
|
62
62
|
#if no XDG .local/share and not XDG .config, we use the old config
|
|
63
63
|
if not os.path.exists(data_home) and os.path.exists(_old_config):
|
|
64
64
|
_DATA_DIR = _CONFIG_DIR
|
|
65
|
+
## get _CACHE_PATH from OFFPUNK_CACHE_PATH environment variable
|
|
66
|
+
# if OFFPUNK_CACHE_PATH empty, set default to ~/.cache/offpunk
|
|
65
67
|
cache_home = os.environ.get('XDG_CACHE_HOME') or\
|
|
66
68
|
os.path.join(_home,'.cache')
|
|
67
|
-
_CACHE_PATH = os.
|
|
69
|
+
_CACHE_PATH = os.environ.get('OFFPUNK_CACHE_PATH', \
|
|
70
|
+
os.path.join(os.path.expanduser(cache_home),"offpunk/"))
|
|
71
|
+
#Check that the cache path ends with "/"
|
|
72
|
+
if not _CACHE_PATH.endswith("/"):
|
|
73
|
+
_CACHE_PATH += "/"
|
|
68
74
|
os.makedirs(_CACHE_PATH,exist_ok=True)
|
|
69
75
|
if folder == "cache" and not UPGRADED:
|
|
70
76
|
upgrade_cache(_CACHE_PATH)
|
|
@@ -48,16 +48,16 @@ else:
|
|
|
48
48
|
# -S : do not wrap long lines. Wrapping is done by offpunk, longlines
|
|
49
49
|
# are there on purpose (surch in asciiart)
|
|
50
50
|
#--incsearch : incremental search starting rev581
|
|
51
|
-
if less_version >= 581:
|
|
52
|
-
less_base = "less --incsearch --save-marks -~ -XRfMWiS"
|
|
53
|
-
elif less_version >= 572:
|
|
54
|
-
less_base = "less --save-marks -XRfMWiS"
|
|
55
|
-
else:
|
|
56
|
-
less_base = "less -XRfMWiS"
|
|
57
|
-
_DEFAULT_LESS = less_base + " \"+''\" %s"
|
|
58
|
-
_DEFAULT_CAT = less_base + " -EF %s"
|
|
59
|
-
|
|
60
51
|
def less_cmd(file, histfile=None,cat=False,grep=None):
|
|
52
|
+
less_prompt = "page %%d/%%D- lines %%lb/%%L - %%Pb\%%"
|
|
53
|
+
if less_version >= 581:
|
|
54
|
+
less_base = "less --incsearch --save-marks -~ -XRfWiS -P \"%s\""%less_prompt
|
|
55
|
+
elif less_version >= 572:
|
|
56
|
+
less_base = "less --save-marks -XRfMWiS"
|
|
57
|
+
else:
|
|
58
|
+
less_base = "less -XRfMWiS"
|
|
59
|
+
_DEFAULT_LESS = less_base + " \"+''\" %s"
|
|
60
|
+
_DEFAULT_CAT = less_base + " -EF %s"
|
|
61
61
|
if histfile:
|
|
62
62
|
env = {"LESSHISTFILE": histfile}
|
|
63
63
|
else:
|
|
@@ -280,12 +280,18 @@ def main():
|
|
|
280
280
|
opnk will fallback to opening the file with xdg-open. If given an URL as input \
|
|
281
281
|
instead of a path, opnk will rely on netcache to get the networked content."
|
|
282
282
|
parser = argparse.ArgumentParser(prog="opnk",description=descri)
|
|
283
|
+
parser.add_argument("--mode", metavar="MODE",
|
|
284
|
+
help="Which mode should be used to render: normal (default), full or source.\
|
|
285
|
+
With HTML, the normal mode try to extract the article.")
|
|
283
286
|
parser.add_argument("content",metavar="INPUT", nargs="*",
|
|
284
287
|
default=sys.stdin, help="Path to the file or URL to open")
|
|
288
|
+
parser.add_argument("--cache-validity",type=int, default=0,
|
|
289
|
+
help="maximum age, in second, of the cached version before \
|
|
290
|
+
redownloading a new version")
|
|
285
291
|
args = parser.parse_args()
|
|
286
292
|
cache = opencache()
|
|
287
293
|
for f in args.content:
|
|
288
|
-
cache.opnk(f)
|
|
294
|
+
cache.opnk(f,mode=args.mode,validity=args.cache_validity)
|
|
289
295
|
|
|
290
296
|
if __name__ == "__main__":
|
|
291
297
|
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|