* mainloop.py: added facility for calling select().  Also added
  embryonic facility for pseudo-modal dialogs.
* stdwinevents.py: added modifier masks for key/mouse events
* renamed exceptions in nntplib.py
* Changed string.join() to call string.joinfields() to profit of
  strop.joinfields()
diff --git a/Lib/lib-stdwin/mainloop.py b/Lib/lib-stdwin/mainloop.py
index 0cf5bde..ab8ad3a 100644
--- a/Lib/lib-stdwin/mainloop.py
+++ b/Lib/lib-stdwin/mainloop.py
@@ -13,6 +13,11 @@
 windows = []
 
 
+# Last window that ever received an event
+#
+last_window = None
+
+
 # Function to register a window.
 #
 def register(win):
@@ -28,6 +33,9 @@
 # (this is useful for cleanup actions).
 #
 def unregister(win):
+	global last_window
+	if win == last_window:
+		last_window = None
 	if win in windows:
 		windows.remove(win) # Not in 0.9.1
 		# 0.9.1 solution:
@@ -49,6 +57,65 @@
 		return None
 
 
+# NEW: register any number of file descriptors
+#
+fdlist = []
+select_args = None
+select_handlers = None
+#
+def registerfd(fd, mode, handler):
+	if mode not in ('r', 'w', 'x'):
+		raise ValueError, 'mode must be r, w or x'
+	if type(fd) <> type(0):
+		fd = fd.fileno() # If this fails it's not a proper select arg
+	for i in range(len(fdlist)):
+		if fdlist[i][:2] == (fd, mode):
+			raise ValueError, \
+				'(fd, mode) combination already registered'
+	fdlist.append((fd, mode, handler))
+	make_select_args()
+#
+def unregisterfd(fd, *args):
+	if type(fd) <> type(0):
+		fd = fd.fileno() # If this fails it's not a proper select arg
+	args = (fd,) + args
+	n = len(args)
+	for i in range(len(fdlist)):
+		if fdlist[i][:n] == args:
+			del fdlist[i]
+	make_select_args()
+#
+def make_select_args():
+	global select_args, select_handlers
+	rlist, wlist, xlist = [], [], []
+	rhandlers, whandlers, xhandlers = {}, {}, {}
+	for fd, mode, handler in fdlist:
+		if mode == 'r':
+			rlist.append(fd)
+			rhandlers[`fd`] = handler
+		if mode == 'w':
+			wlist.append(fd)
+			whandlers[`fd`] = handler
+		if mode == 'x':
+			xlist.append(fd)
+			xhandlers[`fd`] = handler
+	if rlist or wlist or xlist:
+		select_args = rlist, wlist, xlist
+		select_handlers = rhandlers, whandlers, xhandlers
+	else:
+		select_args = None
+		select_handlers = None
+#
+def do_select():
+	import select
+	reply = apply(select.select, select_args)
+	for mode in 0, 1, 2:
+		list = reply[mode]
+		for fd in list:
+			handler = select_handlers[mode][`fd`]
+			handler(fd, 'rwx'[mode])
+
+
 # Event processing main loop.
 # Return when there are no windows left, or when an unhandled
 # exception occurs.  (It is safe to restart the main loop after
@@ -57,17 +124,111 @@
 # into KeyboardInterrupt exceptions; these are turned back in events.
 #
 def mainloop():
-	while windows:
+	stdwin_select_handler() # Process events already in stdwin queue
+	fd = stdwin.fileno()
+	while 1:
+		if windows:
+			registerfd(fd, 'r', stdwin_select_handler)
+			try:
+				while windows:
+					do_select()
+					stdwin_select_handler()
+			finally:
+				unregisterfd(fd)
+		elif fdlist:
+			while fdlist and not windows:
+				do_select()
+		else:
+			break
+
+
+# Handle stdwin events until none are left
+#
+def stdwin_select_handler(*args):
+	while 1:
 		try:
-			dispatch(stdwinq.getevent())
+			event = stdwinq.pollevent()
 		except KeyboardInterrupt:
