1022 lines
32 KiB
Python
Executable File
1022 lines
32 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# Copyright 2016 Ville Rantanen
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
"""Markslider: a slideshow engine based on markdown."""
|
|
|
|
__author__ = "Ville Rantanen <ville.q.rantanen@gmail.com>"
|
|
__version__ = "1.3.2"
|
|
|
|
import sys, os, argparse, re, datetime
|
|
from argparse import ArgumentParser
|
|
import traceback, tty, termios, subprocess, signal
|
|
|
|
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
|
import ansicodes, md_color
|
|
|
|
try:
|
|
import climage
|
|
except ImportError:
|
|
pass
|
|
|
|
HL = ">"
|
|
EOS = "# End of Slides"
|
|
|
|
|
|
class getch:
|
|
def get(self):
|
|
fd = sys.stdin.fileno()
|
|
old_settings = termios.tcgetattr(fd)
|
|
try:
|
|
tty.setraw(sys.stdin.fileno())
|
|
ch = sys.stdin.read(1)
|
|
finally:
|
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
return ch
|
|
|
|
|
|
class EndProgram(Exception):
|
|
"""Nice exit"""
|
|
|
|
pass
|
|
|
|
|
|
class slide_reader:
|
|
"""Class for reading files."""
|
|
|
|
def __init__(self, files, opts):
|
|
self.filename = files[0]
|
|
self.files = files
|
|
self.reader = None
|
|
self.opts = opts
|
|
self.scrsize = get_console_size()
|
|
self.pages = 0
|
|
self.page = 0
|
|
self.file_start_page = []
|
|
self.width = None
|
|
self.height = None
|
|
self.max_width = None
|
|
self.max_height = None
|
|
self.data = []
|
|
self.re_image_convert = re.compile(r"(.*)(!\[.*\])\((.*)\)>")
|
|
self.re_command = re.compile(r"(.*)`(.*)`>(.*)")
|
|
# ~ self.control_chars = ''.join(map(unichr, range(0,32) + range(127,160)))
|
|
# ~ self.control_char_re = re.compile('[%s]' % re.escape(self.control_chars))
|
|
self.background = []
|
|
self.read()
|
|
self.pygments = False
|
|
if opts.syntax_pygments:
|
|
try:
|
|
self.pygments = Pygmentizer(opts.syntax_formatter, opts.syntax_style)
|
|
except ImportError as e:
|
|
self.pygments = False
|
|
|
|
def read(self):
|
|
"""Read a file, set pages and data"""
|
|
|
|
self.pages = 0
|
|
self.background = []
|
|
self.data = []
|
|
self.file_start_page = []
|
|
first_slide_found = False
|
|
for fname in self.files:
|
|
first_slide_found = False
|
|
f = open(fname, "r")
|
|
new_page = []
|
|
in_code = False
|
|
for row in f:
|
|
if not row:
|
|
continue
|
|
row = row.rstrip("\n\r ")
|
|
# find end of show
|
|
if row == EOS:
|
|
break
|
|
# find header to start a new page
|
|
if row.startswith("#") and not row.startswith("##") and not in_code:
|
|
first_slide_found = True
|
|
if len(new_page) > 0:
|
|
self.data.append(new_page)
|
|
new_page = []
|
|
if row.startswith("```"):
|
|
in_code = not in_code
|
|
# if first slide havent been found yet:
|
|
if not first_slide_found:
|
|
self.background.append(row)
|
|
continue
|
|
new_page.extend(self.generate_content(row))
|
|
if len(new_page) > 0:
|
|
self.data.append(new_page)
|
|
f.close()
|
|
self.file_start_page.append(len(self.data))
|
|
if len(self.data) == 0:
|
|
raise ValueError("File does not have a # header")
|
|
self.rename_duplicates()
|
|
self.toc()
|
|
self.pages = len(self.data)
|
|
self.inc_page_no(0)
|
|
self.max_width = 0
|
|
self.max_height = 0
|
|
for page in self.data:
|
|
self.max_height = max(self.max_height, len(page))
|
|
for row in page:
|
|
self.max_width = max(self.max_width, len(row))
|
|
|
|
def get_data(self):
|
|
return self.data
|
|
|
|
def get_current_filename(self):
|
|
for i, offset in enumerate(self.file_start_page):
|
|
if offset > self.page:
|
|
return self.files[i]
|
|
return "NA"
|
|
|
|
def get_filename(self):
|
|
return self.filename
|
|
|
|
def get_page(self, page):
|
|
try:
|
|
return self.data[page]
|
|
except IndexError:
|
|
return None
|
|
|
|
def get_pages(self):
|
|
return self.pages
|
|
|
|
def get_current_page(self):
|
|
return self.data[self.page]
|
|
|
|
def get_page_no(self):
|
|
return self.page
|
|
|
|
def inc_page_no(self, inc=1, loop=False):
|
|
self.page += inc
|
|
if self.page < 0:
|
|
self.page = 0
|
|
if loop:
|
|
self.page = self.pages - 1
|
|
if self.page >= self.pages:
|
|
self.page = self.pages - 1
|
|
if loop:
|
|
self.page = 0
|
|
self.width = max([len(x) for x in self.data[self.page]])
|
|
self.height = len(self.data[self.page])
|
|
|
|
def last_page(self):
|
|
self.page = self.pages - 1
|
|
|
|
def first_page(self):
|
|
self.page = 0
|
|
|
|
def get_page_height(self):
|
|
return self.height
|
|
|
|
def get_page_width(self):
|
|
return self.width
|
|
|
|
def get_max_height(self):
|
|
return self.max_height
|
|
|
|
def get_max_width(self):
|
|
return self.max_width
|
|
|
|
def get_toc(self, display_position=False):
|
|
title = self.opts.toc if self.opts.toc else "Table of Contents"
|
|
TOC = ["# " + title, ""]
|
|
offset = (self.opts.toc_page - 1) if self.opts.toc else 0
|
|
for h1, page in enumerate(self.data[offset:]):
|
|
title = page[0].strip("# ")
|
|
if display_position and h1 == self.page - offset:
|
|
title = "_%s_" % (title,)
|
|
TOC.append("%d. %s" % (h1 + 1, title))
|
|
subh = [0, 0, 0]
|
|
if self.opts.toc_depth > 1:
|
|
for line in page:
|
|
title = line.strip("# ")
|
|
if re.search(r"^##[^#]", line):
|
|
subh = [subh[0] + 1, 0, 0]
|
|
TOC.append(" %d.%d. %s" % (h1 + 1, subh[0], title))
|
|
if self.opts.toc_depth == 2:
|
|
continue
|
|
if re.search(r"^###[^#]", line):
|
|
subh = [subh[0], subh[1] + 1, 0]
|
|
TOC.append(
|
|
" %d.%d.%d. %s" % (h1 + 1, subh[0], subh[1], title)
|
|
)
|
|
if self.opts.toc_depth == 3:
|
|
continue
|
|
if re.search(r"^####[^#]", line):
|
|
subh = [subh[0], subh[1], subh[2] + 1]
|
|
TOC.append(
|
|
" %d.%d.%d.%d. %s"
|
|
% (h1 + 1, subh[0], subh[1], subh[2], title)
|
|
)
|
|
return TOC
|
|
|
|
def toc(self):
|
|
if self.opts.toc:
|
|
TOC = self.get_toc()
|
|
self.data.insert(self.opts.toc_page - 1, TOC)
|
|
# adding 1 is not fullproof, if toc page is after the first document
|
|
self.file_start_page = [1 + i for i in self.file_start_page]
|
|
|
|
def generate_content(self, s):
|
|
"""Check for launchable items, or converted images"""
|
|
if self.opts.execute_read:
|
|
if s.find("`>") > -1:
|
|
command = self.re_command.match(s)
|
|
if command != None:
|
|
return self.launch(command)
|
|
image = self.re_image_convert.match(s)
|
|
try:
|
|
return self.convert_image(image)
|
|
except Exception:
|
|
pass
|
|
return [s]
|
|
|
|
def launch(self, command):
|
|
"""Launch in a string using tags `command`>
|
|
Remove empty lines from beginning and end of stdout.
|
|
"""
|
|
output = subprocess.check_output(command.group(2).strip(), shell=True)
|
|
if type(output) == bytes:
|
|
output = output.decode("utf-8")
|
|
output = output.split("\n")
|
|
while len(output[0].strip()) == 0:
|
|
if len(output) == 1:
|
|
return [""]
|
|
del output[0]
|
|
while len(output[-1].strip()) == 0:
|
|
if len(output) == 1:
|
|
return [""]
|
|
del output[-1]
|
|
return_value = [command.group(1)]
|
|
return_value.extend(output)
|
|
return_value.append(command.group(3))
|
|
return return_value
|
|
# return [s]
|
|
|
|
def convert_image(self, image):
|
|
"""convert image using tags ![]()>
|
|
Remove empty lines from beginning and end of stdout.
|
|
"""
|
|
# ~ 2=title
|
|
# ~ 3=image command
|
|
width = max(5, self.scrsize[1] - 10)
|
|
output = climage.convert(
|
|
image.group(3),
|
|
is_unicode=True,
|
|
is_truecolor=True,
|
|
is_256color=False,
|
|
is_16color=False,
|
|
is_8color=False,
|
|
width=width,
|
|
palette="default",
|
|
)
|
|
output = output.split("\n")
|
|
while len(output[0].strip()) == 0:
|
|
if len(output) == 1:
|
|
return [""]
|
|
del output[0]
|
|
while len(output[-1].strip()) == 0:
|
|
if len(output) == 1:
|
|
return [""]
|
|
del output[-1]
|
|
return_value = [image.group(1), *output]
|
|
return return_value
|
|
|
|
def rename_duplicates(self):
|
|
|
|
if not opts.rename_title:
|
|
return
|
|
|
|
titles = {}
|
|
page_nos = []
|
|
for page in self.data:
|
|
if not page[0] in titles:
|
|
titles[page[0]] = 0
|
|
titles[page[0]] += 1
|
|
page_nos.append(titles[page[0]])
|
|
|
|
for page, page_no in zip(self.data, page_nos):
|
|
if titles[page[0]] > 1:
|
|
page[0] += " [%d/%d]" % (page_no, titles[page[0]])
|
|
|
|
|
|
class Pygmentizer:
|
|
def __init__(self, formatter, style):
|
|
import pygments
|
|
import pygments.lexers
|
|
import pygments.formatters
|
|
import pygments.styles
|
|
|
|
self.pygments = pygments
|
|
self.lexers = pygments.lexers
|
|
self.formatters = pygments.formatters
|
|
self.styles = pygments.styles
|
|
|
|
self.style = self.styles.get_style_by_name(style)
|
|
self.formatter = self.formatters.get_formatter_by_name(
|
|
formatter, style=self.style
|
|
)
|
|
self.lexer = None
|
|
|
|
def format(self, code):
|
|
in_block = False
|
|
blocks = []
|
|
current_block = -1
|
|
for i, row in enumerate(code):
|
|
if row.startswith("```"):
|
|
in_block = not in_block
|
|
lexer_name = row.replace("```", "").strip()
|
|
if len(lexer_name) == 0:
|
|
in_block = False
|
|
if in_block:
|
|
blocks.append({"lexer": lexer_name, "code": [], "rows": []})
|
|
current_block += 1
|
|
else:
|
|
if in_block:
|
|
blocks[current_block]["code"].append(row)
|
|
blocks[current_block]["rows"].append(i)
|
|
|
|
preformatted_rows = []
|
|
for block in blocks:
|
|
lexer = self.lexers.get_lexer_by_name(block["lexer"])
|
|
tokens = self.pygments.lex("\n".join(block["code"]), lexer)
|
|
formatted = self.pygments.format(tokens, self.formatter)
|
|
# ~ print(block["rows"])
|
|
# ~ print(formatted.split("\n"))
|
|
for row, formatted_row in zip(block["rows"], formatted.split("\n")):
|
|
code[row] = formatted_row
|
|
preformatted_rows.append(row)
|
|
return code, preformatted_rows
|
|
|
|
|
|
def get_interactive_help_text():
|
|
return """ left/right,page up/down,home,end
|
|
change page
|
|
c list contents (toc)
|
|
M modify file with VIM
|
|
q exit browser
|
|
r reload the presentation
|
|
s toggle status bar
|
|
t toggle timer (reqs. --timer switch)
|
|
,/. scroll page
|
|
up/down move highlight
|
|
enter execute highlighted line
|
|
h help"""
|
|
|
|
|
|
def setup_options():
|
|
"""Create command line options"""
|
|
usage = (
|
|
"""
|
|
MarkSlider: a markdown based slideshow engine
|
|
Special syntaxes:
|
|
* Colors: insert string ${C}, where C is one of KRGBYMCWkrgbymcwSUZ
|
|
* Text before first "# header" is not shown
|
|
* Text after a "# End of Slides" is not shown
|
|
* Execute shell: ` command -switch `! Beware of malicious code!
|
|
* Execute and print output: ` command `> Beware of malicious code!
|
|
* Convert images to ANSI, with climage: >
|
|
|
|
Keyboard shortcuts:
|
|
"""
|
|
+ get_interactive_help_text()
|
|
)
|
|
|
|
parser = ArgumentParser(
|
|
description=usage,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__author__,
|
|
)
|
|
parser.add_argument("-v", "--version", action="version", version=__version__)
|
|
parser.add_argument(
|
|
"--export",
|
|
action="store",
|
|
dest="screenshots",
|
|
default=False,
|
|
type=str,
|
|
help="Take screenshots of the slideshow in the given folder.",
|
|
)
|
|
|
|
content = parser.add_argument_group("content")
|
|
execution = parser.add_argument_group("execution")
|
|
control = parser.add_argument_group("controls")
|
|
|
|
content.add_argument(
|
|
"--background",
|
|
action="store_true",
|
|
dest="background",
|
|
default=False,
|
|
help="Use the rows before the first # header as slide background",
|
|
)
|
|
content.add_argument(
|
|
"--center",
|
|
action="store_true",
|
|
dest="center",
|
|
default=False,
|
|
help="Center slides on screen.",
|
|
)
|
|
content.add_argument(
|
|
"--dc",
|
|
action="store_true",
|
|
dest="dark_colors",
|
|
default=False,
|
|
help="Use dark colorscheme, better for white background terminals.",
|
|
)
|
|
content.add_argument(
|
|
"-m",
|
|
action="store_false",
|
|
dest="autocolor",
|
|
default=True,
|
|
help="Disable color by markdown structure.",
|
|
)
|
|
content.add_argument(
|
|
"--no-color",
|
|
"-n",
|
|
action="store_false",
|
|
dest="color",
|
|
default=True,
|
|
help="Disable color.",
|
|
)
|
|
content.add_argument(
|
|
"--no-rename",
|
|
"--nr",
|
|
action="store_false",
|
|
dest="rename_title",
|
|
default=True,
|
|
help="Disable automatic renaming of duplicate titles.",
|
|
)
|
|
content.add_argument(
|
|
"--toc",
|
|
action="store",
|
|
dest="toc",
|
|
default=False,
|
|
const="Table of Contents",
|
|
type=str,
|
|
nargs="?",
|
|
help="Insert table of contents. Define the header, or use default: %(const)s",
|
|
)
|
|
content.add_argument(
|
|
"--toc-page",
|
|
action="store",
|
|
dest="toc_page",
|
|
default=2,
|
|
type=int,
|
|
help="Insert table of contents on a chosen page. default: %(const)s",
|
|
)
|
|
content.add_argument(
|
|
"--toc-depth",
|
|
action="store",
|
|
dest="toc_depth",
|
|
default=2,
|
|
type=int,
|
|
choices=list(range(1, 5)),
|
|
help="Table of contents display depth. default: %(const)s",
|
|
)
|
|
|
|
content.add_argument(
|
|
"--no-pygments",
|
|
action="store_false",
|
|
dest="syntax_pygments",
|
|
default=True,
|
|
help="Disable autocoloring syntax with Pygments. Used only for ``` code blocks, if language is mentioned, ex.: ```python . Note! Do not have empty lines after ```",
|
|
)
|
|
content.add_argument(
|
|
"--syntax-formatter",
|
|
action="store",
|
|
dest="syntax_formatter",
|
|
default="terminal256",
|
|
choices=["terminal", "terminal256", "terminal16m"],
|
|
help="Syntax highlighter formatter. default: %(default)s",
|
|
)
|
|
content.add_argument(
|
|
"--syntax-style",
|
|
action="store",
|
|
dest="syntax_style",
|
|
default="monokai",
|
|
help="Syntax highlighter style, see Pygments styles. default: %(default)s",
|
|
)
|
|
|
|
execution.add_argument(
|
|
"-e",
|
|
action="store_true",
|
|
dest="execute",
|
|
default=False,
|
|
help="Execute commands in `! or `> tags at show time with Enter key. WARNING: Potentially very dangerous to run others' slides with this switch!",
|
|
)
|
|
execution.add_argument(
|
|
"-E",
|
|
action="store_true",
|
|
dest="execute_read",
|
|
default=False,
|
|
help="Execute commands in ``> tags at file read time. WARNING: Potentially very dangerous to run others' slides with this switch!",
|
|
)
|
|
|
|
control.add_argument(
|
|
"--exit",
|
|
action="store_true",
|
|
dest="exit_last",
|
|
default=False,
|
|
help="Exit after last slide.",
|
|
)
|
|
|
|
control.add_argument(
|
|
"-s",
|
|
action="store_false",
|
|
dest="menu",
|
|
default=True,
|
|
help="Disable status bar.",
|
|
)
|
|
control.add_argument(
|
|
"--timer",
|
|
action="store",
|
|
dest="slideTimer",
|
|
default=False,
|
|
type=int,
|
|
help="Timer for slideshow. If set, starts automatic slide changing.",
|
|
)
|
|
content.add_argument(
|
|
"-w",
|
|
action="store_false",
|
|
dest="wrap",
|
|
default=True,
|
|
help="Disable line wrapping. Cuts long lines.",
|
|
)
|
|
|
|
parser.add_argument("files", type=str, nargs="+", help="File(s) to show")
|
|
opts = parser.parse_args()
|
|
opts.slideShow = not not opts.slideTimer
|
|
if opts.screenshots:
|
|
opts.slideShow = True
|
|
opts.slideTimer = 1
|
|
opts.exit_last = True
|
|
return opts
|
|
|
|
|
|
def page_print(reader, opts, offset):
|
|
"""Print a page"""
|
|
|
|
page = reader.get_current_page()
|
|
scrsize = opts.size
|
|
# clear page
|
|
bc.clear()
|
|
if opts.center: # Placeholder for 80x25 center alignment
|
|
align_width = reader.get_max_width()
|
|
align_x_offset = int(scrsize[1] / 2 - align_width / 2)
|
|
align_pad = " " * align_x_offset
|
|
align_y_offset = int(scrsize[0] / 2 - reader.get_max_height() / 2)
|
|
bc.down_line(align_y_offset)
|
|
else:
|
|
align_pad = ""
|
|
# Print header
|
|
if opts.dark_colors:
|
|
coloring = "${b}${U}"
|
|
else:
|
|
coloring = "${U}${Y}"
|
|
print(align_pad + colorify(coloring + page[0], opts) + bc.Z)
|
|
if opts.background:
|
|
bc.save()
|
|
if opts.color:
|
|
sys.stdout.write(
|
|
"\n".join([align_pad + bc.color_string(x) for x in reader.background])
|
|
)
|
|
else:
|
|
sys.stdout.write(
|
|
"\n".join([align_pad + bc.nocolor_string(x) for x in reader.background])
|
|
)
|
|
bc.restore()
|
|
if sys.version_info < (3, 0):
|
|
# python2 magic
|
|
page = [row.decode("utf-8") for row in page]
|
|
# Print page rows
|
|
if not opts.wrap:
|
|
page = [cut_line(row, scrsize[1] - 1) for row in page]
|
|
parsed = md_color.parse(page)
|
|
if opts.autocolor:
|
|
if reader.pygments:
|
|
|
|
# ~ to_pygmentize = []
|
|
# ~ pygmented_rows = []
|
|
# ~ for token_i in range(len(parsed)):
|
|
# ~ if parsed[token_i][0] == 'multiline_code':
|
|
# ~ to_pygmentize.append(parsed[token_i][1])
|
|
# ~ pygmented_rows.append(token_i)
|
|
# ~ if len(to_pygmentize) > 0:
|
|
preformatted, preformatted_rows = reader.pygments.format(
|
|
[x[1] for x in parsed]
|
|
)
|
|
|
|
for row_i in preformatted_rows:
|
|
parsed[row_i][0] = "preformatted"
|
|
parsed[row_i][1] = preformatted[row_i]
|
|
colored = md_color.colorize(parsed, not opts.color, opts.dark_colors)
|
|
else:
|
|
if opts.color:
|
|
colored = [bc.color_string(row[1]) for row in parsed]
|
|
else:
|
|
colored = [bc.nocolor_string(row[1]) for row in parsed]
|
|
r = 0
|
|
for row_i in range(len(page)):
|
|
if row_i == 0:
|
|
continue
|
|
if row_i < offset[0]:
|
|
continue
|
|
row = page[row_i]
|
|
# page[1+offset[0]:]:
|
|
if opts.wrap:
|
|
row_lines = int(float(len(row)) / scrsize[1])
|
|
else:
|
|
row_lines = 0
|
|
row_lines = 0
|
|
colored_row = colored[row_i] # =colorify(row,opts)
|
|
if offset[1] == r + 1 + offset[0]:
|
|
colored_row = add_highlight(row, opts)
|
|
sys.stdout.write(align_pad + colored_row)
|
|
|
|
if r >= scrsize[0] - 2:
|
|
break
|
|
r += row_lines + 1
|
|
sys.stdout.write("\n")
|
|
sys.stdout.flush()
|
|
return
|
|
|
|
|
|
def print_menu(reader, opts):
|
|
|
|
bc.posprint(
|
|
opts.size[0],
|
|
0,
|
|
colorify(
|
|
"${y}%d${Z}/%d %s|%s"
|
|
% (
|
|
1 + reader.get_page_no(),
|
|
reader.get_pages(),
|
|
os.path.basename(reader.get_current_filename()),
|
|
"slideshow" if opts.slideShow else "",
|
|
),
|
|
opts,
|
|
),
|
|
)
|
|
|
|
|
|
def print_time(opts):
|
|
now = datetime.datetime.now()
|
|
bc.posprint(
|
|
opts.size[0],
|
|
opts.size[1] - 5,
|
|
colorify("%02d:%02d" % (now.hour, now.minute), opts),
|
|
)
|
|
|
|
|
|
def print_help(reader, opts):
|
|
"""Create a window with help message"""
|
|
helptext = get_interactive_help_text().split("\n")
|
|
maxlen = max([len(x) for x in helptext])
|
|
bc.posprint(3, 5, " +" + "-" * maxlen + "+ ")
|
|
bc.posprint(
|
|
4,
|
|
5,
|
|
colorify(
|
|
" |${U}${Y}" + ("{:^" + str(maxlen) + "}").format("Help") + "${Z}| ", opts
|
|
),
|
|
)
|
|
for y, row in enumerate(helptext):
|
|
bc.posprint(5 + y, 5, (" |{:<" + str(maxlen) + "}| ").format(row))
|
|
bc.posprint(6 + y, 5, " +" + "-" * maxlen + "+ ")
|
|
sys.stdout.write(bc.pos(opts.size[0], opts.size[1]))
|
|
inkey = getch.get()
|
|
|
|
|
|
def print_toc(reader, opts):
|
|
"""Create a window with TOC"""
|
|
text = reader.get_toc(display_position=True)
|
|
title = opts.toc if opts.toc else "Table of Contents"
|
|
maxlen = max([len(x) for x in text])
|
|
bc.posprint(3, 2, " +" + "-" * maxlen + "+ ")
|
|
parsed = md_color.parse(text)
|
|
if opts.autocolor:
|
|
colored = md_color.colorize(parsed, not opts.color, opts.dark_colors)
|
|
else:
|
|
if opts.color:
|
|
|
|
colored = [bc.color_string(row[1]) for row in parsed]
|
|
else:
|
|
colored = [bc.nocolor_string(row[1]) for row in parsed]
|
|
for y, row in enumerate(colored):
|
|
bc.posprint(4 + y, 2, (" |{:<" + str(maxlen) + "}| ").format(" "))
|
|
bc.posprint(4 + y, 3, ("|{:<" + str(maxlen) + "}").format(row))
|
|
bc.posprint(5 + y, 2, " +" + "-" * maxlen + "+ ")
|
|
sys.stdout.write(bc.pos(opts.size[0], opts.size[1]))
|
|
inkey = getch.get()
|
|
|
|
|
|
def offset_change(opts, reader, offset, new_offset):
|
|
"""Change the display position of page"""
|
|
new_offset = (offset[0] + new_offset[0], offset[1] + new_offset[1])
|
|
offsety = min(reader.get_page_height() - 1, new_offset[0])
|
|
offseth = min(reader.get_page_height(), new_offset[1])
|
|
return [max(0, o) for o in (offsety, offseth)]
|
|
|
|
|
|
def timeouthandler(sig, frame):
|
|
# ~ print(sig,frame)
|
|
raise IOError("Input timeout")
|
|
|
|
|
|
def getkeypress():
|
|
try:
|
|
return ord(getch.get())
|
|
except:
|
|
return False
|
|
|
|
|
|
def browser(opts, files):
|
|
"""Main function for printing"""
|
|
|
|
try:
|
|
reader = slide_reader(files, opts)
|
|
except:
|
|
print("Error in reading the file:")
|
|
for o in sys.exc_info():
|
|
print(o)
|
|
sys.exit(1)
|
|
offset = (0, 0)
|
|
try:
|
|
while 1:
|
|
opts.size = get_console_size()
|
|
page_print(reader, opts, offset)
|
|
if opts.menu:
|
|
print_menu(reader, opts)
|
|
print_time(opts)
|
|
sys.stdout.write(bc.pos(opts.size[0], opts.size[1]))
|
|
sys.stdout.flush()
|
|
if opts.screenshots:
|
|
take_screenshot(reader, opts)
|
|
while True:
|
|
if opts.slideTimer and opts.slideShow:
|
|
signal.signal(signal.SIGALRM, timeouthandler)
|
|
signal.alarm(opts.slideTimer)
|
|
elif opts.menu:
|
|
signal.signal(signal.SIGALRM, timeouthandler)
|
|
signal.alarm(15)
|
|
inkey = getkeypress()
|
|
signal.alarm(0)
|
|
if not inkey and not opts.slideShow and opts.menu:
|
|
# normal operation, just update the time
|
|
print_time(opts)
|
|
continue
|
|
if not inkey and opts.slideShow:
|
|
# slideshow mode
|
|
if opts.exit_last:
|
|
if reader.page + 1 == reader.pages:
|
|
return
|
|
reader.inc_page_no(1, True)
|
|
offset = (0, 0)
|
|
break
|
|
# ~ print(inkey)
|
|
if inkey in [113, 3, 120]:
|
|
# print('Exited in: ')
|
|
return
|
|
if inkey in [67, 54, 32]: # PGDN or space
|
|
if opts.exit_last:
|
|
if reader.page + 1 == reader.pages:
|
|
return
|
|
reader.inc_page_no(1)
|
|
offset = (0, 0)
|
|
if inkey in [68, 53, 127]:
|
|
reader.inc_page_no(-1)
|
|
offset = (0, 0)
|
|
if inkey == 72 or inkey == 49: # HOME
|
|
reader.first_page()
|
|
offset = (0, 0)
|
|
if inkey == 70 or inkey == 52: # END
|
|
reader.last_page()
|
|
offset = (0, 0)
|
|
if inkey == ord("c"):
|
|
print_toc(reader, opts)
|
|
if inkey == ord("h"):
|
|
print_help(reader, opts)
|
|
if inkey == ord("s"):
|
|
opts.menu = not opts.menu
|
|
if inkey == ord("t"):
|
|
opts.slideShow = not opts.slideShow
|
|
if inkey == ord("r"):
|
|
reader.read()
|
|
offset = offset_change(opts, reader, offset, (0, 0))
|
|
if inkey == ord("M"):
|
|
modify_file(reader, offset)
|
|
reader.read()
|
|
offset = offset_change(opts, reader, offset, (0, 0))
|
|
if inkey == ord(","):
|
|
offset = offset_change(opts, reader, offset, (-1, 0))
|
|
if inkey == ord("."):
|
|
offset = offset_change(opts, reader, offset, (1, 0))
|
|
if inkey == 65: # up
|
|
offset = offset_change(opts, reader, offset, (0, -1))
|
|
if inkey == 66: # down
|
|
offset = offset_change(opts, reader, offset, (0, 1))
|
|
if inkey == 13: # enter
|
|
launch(reader, opts, offset)
|
|
break
|
|
|
|
except IOError:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
sys.exit(0)
|
|
except EndProgram:
|
|
pass
|
|
except:
|
|
print("Unexpected error:")
|
|
print(traceback.format_exc())
|
|
sys.exit(1)
|
|
|
|
|
|
def get_console_size():
|
|
rows, columns = os.popen("stty size", "r").read().split()
|
|
return (int(rows), int(columns))
|
|
|
|
|
|
def colorify(s, opts):
|
|
"""Add colors to string"""
|
|
if not opts.color:
|
|
return bc.nocolor_string(s)
|
|
c = bc.color_string(s) # +bc.Z
|
|
return c
|
|
|
|
|
|
def cut_line(s, i):
|
|
"""cut a color tagged string, and remove control chars"""
|
|
s = s[:i]
|
|
s = re.sub(r"\$$", "", re.sub(r"\$\{$", "", re.sub(r"\$\{.$", "", s)))
|
|
return s
|
|
|
|
|
|
def add_highlight(s, opts):
|
|
"""Add cursor position highlight"""
|
|
if len(s.strip()) == 0:
|
|
cleaned = HL
|
|
else:
|
|
cleaned = bc.nocolor_string(s)
|
|
tagged = "${Y}" + cleaned + "${Z}"
|
|
return colorify(tagged, opts)
|
|
|
|
|
|
def launch(reader, opts, offset):
|
|
"""Launch in a string using tags $!command$! or $>command$>
|
|
Remove empty lines from beginning and end of stdout in $> commands.
|
|
Detects URLS and markdown images 
|
|
"""
|
|
if not opts.execute:
|
|
bc.posprint(offset[1] - offset[0] + 1, 0, "Execution not enabled!")
|
|
inkey = getch.get()
|
|
return
|
|
s = reader.get_current_page()[offset[1]]
|
|
urls = re.findall(
|
|
r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
|
|
s,
|
|
)
|
|
images = re.findall(r"!\[[^]]+\]\([^\)]+\)", s)
|
|
# sanity check
|
|
if s.find("`") == -1 and len(urls) == 0 and len(images) == 0:
|
|
return
|
|
|
|
run_command = re.match(r"(.*)`(.*)`!(.*)", s)
|
|
show_command = re.match(r"(.*)`(.*)`>(.*)", s)
|
|
if show_command != None:
|
|
output = subprocess.check_output(show_command.group(2).strip(), shell=True)
|
|
if type(output) == bytes:
|
|
output = output.decode("utf-8")
|
|
output = output.split("\n")
|
|
while len(output[0].strip()) == 0:
|
|
if len(output) == 1:
|
|
return
|
|
del output[0]
|
|
while len(output[-1].strip()) == 0:
|
|
if len(output) == 1:
|
|
return
|
|
del output[-1]
|
|
for y, l in enumerate(output):
|
|
bc.posprint(y + offset[1] - offset[0] + 2, 0, " " * len(l))
|
|
bc.clear_to_end()
|
|
bc.posprint(y + offset[1] - offset[0] + 2, 0, l)
|
|
inkey = getch.get()
|
|
return
|
|
if run_command != None:
|
|
subprocess.call(run_command.group(2), shell=True, executable="/bin/bash")
|
|
inkey = getch.get()
|
|
return
|
|
# Open URLS last
|
|
if len(urls) > 0:
|
|
# Remove ) at the end of url: [name](link) markdown syntax
|
|
subprocess.call(
|
|
"%s '%s' &"
|
|
% (
|
|
get_open_command(),
|
|
urls[0].rstrip(")"),
|
|
),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
shell=True,
|
|
)
|
|
return
|
|
if len(images) > 0:
|
|
image = re.sub(r".*\(([^\)]+)\).*", "\\1", images[0])
|
|
subprocess.call(
|
|
"%s '%s' &"
|
|
% (
|
|
get_open_command(),
|
|
image,
|
|
),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
shell=True,
|
|
)
|
|
return
|
|
return
|
|
|
|
|
|
def modify_file(reader, offset):
|
|
row = 1
|
|
row_restarts = reader.file_start_page
|
|
for page in range(reader.page):
|
|
if opts.toc_page == page + 1 and opts.toc:
|
|
continue
|
|
row += len(reader.data[page])
|
|
if (page + 1) in row_restarts:
|
|
row = 1
|
|
subprocess.call(
|
|
"vim +%d -c 'exe \"normal! zt\"' -c %d %s"
|
|
% (row, row + offset[1], reader.get_current_filename()),
|
|
shell=True,
|
|
)
|
|
|
|
|
|
def take_screenshot(reader, opts):
|
|
out_file = os.path.join(opts.screenshots, "slide%03d.png" % (reader.page + 1,))
|
|
if not os.path.exists(opts.screenshots):
|
|
os.mkdir(opts.screenshots)
|
|
subprocess.call(
|
|
"sleep 0.5; import -window %s '%s'" % (opts.xwinid, out_file),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
shell=True,
|
|
)
|
|
|
|
|
|
def get_open_command():
|
|
if sys.platform.startswith("darwin"):
|
|
return "open"
|
|
else:
|
|
return "xdg-open"
|
|
|
|
|
|
def main():
|
|
global bc
|
|
global getch
|
|
global opts
|
|
bc = ansicodes.code()
|
|
getch = getch()
|
|
opts = setup_options()
|
|
if opts.screenshots:
|
|
print("Click the terminal window to read window ID")
|
|
xwininfo = subprocess.check_output("xwininfo", shell=True)
|
|
if type(xwininfo) == bytes:
|
|
xwininfo = xwininfo.decode("utf-8")
|
|
xwininfo = re.search(r"Window id: (0x[0-9]+)", xwininfo)
|
|
if xwininfo:
|
|
opts.xwinid = xwininfo.group(1)
|
|
else:
|
|
print("Cannot parse window ID")
|
|
sys.exit(1)
|
|
browser(opts, opts.files)
|
|
print("\n\n")
|
|
if opts.screenshots:
|
|
print(
|
|
"Crop the images for terminal tabs, and PDFize e.g.:\n- mogrify -chop 0x50 %s/*png\n- convert %s/*png %s.pdf"
|
|
% (
|
|
opts.screenshots,
|
|
opts.screenshots,
|
|
os.path.basename(opts.screenshots),
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|