├── README.md └── main.py /README.md: -------------------------------------------------------------------------------- 1 | # wikipedia 2 | Command-line Wikipedia viewer using Python 3 | 4 | ![ezgif com-gif-maker (1) (1)](https://user-images.githubusercontent.com/30610197/113373932-efc4c080-9339-11eb-9c19-7f0746c2bc91.gif) 5 | 6 | ## Dependencies 7 | - `keyboard` library 8 | - `wikipedia` library 9 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A command line interface for reading articles on wikipedia. 3 | ''' 4 | 5 | ''' IMPORTS ''' 6 | 7 | import os # used to find terminal dimensions 8 | import sys # used to update terminal 9 | import keyboard # used to handle keyboard input 10 | import wikipedia # retrieves data from wikipedia 11 | import time # framerate 12 | import textwrap # split text into equal-length lines 13 | import webbrowser # open wikipedia articles in browser 14 | 15 | cursor_move = False # Can we use ctypes to move the cursor? 16 | if os.name == "nt": 17 | import ctypes # for added pain (and to move the cursor) 18 | from ctypes import c_long, c_wchar_p, c_ulong, c_void_p 19 | cursor_move = True 20 | 21 | ''' PRIMITIVE GRAPHICS ''' 22 | 23 | # Terminal colors 24 | class colors: 25 | HEADER = '\033[95m' 26 | OKBLUE = '\033[94m' 27 | OKCYAN = '\033[96m' 28 | OKGREEN = '\033[92m' 29 | WARNING = '\033[93m' 30 | FAIL = '\033[91m' 31 | ENDC = '\033[0m' 32 | BOLD = '\033[1m' 33 | UNDERLINE = '\033[4m' 34 | 35 | def write(x,y,text,color=""): # Writes text at x, y 36 | sys.stdout.write(color+"\x1b7\x1b[%d;%df%s\x1b8" % (y, x, text)+colors.ENDC) # Black magic 37 | 38 | def drawHorizontal(y,x1,x2,startChar,midChar,endChar): # Draws a horizontal line 39 | line = startChar+midChar*(x2-x1-2)+endChar 40 | write(x1,y,line,colors.OKBLUE) 41 | 42 | def drawVertical(x,y1,y2,startChar,midChar,endChar): # Draws a vertical line 43 | write(x,y1+1,startChar,colors.OKBLUE) 44 | write(x,y2,endChar,colors.OKBLUE) 45 | for y in range(y1+2,y2): 46 | write(x,y,midChar,colors.OKBLUE) 47 | 48 | def clear(): # Clears the entire terminal window 49 | if os.name == "nt": # Windows is cls, Linux is clear 50 | os.system("cls") 51 | else: 52 | os.system("clear") 53 | 54 | def erase(x1,x2,y1,y2): # Erases a specific rectangle of the window 55 | for x in range(x1,x2+1): 56 | for y in range(y1,y2+1): 57 | write(x,y," ") 58 | 59 | ''' DRAW WINDOW ''' 60 | 61 | if cursor_move: 62 | gHandle = ctypes.windll.kernel32.GetStdHandle(c_long(-11)) 63 | sx, sy = (0,0) 64 | old_query_str = "query string" 65 | queryStr = "" 66 | page = [] 67 | rawpage = "" 68 | offset = 0 # how much scroll 69 | quitApp = False 70 | mostRecentQuery = "" 71 | 72 | # padding 73 | pad_t = 1 74 | pad_b = 1 75 | pad_l = 0 76 | pad_r = 0 77 | 78 | def drawSplash(): 79 | clear() 80 | title = [ 81 | "_ ___ __ _ ___ ", 82 | "| | / (_) /__(_)___ ___ ____/ (_)___ _", 83 | "| | /| / / / //_/ / __ \/ _ \/ __ / / __ `/", 84 | "| |/ |/ / / ,< / / /_/ / __/ /_/ / / /_/ / ", 85 | "|__/|__/_/_/|_/_/ .___/\___/\__,_/_/\__,_/ ", 86 | " /_/ " 87 | ] 88 | for i in range(len(title)): 89 | write(0,1+i,title[i],colors.OKGREEN) 90 | sys.stdout.flush() 91 | time.sleep(0.05) 92 | 93 | def drawWindow(sx,sy): 94 | global queryStr 95 | # Clear window 96 | clear() 97 | 98 | # Write "wikipedia" heading 99 | write(pad_l+1,0,"Wikipedia, the Free Encyclopedia",colors.OKGREEN) 100 | 101 | # Draw box 102 | drawHorizontal(1+pad_t,1+pad_l,sx+1-pad_r,"╭","─","╮") 103 | drawHorizontal(sy-pad_b,1+pad_l,sx+1-pad_r,"╰","─","╯") 104 | drawVertical(1+pad_l,pad_t,sy-pad_b,"╭","│","╰") 105 | drawVertical(sx-pad_r,pad_t,sy-pad_b,"╮","│","╯") 106 | 107 | # Draw input/text separation line 108 | drawHorizontal(3+pad_t,1+pad_l,sx+1-pad_r,"├","─","┤") 109 | sys.stdout.flush() 110 | 111 | # Draw "tab" and "exit" text 112 | if page: 113 | write(pad_l+1,sy,"[ctrl-c]: quit | [tab]: open in browser") 114 | else: 115 | write(pad_l+1,sy,"[ctrl-c]: quit") 116 | 117 | def updateRowCounter(): 118 | if mostRecentQuery: 119 | end = min(len(page),offset+sy+2) 120 | lineCounter = "%d-%d of %d rows" %(offset,end,len(page)) 121 | write(sx-pad_r-len(lineCounter),sy,lineCounter) 122 | 123 | def drawQuery(): 124 | global queryStr, old_query_str 125 | erase(2+pad_l,sx-1-pad_r,2+pad_t,2+pad_t) 126 | write(2+pad_l,2+pad_t,queryStr,colors.OKCYAN) 127 | old_query_str = queryStr 128 | # move cursor 129 | # https://stackoverflow.com/questions/27612545/how-to-change-the-location-of-the-pointer-in-python/27612978#27612978 130 | if cursor_move: 131 | value = len(queryStr) + 1 + pad_l + ((1+pad_t) << 16) 132 | ctypes.windll.kernel32.SetConsoleCursorPosition(gHandle, c_ulong(value)) 133 | sys.stdout.flush() 134 | 135 | def redraw(): 136 | global page, rawpage 137 | drawWindow(sx,sy) 138 | drawQuery() 139 | page = makePage(rawpage) 140 | writePage() 141 | 142 | ''' QUERY ''' 143 | 144 | def keyInput(keyObj): 145 | global queryStr, offset, quitApp, mostRecentQuery 146 | key = keyObj.name # get unicode representation of keyObj 147 | if key == "space": key = " " 148 | if key == "backspace": queryStr = queryStr[:-1] 149 | if len(key) == 1: # check if it is actually a character and not something like [shift] 150 | queryStr += key 151 | if key == "enter": 152 | if len(queryStr): # Check if there is actually a query 153 | getPage() # Send request to wikipedia 154 | mostRecentQuery = queryStr 155 | queryStr = "" 156 | if key == "esc": 157 | quitApp = True 158 | if key == "up": 159 | offset = max(0,offset-1) 160 | writePage() 161 | if key == "down": 162 | if len(page)-sy-pad_t-pad_b > offset: 163 | offset += 1 164 | writePage() 165 | if key == "tab": 166 | if page: 167 | webbrowser.open("https://wikipedia.org/wiki/"+mostRecentQuery) 168 | drawQuery() 169 | 170 | keyboard.on_press(keyInput) 171 | 172 | def getPage(): 173 | global queryStr, page, rawpage, offset 174 | # Loading screen 175 | offset = 0 176 | erase(2+pad_l,sx-1-pad_r,4+pad_t,sy-1-pad_b) 177 | page = makePage("Loading...") 178 | writePage() 179 | 180 | # Send query 181 | try: 182 | rawpage = wikipedia.page(queryStr,auto_suggest=False) 183 | rawpage = colors.WARNING+rawpage.title.upper()+colors.ENDC+"\n"+rawpage.content # title (in yellow) and content 184 | except wikipedia.exceptions.DisambiguationError as e: # Show disabiguation page 185 | rawpage = colors.WARNING+queryStr+" may refer to:"+colors.ENDC+"\n"+"\n".join(e.options) 186 | except wikipedia.exceptions.PageError: 187 | rawpage = "No pages found for '%s'. Check your capitalization, or try a different query." %(queryStr) 188 | 189 | # Add color to headings 190 | rawpage = rawpage.replace(" ===="," ===="+colors.ENDC) 191 | rawpage = rawpage.replace("==== ",colors.FAIL+"==== ") 192 | rawpage = rawpage.replace(" === "," === "+colors.ENDC) 193 | rawpage = rawpage.replace("=== ",colors.FAIL+"=== ") 194 | rawpage = rawpage.replace(" == "," == "+colors.ENDC) 195 | rawpage = rawpage.replace("== ",colors.FAIL+"== ") 196 | 197 | # Textwrap lines 198 | erase(2+pad_l,sx-1-pad_r,4+pad_t,sy-1-pad_b) 199 | page = makePage(rawpage) 200 | offset = 0 201 | redraw() 202 | 203 | ''' DRAW PAGE ''' 204 | 205 | def makePage(page): 206 | w = textwrap.TextWrapper(width=sx-2-pad_l-pad_r,replace_whitespace=False) 207 | p = page.split("\n") 208 | newpage = [] 209 | for line in p: 210 | if len(line) > sx-2-pad_l-pad_r: 211 | w = textwrap.TextWrapper(width=sx-2-pad_l-pad_r, break_long_words=False) 212 | for k in w.wrap(line): 213 | newpage.append(k) 214 | else: 215 | newpage.append(line) 216 | return newpage 217 | 218 | def writePage(): 219 | global page, sx, offset 220 | linew = sx-pad_r-pad_l-2 # Width of the drawing area 221 | for i in range(offset,min(len(page),sy-4+offset-pad_t-pad_b)): 222 | # Instead of clearing the canvas and redrawing, we 223 | # clear and redraw line by line. Not a perfect solution, 224 | # but it is not buggy and significantly reduces flickering. 225 | write(2+pad_l,4+i-offset+pad_t," "*linew) 226 | write(2+pad_l,4+i-offset+pad_t,page[i]) 227 | if len(page)-offset < sy-4-pad_b: 228 | write(2+pad_l,sy-1-pad_b," "*linew) 229 | updateRowCounter() 230 | sys.stdout.flush() 231 | 232 | ''' MAIN ''' 233 | def main(): 234 | global sx, sy, pad_r, pad_l 235 | drawSplash() 236 | time.sleep(0.1) 237 | drawQuery() 238 | try: 239 | while True: 240 | # Get dimensions of terminal window 241 | s = os.get_terminal_size() 242 | nsy = s.lines 243 | nsx = s.columns 244 | # If window has been resized, redraw the window 245 | if nsy != sy or nsx != sx: 246 | sx, sy = nsx, nsy 247 | # set left/right padding to 10% if window is large enough 248 | if sx > 60: 249 | pad_r = int(sx/10) 250 | pad_l = int(sx/10) 251 | else: 252 | pad_r = 1 253 | pad_l = 1 254 | redraw() 255 | time.sleep(0.05) 256 | if quitApp: 257 | clear() 258 | sys.exit() 259 | except KeyboardInterrupt: 260 | clear() # Clear the screen before we exit 261 | sys.exit() 262 | 263 | if __name__ == '__main__': 264 | main() 265 | --------------------------------------------------------------------------------