├── .gitignore ├── AskForm.py ├── Associations.py ├── AssociationsEditor.py ├── BUGS ├── Bookmarks ├── Bookmark.py ├── BookmarkEditor.py ├── BookmarkFactory.py ├── BookmarkMenu.py └── __init__.py ├── COPYING ├── Cache.py ├── ChangeLog ├── Connection.py ├── ContentFrame.py ├── Dialogs.py ├── GUIAskForm.py ├── GUIDirectory.py ├── GUIError.py ├── GUIFile.py ├── GUIQuestion.py ├── GUISaveFile.py ├── GUISearch.py ├── GopherConnection.py ├── GopherObject.py ├── GopherResource.py ├── GopherResponse.py ├── List.py ├── ListNode.py ├── Makefile ├── Options.py ├── Question.py ├── README.md ├── ResourceInformation.py ├── State.py ├── TODO ├── TkGui.py ├── Tree.py ├── default_bookmarks.xml ├── default_options ├── forg.py ├── gopher.py ├── mini-forg.py ├── test ├── README ├── bmarks.xml ├── bmetest.py ├── bmtest.py ├── ltest.py ├── newbm.xml ├── things.py └── tri.py ├── utils.py └── xbel-1.0.dtd /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/* 3 | -------------------------------------------------------------------------------- /AskForm.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # Released under the terms of the GNU General Public License 4 | # $Id: AskForm.py,v 1.6 2001/04/07 19:12:44 s2mdalle Exp $ 5 | # An AskForm is essentially a conglomeration of Question objects. 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ######################################################################### 21 | 22 | import GopherResponse 23 | import Question 24 | 25 | class AskForm(GopherResponse.GopherResponse): 26 | def __init__(self, askformdata=""): 27 | GopherResponse.GopherResponse.__init__(self) 28 | self.questions = [] 29 | self.setAskFormData(askformdata) 30 | return None 31 | def questionCount(self): 32 | return len(self.questions) 33 | def nthQuestion(self, nth): 34 | return self.questions[nth] 35 | def setAskFormData(self, data): 36 | self.data = data 37 | 38 | print("ASKFORM: Parsing data block:\n", data) 39 | self.lines = self.data.split("\n") 40 | 41 | for line in self.lines: 42 | line = line.strip() 43 | if line == '' or line == '.': 44 | continue 45 | try: 46 | q = Question.Question(line) 47 | except Question.QuestionException as qstr: 48 | print("Error parsing question \"%s\": %s" % (line, qstr)) 49 | continue 50 | 51 | self.questions.append(q) 52 | -------------------------------------------------------------------------------- /Associations.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Handles associations between file types and programs. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | ######################################################################## 20 | import re 21 | import os 22 | 23 | 24 | class AssociationsException(Exception): 25 | def __init__(self, message): 26 | super(AssociationsException, self).__init__(message) 27 | 28 | 29 | class Associations: 30 | verbose = None 31 | DELIMITER = " => " 32 | 33 | def __init__(self): 34 | self.dict = {} 35 | self.threads = 0 36 | return None 37 | 38 | def addAssociation(self, suffix, pgm): 39 | """Adds an association to the list. suffix holds the file 40 | extension, and pgm holds the name of the executing program. Example 41 | is suffix=.jpg and pgm=gimp $1""" 42 | 43 | if suffix[0] == '.': 44 | suffix = suffix[1:] 45 | self.dict[suffix] = pgm 46 | return None 47 | 48 | def save(self, filename): 49 | """Saves associations to filename""" 50 | return self.writeToFile(filename) 51 | 52 | def writeToFile(self, filename): 53 | """Writes text representation to filename that can be loaded later""" 54 | fp = open(filename, "w") 55 | 56 | lines = ["FORG associations. This is the mechanism that allows the", 57 | "FORG to launch particular programs when dealing with", 58 | "different file extensions. The format is like this:", 59 | "file_suffix%sprogram_to_launch" % self.DELIMITER, 60 | "where file_suffix is something such as \".jpg\", etc.", 61 | "program_to_launch is the command line of the program that", 62 | "you want launched. You may use $1 to represent the file", 63 | "so to launch a HTML file, you might use:", 64 | ".html%s/usr/X11/bin/netscape $1" % self.DELIMITER] 65 | 66 | list(map(lambda line,f=fp: f.write("# %s\n" % line), 67 | lines)) 68 | 69 | for key in list(self.dict.keys()): 70 | fp.write("%s%s%s\n" % (key, self.DELIMITER, self.dict[key])) 71 | fp.flush() 72 | fp.close() 73 | return 1 74 | 75 | def loadFromFile(self, filename): 76 | """Loads instance of Associations from filename""" 77 | fp = open(filename, "r") 78 | self.dict = {} 79 | 80 | for line in fp.readlines(): 81 | line = line.strip() 82 | if len(line) > 0 and line[0] == '#': 83 | continue # Skip comments. 84 | 85 | try: 86 | [key, value] = line.split(self.DELIMITER, 2) 87 | self.addAssociation(key, value) 88 | except: 89 | print("Error parsing line in associations file: %s" % line) 90 | 91 | fp.close() 92 | return 1 93 | 94 | def isEmpty(self): 95 | return len(self.dict) == 0 96 | 97 | def getFileTypes(self): 98 | """Returns the file suffixes the Association list knows of""" 99 | return list(self.dict.keys()) 100 | 101 | def getProgramString(self, key): 102 | try: 103 | ans = self.dict[key] 104 | return ans 105 | except KeyError: 106 | return None 107 | 108 | def removeAssociation(self, suffix): 109 | try: 110 | del(self.dict[suffix]) 111 | except KeyError: 112 | pass 113 | 114 | def getAssociation(self, filename): 115 | """Given a particular filename, find the correct association 116 | for it, if any.""" 117 | # Turn it into a real filename so some programs won't go stupid 118 | # on us and try to look up a partial URL in a filename, particularly 119 | # with cache filenames. 120 | # print "Finding assoc for %s" % filename 121 | filename = "." + os.sep + filename 122 | matchFound = None 123 | ind = len(filename)-1; 124 | while not matchFound and ind != -1: 125 | str = filename[ind+1:] 126 | assoc = None 127 | try: 128 | assoc = self.dict[str] 129 | except: 130 | pass 131 | ind = filename.rfind(".", 0, ind) 132 | if assoc: 133 | matchFound = 1 134 | 135 | if ind == -1 or not matchFound: 136 | # print "Couldn't find association for this filetype." 137 | return None 138 | 139 | # print "Found assoc %s for filename %s" % (assoc, filename) 140 | return assoc 141 | 142 | def applyAssociation(self, filename, assoc=None): 143 | """Given a filename and an association, execute the helper program 144 | in order to properly process the file.""" 145 | 146 | if assoc == None: 147 | assoc = self.getAssociation(filename) 148 | 149 | if assoc == None or assoc == '': 150 | raise AssociationsException("No association found.") 151 | 152 | # Assoc holds the program name 153 | assoc = re.sub("$1", "\"" + filename + "\"", assoc, 1) 154 | fp = os.popen(assoc) 155 | print("Process dump: %s" % assoc) 156 | try: 157 | while 1: 158 | line = fp.readline() 159 | if line == '': 160 | break; 161 | print(line) 162 | except: 163 | print("Process %s exited with an exception." % assoc) 164 | return None 165 | print("Process \"%s\" finished up and exited." % assoc) 166 | return 1 167 | 168 | 169 | -------------------------------------------------------------------------------- /AssociationsEditor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # This pops up a dialog box and allows the user to associate file name 4 | # extensions with various programs to run. 5 | # 6 | # It returns an Associations object, and can optionally take one as an 7 | # argument. 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | ######################################################################## 23 | 24 | import Associations 25 | from gopher import * 26 | from tkinter import * 27 | import Pmw 28 | 29 | class AssociationsEditor: 30 | DELIMITER = Associations.Associations.DELIMITER 31 | def __init__(self, parent, baseAssoc=Associations.Associations()): 32 | self.parent = parent 33 | self.dialog = Pmw.Dialog(parent, title='Edit Associations', 34 | buttons=('OK', 'Cancel'), defaultbutton='OK', 35 | command=self.dispatch) 36 | self.assoc = baseAssoc 37 | self.frame = self.dialog.interior() 38 | self.frame.pack(expand=1, fill='both') 39 | self.left_side = Frame(self.frame) 40 | self.middle = Frame(self.frame) 41 | self.right_side = Frame(self.frame) 42 | 43 | self.left_side.pack(side='left', expand=1, fill='both') 44 | self.middle.pack(side='left', expand=1, fill='both') 45 | self.right_side.pack(side='left', expand=1, fill='both') 46 | 47 | # Left side widgets 48 | inputbox = Frame(self.left_side) 49 | inputbox.grid(row=0, column=0, columnspan=2, rowspan=2, sticky=W) 50 | 51 | # File extension entry box and label. 52 | ftlabel = Label(inputbox, text='File extension:') 53 | self.ftEntry = Entry(inputbox, width=6) 54 | ftlabel.grid(row=0, column=0, sticky=W) 55 | self.ftEntry.grid(row=0, column=1, sticky=W) 56 | 57 | # Application entry box and label 58 | applabel = Label(inputbox, text='Application:') 59 | self.appEntry = Entry(inputbox, width=30) 60 | applabel.grid(row=1, column=0, sticky=W) 61 | self.appEntry.grid(row=1, column=1, sticky=W) 62 | 63 | # Instruction group box. 64 | group = Pmw.Group(self.left_side, tag_text='Instructions:') 65 | group.grid(row=2, column=0, rowspan=2, columnspan=2, sticky=W) 66 | instr1 = Label(group.interior(), 67 | text='When entering in programs associated with files,') 68 | instr2 = Label(group.interior(), 69 | text='Use $1 to represent the file being launched') 70 | instr3 = Label(group.interior(), 71 | text='Filename extensions may be provided with or') 72 | instr4 = Label(group.interior(), 73 | text='dots. I.e. ".html" is the same as "html"') 74 | instr5 = Label(group.interior(), 75 | text="Example: .html might be associated with") 76 | instr6 = Label(group.interior(), 77 | text="netscape $1") 78 | instr7 = Label(group.interior(), 79 | text=" ") 80 | instr8 = Label(group.interior(), 81 | text="Extensions are case-sensitive") 82 | instr1.pack(side='top') 83 | instr2.pack(side='top') 84 | instr3.pack(side='top') 85 | instr4.pack(side='top') 86 | instr5.pack(side='top') 87 | instr6.pack(side='top') 88 | instr7.pack(side='top') 89 | instr8.pack(side='top') 90 | 91 | # Middle widgets 92 | self.addAssociationButton = Button(self.middle, text='Add', 93 | command=self.add) 94 | self.removeAssociationButton = Button(self.middle, text='Remove', 95 | command=self.remove) 96 | self.setDefaultsButton = Button(self.middle, text='Defaults', 97 | command=self.resetAssociations) 98 | # self.addAssociationButton.pack(side='top', expand=1, fill='both') 99 | # self.removeAssociationButton.pack(side='bottom', expand=1, fill='both') 100 | self.addAssociationButton.grid(row=0, column=0, sticky='NEW') 101 | self.removeAssociationButton.grid(row=1, column=0, sticky='NEW') 102 | self.setDefaultsButton.grid(row=2, column=0, sticky='NEW') 103 | 104 | # Right side widgets 105 | self.associationList = Pmw.ScrolledListBox(self.right_side, 106 | hscrollmode='dynamic', 107 | vscrollmode='static', 108 | labelpos='nw', 109 | dblclickcommand=self.reIns, 110 | label_text='Associations:') 111 | self.associationList.pack(expand=1, fill='both') 112 | self.setAssociations(self.assoc) 113 | 114 | # self.dialog.activate() # Make the dialog modal so the user can't 115 | # # mess with things in the other window. 116 | 117 | def resetAssociations(self, *args): 118 | self.setAssociations(self.assoc) 119 | 120 | def reIns(self, *args): 121 | selected = self.associationList.getcurselection() 122 | selected = selected[0] 123 | index = selected.find(self.DELIMITER) 124 | extension = selected[0:index] 125 | pgm = selected[index+len(self.DELIMITER):] 126 | 127 | self.ftEntry.delete(0, 'end') 128 | self.appEntry.delete(0, 'end') 129 | self.ftEntry.insert('end', extension) 130 | self.appEntry.insert('end', pgm) 131 | 132 | return None 133 | 134 | def extensionToAssociationExtension(self, ext): 135 | if len(ext) > 0: 136 | if ext[0] == '.': 137 | return ext[1:] 138 | else: 139 | return ext 140 | return ext 141 | 142 | def add(self, *args): 143 | extension = self.extensionToAssociationExtension(self.ftEntry.get()) 144 | pgm = self.appEntry.get() 145 | 146 | # Set the contents of the entry boxes back to nothing so the user 147 | # doesn't have to delete the contents before adding another association 148 | self.appEntry.delete(0, 'end') 149 | self.ftEntry.delete(0, 'end') 150 | 151 | str = extension + self.DELIMITER + pgm 152 | 153 | items = self.associationList.get() 154 | 155 | addItem = 1 156 | 157 | # Check to see if this entry is already in there somewhere. 158 | for x in range(0, len(items)): 159 | item = items[x] 160 | # If they have the same extension... 161 | if extension == item[0:len(extension)]: 162 | print("Replacing \"%s\"" % item) 163 | # Remove it from the list. 164 | items = items[0:x-1] + (str,) + items[x+1:] 165 | addItem = None 166 | break 167 | 168 | if addItem: 169 | items = items + (str,) 170 | 171 | self.associationList.setlist(items) 172 | return None 173 | 174 | def remove(self, *args): 175 | self.associationList.delete('active') 176 | return None 177 | 178 | def setAssociations(self, assoc): 179 | list = () 180 | 181 | for assocKey in assoc.getFileTypes(): 182 | str = assocKey + self.DELIMITER + assoc.getProgramString(assocKey) 183 | list = list + (str,) 184 | 185 | self.associationList.setlist(list) 186 | return None 187 | 188 | def getAssociations(self, *args): 189 | self.assoc = Associations.Associations() 190 | 191 | for item in self.associationList.get(): 192 | print("Got item %s" % item) 193 | index = item.find(self.DELIMITER) 194 | extension = item[0:index] 195 | pgm = item[index+len(self.DELIMITER):] 196 | self.assoc.addAssociation(extension, pgm) 197 | 198 | return self.assoc 199 | 200 | def dispatch(self, button): 201 | if button == 'OK': 202 | assocs = self.getAssociations() 203 | self.parent.setAssociations(assocs) 204 | self.dialog.destroy() 205 | # Grab data and put it into an Assocations object. 206 | 207 | self.dialog.destroy() 208 | return None 209 | 210 | -------------------------------------------------------------------------------- /BUGS: -------------------------------------------------------------------------------- 1 | Current known bugs in FORG: 2 | 3 | - Some dialog menus have to have their titlebar clicked or be resized 4 | before they will actually appear. This looks like a Pmw thing. 5 | - Find facility for text files works, but it doesn't show the user the 6 | result, so for all practical purposes it doesn't work. :) 7 | - Find facility on directories works, but if the item found wasn't on 8 | the screen, you may have to scroll down to find it. 9 | - Random python segfaults seem to happen occasionally due to Pmw. I 10 | cannot trace this. If you are able to get a core file out of python 11 | and this happens to you, please email it to me. (Along with info on 12 | the version of python you're running) 13 | - Sometimes (rarely) windows will not appear, and there will be an 14 | unhandled exception thrown through Pmw's convoluted scrolling code. 15 | I'm having a hell of a time tracing this too, but it should be done 16 | with eventually. 17 | - Under some strange circumstances, the program may crash when 18 | launching an external application to deal with a file. 19 | - The "STOP" button doesn't work. 20 | - If the program is abnormally killed, it may leave spawned processes 21 | behind, despite its best efforts not to. 22 | -------------------------------------------------------------------------------- /Bookmarks/Bookmark.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This is a subclass of GopherResource and is used in the Bookmark 5 | # management system of the FORG. 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ############################################################################ 21 | import xml.etree.ElementTree as ETs 22 | import GopherResource 23 | 24 | 25 | class Bookmark(GopherResource.GopherResource): 26 | def __init__(self, res=None): 27 | GopherResource.GopherResource.__init__(self) 28 | 29 | if res != None: 30 | # Copy data from the resource to this bookmark. 31 | self.setName(res.getName()) 32 | self.setHost(res.getHost()) 33 | self.setPort(res.getPort()) 34 | self.setLocator(res.getLocator()) 35 | self.setType(res.getTypeCode()) 36 | 37 | def __str__(self): 38 | return self.toString() 39 | 40 | def __repr__(self): 41 | return self.toString() 42 | 43 | def toXML(self): 44 | """Returns an XML representation of the object.""" 45 | bookmark = ETs.Element("bookmark", href=self.getURL()) 46 | title = ETs.Element("title") 47 | title.text = self.getName() 48 | bookmark.append(title) 49 | 50 | return bookmark 51 | 52 | def getURL(self): 53 | return self.toURL() 54 | 55 | def toData(self): 56 | return "%s !! gopher://%s:%s/%s" % (self.getName(), self.getHost(), 57 | self.getPort(), self.getLocator()) 58 | 59 | def toString(self): 60 | if self.getName() != '': 61 | return "%s: %s" % (self.getHost(), self.getName()) 62 | elif self.getLocator() == '/' or self.getLocator() == '': 63 | return "%s Root" % self.getHost() 64 | else: 65 | return "%s:%s %s" % (self.getHost(), self.getPort(), self.getLocator()) 66 | -------------------------------------------------------------------------------- /Bookmarks/BookmarkEditor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | ############################################################################ 18 | 19 | from tkinter import * 20 | import Pmw 21 | 22 | import Bookmarks.BookmarkMenu 23 | import Tree 24 | import Dialogs 25 | from Bookmarks.BookmarkFactory import BookmarkFactory 26 | import os 27 | import Options 28 | 29 | def traversal_function(node): 30 | if node.__class__ != Tree.Node: 31 | print("NODE CLASS: %s" % node.__class__) 32 | return None 33 | else: 34 | bm = node.id 35 | 36 | if bm.__class__ == Bookmarks.BookmarkMenu.BookmarkMenu: 37 | menu = Bookmarks.BookmarkMenu.BookmarkMenu() 38 | menu.setName(bm.getName()) 39 | 40 | # Visit each of the children. Note that this is children as in what 41 | # is displayed on the screen after the user has edited the bookmarks 42 | # with the editor. This is NOT the children that are listed in the 43 | # actual BookmarkMenu's data structure, since that may be wrong after 44 | # the user edits. 45 | for subnode in node.subnodes: 46 | rval = traversal_function(subnode) 47 | 48 | if not rval: 49 | print("**** That's weird. rval ain't.") 50 | continue 51 | 52 | # The items are one of two things - BookmarkMenu's or 53 | # BookmarkMenuNode's. Figure out which, and add it appropriately. 54 | # Note that you can't insert a submenu, you have to use addSubmenu 55 | # which is the reason for this conditional. 56 | if rval.__class__ == Bookmarks.BookmarkMenu.BookmarkMenu: 57 | print("Adding submenu: %s" % rval.getName()) 58 | menu.addSubmenu(rval) 59 | else: 60 | # print "Adding ITEM: %s" % rval.__class__ 61 | # print "Adding ITEM: %s %s" % (rval.__class__, rval.getName()) 62 | menunode = Bookmarks.BookmarkMenu.BookmarkMenuNode(rval) 63 | menu.insert(menunode) 64 | 65 | # Return the generated menu to be added 66 | return menu 67 | 68 | else: # No children...It's a BookmarkMenuNode 69 | return bm 70 | 71 | class BookmarkEditor(Toplevel): 72 | def __init__(self, bmtree, ondestroy=None): 73 | Toplevel.__init__(self) 74 | self.title("Edit Bookmarks...") 75 | # Callback to be fired off when this widget is destroyed. 76 | self.ondestroy = ondestroy 77 | 78 | # If user tries to close the window, call self.destroy 79 | self.protocol("WM_DELETE_WINDOW", self.destroy) 80 | 81 | self.make_menus() 82 | self.config(menu=self.menu) 83 | self.mainBox = Frame(self) 84 | self.mainBox.pack(fill='both', expand=1) 85 | 86 | self.tree = Tree.Tree(self.mainBox, bmtree, 87 | rootlabel="Bookmarks", lineflag=0) 88 | self.tree.grid(row=0, column=0, sticky='NSEW') 89 | 90 | # Make expandable 91 | self.mainBox.grid_rowconfigure(0, weight=1) 92 | self.mainBox.grid_columnconfigure(0, weight=1) 93 | 94 | self.vscrollbar = Scrollbar(self.mainBox, orient=VERTICAL) 95 | self.vscrollbar.grid(row=0, column=1, sticky='NS') 96 | self.tree.configure(yscrollcommand=self.vscrollbar.set) 97 | self.vscrollbar.configure(command=self.tree.yview) 98 | 99 | self.hscrollbar = Scrollbar(self.mainBox, orient=HORIZONTAL) 100 | self.hscrollbar.grid(row=1, column=0, sticky='EW') 101 | self.tree.configure(xscrollcommand=self.hscrollbar.set) 102 | self.hscrollbar.configure(command=self.tree.xview) 103 | 104 | # must get focus so keys work for demo 105 | self.tree.focus_set() 106 | 107 | # This is CRITICAL. Make sure this is done for several reasons: first 108 | # it expands the tree so the user can see the whole thing when the 109 | # editor pops open. Second, unless items in the tree are expanded, 110 | # their data elements aren't associated with the tree branches, so in 111 | # order for the bookmarks to save properly, everything must have been 112 | # expanded at one point or another, and this just ensures that. 113 | self.tree.expandAll() 114 | 115 | return None 116 | 117 | def destroy(self, *args): 118 | """User closed the window. Prompt for saving the bookmarks to 119 | disk.""" 120 | 121 | print("BookmarkEditor::destroy()") 122 | 123 | def cb(buttonName, self=self): 124 | print("Confirm callback: ", buttonName) 125 | 126 | if buttonName == 'OK': 127 | self.save() 128 | 129 | if self.ondestroy: 130 | # Call the destroy callback specified by our parent if it's 131 | # present. 132 | self.ondestroy() 133 | 134 | # Call superclass method to actually destroy the window. 135 | Toplevel.destroy(self) 136 | return None 137 | 138 | # Create a confirmation dialog box 139 | self._confirmDialog = Pmw.MessageDialog(self, 140 | message_text="Save Bookmarks?", 141 | buttons=('OK', 'Cancel'), 142 | defaultbutton='OK', 143 | title='Save Bookmarks?', 144 | command=cb) 145 | return None 146 | 147 | def getActive(self): 148 | i = self.tree.getActive() 149 | print("Active is %s class %s" % (i, i.__class__)) 150 | 151 | def make_menus(self, *args): 152 | self.menu = Menu(self) 153 | self.filemenu = Menu(self.menu) 154 | self.filemenu.add_command(label="Save", 155 | command=self.save) 156 | self.filemenu.add_command(label="Create Folder", 157 | command=self.createFolder) 158 | self.filemenu.add_command(label="Delete Folder", 159 | command=self.deleteFolder) 160 | self.filemenu.add_command(label="Add a Bookmark", 161 | command=self.addBookmark) 162 | self.filemenu.add_command(label="Close", command=self.destroy) 163 | 164 | self.editmenu = Menu(self.menu) 165 | self.editmenu.add_command(label="Cut", command=self.cut) 166 | self.editmenu.add_command(label="Copy", command=self.copy) 167 | self.editmenu.add_command(label="Paste", command=self.paste) 168 | self.editmenu.add_command(label="Delete", command=self.delete) 169 | 170 | self.testmenu = Menu(self.menu) 171 | self.testmenu.add_command(label="Get Active", command=self.getActive) 172 | 173 | self.menu.add_cascade(label="File", menu=self.filemenu) 174 | self.menu.add_cascade(label="Edit", menu=self.editmenu) 175 | self.menu.add_cascade(label="Test", menu=self.testmenu) 176 | return None 177 | 178 | def save(self, *args): 179 | # data_tree is a Node object, not a BookmarkMenu 180 | self.tree.expandAll() # Expand all nodes so data is present in ADT's 181 | data_tree = self.tree.getTree() 182 | 183 | # Take the id attribute out of each Node and string them together. 184 | # things may have been moved around, so the links inside the data 185 | # structures are no good, only copy. 186 | 187 | bmarks = traversal_function(data_tree) 188 | prefs_dir = Options.program_options.getOption('prefs_directory') 189 | filename = prefs_dir + os.sep + "bookmarks" 190 | factory = BookmarkFactory() 191 | 192 | try: 193 | factory.writeXML(filename, bmarks) 194 | except IOError as errstr: 195 | e = "Could not save bookmarks to\n%s:\n%s" % (filename, errstr) 196 | d = Dialogs.ErrorDialog(self, e, "Error Saving Bookmarks") 197 | 198 | def insertBookmark(self, bookmark): 199 | original_cut_buffer = self.tree.getCutBuffer() 200 | p = self.tree.getActive().parent 201 | 202 | newbm = Tree.Node(parent=None, 203 | name=bookmark.getName(), 204 | id=bookmark, 205 | closed_icon=Tree.Icons.FILE_ICON, 206 | open_icon=None, 207 | x=10, 208 | y=10, 209 | parentwidget=self.tree) 210 | 211 | co = Tree.Cut_Object(newbm, newbm.full_id(), None) 212 | 213 | # Set the cutbuffer to be the custom folder we just created. 214 | self.tree.setCutBuffer(co) 215 | # Paste it into the heirarchy 216 | self.tree.paste() 217 | # Set the cut buffer back to its original position. 218 | self.tree.setCutBuffer(original_cut_buffer) 219 | return None 220 | 221 | def addBookmark(self): 222 | # Prompt the user for info. After the info is completed, 223 | # insertBookmark will be called with a GopherResponse object as an 224 | # argument. 225 | dialog = Dialogs.NewBookmarkDialog(parentwin=self, 226 | cmd=self.insertBookmark) 227 | return None 228 | 229 | def createFolder(self, folderName=None): 230 | # Basically, just create a Cut_Object, and then call the paste method 231 | # to insert it into the tree. Make sure to preserve the cut object 232 | # that the tree is working with. 233 | 234 | if not folderName: 235 | self.__newFolderDialog = Dialogs.NewFolderDialog(self, 236 | self.createFolder) 237 | return None 238 | 239 | original_cut_buffer = self.tree.getCutBuffer() 240 | bmarkmenu = Bookmarks.BookmarkMenu.BookmarkMenu() 241 | bmarkmenu.setName(folderName) 242 | 243 | # We have to create a Node to insert into the tree, just like all other 244 | # nodes in the tree. Since we're pasting it into the heirarchy and 245 | # the parent is going to change, we don't need to specify one. 'id' 246 | # is the data associated with the Node object. 247 | folder_node = Tree.Node(parent=None, 248 | name=folderName, 249 | id=bmarkmenu, 250 | closed_icon=Tree.Icons.SHUT_ICON, 251 | open_icon=Tree.Icons.OPEN_ICON, 252 | x=10, 253 | y=10, 254 | parentwidget=self.tree) 255 | 256 | # Create a Cut_Object. This is done just as it is done in the 257 | # Node.cut() method in Tree.py - we have to use our custom created 258 | # node in order to create this Cut_Object. 259 | co = Tree.Cut_Object(folder_node, folder_node.full_id(), 1) 260 | 261 | # Set the cutbuffer to be the custom folder we just created. 262 | self.tree.setCutBuffer(co) 263 | 264 | # Paste it into the heirarchy 265 | self.tree.paste() 266 | 267 | # Set the cut buffer back to its original position. 268 | self.tree.setCutBuffer(original_cut_buffer) 269 | return None 270 | 271 | 272 | def deleteFolder(self, *args): 273 | if not self.tree.getActive().isFolder(): 274 | errstr = "Error:\nThe selected item\nisn't a folder." 275 | err = Dialogs.ErrorDialog(self, errstr, "Error") 276 | else: 277 | cutBuffer = self.tree.getCutBuffer() 278 | # Delete the item using the cut operation 279 | self.tree.cut() 280 | 281 | # Restore the old cutbuffer. Normally after deleting whatever we 282 | # deleted would be in the cut buffer, but since we're deleting and 283 | # not cutting, it need not be in the cutbuffer. 284 | self.tree.setCutBuffer(cutBuffer) 285 | return None 286 | 287 | def delete(self, *args): 288 | """Deletes the currently selected node out of the tree.""" 289 | a = self.tree.getActive() 290 | 291 | if a == self.tree.getRoot(): 292 | d = Dialogs.ErrorDialog(self, 293 | "Error:\nYou cannot delete the root.") 294 | return None 295 | 296 | # Get the old cut buffer 297 | cutbuf = self.tree.getCutBuffer() 298 | # Cut the item out (which overwrites the cutbuffer) 299 | self.cut() 300 | # Set the old cutbuffer back, meaning that the deleted item is gone 301 | # forever. 302 | self.tree.setCutBuffer(cutbuf) 303 | return None 304 | 305 | def cut(self, *args): 306 | """Cuts the selected node out of the tree.""" 307 | a = self.tree.getActive() 308 | 309 | if a == self.tree.getRoot(): 310 | # Bad mojo. You can't cut the root node. 311 | d = Dialogs.ErrorDialog(self, 312 | "Error:\nYou cannot cut the root element") 313 | return None 314 | 315 | return self.tree.cut() 316 | 317 | def copy(self, *args): 318 | a = self.tree.getActive() 319 | 320 | if a == self.tree.getRoot(): 321 | # Nope, can't copy the root. 322 | # Should we allow this? They couldn't paste it in at the same 323 | # level, but they could paste it in at a sublevel. I don't 324 | # know why anybody would want to have a subfolder, but they 325 | # might. So let's flag this with a FIXME with the implementation 326 | # note that if you're going to allow this you can't use the 327 | # cut() method to do it. 328 | d = Dialogs.ErrorDialog(self, 329 | "Error:\nYou cannot copy the root element") 330 | return None 331 | 332 | self.cut() 333 | return self.paste() 334 | def paste(self, *args): 335 | if self.tree.getCutBuffer(): 336 | return self.tree.paste() 337 | else: 338 | d = Dialogs.ErrorDialog(self, 339 | "There is no active\nbookmark to paste") 340 | 341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /Bookmarks/BookmarkFactory.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Tom4hawk 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 16 | ############################################################################ 17 | 18 | import xml.etree.ElementTree as ETs 19 | 20 | from .Bookmark import Bookmark 21 | from Bookmarks.BookmarkMenu import BookmarkMenuNode, BookmarkMenu 22 | 23 | 24 | class BookmarkFactory: 25 | verbose = None 26 | 27 | def __init__(self): 28 | self.menu = None 29 | self.currentMenu = None 30 | self.currentBmrk = None 31 | self.folders = [] 32 | 33 | def getMenu(self): 34 | """Menu object accessor""" 35 | return self.menu 36 | 37 | def parseResource(self, filename): 38 | bookmarksTree = ETs.parse(filename) 39 | 40 | for element in bookmarksTree.getroot(): 41 | self.__parse_element(element) 42 | 43 | def writeXML(self, filename, menu): 44 | """Writes an XML representation of bookmark menu to file""" 45 | xbel = ETs.Element("xbel") 46 | bookmarks = menu.toXML() 47 | 48 | xbel.append(bookmarks) 49 | 50 | tree = ETs.ElementTree(xbel) 51 | tree.write(filename, "UTF-8") 52 | 53 | def __parse_element(self, item): 54 | if item.tag == 'folder': 55 | self.__parse_folder(item) 56 | elif item.tag == 'bookmark': 57 | self.__parse_bookmark(item) 58 | 59 | def __parse_folder(self, item): 60 | # start_folder 61 | self.currentMenu = BookmarkMenu() 62 | self.folders.append(self.currentMenu) 63 | self.lastTag = "folder" 64 | self.log_verbose("Creating new folder") 65 | 66 | # take care of the folder title 67 | title = item.find('title') # folder should have only one title 68 | self.log_verbose('Setting menu name: ' + title.text) 69 | self.currentMenu.setName(title.text) 70 | 71 | # it's a folder so we have to go deep into the rabbit hole 72 | for element in item: 73 | self.__parse_element(element) 74 | 75 | # folder processed, let's finish it 76 | # end_folder 77 | finished_folder = self.folders.pop() 78 | 79 | self.log_verbose("Finishing up folder: %s" % finished_folder.getName()) 80 | 81 | if self.folders: 82 | self.currentMenu = self.folders[-1] 83 | 84 | self.log_verbose("Adding submenu \"%s\" to \"%s\"" % (finished_folder.getName(), self.currentMenu.getName())) 85 | 86 | self.currentMenu.addSubmenu(finished_folder) 87 | else: 88 | # The stack is empty - assign the main menu to be this item 89 | # here. 90 | self.log_verbose("Finished toplevel folder.") 91 | self.menu = finished_folder 92 | 93 | def __parse_bookmark(self, item): 94 | # start_bookmark 95 | self.currentBmrk = Bookmark() 96 | self.lastTag = "bookmark" 97 | 98 | self.log_verbose("Setting URL to be " + item.attrib['href']) 99 | 100 | try: 101 | self.currentBmrk.setURL(item.attrib['href']) 102 | except KeyError: 103 | self.log_error("**** Error parsing XML: bookmark is missing 'href'") 104 | except Exception as errstr: 105 | self.log_error("**** Parse error: Couldn't parse %s: %s" % (item.attrib['href'], errstr)) 106 | self.currentBmrk = None 107 | return 108 | 109 | self.log_verbose("Creating new bookmark") 110 | 111 | # Take care of the bookmark title 112 | title = item.find('title') # bookmark should have only one title, we can ignore rest of them 113 | self.log_verbose("Setting bmark name: " + title.text) 114 | self.currentBmrk.setName(title.text) 115 | 116 | # end_bookmaark 117 | self.log_verbose("Inserting new bmark") 118 | 119 | if self.currentBmrk: 120 | self.currentMenu.insert(BookmarkMenuNode(self.currentBmrk)) 121 | else: 122 | self.log_error("**** Error parsing XML: could not insert invalid bookmark.") 123 | 124 | #TODO: Create separate logger 125 | def log_verbose(self, message): 126 | if self.verbose: 127 | print(message) 128 | 129 | def log_error(self, message): 130 | print(message) 131 | -------------------------------------------------------------------------------- /Bookmarks/BookmarkMenu.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | ############################################################################ 18 | 19 | from tkinter import Menu 20 | from xml.etree import ElementTree as ETs 21 | 22 | import List 23 | import ListNode 24 | from Bookmarks.Bookmark import Bookmark 25 | 26 | 27 | # Subclass ListNode just for naming purposes so BookmarkMenuNodes can be 28 | # dealt with inside of BookmarkMenus 29 | class BookmarkMenuNode(ListNode.ListNode): 30 | pass 31 | 32 | 33 | # BookmarkMenu is to organize heirarchical menus of bookmarks which will be 34 | # editable by the user. 35 | class BookmarkMenu(List.List): 36 | verbose = None 37 | 38 | def __init__(self, menuName=" "): 39 | List.List.__init__(self) 40 | self.menuName = menuName 41 | return None 42 | 43 | def getName(self): 44 | if len(self.menuName.strip()) > 0: 45 | return self.menuName 46 | else: 47 | self.setName("Bookmarks") 48 | return self.menuName 49 | return "Error fetching name" 50 | 51 | def setName(self, newname): 52 | self.menuName = newname 53 | return self.menuName 54 | 55 | def toString(self): 56 | return "BookmarkMenu: \"%s\"" % self.getName() 57 | 58 | def __str__(self): 59 | return self.toString() 60 | 61 | def __repr__(self): 62 | return self.toString() 63 | 64 | def insert(self, item, truncate=0): 65 | """Overrides the insert method to always pass a truncate argument of 66 | 0 so the list is never truncated on insertions.""" 67 | return List.List.insert(self, item, 0) 68 | 69 | def addSubmenu(self, menu): 70 | """Adds menu as a submenu of this menu""" 71 | if menu.__class__ != BookmarkMenu and menu.__class__ != Bookmark: 72 | raise Exception("Cannot add a non-Bookmark/Menu as submenu") 73 | return self.insert(BookmarkMenuNode(menu)) 74 | 75 | def toXML(self): 76 | """Returns an XML representation of this object. This is called 77 | recursively""" 78 | 79 | if self.verbose: 80 | print("BookmarkMenu.toXML()") 81 | 82 | folder = ETs.Element("folder") 83 | title = ETs.Element("title") 84 | title.text = self.getName() 85 | folder.append(title) 86 | 87 | def fn(item, parent=folder): 88 | data = item.getData() 89 | # It doesn't matter which kind it is, whether it's a 90 | # Bookmark or a BookmarkMenu since they both have toXML() methods 91 | # and they'll take it from here. If it's a BookmarkMenu, this 92 | # will happen recursively. 93 | node = data.toXML() 94 | parent.append(node) 95 | 96 | return data.getName() 97 | 98 | # Apply the above function to each item in the menu 99 | self.traverse(fn) 100 | 101 | return folder 102 | 103 | def getTkMenu(self, parent_widget, callback): 104 | """Return a Tk Menu object which is suitable for being added to other 105 | submenus. parent_widget is the menu you will add it to, and callback 106 | is a function that takes exactly one argument. That argument is the 107 | Bookmark object that the user-clicked item represents.""" 108 | # This is the menu we will return. 109 | m = Menu(parent_widget) 110 | 111 | # Create a subfunction to deal with each item in the list. 112 | # This way since this object already extends List and we already have 113 | # a traverse function, we can go through the entire list in order 114 | def fn(listitem, cb=callback, addTo=m): 115 | # Each item in the list is a ListNode, not a Bookmark, so get the 116 | # data out of the ListNode. 117 | data = listitem.getData() 118 | 119 | def real_callback(item=data, oldCallback=cb): 120 | """This is a way of converting a callback that takes no 121 | arguments into one that takes two for our purposes. The 122 | user passes in a callback function that takes only one 123 | parameter (the resource you want to go to) and this is 124 | the actual function that is bound to the action, which calls 125 | the user defined function with the appropriate argument""" 126 | return oldCallback(item) 127 | 128 | try: 129 | # If it's a menu, then add the submenu recursively. 130 | if data.__class__ == BookmarkMenu: 131 | addTo.add_cascade(label=data.getName(), 132 | menu=data.getTkMenu(addTo, cb)) 133 | else: 134 | # Otherwise add a regular command into the menu. 135 | addTo.add_command(label=data.getName(), 136 | command=real_callback) 137 | except: 138 | pass 139 | return data.getName() 140 | 141 | # Apply the above to each item in the list, adding menu items and 142 | # submenus where needed. 143 | self.traverse(fn) # Don't care about the return results. 144 | 145 | return m 146 | -------------------------------------------------------------------------------- /Bookmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tom4hawk/FORG/3482faa5a915930c82862028f6c41020db8ad8c9/Bookmarks/__init__.py -------------------------------------------------------------------------------- /Cache.py: -------------------------------------------------------------------------------- 1 | # Cache.py 2 | # $Id: Cache.py,v 1.14 2001/07/14 22:50:28 s2mdalle Exp $ 3 | # Written by David Allen 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # Handles cache-file related operations. 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 21 | ############################################################################ 22 | import os 23 | import utils 24 | import string 25 | from gopher import * 26 | from tkinter import * 27 | import GopherResponse 28 | import Pmw 29 | import Options 30 | 31 | 32 | class CacheException(Exception): 33 | def __init__(self, message): 34 | super(CacheException, self).__init__(message) 35 | 36 | 37 | class Cache: 38 | verbose = None 39 | 40 | def __init__(self, *args): 41 | self._d = None 42 | 43 | def getCacheDirectory(self): 44 | try: 45 | dir = Options.program_options.getOption('cache_directory') 46 | return os.path.abspath(dir) 47 | except: 48 | return os.path.abspath("cache") 49 | 50 | def getCachePrefix(self): 51 | try: 52 | dir = Options.program_options.getOption('cache_prefix') 53 | return os.path.abspath(dir) 54 | except: 55 | return os.path.abspath("cache%scache" % os.sep) 56 | 57 | def getCacheStats(self): 58 | cdir = self.getCacheDirectory() 59 | 60 | [filecount, totalBytes, dircount] = utils.summarize_directory(cdir) 61 | 62 | kbcount = float(totalBytes)/float(1024) 63 | mbcount = kbcount/float(1024) 64 | 65 | kbcount = "%0.2fKB" % kbcount 66 | mbcount = "%0.2fMB" % mbcount 67 | 68 | datasize = "There is %s (%s) of data" % (kbcount, mbcount) 69 | fdcount = "in %s files (%s directories total)" % (filecount, dircount) 70 | closer = "underneath %s" % cdir 71 | 72 | return "%s\n%s\n%s" % (datasize, fdcount, closer) 73 | 74 | def emptyCache(self, parentTk): 75 | pref = self.getCachePrefix() 76 | 77 | self.dialog = Pmw.Dialog(parent=parentTk, 78 | title="Really delete cache?", 79 | buttons=('OK', 'Cancel'), 80 | defaultbutton='Cancel', 81 | command=self.__confirmDelete) 82 | 83 | fr = Frame(self.dialog.interior()) 84 | fr.pack(side='top', fill='both', expand=1) 85 | 86 | lab1 = Label(fr, text="Are you sure you want to empty the cache?") 87 | lab2 = Label(fr, text="WARNING: THIS WILL DELETE ALL FILES AND") 88 | lab3 = Label(fr, text="ALL SUBDIRECTORIES IN YOUR CACHE DIRECTORY,") 89 | lab4 = Label(fr, text="%s" % pref) 90 | lab5 = Label(fr, text=" ") 91 | lab6 = Label(fr, text="Continue?") 92 | 93 | lab1.grid(row=1, column=0, columnspan=5) 94 | lab2.grid(row=2, column=0, columnspan=5) 95 | lab3.grid(row=3, column=0, columnspan=5) 96 | lab4.grid(row=4, column=0, columnspan=5) 97 | lab5.grid(row=5, column=0, columnspan=5) 98 | lab6.grid(row=6, column=0, columnspan=5) 99 | 100 | # self.dialog.activate() 101 | 102 | def deleteCacheNoPrompt(self, *args): 103 | """Delete the cache without asking the user. Do not do this unless 104 | you know what you're doing.""" 105 | return self.__deleteCache() 106 | 107 | def __confirmDelete(self, button): 108 | if button != 'OK': 109 | self.dialog.destroy() 110 | return None 111 | 112 | self.dialog.destroy() # Destroy the dialog anyway 113 | self.__deleteCache() # Clean the files out 114 | return None 115 | 116 | def __deleteCache(self): 117 | pref = self.getCachePrefix() 118 | 119 | if not utils.dir_exists(pref): 120 | raise CacheException("Cache prefix %s doesn't exist." % pref) 121 | 122 | cache_directories = os.listdir(pref) 123 | 124 | # I've been told that there's a shell utility module that does this 125 | # probably safer and better, but I'm not sure where it is, or whether 126 | # or not it's portable to crappy no-name 32 bit operating systems 127 | # out of Redmond, Washington. 128 | 129 | for item in cache_directories: 130 | item = "%s%s%s" % (pref, os.sep, item) 131 | if os.path.isdir(item): 132 | print("Recursively deleting \"%s\"" % item) 133 | utils.recursive_delete(item) 134 | else: 135 | print("Eh? \"%s\" isn't a directory. That's odd..." % item) 136 | 137 | def isInCache(self, resource): 138 | """Takes a resource, and returns true if the resource seems to have 139 | an available cache file associated with it, and None otherwise.""" 140 | 141 | pref = self.getCachePrefix() 142 | filename = resource.toCacheFilename() 143 | 144 | if pref[len(pref)-1] != os.sep and filename[0] != os.sep: 145 | # When joining together, separate paths with os.sep 146 | pref = "%s%s" % (pref, os.sep) 147 | 148 | # Now join together, and they will be properly joined. 149 | filename = pref + filename 150 | 151 | try: 152 | info = os.stat(os.path.abspath(filename)) 153 | return [os.path.abspath(filename), info[6]] 154 | except OSError: 155 | return None 156 | return None 157 | 158 | def uncache(self, resource): 159 | """Takes a resource, and returns either None if the given resource 160 | is not cached on disk, or it returns a GopherResponse corresponding 161 | to what would have been gotten if it had been fetched from the 162 | server.""" 163 | 164 | pref = self.getCachePrefix() 165 | file = resource.toCacheFilename() 166 | 167 | if pref[len(pref)-1] != os.sep and file[0] != os.sep: 168 | # When joining together, separate paths with os.sep 169 | pref = "%s%s" % (pref, os.sep) 170 | 171 | filename = pref + file 172 | 173 | try: 174 | # See if the file exists... 175 | tuple = os.stat(filename) 176 | if self.verbose: 177 | print("File %s of size %d exists." % (filename, tuple[6])) 178 | except OSError: 179 | # The file doesn't exist, we can't uncache it. 180 | return None 181 | 182 | print("===> Uncaching \"%s\"" % filename) 183 | resp = GopherResponse.GopherResponse() 184 | resp.setType(resource.getTypeCode()) 185 | resp.setHost(resource.getHost()) 186 | resp.setPort(resource.getPort()) 187 | resp.setLocator(resource.getLocator()) 188 | resp.setName(resource.getName()) 189 | 190 | try: 191 | fp = open(filename, "r") 192 | 193 | # Consider reworking this somehow. Slurp the entire file into 194 | # buffer. 195 | buffer = fp.read() 196 | fp.close() 197 | 198 | try: 199 | resp.parseResponse(buffer) 200 | resp.setData(None) 201 | # print "Loaded cache is a directory entry." 202 | except: 203 | # print "Loaded cache is not a directory." 204 | resp.setData(buffer) 205 | 206 | # Got it! Loaded from cache anyway... 207 | # print "UNCACHE found data for use." 208 | return resp 209 | except IOError as errstr: 210 | raise CacheException("Couldn't read data on\n%s:\n%s" % (filename, 211 | errstr)) 212 | 213 | # We failed. Oh well... 214 | return None 215 | 216 | def cache(self, resp, resource): 217 | """Takes a GopherResponse and a GopherResource. Saves the content of 218 | the response to disk, and returns the filename saved to.""" 219 | 220 | if resource.isAskType(): 221 | # Don't cache ASK blocks. This is because if you do, the program 222 | # will interpret it as data, and put the question structure inside 223 | # a text box. Plus, since these may be dynamic, caching could 224 | # lead to missing out on things. 225 | raise CacheException("Do not cache AskTypes. Not a good idea.") 226 | 227 | basedir = self.getCachePrefix() 228 | basefilename = resource.toCacheFilename() 229 | 230 | # Problem - basedir is our base directory, but basefilename contains 231 | # trailing filename info that shouldn't be part of the directories 232 | # we're creating. 233 | filenamecopy = basefilename[:] 234 | ind = filenamecopy.rfind(os.sep) 235 | 236 | # Chop off extra info so "/home/x/foobar" becomes "/home/x/foobar" 237 | # this is because make_directories will otherwise create foobar as 238 | # a directory when it's actually a filename 239 | filenamecopy = filenamecopy[0:ind] 240 | 241 | # Create the directory structure where necessary 242 | utils.make_directories(filenamecopy, basedir) 243 | 244 | basedirlastchar = basedir[len(basedir)-1] 245 | if basedirlastchar == os.sep: 246 | filename = "%s%s" % (basedir, basefilename) 247 | else: 248 | filename = "%s%s%s" % (basedir, os.sep, basefilename) 249 | 250 | # print "Cache: caching data to \"%s\"" % filename 251 | 252 | try: 253 | fp = open(filename, "w") 254 | 255 | if resp.getData() is None: # This is a directory entry. 256 | response_lines = resp.getResponses() 257 | # Each response line is a GopherResource 258 | # write them as if it was a file served by the gopher server. 259 | # that way it can be easily reparsed when loaded from the 260 | # cache. 261 | for response_line in response_lines: 262 | fp.write(response_line.toProtocolString()) 263 | 264 | # write the string terminator. This isn't really needed 265 | # since it isn't data, but it helps fool our other objects 266 | # into thinking that it's dealing with data off of a socket 267 | # instead of data from a file. So do it. 268 | fp.write("\r\n.\r\n") 269 | else: 270 | fp.write(resp.getData()) 271 | 272 | fp.flush() 273 | fp.close() 274 | except IOError as errstr: 275 | # Some error writing data to the file. Bummer. 276 | raise CacheException("Couldn't write to\n%s:\n%s" % (filename, errstr)) 277 | # Successfully wrote the data - return the filename that was used 278 | # to save the data into. (Absolute path) 279 | return os.path.abspath(filename) 280 | 281 | 282 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Changes since version 0.5 2 | - Fixed short read for gopher servers that misreport the content 3 | lengths of documents. FORG now totally disregards content 4 | lenghts since they are so unreliable. 5 | 6 | Changes since version 0.4.1 7 | - Added the 'delete' feature in the Bookmark Editor. 8 | - Bookmarks now reload after the editor widget is destroyed. 9 | - Fixed "inactive bookmark menu" problem when reloading bookmark 10 | menu. 11 | - Added many more docstrings to the source. 12 | - Fixed URL parsing bug on startup. Previously the program 13 | mishandled fully qualified URLs as arguments, now it doesn't. 14 | - Added option: delete cache on exit 15 | - Added option: home. 16 | - Added "Home" button on toolbar and "Set this site as my home 17 | site" option in the options menu. 18 | 19 | Changes since version 0.4: 20 | - Fixed Makefile bug pointed out by stain 21 | - Fixed Python 2.1 compatibility bug pointed out by stain 22 | 23 | - Migrated all regsub.[g]sub calls to re.sub 24 | - Fixed other Python 2.1 compatibility issues in Tree.py 25 | 26 | Changes since version 0.3: 27 | - Added the ability to select fonts/colors with 28 | $HOME/.forg/options 29 | - Fixed critical bug that was causing directory creation errors on 30 | windows systems. 31 | - Fixed fatal bug if the options file didn't exist. Whoops. :) 32 | - Fixed several bookmark editor bugs including disallowing cutting 33 | the root node, adding error dialogs for inactive conditions 34 | (like when the user tries to paste and no node is on the 35 | clipboard) 36 | - Disabled menu tearoffs. If anybody really wants them, I'll make 37 | them an option, but they seem to confuse windows users, and 38 | they're not exactly a super useful innovation. :) 39 | - Small GUI tweaks. 40 | 41 | Changes since version 0.2: 42 | Many bugfixes to the bookmark editor, including: 43 | - pasting folders into other folders 44 | - inserting folders as siblings of other folders 45 | - editing bookmarks and folder names in their places 46 | Display bugfixes, including a fix for some menu lockups. 47 | 48 | Program redesign - rather than having most of the logic of the 49 | program in TkGui, it has been separated out into a module allowing 50 | the FORG to be embedded in other applications. 51 | 52 | Improved caching and directory creation 53 | 54 | Addition of "purge cache" and "query cache" features. 55 | 56 | Changes since version 0.1: 57 | Added the "Purge Cache" functionality, which does the equivalent 58 | of a "rm -rf" on the users cache directory, which is usually 59 | $HOME/.forg/cache/. 60 | 61 | Added the "Cache Statistics" functionality to count 62 | files/directories and total filesize. 63 | 64 | Added support for the PIL (Python Imaging Library). If you have 65 | this software installed, it will be used to display images 66 | supported by the PIL inside the FORG's window when selecting 67 | images. This will only happen if you don't have an application 68 | bound to that filetype, and if the "Use PIL" option is checked. 69 | 70 | Added rudimentary bookmark editor, which isn't working yet, but 71 | gives you an idea of what it will be like. 72 | 73 | Added the Tree widget written by Charles E. "Gene" Cash 74 | . Used in the Bookmark Editor. 75 | -------------------------------------------------------------------------------- /Connection.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Base class for socket connections. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | ############################################################################# 20 | import socket 21 | import Options 22 | import utils 23 | import errno 24 | 25 | 26 | class ConnectionException(Exception): 27 | def __init__(self, message): 28 | super(ConnectionException, self).__init__(message) 29 | 30 | 31 | class Connection: 32 | def __init__(self, *args): 33 | self._d = None 34 | self.bytes_transferred = {'sent' : 0, 35 | 'received' : 0 } 36 | 37 | def received(self, bytes): 38 | """Keep track of the number of bytes received on this socket object""" 39 | oldval = self.bytes_transferred['received'] 40 | self.bytes_transferred['received'] = oldval + bytes 41 | return self.bytes_transferred['received'] 42 | 43 | def sent(self, bytes): 44 | """Keep track of the number of bytes sent on this socket object""" 45 | self.bytes_transferred['sent'] = self.bytes_transferred['sent'] + bytes 46 | return self.bytes_transferred['sent'] 47 | 48 | def getBytesSent(self): 49 | return self.bytes_transferred['sent'] 50 | 51 | def getBytesRead(self): 52 | return self.bytes_transferred['received'] 53 | 54 | def readloop(self, sock, bytesToRead, msgBar=None): 55 | """Reads bytesToRead data off of sock or until EOF if 56 | bytesToRead < 0. Optionally uses msgBar to log information to the 57 | user.""" 58 | timesRead = 0 59 | data = b"" 60 | CHUNKSIZE = 1024 # Default read block size. 61 | # This may get overridden depending on how much data 62 | # we have to read. Optimally we want to read all of 63 | # the data that's going to come in 100 steps. 64 | 65 | if bytesToRead < 0: 66 | numKBtoRead = "" # Don't report total size since we don't know 67 | else: 68 | # Report the total size so the user has an idea of how long it's 69 | # going to take. 70 | val = float(bytesToRead) / float(1024) 71 | numKBtoRead = "of %0.2f kilobytes total size" % val 72 | 73 | if bytesToRead > (1024 * 100): # 100 Kb 74 | CHUNKSIZE = bytesToRead / 100 75 | 76 | chunk = 'foobar' 77 | 78 | while len(chunk) > 0: 79 | self.checkStopped(msgBar) # Constantly make sure we should 80 | chunk = sock.recv(CHUNKSIZE) 81 | self.received(CHUNKSIZE) 82 | self.checkStopped(msgBar) # continue... 83 | 84 | timesRead = timesRead + 1 85 | # print "Read %s: %s" % (CHUNKSIZE, timesRead * CHUNKSIZE) 86 | data = data + chunk 87 | 88 | if bytesToRead > 0 and len(data) >= bytesToRead: 89 | # print "BTR=%s, len(data)=%s, breaking" % (bytesToRead, 90 | # len(data)) 91 | 92 | # Disregard content length for broken gopher+ servers. 93 | # break 94 | pass 95 | 96 | if msgBar: 97 | # Report statistics on how far along we are... 98 | bytesRead = timesRead * CHUNKSIZE 99 | kbRead = (float(timesRead) * float(CHUNKSIZE)) / float(1024) 100 | 101 | if bytesToRead > 0: 102 | pct = (float(bytesRead) / float(bytesToRead))*float(100) 103 | 104 | # This happens sometimes when the server tells us to read 105 | # fewer bytes than there are in the file. In this case, 106 | # we need to only display 100% read even though it's 107 | # actually more. Becase reading 120% of a file doesn't 108 | # make sense. 109 | if pct >= float(100): 110 | pct = float(100) 111 | pctDone = ("%0.2f" % pct) + "%" 112 | else: 113 | pctDone = "" 114 | 115 | msgBar.message('state', 116 | "Read %d bytes (%0.2f Kb) %s %s" % 117 | (bytesRead, 118 | kbRead, 119 | numKBtoRead, 120 | pctDone)) 121 | 122 | # Break standards-compliance because of all those terrible gopher 123 | # servers out there. Return all of the data that we read, not just 124 | # the first bytesToRead characters of it. This will produce the user 125 | # expected behavior, but it disregards content lenghts in gopher+ 126 | if bytesToRead > 0: 127 | # return data[0:bytesToRead] 128 | return data.decode(encoding="utf-8", errors="ignore") 129 | else: 130 | return data.decode(encoding="utf-8", errors="ignore") 131 | 132 | def checkStopped(self, msgBar): 133 | """Issue a message to the user and jump out if greenlight 134 | isn't true.""" 135 | if not Options.program_options.GREEN_LIGHT: 136 | raise ConnectionException("Connection stopped") 137 | 138 | def requestToData(self, resource, request, msgBar=None, grokLine=None): 139 | """Sends request to the host/port stored in resource, and returns 140 | any data returned by the server. This may throw 141 | ConnectionException. msgBar is optional. 142 | May throw ConnectionException if green_light ever becomes false. 143 | This is provided so that the user can immediately stop the connection 144 | if it exists.""" 145 | 146 | utils.msg(msgBar, "Creating socket...") 147 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 148 | if not self.socket: 149 | raise ConnectionException("Cannot create socket.") 150 | 151 | self.checkStopped(msgBar) 152 | utils.msg(msgBar, "Looking up hostname...") 153 | try: 154 | # Try to get a cached copy of the IP address rather than 155 | # looking it up again which takes longer... 156 | ipaddr = Options.program_options.getIP(resource.getHost()) 157 | except KeyError: 158 | # OK, it wasn't in the cache. Figure out what it is, 159 | # and then put it in the cache. 160 | try: 161 | self.checkStopped(msgBar) 162 | ipaddr = socket.gethostbyname(resource.getHost()) 163 | except socket.error as err: 164 | host = resource.getHost() 165 | estr = "Cannot lookup\n%s:\n%s" % (host, err) 166 | raise ConnectionException(estr) 167 | 168 | Options.program_options.setIP(resource.getHost(), ipaddr) 169 | 170 | # At this point, ipaddr holds the actual network address of the 171 | # host we're contacting. 172 | utils.msg(msgBar, 173 | "Connecting to %s:%s..." % (ipaddr, resource.getPort())) 174 | try: 175 | retval = self.socket.connect((ipaddr, int(resource.getPort()))) 176 | #if retval != 0: 177 | # errortype = errno.errorcode[retval] 178 | # raise socket.error, errortype 179 | except socket.error as err: 180 | newestr = "Cannot connect to\n%s:%s:\n%s" % (resource.getHost(), resource.getPort(), err) 181 | raise ConnectionException(newestr) 182 | 183 | data = "" 184 | 185 | self.checkStopped(msgBar) 186 | self.socket.send(request.encode()) # Send full request - usually quite short 187 | self.checkStopped(msgBar) 188 | self.sent(len(request)) # We've sent this many bytes so far... 189 | 190 | if grokLine: # Read the first line...this is for Gopher+ retrievals 191 | line = b"" # and usually tells us how many bytes to read later 192 | byte = "" 193 | 194 | while byte != "\n": 195 | self.checkStopped(msgBar) 196 | byte = self.socket.recv(1) 197 | if len(byte) <= 0: 198 | print("****************BROKE out of byte loop") 199 | break 200 | line = line + byte 201 | 202 | bytesread = len(line) 203 | line = line.decode(encoding="utf-8", errors="ignore") # str() should be enough but you never know... 204 | line = line.strip() 205 | 206 | try: 207 | if line[0] == '+': 208 | bytecount = int(line[1:]) # Skip first char: "+-1" => "-1" 209 | resource.setLen(bytecount) 210 | 211 | # Read all of the data into the 'data' variable. 212 | data = self.readloop(self.socket, bytecount, msgBar) 213 | else: 214 | data = self.readloop(self.socket, -1, msgBar) 215 | except: 216 | print("*** Couldn't read bytecount: skipping.") 217 | data = self.readloop(self.socket, -1, msgBar) 218 | else: 219 | data = self.readloop(self.socket, -1, msgBar) 220 | 221 | utils.msg(msgBar, "Closing socket.") 222 | self.socket.close() 223 | 224 | # FIXME: 'data' may be huge. Buffering? Write to cache file here 225 | # and return a cache file name? 226 | return data 227 | -------------------------------------------------------------------------------- /ContentFrame.py: -------------------------------------------------------------------------------- 1 | # ContentFrame.py 2 | # $Id: ContentFrame.py,v 1.6 2001/03/27 22:18:02 s2mdalle Exp $ 3 | # Written by David Allen 4 | # 5 | # This is the base class for anything that gets displayed by the program 6 | # in the main window to represent a response from the server. In other words, 7 | # things such as text boxes and directory listings from gopher servers should 8 | # extend this class. 9 | # 10 | # This class defines the behavior of anything that is presented to the user. 11 | # OK, so I'm a bit new to python. What I mean here is like a Java interface. 12 | # I know I can't force subclasses to 'implement' this function, but they 13 | # should, otherwise the functions here will spit out obscene messages to the 14 | # user. :) 15 | # 16 | # Note that creating the object is NOT ENOUGH. You must call pack_content 17 | # after creating it to actually add all the things that the widget will display 18 | # this is a workaround for some odd behavior in Pmw. For this reason you 19 | # should NOT rely on nothing being present in the widget if you don't call 20 | # pack_content. 21 | # 22 | # This program is free software; you can redistribute it and/or modify 23 | # it under the terms of the GNU General Public License as published by 24 | # the Free Software Foundation; either version 2 of the License, or 25 | # (at your option) any later version. 26 | # 27 | # This program is distributed in the hope that it will be useful, 28 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | # GNU General Public License for more details. 31 | # 32 | # You should have received a copy of the GNU General Public License 33 | # along with this program; if not, write to the Free Software 34 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 35 | ############################################################################### 36 | 37 | from tkinter import * 38 | import Pmw 39 | 40 | class ContentFrame: 41 | useStatusLabels = None 42 | 43 | def __init__(self): 44 | pass 45 | def pack_content(self, *args): 46 | """Packs the content of the box into the frame. Note this does NOT 47 | pack this object into its parent, only its children into itself.""" 48 | print("ContentFrame.pack_content: Superclass failed to override.") 49 | return None 50 | def find(self, term, caseSensitive=None, lastIdentifier=None): 51 | """Find some term within a frame. If caseSensitive is true, then the 52 | search will be caseSensitive. lastIdentifier is something that was 53 | previously returned by this function as the last match. If you want 54 | to find the next occurance of something, pass the last occurance in 55 | and it will search from then on.""" 56 | print("**********************************************************") 57 | print("***** ContentFrame.find(): Some subclass fucked up. *****") 58 | print("**********************************************************") 59 | return None 60 | -------------------------------------------------------------------------------- /Dialogs.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Contains many different program dialogs used for information and data 5 | # entry purposes. 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ############################################################################ 21 | from tkinter import * 22 | from types import * 23 | import Pmw 24 | 25 | from Bookmarks.Bookmark import Bookmark 26 | 27 | 28 | class FindDialog: 29 | def __init__(self, parent, searchwidget, parentobj=None, *args): 30 | self.parent = parent 31 | self.parentobj = parentobj 32 | self.searchwidget = searchwidget 33 | self.dialog = Pmw.Dialog(parent, title='Find Text...', 34 | buttons=('OK', 'Cancel'), defaultbutton='OK', 35 | command=self.dispatch) 36 | self.frame = Frame(self.dialog.interior()) 37 | self.frame.pack(expand=1, fill='both') 38 | 39 | Label(self.frame, text="Find a term...").grid(row=0, column=0, 40 | columnspan=5) 41 | Label(self.frame, text="Term: ").grid(row=1, column=0) 42 | 43 | self.searchEntry = Entry(self.frame, text="") 44 | self.searchEntry.grid(row=1, column=1, columnspan=4) 45 | 46 | self.css = IntVar() 47 | 48 | self.caseSensitiveCheckBox = Checkbutton(self.frame, 49 | text="Case-sensitive search", 50 | variable = self.css, 51 | command = self.cb) 52 | self.caseSensitiveCheckBox.grid(row=2, column=0, columnspan=4) 53 | 54 | self.lastMatch = None 55 | # self.dialog.activate() 56 | return None 57 | 58 | def cb(self, *args): 59 | # print "Var is ", self.css.get() 60 | return None 61 | 62 | def getSearchTerm(self): 63 | """Returns the search term currently in the search box.""" 64 | return self.searchEntry.get() 65 | 66 | def getCaseSensitive(self): 67 | """Returns the status of the case sensitive check button""" 68 | return self.css.get() 69 | 70 | def dispatch(self, button): 71 | """Handles button clicking in the dialog. (OK/Cancel)""" 72 | if button != 'OK': 73 | # Smack... 74 | self.dialog.destroy() 75 | return None 76 | 77 | # Otherwise, go look for a term... 78 | # try: 79 | self.lastMatch = self.searchwidget.find(self.getSearchTerm(), 80 | self.getCaseSensitive(), 81 | self.lastMatch) 82 | 83 | print("Last match is now ", self.lastMatch) 84 | return self.lastMatch 85 | 86 | class OpenURLDialog: 87 | def __init__(self, parentwin, callback): 88 | self.callback = callback 89 | self.dialog = Pmw.Dialog(parentwin, title="Open URL:", 90 | command=self.dispatch, 91 | buttons=('OK', 'Cancel'), 92 | defaultbutton='OK') 93 | i = self.dialog.interior() 94 | Label(i, text="Enter URL to Open:").pack(side='top', expand=1, 95 | fill='both') 96 | self.urlEntry = Entry(i, width=30) 97 | self.urlEntry.insert('end', "gopher://") 98 | self.urlEntry.pack() 99 | 100 | def dispatch(self, button): 101 | if button == 'OK': 102 | # If OK is clicked, fire the callback with whatever the URL 103 | # happens to be. 104 | self.callback(self.urlEntry.get()) 105 | 106 | # In any case, destroy the dialog when finished. 107 | self.dialog.destroy() 108 | return None 109 | 110 | 111 | class NewBookmarkDialog: 112 | def __init__(self, parentwin, cmd, resource=None): 113 | self.cmd = cmd 114 | self.dialog = Pmw.Dialog(parentwin, title="New Bookmark:", 115 | command=self.callback, 116 | buttons=('OK', 'Cancel')) 117 | i = self.dialog.interior() 118 | namebox = Frame(i) 119 | urlbox = Frame(i) 120 | Label(i, text="Enter Bookmark Information:").pack(side='top', expand=1, 121 | fill='both') 122 | namebox.pack(fill='both', expand=1) 123 | urlbox.pack(fill='both', expand=1) 124 | 125 | Label(namebox, text="Name:").pack(side='left') 126 | self.nameEntry = Entry(namebox, width=30) 127 | self.nameEntry.pack(side='right', fill='x', expand=1) 128 | Label(urlbox, text="URL:").pack(side='left') 129 | self.URLEntry = Entry(urlbox, width=30) 130 | self.URLEntry.pack(side='right', fill='x', expand=1) 131 | 132 | if resource: 133 | self.URLEntry.insert('end', resource.toURL()) 134 | self.nameEntry.insert('end', resource.getName()) 135 | 136 | return None 137 | 138 | def callback(self, button): 139 | if button and button != 'Cancel': 140 | res = Bookmark() 141 | res.setURL(self.URLEntry.get()) 142 | res.setName(self.nameEntry.get()) 143 | self.cmd(res) # Do whatever our parent wants us to with this... 144 | 145 | self.dialog.destroy() 146 | return None 147 | 148 | class NewFolderDialog: 149 | BUTTON_OK = 0 150 | BUTTON_CANCEL = 1 151 | 152 | def __init__(self, parentwin, callback, folderName=None): 153 | self.buttons = ('OK', 'Cancel') 154 | self.callback = callback 155 | self.parent = parentwin 156 | self.dialog = Pmw.Dialog(parentwin, 157 | title="New Folder:", 158 | command=self.closeDialog, 159 | buttons=self.buttons) 160 | 161 | i = self.dialog.interior() 162 | Label(i, text="New Folder Title:").grid(row=0, column=0, 163 | sticky='EW') 164 | self.__entry = Entry(i) 165 | self.__entry.grid(row=1, column=0, sticky='EW') 166 | 167 | if folderName: 168 | self.__entry.insert('end', folderName) 169 | 170 | self.bindCallbacks() 171 | return None 172 | 173 | def bindCallbacks(self): 174 | self.__entry.bind('', self.closeDialog) 175 | return None 176 | 177 | def closeDialog(self, result): 178 | if result == self.buttons[self.BUTTON_CANCEL]: 179 | self.dialog.destroy() 180 | return None 181 | else: 182 | str = self.__entry.get() 183 | self.dialog.destroy() 184 | return self.callback(str) 185 | # End NewFolderDialog 186 | 187 | class InformationDialog: 188 | def __init__(self, parent, errstr, title='Information:'): 189 | # We don't need an activate command since we want the dialog to just 190 | # get smacked when the user presses close. 191 | if title == '': 192 | title = errstr 193 | self.dialog = Pmw.Dialog(parent, title=title, buttons=["Close"]) 194 | 195 | # print "========================================================" 196 | # print "Error Dialog: %s" % errstr 197 | # print "========================================================" 198 | 199 | if type(errstr) != StringType: 200 | errstr = str(errstr) 201 | 202 | labels = split(errstr, "\n") 203 | 204 | for label in labels: 205 | Label(self.dialog.interior(), text=label).pack(side='top') 206 | 207 | # self.dialog.activate() # Modalize :) 208 | return None 209 | 210 | class ErrorDialog(InformationDialog): 211 | def __init__(self, parent, errstr, title="Error:"): 212 | InformationDialog.__init__(self, parent, errstr, title) 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /GUIAskForm.py: -------------------------------------------------------------------------------- 1 | # GUIAskForm.py 2 | # Written by David Allen 3 | # Released under the terms of the GNU General Public License 4 | # 5 | # The Tk/widget incarnation of AskForm 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ######################################################################## 21 | 22 | from tkinter import * 23 | from gopher import * 24 | import Pmw 25 | import ContentFrame 26 | import GopherResource 27 | import GopherResponse 28 | import GopherConnection 29 | import AskForm 30 | import Question 31 | import GUIQuestion 32 | 33 | class GUIAskForm(ContentFrame.ContentFrame, Frame): 34 | verbose = None 35 | 36 | def __init__(self, parent_widget, parent_object, resp, 37 | resource, filename=None, menuAssocs={}): 38 | Frame.__init__(self, parent_widget) # Superclass constructor 39 | self.resource = resource 40 | self.response = resp 41 | self.parent = parent_object 42 | self.filename = filename 43 | 44 | self.question_widgets = [] 45 | self.questionBox = Pmw.ScrolledFrame(self, 46 | horizflex='fixed', 47 | vertflex ='fixed', 48 | hscrollmode='dynamic', 49 | vscrollmode='dynamic') 50 | return None 51 | 52 | def pack_content(self, *args): 53 | for x in range(0, self.response.questionCount()): 54 | q = self.response.nthQuestion(x) 55 | try: 56 | typename = questions_types["%s" % q.getType()] 57 | except KeyError: 58 | typename = "(ERROR NO TYPE)" 59 | print("PROCESSING Question %s %s %s" % (typename, 60 | q.getDefault(), 61 | q.getPromptString())) 62 | try: 63 | wid = GUIQuestion.GUIQuestion(self.questionBox.interior(), q) 64 | except Exception as errstr: 65 | print("Couldn't make wid: %s" % errstr) 66 | continue 67 | 68 | wid.grid(row=x, column=0, sticky=W) 69 | self.question_widgets.append(wid) 70 | 71 | self.submit = Button(self, 72 | text="Submit", command=self.submit) 73 | self.questionBox.pack(side='top', expand=1, fill='both') 74 | self.submit.pack(side='bottom') 75 | return None 76 | 77 | def find(self, term, caseSensitive=None, lastIdentifier=None): 78 | self.parent.genericMessage("Sorry, AskForms are not searchable.\n" + 79 | "Or at least, not yet.") 80 | return None 81 | 82 | def submit(self, *args): 83 | """Submit the answers to the form questions to the server for 84 | processing, and load the results page.""" 85 | values = [] 86 | retstr = "" 87 | for widget in self.question_widgets: 88 | if widget.getType() == QUESTION_NOTE: 89 | continue 90 | values.append(widget.getResponse()) 91 | retstr = "%s%s" % (retstr, widget.getResponse()) 92 | 93 | print("Retstr is:\n%s" % retstr) 94 | getMe = GopherResource.GopherResource() 95 | 96 | # Shouldn't cache this resource. 97 | getMe.setShouldCache(None) 98 | getMe.dup(self.resource) # Copy defaults 99 | 100 | l = len(retstr) 101 | 102 | # Tell the server how much data we're sending... 103 | retstr = "+%d\r\n%s" % (l, retstr) 104 | 105 | getMe.setDataBlock(retstr) 106 | return self.parent.goElsewhere(getMe) 107 | -------------------------------------------------------------------------------- /GUIError.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Displays errors similar to "Cannot Load Page" in the main window. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | ########################################################################## 20 | 21 | from gopher import * 22 | from tkinter import * 23 | import Pmw 24 | 25 | import GopherResource 26 | import GopherResponse 27 | import GopherConnection 28 | import ContentFrame 29 | import Cache 30 | import Options 31 | 32 | class GUIError(ContentFrame.ContentFrame, Frame): 33 | verbose = None 34 | 35 | def __init__(self, parent_widget, resource, error_message): 36 | Frame.__init__(self, parent_widget) 37 | ContentFrame.ContentFrame.__init__(self) 38 | self.parent = parent_widget 39 | 40 | Label(self, text="Unable to load").pack() 41 | Label(self, text=resource.toURL()).pack() 42 | Label(self, text=error_message).pack() 43 | def pack_content(self, *args): 44 | return None 45 | def find(self, term, caseSensitive=None, lastIdentifier=None): 46 | self.parent.genericError("Error: Error messages\n" + 47 | "are not searchable.") 48 | return None 49 | -------------------------------------------------------------------------------- /GUIFile.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This is the class that describes how files behave when loaded into 5 | # the FORG. 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ############################################################################## 21 | 22 | from tkinter import * 23 | from gopher import * 24 | import Pmw 25 | import re 26 | import Options 27 | 28 | import ContentFrame 29 | import GopherResource 30 | import GopherResponse 31 | 32 | class GUIFile(ContentFrame.ContentFrame, Frame): 33 | verbose = None 34 | 35 | def __init__(self, parent_widget, parent_object, resp, 36 | resource, filename): 37 | Frame.__init__(self, parent_widget) # Superclass constructor 38 | self.resp = resp 39 | 40 | if self.useStatusLabels: 41 | labeltext = "%s:%d" % (resource.getHost(), int(resource.getPort())) 42 | 43 | if resource.getName() != '' and resource.getLocator() != '': 44 | label2 = "\"%s\" ID %s" % (resource.getName(), 45 | resource.getLocator()) 46 | else: 47 | label2 = " " 48 | 49 | if len(label2) > 50: 50 | label2 = label2[0:47] + "..." 51 | 52 | Label(self, text=labeltext).pack(expand=0, fill='x') 53 | Label(self, text=label2).pack(expand=0, fill='x') 54 | 55 | if resp.getTypeCode() != RESPONSE_FILE: 56 | Label(self, text="This file has been saved in:").pack() 57 | Label(self, text=filename).pack() 58 | else: 59 | self.textwid = Pmw.ScrolledText(self, hscrollmode='dynamic', 60 | vscrollmode='static') 61 | tw = self.textwid.component('text') 62 | tw.configure(background='#FFFFFF', foreground='#000000') 63 | 64 | self.textwid.component('text').bind('', 65 | parent_object.popupMenu) 66 | 67 | self.textwid.pack(expand=1, fill='both') 68 | return None 69 | 70 | def pack_content(self, *args): 71 | if Options.program_options.getOption('strip_carraige_returns'): 72 | # print "Stripping carriage returns..." 73 | data = self.resp.getData().replace("\r", "") 74 | else: 75 | data = self.resp.getData() 76 | 77 | if len(data) < 1024: 78 | self.textwid.settext(data) 79 | else: 80 | for index in range(0, len(data), 500): 81 | self.textwid.insert('end', data[index:index+500]) 82 | return None 83 | 84 | def destroy(self, *args): 85 | self.pack_forget() 86 | self.textwid.destroy() 87 | Frame.destroy(self) 88 | return None 89 | 90 | def find(self, term, caseSensitive=None, lastIdentifier=None): 91 | """Overrides the function of the same type from ContentFrame""" 92 | try: 93 | # This will raise an exception if it's a 'save' type layout 94 | # where the data isn't displayed to the user. 95 | tw = self.textwid.component('text') 96 | print("Component is ", tw) 97 | except: 98 | # Don't mess with this. The user can read the entire label, all 99 | # big bad few lines of it. 100 | raise Exception("This window is not searchable.") 101 | 102 | if lastIdentifier is None: 103 | lastIdentifier = '0.0' 104 | 105 | # The variable caseSensitive is true if the search is case sensitive, 106 | # and false otherwise. But since tkinter wants to know whether or not 107 | # the search is case INsensitive, we flip the boolean value, and use 108 | # it for the 'nocase' keyword arg to the search method. 109 | csflipped = (not caseSensitive) 110 | pos = tw.search(pattern=term, forwards=1, 111 | nocase=csflipped, index=lastIdentifier, 112 | stopindex=END) 113 | 114 | if pos: 115 | # Find the real index of the position returned. 116 | found_index = tw.index(pos) 117 | else: 118 | found_index = None 119 | 120 | print("Found index is \"%s\"" % found_index) 121 | 122 | if found_index: 123 | tw.yview(found_index) 124 | ending_index = tw.index("%s + %d chars" % (found_index, len(term))) 125 | # Set the selection to highlight the given word. 126 | tw.tag_add(SEL, found_index, ending_index) 127 | return tw.index("%s + 1 chars" % found_index) 128 | 129 | return None 130 | -------------------------------------------------------------------------------- /GUIQuestion.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # A GUI representation of a question. It should provide all methods needed 7 | # to get the response, set default values, etc. 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | ############################################################################# 23 | 24 | from tkinter import * 25 | import Pmw 26 | import tkinter.filedialog 27 | 28 | from gopher import * 29 | import GopherResource 30 | import GopherResponse 31 | import GopherConnection 32 | import ContentFrame 33 | import Cache 34 | import Options 35 | import Question 36 | 37 | GUIQuestionException = "Sorry. Things just sometimes go wrong." 38 | 39 | class GUIQuestion(Frame): 40 | verbose = None 41 | 42 | def __init__(self, parent_widget, question): 43 | Frame.__init__(self, parent_widget) # Superclass constructor 44 | self.parent = parent_widget 45 | self.question = question 46 | self.type = self.question.getType() 47 | 48 | promptString = self.question.getPromptString() 49 | defaultValue = self.question.getDefault() 50 | 51 | Label(self, text=promptString).grid(row=0, column=0, sticky=W) 52 | 53 | if self.type == QUESTION_NOTE: 54 | # Prompt string is all we need for this one. 55 | return None 56 | 57 | if self.type == QUESTION_ASK: 58 | self.entry = Entry(self) 59 | if len(defaultValue) > 0: 60 | self.entry.insert('end', defaultValue) 61 | self.entry.grid(row=0, column=1, columnspan=4, sticky=W) 62 | return None 63 | if self.type == QUESTION_ASKF or self.type == QUESTION_CHOOSEF: 64 | self.entry = Entry(self) 65 | if len(defaultValue) > 0: 66 | self.entry.insert('end', defaultValue) 67 | self.entry.grid(row=0, column=1, columnspan=4, sticky=W) 68 | 69 | # Browse buttons for file selection. 70 | self.browse = Button(text="Browse", command=self.browse) 71 | self.browse.grid(row=0, column=5, sticky=W) 72 | return None 73 | if self.type == QUESTION_ASKP: 74 | self.entry = Entry(self, show="*") 75 | 76 | if len(defaultValue) > 0: 77 | self.entry.insert('end', defaultValue) 78 | self.entry.grid(row=0, column=1, columnspan=4, sticky=W) 79 | return None 80 | if self.type == QUESTION_ASKL: 81 | self.entry = Pmw.ScrolledText(self, hscrollmode='dynamic', 82 | text_width=80, text_height=6, 83 | vscrollmode='dynamic') 84 | self.entry.grid(row=1, column=0, columnspan=2, rowspan=2, 85 | sticky='N') 86 | return None 87 | if self.type == QUESTION_SELECT: 88 | self.entry = Pmw.RadioSelect(self, buttontype='checkbutton', 89 | command=self.changed) 90 | 91 | for opt in self.question.options: 92 | self.entry.add(opt) 93 | 94 | if defaultValue: 95 | print("Invoking defalut %s" % defaultValue) 96 | self.entry.invoke(defaultValue) 97 | 98 | self.entry.grid(row=1, column=0, columnspan=4, rowspan=4, 99 | sticky='NSEW') 100 | print('Returning SELECT GUIQuestion') 101 | return None 102 | if self.type == QUESTION_CHOOSE: 103 | self.entry = Pmw.RadioSelect(self, buttontype='radiobutton', 104 | command=self.changed) 105 | for opt in self.question.options: 106 | self.entry.add(opt) 107 | if defaultValue: 108 | print("Invoking defalut %s" % defaultValue) 109 | self.entry.invoke(defaultValue) 110 | 111 | self.entry.grid(row=1, column=0, columnspan=4, rowspan=4, 112 | sticky='NSEW') 113 | print("Returning CHOOSE GUIQuestion") 114 | return None 115 | return None 116 | def browse(self, *args): 117 | dir = os.path.abspath(os.getcwd()) 118 | filename = tkinter.filedialog.asksaveasfilename(initialdir=dir) 119 | self.entry.delete(0, 'end') 120 | if filename: 121 | self.entry.insert('end', filename) 122 | return None 123 | def getType(self): 124 | return self.question.getType() 125 | def changed(self, *args): 126 | print("Selection changed: Current selection: ", args) 127 | def getResponse(self): 128 | """Returns the current state of the widget, or what should be sent 129 | to the server.""" 130 | if self.entry.__class__ == Entry: 131 | return "%s\n" % self.entry.get() 132 | elif self.entry.__class__ == Pmw.ScrolledText: 133 | buf = self.entry.get() 134 | lines = count(buf, "\n") 135 | return "%d\n%s\n" % (lines, buf) 136 | elif self.entry.__class__ == Pmw.RadioSelect: 137 | list = self.entry.getcurselection() 138 | return "%s\n" % list[0] 139 | else: 140 | # Huh? What? Eh? WTF is going on? 141 | raise GUIQuestionException("Cannot get content: Unknown type") 142 | return "" 143 | 144 | 145 | -------------------------------------------------------------------------------- /GUISaveFile.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This class asks users graphically which filename they'd like to save certain 5 | # files into. This is generally used for downloaded files that don't have 6 | # associations bound to them, and that also can't be displayed, because 7 | # they're binary data or otherwise ugly to look at as text. :) (e.g. 8 | # *.zip, *.tar.gz, *.tgz, binhex, etc. 9 | # 10 | # Since this is a last resort, this module will try to import the Python 11 | # Imaging Library (PIL) to display images if it is present. Note that this 12 | # requires that there be no association for the image files, since if there 13 | # is, it won't even get as far as creating a GUISaveFile object to display. 14 | # 15 | # This program is free software; you can redistribute it and/or modify 16 | # it under the terms of the GNU General Public License as published by 17 | # the Free Software Foundation; either version 2 of the License, or 18 | # (at your option) any later version. 19 | # 20 | # This program is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | # GNU General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public License 26 | # along with this program; if not, write to the Free Software 27 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 28 | ############################################################################## 29 | from tkinter import * 30 | from gopher import * 31 | import os 32 | import tkinter.filedialog 33 | import Pmw 34 | import re 35 | import Options 36 | 37 | import ContentFrame 38 | import GopherResource 39 | import GopherResponse 40 | import Dialogs 41 | 42 | try: 43 | import PIL 44 | from PIL import Image 45 | from PIL import ImageTk 46 | except: 47 | print("Bummer dude! You don't have the PIL installed on your machine!") 48 | print("That means that the \"Use PIL\" option is going to be irrelevant") 49 | print("for you.") 50 | 51 | class PILImage(Label): 52 | # This was borrowed and adapted from the PIL programming examples. 53 | def __init__(self, master, im): 54 | if im.mode == "1": 55 | # bitmap image 56 | self.image = ImageTk.BitmapImage(im, foreground="white") 57 | Label.__init__(self, master, image=self.image, bg="black", bd=0) 58 | else: 59 | # Photo image object would be better... 60 | self.image = ImageTk.PhotoImage(im) 61 | Label.__init__(self, master, image=self.image, bd=0) 62 | 63 | class GUISaveFile(ContentFrame.ContentFrame, Frame): 64 | verbose = None 65 | 66 | def __init__(self, parent_widget, parent_object, resp, resource, filename): 67 | Frame.__init__(self, parent_widget) # Superclass constructor 68 | self.r1 = None 69 | self.r2 = None 70 | self.filename = filename[:] 71 | self.parent = parent_object 72 | self.response = resp 73 | self.resource = resource 74 | 75 | # Don't even try to use the PIL unless the option is set 76 | # to allow it. 77 | usePIL = Options.program_options.getOption("use_PIL") 78 | 79 | if usePIL and self.canDisplay(): 80 | try: 81 | self.packImageContent() 82 | except Exception as errstr: 83 | self.packSaveContent() 84 | else: 85 | self.packSaveContent() 86 | print("Packed save content") 87 | return None 88 | 89 | def packImageContent(self, *args): 90 | self.createPopup() 91 | self.image = Image.open(self.filename) 92 | self.scrframe = Pmw.ScrolledFrame(self) 93 | imgwidget = PILImage(self.scrframe.interior(), self.image) 94 | imgwidget.pack() 95 | imgwidget.bind('', self.popupMenu) 96 | self.scrframe.pack(fill='both', expand=1) 97 | 98 | # Return and DON'T display the save file as box only if the 99 | # file had no problems. Otherwise an exception was raised. 100 | return None 101 | 102 | def revertToSaveDialog(self, *args): 103 | self.scrframe.pack_forget() # Unpack the scrolled frame 104 | self.scrframe = None # Lose the ref to the scrolled frame 105 | self.image = None # Lose the ref to the image object. 106 | self.packSaveContent() # Go back to the "Save File" content 107 | self.pack_content() 108 | return None 109 | 110 | def imageInfo(self, *args): 111 | try: 112 | if not self.image: 113 | return None 114 | except: 115 | return None 116 | 117 | info = "Bands: %s" % self.image.getbands().join(", ") 118 | size = self.image.size 119 | info = "%s\nWidth: %d pixels\nHeight: %d pixels" % (info, 120 | size[0], size[1]) 121 | info = "%s\nMode: %s" % (info, self.image.mode) 122 | 123 | for key in list(self.image.info.keys()): 124 | info = "%s\n%s = %s" % (info, key, self.image.info[key]) 125 | 126 | d = Dialogs.ErrorDialog(self, errstr=info, 127 | title='Image Information (PIL)') 128 | return None 129 | 130 | def createPopup(self): 131 | """Pop-up menu on right click on a message""" 132 | self.popup = Menu(self) 133 | self.popup['tearoff'] = FALSE 134 | self.popup.add_command(label='Save', 135 | command=self.revertToSaveDialog) 136 | self.popup.add_command(label='Info', 137 | command=self.imageInfo) 138 | 139 | def popupMenu(self, event): 140 | """Display pop-up menu on right click on a message""" 141 | self.popup.tk_popup(event.x_root, event.y_root) 142 | 143 | def canDisplay(self): 144 | try: 145 | fn = Image.open 146 | except: 147 | return None 148 | 149 | s = self.filename 150 | ind = s.rfind(".") 151 | 152 | if ind != -1 and ind != (len(s)-1): 153 | fileExtension = s[ind:].lower() 154 | 155 | return 1 156 | 157 | def find(self, term, caseSensitive=None, lastIdentifier=None): 158 | self.parent.genericError("Error: Save Dialogs\n" + 159 | "are not searchable.") 160 | return None 161 | 162 | def pack_content(self, *args): 163 | return None 164 | 165 | def packSaveContent(self, *args): 166 | # Explicit copy - damn python and its ability to trip me up with all 167 | # that "everything's a reference" stuff. :) 168 | default_filename = self.filename[:] 169 | 170 | for char in ['/', ':', ' ', r'\\']: 171 | strtofind = "%" + "%d;" % ord(char[0]) 172 | default_filename = re.sub(strtofind, char, default_filename) 173 | 174 | for separator in ['/', ':', '\\']: 175 | ind = default_filename.rfind(separator) 176 | if ind != -1: 177 | default_filename = default_filename[ind+len(separator):] 178 | break 179 | 180 | if self.useStatusLabels: 181 | labeltext = "%s:%d" % (self.resource.getHost(), int(self.resource.getPort())) 182 | 183 | if self.resource.getName() != '' and self.resource.getLocator() != '': 184 | label2 = "\"%s\" ID %s" % (self.resource.getName(), 185 | self.resource.getLocator()) 186 | else: 187 | label2 = " " 188 | 189 | if len(label2) > 50: 190 | label2 = label2[0:47] + "..." 191 | 192 | Label(self, text=labeltext).pack(expand=0, fill='x') 193 | Label(self, text=label2).pack(expand=0, fill='x') 194 | 195 | Label(self, text=" ").pack() # Empty line. 196 | 197 | Label(self, 198 | text="Please enter a filename to save this file as:").pack() 199 | 200 | cframe = Frame(self) 201 | cframe.pack(expand=1, fill='both') 202 | Label(cframe, text="Filename:").pack(side='left') 203 | 204 | self.filenameEntry = Entry(cframe) 205 | self.filenameEntry.insert('end', default_filename) 206 | self.filenameEntry.pack(side='left', expand=1, fill='x') 207 | self.filenameEntry.bind("", self.save) 208 | Button(cframe, text="Browse", command=self.browse).pack(side='right') 209 | self.saveButton = Button(cframe, text="Save", command=self.save) 210 | self.saveButton.pack(side='right') 211 | 212 | return None 213 | 214 | def browse(self, *args): 215 | dir = os.path.abspath(os.getcwd()) 216 | filename = tkinter.filedialog.asksaveasfilename(initialdir=dir) 217 | 218 | if filename: 219 | self.filenameEntry.delete(0, 'end') 220 | self.filenameEntry.insert('end', filename) 221 | 222 | return None 223 | def save(self, *args): 224 | filename = self.filenameEntry.get() 225 | 226 | try: 227 | fp = open(filename, "w") 228 | fp.write(self.response.getData()) 229 | fp.flush() 230 | fp.close() 231 | except IOError as errstr: 232 | self.parent.genericError("Couldn't save file\n%s:\n%s" % (filename, 233 | errstr)) 234 | return None 235 | 236 | if self.r1: 237 | self.r1.destroy() 238 | if self.r2: 239 | self.r2.destroy() 240 | 241 | self.r1 = Label(self, text="File successfully saved into").pack() 242 | self.r2 = Label(self, text=filename).pack() 243 | return None 244 | -------------------------------------------------------------------------------- /GUISearch.py: -------------------------------------------------------------------------------- 1 | # GUISearch.py 2 | # $Id: GUISearch.py,v 1.6 2001/04/15 19:27:00 s2mdalle Exp $ 3 | # Written by David Allen 4 | # 5 | # Released under the terms of the GNU General Public License 6 | # 7 | # This is the graphical component used for getting information from the user 8 | # about search terms. (And then sending them on their merry way) 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 23 | ############################################################################## 24 | 25 | from tkinter import * 26 | from gopher import * 27 | import Pmw 28 | import ContentFrame 29 | import GopherResource 30 | import GopherResponse 31 | import GopherConnection 32 | 33 | class GUISearch(ContentFrame.ContentFrame, Frame): 34 | verbose = None 35 | 36 | def __init__(self, parent_widget, parent_object, resp, 37 | resource, filename=None, menuAssocs={}): 38 | Frame.__init__(self, parent_widget) # Superclass constructor 39 | self.resource = resource 40 | self.response = resp 41 | self.parent = parent_object 42 | self.filename = filename 43 | 44 | labels = ["Please enter a space-separated list of search terms."] 45 | 46 | last = 0 47 | 48 | for x in range(0, len(labels)): 49 | last = x 50 | label = labels[x] 51 | Label(self, text=label, foreground='#000000').grid(row=x, column=0, 52 | columnspan=2) 53 | 54 | self.entryArea = Frame(self) 55 | self.entryArea.grid(row=(x+1), column=0, columnspan=5, sticky='EW') 56 | self.entryBox = Entry(self.entryArea, text='') 57 | self.entryBox.pack(side='left', expand=1, fill='x') 58 | self.entryBox.bind("", self.submit) 59 | self.GO = Button(self.entryArea, text='Submit', command=self.submit) 60 | self.GO.pack(side='right') 61 | self.bottom_label = None 62 | return None 63 | def pack_content(self, *args): 64 | return None 65 | def find(self, term, caseSensitive=None, lastIdentifier=None): 66 | self.parent.genericError("Error: Search boxes\n" + 67 | "are not searchable.") 68 | return None 69 | def submit(self, *args): 70 | terms = self.entryBox.get() 71 | 72 | print("Terms are \"%s\"" % terms) 73 | 74 | if self.bottom_label: 75 | self.bottom_label.destroy() 76 | self.bottom_label = Label(self, "Searching for \"%s\"" % terms) 77 | self.bottom_label.grid(row=10, column=0, columnspan=2, 78 | sticky=W) 79 | 80 | # Copy the data from the current resource. 81 | res = GopherResource.GopherResource() 82 | res.setHost(self.resource.getHost()) 83 | res.setPort(self.resource.getPort()) 84 | res.setName(self.resource.getName()) 85 | 86 | # This is a nasty way of faking the appropriate protocol message, 87 | # but oh well... 88 | res.setLocator("%s\t%s" % (self.resource.getLocator(), 89 | terms)) 90 | res.setInfo(None) 91 | res.setLen(-2) 92 | res.setTypeCode(RESPONSE_DIR) # Response *will* be a directory. 93 | 94 | self.parent.goElsewhere(res) 95 | return None 96 | 97 | 98 | -------------------------------------------------------------------------------- /GopherConnection.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # This object handles connections and sending requests to gopher servers. 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 21 | ####################################################################### 22 | import socket 23 | import re 24 | import utils 25 | import GopherResponse 26 | import Connection 27 | import GopherObject 28 | import GopherResource 29 | import ResourceInformation 30 | import AskForm 31 | from gopher import * 32 | 33 | class GopherConnectionException(Exception): 34 | pass 35 | 36 | class GopherConnection(Connection.Connection): 37 | SNARFSIZE = 1024 38 | verbose = None 39 | def __init__(self, server="gopher://gopher.floodgap.com", port=70): 40 | Connection.Connection.__init__(self) 41 | self.server = re.sub("gopher://", "", server, 1) 42 | self.port = port 43 | self.forgetResponse() 44 | return None 45 | 46 | def forgetResponse(self): 47 | """Remove the response field of this object. This is periodically 48 | necessary, since it can get quite large.""" 49 | self.response = None 50 | return self.response 51 | def stripTail(self, data): 52 | ind = data.rfind("\r\n.\r\n") 53 | 54 | if ind != -1: 55 | if self.verbose: 56 | print("Stripping protocol footer at index %d" % int(ind)) 57 | return data[0:ind] 58 | 59 | if self.verbose: 60 | print("No protocol footer found.") 61 | return data 62 | 63 | # Get extended information about a resource. 64 | def getInfo(self, resource, msgBar=None): 65 | try: 66 | req = "%s\t!\r\n" % resource.getLocator() 67 | data = self.requestToData(resource, req, msgBar) 68 | except Connection.ConnectionException as errstr: 69 | raise GopherConnectionException(errstr) 70 | 71 | if self.verbose: 72 | print("Got %d bytes from INFO conn:\n%s" % (len(data), data)) 73 | 74 | # The server sends back a length of -2 when it doesn't know how long 75 | # the document is, and when the document may contain the \r\n.\r\n 76 | # pattern. So if it might, do not strip it out. Otherwise do. This 77 | # will probably keep out a very subtle bug of having files downloaded 78 | # be truncated in really weird places. (Where the \r\n.\r\n would have 79 | # been) 80 | if resource.getLen() != -2: 81 | if self.verbose: 82 | print("Stripping protocol footer.") 83 | data = self.stripTail(data) 84 | 85 | try: 86 | info = ResourceInformation.ResourceInformation(data) 87 | except Exception as estr: 88 | print("*********************************************************") 89 | print("***GopherConnection: ResourceInformation Error: %s" % estr) 90 | print("*********************************************************") 91 | raise GopherConnectionException(estr) 92 | 93 | return info 94 | 95 | def getResource(self, resource, msgBar=None): 96 | self.forgetResponse() 97 | self.host = re.sub("gopher://", "", resource.getHost(), 1) 98 | self.port = resource.getPort() 99 | self.lastType = resource.getType() 100 | 101 | try: 102 | if resource.getDataBlock(): 103 | request = resource.getLocator() + "\t+\t1\r\n" 104 | request = request + resource.getDataBlock() 105 | 106 | self.response = self.requestToData(resource, 107 | request, 108 | msgBar, 1) 109 | elif resource.isGopherPlusResource() and resource.isAskType(): 110 | info = resource.getInfo(shouldFetch=1) 111 | af = AskForm.AskForm(info.getBlock("ASK")) 112 | 113 | # Copy host/port/locator information into af 114 | af.dup(resource) 115 | return af 116 | elif resource.isGopherPlusResource(): 117 | request = resource.getLocator() + "\t+\r\n" 118 | self.response = self.requestToData(resource, 119 | request, 120 | msgBar, 1) 121 | else: 122 | request = resource.getLocator() + "\r\n" 123 | self.response = self.requestToData(resource, request, msgBar, None) 124 | except Connection.ConnectionException as estr: 125 | error_resp = GopherResponse.GopherResponse() 126 | errstr = "Cannot fetch\n%s:\n%s" % (resource.toURL(), estr) 127 | 128 | error_resp.setError(errstr) 129 | return error_resp 130 | 131 | utils.msg(msgBar, "Examining response...") 132 | 133 | resp = GopherResponse.GopherResponse() 134 | resp.setType(resource.getTypeCode()) 135 | 136 | if resource.getLen() != -2: 137 | self.response = self.stripTail(self.response) 138 | 139 | try: 140 | # The parser picks up directory entries and sets the internal 141 | # data of the object as needed. 142 | resp.parseResponse(self.response) 143 | 144 | # if we get this far, then it's a directory entry, so set the 145 | # data to nothing. 146 | resp.setData(None) 147 | except Exception as erstr: 148 | # print "Caught exception while parsing response: \"%s\"" % erstr 149 | if self.verbose: 150 | print("OK, it's data.") 151 | resp.setData(self.response) 152 | 153 | return resp 154 | -------------------------------------------------------------------------------- /GopherObject.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # Base class for GopherResource, GopherResponse 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 21 | ############################################################################# 22 | 23 | import re 24 | import os 25 | import utils 26 | 27 | from gopher import * 28 | 29 | class GopherObject: 30 | def __init__(self, 31 | typecode = None, 32 | host = None, 33 | port = None, 34 | locator = None, 35 | name = None, 36 | len = -2): 37 | self.__class = "GopherObject" 38 | self._shouldCache = "YES" 39 | self.setTypeCode(typecode) 40 | self.setHost(host) 41 | self.setPort(port) 42 | self.setLocator(locator) 43 | self.setName(name) 44 | self.setLen(len) 45 | self.setDataBlock("") 46 | return None 47 | def dup(self, res): 48 | """Returns a copy of this object.""" 49 | self.setLen(res.getLen()) 50 | self.setTypeCode(res.getTypeCode()) 51 | self.setHost(res.getHost()) 52 | self.setLocator(res.getLocator()) 53 | self.setName(res.getName()) 54 | self.setPort(res.getPort()) 55 | return self 56 | # Accessors, mutators 57 | def shouldCache(self): 58 | return self._shouldCache 59 | def setShouldCache(self, boolval): 60 | self._shouldCache = boolval 61 | return self.getShouldCache() 62 | def getShouldCache(self): 63 | return self._shouldCache 64 | def getLen(self): 65 | return self.len 66 | def setLen(self, newlen): 67 | self.len = newlen 68 | return self.len 69 | def getTypeCode(self): 70 | return self.type[0] 71 | def setDataBlock(self, block): 72 | self.datablock = block 73 | return self.datablock 74 | def getDataBlock(self): 75 | return self.datablock 76 | def setType(self, newtype): 77 | return self.setTypeCode(newtype) 78 | def setTypeCode(self, newtype): 79 | self.type = newtype 80 | return self.type 81 | def getType(self): 82 | """Return a string representing the type code of the response. See 83 | the gopher module for more information.""" 84 | try: 85 | return responses[self.type] 86 | except KeyError: 87 | return "-Unknown-" 88 | def getHost(self): 89 | return self.host 90 | def setHost(self, newhost): 91 | self.host = newhost 92 | return self.host 93 | def getPort(self): 94 | return self.port 95 | def setPort(self, newport): 96 | self.port = newport 97 | return self.port 98 | def getLocator(self): 99 | return self.locator 100 | def setLocator(self, newlocator): 101 | self.locator = newlocator 102 | return self.locator 103 | def getName(self): 104 | if self.name.strip() == '/': 105 | self.setName("%s root" % self.getHost()) 106 | elif self.name == '' or self.name is None: 107 | loc = self.getLocator().strip() 108 | if loc == '' or loc == '/': 109 | self.setName("%s root" % self.getHost()) 110 | else: 111 | self.setName(" ") 112 | # self.setName("%s %s" % (self.getHost(), self.getLocator())) 113 | return self.name 114 | def setName(self, newname): 115 | self.name = newname 116 | return self.name 117 | # Methods 118 | def toURL(self): 119 | """Return the URL form of this GopherResource""" 120 | return "gopher://%s:%s/%s%s" % (self.getHost(), 121 | self.getPort(), 122 | self.getTypeCode(), 123 | re.sub("\t", "%9;", self.getLocator())) 124 | def toProtocolString(self): 125 | """Returns the protocol string, i.e. how it would have been served 126 | by the server, in a string.""" 127 | return "%s%s\t%s\t%s\t%s\r\n" % (self.getTypeCode(), 128 | self.getName(), 129 | self.getLocator(), 130 | self.getHost(), 131 | self.getPort()) 132 | def toXML(self): 133 | """Returns a small XML tree corresponding to this object. The root 134 | element is the name of the object. Override me in subclasses.""" 135 | tags = [["type", self.type], 136 | ["locator", self.locator], 137 | ["host", self.host], 138 | ["port", self.port], 139 | ["name", self.name]] 140 | 141 | str = "" 142 | for tag in tags: 143 | str = str + "<%s>%s" % (tag[0], tag[1], tag[0]) 144 | str = str + "" 145 | 146 | return str 147 | def __str__(self): 148 | return self.__class 149 | # return self.toString() 150 | def toString(self): 151 | """Returns a string form of this object. Mostly only good for 152 | debugging.""" 153 | return ("Type: %s\nLocator: %s\nHost: %s\nPort: %s\nName: %s\n" % 154 | (self.getTypeCode(), self.getLocator(), 155 | self.getHost(), self.getPort(), self.getName())) 156 | 157 | def filenameToURL(self, filename): 158 | """Unencodes filenames returned by toFilename() into URLs""" 159 | try: 160 | return utils.character_replace(filename, os.sep, "/") 161 | except: 162 | # os.sep is '/' - character_replace doesn't allow replacing a 163 | # character with itself. we're running Eunuchs. 164 | return filename 165 | 166 | def toCacheFilename(self): 167 | filename = self.toFilename() 168 | lastchr = filename[len(filename)-1] 169 | if lastchr == os.sep or lastchr == '/': 170 | # Pray for no name clashes... :) 171 | # We have to call it something inside the leaf directory, because 172 | # if you don't, you get into the problem of caching responses from 173 | # directories as files, and not having a directory to put the items 174 | # in the directory in. And you can't call the file "" on any 175 | # filesystem. :) 176 | filename = "%sgopherdir.idx" % filename 177 | elif self.getTypeCode() == RESPONSE_DIR: 178 | filename = "%s%sgopherdir.idx" % (filename, os.sep) 179 | 180 | port = self.getPort() 181 | str_to_find = ":%d" % int(port) 182 | 183 | # Cut out the port portion of the filename. This is because some 184 | # OS's throw up with ':' in filenames. Bummer, but this will make it 185 | # hard to translate a filename/path -> URL 186 | ind = filename.find(str_to_find) 187 | if ind != -1: 188 | filename = "%s%s" % (filename[0:ind], 189 | filename[ind+len(str_to_find):]) 190 | 191 | if os.sep == '\\': # No-name mac wannabe OS's... :) 192 | for char in ['/', ':', ';', '%', '*', '|']: 193 | # This isn't necessarily a good idea, but it's somewhat 194 | # necessary for windows boxen. 195 | filename = utils.character_replace(filename, char, ' ') 196 | 197 | return filename 198 | 199 | def toFilename(self): 200 | """Returns the name of a unique file containing the elements of this 201 | object. This file is not guaranteed to not exist, but it probably 202 | doesn't. :) Get rid of all of the slashes, since they are 203 | Frowned Upon (TM) by most filesystems.""" 204 | 205 | replaceables = ['\t', '\n', '\\', '/'] 206 | data = self.toURL() 207 | 208 | if data.lower().lstrip().find("gopher://") == 0: 209 | # Chomp the "gopher://" part 210 | data = data[len("gopher://"):] 211 | 212 | for r in replaceables: 213 | try: 214 | data = utils.character_replace(data, r, os.sep) 215 | except: 216 | pass 217 | 218 | return data 219 | -------------------------------------------------------------------------------- /GopherResource.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # This class defines the information needed for a gopher resource. That 5 | # usually contains all the needed information about one instance of a file, 6 | # directory, or other "thingy" on a gopher server. This class extends 7 | # GopherObject which gives it most of its accessors/mutators. 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 | ############################################################################# 23 | from urllib.parse import * 24 | from gopher import * 25 | import GopherConnection 26 | import GopherObject 27 | # import Options 28 | 29 | 30 | class GopherResource(GopherObject.GopherObject): 31 | verbose = None 32 | debugging = None # Set to true for messages at the prompt, etc. 33 | 34 | def __init__(self, type=RESPONSE_DIR, host="gopher.floodgap.com", port=70, locator="/", stringName = "", auxFields=None): 35 | GopherObject.GopherObject.__init__(self, type, host, port, locator, stringName) 36 | self.__class = "GopherResource" 37 | if self.debugging: 38 | print("NEW GOPHER RESOURCE: " + self.toString()) 39 | self.info = None 40 | if not auxFields: 41 | auxFields = [] 42 | self.setAuxFields(auxFields) 43 | 44 | def setInfo(self, newinfo): 45 | self.info = newinfo 46 | return self.info 47 | 48 | def getInfo(self, shouldFetch=None): 49 | """Returns the ResourceInformation block associated with this 50 | Resource. If shouldFetch is true, the resource block will fetch 51 | information about itself if none is present.""" 52 | if not self.info and shouldFetch: 53 | try: 54 | self.setInfo(conn.getInfo(self)) 55 | except Exception as errstr: 56 | print("**** GopherResource couldn't get info about itself:") 57 | print(errstr) 58 | # This is bad. 59 | self.setInfo(None) 60 | 61 | return self.info 62 | 63 | def setAuxFields(self, fields): 64 | self.auxFields = fields 65 | 66 | if len(self.auxFields) > 0 and self.auxFields[0] == '?': 67 | # We need to fetch information about this one, since it's 68 | # going to contain ASK blocks. 69 | conn = GopherConnection.GopherConnection() 70 | 71 | try: 72 | self.setInfo(conn.getInfo(self)) 73 | except Exception as errstr: 74 | print("**** GopherResource couldn't get info about itself:") 75 | print(errstr) 76 | # This is bad. 77 | self.setInfo(None) 78 | 79 | return self.auxFields 80 | 81 | def getAuxFields(self): 82 | return self.auxFields 83 | 84 | def isAskType(self): 85 | if not self.isGopherPlusResource(): 86 | return None 87 | 88 | if len(self.auxFields) > 0 and self.auxFields[0].strip() == '?': 89 | return 1 90 | else: 91 | return None 92 | 93 | def isGopherPlusResource(self): 94 | if len(self.auxFields) > 0: 95 | return 1 96 | else: 97 | return None 98 | 99 | def toProtocolString(self): 100 | """Overrides the GopherObject method of the same name to provide 101 | support for printing out the auxFields in this object.""" 102 | return "%s%s\t%s\t%s\t%s\t%s\r\n" % (self.getTypeCode(), 103 | self.getName(), 104 | self.getLocator(), 105 | self.getHost(), self.getPort(), 106 | "\t".join(self.getAuxFields())) 107 | 108 | def toXML(self): 109 | """Returns a small XML tree corresponding to this object. The root 110 | element is the name of the object. Override me in subclasses.""" 111 | tags = [["type", self.getType()], 112 | ["locator", self.getLocator()], 113 | ["host", self.getHost()], 114 | ["port", self.getPort()], 115 | ["name", self.getName()]] 116 | 117 | str = "" 118 | for tag in tags: 119 | str = str + "<%s>%s" % (tag[0], tag[1], tag[0]) 120 | str = str + "" 121 | 122 | return str 123 | 124 | def toURL(self): 125 | """Return the URL form of this GopherResource""" 126 | return "gopher://%s:%s/%s%s" % (self.getHost(), self.getPort(), 127 | self.getTypeCode(), 128 | self.getLocator()) 129 | 130 | def setURL(self, URL): 131 | """Take a URL string, and convert it into a GopherResource object. 132 | This destructively modifies this object and returns a copy of itself 133 | 134 | FROM RFC 1738: 135 | Gopher URLs look like this: 136 | 137 | gopher://host:port/TypeCodeLocator 138 | 139 | where TypeCode is a one-character code corresponding to some entry 140 | in gopher.py (hopefully) and Locator is the locator string for the 141 | resource. 142 | """ 143 | thingys = urlparse(URL) 144 | type = thingys[0] 145 | hostport = thingys[1] 146 | resource = thingys[2] 147 | sublist = hostport.split(":", 2) 148 | host = sublist[0] 149 | 150 | try: 151 | port = sublist[1] 152 | port = int(port) 153 | except IndexError: 154 | port = 70 155 | except ValueError: 156 | port = 70 157 | 158 | self.setHost(host) 159 | self.setPort(port) 160 | 161 | # Strip the leading slash from the locator. 162 | if resource != '' and resource[0] == '/': 163 | resource = resource[1:] 164 | 165 | if len(resource) >= 2: 166 | newtypecode = resource[0] 167 | locator = resource[1:] 168 | else: 169 | newtypecode = RESPONSE_DIR 170 | locator = "/" 171 | 172 | self.setLocator(locator) 173 | self.setName(self.getLocator()) 174 | self.setTypeCode(newtypecode) 175 | return self # Return a copy of me 176 | 177 | # End GopherResource 178 | 179 | def URLtoResource(URL): 180 | """Non-class method mimicing GopherResource.setURL""" 181 | res = GopherResource() 182 | return res.setURL(URL) 183 | 184 | -------------------------------------------------------------------------------- /GopherResponse.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # This object holds the data corresponding to how a gopher server responded 7 | # to a given request. It holds one of two things in general, either a pile 8 | # of data corresponding to text, a gif file, whatever, or it holds a list of 9 | # GopherResource objects. (This is when it's a directory entry that's being 10 | # stored. 11 | # 12 | # This program is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by 14 | # the Free Software Foundation; either version 2 of the License, or 15 | # (at your option) any later version. 16 | # 17 | # This program is distributed in the hope that it will be useful, 18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | # GNU General Public License for more details. 21 | # 22 | # You should have received a copy of the GNU General Public License 23 | # along with this program; if not, write to the Free Software 24 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 25 | ############################################################################## 26 | import re 27 | from urllib.parse import * 28 | from gopher import * 29 | import Connection 30 | import GopherConnection 31 | import GopherObject 32 | import GopherResource 33 | import ResourceInformation 34 | import Options 35 | 36 | class GopherException(Exception): 37 | pass 38 | 39 | 40 | class GopherConnectionException(Exception): 41 | pass 42 | 43 | 44 | class GopherResponseException(Exception): 45 | def __init__(self, message): 46 | super(GopherResponseException, self).__init__(message) 47 | 48 | 49 | class GopherResponse(GopherObject.GopherObject): 50 | verbose = None 51 | 52 | def __init__(self, type=None, host=None, port=None, loc=None, name=None): 53 | GopherObject.GopherObject.__init__(self, type, host, port, loc, name) 54 | self.__class = "GopherResponse" 55 | self.data = None 56 | self.responses = [] 57 | 58 | def toProtocolString(self): 59 | if self.getData() is None: 60 | return "".join(list(map(lambda x: x.toProtocolString(), self.getResponses()))) 61 | else: 62 | return self.getData() 63 | 64 | def writeToFile(self, filename): 65 | """Writes the contents of this response to a disk file at filename 66 | this may raise IOError which the caller should deal with""" 67 | fp = open(filename, "w") 68 | 69 | if self.getData() == None: 70 | for resp in self.getResponses(): 71 | fp.write(resp.toProtocolString()) 72 | else: 73 | fp.write(self.getData()) 74 | 75 | fp.flush() 76 | fp.close() 77 | return filename 78 | 79 | def getResponses(self): 80 | """Return a list of responses. This is only really good when the 81 | response was a directory and set of entries.""" 82 | if self.getData() != None: 83 | raise GopherResponseException("Get data instead.") 84 | return self.responses 85 | 86 | def getData(self): 87 | """Return the data associated with the response. This is usually all 88 | of the data off of the socket except the trailing closer.""" 89 | return self.data 90 | 91 | def getDataLength(self): 92 | return len(self.data) 93 | 94 | def getDataChunk(self, startIndex, endIndex=-1): 95 | """This method provides a way to get chunks of data at a time, 96 | rather than snarfing the entire ball.""" 97 | 98 | if endIndex == -1: 99 | endIndex = len(self.data) 100 | return self.data[startIndex, endIndex] 101 | 102 | def getError(self): 103 | """If there was an error message, return it.""" 104 | try: 105 | return self.error 106 | except: 107 | return None 108 | 109 | def setError(self, errorstr): 110 | """Modify the error message within this response.""" 111 | self.error = errorstr 112 | return self.getError() 113 | 114 | def setData(self, data): 115 | """Modify the data within the response. You probably don't want to 116 | do this.""" 117 | self.data = data 118 | return None 119 | 120 | def looksLikeDir(self, data): 121 | """Takes a chunk of data and returns true if it looks like directory 122 | data, and false otherwise. This is tricky, and is of course not 100%. 123 | Basically, we look at each line, see if the first bit in the line is a 124 | legal type, check to see that the line has at least 3 tabs in it. If 125 | all of the first 20 lines of the data follow that rule, then it's good 126 | enough to be used as directory data. If not, it gets chucked. Notice 127 | that if this really is a directory but it's using file types we've 128 | never heard of (see gopher.py) then it will still get thrown out. 129 | Bummer. This should only be called anyway if the type indictator is 130 | incorrect, so cope. :)""" 131 | 132 | def linefn(l): 133 | return l.replace("\r", "") 134 | 135 | # Some very strange non-standards compliant servers send \r on some 136 | # lines and not on others. So split by newline and remove all 137 | # carriage returns as they occur. 138 | lines = list(map(linefn, data.split("\n", 10))) 139 | 140 | for line in lines: 141 | d = line.strip() 142 | if not d or d == '' or d == '.': 143 | continue 144 | 145 | if line.count("\t") < 2: 146 | return None # Not enough tabs. Bummer. 147 | 148 | isResponse = None 149 | byte = line[0] 150 | try: 151 | resp = responses[byte] 152 | isRespose = 1 153 | except: 154 | pass 155 | 156 | if isResponse: 157 | continue 158 | 159 | try: 160 | resp = errors[byte] 161 | except: 162 | return None 163 | 164 | if len(lines) > 0: 165 | return 1 # Matched all tests for max 20 lines. Looks OK. 166 | else: 167 | return 0 # Empty data isn't a directory. 168 | 169 | def parseResponse(self, data): 170 | """Takes a lump of data, and tries to parse it as if it was a directory 171 | response. It will set the responses array to the proper thing if the 172 | result was good, so that you can use self.getRepsonses() to access 173 | them. Otherwise it raises GopherResponseException""" 174 | 175 | self.responses = [] 176 | 177 | if self.type == RESPONSE_DIR: 178 | pass # Keep going 179 | elif self.looksLikeDir(data): 180 | self.type = RESPONSE_DIR 181 | else: 182 | raise GopherException("This isn't a directory.") 183 | 184 | def stripCR(dat): 185 | return dat.replace("\r", "") 186 | 187 | # This is done because \r may or may not be present, so we can't 188 | # split by \r\n because it fails for some misbehaved gopher servers. 189 | self.lines = list(map(stripCR, data.split("\n"))) 190 | 191 | for line in self.lines: 192 | if len(line) <= 1: 193 | continue 194 | 195 | # Type of the resource. See gopher.py for the possibilities. 196 | stype = "%s" % line[0] 197 | line = line[1:] 198 | 199 | # Gopher protocol uses tab delimited fields... 200 | linedata = line.split("\t") 201 | name = "Unknown" # Silly defaults 202 | locator = "Unknown" 203 | host = "Unknown" 204 | port = 70 205 | 206 | try: 207 | name = linedata[0] # Assign the right things in the 208 | except IndexError: pass # right places (maybe) 209 | try: # Catch those errors coming from 210 | locator = linedata[1] # the line not being split into 211 | except IndexError: pass # enough tokens. Realistically, 212 | try: # if those errors happen, 213 | host = linedata[2] # something is really screwed up 214 | except IndexError: pass # and there's not much we can do 215 | try: # anyway. 216 | port = linedata[3] 217 | except IndexError: pass 218 | 219 | try: 220 | remainder = linedata[4:] # Extra Gopher+ fields. 221 | except: 222 | remainder = [] 223 | 224 | # UMN gopherd errors do this sometimes. It's quite annoying. 225 | # they list the response type as 'directory' and then put the host 226 | # as 'error.host' to flag errors 227 | if host == 'error.host' and stype != RESPONSE_BLURB: 228 | stype = RESPONSE_ERR 229 | 230 | newresource = GopherResource.GopherResource(stype, host, 231 | port, locator, name, 232 | remainder) 233 | 234 | # Are the options set to allow us to get info? 235 | # THIS SHOULD BE USED WITH CAUTION since it can slow things down 236 | # more than you might think. 237 | if Options.program_options.getOption('grab_resource_info'): 238 | if len(remainder) >= 1 and remainder[0] == '+': 239 | try: 240 | conn = GopherConnection.GopherConnection() 241 | info = conn.getInfo(newresource) 242 | newresource.setInfo(info) 243 | except GopherConnectionException as estr: 244 | print("***(GCE) can't get info: %s" % estr) 245 | except Exception as estr: 246 | print("***(unknown) Couldn't %s %s" % (Exception,estr)) 247 | 248 | self.responses.append(newresource) 249 | return None 250 | # End GopherResponse 251 | -------------------------------------------------------------------------------- /List.py: -------------------------------------------------------------------------------- 1 | # List.py 2 | # $Id: List.py,v 1.8 2001/08/12 20:40:14 s2mdalle Exp $ 3 | # Written by David Allen 4 | # 5 | # This is a data structure similar to a doubly linked list, but with a few 6 | # exceptions - it has to keep track of all nodes that have EVER been in the 7 | # list, to cache old nodes for jumping around. It also has to know that when 8 | # you insert a completely new item, it removes everything after the current 9 | # slot in the list. This is because when you jump to a new document like in 10 | # a web browser, you can't then hit the 'forward' button. 11 | # 12 | # The forward and back buttons in the program will correspond to methods in 13 | # this object. 14 | # 15 | # Only put ListNode objects into this. 16 | # 17 | # This program is free software; you can redistribute it and/or modify 18 | # it under the terms of the GNU General Public License as published by 19 | # the Free Software Foundation; either version 2 of the License, or 20 | # (at your option) any later version. 21 | # 22 | # This program is distributed in the hope that it will be useful, 23 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | # GNU General Public License for more details. 26 | # 27 | # You should have received a copy of the GNU General Public License 28 | # along with this program; if not, write to the Free Software 29 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 30 | ############################################################################### 31 | 32 | from ListNode import * 33 | 34 | 35 | class ListException(Exception): 36 | def __init__(self, message): 37 | super(ListException, self).__init__(message) 38 | 39 | 40 | class List: 41 | def __init__(self): 42 | self.front = ListNode() 43 | self.back = ListNode() 44 | self.front.setNext(self.back) 45 | self.front.setPrev(None) 46 | self.back.setPrev(self.front) 47 | self.back.setNext(None) 48 | self.current = self.front 49 | return None 50 | 51 | def goToFront(self): 52 | """Sets the current node to the first node in the list.""" 53 | self.current = self.front.next 54 | return None 55 | 56 | def goToBack(self): 57 | """Sets the current node to the last node in the list.""" 58 | self.current = self.back.prev 59 | return None 60 | 61 | def atEnd(self): 62 | """Predicate: Returns true if the current item is the last item in 63 | the list.""" 64 | return self.current == self.back 65 | 66 | def atFront(self): 67 | """Predicate: Returns true if the current item is the first item in 68 | the list.""" 69 | return self.current == self.front 70 | 71 | def itemsRemaining(self): 72 | """Predicate: Returns true if the current item is not the last 73 | item""" 74 | return not self.atEnd() 75 | 76 | def isEmpty(self): 77 | """Predicate: Returns true if the list contains > 0 items.""" 78 | if self.front.getNext() == self.back: 79 | return 1 80 | else: 81 | return None 82 | 83 | def traverse(self, function): 84 | """Takes a function argument, returns a list which contains the result 85 | of applying the function to each item in the list successivly. Sort 86 | of like map() for this list particularly.""" 87 | n = self.front.getNext() 88 | listresults = [] 89 | while n != self.back: 90 | listresults.append(function(n)) 91 | n = n.getNext() 92 | return listresults 93 | 94 | def insertAfter(self, newnode, afterWhat): 95 | """Inserts newnode after afterWhat. afterWhat may actually be either 96 | a ListNode in the list, or it may be the data inside a particular 97 | ListNode. But it must be a reference to the *same* data, (or node) 98 | not a reference to equivalent data (or node).""" 99 | 100 | n = self.front.getNext() 101 | 102 | if newnode.__class__ != ListNode: 103 | raise ListException("newnode argument must be a ListNode") 104 | 105 | while n != self.back.getPrev(): 106 | if afterWhat == n or afterWhat == n.getData(): 107 | nn = n.getNext() 108 | newnode.setPrev(n) 109 | newnode.setNext(nn) 110 | afterWhat.setNext(newnode) 111 | nn.setPrev(newnode) 112 | return newnode 113 | 114 | n = n.getNext() 115 | 116 | raise ListException("cannot insert after nonexistent node.") 117 | 118 | def removeReference(self, ref): 119 | """Removes ref from the list. Note this must be a reference to 120 | something in the list, not just something that has the same data.""" 121 | 122 | n = self.front.getNext() 123 | 124 | while n != self.back: 125 | # We're going to be very forgiving and let the user pass us a 126 | # reference to either the data contained in the ListNode object, 127 | # or a reference to the ListNode object itself. 128 | if n == ref or n.getData() == ref: 129 | np = n.getPrev() 130 | nn = n.getNext() 131 | n.setNext(None) # Kill the links on the node we delete... 132 | n.setPrev(None) 133 | np.setNext(nn) # Update the links on the surrounding 134 | nn.setPrev(np) # nodes... 135 | return ref # Get out...we only remove the 1st one. 136 | 137 | n = n.getNext() # Next item in the list. 138 | 139 | raise ListException("Reference not found in list") 140 | 141 | def countNodes(self, f=None): 142 | """Returns the number of nodes in the list.""" 143 | if f is None: 144 | f = self.front 145 | nodes = 0 146 | n = f.getNext() 147 | while n != self.back: 148 | nodes = nodes + 1 149 | n = n.getNext() 150 | return nodes 151 | 152 | def prepend(self, node): 153 | """Inserts the given node at the front of the list.""" 154 | self.current = self.front 155 | return self.insert(node, truncate=0) 156 | 157 | def postpend(self, node): 158 | """Inserts the given node at the very back of the list.""" 159 | self.current = self.back.getPrev() 160 | return self.insert(node, 0) 161 | 162 | def insert(self, node, truncate=1): 163 | """Inserts node as the next item in the list. If truncate is true, 164 | then all subsequent elements are dropped, and the new node becomes 165 | the last node in the list.""" 166 | if truncate: 167 | node.setPrev(self.current) 168 | node.setNext(self.back) 169 | self.current.setNext(node) 170 | self.current = node 171 | return self.current 172 | else: 173 | oldnext = self.current.getNext() 174 | node.setPrev(self.current) 175 | node.setNext(oldnext) 176 | self.current.setNext(node) 177 | oldnext.setPrev(node) 178 | self.current = node 179 | return self.current 180 | 181 | def getCurrent(self): 182 | """Returns the current node.""" 183 | return self.current 184 | 185 | def removeCurrent(self): 186 | """Removes the current node. The current node then becomes the next 187 | node in the list.""" 188 | if not self.current: 189 | raise ListException("Error: Cannot delete NONE") 190 | if self.current == self.front: 191 | raise ListException("Cannot delete FRONT") 192 | if self.current == self.back: 193 | raise ListException("Cannot delete BACK") 194 | 195 | one_before_this_one = self.current.getPrev() 196 | one_after_this_one = self.current.getNext() 197 | one_before_this_one.setNext(one_after_this_one) 198 | one_after_this_one.setPrev(one_before_this_one) 199 | self.current.setPrev(None) 200 | self.current.setNext(None) 201 | self.current.setData(None) 202 | self.current = one_after_this_one 203 | return self.current 204 | 205 | def getFirst(self): 206 | """Returns the first item in the list. Does not change the current 207 | node""" 208 | 209 | first = self.front.next 210 | 211 | if first == self.back: 212 | raise ListException("The list is empty") 213 | return first 214 | 215 | def getLast(self): 216 | """Returns the last item in the list. Does not change the current 217 | node""" 218 | last = self.back.prev 219 | 220 | if last == self.front: 221 | raise ListException("The list is empty") 222 | return last 223 | 224 | def getNext(self): 225 | """Returns the next node in the list, and advances the current node.""" 226 | next = self.current.getNext() 227 | 228 | if next == self.back or (next == None and next == self.back): 229 | raise ListException("Already at the end of the list") 230 | elif next == None: 231 | raise ListException("getNext(): Null next field") 232 | 233 | self.current = next 234 | return self.current 235 | 236 | def getPrev(self): 237 | """Returns the previous node in the list, which then becomes the 238 | current node.""" 239 | prev = self.current.getPrev() 240 | 241 | if prev == self.front or (prev == None and prev == self.front): 242 | raise ListException("Already at the beginning of the list") 243 | elif prev == None: 244 | raise ListException("getPrev(): Null prev field.") 245 | 246 | self.current = self.current.getPrev() 247 | return self.current 248 | 249 | # EOF 250 | -------------------------------------------------------------------------------- /ListNode.py: -------------------------------------------------------------------------------- 1 | # ListNode.py 2 | # $Id: ListNode.py,v 1.6 2001/07/11 22:43:09 s2mdalle Exp $ 3 | # Nodes to be placed in List objects. 4 | # Subclass freely! Life is short! 5 | # 6 | # Written by David Allen 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 21 | ############################################################################# 22 | 23 | class ListNode: 24 | def __init__(self, data=None, next=None, prev=None): 25 | self.next = next 26 | self.prev = prev 27 | self.data = data 28 | return None 29 | 30 | def __str__(self): 31 | return self.data.__str__ 32 | 33 | def __repr__(self): 34 | return self.data.__repr__ 35 | 36 | def getNext(self): 37 | """Return the next item in the list""" 38 | return self.next 39 | 40 | def getPrev(self): 41 | """Return the previous item in the list""" 42 | return self.prev 43 | 44 | def getData(self): 45 | """Return the data inside this object Node""" 46 | return self.data 47 | 48 | def setNext(self, newnext): 49 | """Set the next item in the list""" 50 | self.next = newnext 51 | return self.next 52 | 53 | def setPrev(self, newprev): 54 | """Set the previous item in the list""" 55 | self.prev = newprev 56 | return self.prev 57 | 58 | def setData(self, data): 59 | """Set the data inside the object.""" 60 | self.data = data 61 | return self.data 62 | 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Rudimentary Makefile for the FORG 2 | # 3 | # Copyright (C) 2001 David Allen 4 | # Copyright (C) 2020 Tom4hawk 5 | # 6 | # Running ./forg.py hostname is sufficient to run the program, so don't 7 | # use make at all unless you know what this does. :) 8 | ############################################################################### 9 | 10 | srcdir = $(shell pwd) 11 | PYTHON = /usr/bin/python3 12 | RM = /bin/rm -rf 13 | CONFDIR = "$(HOME)/.config/forg" 14 | CACHEDIR= "$(HOME)/.cache/forg" 15 | 16 | all: 17 | $(PYTHON) forg.py gopher.quux.org 18 | 19 | clean-cache: 20 | $(RM) -r $(CACHEDIR)/* 21 | 22 | clean: 23 | @echo Yeeeeeeeeeeeeeehaw\!\!\!\!\! 24 | find . -name \*.pyc -delete 25 | find . -name \*.~ -delete 26 | find . -type d -name \__pycache__ -delete 27 | 28 | dist: 29 | tar zcvf ../forg-latest.tar.gz --exclude='.*' --exclude='__pycache__' --exclude='*.pyc' -C $(srcdir) * 30 | 31 | install: 32 | mkdir -v -p $(CONFDIR) 33 | cp -i $(srcdir)/default_bookmarks.xml $(CONFDIR)/bookmarks 34 | cp -i $(srcdir)/default_options $(CONFDIR)/options 35 | 36 | restore: clean clean-cache 37 | @echo "Done" 38 | -------------------------------------------------------------------------------- /Options.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Eventually this will hold all program options, and maybe be incorporated 5 | # into some sort of options editor. (Hopefully) 6 | # 7 | # For now, it holds information about what the program should and shouldn't 8 | # do. Variable names should be pretty self explanatory. 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 23 | ############################################################################# 24 | import os 25 | import Cache 26 | import Associations 27 | 28 | class Options: 29 | def __init__(self, *args): 30 | # Default values for some important options... 31 | self.ip_cache = {} 32 | self.cache = Cache.Cache() 33 | self.associations = Associations.Associations() 34 | self.setDefaultOpts() 35 | self.greenLight() 36 | 37 | # Accessors/Mutators 38 | 39 | def getCache(self): 40 | return self.cache 41 | 42 | def setCache(self, newcache): 43 | self.cache = newcache 44 | return self.getCache() 45 | 46 | def getAssociations(self): 47 | return self.associations 48 | 49 | def setAssociations(self, newassoc): 50 | self.associations = newassoc 51 | return self.getAssociations() 52 | 53 | def getIP(self, hostname): 54 | """Return the cached IP of hostname. May throw KeyError""" 55 | return self.ip_cache[hostname] 56 | 57 | def setIP(self, hostname, IP): 58 | self.ip_cache[hostname] = IP 59 | return self.getIP(hostname) 60 | 61 | def save(self, alternate_filename=None): 62 | """Saves all options to the prefs directory/forgrc unless an 63 | alternate filename is specified. Throws all IOErrors out""" 64 | if not alternate_filename: 65 | prefs = self.getOption('prefs_directory') 66 | filename = "%s%s%s" % (prefs, os.sep, "forgrc") 67 | else: 68 | filename = alternate_filename 69 | 70 | fp = open(filename, "w") 71 | fp.write(self.toString()) 72 | fp.flush() 73 | fp.close() 74 | return 1 75 | 76 | def toggle(self, key): 77 | """Flip the value of the option specified by key. If it was true, 78 | it becomes false, and if it was false, it becomes true.""" 79 | try: 80 | val = self.opts[key] 81 | 82 | if val: 83 | print("Toggle(%s): FALSE" % key) 84 | self.opts[key] = None 85 | else: 86 | print("Toggle(%s): TRUE" % key) 87 | self.opts[key] = 1 88 | except: 89 | print("Toggle(%s): TRUE" % key) 90 | self.opts[key] = 1 91 | return self.opts[key] 92 | 93 | def greenLight(self): 94 | """Set the green light. This is used for thread synchronization""" 95 | self.GREEN_LIGHT = 1 96 | return self.GREEN_LIGHT 97 | 98 | def redLight(self): 99 | """Turn off the green light. For thread synchronization.""" 100 | self.GREEN_LIGHT = None 101 | return self.GREEN_LIGHT 102 | 103 | def __get_xdg_path(self, variable: str) -> str: 104 | """ Returns path to XDG_CONFIG_HOME, XDG_CACHE_HOME and XDG_DATA_HOME according to XDG Base spec""" 105 | xdg_vars = { 106 | "XDG_CONFIG_HOME": ".config", 107 | "XDG_CACHE_HOME": ".cache", 108 | "XDG_DATA_HOME": ".local" + os.sep + "share" 109 | } 110 | 111 | if not xdg_vars[variable]: 112 | raise ValueError 113 | 114 | xdg_path = os.environ.get(variable) 115 | 116 | if not xdg_path: 117 | xdg_path = os.path.expandvars("$HOME") + os.sep + xdg_vars[variable] 118 | 119 | return xdg_path 120 | 121 | def setDefaultOpts(self): 122 | """Sets default set of options so that the structure is not empty.""" 123 | self.opts = {} 124 | 125 | if os.sep == '\\': 126 | # Ugly hack for windows. Some versions of windows 127 | # yield c:\ for the home directory, and some return something 128 | # really screwed up like \ or c\ even. 129 | self.opts['prefs_directory'] = "C:\\FORG-DATA" + os.sep + "Config" 130 | self.opts['cache_directory'] = "C:\\FORG-DATA" + os.sep + "Cache" 131 | else: 132 | self.opts['prefs_directory'] = self.__get_xdg_path("XDG_CONFIG_HOME") + os.sep + "forg" 133 | self.opts['cache_directory'] = self.__get_xdg_path("XDG_CACHE_HOME") + os.sep + "forg" 134 | 135 | os.makedirs(self.opts['prefs_directory'], exist_ok=True) 136 | os.makedirs(self.opts['cache_directory'], exist_ok=True) 137 | 138 | self.opts['home'] = "gopher://gopher.floodgap.com:70/1/" 139 | 140 | self.opts['use_cache'] = 1 141 | self.opts['delete_cache_on_exit'] = None 142 | self.opts['use_url_format'] = 1 143 | self.opts['grab_resource_info'] = None 144 | self.opts['show_host_port'] = None 145 | self.opts['save_options_on_exit'] = 1 146 | self.opts['strip_carraige_returns'] = 1 147 | self.opts['show_cached'] = None 148 | self.opts['display_info_in_directories'] = None 149 | self.opts['use_PIL'] = 1 # Use PIL for images 150 | 151 | self.opts['cache_prefix'] = "%s%s" % (self.opts['cache_directory'], os.sep) 152 | 153 | def makeToggleWrapper(self, keyname): 154 | """Returns a function which when called with no arguments toggles the 155 | value of keyname within the options structure. This is used for menu 156 | callbacks connected to check buttons.""" 157 | 158 | def toggle_wrapper(opts=self, key=keyname): 159 | return opts.toggle(key) 160 | 161 | return toggle_wrapper 162 | 163 | def setOption(self, optionname, optionvalue): 164 | """Set optionname to optionvalue""" 165 | self.opts[optionname] = optionvalue 166 | return self.getOption(optionname) 167 | 168 | def getOption(self, optionname): 169 | """Get an option named optionname.""" 170 | try: 171 | optionname = optionname.lower() 172 | return self.opts[optionname] 173 | except KeyError: 174 | return None 175 | 176 | def toString(self): 177 | """Returns string representation of the object.""" 178 | return self.__str__() 179 | 180 | def __repr__(self): 181 | return self.__str__() 182 | 183 | def __str__(self): 184 | # God I love the map() function. 185 | lines = list(map(lambda x, self=self: "%s = %s" % (x, self.opts[x]), list(self.opts.keys()))) 186 | comments = "%s%s%s" % ("# Options for the FORG\n", 187 | "# Please don't edit me unless you know what\n", 188 | "# you're doing.\n") 189 | return comments + "\n".join(lines) + "\n" 190 | 191 | def parseFile(self, filename): 192 | """Parse filename into a set of options. Caller is responsible 193 | for catching IOError related to reading files.""" 194 | print("Previously had %d keys" % len(list(self.opts.keys()))) 195 | self.setDefaultOpts() 196 | fp = open(filename, "r") 197 | 198 | line = fp.readline() 199 | line_num = 0 200 | while line != '': 201 | line_num = line_num + 1 202 | commentIndex = line.find("#") 203 | if commentIndex != -1: 204 | line = line[0:commentIndex] 205 | 206 | line = line.strip() 207 | 208 | if line == '': # Nothing to grokk 209 | line = fp.readline() # Get next line... 210 | continue 211 | 212 | items = line.split("=") 213 | if len(items) < 2: 214 | print("Options::parseFile: no '=' on line number %d" % line_num) 215 | line = fp.readline() # Get next line... 216 | continue 217 | if len(items) > 2: 218 | print(("Options::parseFile: too many '=' on line number %d" % 219 | line_num)) 220 | line = fp.readline() # Get next line... 221 | continue 222 | 223 | key = items[0].strip().lower() # Normalize and lowercase 224 | val = items[1].strip().lower() 225 | 226 | # Figure out what the hell val should be 227 | if val == 'no' or val == 'none' or val == 0 or val == '0': 228 | val = None 229 | 230 | self.opts[key] = val 231 | line = fp.readline() 232 | 233 | return self 234 | 235 | # Here is one instance of an options structure. 236 | # This is used by TkGui and other modules as a centralized place 237 | # to store program options. 238 | program_options = Options() 239 | 240 | -------------------------------------------------------------------------------- /Question.py: -------------------------------------------------------------------------------- 1 | # Question.py 2 | # Written by David Allen 3 | # Released under the terms of the GNU General Public License 4 | # $Id: Question.py,v 1.6 2001/07/11 22:43:09 s2mdalle Exp $ 5 | # Represents one question inside of a multi-question ASK block 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ############################################################################ 21 | 22 | from gopher import * 23 | 24 | QuestionException = "Error dealing with Question" 25 | 26 | class Question: 27 | RESPONSE_ONE_LINE = 1 28 | RESPONSE_PASSWORD = 2 29 | RESPONSE_MULTI_LINE = 3 30 | RESPONSE_FILENAME = 4 31 | RESPONSE_CHOICES = 5 32 | verbose = None 33 | 34 | def __init__(self, data=""): 35 | self.qtype = None 36 | self.promptString = "%s%s" % ("Answer the nice man's question,\n", 37 | "and nobody gets hurt.") 38 | self.default = "" 39 | self.setData(data) 40 | return None 41 | 42 | def getDefault(self): 43 | return self.default 44 | 45 | def setDefault(self, newdefault): 46 | self.default = newdefault 47 | return self.default 48 | 49 | def getPromptString(self): 50 | return self.promptString 51 | 52 | def getType(self): 53 | if self.verbose: 54 | print("QTYPE is ", self.qtype) 55 | return self.qtype 56 | 57 | def setData(self, data): 58 | """Given data on a question in ASK format, this parses the data 59 | and sets the internal data of the object correctly. This should be 60 | done pretty much first after creating the object.""" 61 | 62 | self.linedata = data[:] # Copy 63 | 64 | ind = data.find(":") 65 | if ind == -1: 66 | raise QuestionException("Cannot find \":\" on line") 67 | qtype = data[0:ind].strip() 68 | data = data[ind+1:] 69 | 70 | try: 71 | self.qtype = questions[qtype] 72 | except KeyError: 73 | raise QuestionException 74 | 75 | # Do the rest here... 76 | if (self.qtype == QUESTION_ASK 77 | or self.qtype == QUESTION_ASKL 78 | or self.qtype == QUESTION_ASKP): 79 | if data.find("\t") != -1: 80 | try: 81 | [promptStr, default_val] = data.strip("\t") 82 | except: 83 | raise QuestionException("Too many tabs in line") 84 | self.promptString = promptStr.strip() 85 | self.default = default_val 86 | if self.verbose: 87 | print("Block has default of ", self.default) 88 | else: 89 | self.promptString = data.strip() 90 | elif self.qtype == QUESTION_ASKP: 91 | pass 92 | elif (self.qtype == QUESTION_ASKL 93 | or self.qtype == QUESTION_ASKF 94 | or self.qtype == QUESTION_CHOOSEF 95 | or self.qtype == QUESTION_NOTE): 96 | self.promptString = data.strip() 97 | elif self.qtype == QUESTION_CHOOSE or self.qtype == QUESTION_SELECT: 98 | try: 99 | ind = data.find("\t") 100 | prompt = data[0:ind] 101 | opts = data[ind+1:].strip("\t") 102 | except : 103 | raise QuestionException("Too many tabs in line") 104 | 105 | self.promptString = prompt.strip() 106 | self.options = opts 107 | self.default = self.options[0] 108 | else: 109 | raise QuestionException("Unknown QType on parse") 110 | 111 | if self.verbose: 112 | print("Successfully parsed data line: ", self.linedata) 113 | 114 | return None 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Forg 2 | === 3 | The FORG is a __Linux__ graphical client for [Gopher](https://en.wikipedia.org/wiki/Gopher_\(protocol\)) written in Python. It will let you browse the world-wide gopherspace and handles various types of media, including HTML and video. 4 | 5 | Table of contents 6 | ================= 7 | * [Features](#features) 8 | * [Requirements](#requirements) 9 | * [Python & Libraries](#python--libraries) 10 | * [Windows/MacOSX support](#windowsmacosx-support) 11 | * [How to use](#how-to-use) 12 | * [Basics](#basics) 13 | * [Associate extensions](#associate-extensions) 14 | * [Bookmarks](#bookmarks) 15 | * [Original author](#original-author) 16 | * [Why fork](#why-fork) 17 | * [To do](#to-do) 18 | * [License](#license) 19 | 20 | 21 | 22 | Features 23 | ======== 24 | - Ability to load other programs with different file formats. I.e. this program does not interpret HTML, so you may want to associate .html files with [Firefox](https://firefox.com/), so it will be launched upon saving a .html file 25 | - Full caching of directories and files 26 | - Searching support. (Tested against gopher.floodgap.com's Veronica, but of course) 27 | - Bookmarking support. Bookmarks are written in XBEL and support arbitrary folders and subfolders 28 | - Bookmark editing, similar to Firefox 29 | - Directories are searchable by name 30 | - Statistics on size of cache, number of files in the cache, and number of documents in the queue 31 | - Ability to save files and directories. (Note: when directories are saved, the protocol information that the server sent is what is saved to disk...for now) 32 | - ASK menus - users can interact with programs on foreign servers through the use of miniature questionnaires 33 | - Right click context menus on all items providing all available information on a resource. (If the server supports Gopher+) 34 | - Between 0 and 100% complete implementation of Gopher *AND* Gopher+!!! :) 35 | - Managerspeak. This program leverages automatic internet data retrieval and storaging technologies for quasi-reliable interlocked handshake protocol interaction, allowing open-ended failsafe solutions to be developed in the realm of...oh whatever 36 | 37 | Requirements 38 | ============ 39 | Python & Libraries 40 | ------------------ 41 | You will need 4 things. Python 3.4 or newer, Tkinter (should be bundled with Python), Pmw and Pillow. There is a good chance that your distribution has all of them in official repository. You can use _pip_ if you cannot/don't want to use official repository. 42 | 43 | Windows/MacOSX support 44 | ---------------------- 45 | Theoretically, this program should run under these systems. I've never tested this theory and I'm not going to. 46 | 47 | How to use 48 | ========== 49 | Basics 50 | ------ 51 | To run the program, run: 52 | 53 | ./forg.py host 54 | as an example, host can be "___gopher.floodgap.com___", "___gopher.meulie.net___" or "___gopher.quux.org___", which are all valid gopher sites. 55 | Alternatively, you can use URL syntax, as in: 56 | 57 | ./forg gopher://gopher.quux.org 58 | 59 | Associate extensions 60 | -------------------- 61 | How to Associate extensions 62 | 63 | Bookmarks 64 | --------- 65 | How to bookmarks 66 | 67 | Original author 68 | =============== 69 | _David Allen_ 70 | __http://opop.nols.com/__ 71 | The upstream home page mentioned in original README is unavailable but if you are curious there is a copy of the site on [archve.org](http://web.archive.org/web/20030416195623/http://opop.nols.com/forg.shtml) 72 | 73 | Why fork 74 | ======== 75 | Original program is no longer maintained (latest release is from 2001) and doesn't work well with newer Pythons (quote from original readme: "The version of python required is 1.5.2, (...) Python version 2.0 is strongly suggested."). This fork aims to address those two problems. 76 | 77 | To do 78 | ===== 79 | - [x] Port to newest Python 2.x 80 | - [x] Port to Python 3.x 81 | - [x] Remove dependency on _xmllib_ 82 | - [ ] Change name 83 | - [ ] Stop using [PMW](http://pmw.sourceforge.net/) (library doesn't look alive) 84 | - [x] Scrolling directories with mouse wheel 85 | - [ ] Start using _[flake8](http://flake8.pycqa.org/)_ 86 | - [ ] Add Gemini protocol support 87 | - [ ] Gtk Gui 88 | - [ ] Ncurses Gui 89 | - [ ] QT Gui 90 | - [ ] Mobile version for Phosh 91 | - [ ] Mobile version for UBPorts 92 | - [ ] Mobile version for Plasma Mobile 93 | - [ ] AppImage package 94 | - [x] Support for XDG Base Directory Specification 95 | - [ ] Clean directory structure 96 | 97 | License 98 | ======= 99 | [GNU General Public License, version 2](https://www.gnu.org/licenses/gpl-2.0.html) - copy of the licence is in _COPYING_ file 100 | -------------------------------------------------------------------------------- /ResourceInformation.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Released under the terms of the GNU General Public License 5 | # 6 | # When dealing with a Gopher+ server, information about a document can be 7 | # fetched by sending the request: 8 | # some_locator\t!\r\n 9 | # 10 | # This module handles the parsing and storing of that information. 11 | # 12 | # This program is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by 14 | # the Free Software Foundation; either version 2 of the License, or 15 | # (at your option) any later version. 16 | # 17 | # This program is distributed in the hope that it will be useful, 18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | # GNU General Public License for more details. 21 | # 22 | # You should have received a copy of the GNU General Public License 23 | # along with this program; if not, write to the Free Software 24 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 25 | ############################################################################ 26 | 27 | from tkinter import * 28 | import Pmw 29 | import os 30 | import re 31 | import ContentFrame 32 | from gopher import * 33 | import GopherResource 34 | import GopherResponse 35 | 36 | class ResourceInformation: 37 | verbose = None 38 | def __init__(self, data=None): 39 | self.blockdict = {} 40 | 41 | self.data = data 42 | 43 | if self.data != None: 44 | self.setData(self.data) 45 | 46 | return None 47 | 48 | def __str__(self): 49 | return self.toString() 50 | 51 | def toString(self): 52 | def fn(key, obj=self): 53 | return "%s:\n%s\n" % (key.upper(), obj.getBlock(key)) 54 | 55 | return "".join(list(map(fn, self.getBlockNames()))) + "\n" 56 | 57 | def setData(self, data): 58 | self.data = data 59 | self.data = re.sub("\r\n", "\n", self.data) 60 | 61 | lastindex = -1 62 | blocks = [] 63 | 64 | try: 65 | while 1: 66 | # This will throw ValueError if not found. 67 | newindex = self.data.index("\n+", (lastindex + 1), len(self.data)) 68 | blocks.append(self.data[lastindex+1:newindex]) 69 | lastindex = newindex 70 | except ValueError: # When no more "\n+" are found. 71 | # The remaining block is what's left... 72 | blocks.append(self.data[lastindex+1:len(self.data)]) 73 | 74 | # What we're going to do is hash each data block in by its block 75 | # 'title'. This way, when we're done hashing all of the data, we 76 | # can just go through and pick out what we want. So this type of 77 | # data: 78 | # +ADMIN: 79 | # Grendel The Evil 80 | # Some more admin information 81 | # Gets hashed in like this essentially: 82 | # hash['admin'] = "Grendel The Evil \n..." 83 | self.blockdict = {} 84 | 85 | # We now have a list of blocks. 86 | for block in blocks: 87 | lines = block.split("\n") 88 | blocklabel = lines[0] 89 | 90 | front = blocklabel.find("+") # This defines the start of a block 91 | back = blocklabel.find(":") # This may not be present 92 | 93 | if front != -1: 94 | if back == -1: 95 | back = len(blocklabel) # End of string if not present 96 | 97 | # Get the name, which is like this: "+ADMIN:" => 'ADMIN' 98 | blockname = blocklabel[front+1:back] 99 | key = blockname.lower() # Lowercase so it hashes nicely. :) 100 | 101 | # strip the leading space. This is because in gopher+ 102 | # when it responds to info queries, each response line that 103 | # isn't a block header is indented by one space. 104 | data = re.sub("\n ", "\n", lines[1:].join("\n")) 105 | 106 | # Get the first space in the data. 107 | if self.verbose: 108 | print("Data is %s" % data) 109 | 110 | # Watch out for the case when data is ''. This could be in 111 | # particular if the server sends us a size packet like this: 112 | # +-1\r\n 113 | # Which would have a '' data segment. 114 | if data != '' and data[0] == ' ': 115 | data = data[1:] 116 | 117 | # Assign it into the hash. 118 | if self.verbose: 119 | print("Assigned data to key %s" % key) 120 | 121 | if data != '' and not data is None: 122 | # No sense in assigning nothing into a key. The getBlock() 123 | # handles when there is no data and returns '' 124 | self.blockdict[key] = data 125 | else: 126 | print("BLOCK ERROR: cannot find blockname in %s" % blocklabel) 127 | 128 | if self.verbose: 129 | k = list(self.blockdict.keys()) 130 | print("Available block titles are:\n%s" % k.join("\n")) 131 | 132 | print("Keys are ", list(self.blockdict.keys())) 133 | return self 134 | 135 | # Simple accessors/mutators. 136 | # Sure, I could just monkey around with the data in an object from outside, 137 | # but in some cultures, people are executed for such offenses against the 138 | # OOP paradigm. :) 139 | def setBlock(self, blockname, blockval): 140 | self.blockdict[blockname.lower()] = blockval 141 | return self.getBlock(blockname.lower()) 142 | def setInfo(self, newinfo): 143 | self.blockdict['info'] = newinfo 144 | return self.getInfo() 145 | def setAdmin(self, newadmin): 146 | self.blockdict['admin'] = newadmin 147 | return self.getAdmin() 148 | def setViews(self, newviews): 149 | self.blockdict['views'] = newviews 150 | return self.getViews() 151 | def setAbstract(self, newabstract): 152 | self.blockdict['abstract'] = newabstract 153 | return self.getAbstract() 154 | def getAbstract(self): 155 | return self.blockdict['abstract'] 156 | def getViews(self): 157 | return self.blockdict['views'] 158 | def getInfo(self): 159 | return self.blockdict['info'] 160 | def getAdmin(self): 161 | return self.blockdict['admin'] 162 | def getBlockNames(self): 163 | return list(self.blockdict.keys()) 164 | def getBlock(self, blockname): 165 | try: 166 | return self.blockdict[blockname.lower()] 167 | except KeyError: 168 | return '' 169 | 170 | class GUIResourceInformation(Pmw.TextDialog): 171 | def __init__(self, resource_info_object): 172 | Pmw.TextDialog.__init__(self, title="Resource Information") 173 | self.insert('end', resource_info_object.toString()) 174 | return None 175 | 176 | 177 | -------------------------------------------------------------------------------- /State.py: -------------------------------------------------------------------------------- 1 | # State.py 2 | # $Id: State.py,v 1.2 2001/07/11 22:43:09 s2mdalle Exp $ 3 | # Written by David Allen 4 | # 5 | # Saves state information about a particular GopherResource. 6 | # This is a bit overkill, but it was easier than putting the information 7 | # in a tuple and then having to worry about which index gets to which item 8 | # in the tuple. Easier just to call methods like getResource() :) 9 | # 10 | # This is just a souped up structure. No calculation is done, just accessors 11 | # and mutators are provided to bundle several different data items together 12 | # under one object. 13 | # 14 | # This program is free software; you can redistribute it and/or modify 15 | # it under the terms of the GNU General Public License as published by 16 | # the Free Software Foundation; either version 2 of the License, or 17 | # (at your option) any later version. 18 | # 19 | # This program is distributed in the hope that it will be useful, 20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | # GNU General Public License for more details. 23 | # 24 | # You should have received a copy of the GNU General Public License 25 | # along with this program; if not, write to the Free Software 26 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 27 | ############################################################################ 28 | class State: 29 | verbose = None 30 | 31 | def __init__(self, response, resource, widget): 32 | self.response = response 33 | self.resource = resource 34 | self.widget = widget 35 | return None 36 | 37 | def __str__(self): 38 | return "%s" % self.resource.toURL() 39 | 40 | def __repr__(self): 41 | return self.__str__() 42 | 43 | # Accessors 44 | 45 | def getResponse(self): 46 | return self.response 47 | 48 | def getResource(self): 49 | return self.resource 50 | 51 | def getWidget(self): 52 | return self.widget 53 | 54 | # Mutators 55 | 56 | def setResponse(self, resp): 57 | self.response = resp 58 | return self.getResponse() 59 | 60 | def setResource(self, res): 61 | self.resource = res 62 | return self.getResource() 63 | 64 | def setWidget(self, widget): 65 | self.widget = widget 66 | return self.getWidget() 67 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Optionally make use of PIL if it is installed (Python Imaging 2 | Library). I don't know how well this is going to work across 3 | platforms though. This could be used to display jpeg/gif/png when 4 | downloaded rather than using a launching program. 5 | - Maybe work in an alternative to GUIDirectory for listing things in 6 | an icon-list fashion. 7 | - Add Options editor. (See Options.py -- there are a number of 8 | configurable options, but no way to do that yet) 9 | - Improve associations editor. 10 | - Make it irrelevant whether users enter URLs or hostnames for 11 | starting points. (This will probably disallow interpretation as a 12 | local flie) 13 | - Support bookmark subfolders 14 | - Write bookmark handling widget (add/delete/rename/folders, etc) 15 | - Do more with information from server on different resources. 16 | (i.e. more than just block separation and presenting it to the user 17 | in a dialog) 18 | -------------------------------------------------------------------------------- /default_bookmarks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | Bookmarks 11 | 12 | Essential Links 13 | 14 | gopher.floodgap.com root 15 | 16 | 17 | gopher.quux.org root 18 | 19 | 20 | Development Projects 21 | 22 | 23 | Search gopherspace with Veronica 2 24 | 25 | 26 | The Heatdeath Organization 27 | 28 | 29 | Whole Earth 'Lectronic Links 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /default_options: -------------------------------------------------------------------------------- 1 | # This is a FORG configuration file. 2 | # From here, you can configure which fonts and colors the FORG uses. 3 | # 4 | # I strongly recommend a fixed-width font, particularly since on many 5 | # gopher sites, you'll find ASCII art that doesn't display well with 6 | # variable width fonts. 7 | # 8 | # In order to make this options file "active", copy it to $HOME/.forg/options 9 | # and that's all there is to it. 10 | # 11 | ############################################################################## 12 | *font: -adobe-courier-*-r-*-*-12-*-*-*-*-*-*-* 13 | *Label*font: -adobe-courier-*-r-*-*-12-*-*-*-*-*-*-* 14 | *background: Gray80 15 | *Entry*background: white 16 | -------------------------------------------------------------------------------- /gopher.py: -------------------------------------------------------------------------------- 1 | # gopher.py 2 | # $Id: gopher.py,v 1.7 2001/07/11 22:43:09 s2mdalle Exp $ 3 | # Gopher protocol definitions. 4 | # Written by David Allen 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 | ######################################################################## 20 | RESPONSE_FILE = '0' # Item is a file 21 | RESPONSE_DIR = '1' # Item is a directory 22 | RESPONSE_CSO = '2' # Item is a CSO phone-book server 23 | RESPONSE_ERR = '3' # Error 24 | RESPONSE_BINHEX = '4' # Item is a BinHexed Macintosh file. 25 | RESPONSE_DOSBIN = '5' # Item is DOS binary archive of some sort. 26 | RESPONSE_UUE = '6' # Item is a UNIX uuencoded file. 27 | RESPONSE_INDEXS = '7' # Item is an Index-Search server. 28 | RESPONSE_TELNET = '8' # Item points to a text-based telnet session. 29 | RESPONSE_BINFILE = '9' # Item is a binary file! 30 | RESPONSE_REDSERV = '+' # Item is a redundant server 31 | RESPONSE_TN3270 = 'T' # Item points to a text-based tn3270 session. 32 | RESPONSE_GIF = 'g' # Item is a GIF format graphics file. 33 | RESPONSE_IMAGE = 'I' # Item is some kind of image file. 34 | RESPONSE_UNKNOWN = '?' # Unknown. WTF? 35 | RESPONSE_BITMAP = ':' # Gopher+ Bitmap response 36 | RESPONSE_MOVIE = ";" # Gopher+ Movie response 37 | RESPONSE_SOUND = "<" # Gopher+ Sound response 38 | 39 | # The following are types not found in the RFC definition of gopher but that 40 | # I've encountered on the net, so I added 41 | RESPONSE_BLURB = 'i' 42 | RESPONSE_HTML = 'h' 43 | 44 | # Gopher+ errors 45 | ERROR_NA = '1' # Item is not available. 46 | ERROR_TA = '2' # Try again later (eg. My load is too high right now.) 47 | ERROR_MOVED = '3' # Item has moved. 48 | 49 | responses = { "%s" % RESPONSE_FILE : "File:", 50 | "%s" % RESPONSE_DIR : "Directory:", 51 | "%s" % RESPONSE_CSO : "CSO phone-book server:", 52 | "%s" % RESPONSE_ERR : "Error:", 53 | "%s" % RESPONSE_BINHEX : "BinHexed Macintosh file:", 54 | "%s" % RESPONSE_DOSBIN : "DOS binary archive:", 55 | "%s" % RESPONSE_UUE : "UNIX UUEncoded file:", 56 | "%s" % RESPONSE_INDEXS : "Index-Search server:", 57 | "%s" % RESPONSE_TELNET : "Telnet session:", 58 | "%s" % RESPONSE_BINFILE : "Binary file:", 59 | "%s" % RESPONSE_REDSERV : "Redundant server:", 60 | "%s" % RESPONSE_TN3270 : "tn3270 session:", 61 | "%s" % RESPONSE_GIF : "GIF file:", 62 | "%s" % RESPONSE_IMAGE : "Image file:", 63 | "%s" % RESPONSE_BLURB : " ", 64 | "%s" % RESPONSE_HTML : "HTML file:", 65 | "%s" % RESPONSE_BITMAP : "Bitmap Image:", 66 | "%s" % RESPONSE_MOVIE : "Movie:", 67 | "%s" % RESPONSE_SOUND : "Sound:", 68 | "%s" % RESPONSE_UNKNOWN : "Unknown:" } 69 | errors = { "%s" % ERROR_NA : "Error: Item is not available.", 70 | "%s" % ERROR_TA : "Error: Try again (server busy)", 71 | "%s" % ERROR_MOVED : "Error: This resource has moved." } 72 | 73 | # There is nothing special about these numbers, just make sure they're 74 | # all unique. 75 | QUESTION_ASK = 20 76 | QUESTION_ASKP = 21 77 | QUESTION_ASKL = 22 78 | QUESTION_ASKF = 23 79 | QUESTION_SELECT = 24 80 | QUESTION_CHOOSE = 25 81 | QUESTION_CHOOSEF = 26 82 | QUESTION_NOTE = 27 83 | 84 | # Mapping from Gopher+ types to internal values. 85 | questions = { "Ask" : QUESTION_ASK, 86 | "AskP" : QUESTION_ASKP, 87 | "AskL" : QUESTION_ASKL, 88 | "AskF" : QUESTION_ASKF, 89 | "Select" : QUESTION_SELECT, 90 | "Choose" : QUESTION_CHOOSE, 91 | "ChooseF" : QUESTION_CHOOSEF, 92 | "Note" : QUESTION_NOTE } 93 | 94 | questions_types = { "%s" % QUESTION_ASK : "Ask", 95 | "%s" % QUESTION_ASKP : "AskP", 96 | "%s" % QUESTION_ASKL : "AskL", 97 | "%s" % QUESTION_ASKF : "AskF", 98 | "%s" % QUESTION_SELECT : "Select", 99 | "%s" % QUESTION_CHOOSE : "Choose", 100 | "%s" % QUESTION_CHOOSEF : "ChooseF" } 101 | 102 | # Colors - not related to gopher protocol functioning, but useful. 103 | RED = "#FF0000" 104 | GREEN = "#00FF00" 105 | BLUE = "#0000FF" 106 | WHITE = "#FFFFFF" 107 | BLACK = "#000000" 108 | -------------------------------------------------------------------------------- /mini-forg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Written by David Allen 3 | # This is a proof of concept on how to embed the application. Just using this 4 | # code embeds the entire application minus a few niceties (like bookmarks, etc) 5 | # into a popup window. 6 | # 7 | # This was just meant to display that the operation of the program is 8 | # segregated from the operation of the main GUI, and how to embed the FORG 9 | # in python/Tkinter programs. 10 | # 11 | # This file is released under the terms of the GNU General Public License. 12 | ############################################################################## 13 | 14 | from Tkinter import * 15 | import GopherResource 16 | import forg 17 | 18 | x = Tk() 19 | r = GopherResource.GopherResource() 20 | r.setURL("gopher://gopher.quux.org") 21 | r.setName("QUUX.org") 22 | 23 | # Create a FORG object. You only have to tell it what your parent 24 | # window is, and what resource it should load when it starts. 25 | f = forg.FORG(parent_widget=x, resource=r) 26 | 27 | # Pack it in 28 | f.pack(fill='both', expand=1) 29 | 30 | # Start the mini application. 31 | x.mainloop() 32 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | What you'll find here is several miscellaneous python scripts used to 2 | test different components of the system. When I first was writing 3 | things like List.py and the various socket routines, I needed tiny 4 | modules to test them and this is what I used. 5 | 6 | They aren't really of any value to the main program. -------------------------------------------------------------------------------- /test/bmarks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bookmarks 6 | 7 | Mothra Madness 8 | 9 | 10 | Essential Resources 11 | 12 | Floodgap Home 13 | 14 | 15 | Heatdeath Organization 16 | 17 | 18 | Quux.org 19 | 20 | 21 | UMN Home 22 | 23 | 24 | Veronica-2 Search 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/bmetest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Bookmarks import BookmarkEditor 3 | 4 | x = BookmarkEditor.BookmarkEditor('foobar') 5 | x.mainloop() 6 | -------------------------------------------------------------------------------- /test/bmtest.py: -------------------------------------------------------------------------------- 1 | 2 | from Tkinter import * 3 | 4 | from Bookmarks import Bookmark 5 | 6 | f = Bookmark.BookmarkFactory() 7 | f.verbose = 1 8 | f.parseResource(open("bmarks.xml")) 9 | 10 | win = Tk() 11 | m = Menu() 12 | 13 | mymenu = f.getMenu() 14 | 15 | def fn(item): 16 | print "You're going to \"%s\"-vill via way of %s" % (item.getName(), 17 | item.getURL()) 18 | return None 19 | 20 | m.add_cascade(label=mymenu.getName(), 21 | menu=mymenu.getTkMenu(m, fn)) 22 | 23 | print "Added cascade: %s" % mymenu.getName() 24 | win.config(menu=m) 25 | win.mainloop() 26 | 27 | f.writeXML(open("newbm.xml", "w"), f.getMenu()) 28 | -------------------------------------------------------------------------------- /test/ltest.py: -------------------------------------------------------------------------------- 1 | # Quick module to test the List and ListNode classes. 2 | 3 | from List import * 4 | from ListNode import * 5 | 6 | def p(x): 7 | print x.getData() 8 | 9 | def makenode(y): 10 | return ListNode("The number is: %d" % y) 11 | 12 | nodes = map(makenode, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) 13 | 14 | lst = List() 15 | for node in nodes: 16 | lst.insert(node) 17 | 18 | try: 19 | lst.getNext() 20 | lst.getNext() 21 | lst.getNext() 22 | lst.getNext() 23 | lst.getNext() 24 | except: 25 | pass 26 | 27 | try: 28 | lst.getPrev() 29 | lst.getPrev() 30 | lst.getPrev() 31 | except: 32 | pass 33 | 34 | 35 | lst.insert(ListNode("foo"), None) 36 | lst.insert(ListNode("bar"), None) 37 | lst.insert(ListNode("Baz"), None) 38 | 39 | lst.traverse(p) # Prints all numbers to 15 40 | 41 | try: 42 | p(lst.getNext()) 43 | except: 44 | print "Can't get next item." 45 | 46 | print "===================" 47 | 48 | p(lst.getPrev()) 49 | p(lst.getPrev()) 50 | p(lst.getPrev()) 51 | p(lst.getPrev()) 52 | p(lst.getPrev()) 53 | p(lst.getPrev()) 54 | p(lst.getPrev()) 55 | p(lst.getPrev()) 56 | p(lst.getPrev()) 57 | p(lst.getPrev()) 58 | p(lst.getPrev()) 59 | p(lst.getPrev()) 60 | -------------------------------------------------------------------------------- /test/newbm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bookmarks 8 | 9 | Mothra Madness 10 | 11 | 12 | Essential Resources 13 | 14 | Floodgap Home 15 | 16 | 17 | Heatdeath Organization 18 | 19 | 20 | Quux.org 21 | 22 | 23 | UMN Home 24 | 25 | 26 | Veronica-2 Search 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/things.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/tri.py: -------------------------------------------------------------------------------- 1 | from GopherConnection import * 2 | from GopherResource import * 3 | from ResourceInformation import * 4 | 5 | res = GopherResource() 6 | res.setHost("mothra") 7 | res.setPort(70) 8 | res.setLocator("1/Source code") 9 | res.setName("Source code") 10 | 11 | conn = GopherConnection() 12 | resinfo = conn.getInfo(res) 13 | 14 | print resinfo.toString() 15 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 David Allen 2 | # Copyright (C) 2020 Tom4hawk 3 | # 4 | # Random functions used in many places that don't belong together in an 5 | # object. 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | ############################################################################# 21 | import os 22 | import stat 23 | 24 | def summarize_directory(dirname): 25 | """Returns an array [x, y, z] where x is the number of files in all subdirs 26 | of dirname and y is the total summed size of all of the files and z is 27 | the total number of directories.""" 28 | 29 | filecount = 0 30 | summedSize = 0 31 | dircount = 0 32 | 33 | files = os.listdir(dirname) 34 | for file in files: 35 | if file == '.' or file == '..': 36 | continue 37 | 38 | path = os.path.join(dirname, file) 39 | if os.path.isdir(path): 40 | dircount = dircount + 1 # Discovered a new directory 41 | [tfc, tfs, tdc] = summarize_directory(path) 42 | # Add whatever came back from the recursive call. 43 | filecount = filecount + tfc 44 | summedSize = summedSize + tfs 45 | dircount = dircount + tdc 46 | else: 47 | filecount = filecount + 1 48 | statinfo = os.stat(path) 49 | summedSize = summedSize + statinfo[stat.ST_SIZE] 50 | 51 | return [filecount, summedSize, dircount] 52 | 53 | def file_exists(filename): 54 | try: 55 | os.stat(filename) 56 | except OSError: 57 | return None 58 | 59 | return 1 60 | 61 | def recursive_delete(dirname): 62 | """Runs the equivalent of an rm -rf on a given directory. Does not make 63 | any distinction between symlinks, etc. so use with extreme care. 64 | Thanks to comp.lang.python for tips on this one...""" 65 | files = os.listdir(dirname) 66 | for file in files: 67 | if file == '.' or file == '..': 68 | # The calls used to find filenames shouldn't ever return this, 69 | # but just in case, we check since this would be horrendously 70 | # bad. 71 | continue 72 | 73 | path = os.path.join(dirname, file) 74 | if os.path.isdir(path): 75 | recursive_delete(path) 76 | else: 77 | print('Removing file: "%s"' % path) 78 | retval = os.unlink(path) 79 | 80 | print('Removing directory:', dirname) 81 | os.rmdir(dirname) 82 | return 1 83 | 84 | def character_replace(str, findchar, replacechar): 85 | 86 | if findchar is replacechar or findchar == replacechar: 87 | # That's a no-no... 88 | raise Exception("character_replace: findchar == replacechar") 89 | 90 | ind = str.find(findchar) 91 | 92 | while ind != -1: 93 | str = str[0:ind] + "%s" % replacechar + str[ind+1:] 94 | ind = str.find(findchar) 95 | return str 96 | 97 | def dir_exists(dirname): 98 | try: 99 | stat_tuple = os.stat(dirname) 100 | except OSError: 101 | return None 102 | return stat.S_ISDIR(stat_tuple[0]) 103 | 104 | def make_directories(path, basedir): 105 | """Makes path directories off of basedir. Path is a relative path, 106 | and basedir is an absolute path. Example of invocation: 107 | make_directories('foo/bar/baz/quux', '/home/user/') will ensure that 108 | the path /home/user/foo/bar/baz/quux exists""" 109 | arr = path.split(os.sep) 110 | 111 | if basedir[len(basedir)-1] == os.sep: 112 | # Trim tailing dir separator 113 | basedir = basedir[0:len(basedir)-1] 114 | 115 | if not dir_exists(basedir): 116 | os.mkdir(basedir) 117 | 118 | for item in arr: 119 | if not item or item == '': 120 | continue 121 | dirname = "%s%s%s" % (basedir, os.sep, item) 122 | 123 | if not dir_exists(dirname): 124 | os.mkdir(dirname) 125 | 126 | basedir = dirname 127 | return 1 128 | 129 | def msg(msgBar, msgtxt): 130 | """Puts msgtext into the msgBar. Does nothing if msgBar is None""" 131 | if msgBar is not None: 132 | msgBar.message('state', msgtxt) 133 | #else: 134 | # print "=> msgbar: %s" % msgtxt 135 | 136 | def indent(indentlevel=1): 137 | str = "" 138 | if indentlevel < 0: 139 | raise Exception("Indentlevel < 0 - you can't do that! :)") 140 | while indentlevel > 0: 141 | str = str + " " 142 | indentlevel = indentlevel - 1 143 | return str 144 | -------------------------------------------------------------------------------- /xbel-1.0.dtd: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 38 | 39 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 90 | 91 | 92 | 95 | --------------------------------------------------------------------------------