| 1 | #!/usr/bin/python |
| 2 | """This is wxSlash 1.1 |
| 3 | |
| 4 | It's the obligatory Slashdot.org headlines reader that any modern |
| 5 | widget set/library must have in order to be taken seriously :-) |
| 6 | |
| 7 | Usage is quite simple; wxSlash attempts to download the 'ultramode.txt' |
| 8 | file from http://slashdot.org, which contains the headlines in a computer |
| 9 | friendly format. It then displays said headlines in a wxWindows list control. |
| 10 | |
| 11 | You can read articles using either Python's html library or an external |
| 12 | browser. Uncheck the 'browser->internal' menu item to use the latter option. |
| 13 | Use the settings dialog box to set which external browser is started. |
| 14 | |
| 15 | This code is available under the wxWindows license, see elsewhere. If you |
| 16 | modify this code, be aware of the fact that slashdot.org's maintainer, |
| 17 | CmdrTaco, explicitly asks 'ultramode.txt' downloaders not to do this |
| 18 | automatically more than twice per hour. If this feature is abused, CmdrTaco |
| 19 | may remove the ultramode file completely and that will make a *lot* of people |
| 20 | unhappy. |
| 21 | |
| 22 | I want to thank Alex Shnitman whose slashes.pl (Perl/GTK) script gave me |
| 23 | the idea for this applet. |
| 24 | |
| 25 | Have fun with it, |
| 26 | |
| 27 | Harm van der Heijden (H.v.d.Heijden@phys.tue.nl) |
| 28 | """ |
| 29 | |
| 30 | from wxPython.wx import * |
| 31 | from httplib import HTTP |
| 32 | from htmllib import HTMLParser |
| 33 | import os |
| 34 | import re |
| 35 | import formatter |
| 36 | |
| 37 | class HTMLTextView(wxFrame): |
| 38 | def __init__(self, parent, id, title='HTMLTextView', url=None): |
| 39 | wxFrame.__init__(self, parent, id, title, wxPyDefaultPosition, |
| 40 | wxSize(600,400)) |
| 41 | |
| 42 | self.mainmenu = wxMenuBar() |
| 43 | |
| 44 | menu = wxMenu() |
| 45 | menu.Append(201, '&Open URL...', 'Open URL') |
| 46 | EVT_MENU(self, 201, self.OnFileOpen) |
| 47 | menu.Append(209, 'E&xit', 'Exit viewer') |
| 48 | EVT_MENU(self, 209, self.OnFileExit) |
| 49 | |
| 50 | self.mainmenu.Append(menu, '&File') |
| 51 | self.SetMenuBar(self.mainmenu) |
| 52 | self.CreateStatusBar(1) |
| 53 | |
| 54 | self.text = wxTextCtrl(self, -1, "", wxPyDefaultPosition, |
| 55 | wxPyDefaultSize, wxTE_MULTILINE | wxTE_READONLY) |
| 56 | |
| 57 | if (url): |
| 58 | self.OpenURL(url) |
| 59 | |
| 60 | def logprint(self, x): |
| 61 | self.SetStatusText(x) |
| 62 | |
| 63 | def OpenURL(self, url): |
| 64 | self.url = url |
| 65 | m = re.match('file:(\S+)\s*', url) |
| 66 | if m: |
| 67 | f = open(m.groups()[0],'r') |
| 68 | else: |
| 69 | m = re.match('http://([^/]+)(/\S*)\s*', url) |
| 70 | if m: |
| 71 | host = m.groups()[0] |
| 72 | path = m.groups()[1] |
| 73 | else: |
| 74 | m = re.match('http://(\S+)\s*', url) |
| 75 | if not m: |
| 76 | # Invalid URL |
| 77 | self.logprint("Invalid or unsupported URL: %s" % (url)) |
| 78 | return |
| 79 | host = m.groups()[0] |
| 80 | path = '' |
| 81 | f = RetrieveAsFile(host,path,self.logprint) |
| 82 | if not f: |
| 83 | self.logprint("Could not open %s" % (url)) |
| 84 | return |
| 85 | self.logprint("Receiving data...") |
| 86 | data = f.read() |
| 87 | tmp = open('tmphtml.txt','w') |
| 88 | fmt = formatter.AbstractFormatter(formatter.DumbWriter(tmp)) |
| 89 | p = HTMLParser(fmt) |
| 90 | self.logprint("Parsing data...") |
| 91 | p.feed(data) |
| 92 | p.close() |
| 93 | tmp.close() |
| 94 | tmp = open('tmphtml.txt', 'r') |
| 95 | self.text.SetValue(tmp.read()) |
| 96 | self.SetTitle(url) |
| 97 | self.logprint(url) |
| 98 | |
| 99 | def OnFileOpen(self, event): |
| 100 | dlg = wxTextEntryDialog(self, "Enter URL to open:", "") |
| 101 | if dlg.ShowModal() == wxID_OK: |
| 102 | url = dlg.GetValue() |
| 103 | else: |
| 104 | url = None |
| 105 | if url: |
| 106 | self.OpenURL(url) |
| 107 | |
| 108 | def OnFileExit(self, event): |
| 109 | self.Close() |
| 110 | |
| 111 | def OnCloseWindow(self, event): |
| 112 | self.Destroy() |
| 113 | |
| 114 | |
| 115 | def ParseSlashdot(f): |
| 116 | art_sep = re.compile('%%\r?\n') |
| 117 | line_sep = re.compile('\r?\n') |
| 118 | data = f.read() |
| 119 | list = art_sep.split(data) |
| 120 | art_list = [] |
| 121 | for i in range(1,len(list)-1): |
| 122 | art_list.append(line_sep.split(list[i])) |
| 123 | return art_list |
| 124 | |
| 125 | def myprint(x): |
| 126 | print x |
| 127 | |
| 128 | def RetrieveAsFile(host, path='', logprint = myprint): |
| 129 | try: |
| 130 | h = HTTP(host) |
| 131 | except: |
| 132 | logprint("Failed to create HTTP connection to %s... is the network available?" % (host)) |
| 133 | return None |
| 134 | h.putrequest('GET',path) |
| 135 | h.putheader('Accept','text/html') |
| 136 | h.putheader('Accept','text/plain') |
| 137 | h.endheaders() |
| 138 | errcode, errmsg, headers = h.getreply() |
| 139 | if errcode != 200: |
| 140 | logprint("HTTP error code %d: %s" % (errcode, errmsg)) |
| 141 | return None |
| 142 | f = h.getfile() |
| 143 | # f = open('/home/harm/ultramode.txt','r') |
| 144 | return f |
| 145 | |
| 146 | |
| 147 | class AppStatusBar(wxStatusBar): |
| 148 | def __init__(self, parent): |
| 149 | wxStatusBar.__init__(self,parent, -1) |
| 150 | self.SetFieldsCount(2) |
| 151 | self.SetStatusWidths([-1, 100]) |
| 152 | self.but = wxButton(self, 1001, "Refresh") |
| 153 | EVT_BUTTON(self, 1001, parent.OnViewRefresh) |
| 154 | self.OnSize(None) |
| 155 | |
| 156 | def logprint(self,x): |
| 157 | self.SetStatusText(x,0) |
| 158 | |
| 159 | def OnSize(self, event): |
| 160 | rect = self.GetFieldRect(1) |
| 161 | self.but.SetPosition(wxPoint(rect.x+2, rect.y+2)) |
| 162 | self.but.SetSize(wxSize(rect.width-4, rect.height-4)) |
| 163 | |
| 164 | # This is a simple timer class to start a function after a short delay; |
| 165 | class QuickTimer(wxTimer): |
| 166 | def __init__(self, func, wait=100): |
| 167 | wxTimer.__init__(self) |
| 168 | self.callback = func |
| 169 | self.Start(wait); # wait .1 second (.001 second doesn't work. why?) |
| 170 | def Notify(self): |
| 171 | self.Stop(); |
| 172 | apply(self.callback, ()); |
| 173 | |
| 174 | class AppFrame(wxFrame): |
| 175 | def __init__(self, parent, id, title): |
| 176 | wxFrame.__init__(self, parent, id, title, wxPyDefaultPosition, |
| 177 | wxSize(650, 250)) |
| 178 | |
| 179 | # if the window manager closes the window: |
| 180 | EVT_CLOSE(self, self.OnCloseWindow); |
| 181 | |
| 182 | # Now Create the menu bar and items |
| 183 | self.mainmenu = wxMenuBar() |
| 184 | |
| 185 | menu = wxMenu() |
| 186 | menu.Append(209, 'E&xit', 'Enough of this already!') |
| 187 | EVT_MENU(self, 209, self.OnFileExit) |
| 188 | self.mainmenu.Append(menu, '&File') |
| 189 | menu = wxMenu() |
| 190 | menu.Append(210, '&Refresh', 'Refresh headlines') |
| 191 | EVT_MENU(self, 210, self.OnViewRefresh) |
| 192 | menu.Append(211, '&Slashdot Index', 'View Slashdot index') |
| 193 | EVT_MENU(self, 211, self.OnViewIndex) |
| 194 | menu.Append(212, 'Selected &Article', 'View selected article') |
| 195 | EVT_MENU(self, 212, self.OnViewArticle) |
| 196 | self.mainmenu.Append(menu, '&View') |
| 197 | menu = wxMenu() |
| 198 | menu.Append(220, '&Internal', 'Use internal text browser',TRUE) |
| 199 | menu.Check(220, true) |
| 200 | self.UseInternal = 1; |
| 201 | EVT_MENU(self, 220, self.OnBrowserInternal) |
| 202 | menu.Append(222, '&Settings...', 'External browser Settings') |
| 203 | EVT_MENU(self, 222, self.OnBrowserSettings) |
| 204 | self.mainmenu.Append(menu, '&Browser') |
| 205 | menu = wxMenu() |
| 206 | menu.Append(230, '&About', 'Some documentation'); |
| 207 | EVT_MENU(self, 230, self.OnAbout) |
| 208 | self.mainmenu.Append(menu, '&Help') |
| 209 | |
| 210 | self.SetMenuBar(self.mainmenu) |
| 211 | |
| 212 | if wxPlatform == '__WXGTK__': |
| 213 | # I like lynx. Also Netscape 4.5 doesn't react to my cmdline opts |
| 214 | self.BrowserSettings = "xterm -e lynx %s &" |
| 215 | elif wxPlatform == '__WXMSW__': |
| 216 | # netscape 4.x likes to hang out here... |
| 217 | self.BrowserSettings = '\\progra~1\\Netscape\\Communicator\\Program\\netscape.exe %s' |
| 218 | else: |
| 219 | # a wild guess... |
| 220 | self.BrowserSettings = 'netscape %s' |
| 221 | |
| 222 | # A status bar to tell people what's happening |
| 223 | self.sb = AppStatusBar(self) |
| 224 | self.SetStatusBar(self.sb) |
| 225 | |
| 226 | self.list = wxListCtrl(self, 1100) |
| 227 | self.list.SetSingleStyle(wxLC_REPORT) |
| 228 | self.list.InsertColumn(0, 'Subject') |
| 229 | self.list.InsertColumn(1, 'Date') |
| 230 | self.list.InsertColumn(2, 'Posted by') |
| 231 | self.list.InsertColumn(3, 'Comments') |
| 232 | self.list.SetColumnWidth(0, 300) |
| 233 | self.list.SetColumnWidth(1, 150) |
| 234 | self.list.SetColumnWidth(2, 100) |
| 235 | self.list.SetColumnWidth(3, 100) |
| 236 | |
| 237 | EVT_LIST_ITEM_SELECTED(self, 1100, self.OnItemSelected) |
| 238 | EVT_LEFT_DCLICK(self.list, self.OnLeftDClick) |
| 239 | |
| 240 | self.logprint("Connecting to slashdot... Please wait.") |
| 241 | # wxYield doesn't yet work here. That's why we use a timer |
| 242 | # to make sure that we see some GUI stuff before the slashdot |
| 243 | # file is transfered. |
| 244 | self.timer = QuickTimer(self.DoRefresh, 1000) |
| 245 | |
| 246 | def logprint(self, x): |
| 247 | self.sb.logprint(x) |
| 248 | |
| 249 | def OnFileExit(self, event): |
| 250 | self.Destroy() |
| 251 | |
| 252 | def DoRefresh(self): |
| 253 | f = RetrieveAsFile('slashdot.org','/ultramode.txt',self.sb.logprint) |
| 254 | art_list = ParseSlashdot(f) |
| 255 | self.list.DeleteAllItems() |
| 256 | self.url = [] |
| 257 | self.current = -1 |
| 258 | i = 0; |
| 259 | for article in art_list: |
| 260 | self.list.InsertStringItem(i, article[0]) |
| 261 | self.list.SetStringItem(i, 1, article[2]) |
| 262 | self.list.SetStringItem(i, 2, article[3]) |
| 263 | self.list.SetStringItem(i, 3, article[6]) |
| 264 | self.url.append(article[1]) |
| 265 | i = i + 1 |
| 266 | self.logprint("File retrieved OK.") |
| 267 | |
| 268 | def OnViewRefresh(self, event): |
| 269 | self.logprint("Connecting to slashdot... Please wait."); |
| 270 | wxYield() |
| 271 | self.DoRefresh() |
| 272 | |
| 273 | def DoViewIndex(self): |
| 274 | if self.UseInternal: |
| 275 | self.view = HTMLTextView(self, -1, 'slashdot.org', |
| 276 | 'http://slashdot.org') |
| 277 | self.view.Show(true) |
| 278 | else: |
| 279 | self.logprint(self.BrowserSettings % ('http://slashdot.org')) |
| 280 | os.system(self.BrowserSettings % ('http://slashdot.org')) |
| 281 | self.logprint("OK") |
| 282 | |
| 283 | def OnViewIndex(self, event): |
| 284 | self.logprint("Starting browser... Please wait.") |
| 285 | wxYield() |
| 286 | self.DoViewIndex() |
| 287 | |
| 288 | def DoViewArticle(self): |
| 289 | if self.current<0: return |
| 290 | url = self.url[self.current] |
| 291 | if self.UseInternal: |
| 292 | self.view = HTMLTextView(self, -1, url, url) |
| 293 | self.view.Show(true) |
| 294 | else: |
| 295 | self.logprint(self.BrowserSettings % (url)) |
| 296 | os.system(self.BrowserSettings % (url)) |
| 297 | self.logprint("OK") |
| 298 | |
| 299 | def OnViewArticle(self, event): |
| 300 | self.logprint("Starting browser... Please wait.") |
| 301 | wxYield() |
| 302 | self.DoViewArticle() |
| 303 | |
| 304 | def OnBrowserInternal(self, event): |
| 305 | if self.mainmenu.Checked(220): |
| 306 | self.UseInternal = 1 |
| 307 | else: |
| 308 | self.UseInternal = 0 |
| 309 | |
| 310 | def OnBrowserSettings(self, event): |
| 311 | dlg = wxTextEntryDialog(self, "Enter command to view URL.\nUse %s as a placeholder for the URL.", "", self.BrowserSettings); |
| 312 | if dlg.ShowModal() == wxID_OK: |
| 313 | self.BrowserSettings = dlg.GetValue() |
| 314 | |
| 315 | def OnAbout(self, event): |
| 316 | dlg = wxMessageDialog(self, __doc__, "wxSlash", wxOK | wxICON_INFORMATION) |
| 317 | dlg.ShowModal() |
| 318 | |
| 319 | def OnItemSelected(self, event): |
| 320 | self.current = event.m_itemIndex |
| 321 | self.logprint("URL: %s" % (self.url[self.current])) |
| 322 | |
| 323 | def OnLeftDClick(self, event): |
| 324 | (x,y) = event.Position(); |
| 325 | # Actually, we should convert x,y to logical coords using |
| 326 | # a dc, but only for a wxScrolledWindow widget. |
| 327 | # Now wxGTK derives wxListCtrl from wxScrolledWindow, |
| 328 | # and wxMSW from wxControl... So that doesn't work. |
| 329 | #dc = wxClientDC(self.list) |
| 330 | ##self.list.PrepareDC(dc) |
| 331 | #x = dc.DeviceToLogicalX( event.GetX() ) |
| 332 | #y = dc.DeviceToLogicalY( event.GetY() ) |
| 333 | id = self.list.HitTest(wxPoint(x,y)) |
| 334 | #print "Double click at %d %d" % (x,y), id |
| 335 | # Okay, we got a double click. Let's assume it's the current selection |
| 336 | wxYield() |
| 337 | self.OnViewArticle(event) |
| 338 | |
| 339 | def OnCloseWindow(self, event): |
| 340 | self.Destroy() |
| 341 | |
| 342 | class MyApp(wxApp): |
| 343 | def OnInit(self): |
| 344 | frame = AppFrame(NULL, -1, "Slashdot Breaking News") |
| 345 | frame.Show(true) |
| 346 | self.SetTopWindow(frame) |
| 347 | return true |
| 348 | |
| 349 | # |
| 350 | # main thingy |
| 351 | # |
| 352 | if __name__ == '__main__': |
| 353 | app = MyApp(0) |
| 354 | app.MainLoop() |
| 355 | |
| 356 | |
| 357 | |
| 358 | |
| 359 | |