aboutsummaryrefslogblamecommitdiffstats
path: root/epy.py
blob: fca379faa156b1d1b9f64b81e0d6154f3f8de283 (plain) (tree)
1
2
3
4
5
6
7


                      



                                                     




                                          
                                          


   
                         













                                         
                 


                                  
                            

                                         
                           

 






                                     












                             
                        




                                

                              



                          
 
         
                   

                












                                                               

                            








                              
               

              



                    
             
               
             
                                             






































































































                                                                             

                                          










                                                 
                           



                             
                           






                                               

                              









































                                                                       



                                    














                                     



                                                         






                                                 

                                                 













                                                                       
                                                                

                                  
                                                 


                                                                    





                                                                          
                 
                                                      


                                    
















































                                                                        
                                                                









                                                       





































































                                                                                
                                                                    























                                                                         
                

                                         
                                     




                                                            
                                              
                                                               
         
                            
                              


                                                  
 



                                  
                                

                             

 
                 
                  

                                         
                                              

 
                                             

                                   




                                          



























                                                
           
                    
                                                    

 
         
                
              

                                          
                                                  
                                     

                                                                    

 
         
           
                                                         
                          
                                

                                                      

 










                                            

                                               




























                                                  

                                                       








                             















                                


                                                      



                                  









                          

                                           

                     


                      






                                           






                                           





                       
                                           
                        
                                  

                             
                                          





                                           


















                                                                     
                                  
                                        

                                    







                                                         

                              



                                                                      
                                




















                                                        
                          


                                                          

                            



















































                                                                           


                                                       














                                                             
                                               


                                                
                                  



















                                                        
                                                            









                                                               
                                  





                                            
                                                                        

                               

                                                      
                    
                                                       
        
                                                                   

                        
 


                                                
 

                    




                                                       




                                  











                                                         
                         









                                                                          
                         
































                                                                                          
                                                     































                                                                                          
                                












                                                                                          
                         






















                                                               
                         








































                                                                                                   
                                                
















                                                                                                           











                                                                                   

                                                              




                                                             
 


                                                                                   
 















                                                                      


                          
                                                         



                                   

                                                                     



                             


                       
                      



                                          


                     



                                                   
         
                                       


                  

               

                        
                                                           


                       
                
 











                                                 
                                                                                 



                                           
                                          









                                              
                               
                  
 
               

                                                       
                                     


                        
                                










                                               
                  


                                    





                                                       
                   
                  


                  
                                        









                                                               


                                                                  
 
                       
                                  

                                         
 

                                                                       
                                                                   
                          
                              


                                                                      
                                     

                                                          
                                         
                                                                       
                            
























                                                                     
#!/usr/bin/env python3
"""\
Usages:
    epy             read last epub
    epy EPUBFILE    read EPUBFILE
    epy STRINGS     read matched STRINGS from history
    epy NUMBER      read file from history
                    with associated NUMBER

Options:
    -r              print reading history
    -d              dump epub
    -h, --help      print short, long help
"""


__version__ = "2020.4.15"
__license__ = "MIT"
__author__ = "Benawi Adha"
__url__ = "https://github.com/wustho/epy"


import curses
import zipfile
import sys
import re
import os
import textwrap
import json
import tempfile
import shutil
import subprocess
import xml.etree.ElementTree as ET
from urllib.parse import unquote
from html import unescape
# from subprocess import run
from html.parser import HTMLParser
from difflib import SequenceMatcher as SM
from functools import wraps


# -1 is default terminal fg/bg colors
CFG = {
    "DefaultViewer": "Default",
    "EnableProgressIndicator": True,
    "DarkColorFG": 252,
    "DarkColorBG": 235,
    "LightColorFG": 238,
    "LightColorBG": 253,
    "Keys": {
        "ScrollUp": "k",
        "ScrollDown": "j",
        "PageUp": "h",
        "PageDown": "l",
        "NextChapter": "n",
        "PrevChapter": "p",
        "BeginningOfCh": "g",
        "EndOfCh": "G",
        "Shrink": "-",
        "Enlarge": "+",
        "SetWidth": "=",
        "Metadata": "M",
        "ToC": "t",
        "Follow": "f",
        "OpenImage": "o",
        "RegexSearch": "/",
        "ShowHideProgress": "s",
        "MarkPosition": "m",
        "JumpToPosition": "`",
        "Quit": "q",
        "Help": "?",
        "SwitchColor": "c"
    }
}
STATE = {
    "LastRead": "",
    "States": {}
}
K = {
    "ScrollUp": {curses.KEY_UP},
    "ScrollDown": {curses.KEY_DOWN},
    "PageUp": {curses.KEY_PPAGE, curses.KEY_LEFT},
    "PageDown": {curses.KEY_NPAGE, ord(" "), curses.KEY_RIGHT},
    "NextChapter": set(),
    "PrevChapter": set(),
    "BeginningOfCh": {curses.KEY_HOME},
    "EndOfCh": {curses.KEY_END},
    "Shrink": set(),
    "Enlarge": set(),
    "SetWidth": set(),
    "Metadata": set(),
    "MarkPosition": set(),
    "JumpToPosition": set(),
    "ToC": {9, ord("\t")},
    "Follow": {10},
    "OpenImage": set(),
    "RegexSearch": set(),
    "ShowHideProgress": set(),
    "Quit": {3, 27, 304},
    "Help": set(),
    "SwitchColor": set()
}
WINKEYS = set()
CFGFILE = ""
STATEFILE = ""
COLORSUPPORT = False
LINEPRSRV = 0  # 2
SEARCHPATTERN = None
VWR = None
SCREEN = None
PERCENTAGE = []
JUMPLIST = {}
SHOWPROGRESS = CFG["EnableProgressIndicator"]