-			dispatch(WE_COMMAND, stdwin.getactive(), WC_CANCEL)
+			event = (WE_COMMAND, None, WC_CANCEL)
+		if event is None:
+			break
+		dispatch(event)
+
+
+# Run a modal dialog loop for a window.  The dialog window must have
+# been registered first.  This prohibits most events (except size/draw
+# events) to other windows.  The modal dialog loop ends when the
+# dialog window unregisters itself.
+#
+passthrough = WE_SIZE, WE_DRAW
+beeping = WE_MOUSE_DOWN, WE_COMMAND, WE_CHAR, WE_KEY, WE_CLOSE, WE_MENU
+#
+def modaldialog(window):
+	if window not in windows:
+		raise ValueError, 'modaldialog window not registered'
+	while window in windows:
+		try:
+			event = stdwinq.getevent()
+		except KeyboardInterrupt:
+			event = WE_COMMAND, None, WC_CANCEL
+		etype, ewindow, edetail = event
+		if etype not in passthrough and ewindow <> window:
+			if etype in beeping:
+				stdwin.fleep()
+			continue
+		dispatch(event)
 
 
 # Dispatch a single event.
+# Events for the no window in particular are sent to the active window
+# or to the last window that received an event (these hacks are for the
+# WE_LOST_SEL event, which is directed to no particular window).
 # Windows not in the windows list don't get their events:
 # events for such windows are silently ignored.
 #
 def dispatch(event):
-	if event[1] in windows:
-		event[1].dispatch(event)
+	global last_window
+	if event[1] == None:
+		active = stdwin.getactive()
+		if active: last_window = active
+	else:
+		last_window = event[1]
+	if last_window in windows:
+		last_window.dispatch(event)
+
+
+# Dialog base class
+#
+class Dialog:
+	#
+	def init(self, title):
+		self.window = stdwin.open(title)
+		self.window.dispatch = self.dispatch
+		register(self.window)
+		return self
+	#
+	def close(self):
+		unregister(self.window)
+		del self.window.dispatch
+		self.window.close()
+	#
+	def dispatch(self, event):
+		etype, ewindow, edetail = event
+		if etype == WE_CLOSE:
+			self.close()
+
+
+# Standard modal dialogs
+# XXX implemented using stdwin dialogs for now
+#
+def askstr(prompt, default):
+	return stdwin.askstr(prompt, default)
+#
+def askync(prompt, yesorno):
+	return stdwin.askync(prompt, yesorno)
+#
+def askfile(prompt, default, new):
+	return stdwin.askfile(prompt, default, new)
+#
+def message(msg):
+	stdwin.message(msg)
diff --git a/Lib/lib-stdwin/stdwinevents.py b/Lib/lib-stdwin/stdwinevents.py
index 9f22dd9..62cf8d2 100644
--- a/Lib/lib-stdwin/stdwinevents.py
+++ b/Lib/lib-stdwin/stdwinevents.py
@@ -44,3 +44,18 @@
 WS_CLIPBOARD   = 0
 WS_PRIMARY     = 1
 WS_SECONDARY   = 2
+
+# Modifier masks in key and mouse events
+
+WM_SHIFT       = (1 << 0)
+WM_LOCK 	= (1 << 1)
+WM_CONTROL 	= (1 << 2)
+WM_META 	= (1 << 3)
+WM_OPTION 	= (1 << 4)
+WM_NUM 		= (1 << 5)
+
+WM_BUTTON1 	= (1 << 8)
+WM_BUTTON2 	= (1 << 9)
+WM_BUTTON3 	= (1 << 10)
+WM_BUTTON4 	= (1 << 11)
+WM_BUTTON5 	= (1 << 12)
diff --git a/Lib/nntplib.py b/Lib/nntplib.py
index 18fa398..c448d48 100644
--- a/Lib/nntplib.py
+++ b/Lib/nntplib.py
@@ -4,7 +4,7 @@
 
 # Example:
 #
-# >>> from nntp import NNTP
+# >>> from nntplib import NNTP
 # >>> s = NNTP().init('charon')
 # >>> resp, count, first, last, name = s.group('nlnet.misc')
 # >>> print 'Group', name, 'has', count, 'articles, range', first, 'to', last
@@ -32,12 +32,12 @@
 import string
 
 
-# Exception raiseds when an error or invalid response is received
+# Exception raised when an error or invalid response is received
 
-error_reply = 'nntp.error_reply'	# unexpected [123]xx reply
-error_function = 'nntp.error_function'	# 4xx errors
-error_form = 'nntp.error_form'		# 5xx errors
-error_protocol = 'nntp.error_protocol'	# response does not begin with [1-5]
+error_reply = 'nntplib.error_reply'	# unexpected [123]xx reply
+error_temp = 'nntplib.error_temp'	# 4xx errors
+error_perm = 'nntplib.error_perm'	# 5xx errors
+error_proto = 'nntplib.error_proto'	# response does not begin with [1-5]
 
 
 # Standard port used by NNTP servers
