💾 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:



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()