class Epub:
    NS = {
        "DAISY": "http://www.daisy.org/z3986/2005/ncx/",
        "OPF": "http://www.idpf.org/2007/opf",
        "CONT": "urn:oasis:names:tc:opendocument:xmlns:container",
        "XHTML": "http://www.w3.org/1999/xhtml",
        "EPUB": "http://www.idpf.org/2007/ops"
    }

    def __init__(self, fileepub):
        self.path = os.path.abspath(fileepub)
        self.file = zipfile.ZipFile(fileepub, "r")
        cont = ET.parse(self.file.open("META-INF/container.xml"))
        self.rootfile = cont.find(
            "CONT:rootfiles/CONT:rootfile",
            self.NS
        ).attrib["full-path"]
        self.rootdir = os.path.dirname(self.rootfile)\
            + "/" if os.path.dirname(self.rootfile) != "" else ""
        cont = ET.parse(self.file.open(self.rootfile))
        # EPUB3
        self.version = cont.getroot().get("version")
        if self.version == "2.0":
            # "OPF:manifest/*[@id='ncx']"
            self.toc = self.rootdir\
                + cont.find(
                    "OPF:manifest/*[@media-type='application/x-dtbncx+xml']",
                    self.NS
                ).get("href")
        elif self.version == "3.0":
            self.toc = self.rootdir\
                + cont.find(
                    "OPF:manifest/*[@properties='nav']",
                    self.NS
                ).get("href")

        self.contents = []
        self.toc_entries = [[], [], []]

    def get_meta(self):
        meta = []
        # why self.file.read(self.rootfile) problematic
        cont = ET.fromstring(self.file.open(self.rootfile).read())
        for i in cont.findall("OPF:metadata/*", self.NS):
            if i.text is not None:
                meta.append([re.sub("{.*?}", "", i.tag), i.text])
        return meta

    def initialize(self):
        cont = ET.parse(self.file.open(self.rootfile)).getroot()
        manifest = []
        for i in cont.findall("OPF:manifest/*", self.NS):
            # EPUB3
            # if i.get("id") != "ncx" and i.get("properties") != "nav":
            if i.get("media-type") != "application/x-dtbncx+xml"\
               and i.get("properties") != "nav":
                manifest.append([
                    i.get("id"),
                    i.get("href")
                ])

        spine, contents = [], []
        for i in cont.findall("OPF:spine/*", self.NS):
            spine.append(i.get("idref"))
        for i in spine:
            for j in manifest:
                if i == j[0]:
                    self.contents.append(self.rootdir+unquote(j[1]))
                    contents.append(unquote(j[1]))
                    manifest.remove(j)
                    # TODO: test is break necessary
                    break

        toc = ET.parse(self.file.open(self.toc)).getroot()
        # EPUB3
        if self.version == "2.0":
            navPoints = toc.findall("DAISY:navMap//DAISY:navPoint", self.NS)
        elif self.version == "3.0":
            navPoints = toc.findall(
                "XHTML:body//XHTML:nav[@EPUB:type='toc']//XHTML:a",
                self.NS
            )
        for i in navPoints:
            if self.version == "2.0":
                src = i.find("DAISY:content", self.NS).get("src")
                name = i.find("DAISY:navLabel/DAISY:text", self.NS).text
            elif self.version == "3.0":
                src = i.get("href")
                name = "".join(list(i.itertext()))
            src = src.split("#")
            idx = contents.index(unquote(src[0]))
            self.toc_entries[0].append(name)
            self.toc_entries[1].append(idx)
            if len(src) == 2:
                self.toc_entries[2].append(src[1])
            elif len(src) == 1:
                self.toc_entries[2].append("")


