diff --git a/README.md b/README.md index 59f9998..e5a841e 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -# Digger +# Burrow A client/browser that accesses _gopherspace_. It is under current early stage development. - ![Digger browser](http://brianmevans.com/files/digger.png "Digger v0.1.5 main window") + ![Burrow browser](http://brianmevans.com/files/digger.png "Burrow v0.1.5 main window") ## Gopher [_Gopher_](https://en.wikipedia.org/wiki/Gopher_(protocol)) is a communications protocol that, in the early 90s, competed (briefly) with what became the world wide web. _Gopher_ serves up files and text based menus. As such, it is much lighter weight than HTML documents and the like served over http. Due to its text based nature it also has the benefit of being reliable in its visual output and style, and for being relatively accessible. -## Digger -The following is a list of current and future Digger features: +## Browser Features +The following is a list of current and future Burrow features: - Tk based GUI - Back button, move backwards in session history - Forward button, move forward in session history - Refresh button, will be replaced by a favorite button __(non-functional)__ - - Home, shows favorites and is a start page __(non-functinoal)__ + - Home, shows favorites and is a start page - An address bar, on _ENTER_ submits a request for a _gopher_ page - Settings button __(non-functional)__ - A display area for the requested information - - Scroll bar, scrolling works, but no bar is present __(non-functional)__ + - Scroll bar - A status bar to display various information - Links to menus and files - On hover, link destination shows in status bar @@ -30,7 +30,7 @@ The following is a list of current and future Digger features: - Menus Pages and files - Menus display correctly and quickly - Text files display correctly and quickly - - Images files __(non-functional)__ + - Images files - Sound files __(non-functional)__ - Binary files __(non-functional)__ - HTML files, will open in default browser __(non-functional)__ @@ -56,19 +56,22 @@ The following is a list of current and future Digger features: ## Installation -Digger requires python3 to be installed on the system prior to running. - # can be prefixed with python3 - # but not required if you have python3 installed - /path/to/digger.py +Burrow requires python3 to be installed on the system prior to running. +Until some bundling/setup file creation occurs, you will also need to add: + + pip3 install pillow + + On some linux distributions an additional package for tkinter may be required. If you get a console error complaining about tkinter try the following (or equivalent for your package manager): - sudo apt-get python3-tk -Once up an running you should be good to go. + sudo apt-get install python3-tk + sudo apt-get install python3-pil.imagetk + ## Distribution -Digger's primary system target is linux. Once a version 1.0 is reached the plan is to distribute primarily through [Snapcraft](https://snapcraft.io/) packages. +Burrow's primary system target is linux. Once a version 1.0 is reached the plan is to distribute primarily through [Snapcraft](https://snapcraft.io/) packages. Some version of windows executable may come along as well, depending on configurability of build tools (py2exe, freeze, etc) for windows executables and time. diff --git a/btn_back.png b/btn_back.png deleted file mode 100644 index 3b19330..0000000 Binary files a/btn_back.png and /dev/null differ diff --git a/btn_forward.png b/btn_forward.png deleted file mode 100644 index 70d1fe8..0000000 Binary files a/btn_forward.png and /dev/null differ diff --git a/btn_home.png b/btn_home.png deleted file mode 100644 index 2536137..0000000 Binary files a/btn_home.png and /dev/null differ diff --git a/btn_menu.png b/btn_menu.png deleted file mode 100644 index 7d9694f..0000000 Binary files a/btn_menu.png and /dev/null differ diff --git a/btn_refresh.png b/btn_refresh.png deleted file mode 100644 index 48a8229..0000000 Binary files a/btn_refresh.png and /dev/null differ diff --git a/burrow.py b/burrow.py new file mode 100644 index 0000000..8770d7d --- /dev/null +++ b/burrow.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +################################################ +## Burrow, a gopher client/browser +## - - - - - - - - - - - - - - - - - - - - - - - +## Version 0.2.1 +################################################ + +from gui import GUI + + +if __name__ == '__main__': + app = GUI() + app.root.mainloop() diff --git a/conn.py b/conn.py deleted file mode 100644 index 23d19f7..0000000 --- a/conn.py +++ /dev/null @@ -1,106 +0,0 @@ -import socket -import sys -import re - -class Tunnel: - def __init__(self): - self.raw_request = None - self.text_output = None - self.types = { - '0': '(TXT)', - '1': '(MENU)', - '2': None, - '3': 'Error code', - '4': None, - '5': None, - '6': None, - '7': '(INTER)', - '8': '(TLNET)', - '9': '(BIN)', - '+': None, - 'g': '(GIF)', - 'I': '(IMG)', - 't': None, - 'h': '(HTML)', - 'i': '(INFO)', - 's': '(SOUND)' - } - - - def make_connection(self, resource, host, itemtype, port=70): - endline = '\r\n' - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(30.0) - try: - port = int(port) - except: - port = 70 - s.connect((host, port)) - s.sendall((resource + '\r\n').encode('utf-8')) - r = s.makefile(mode = 'r', errors='ignore') - try: - raw_data = r.read() - except UnicodeDecodeError: - raw_data = 'iError decoding server response :(\tfalse\tnull.host\t1' - - try: - data = raw_data.decode('utf-8','ignore') - except: - data = raw_data - - self.raw_request = data - - if itemtype[1] == '1': - #handle menus - self.text_output = self.gopher_to_text(self.raw_request) - elif itemtype[1] == '0': - #handle text files - self.text_output = [self.raw_request] - self.text_output.insert(0,itemtype[1]) - s.close() - return self.text_output - - - - def gopher_to_text(self, message): - message = message.split('\n') - message = [x.split('\t') for x in message] - message = [{'type': x[0][0], 'description': x[0][1:], 'resource': x[1], 'host': x[2], 'port': x[3]} for x in message if len(x) >= 4] - return message - - - def parse_url(self, url, execute=True): - regex = r'^(?P(?:gopher:\/\/)?)?(?P[\w\.\d]+)(?P(?::\d+)?)?(?P(?:\/\d)?)?(?P(?:\/[\w\/\d\-?.]*)?)?$' - match = re.match(regex, url) - protocol = match.group('protocol') - itemtype = match.group('type') - host = match.group('host') - port = match.group('port') - resource = match.group('resource') - - if protocol != 'gopher://' and protocol: - return False - if itemtype and not self.types[itemtype[1]]: - return False - elif not itemtype: - itemtype = '/1' - if not host: - return False - if not resource: - resource = '/' - if port: - port = port[1:] - - if execute: - self.make_connection(resource, host, itemtype, port) - else: - return {'host': host, 'resource': resource, 'type': itemtype} - - -if __name__ == '__main__': - inp = sys.argv[1:] - if len(inp) >= 2: - test = Tunnel() - test.make_connection(inp[1],inp[0],'70') - else: - print('Incorrect request') diff --git a/connect.py b/connect.py new file mode 100644 index 0000000..d9e0adf --- /dev/null +++ b/connect.py @@ -0,0 +1,29 @@ +import socket + +class connect: + def __init__(self): + self.raw_response = None + self.filetype = None + + def request(self, resource, host, itemtype, port=70): + #connects to server and returns list with response type and body + socket_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_conn.settimeout(20.0) + socket_conn.connect((host, port)) + socket_conn.sendall((resource + '\r\n').encode('utf-8')) + + if itemtype in ['I','p','g']: + response = socket_conn.makefile(mode = 'rb', errors = 'ignore') + else: + response = socket_conn.makefile(mode = 'r', errors = 'ignore') + + try: + self.raw_response = response.read() + self.filetype = itemtype + except UnicodeDecodeError: + self.raw_response = '3Error decoding server response\tfalse\tnull.host\t1' + self.filetype = '3' + + socket_conn.close() + + return {'type': self.filetype, 'body': self.raw_response} diff --git a/digger.py b/digger.py deleted file mode 100755 index 4421289..0000000 --- a/digger.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python3 - -import tkinter as tk -from conn import Tunnel as go -import time -import sys -import json -import os.path - -class GUI: - def __init__(self): - self.history = [] - self.history_location = -1 - self.message_bar_content = '' - self.config = None - self.read_config() - - #colors - self.FG = 'black' - self.BG = 'white' - self.LINK = 'blue' - self.ACTIVELINK = 'red' - self.HLB = '#e37800' - self.HLF = 'white' - self.STATUS = 'silver' - self.ERROR = 'red' - - #create and configure root window - self.root = tk.Tk(className='Digger') - self.root.title('Digger') - self.root.geometry("1200x800") - self.add_assets() - - #main frame objects - self.top_bar = tk.Frame(self.root, padx=10, height=70, relief=tk.FLAT, bd=2) - self.body = tk.Frame(self.root, relief=tk.RIDGE, bd=2) - self.status_bar = tk.Frame(self.root, height="20", bg=self.STATUS, takefocus=0) - - #top bar objects - self.btn_back = tk.Button(self.top_bar, image=self.img_back, bd=0, highlightthickness=0, takefocus=1) - self.btn_forward = tk.Button(self.top_bar,image=self.img_forward, bd=0, highlightthickness=0) - self.btn_favorite = tk.Button(self.top_bar,image=self.img_favorite, bd=0, highlightthickness=0) - self.btn_home = tk.Button(self.top_bar, image=self.img_home, bd=0, highlightthickness=0) - self.entry_url = tk.Entry(self.top_bar, selectbackground=self.HLB, selectforeground=self.HLF, highlightcolor=self.HLB) - self.btn_menu = tk.Button(self.top_bar, image=self.img_menu, bd=0, highlightthickness=0) - - #body objects - self.scroll_bar = tk.Scrollbar(self.body) - self.site_display = tk.Text(self.body, bg=self.BG, foreground=self.FG, padx=20, pady=20, wrap=tk.WORD, state=tk.DISABLED, spacing2=5, spacing1=5, yscrollcommand=self.scroll_bar.set) - self.scroll_bar.config(command=self.site_display.yview, width=20, relief=tk.RIDGE) - self.site_display.tag_configure('linkcolor', foreground=self.LINK, spacing1=5) - self.site_display.tag_configure('type_tag', background=self.FG, foreground=self.BG, spacing2=0, spacing1=0) - self.site_display.tag_configure('error_text', foreground=self.ERROR, spacing1=5, spacing2=5, spacing3=5) - - #status bar objects - self.status_info = tk.Label(self.status_bar, textvariable=self.message_bar_content, bg=self.STATUS, takefocus=0) - - self.pack_geometry() - self.add_status_titles() - self.add_event_listeners() - - #load the home screen - self.load_home_screen() - - #--------------------------------------------------- - - def add_event_listeners(self): - buttons = [ - self.btn_back, - self.btn_forward, - self.btn_favorite, - self.btn_home, - self.btn_menu - ] - - for x in buttons: - x.bind('', self.update_status) - x.bind('', self.clear_status) - self.entry_url.bind('', self.execute_address) - self.btn_back.bind('', self.go_back) - self.btn_forward.bind('', self.go_forward) - self.btn_home.bind('', self.load_home_screen) - self.site_display.bind("", lambda event: self.site_display.yview_scroll(-1, 'units')) - self.site_display.bind("", lambda event: self.site_display.yview_scroll(1, 'units')) - self.site_display.bind("", lambda event: self.site_display.focus_set()) - self.entry_url.bind("", lambda event: self.entry_url.focus_set()) - self.root.protocol('WM_DELETE_WINDOW', self.close_window) - - - def pack_geometry(self): - self.top_bar.pack(expand=False,fill=tk.BOTH,side=tk.TOP,anchor=tk.NW) - self.top_bar.pack_propagate(False) - self.body.pack(expand=True,fill=tk.BOTH,side=tk.TOP) - self.status_bar.pack(expand=False,fill=tk.X,side=tk.TOP,anchor=tk.SW) - self.btn_back.pack(side=tk.LEFT, padx=(0,20)) - self.btn_forward.pack(side=tk.LEFT, padx=(0,20)) - self.btn_home.pack(side=tk.LEFT, padx=(0,20)) - self.entry_url.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=10, ipadx=10) - self.btn_favorite.pack(side=tk.LEFT, padx=(10,10)) - self.btn_menu.pack(side=tk.LEFT) - self.scroll_bar.pack(side=tk.RIGHT,fill=tk.Y) - self.site_display.pack(expand=True, side=tk.TOP, fill=tk.BOTH) - self.status_info.pack(side=tk.LEFT) - - - def add_status_titles(self): - self.btn_back.pop_title = 'Back' - self.btn_forward.pop_title = 'Forward' - self.btn_favorite.pop_title = 'Favorite' - self.btn_home.pop_title = 'Home' - self.btn_menu.pop_title = 'Menu' - - - def add_assets(self): - self.img_back = tk.PhotoImage(file='./btn_back.png') - self.img_forward = tk.PhotoImage(file='./btn_forward.png') - self.img_favorite = tk.PhotoImage(file='./btn_refresh.png') - self.img_menu = tk.PhotoImage(file='./btn_menu.png') - self.img_home = tk.PhotoImage(file='./btn_home.png') - self.img_menu = tk.PhotoImage(file='./btn_menu.png') - self.message_bar_content = tk.StringVar() - self.message_bar_content.set('Ready.') - - - def execute_address(self, event, btn_url=False): - if btn_url and btn_url != -1: - url = btn_url - elif btn_url and btn_url == -1: - return False - else: - url = self.entry_url.get() - if url == 'home': - self.load_home_screen() - return True - self.history = self.history[:self.history_location+1] - self.history.append(url) - self.history_location = len(self.history) - 1 - self.site_display.focus_set() - request = go() - request.parse_url(url) - self.send_to_screen(request.text_output) - self.config["last_viewed"] = url - return True - - - def gotolink(self, event, href, tag_name): - res = event.widget - res.tag_config(tag_name, background=self.ACTIVELINK) # change tag text style - res.update_idletasks() # make sure change is visible - time.sleep(.5) # optional delay to show changed text - self.entry_url.delete(0,tk.END) - self.entry_url.insert(tk.END,href) - self.execute_address(0) #the zero is meaningless, but the function expects a param - res.tag_config(tag_name, background=self.BG) # restore tag text style - res.update_idletasks() - - - def hoverlink(self, event, href, tag_name): - self.update_status(event, href) - e = event.widget - e.tag_config(tag_name, underline=1) - self.site_display.config(cursor="arrow") - e.update_idletasks() - - def load_home_screen(self,event=False): - with open('./home.gopher','r') as f: - data = f.read() - parser = go() - data = parser.gopher_to_text(data) - data.insert(0,'1') - self.entry_url.delete(0, tk.END) - self.entry_url.insert(tk.END, 'home') - self.send_to_screen(data, True) - - def load_favorites(self): - header = 'i#############\tfalse\tnull.host\t1\r\ni manually edit in go.config.json\tfalse\tnull.host\t1\r\n or add using the favorites button\tfalse\tnull.host\t1\r\ni\tfalse\tnull.host\t1\r\n' - #soon add code to load in favorites here - self.send_to_screen(header, False) - - - def send_to_screen(self, data, clear=True): - link_count = 0 - self.site_display.config(state=tk.NORMAL) - if clear: - self.site_display.delete(1.0, tk.END) - types = { - '0': '( TEXT )', - '1': '( MENU )', - '2': None, - '3': '( ERROR)', - '4': None, - '5': None, - '6': None, - '7': '( INTR )', - '8': '(TELNET)', - '9': '( BIN )', - '+': None, - 'g': '( GIF )', - 'I': '( IMG )', - 't': None, - 'h': '( HTML )', - 'i': '( INFO )', - 's': '( SOUND)' - } - - if data[0] == '0': - self.site_display.insert(tk.END, data[1]) - elif data[0] in ['1','3']: - for x in data[1:]: - if x['type'] == 'i': - self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) - elif x['type'] == '3': - self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) - else: - # adapted from: - # https://stackoverflow.com/questions/27760561/tkinter-and-hyperlinks - if x['port'] and x['port'][0] != ':': - x['port'] = ':{}'.format(x['port']) - - link = 'gopher://{}{}/{}{}'.format(x['host'], x['port'], x['type'], x['resource']) - tag_name = 'link{}'.format(link_count) - callback = (lambda event, href=link, tag_name=tag_name: self.gotolink(event, href, tag_name)) - hover = (lambda event, href=link, tag_name=tag_name: self.hoverlink(event, href, tag_name)) - clear = (lambda event, tag_name=tag_name: self.clear_status(event, tag_name)) - self.site_display.tag_bind(tag_name, "", callback) - self.site_display.tag_bind(tag_name, "", hover) - self.site_display.tag_bind(tag_name, '', clear) - self.site_display.insert(tk.END, types[x['type']], ('type_tag',)) - self.site_display.insert(tk.END,'\t\t') - self.site_display.insert(tk.END, x['description'], (tag_name,'linkcolor')) - self.site_display.insert(tk.END, '\n') - link_count += 1 - self.site_display.config(state=tk.DISABLED) - - - def update_status(self, event, href=False): - if href: - self.message_bar_content.set(href) - else: - self.message_bar_content.set(event.widget.pop_title) - - - def clear_status(self, event, tag_name=False): - if tag_name: - e = event.widget - e.tag_config(tag_name, underline=0) - self.site_display.config(cursor='xterm') - e.update_idletasks() - self.message_bar_content.set('') - - - def go_back(self, event): - if len(self.history) > 1 and self.history_location > 0: - self.history_location -= 1 - href = self.history[self.history_location] - self.entry_url.delete(0, tk.END) - self.entry_url.insert(tk.END, href) - else: - href = -1 - self.execute_address(False, href) - - - def go_forward(self, event): - if len(self.history) > 1 and self.history_location < len(self.history) - 1: - self.history_location += 1 - href = self.history[self.history_location] - self.entry_url.delete(0,tk.END) - self.entry_url.insert(tk.END, href) - else: - href = -1 - self.execute_address(False, href) - - def read_config(self, url='./go.config.json'): - if not os.path.isfile(url): - self.create_config() - with open('./go.config.json', 'r') as f: - config = f.read() - config = json.loads(config) - self.config = config - - def write_config(self, config, url='./go.config.json'): - with open(url, 'w') as f: - data = json.dumps(config) - f.write(data) - - def create_config(self): - config = {"favorites": [],"last_viewed": None} - self.write_config(config) - - def close_window(self): - self.write_config(self.config) - self.root.destroy() - - - -if __name__ == '__main__': - app = GUI() - app.root.mainloop() - - diff --git a/go.config.json b/go.config.json deleted file mode 100644 index 879e5ab..0000000 --- a/go.config.json +++ /dev/null @@ -1 +0,0 @@ -{"favorites": [], "last_viewed": "gopher://gopher.club:70/1/phlogs/"} \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..2ae690a --- /dev/null +++ b/gui.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 + +import tkinter as tk +from connect import connect as conn +from parser import parser +import time +import sys +import json +import os.path +from io import BytesIO +from PIL import Image, ImageTk +import webbrowser as wb + +class GUI: + def __init__(self): + self.history = [] + self.history_location = -1 + self.message_bar_content = '' + self.config = None + self.read_config() + self.conn = conn() + self.parser = parser() + + #colors + self.FG = '#E0E2E4' + self.BG = '#2F393C' + self.LINK = '#E8E2B7' + self.ACTIVELINK = '#678CB1' + self.HLB = '#804000' + self.HLF = '#E0E2E4' + self.STATUS_BG = '#293134' + self.STATUS_FG = '#FFCD22' + self.ERROR = '#E8E2B7' + self.BAR_BG = '#293134' + self.BAR_FG = '#2F393C' + self.BAR_HLB = '#804000' + self.BAR_HLF = '#E0E2E4' + self.BAR_SLOT = '#E0E2E4' + self.SCROLL = '#434A57' + self.TYPES = '#A082BD' + + + #create and configure root window + self.root = tk.Tk(className='Burrow') + self.root.title('Burrow') + self.root.geometry("1200x800") + self.add_assets() + + #main frame objects + self.top_bar = tk.Frame(self.root, padx=10, height=50, relief=tk.FLAT, bd=2, bg=self.BAR_BG) + self.body = tk.Frame(self.root, relief=tk.FLAT, bd=0, bg=self.BG) + self.status_bar = tk.Frame(self.root, height="20", relief=tk.FLAT, bg=self.STATUS_BG, takefocus=0) + + #top bar objects + self.btn_back = tk.Button(self.top_bar, image=self.img_back, bd=0, highlightthickness=0, takefocus=1, bg=self.BAR_BG) + self.btn_forward = tk.Button(self.top_bar,image=self.img_forward, bd=0, highlightthickness=0, bg=self.BAR_BG) + self.btn_favorite = tk.Button(self.top_bar,image=self.img_favorite, bd=0, highlightthickness=0, bg=self.BAR_BG) + self.btn_home = tk.Button(self.top_bar, image=self.img_home, bd=0, highlightthickness=0, bg=self.BAR_BG) + self.entry_url = tk.Entry(self.top_bar, selectbackground=self.HLB, selectforeground=self.HLF, highlightcolor=self.FG, highlightbackground=self.BAR_BG, fg=self.BAR_FG, bg=self.BAR_SLOT) + self.btn_menu = tk.Button(self.top_bar, image=self.img_menu, bd=0, highlightthickness=0, bg=self.BAR_BG) + + #body objects + self.scroll_bar = tk.Scrollbar(self.body, bg=self.BAR_BG, bd=0, highlightthickness=0, troughcolor=self.BG, activebackground=self.SCROLL, activerelief=tk.RAISED) + self.site_display = tk.Text(self.body, bg=self.BG, foreground=self.FG, padx=20, pady=20, wrap=tk.WORD, state=tk.DISABLED, spacing2=2, spacing1=2, spacing3=2, yscrollcommand=self.scroll_bar.set, highlightcolor=self.BG, highlightbackground=self.BAR_BG, relief=tk.FLAT) + self.scroll_bar.config(command=self.site_display.yview, width=20, relief=tk.RIDGE) + self.site_display.tag_configure('linkcolor', foreground=self.LINK, spacing1=5, spacing2=5, spacing3=5) + self.site_display.tag_configure('type_tag', background=self.BG, foreground=self.TYPES, spacing2=1, spacing1=1, spacing3=1) + self.site_display.tag_configure('error_text', foreground=self.ERROR, spacing1=5, spacing2=5, spacing3=5) + + #status bar objects + self.status_info = tk.Label(self.status_bar, textvariable=self.message_bar_content, bg=self.STATUS_BG, takefocus=0, fg=self.ACTIVELINK) + + self.pack_geometry() + self.add_status_titles() + self.add_event_listeners() + + #load the home screen + self.load_home_screen(1) + + #-----------Start GUI configuration----------------------- + + def add_event_listeners(self): + buttons = [ + self.btn_back, + self.btn_forward, + self.btn_favorite, + self.btn_home, + self.btn_menu + ] + + for x in buttons: + x.bind('', self.update_status) + x.bind('', self.clear_status) + x.config(activebackground=self.BG) + self.entry_url.bind('', self.execute_address) + self.btn_back.bind('', self.go_back) + self.btn_forward.bind('', self.go_forward) + self.btn_home.bind('', self.load_home_screen) + self.site_display.bind("", lambda event: self.site_display.yview_scroll(-1, 'units')) + self.site_display.bind("", lambda event: self.site_display.yview_scroll(1, 'units')) + self.site_display.bind("", lambda event: self.site_display.focus_set()) + self.entry_url.bind("", lambda event: self.entry_url.focus_set()) + self.root.protocol('WM_DELETE_WINDOW', self.close_window) + + + def pack_geometry(self): + self.top_bar.pack(expand=False,fill=tk.BOTH,side=tk.TOP,anchor=tk.NW) + self.top_bar.pack_propagate(False) + self.body.pack(expand=True,fill=tk.BOTH,side=tk.TOP) + self.status_bar.pack(expand=False,fill=tk.X,side=tk.TOP,anchor=tk.SW) + self.btn_back.pack(side=tk.LEFT, padx=(0,20)) + self.btn_forward.pack(side=tk.LEFT, padx=(0,20)) + self.btn_home.pack(side=tk.LEFT, padx=(0,20)) + self.entry_url.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=5, ipadx=10) + self.btn_favorite.pack(side=tk.LEFT, padx=(10,10)) + self.btn_menu.pack(side=tk.LEFT) + self.scroll_bar.pack(side=tk.RIGHT,fill=tk.Y) + self.site_display.pack(expand=True, side=tk.TOP, fill=tk.BOTH) + self.status_info.pack(side=tk.LEFT) + + + def add_status_titles(self): + self.btn_back.pop_title = 'Back' + self.btn_forward.pop_title = 'Forward' + self.btn_favorite.pop_title = 'Favorite' + self.btn_home.pop_title = 'Home' + self.btn_menu.pop_title = 'Menu' + + + def add_assets(self): + self.img_back = tk.PhotoImage(file='./images/back.png') + self.img_forward = tk.PhotoImage(file='./images/forward.png') + self.img_favorite = tk.PhotoImage(file='./images/favorite.png') + self.img_home = tk.PhotoImage(file='./images/home.png') + self.img_menu = tk.PhotoImage(file='./images/settings.png') + self.message_bar_content = tk.StringVar() + self.message_bar_content.set('Ready.') + + + # ------------Start navigation methods---------------------------- + + + def execute_address(self, event=False, btn_url=False, history=True): + url = btn_url if btn_url else self.entry_url.get() + if url == 'home': + adjust_history = None if btn_url else 1 + self.load_home_screen(adjust_history) + return True + + parsed_url = self.parser.parse_url(url) + + if not parsed_url: + # To do: build errors class to handle displaying errors + # return errors.url_error + return False + + + if parsed_url['type'] == '7': + self.send_to_screen(parsed_url, parsed_url['type']) + + response = self.conn.request(self.parser.resource, self.parser.host, self.parser.filetype, self.parser.port) + + if not response: + # To do: build errors class to handle displaying errors + # return errors.connection_error_NUMBER + return False + + if history: + self.history = self.history[:self.history_location+1] + self.history.append(url) + self.history_location = len(self.history) - 1 + + # Get the data to the screen + self.site_display.focus_set() + self.config["last_viewed"] = url + + self.send_to_screen(self.conn.raw_response, self.conn.filetype) + return True + + + def add_to_history(self, url): + self.history = self.history[:self.history_location+1] + self.history.append(url) + self.history_location = len(self.history) - 1 + + + + def gotolink(self, event, href, tag_name): + if href[:4] == 'http': + wb.open(href, 2, True) + return True + element = event.widget + element.tag_config(tag_name, background=self.ACTIVELINK) + element.update_idletasks() # make sure change is visible + time.sleep(.5) # optional delay to show changed text + self.entry_url.delete(0,tk.END) + self.entry_url.insert(tk.END,href) + success = self.execute_address() + element.tag_config(tag_name, background=self.BG) # restore tag text style + element.update_idletasks() + + + def load_home_screen(self,event=None): + with open('./home.gopher','r') as f: + data = f.read() + self.entry_url.delete(0, tk.END) + self.entry_url.insert(tk.END, 'home') + if event is not None: + self.add_to_history('home') + self.send_to_screen(data, '1') + + + def go_back(self, event): + if len(self.history) <= 1 or self.history_location <= 0: + return False + + self.history_location -= 1 + href = self.history[self.history_location] + self.populate_url_bar(href) + self.execute_address(False, href, False) + + + def go_forward(self, event): + if len(self.history) <= 1 or self.history_location >= len(self.history) - 1: + return False + + self.history_location += 1 + href = self.history[self.history_location] + self.populate_url_bar(href) + self.execute_address(False, href, False) + + + #-------------Start view methods---------------- + + + def load_favorites(self): + header = 'i#############\tfalse\tnull.host\t1\r\ni manually edit in go.config.json\tfalse\tnull.host\t1\r\n or add using the favorites button\tfalse\tnull.host\t1\r\ni\tfalse\tnull.host\t1\r\n' + #soon add code to load in favorites here + self.send_to_screen(data=header, clear=False) + + def show_menu(self, data, clear=True): + if not data: + #error handling will go here + return False + + types = { + '0': '( TXT )', + '1': '( MNU )', + '3': '( ERR )', + '7': '( INT )', + '9': '( BIN )', + 'g': '( GIF )', + 'I': '( IMG )', + 'h': '( HTM )', + 'i': '( INF )', + 's': '( SND )', + 'p': '( PNG )' + } + + self.site_display.config(state=tk.NORMAL) + + if clear: + self.site_display.delete(1.0, tk.END) + + link_count = 0 + + for x in data[1:]: + if x['type'] == 'i': + self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) + elif x['type'] == '3': + self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) + elif x['type'] in types: + + # adapted from: + # https://stackoverflow.com/questions/27760561/tkinter-and-hyperlinks + if x['port'] and x['port'][0] != ':': + x['port'] = ':{}'.format(x['port']) + + if x['type'] == 'h': + link = '{}/{}'.format(x['host'], x['resource']) + link = 'http://{}'.format(link.replace('//','/')) + else: + link = 'gopher://{}{}/{}{}'.format(x['host'], x['port'], x['type'], x['resource']) + + tag_name = 'link{}'.format(link_count) + callback = (lambda event, href=link, tag_name=tag_name: self.gotolink(event, href, tag_name)) + hover = (lambda event, href=link, tag_name=tag_name: self.hoverlink(event, href, tag_name)) + clear = (lambda event, tag_name=tag_name: self.clear_status(event, tag_name)) + self.site_display.tag_bind(tag_name, "", callback) + self.site_display.tag_bind(tag_name, "", hover) + self.site_display.tag_bind(tag_name, '', clear) + self.site_display.insert(tk.END, types[x['type']], ('type_tag',)) + self.site_display.insert(tk.END,'\t\t') + self.site_display.insert(tk.END, x['description'], (tag_name,'linkcolor')) + self.site_display.insert(tk.END, '\n') + link_count += 1 + + self.site_display.config(state=tk.DISABLED) + + return True + + + def show_text(self, data): + self.site_display.config(state=tk.NORMAL) + self.site_display.delete(1.0, tk.END) + self.site_display.insert(tk.END, data[:-2]) + self.site_display.config(state=tk.DISABLED) + + + def show_image(self, data): + self.current_image = self.build_image(data) + self.site_display.config(state=tk.NORMAL) + self.site_display.delete(1.0, tk.END) + self.site_display.image_create(tk.END, image = self.current_image) + self.site_display.config(state=tk.DISABLED) + + + def send_to_screen(self, data, itemtype='1', clear=True): + if itemtype == '0': + self.show_text(data) + elif itemtype in ['1','3']: + data = self.parser.parse_menu(data) + self.show_menu(data, clear) + elif itemtype in ['p','I','g']: + self.show_image(data) + elif itemtype == '7': + pass + + + def update_status(self, event, href=False): + if href: + self.message_bar_content.set(href) + else: + self.message_bar_content.set(event.widget.pop_title) + + + def clear_status(self, event, tag_name=False): + if tag_name: + e = event.widget + e.tag_config(tag_name, underline=0) + self.site_display.config(cursor='xterm') + e.update_idletasks() + self.message_bar_content.set('') + + + def populate_url_bar(self, url): + self.entry_url.delete(0, tk.END) + self.entry_url.insert(tk.END, url) + + + def hoverlink(self, event, href, tag_name): + self.update_status(event, href) + e = event.widget + e.tag_config(tag_name, underline=1, foreground=self.LINK) + self.site_display.config(cursor="arrow") + e.update_idletasks() + + def build_image(self, bytes_str): + stream = BytesIO(bytes_str) + pilimage = Image.open(stream) + tkimage = ImageTk.PhotoImage(pilimage) + return tkimage + + + + #--------Start file handling methods------------ + + + def read_config(self, url='./go.config.json'): + if not os.path.isfile(url): + self.create_config() + with open('./go.config.json', 'r') as f: + config = f.read() + config = json.loads(config) + self.config = config + + + def write_config(self, config, url='./go.config.json'): + with open(url, 'w') as f: + data = json.dumps(config) + f.write(data) + + + def create_config(self): + config = {"favorites": [],"last_viewed": None} + self.write_config(config) + + + def close_window(self): + self.write_config(self.config) + self.root.destroy() + + + +if __name__ == '__main__': + app = GUI() + app.root.mainloop() + + diff --git a/home.gopher b/home.gopher index 6e8e4d2..9abdffe 100644 --- a/home.gopher +++ b/home.gopher @@ -1,10 +1,10 @@ i false null.host 1 -i██████╗ ██╗ ██████╗ ██████╗ ███████╗██████╗ false null.host 1 -i██╔══██╗██║██╔════╝ ██╔════╝ ██╔════╝██╔══██╗ false null.host 1 -i██║ ██║██║██║ ███╗██║ ███╗█████╗ ██████╔╝ false null.host 1 -i██║ ██║██║██║ ██║██║ ██║██╔══╝ ██╔══██╗ false null.host 1 -i██████╔╝██║╚██████╔╝╚██████╔╝███████╗██║ ██║ false null.host 1 -i╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ false null.host 1 +i██████╗ ██╗ ██╗██████╗ ██████╗ ██████╗ ██╗ ██╗ false null.host 1 +i██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔═══██╗██║ ██║ false null.host 1 +i██████╔╝██║ ██║██████╔╝██████╔╝██║ ██║██║ █╗ ██║ false null.host 1 +i██╔══██╗██║ ██║██╔══██╗██╔══██╗██║ ██║██║███╗██║ false null.host 1 +i██████╔╝╚██████╔╝██║ ██║██║ ██║╚██████╔╝╚███╔███╔╝ false null.host 1 +i╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ false null.host 1 i false null.host 1 i##########CONTENT PORTALS########## false null.host 1 i false null.host 1 diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..fc08caf --- /dev/null +++ b/parser.py @@ -0,0 +1,54 @@ +import re + +# Handles parsing gopher data: +# URLs, Menus + +class parser: + + def __init__(self): + self.host = None + self.resource = None + self.filetype = None + self.protocol = None + self.port = None + self.menu = None + + def parse_url(self, url): + # Take in a URL and output a dict of the url parts + + regex = r'^(?P(?:gopher:\/\/)?)?(?P[\w\.\d]+)(?P(?::\d+)?)?(?P(?:\/[\dgIp])?)?(?P(?:\/[\w\/\d\-?.]*)?)?$' + + match = re.match(regex, url) + protocol = match.group('protocol') + itemtype = match.group('type') + host = match.group('host') + port = match.group('port') + resource = match.group('resource') + + if not host: + return False + + if not resource: + resource = '/' + + + self.filetype = itemtype[len(itemtype) - 1] if itemtype else '1' + self.protocol = protocol if protocol else 'gopher://' + self.port = int(port[1:]) if port else 70 + self.host = host + self.resource = resource + + return {'host': self.host, 'resource': self.resource, 'type': self.filetype, 'protocol': self.protocol, 'port': self.port} + + + def parse_menu(self, text): + # Take in text from connection and output a list + # w/ objects representing each menu item + + message_list = text.split('\n') + message_list = [x.split('\t') for x in message_list] + message_list = [{'type': x[0][0], 'description': x[0][1:], 'resource': x[1], 'host': x[2], 'port': x[3]} for x in message_list if len(x) >= 4] + self.menu = message_list + + return message_list +