@@ -119,11 +119,11 @@
 		if self.debugging: print '*resp*', `resp`
 		c = resp[:1]
 		if c == '4':
-			raise error_function, resp
+			raise error_temp, resp
 		if c == '5':
-			raise error_form, resp
+			raise error_perm, resp
 		if c not in '123':
-			raise error_protocol, resp
+			raise error_proto, resp
 		return resp
 
 	# Internal: get a response plus following text from the server.
@@ -342,7 +342,7 @@
 
 	def ihave(self, id, f):
 		resp = self.shortcmd('IHAVE ' + id)
-		# Raises error_function if the server already has it
+		# Raises error_??? if the server already has it
 		if resp[0] <> '3':
 			raise error_reply, resp
 		while 1:
diff --git a/Lib/stdwin/mainloop.py b/Lib/stdwin/mainloop.py
index 0cf5bde..ab8ad3a 100755
--- a/Lib/stdwin/mainloop.py
+++ b/Lib/stdwin/mainloop.py
@@ -13,6 +13,11 @@
 windows = []
 
 
+# Last window that ever received an event
+#
+last_window = None
+
+
 # Function to register a window.
 #
 def register(win):
@@ -28,6 +33,9 @@
 # (this is useful for cleanup actions).
 #
 def unregister(win):
+	global last_window
+	if win == last_window:
+		last_window = None
 	if win in windows:
 		windows.remove(win) # Not in 0.9.1
 		# 0.9.1 solution:
@@ -49,6 +57,65 @@
 		return None
 
 
+# NEW: register any number of file descriptors
+#
+fdlist = []
+select_args = None
+select_handlers = None
+#
+def registerfd(fd, mode, handler):
+	if mode not in ('r', 'w', 'x'):
+		raise ValueError, 'mode must be r, w or x'
+	if type(fd) <> type(0):
+		fd = fd.fileno() # If this fails it's not a proper select arg
+	for i in range(len(fdlist)):
+		if fdlist[i][:2] == (fd, mode):
+			raise ValueError, \
+				'(fd, mode) combination already registered'
+	fdlist.append((fd, mode, handler))
+	make_select_args()
+#
+def unregisterfd(fd, *args):
+	if type(fd) <> type(0):
+		fd = fd.fileno() # If this fails it's not a proper select arg
+	args = (fd,) + args
+	n = len(args)
+	for i in range(len(fdlist)):
+		if fdlist[i][:n] == args:
+			del fdlist[i]
+	make_select_args()
+#
+def make_select_args():
+	global select_args, select_handlers
+	rlist, wlist, xlist = [], [], []
+	rhandlers, whandlers, xhandlers = {}, {}, {}
+	for fd, mode, handler in fdlist:
+		if mode == 'r':
+			rlist.append(fd)
+			rhandlers[`fd`] = handler
+		if mode == 'w':
+			wlist.append(fd)
+			whandlers[`fd`] = handler
+		if mode == 'x':
+			xlist.append(fd)
+			xhandlers[`fd`] = handler
+	if rlist or wlist or xlist:
+		select_args = rlist, wlist, xlist
+		select_handlers = rhandlers, whandlers, xhandlers
+	else:
+		select_args = None
+		select_handlers = None
+#
+def do_select():
+	import select
+	reply = apply(select.select, select_args)
+	for mode in 0, 1, 2:
+		list = reply[mode]
+		for fd in list:
+			handler = select_handlers[mode][`fd`]
+			handler(fd, 'rwx'[mode])
+
+
 # Event processing main loop.
 # Return when there are no windows left, or when an unhandled
 # exception occurs.  (It is safe to restart the main loop after
@@ -57,17 +124,111 @@
 # into KeyboardInterrupt exceptions; these are turned back in events.
 #
 def mainloop():
-	while windows:
+	stdwin_select_handler() # Process events already in stdwin queue
+	fd = stdwin.fileno()
+	while 1:
+		if windows:
+			registerfd(fd, 'r', stdwin_select_handler)
+			try:
+				while windows:
+					do_select()
+					stdwin_select_handler()
+			finally:
+				unregisterfd(fd)
+		elif fdlist:
+			while fdlist and not windows:
+				do_select()
+		else:
+			break
+
+
+# Handle stdwin events until none are left
+#
+def stdwin_select_handler(*args):
+	while 1:
 		try:
-			dispatch(stdwinq.getevent())
+			event = stdwinq.pollevent()
 		except KeyboardInterrupt:
-			dispatch(WE_COMMAND, stdwin.getactive(), WC_CANCEL)
+			event = (WE_COMMAND, None, WC_CANCEL)
+		if event is None:
+			break
+		dispatch(event)
+
+
+# Run a modal dialog loop for a window.  The dialog window must have
+# been registered first.  This prohibits most events (except size/draw
+# events) to other windows.  The modal dialog loop ends when the
+# dialog window unregisters itself.
+#
+passthrough = WE_SIZE, WE_DRAW
+beeping = WE_MOUSE_DOWN, WE_COMMAND, WE_CHAR, WE_KEY, WE_CLOSE, WE_MENU
+#
+def modaldialog(window):
+	if window not in windows:
+		raise ValueError, 'modaldialog window not registered'
+	while window in windows:
+		try:
+			event = stdwinq.getevent()
+		except KeyboardInterrupt:
+			event = WE_COMMAND, None, WC_CANCEL
+		etype, ewindow, edetail = event
+		if etype not in passthrough and ewindow <> window:
+			if etype in beeping:
+				stdwin.fleep()
+			continue
+		dispatch(event)
 
 
 # Dispatch a single event.
+# Events for the no window in particular are sent to the active window
+# or to the last window that received an event (these hacks are for the
+# WE_LOST_SEL event, which is directed to no particular window).
 # Windows not in the windows list don't get their events:
 # events for such windows are silently ignored.
 #
 def dispatch(event):
-	if event[1] in windows:
-		event[1].dispatch(event)
+	global last_window
+	if event[1] == None:
+		active = stdwin.getactive()
+		if active: last_window = active
+	else:
+		last_window = event[1]
+	if last_window in windows:
+		last_window.dispatch(event)
+
+
+# Dialog base class
+#
+class Dialog:
+	#
+	def init(self, title):
+		self.window = stdwin.open(title)
+		self.window.dispatch = self.dispatch
+		register(self.window)
+		return self
+	#
+	def close(self):
+		unregister(self.window)
+		del self.window.dispatch
+		self.window.close()
+	#
+	def dispatch(self, event):
+		etype, ewindow, edetail = event
+		if etype == WE_CLOSE:
+			self.close()
+
+
+# Standard modal dialogs
+# XXX implemented using stdwin dialogs for now
+#
+def askstr(prompt, default):
+	return stdwin.askstr(prompt, default)
+#
+def askync(prompt, yesorno):
+	return stdwin.askync(prompt, yesorno)
+#
+def askfile(prompt, default, new):
+	return stdwin.askfile(prompt, default, new)
+#
+def message(msg):
+	stdwin.message(msg)
diff --git a/Lib/stdwin/stdwinevents.py b/Lib/stdwin/stdwinevents.py
index 9f22dd9..62cf8d2 100755
--- a/Lib/stdwin/stdwinevents.py
+++ b/Lib/stdwin/stdwinevents.py
@@ -44,3 +44,18 @@
 WS_CLIPBOARD   = 0
 WS_PRIMARY     = 1
 WS_SECONDARY   = 2
+
+# Modifier masks in key and mouse events
+
+WM_SHIFT       = (1 << 0)
+WM_LOCK 	= (1 << 1)
+WM_CONTROL 	= (1 << 2)
+WM_META 	= (1 << 3)
+WM_OPTION 	= (1 << 4)
+WM_NUM 		= (1 << 5)
+
+WM_BUTTON1 	= (1 << 8)
+WM_BUTTON2 	= (1 << 9)
+WM_BUTTON3 	= (1 << 10)
+WM_BUTTON4 	= (1 << 11)
+WM_BUTTON5 	= (1 << 12)
diff --git a/Lib/string.py b/Lib/string.py
index b4e0d5e..aed3eaf 100644
--- a/Lib/string.py
+++ b/Lib/string.py
@@ -82,10 +82,7 @@
 
 # Join words with spaces between them
 def join(words):
-	res = ''
-	for w in words:
-		res = res + (' ' + w)
-	return res[1:]
+	return joinfields(words, ' ')
 
 # Join fields with separator
 def joinfields(words, sep):
diff --git a/Lib/stringold.py b/Lib/stringold.py
index b4e0d5e..aed3eaf 100644
--- a/Lib/stringold.py
+++ b/Lib/stringold.py
@@ -82,10 +82,7 @@
 
 # Join words with spaces between them
 def join(words):
-	res = ''
-	for w in words:
-		res = res + (' ' + w)
-	return res[1:]
+	return joinfields(words, ' ')
 
 # Join fields with separator
 def joinfields(words, sep):