class HTMLtoLines(HTMLParser):
    para = {"p", "div"}
    inde = {"q", "dt", "dd", "blockquote"}
    pref = {"pre"}
    bull = {"li"}
    hide = {"script", "style", "head"}
    # hide = {"script", "style", "head", ", "sub}

    def __init__(self, sects={""}):
        HTMLParser.__init__(self)
        self.text = [""]
        self.imgs = []
        self.ishead = False
        self.isinde = False
        self.isbull = False
        self.ispref = False
        self.ishidden = False
        self.idhead = set()
        self.idinde = set()
        self.idbull = set()
        self.idpref = set()
        self.sects = sects

    def handle_starttag(self, tag, attrs):
        if re.match("h[1-6]", tag) is not None:
            self.ishead = True
        elif tag in self.inde:
            self.isinde = True
        elif tag in self.pref:
            self.ispref = True
        elif tag in self.bull:
            self.isbull = True
        elif tag in self.hide:
            self.ishidden = True
        elif tag == "sup":
            self.text[-1] += "^{"
        elif tag == "sub":
            self.text[-1] += "_{"
        elif tag == "image":
            for i in attrs:
                if i[0] == "xlink:href":
                    self.text.append("[IMG:{}]".format(len(self.imgs)))
                    self.imgs.append(unquote(i[1]))
        if self.sects != {""}:
            for i in attrs:
                if i[1] in self.sects:
                    self.text[-1] += " (#" + i[1] + ") "

    def handle_startendtag(self, tag, attrs):
        if tag == "br":
            self.text += [""]
        elif tag in {"img", "image"}:
            for i in attrs:
                if (tag == "img" and i[0] == "src")\
                   or (tag == "image" and i[0] == "xlink:href"):
                    self.text.append("[IMG:{}]".format(len(self.imgs)))
                    self.imgs.append(unquote(i[1]))
                    self.text.append("")

    def handle_endtag(self, tag):
        if re.match("h[1-6]", tag) is not None:
            self.text.append("")
            self.text.append("")
            self.ishead = False
        elif tag in self.para:
            self.text.append("")
        elif tag in self.hide:
            self.ishidden = False
        elif tag in self.inde:
            if self.text[-1] != "":
                self.text.append("")
            self.isinde = False
        elif tag in self.pref:
            if self.text[-1] != "":
                self.text.append("")
            self.ispref = False
        elif tag in self.bull:
            if self.text[-1] != "":
                self.text.append("")
            self.isbull = False
        elif tag in {"sub", "sup"}:
            self.text[-1] += "}"
        elif tag == "image":
            self.text.append("")

    def handle_data(self, raw):
        if raw and not self.ishidden:
            if self.text[-1] == "":
                tmp = raw.lstrip()
            else:
                tmp = raw
            if self.ispref:
                line = unescape(tmp)
            else:
                line = unescape(re.sub(r"\s+", " ", tmp))
            self.text[-1] += line
            if self.ishead:
                self.idhead.add(len(self.text)-1)
            elif self.isbull:
                self.idbull.add(len(self.text)-1)
            elif self.isinde:
                self.idinde.add(len(self.text)-1)
            elif self.ispref:
                self.idpref.add(len(self.text)-1)

    def get_lines(self, width=0):
        text, sect = [], {}
        if width == 0:
            return self.text
        for n, i in enumerate(self.text):
            findsect = re.search(r"(?<= \(#).*?(?=\) )", i)
            if findsect is not None and findsect.group() in self.sects:
                i = i.replace(" (#" + findsect.group() + ") ", "")
                sect[findsect.group()] = len(text)
            if n in self.idhead:
                text += [i.rjust(width//2 + len(i)//2)] + [""]
            elif n in self.idinde:
                text += [
                    "   "+j for j in textwrap.wrap(i, width - 3)
                ] + [""]
            elif n in self.idbull:
                tmp = textwrap.wrap(i, width - 3)
                text += [
                    " - "+j if j == tmp[0] else "   "+j for j in tmp
                ] + [""]
            elif n in self.idpref:
                tmp = i.splitlines()
                wraptmp = []
                for line in tmp:
                    wraptmp += [j for j in textwrap.wrap(line, width - 6)]
                text += ["   "+j for j in wraptmp] + [""]
            else:
                text += textwrap.wrap(i, width) + [""]
        return text, self.imgs, sect


def text_win(textfunc):
    @wraps(textfunc)
    def wrapper(*args, **kwargs):
        rows, cols = SCREEN.getmaxyx()
        hi, wi = rows - 4, cols - 4
        Y, X = 2, 2
        textw = curses.newwin(hi, wi, Y, X)
        if COLORSUPPORT:
            textw.bkgd(SCREEN.getbkgd())

        title, raw_texts, key = textfunc(*args, **kwargs)

        texts = []
        for i in raw_texts.splitlines():
            texts += textwrap.wrap(i, wi - 6)

        textw.box()
        textw.keypad(True)
        textw.addstr(1, 2, title)
        textw.addstr(2, 2, "-"*len(title))
        key_textw = 0

        totlines = len(texts)

        pad = curses.newpad(totlines, wi - 2)
        if COLORSUPPORT:
            pad.bkgd(SCREEN.getbkgd())

        pad.keypad(True)
        for n, i in enumerate(texts):
            pad.addstr(n, 0, i)
        y = 0
        textw.refresh()
        pad.refresh(y, 0, Y+4, X+4, rows - 5, cols - 6)
        padhi = rows - 8 - Y

        while key_textw not in K["Quit"]|key:
            if key_textw in K["ScrollUp"] and y > 0:
                y -= 1
            elif key_textw in K["ScrollDown"] and y < totlines - hi + 6:
                y += 1
            elif key_textw in K["PageUp"]:
                y = pgup(y, padhi)
            elif key_textw in K["PageDown"]:
                y = pgdn(y, totlines, padhi)
            elif key_textw in K["BeginningOfCh"]:
                y = 0
            elif key_textw in K["EndOfCh"]:
                y = pgend(totlines, padhi)
            elif key_textw in {curses.KEY_RESIZE}|WINKEYS - key:
                return key_textw
            pad.refresh(y, 0, 6, 5, rows - 5, cols - 5)
            key_textw = textw.getch()

        textw.clear()
        textw.refresh()
        return
    return wrapper


def choice_win(listgen):
    @wraps(listgen)
    def wrapper(*args, **kwargs):
        rows, cols = SCREEN.getmaxyx()
        hi, wi = rows - 4, cols - 4
        Y, X = 2, 2
        chwin = curses.newwin(hi, wi, Y, X)
        if COLORSUPPORT:
            chwin.bkgd(SCREEN.getbkgd())

        title, ch_list, index, key = listgen(*args, **kwargs)

        chwin.box()
        chwin.keypad(True)
        chwin.addstr(1, 2, title)
        chwin.addstr(2, 2, "-"*len(title))
        key_chwin = 0

        totlines = len(ch_list)
        chwin.refresh()
        pad = curses.newpad(totlines, wi - 2)
        if COLORSUPPORT:
            pad.bkgd(SCREEN.getbkgd())

        pad.keypad(True)

        padhi = rows - 5 - Y - 4 + 1
        y = 0
        if index in range(padhi//2, totlines - padhi//2):
            y = index - padhi//2 + 1
        span = []

        for n, i in enumerate(ch_list):
            # strs = "  " + str(n+1).rjust(d) + " " + i[0]
            strs = "  " + i
            strs = strs[0:wi-3]
            pad.addstr(n, 0, strs)
            span.append(len(strs))

        countstring = ""
        while key_chwin not in K["Quit"]|key:
            if countstring == "":
                count = 1
            else:
                count = int(countstring)
            if key_chwin in range(48, 58): # i.e., k is a numeral
                countstring = countstring + chr(key_chwin)
            else:
                if key_chwin in K["ScrollUp"] or key_chwin in K["PageUp"]:
                    index -= count
                    if index < 0:
                        index = 0
                elif key_chwin in K["ScrollDown"] or key_chwin in K["PageDown"]:
                    index += count
                    if index + 1 >= totlines:
                        index = totlines - 1
                elif key_chwin in K["Follow"]:
                    return index
                # elif key_chwin in K["PageUp"]:
                #     index -= 3
                #     if index < 0:
                #         index = 0
                # elif key_chwin in K["PageDown"]:
                #     index += 3
                #     if index >= totlines:
                #         index = totlines - 1
                elif key_chwin in K["BeginningOfCh"]:
                    index = 0
                elif key_chwin in K["EndOfCh"]:
                    index = totlines - 1
                elif key_chwin in {curses.KEY_RESIZE}|WINKEYS - key:
                    return key_chwin
                countstring = ""

            while index not in range(y, y+padhi):
                if index < y:
                    y -= 1
                else:
                    y += 1

            for n in range(totlines):
                att = curses.A_REVERSE if index == n else curses.A_NORMAL
                pre = ">>" if index == n else "  "
                pad.addstr(n, 0, pre)
                pad.chgat(n, 0, span[n], pad.getbkgd() | att)

            pad.refresh(y, 0, Y+4, X+4, rows - 5, cols - 6)
            key_chwin = chwin.getch()

        chwin.clear()
        chwin.refresh()
        return
    return wrapper


def loadstate():
    global CFG, STATE, CFGFILE, STATEFILE
    prefix = ""
    if os.getenv("HOME") is not None:
        homedir = os.getenv("HOME")
        if os.path.isdir(os.path.join(homedir, ".config")):
            prefix = os.path.join(homedir, ".config", "epy")
        else:
            prefix = os.path.join(homedir, ".epy")
    elif os.getenv("USERPROFILE") is not None:
        prefix = os.path.join(os.getenv("USERPROFILE"), ".epy")
    else:
        CFGFILE = os.devnull
        STATEFILE = os.devnull
    os.makedirs(prefix, exist_ok=True)
    CFGFILE = os.path.join(prefix, "config.json")
    STATEFILE = os.path.join(prefix, "state.json")

    try:
        with open(CFGFILE) as f:
            CFG = json.load(f)
        with open(STATEFILE) as f:
            STATE = json.load(f)
    except FileNotFoundError:
        pass


def parse_keys():
    global WINKEYS
    for i in K.keys():
        K[i] = K[i]|{ord(CFG["Keys"][i])}
    WINKEYS = K["Metadata"]|K["Help"]|K["ToC"]


def savestate(file, index, width, pos, pctg):
    with open(CFGFILE, "w") as f:
        json.dump(CFG, f, indent=2)
    STATE["LastRead"] = file
    STATE["States"][file]["index"] = index
    STATE["States"][file]["width"] = width
    STATE["States"][file]["pos"] = pos
    STATE["States"][file]["pctg"] = pctg
    with open(STATEFILE, "w") as f:
        json.dump(STATE, f, indent=4)


def pgup(pos, winhi, preservedline=0, c=1):
    if pos >= (winhi - preservedline) * c:
        return pos - (winhi + preservedline) * c
    else:
        return 0


def pgdn(pos, tot, winhi, preservedline=0, c=1):
    if pos + (winhi * c) <= tot - winhi:
        return pos + (winhi * c)
    else:
        pos = tot - winhi
        if pos < 0:
            return 0
        return pos


def pgend(tot, winhi):
    if tot - winhi >= 0:
        return tot - winhi
    else:
        return 0


@choice_win
def toc(src, index):
    return "Table of Contents", src, index, K["ToC"]


@text_win
def meta(ebook):
    mdata = ""
    for i in ebook.get_meta():
        data = re.sub("<[^>]*>", "", i[1])
        mdata += i[0].upper() + ": " + data + "\n"
        data = re.sub("\t", "", data)
        # mdata += textwrap.wrap(i[0].upper() + ": " + data, wi - 6)
    return "Metadata", mdata, K["Metadata"]


@text_win
def help():
    # src = re.search("Key Bind(\n|.)*", __doc__).group()
    src = "Key Bindings\n"
    for i in CFG["Keys"].keys():
        src += "  " + i + ": " + CFG["Keys"][i] + "\n"
    return "Help", src, K["Help"]


def input_prompt(prompt):
    rows, cols = SCREEN.getmaxyx()
    stat = curses.newwin(1, cols, rows-1, 0)
    if COLORSUPPORT:
        stat.bkgd(SCREEN.getbkgd())
    stat.keypad(True)
    curses.echo(1)
    curses.curs_set(1)

    init_text = ""

    stat.addstr(0, 0, prompt, curses.A_REVERSE)
    stat.addstr(0, len(prompt), init_text)
    stat.refresh()

    try:
        while True:
            ipt = stat.getch()
            if ipt == 27:
                stat.clear()
                stat.refresh()
                curses.echo(0)
                curses.curs_set(0)
                return
            elif ipt == 10:
                stat.clear()
                stat.refresh()
                curses.echo(0)
                curses.curs_set(0)
                return init_text
            elif ipt in {8, curses.KEY_BACKSPACE}:
                init_text = init_text[:-1]
            elif ipt == curses.KEY_RESIZE:
                stat.clear()
                stat.refresh()
                curses.echo(0)
                curses.curs_set(0)
                return curses.KEY_RESIZE
            else:
                init_text += chr(ipt)

            stat.clear()
            stat.addstr(0, 0, prompt, curses.A_REVERSE)
            stat.addstr(0, len(prompt), init_text)
            stat.refresh()
    except KeyboardInterrupt:
        stat.clear()
        stat.refresh()
        curses.echo(0)
        curses.curs_set(0)
        return


def dots_path(curr, tofi):
    candir = curr.split("/")
    tofi = tofi.split("/")
    alld = tofi.count("..")
    t = len(candir)
    candir = candir[0:t-alld-1]
    try:
        while True:
            tofi.remove("..")
    except ValueError:
        pass
    return "/".join(candir+tofi)


def find_media_viewer():
    global VWR
    if shutil.which(CFG["DefaultViewer"]) is not None:
        VWR = CFG["DefaultViewer"]
    elif sys.platform == "win32":
        VWR = "start"
    elif sys.platform == "darwin":
        VWR = "open"
    else:
        VWR_LIST = [
            "feh",
            "gio",
            "gnome-open",
            "gvfs-open",
            "xdg-open",
            "kde-open",
            "firefox"
        ]
        for i in VWR_LIST:
            if shutil.which(i) is not None:
                VWR = i
                break

    if VWR in {"gio"}:
        VWR += " open"


def open_media(scr, epub, src):
    sfx = os.path.splitext(src)[1]
    fd, path = tempfile.mkstemp(suffix=sfx)
    try:
        with os.fdopen(fd, "wb") as tmp:
            tmp.write(epub.file.read(src))
        # run(VWR + " " + path, shell=True)
        subprocess.call(
            VWR + " " + path,
            shell=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL
        )
        k = scr.getch()
    finally:
        os.remove(path)
    return k


def searching(pad, src, width, y, ch, tot):
    global SEARCHPATTERN
    rows, cols = SCREEN.getmaxyx()
    x = (cols - width) // 2
    if SEARCHPATTERN is None:
        candtext = input_prompt(" Regex:")
        if candtext is None:
            return y
        elif isinstance(candtext, str):
            SEARCHPATTERN = "/" + candtext
        elif candtext == curses.KEY_RESIZE:
            return candtext

    if SEARCHPATTERN in {"?", "/"}:
        SEARCHPATTERN = None
        return y

    found = []
    pattern = re.compile(SEARCHPATTERN[1:], re.IGNORECASE)
    for n, i in enumerate(src):
        for j in pattern.finditer(i):
            found.append([n, j.span()[0], j.span()[1] - j.span()[0]])

    if found == []:
        if SEARCHPATTERN[0] == "/" and ch + 1 < tot:
            return 1
        elif SEARCHPATTERN[0] == "?" and ch > 0:
            return -1
        else:
            s = 0
            while True:
                if s in K["Quit"]:
                    SEARCHPATTERN = None
                    SCREEN.clear()
                    SCREEN.refresh()
                    return y
                elif s == ord("n") and ch == 0:
                    SEARCHPATTERN = "/"+SEARCHPATTERN[1:]
                    return 1
                elif s == ord("N") and ch + 1 == tot:
                    SEARCHPATTERN = "?"+SEARCHPATTERN[1:]
                    return -1

                SCREEN.clear()
                SCREEN.addstr(
                    rows-1, 0,
                    " Finished searching: " + SEARCHPATTERN[1:] + " ",
                    curses.A_REVERSE
                )
                SCREEN.refresh()
                pad.refresh(y, 0, 0, x, rows-2, x+width)
                s = pad.getch()

    sidx = len(found) - 1
    if SEARCHPATTERN[0] == "/":
        if y > found[-1][0]:
            return 1
        for n, i in enumerate(found):
            if i[0] >= y:
                sidx = n
                break

    s = 0
    msg = " Searching: "\
        + SEARCHPATTERN[1:]\
        + " --- Res {}/{} Ch {}/{} ".format(
            sidx + 1,
            len(found),
            ch+1, tot
        )
    while True:
        if s in K["Quit"]:
            SEARCHPATTERN = None
            for i in found:
                pad.chgat(i[0], i[1], i[2], pad.getbkgd())
            SCREEN.clear()
            SCREEN.refresh()
            return y
        elif s == ord("n"):
            SEARCHPATTERN = "/"+SEARCHPATTERN[1:]
            if sidx == len(found) - 1:
                if ch + 1 < tot:
                    return 1
                else:
                    s = 0
                    msg = " Finished searching: " + SEARCHPATTERN[1:] + " "
                    continue
            else:
                sidx += 1
                msg = " Searching: "\
                    + SEARCHPATTERN[1:]\
                    + " --- Res {}/{} Ch {}/{} ".format(
                        sidx + 1,
                        len(found),
                        ch+1, tot
                    )
        elif s == ord("N"):
            SEARCHPATTERN = "?"+SEARCHPATTERN[1:]
            if sidx == 0:
                if ch > 0:
                    return -1
                else:
                    s = 0
                    msg = " Finished searching: " + SEARCHPATTERN[1:] + " "
                    continue
            else:
                sidx -= 1
                msg = " Searching: "\
                    + SEARCHPATTERN[1:]\
                    + " --- Res {}/{} Ch {}/{} ".format(
                        sidx + 1,
                        len(found),
                        ch+1, tot
                    )
        elif s == curses.KEY_RESIZE:
            return s

        while found[sidx][0] not in list(range(y, y+rows-1)):
            if found[sidx][0] > y:
                y += rows - 1
            else:
                y -= rows - 1
                if y < 0:
                    y = 0

        for n, i in enumerate(found):
            attr = curses.A_REVERSE if n == sidx else curses.A_NORMAL
            pad.chgat(i[0], i[1], i[2], pad.getbkgd() | attr)

        SCREEN.clear()
        SCREEN.addstr(rows-1, 0, msg, curses.A_REVERSE)
        SCREEN.refresh()
        pad.refresh(y, 0, 0, x, rows-2, x+width)
        s = pad.getch()


def find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y):
    ntoc = 0
    for n, (i, j) in enumerate(zip(toc_idx, toc_sect)):
        if i <= index:
            if y >= toc_secid.get(j, 0):
                ntoc = n
        else:
            break
    return ntoc


def reader(ebook, index, width, y, pctg, sect):
    global SHOWPROGRESS

    k = 0 if SEARCHPATTERN is None else ord("/")
    rows, cols = SCREEN.getmaxyx()
    x = (cols - width) // 2

    contents = ebook.contents
    toc_name = ebook.toc_entries[0]
    toc_idx = ebook.toc_entries[1]
    toc_sect = ebook.toc_entries[2]
    toc_secid = {}
    chpath = contents[index]
    content = ebook.file.open(chpath).read()
    content = content.decode("utf-8")

    parser = HTMLtoLines(set(toc_sect))
    # parser = HTMLtoLines()
    # try:
    parser.feed(content)
    parser.close()
    # except:
    #     pass

    src_lines, imgs, toc_secid = parser.get_lines(width)
    totlines = len(src_lines) + 1  # 1 extra line for suffix

    if y < 0 and totlines <= rows:
        y = 0
    elif pctg is not None:
        y = round(pctg*totlines)
    else:
        y = y % totlines

    pad = curses.newpad(totlines, width + 2)  # + 2 unnecessary
    if COLORSUPPORT:
        pad.bkgd(SCREEN.getbkgd())

    pad.keypad(True)

    LOCALPCTG = []
    for n, i in enumerate(src_lines):
        if re.search("\\[IMG:[0-9]+\\]", i):
            pad.addstr(n, width//2 - len(i)//2 + 1, i, curses.A_REVERSE)
        else:
            pad.addstr(n, 0, i)
        if CFG["EnableProgressIndicator"]:
            LOCALPCTG.append(len(re.sub("\s", "", i)))
    # chapter suffix
    ch_suffix = "***"  # "\u3064\u3065\u304f" つづく
    try:
        pad.addstr(n+1, (width - len(ch_suffix))//2 + 1, ch_suffix)
    except curses.error:
        pass

    if CFG["EnableProgressIndicator"]:
        TOTALPCTG = sum(PERCENTAGE)
        TOTALLOCALPCTG = sum(PERCENTAGE[:index])

    SCREEN.clear()
    SCREEN.refresh()
    # try except to be more flexible on terminal resize
    try:
        pad.refresh(y, 0, 0, x, rows-1, x+width)
    except curses.error:
        pass

    if sect != "":
        y = toc_secid.get(sect, 0)

    countstring = ""
    try:
        while True:
            if countstring == "":
                count = 1
            else:
                count = int(countstring)
            if k in range(48, 58): # i.e., k is a numeral
                countstring = countstring + chr(k)
            else:
                if k in K["Quit"]:
                    if k == 27 and countstring != "":
                        countstring = ""
                    else:
                        savestate(ebook.path, index, width, y, y/totlines)
                        sys.exit()
                elif k in K["ScrollUp"]:
                    if y >= count:
                        y -= count
                    elif index != 0:
                        return -1, width, -rows, None, ""
                elif k in K["PageUp"]:
                    if y == 0 and index != 0:
                        return -1, width, -rows, None, ""
                    else:
                        y = pgup(y, rows, LINEPRSRV, count)
                elif k in K["ScrollDown"]:
                    if y + count <= totlines - rows:
                        y += count
                    elif index != len(contents)-1:
                        return 1, width, 0, None, ""
                elif k in K["PageDown"]:
                    if totlines - y - LINEPRSRV > rows:
                        y += rows - LINEPRSRV
                        # SCREEN.clear()
                        # SCREEN.refresh()
                    elif index != len(contents)-1:
                        return 1, width, 0, None, ""
                elif k in K["NextChapter"]:
                    ntoc = find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y)
                    if ntoc < len(toc_idx) - 1:
                        if index == toc_idx[ntoc+1]:
                            try:
                                y = toc_secid[toc_sect[ntoc+1]]
                            except KeyError:
                                pass
                        else:
                            return toc_idx[ntoc+1]-index, width, 0, None, toc_sect[ntoc+1]
                elif k in K["PrevChapter"]:
                    ntoc = find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y)
                    if ntoc > 0:
                        if index == toc_idx[ntoc-1]:
                            y = toc_secid.get(toc_sect[ntoc-1], 0)
                        else:
                            return toc_idx[ntoc-1]-index, width, 0, None, toc_sect[ntoc-1]
                elif k in K["BeginningOfCh"]:
                    ntoc = find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y)
                    try:
                        y = toc_secid[toc_sect[ntoc]]
                    except KeyError:
                        y = 0
                elif k in K["EndOfCh"]:
                    ntoc = find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y)
                    try:
                        if toc_secid[toc_sect[ntoc+1]] - rows >= 0:
                            y = toc_secid[toc_sect[ntoc+1]] - rows
                        else:
                            y = toc_secid[toc_sect[ntoc]]
                    except (KeyError, IndexError):
                        y = pgend(totlines, rows)
                elif k in K["ToC"]:
                    ntoc = find_curr_toc_id(toc_idx, toc_sect, toc_secid, index, y)
                    fllwd = toc(toc_name, ntoc)
                    if fllwd is not None:
                        if fllwd in {curses.KEY_RESIZE}|K["Help"]|K["Metadata"]:
                            k = fllwd
                            continue
                        if index == toc_idx[fllwd]:
                            try:
                                y = toc_secid[toc_sect[fllwd]]
                            except KeyError:
                                y = 0
                        else:
                            return toc_idx[fllwd] - index, width, 0, None, toc_sect[fllwd]
                elif k in K["Metadata"]:
                    k = meta(ebook)
                    if k in {curses.KEY_RESIZE}|K["Help"]|K["ToC"]:
                        continue
                elif k in K["Help"]:
                    k = help()
                    if k in {curses.KEY_RESIZE}|K["Metadata"]|K["ToC"]:
                        continue
                elif k in K["Enlarge"] and (width + count) < cols - 2:
                    width += count
                    return 0, width, 0, y/totlines, ""
                elif k in K["Shrink"] and width >= 22:
                    width -= count
                    return 0, width, 0, y/totlines, ""
                elif k in K["SetWidth"]:
                    if countstring == "":
                        # if called without a count, toggle between 80 cols and full width
                        if width != 80 and cols - 2 >= 80:
                            return 0, 80, 0, y/totlines, ""
                        else:
                            return 0, cols - 2, 0, y/totlines, ""
                    else:
                        width = count
                    if width < 20:
                        width = 20
                    elif width >= cols -2:
                        width = cols - 2
                    return 0, width, 0, y/totlines, ""
                # elif k == ord("0"):
                #     if width != 80 and cols - 2 >= 80:
                #         return 0, 80, 0, y/totlines, ""
                #     else:
                #         return 0, cols - 2, 0, y/totlines, ""
                elif k in K["RegexSearch"]:
                    fs = searching(
                        pad,
                        src_lines,
                        width, y,
                        index, len(contents)
                    )
                    if fs == curses.KEY_RESIZE:
                        k = fs
                        continue
                    elif SEARCHPATTERN is not None:
                        return fs, width, 0, None, ""
                    else:
                        y = fs
                elif k in K["OpenImage"] and VWR is not None:
                    gambar, idx = [], []
                    for n, i in enumerate(src_lines[y:y+rows]):
                        img = re.search("(?<=\\[IMG:)[0-9]+(?=\\])", i)
                        if img is not None:
                            gambar.append(img.group())
                            idx.append(n)

                    impath = ""
                    if len(gambar) == 1:
                        impath = imgs[int(gambar[0])]
                    elif len(gambar) > 1:
                        p, i = 0, 0
                        while p not in K["Quit"] and p not in K["Follow"]:
                            SCREEN.move(idx[i], x + width//2 + len(gambar[i]) + 1)
                            SCREEN.refresh()
                            curses.curs_set(1)
                            p = pad.getch()
                            if p in K["ScrollDown"]:
                                i += 1
                            elif p in K["ScrollUp"]:
                                i -= 1
                            i = i % len(gambar)

                        curses.curs_set(0)
                        if p in K["Follow"]:
                            impath = imgs[int(gambar[i])]

                    if impath != "":
                        imgsrc = dots_path(chpath, impath)
                        k = open_media(pad, ebook, imgsrc)
                        continue
                elif k in K["SwitchColor"] and COLORSUPPORT and countstring in {"", "0", "1", "2"}:
                    if countstring == "":
                        count_color = curses.pair_number(SCREEN.getbkgd())
                        if count_color not in {2, 3}: count_color = 1
                        count_color = count_color % 3
                    else:
                        count_color = count
                    SCREEN.bkgd(curses.color_pair(count_color+1))
                    return 0, width, y, None, ""
                elif k in K["MarkPosition"]:
                    jumnum = pad.getch()
                    if jumnum in range(49, 58):
                        JUMPLIST[chr(jumnum)] = [index, width, y, y/totlines]
                    else:
                        k = jumnum
                        continue
                elif k in K["JumpToPosition"]:
                    jumnum = pad.getch()
                    if jumnum in range(49, 58) and chr(jumnum) in JUMPLIST.keys():
                        tojumpidxdiff = JUMPLIST[chr(jumnum)][0]-index
                        tojumpy = JUMPLIST[chr(jumnum)][2]
                        tojumpctg = None if JUMPLIST[chr(jumnum)][1] == width else JUMPLIST[chr(jumnum)][3]
                        return tojumpidxdiff, width, tojumpy, tojumpctg, ""
                    else:
                        k = jumnum
                        continue
                elif k in K["ShowHideProgress"] and CFG["EnableProgressIndicator"]:
                    SHOWPROGRESS = not SHOWPROGRESS
                elif k == curses.KEY_RESIZE:
                    savestate(ebook.path, index, width, y, y/totlines)
                    # stated in pypi windows-curses page:
                    # to call resize_term right after KEY_RESIZE
                    if sys.platform == "win32":
                        curses.resize_term(rows, cols)
                        rows, cols = SCREEN.getmaxyx()
                    else:
                        rows, cols = SCREEN.getmaxyx()
                        curses.resize_term(rows, cols)
                    if cols < 22 or rows < 12:
                        sys.exit("ERR: Screen was too small.")
                    if cols <= width:
                        return 0, cols - 2, 0, y/totlines, ""
                    else:
                        return 0, width, y, None, ""
                countstring = ""

            if CFG["EnableProgressIndicator"]:
                PROGRESS = (TOTALLOCALPCTG + sum(LOCALPCTG[:y+rows-1])) / TOTALPCTG
                PROGRESSTR = "{}%".format(int(PROGRESS*100))

            try:
                SCREEN.clear()
                SCREEN.addstr(0, 0, countstring)
                if SHOWPROGRESS and (cols-width-2)//2 > 3:
                    SCREEN.addstr(0, cols-len(PROGRESSTR), PROGRESSTR)
                SCREEN.refresh()
                if totlines - y < rows:
                    pad.refresh(y, 0, 0, x, totlines-y, x+width)
                else:
                    pad.refresh(y, 0, 0, x, rows-1, x+width)
            except curses.error:
                pass
            k = pad.getch()
    except KeyboardInterrupt:
        savestate(ebook.path, index, width, y, y/totlines)
        sys.exit()


def preread(stdscr, file):
    global COLORSUPPORT, SHOWPROGRESS, PERCENTAGE, SCREEN

    curses.use_default_colors()
    try:
        curses.init_pair(1, -1, -1)
        curses.init_pair(2, CFG["DarkColorFG"], CFG["DarkColorBG"])
        curses.init_pair(3, CFG["LightColorFG"], CFG["LightColorBG"])
        COLORSUPPORT = True
    except:
        COLORSUPPORT  = False

    SCREEN = stdscr

    SCREEN.keypad(True)
    curses.curs_set(0)
    SCREEN.clear()
    rows, cols = SCREEN.getmaxyx()
    SCREEN.addstr(rows-1, 0, "Loading...")
    SCREEN.refresh()

    epub = Epub(file)

    if epub.path in STATE["States"]:
        idx = STATE["States"][epub.path]["index"]
        width = STATE["States"][epub.path]["width"]
        y = STATE["States"][epub.path]["pos"]
    else:
        STATE["States"][epub.path] = {}
        idx = 0
        y = 0
        width = 80
    pctg = None

    if cols <= width:
        width = cols - 2
        pctg = STATE["States"][epub.path].get("pctg", None)

    epub.initialize()
    find_media_viewer()
    parse_keys()

    SHOWPROGRESS = CFG["EnableProgressIndicator"]
    if SHOWPROGRESS:
        for i in epub.contents:
            content = epub.file.open(i).read()
            content = content.decode("utf-8")
            parser = HTMLtoLines()
            # try:
            parser.feed(content)
            parser.close()
            # except:
            #     pass
            src_lines = parser.get_lines()
            PERCENTAGE.append(sum([len(re.sub("\s", "", j)) for j in src_lines]))

    sec = ""
    while True:
        incr, width, y, pctg, sec = reader(
            epub, idx, width, y, pctg, sec
        )
        idx += incr


def main():
    args = []
    if sys.argv[1:] != []:
        args += sys.argv[1:]

    if len({"-h", "--help"} & set(args)) != 0:
        print(__doc__.rstrip())
        sys.exit()

    loadstate()

    if len({"-v", "--version", "-V"} & set(args)) != 0:
        print("Startup file loaded:")
        print(CFGFILE)
        print(STATEFILE)
        print()
        print("v" + __version__)
        print(__license__, "License")
        print("Copyright (c) 2019", __author__)
        print(__url__)
        sys.exit()

    if len({"-d"} & set(args)) != 0:
        args.remove("-d")
        dump = True
    else:
        dump = False

    if args == []:
        file = STATE["LastRead"]
        if not os.path.isfile(file):
            # print(__doc__)
            sys.exit("ERROR: Found no last read file.")

    elif os.path.isfile(args[0]):
        file = args[0]

    else:
        file = None
        todel = []
        xitmsg = 0

        val = 0
        for i in STATE["States"].keys():
            if not os.path.exists(i):
                todel.append(i)
            else:
                match_val = sum([
                    j.size for j in SM(
                        None, i.lower(), " ".join(args).lower()
                    ).get_matching_blocks()
                ])
                if match_val >= val:
                    val = match_val
                    file = i
        if val == 0:
            xitmsg = "\nERROR: No matching file found in history."

        for i in todel:
            del STATE["States"][i]
        with open(STATEFILE, "w") as f:
            json.dump(STATE, f, indent=4)

        if len(args) == 1 and re.match(r"[0-9]+", args[0]) is not None:
            try:
                file = list(STATE["States"].keys())[int(args[0])-1]
                xitmsg = 0
            except IndexError:
                xitmsg = "\nERROR: No matching file found in history."

        if xitmsg != 0 or "-r" in args:
            print("Reading history:")
            dig = len(str(len(STATE["States"].keys())+1))
            for n, i in enumerate(STATE["States"].keys()):
                print(str(n+1).rjust(dig)
                      + ("* " if STATE["LastRead"] == i else "  ") + i)
            sys.exit(xitmsg)

    if dump:
        epub = Epub(file)
        epub.initialize()
        for i in epub.contents:
            content = epub.file.open(i).read()
            content = content.decode("utf-8")
            parser = HTMLtoLines()
            # try:
            parser.feed(content)
            parser.close()
            # except:
            #     pass
            src_lines = parser.get_lines()
            # sys.stdout.reconfigure(encoding="utf-8")  # Python>=3.7
            for j in src_lines:
                sys.stdout.buffer.write((j+"\n\n").encode("utf-8"))
        sys.exit()

    else:
        curses.wrapper(preread, file)


if __name__ == "__main__":
    main()