💾 Archived View for gemini.ctrl-c.club › ~nttp › toys › gemview › gemview.py captured on 2024-02-05 at 10:53:12.
View Raw
More Information
⬅️ Previous capture (2023-03-20)
-=-=-=-=-=-=-
#!/usr/bin/env python3
#
# GemView: text-based file viewer with GemText support
# Copyright 2023 Felix Pleșoianu <https://felix.plesoianu.ro/>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import webbrowser
import zipfile
import glob
import os.path
import os
import sys
import urwid as ur
version_string = "GemView 0.7 beta (03 Feb 2023)"
about = """
# About GemView
> Text-based file viewer with GemText support
GemView is a little command-line tool for viewing and browsing local text files, mainly in the GemText markup language (see below). Usage:
gemview.py [ -h | -v | <file or directory> ]
Links can be clicked, but you need a keyboard to navigate. Shortcuts:
- Press 'b' to go back.
- Press 'r' or F5 to reload.
- Press 'q' or F10 to exit.
For more information see the included or online documentation.
=> https://gemini.circumlunar.space/ Gemini protocol
=> https://ctrl-c.club/~nttp/toys/gemview/ Project website
=> gemini://gemini.ctrl-c.club/toys/gemview/ Project capsule
"""
history = []
pre_flag = False
def render_gem_text(line):
global pre_flag
if pre_flag:
if line.startswith("```"):
pre_flag = False
return None
else:
line = ur.Text(line, wrap="clip")
return ur.AttrMap(line, "pre")
elif line.startswith("```"):
pre_flag = True
return None
elif len(line) == 0:
return ur.Divider()
elif line.startswith('>'):
line = ur.Text(line[1:].lstrip())
line = ur.Padding(line, left=2, right=2)
return ur.AttrMap(line, "quote")
elif line.startswith('*'):
return ur.Padding(ur.Text(line), left=2)
elif line.startswith('#'):
return ur.AttrMap(ur.Text(line), "heading")
elif line.startswith("=>"):
line = line[2:].lstrip().split(maxsplit=1)
if len(line) < 2: line.append(line[0])
button = ur.Button(line[1], follow_link, line[0])
button = ur.Padding(button, left=1, right=1)
return ur.AttrMap(button, "control")
else:
return ur.Text(line)
def render_markdown(line):
global pre_flag
if pre_flag:
if line.startswith("```"):
pre_flag = False
return None
else:
line = ur.Text(line, wrap="clip")
return ur.AttrMap(line, "pre")
elif line.startswith("```"):
pre_flag = True
return None
elif line.startswith(' ') or line.startswith('\t'):
line = ur.Text(line, wrap="clip")
return ur.AttrMap(line, "pre")
elif len(line) == 0:
return ur.Divider()
elif line.startswith("---"):
return ur.AttrMap(ur.Divider('-'), "heading")
elif line.startswith('>'):
line = ur.Text(line[1:].lstrip())
line = ur.Padding(line, left=2, right=2)
return ur.AttrMap(line, "quote")
elif line.startswith("* ") or line.startswith('-'):
return ur.Padding(ur.Text(line), left=2)
elif line.startswith('#'):
return ur.AttrMap(ur.Text(line), "heading")
else:
return ur.Text(line)
def render_org_mode(line):
if len(line) == 0:
return ur.Divider()
elif line.startswith("-----"):
return ur.AttrMap(ur.Divider('-'), "heading")
elif line.startswith("#+"):
return ur.AttrMap(ur.Text(line), "pre")
elif line.startswith('-'):
return ur.Padding(ur.Text(line), left=2)
elif line.startswith('*'):
return ur.AttrMap(ur.Text(line), "heading")
else:
return ur.Text(line)
def render_file_path(line):
if len(line) == 0:
return ur.Divider()
else:
button = ur.Button(os.path.basename(line), follow_link, line)
button = ur.Padding(button, left=1, right=1)
return ur.AttrMap(button, "control")
def render_history(line):
if len(line) == 0:
return ur.Divider()
else:
button = ur.Button(line, follow_link, line)
button = ur.Padding(button, left=1, right=1)
return ur.AttrMap(button, "control")
def render_script(line):
if len(line) == 0:
return ur.Divider()
elif line.startswith('#'):
line = ur.Text(line, wrap="clip")
return ur.AttrMap(line, "pre")
else:
return ur.Text(line, wrap="clip")
def render_plain_text(line):
if len(line) == 0:
return ur.Divider()
else:
return ur.Text(line)
def follow_link(widget, link):
for i in ["http://", "https://", "ftp://", "gopher://", "gemini://"]:
if link.startswith(i):
open_in_browser(link)
return
for i in ["irc:", "mailto:", "telnet:"]:
if link.startswith(i):
status.set_text("Error: can't open " + i + " link.")
return
if link.startswith("about:"):
load_page(link)
elif link.startswith("file://"):
navigate_to(link[7:])
else:
navigate_to(link)
def open_in_browser(link):
try:
webbrowser.open_new_tab(link)
except webbrowser.Error as e:
status.set_text("Error opening browser: " + str(e))
def navigate_to(link):
link = normal_path(link)
history.append(link)
load_page(link)
def normal_path(path):
if not path.startswith("/"):
path = os.path.join(base_dir(), path)
if os.path.isdir(path):
index = os.path.join(path, "index.gmi")
if os.path.exists(index):
path = index
return os.path.abspath(path)
def base_dir():
if len(history) > 0:
return os.path.dirname(history[-1])
else:
return os.getcwd()
def load_file(path, ext):
if not os.path.exists(path):
status.set_text("No such file: " + path)
return []
elif os.path.isdir(path):
status.set_text("GemView - " + path)
path = os.path.join(path, '*')
return [".", ".."] + sorted(glob.glob(path))
elif ext == ".zip":
try:
with zipfile.ZipFile(path) as f:
status.set_text("GemView - " + path)
return f.namelist()
except zipfile.BadZipFile as e:
status.set_text(
"Error reading file " + path + ": " + str(e))
return []
try:
with open(path) as f:
status.set_text("GemView - " + path)
return [i.rstrip() for i in f.readlines()]
except OSError as e:
status.set_text("Error opening file " + path + ": " + str(e))
return []
except UnicodeDecodeError as e:
status.set_text("Error reading file " + path + ": " + str(e))
return []
def load_page(path):
if path == "about:gemview":
ext = ".gmi"
data = about.splitlines()
elif path == "about:history":
ext = None
data = [""] + list(reversed(history)) + [""]
else:
_, ext = os.path.splitext(path)
data = [""] + load_file(path, ext) + [""]
if ext == ".gmi":
data = render_list(data, render_gem_text)
elif ext == ".md":
data = render_list(data, render_markdown)
elif ext == ".org":
data = render_list(data, render_org_mode)
elif ext == ".sh" or ext == ".py" or ext == ".tcl":
data = render_list(data, render_script)
elif ext == None:
data = render_list(data, render_history)
elif os.path.isdir(path):
data = render_list(data, render_file_path)
else:
data = render_list(data, render_plain_text)
viewport.body.clear()
viewport.body.extend(data)
def render_list(lines, renderer):
lines = [renderer(i.expandtabs()) for i in lines]
return [i for i in lines if i != None]
def go_back(widget):
if len(history) > 1:
history.pop()
load_page(history[-1])
# status.set_text("GemView - " + history[-1])
else:
status.set_text("Nowhere to go")
def do_reload(widget):
if len(history) > 0:
load_page(history[-1])
# status.set_text("GemView - " + history[-1])
else:
status.set_text("No page loaded")
def to_history(widget):
load_page("about:history")
def do_quit(widget):
raise ur.ExitMainLoop()
def on_input(key):
if key == "b":
go_back(None)
return True
elif key == "r" or key == "f5":
do_reload(None)
return True
elif key == "h":
to_history(None)
return True
elif key == "q" or key == "f10":
raise ur.ExitMainLoop()
bBack = ur.Button("Back", go_back)
bReload = ur.Button("Reload", do_reload)
bHistory = ur.Button("History", to_history)
bQuit = ur.Button("Quit", do_quit)
buttons = [bBack, bReload, bHistory, bQuit]
status = ur.AttrWrap(ur.Text(version_string), "chrome")
viewport = ur.ListBox([])
workspace = ur.AttrWrap(ur.Padding(viewport, left=2, right=2), "bg")
toolbar = ur.AttrWrap(ur.GridFlow(buttons, 11, 1, 0, "right"), "chrome")
top = ur.Frame(workspace, header=status, footer=toolbar)
palette = [
("chrome", "black", "white"),
("bg", "white", "dark blue"),
("heading", "bold,yellow", "dark blue"),
("quote", "italics,white", "dark blue"),
("pre", "light cyan", "dark blue"),
("control", "light cyan", "black")]
loop = ur.MainLoop(top, palette, unhandled_input=on_input)
help_text = """
GemView - a simple viewer for local GemText files
Usage:
gemview.py [ -h | -v | <file or directory> ]
Options:
-v, --version show version string and exit
-h, --help show this message and exit
"""
if __name__ == "__main__":
if len(sys.argv) < 2:
load_page("about:gemview")
loop.run()
elif sys.argv[1] == "-v" or sys.argv[1] == "--version":
print(version_string)
elif sys.argv[1] == "-h" or sys.argv[1] == "--help":
print(help_text)
else:
navigate_to(sys.argv[1])
loop.run()