COPYING
+config/confsubst
EXTRA_DIST =
CLEANFILES =
+confsubst = $(srcdir)/config/confsubst
+EXTRA_DIST += config/confsubst
+
###--------------------------------------------------------------------------
### Distribution arrangements.
xatom_SOURCES += libxatom.h libxatom.c
###--------------------------------------------------------------------------
+### Graphical tools in Python.
+
+if HAVE_PYGTK
+
+python_PYTHON =
+
+## Common code.
+python_PYTHON += xtoys.py
+
+## xmsg
+bin_SCRIPTS += xmsg
+CLEANFILES += xmsg
+EXTRA_DIST += xmsg.in
+
+dist_man_MANS += xmsg.1
+
+xmsg: xmsg.in Makefile
+ $(confsubst) $(srcdir)/xmsg.in >$@.new \
+ PYTHON=$(PYTHON) VERSION=$(VERSION)
+ chmod +x $@.new
+ mv $@.new $@
+
+## xcatch
+bin_SCRIPTS += xcatch
+CLEANFILES += xcatch
+EXTRA_DIST += xcatch.in
+
+dist_man_MANS += xcatch.1
+
+xcatch: xcatch.in Makefile
+ $(confsubst) $(srcdir)/xcatch.in >$@.new \
+ PYTHON=$(PYTHON) VERSION=$(VERSION)
+ chmod +x $@.new
+ mv $@.new $@
+
+## xgetline
+bin_SCRIPTS += xgetline
+CLEANFILES += xgetline
+EXTRA_DIST += xgetline.in
+
+dist_man_MANS += xgetline.1
+
+xgetline: xgetline.in Makefile
+ $(confsubst) $(srcdir)/xgetline.in >$@.new \
+ PYTHON=$(PYTHON) VERSION=$(VERSION)
+ chmod +x $@.new
+ mv $@.new $@
+
+endif
+
+###--------------------------------------------------------------------------
### Debian.
EXTRA_DIST += debian/rules
EXTRA_DIST += debian/xtoys.install
+EXTRA_DIST += debian/xtoys-gtk.install
+
###----- That's all, folks --------------------------------------------------
LIBS="$LIBS $mLib_LIBS"
dnl--------------------------------------------------------------------------
+dnl Python programming environment.
+
+AM_PATH_PYTHON([2.4], [python=yes], [python=no])
+AM_CONDITIONAL([HAVE_PYTHON], [test $python = yes])
+
+if test $python = yes; then
+ AC_PYTHON_MODULE([pygtk])
+fi
+AM_CONDITIONAL([HAVE_PYGTK], [test ${HAVE_PYMOD_PYGTK-no} = yes])
+
+dnl--------------------------------------------------------------------------
dnl Output.
AC_CONFIG_FILES(
Source: xtoys
Section: x11
Priority: extra
-Build-Depends: libx11-dev, mlib-dev (>= 2.0.4), debhelper (>= 5)
+Build-Depends:
+ libx11-dev, mlib-dev (>= 2.0.4), debhelper (>= 5),
+ python-central, python (>= 2.4), python-gtk2
Maintainer: Mark Wooding <mdw@distorted.org.uk>
+XS-Python-Version: >= 2.4
Standards-Version: 3.1.1
Package: xtoys
We have:
xscsize -- reports the display size as shell variables
xatom -- inspect properties on windows, and wait for changes
+
+Package: xtoys-gtk
+Architecture: all
+Depends: ${python:Depends}, python-gtk2 (>= 2.10)
+Recommends: xtoys
+XB-Python-Version: ${python:Versions}
+Description: A collection of small X11 tools
+ We have:
+ xcatch -- run a program, displaying its output in a scrolling window if
+ there is any
+ xgetline -- pops up a dialogue, reads a line of text, and reports the
+ entered line on stdout; can do passwords, history with optional size
+ limit, or simple selection from a list
+ xmsg -- pop up a dialogue showing a message and a bunch of buttons, and
+ gets a response
include $(CDBS)/class/autotools.mk
DEB_BUILDDIR = $(CURDIR)/build
-DEB_DESTDIR = $(CURDIR)/debian/tmp
+
+binary-install/xtoys-gtk::
+ dh_pycentral -pxtoys-gtk
--- /dev/null
+debian/tmp/usr/bin/xcatch
+debian/tmp/usr/bin/xgetline
+debian/tmp/usr/bin/xmsg
+debian/tmp/usr/lib/python*
xcatch \- catch input and trap it in a window
.SH SYNOPSIS
.B xcatch
-.RB [ \-f
-.IR file ]
+.RI [ gtk-options ...]
.RB [ \-F
.IR font ]
+.RB [ \-f
+.IR file ]
.RI [ command
.RI [ args ]...]
.SH DESCRIPTION
otherwise,
.B xcatch
reads from its own standard input.
+.PP
+It is better to write
+.IP
+.B xcatch
+.I command args
+.PP
+than
+.IP
+.I command args
+.B | xcatch
+.PP
+The former allows
+.B xcatch
+to collect the exit status of the
+.I command
+and (a) report it in its window, and (b) provide it (or a version of it)
+as its own exit status for the caller to check. It also enables
+.B xcatch
+to put the command line in its title bar.
.SS Options
.TP
-.BI "\-\-display " display
-Create the window on the named
-.IR display .
+.BI "\-F, \-\-font " font
+Display output text in
+.I font
+rather than GTK's default.
.TP
.BI "\-f, \-\-file " file
Read data from
.I file
instead of standard output.
-.TP
-.BI "\-F, \-\-font " font
-Display output text in
-.I font
-rather than GTK's default (Helvetica).
.SH BUGS
None currently known.
+.SH SEE ALSO
+.BR gtk-options (7).
.SH AUTHOR
Mark Wooding (mdw@distorted.org.uk).
--- /dev/null
+#! @PYTHON@
+### -*-python-*-
+###
+### Catch input and trap it in an X window
+###
+### (c) 2008 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Edgeware X tools collection.
+###
+### X tools is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### X tools is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with X tools; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+VERSION = '@VERSION@'
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import optparse as O
+from sys import stdin, stdout, stderr, exit
+import sys as SYS
+import os as OS
+import fcntl as FC
+import errno as E
+import subprocess as S
+import pango as P
+import signal as SIG
+import traceback as TB
+
+import xtoys as XT
+GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+def nonblocking(file):
+ """
+ Make the FILE be nonblocking.
+
+ FILE may be either an integer file descriptor, or something whose fileno
+ method yields up a file descriptor.
+ """
+ if isinstance(file, int):
+ fd = file
+ else:
+ fd = file.fileno()
+ flags = FC.fcntl(fd, FC.F_GETFL)
+ FC.fcntl(fd, FC.F_SETFL, flags | OS.O_NONBLOCK)
+
+class complain (object):
+ """
+ Function decorator: catch exceptions and report them in an error box.
+
+ Example:
+
+ @complain(DEFAULT)
+ def foo(...):
+ ...
+
+ The decorated function is called normally. If no exception occurs (or at
+ least none propagates out of the function) then it returns normally too.
+ Otherwise, the exception is trapped and displayed in a Message window, and
+ the function returns DEFAULT, which defaults to None.
+ """
+
+ def __init__(me, default = None):
+ """Initializer: store the DEFAULT value for later."""
+ me._default = default
+
+ def __call__(me, func):
+ """Decorate the function."""
+ def _(*args, **kw):
+ try:
+ return func(*args, **kw)
+ except:
+ type, info, _ = SYS.exc_info()
+ if isinstance(type, str):
+ head = 'Unexpected exception'
+ msg = type
+ else:
+ head = type.__name__
+ msg = ', '.join(info.args)
+ XT.Message(title = 'Error!', type = 'error', headline = head,
+ buttons = ['gtk-ok'], message = msg).ask()
+ return me._default
+ return _
+
+###--------------------------------------------------------------------------
+### Watching processes.
+
+class Reaper (object):
+ """
+ The Reaper catches SIGCHLD and collects exit statuses.
+
+ There should ideally be only one instance of the class; the reaper method
+ returns the instance, creating it if necessary. (The reaper uses up
+ resources which we can avoid wasting under some circumstances.)
+
+ Call add(KID, FUNC) to watch process-id KID; when it exits, the reaper
+ calls FUNC(KID, STATUS), where STATUS is the exit status directly from
+ wait.
+
+ Even though SIGCHLD occurs at unpredictable times, processes are not reaped
+ until we return to the GTK event loop. This means that you can safely
+ create processes and add them without needing to interlock with the reaper
+ in complicated ways, and it also means that handler functions are not
+ called at unpredictable times.
+ """
+
+ def __init__(me):
+ """
+ Initialize the reaper.
+
+ We create a pipe and register the read end of it with the GTK event
+ system. The SIGCHLD handler writes a byte to the pipe.
+ """
+ me._kidmap = {}
+ me._prd, pwr = OS.pipe()
+ nonblocking(me._prd)
+ SIG.signal(SIG.SIGCHLD, lambda sig, tb: OS.write(pwr, '?'))
+ GO.io_add_watch(me._prd, GO.IO_IN | GO.IO_HUP, me._wake)
+
+ _reaper = None
+ @classmethod
+ def reaper(cls):
+ """Return the instance of the Reaper, creating it if necessary."""
+ if cls._reaper is None:
+ cls._reaper = cls()
+ return cls._reaper
+
+ def add(me, kid, func):
+ """
+ Register the process-id KID with the reaper, calling FUNC when it exits.
+
+ As described, FUNC is called with two arguments, the KID and its exit
+ status.
+ """
+ me._kidmap[kid] = func
+
+ def _wake(me, file, reason):
+ """
+ Called when the event loop notices something in the signal pipe.
+
+ We empty the pipe and then reap any processes which need it.
+ """
+
+ ## Empty the pipe. It doesn't matter how many bytes are stored in the
+ ## pipe, or what their contents are.
+ try:
+ while True:
+ OS.read(me._prd, 16384)
+ except OSError, err:
+ if err.errno != E.EAGAIN:
+ raise
+
+ ## Reap processes and pass their exit statuses on.
+ while True:
+ try:
+ kid, st = OS.waitpid(-1, OS.WNOHANG)
+ except OSError, err:
+ if err.errno == E.ECHILD:
+ break
+ else:
+ raise
+ if kid == 0:
+ break
+ try:
+ func = me._kidmap[kid]
+ del me._kidmap[kid]
+ except KeyError:
+ continue
+ func(kid, st)
+
+ ## Done: call me again.
+ return True
+
+###--------------------------------------------------------------------------
+### Catching and displaying output.
+
+class Catcher (object):
+ """
+ Catcher objects watch an input file and display the results in a window.
+
+ Initialization is a little cumbersome. You make an object, and then add a
+ file and maybe a process-id to watch. The catcher will not create a window
+ until it actually needs to display something.
+
+ The object can be configured by setting attributes before it first opens
+ its window.
+
+ * title: is the title for the window
+ * font: is the font to display text in, as a string
+
+ The rc attribute is set to a suitable exit status.
+ """
+
+ def __init__(me):
+ """Initialize the catcher."""
+ me.title = 'xcatch'
+ me._file = None
+ me._window = None
+ me._buf = None
+ me.font = None
+ me._openp = False
+ me.rc = 0
+
+ def watch_file(me, file):
+ """
+ Watch the FILE for input.
+
+ Any data arriving for the FILE is recoded into UTF-8 and displayed in the
+ window. The file not reaching EOF is considered a reason not to end the
+ program.
+ """
+ XT.addreason()
+ nonblocking(file)
+ me._src = GO.io_add_watch(file, GO.IO_IN | GO.IO_HUP, me._ready)
+ me._file = file
+
+ def watch_kid(me, kid):
+ """
+ Watch the process-id KID for exit.
+
+ If the child dies abnormally then a message is written to the window.
+ """
+ XT.addreason()
+ Reaper.reaper().add(kid, me._exit)
+
+ def make_window(me):
+ """
+ Construct the output window if necessary.
+
+ Having the window open is a reason to continue.
+ """
+
+ ## If the window exists, just make sure it's visible.
+ if me._window is not None:
+ if me._openp == False:
+ me._window.present()
+ XT.addreason()
+ me._openp = True
+ return
+
+ ## Make the buffer.
+ buf = GTK.TextBuffer()
+ me._deftag = buf.create_tag('default')
+ if me.font is not None:
+ me._deftag.set_properties(font = me.font)
+ me._exittag = \
+ buf.create_tag('exit', style_set = True, style = P.STYLE_ITALIC)
+ me._buf = buf
+
+ ## Make the window.
+ win = GTK.Window(GTK.WINDOW_TOPLEVEL)
+ win.set_title(me.title)
+ win.connect('delete-event', me._delete)
+ win.connect('key-press-event', me._keypress)
+ view = GTK.TextView(buf)
+ view.set_editable(False)
+ scr = GTK.ScrolledWindow()
+ scr.set_policy(GTK.POLICY_AUTOMATIC, GTK.POLICY_AUTOMATIC)
+ scr.set_shadow_type(GTK.SHADOW_IN)
+ scr.add(view)
+ win.set_default_size(480, 200)
+ win.add(scr)
+
+ ## All done.
+ win.show_all()
+ XT.addreason()
+ me._openp = True
+ me._window = win
+
+ def _keypress(me, win, event):
+ """
+ Handle a keypress on the window.
+
+ Escape or Q will close the window.
+ """
+ key = GDK.keyval_name(event.keyval)
+ if key in ['Escape', 'q', 'Q']:
+ me._delete()
+ return True
+ return False
+
+ def _delete(me, *_):
+ """
+ Handle a close request on the window.
+
+ Closing the window removes a reason to continue.
+ """
+ me._window.hide()
+ XT.delreason()
+ me._openp = False
+
+ @complain(True)
+ def _ready(me, file, *_):
+ """
+ Process input arriving on the FILE.
+ """
+ try:
+ buf = file.read(16384)
+ except IOError, err:
+ if err.errno == E.EAGAIN:
+ return True
+ me._close()
+ me.rc = 127
+ raise
+ if buf == '':
+ me._close()
+ return True
+ me.make_window()
+ uni = buf.decode(SYS.getdefaultencoding(), 'replace')
+ utf8 = uni.encode('utf-8')
+ end = me._buf.get_end_iter()
+ me._buf.insert_with_tags(end, utf8, me._deftag)
+ return True
+
+ def _close(me):
+ """
+ Close the input file.
+ """
+ XT.delreason()
+ GO.source_remove(me._src)
+ me._file = None
+
+ def _exit(me, kid, st):
+ """
+ Handle the child process exiting.
+ """
+ if st == 0:
+ XT.delreason()
+ return
+ me.make_window()
+ XT.delreason()
+ end = me._buf.get_end_iter()
+ if not end.starts_line():
+ me._buf.insert(end, '\n')
+ if OS.WIFEXITED(st):
+ msg = 'exited with status %d' % OS.WEXITSTATUS(st)
+ me.rc = OS.WEXITSTATUS(st)
+ elif OS.WIFSIGNALED(st):
+ msg = 'killed by signal %d' % OS.WTERMSIG(st)
+ me.rc = OS.WTERMSIG(st) | 128
+ if OS.WCOREDUMP(st):
+ msg += ' (core dumped)'
+ else:
+ msg = 'exited with unknown code 0x%x' % st
+ me.rc = 255
+ me._buf.insert_with_tags(end, '\n[%s]\n' % msg,
+ me._deftag, me._exittag)
+
+###--------------------------------------------------------------------------
+### Option parsing.
+
+def parse_args():
+ """
+ Parse the command line, returning a triple (PARSER, OPTS, ARGS).
+ """
+
+ op = XT.make_optparse \
+ ([('f', 'file',
+ {'dest': 'file',
+ 'help': "Read input from FILE."}),
+ ('F', 'font',
+ {'dest': 'font',
+ 'help': "Display output using FONT."})],
+ version = VERSION,
+ usage = '%prog [-f FILE] [-F FONT] [COMMAND [ARGS...]]')
+
+ op.set_defaults(file = None,
+ font = 'monospace')
+
+ opts, args = op.parse_args()
+ if len(args) > 0 and opts.file is not None:
+ op.error("Can't read from a file and a command simultaneously.")
+ return op, opts, args
+
+###--------------------------------------------------------------------------
+### Main program.
+
+def main():
+
+ ## Check options.
+ op, opts, args = parse_args()
+
+ ## Set up the file to read from.
+ catcher = Catcher()
+ if opts.file is not None:
+ if opts.file == '-':
+ name = '<stdin>'
+ catcher.watch_file(stdin)
+ else:
+ name = file
+ catcher.watch(open(opts.file, 'r'))
+ elif len(args) == 0:
+ name = '<stdin>'
+ catcher.watch_file(stdin)
+ else:
+ name = ' '.join(args)
+ Reaper.reaper()
+ proc = S.Popen(args, stdout = S.PIPE, stderr = S.STDOUT)
+ catcher.watch_file(proc.stdout)
+ catcher.watch_kid(proc.pid)
+
+ catcher.title = 'xcatch: ' + name
+ catcher.font = opts.font
+
+ ## Let things run their course.
+ GTK.main()
+ exit(catcher.rc)
+
+if __name__ == '__main__':
+ main()
+
+###----- That's all, folks --------------------------------------------------
xgetline \- request a line of text in an X dialogue box
.SH SYNOPSIS
.B xgetline
-.RB [ -in ]
-.RB [ \-t
-.IR title ]
-.RB [ \-p
-.IR prompt ]
+.RI [ gtk-options ...]
+.RB [ -Hin ]
+.RB [ \- M
+.IR max ]
.RB [ \-d
.IR default ]
-.RB [ \-l | \-H
+.RB [ \-l
.IR file ]
-.RB [ \- m
-.IR max ]
+.RB [ \-p
+.IR prompt ]
+.RB [ \-t
+.IR title ]
.SH DESCRIPTION
The
.B xgetline
.RS 5
.ft B
.nf
-cmd=`xgetline -t "Shell command in window" -p "Command:"` &&
+cmd=$(xgetline -t "Shell command in window" -p "Command:") &&
xterm -T "$cmd" -e sh -c "$cmd"
.ft R
.fi
.SS Options
.TP 5
-.B \-i, \-\-invisible
-Don't echo characters to the screen when they're typed. Useful when
-requesting passwords and similar secrets.
+.B "\-H, \-\-history"
+With
+.BR \-l ,
+update the file with the newly entered line at the top. Other lines
+matching the newly entered string are not written. No effect without
+.BR \-l .
.TP 5
-.BI "\-t, \-\-title " title
-Sets the title of the dialogue box to
-.IR title .
-The default title is
-.RB ` "Input request" '.
-.TP 5
-.BI "\-p, \-\-prompt " prompt
-Sets the prompt string in the dialogue box to
-.IR prompt .
-The default is to have no prompt string.
+.BI "\-M, \-\-histmax " max
+When writing an updated history file, do not write more than
+.I max
+lines. The default is 20; a value of 0 disables a length limit on the
+history file.
.TP 5
.BI "\-d, \-\-default " default
Sets the default text in the entry field to
sets the default to be the first item in the history list, if one is
supplied.
.TP 5
+.B \-i, \-\-invisible
+Don't echo characters to the screen when they're typed. Useful when
+requesting passwords and similar secrets.
+.TP 5
.BI "\-l, \-\-list " file
Reads a list of alternatives from
.I file
One of the items from the selection list must be chosen; the user may
not type an entry in directly.
.TP 5
-.BI "\-H, \-\-history " file
-Reads a file and displays the contents in a drop-down list, as for
-.B \-\-list
-above. Once the user has entered a string, a new list written to
-.I file
-containing the newly entered string as the first item; other lines
-matching the newly entered string are not written.
+.BI "\-p, \-\-prompt " prompt
+Sets the prompt string in the dialogue box to
+.IR prompt .
+The default is to have no prompt string.
.TP 5
-.BI "\-m, \-\-histmax " max
-When writing an updated history file, do not write more than
-.I max
-lines. The default is 20; a value of 0 disables a length limit on the
-history file.
+.BI "\-t, \-\-title " title
+Sets the title of the dialogue box to
+.IR title .
+The default title is
+.RB ` "Input request" '.
.SH BUGS
Hopefully none.
+.SH SEE ALSO
+.BR gtk-options (7).
.SH AUTHOR
Mark Wooding (mdw@distorted.org.uk).
--- /dev/null
+#! @PYTHON@
+### -*-python-*-
+###
+### Utility module for xtoys Python programs
+###
+### (c) 2007 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Edgeware XT tools collection.
+###
+### XT tools is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### XT tools is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with XT tools; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+VERSION = '@VERSION@'
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import optparse as O
+from sys import stdout, stderr, exit
+import os as OS
+import errno as E
+
+import xtoys as XT
+GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO
+
+###--------------------------------------------------------------------------
+### Entry classes.
+
+### These package up the mess involved with the different kinds of dialogue
+### box xgetline can show. The common interface is informal, but looks like
+### this:
+###
+### ready(): prepare the widget for action
+### value(): extract the value the user entered
+### setvalue(VALUE): show VALUE as the existing value in the widget
+### sethistory(HISTORY): store the HISTORY in the widget's history list
+### gethistory(): extract the history list back out again
+
+def setup_entry(entry):
+ """Standard things to do to an entry widget."""
+ entry.grab_focus()
+ entry.set_activates_default(True)
+
+class SimpleEntry (GTK.Entry):
+ """A plain old Entry widget with no bells or whistles."""
+ def setvalue(me, value):
+ me.set_text(value)
+ def ready(me):
+ setup_entry(me)
+ def value(me):
+ return me.get_text()
+ def gethistory(me):
+ return ()
+
+class ModelMixin (object):
+ """
+ A helper mixin for classes which make use of a TreeModel.
+
+ It provides the sethistory and gethistory methods for the common widget
+ interface, and can produce a CellRenderer for stuffing into the viewer
+ widget, whatever that might be.
+ """
+
+ def __init__(me):
+ """Initialize the ModelMixin."""
+ me.model = GTK.ListStore(GO.TYPE_STRING)
+ me.set_model(me.model)
+
+ def setlayout(me):
+ """Insert a CellRenderer for displaying history items into the widget."""
+ cell = GTK.CellRendererText()
+ me.pack_start(cell)
+ me.set_attributes(cell, text = 0)
+
+ def sethistory(me, history):
+ """Write the HISTORY into the model."""
+ for line in history:
+ me.model.append([line])
+
+ def gethistory(me):
+ """Extract the history from the model."""
+ return (l[0] for l in me.model)
+
+class Combo (ModelMixin, GTK.ComboBox):
+ """
+ A widget which uses a non-editable Combo box for entry.
+ """
+
+ def __init__(me):
+ """Initialize the widget."""
+ GTK.ComboBox.__init__(me)
+ ModelMixin.__init__(me)
+ me.setlayout()
+
+ def sethistory(me, history):
+ """
+ Insert the HISTORY.
+
+ We have to select some item, so it might as well be the first one.
+ """
+ ModelMixin.sethistory(me, history)
+ me.set_active(0)
+
+ def ready(me):
+ """Nothing special needed to make us ready."""
+ pass
+
+ def setvalue(me, value):
+ """
+ Store a value in the widget.
+
+ This involves finding it in the list and setting it by index. I suppose
+ I could keep a dictionary, but it seems bad to have so many copies.
+ """
+ for i in xrange(len(me.model)):
+ if me.model[i][0] == value:
+ me.set_active(i)
+
+ def value(me):
+ """Extract the current selection."""
+ return me.model[me.get_active()][0]
+
+class ComboEntry (ModelMixin, GTK.ComboBoxEntry):
+ """
+ A widget which uses an editable combo box.
+ """
+
+ def __init__(me):
+ """
+ Initialize the widget.
+ """
+ GTK.ComboBoxEntry.__init__(me)
+ ModelMixin.__init__(me)
+ me.set_text_column(0)
+
+ def ready(me):
+ """
+ Set up the entry widget.
+
+ We grab the arrow keys to step through the history.
+ """
+ setup_entry(me.child)
+ me.child.connect('key-press-event', me.press)
+
+ def press(me, _, event):
+ """
+ Handle key-press events.
+
+ Specifically, up and down to move through the history.
+ """
+ if GDK.keyval_name(event.keyval) in ('Up', 'Down'):
+ me.popup()
+ return True
+ return False
+
+ def setvalue(me, value):
+ me.child.set_text(value)
+ def value(me):
+ return me.child.get_text()
+
+###--------------------------------------------------------------------------
+### Utility functions.
+
+def chomped(lines):
+ """For each line in LINES, generate a line without trailing newline."""
+ for line in lines:
+ if line != '' and line[-1] == '\n':
+ line = line[:-1]
+ yield line
+
+###--------------------------------------------------------------------------
+### Create the window.
+
+def escape(_, event, win):
+ """Key-press handler: on escape, destroy WIN."""
+ if GDK.keyval_name(event.keyval) == 'Escape':
+ win.destroy()
+ return True
+ return False
+
+def accept(_, entry, win):
+ """OK button handler: store user's value and end."""
+ global result
+ result = entry.value()
+ win.destroy()
+ return True
+
+def make_window(opts):
+ """
+ Make and return the main window.
+ """
+
+ ## Create the window.
+ win = GTK.Window(GTK.WINDOW_TOPLEVEL)
+ win.set_title(opts.title)
+ win.set_position(GTK.WIN_POS_MOUSE)
+ win.connect('destroy', lambda _: XT.delreason())
+
+ ## Make a horizontal box for the widgets.
+ box = GTK.HBox(spacing = 4)
+ box.set_border_width(4)
+ win.add(box)
+
+ ## If we have a prompt, insert it.
+ if opts.prompt is not None:
+ box.pack_start(GTK.Label(opts.prompt), False)
+
+ ## Choose the appropriate widget.
+ if opts.file is None:
+ entry = SimpleEntry()
+ opts.history = False
+ if opts.invisible:
+ entry.set_visibility(False)
+ else:
+ if opts.nochoice:
+ entry = Combo()
+ else:
+ entry = ComboEntry()
+ try:
+ entry.sethistory(chomped(open(opts.file, 'r')))
+ except IOError, error:
+ if error.errno == E.ENOENT and opts.history:
+ pass
+ else:
+ raise
+
+ ## Insert the widget and configure it.
+ box.pack_start(entry, True)
+ if opts.default == '@':
+ try:
+ entry.setvalue(entry.gethistory.__iter__.next())
+ except StopIteration:
+ pass
+ elif opts.default is not None:
+ entry.setvalue(opts.default)
+ entry.ready()
+
+ ## Sort out the OK button.
+ ok = GTK.Button('OK')
+ ok.set_flags(ok.flags() | GTK.CAN_DEFAULT)
+ box.pack_start(ok, False)
+ ok.connect('clicked', accept, entry, win)
+ ok.grab_default()
+
+ ## Handle escape.
+ win.connect('key-press-event', escape, win)
+
+ ## Done.
+ win.show_all()
+ return entry
+
+###--------------------------------------------------------------------------
+### Option parsing.
+
+def parse_args():
+ """
+ Parse the command line, returning a triple (PARSER, OPTS, ARGS).
+ """
+
+ op = XT.make_optparse \
+ ([('H', 'history',
+ {'action': 'store_true', 'dest': 'history',
+ 'help': "With `--list', update with new string."}),
+ ('M', 'history-max',
+ {'type': 'int', 'dest': 'histmax',
+ 'help': "Maximum number of items written back to file."}),
+ ('d', 'default',
+ {'dest': 'default',
+ 'help': "Set the default entry."}),
+ ('i', 'invisible',
+ {'action': 'store_true', 'dest': 'invisible',
+ 'help': "Don't show the user's string as it's typed."}),
+ ('l', 'list',
+ {'dest': 'file',
+ 'help': "Read FILE into a drop-down list."}),
+ ('n', 'no-choice',
+ {'action': 'store_true', 'dest': 'nochoice',
+ 'help': "No free text input: user must choose item from list."}),
+ ('p', 'prompt',
+ {'dest': 'prompt',
+ 'help': "Set the window's prompt string."}),
+ ('t', 'title',
+ {'dest': 'title',
+ 'help': "Set the window's title string."})],
+ version = VERSION,
+ usage = '%prog [-Hin] [-M HISTMAX] [-p PROMPT] [-l FILE] [-t TITLE]')
+
+ op.set_defaults(title = 'Input request',
+ invisible = False,
+ prompt = None,
+ file = None,
+ nochoice = False,
+ history = False,
+ histmax = 20)
+
+ opts, args = op.parse_args()
+ return op, opts, args
+
+###--------------------------------------------------------------------------
+### Main program.
+
+result = None
+
+def main():
+
+ ## Startup.
+ op, opts, args = parse_args()
+ if len(args) > 0:
+ op.print_usage(stderr)
+ exit(1)
+ entry = make_window(opts)
+ XT.addreason()
+ GTK.main()
+
+ ## Closedown.
+ if result is None:
+ exit(1)
+ if opts.history:
+ try:
+ new = '%s.new' % opts.file
+ out = open(new, 'w')
+ print >>out, result
+ i = 0
+ for l in entry.gethistory():
+ if opts.histmax != 0 and i >= opts.histmax:
+ break
+ if l != result:
+ print >>out, l
+ i += 1
+ out.close()
+ OS.rename(new, opts.file)
+ finally:
+ try:
+ OS.unlink(new)
+ except OSError, err:
+ if err.errno != E.ENOENT:
+ raise
+ print result
+ exit(0)
+
+if __name__ == '__main__':
+ main()
+
+###----- That's all, folks --------------------------------------------------
.SH NAME
xmsg \- pops up a message box
.SH SYNOPSIS
-.ll +5i
.B xmsg
-.RB [ \-\-display
-.IR display ]
-.RB [ \-f ]
+.RI [ gtk-options ...]
+.RB [ \-EIQWm ]
+.RB [ \-d
+.IR headline ]
.RB [ \-t
.IR title ]
-.RB [ \-c | \-d
-.IR button ]
-.if n \{\
-.br
-\h'1i'
-..
-\}
.I message
.RI [ button ...]
-.ll -5i
.SH DESCRIPTION
The
.B xmsg
.B OK
button is provided anyway.
.PP
-If the
+The user dismisses the message window by activating one of the buttons
+or just closing the window using the window manager. The
+.B xsmg
+program then writes a string to its standard output describing the
+user's action and exits. The string written is, by default, the label
+of the activated button, though this can be overridden: see below.
+.SS "Message specifications"
+The
+.I message
+argument is usually just a text string to be displayed. However, if the
.I message
is
.RB ` \- '
-then instead the message to display is read from standard input. If the
+then, instead, the message to display is read from standard input. If the
first character of
.I message
is
-.RB ` % '
-then that character is removed.
+.RB ` ! '
+then that character is removed. (Hence, if you really wanted to show
+the message
+.RB ` \- ',
+you need to pass
+.RB ` !\- '.)
+Conscientious script authors will prefix strings appropriately.
+.PP
+Pango markup may be used in message and headline strings if the
+.B \-m
+option is requested.
+.SS "Button specifications"
+A
+.I button
+argument has the form
+.RI [ opt \fB: opt \fB: ...] \c
+.RB [ ! ] \c
+.IR label .
+The
+.I label
+is either a text string, or a GTK stock-id (e.g.,
+.BR gtk-ok ).
+Mnemonic characters in button labels may be marked by prefixing them
+with underscores. Write two underscores if you really want a literal
+underscore to appear.
.PP
-A button may be selected as being the default (i.e., may be chosen by
-pressing
-.IR enter ),
-using the
-.B \-d
-option: the argument must either match a button name, or be an index
-(zero-based) of the requested button. If you don't select a default,
-the first (rightmost) button becomes the default anyway.
+Each
+.I opt
+may be one of the following.
+.TP
+.B default
+This should be the default button, activated when the user presses the
+.I enter
+or
+.I return
+key.
+.TP
+.B cancel
+This should be the cancel button, activated when the user presses the
+.I escape
+key or simply dismisses the window.
+.TP
+.BI = tag
+If the user activates this button, output the
+.I tag
+rather than the button's label.
.PP
-Similarly, a button may be selected as being the `cancel' action (i.e.,
-may be chosen by closing the window or pressing
-.IR escape ),
-using
-the
-.B \-c
-option. Again, the argument must either match a button name or be an
-index of a button. If you don't select a cancel button, the last
-(leftmost) button because the cancel button.
+If no button is marked as the default, then the rightmost (first
+specified) is chosen automatically; similarly, if there is no specified
+cancel button then the last is chosen. If several buttons are marked as
+default or cancel buttons then the behaviour is unspecified.
.PP
-If there is more than one button, the name of the selected button is
-printed on standard output when the program exits.
+Button options are usually processed while colons remain in the button
+specification. Processing stops early if an exclamation mark
+.RB ` ! '
+is reached. For example,
+.B default:!cancel:button
+is parsed has specifying the
+.B default
+option and a label text of
+.BR cancel:button .
+.PP
+If no
+.I button
+arguments are given,
+.B xmsg
+automatically provides an OK button (it actually uses the GTK
+.B gtk-ok
+stock button) but produces no output.
.SS Options
-.TP 5
-.BI "\-\-display " display
-Attempt to connect to
-.I display
-rather than the display named in the usual environment variable.
-.TP 5
-.B "\-f, \-\-focus "
-Sets a magical property to ensure that the window acquires the focus
-under a customized
-.BR fvwm (3)
-version I don't use any more.
-.TP 5
+.TP
+.BR \-E ", " \-I ", " \-Q ", " \-W
+Mark the message window as, respectively, reporting an error, providing
+information, asking a question, or giving a warning.
+.TP
+.BR "\-d, \-\-headline " headline
+Write the
+.I headline
+above the main message, in larger and bolder text.
+.TP
+.B "\-m, \-\-markup"
+Enable the use of Pango XML-like markup in the message and headline
+strings. See the Pango documentation for a description of the markup
+tags available.
+.TP
.BI "\-t, \-\-title " title
Sets the title for the window. If you don't specify a title, the window
is labelled
.RB ` xmsg '.
-.TP 5
-.BI "\-c, \-\-cancel " button
-Selects the given
-.I button
-as being the cancel button.
-.TP 5
-.BI "\-d, \-\-default " button
-Selects the given
-.I button
-as being the default button.
.SH BUGS
None currently known.
+.SH SEE ALSO
+.BR gtk-options (7).
.SH AUTHOR
Mark Wooding (mdw@distorted.org.uk).
--- /dev/null
+#! @PYTHON@
+### -*-python-*-
+###
+### Report a message to the user
+###
+### (c) 2008 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Edgeware X tools collection.
+###
+### X tools is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### X tools is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with X tools; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+VERSION = '@VERSION@'
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import optparse as O
+from sys import stdin, stdout, stderr, exit
+import os as OS
+import errno as E
+
+import xtoys as XT
+GTK = XT.GTK
+
+###--------------------------------------------------------------------------
+### Option parsing.
+
+def parse_args():
+ """
+ Parse the command line, returning a triple (PARSER, OPTS, ARGS).
+ """
+
+ op = XT.make_optparse \
+ ([('E', 'error',
+ {'action': 'store_const', 'dest': 'type', 'const': 'error',
+ 'help': "Mark the window as reporting an error."}),
+ ('I', 'informational',
+ {'action': 'store_const', 'dest': 'type', 'const': 'info',
+ 'help': "Mark the window as providing information."}),
+ ('Q', 'question',
+ {'action': 'store_const', 'dest': 'type', 'const': 'question',
+ 'help': "Mark the window as asking a question."}),
+ ('W', 'warning',
+ {'action': 'store_const', 'dest': 'type', 'const': 'warning',
+ 'help': "Mark the window as giving a warning."}),
+ ('d', 'headline',
+ {'dest': 'headline',
+ 'help': "Set the window's headline message."}),
+ ('m', 'markup',
+ {'action': 'store_true', 'dest': 'markupp',
+ 'help': "Parse message strings for Pango markup."}),
+ ('t', 'title',
+ {'dest': 'title',
+ 'help': "Set the window's title string."})],
+ version = VERSION,
+ usage = '%prog [-EIQWm] [-t TITLE] [-d HEADLINE] '
+ 'MESSAGE [BUTTONS...]')
+
+ op.set_defaults(title = 'xmsg',
+ type = 'info',
+ headline = None,
+ markupp = False)
+
+ opts, args = op.parse_args()
+ return op, opts, args
+
+###--------------------------------------------------------------------------
+### Main program.
+
+def main():
+ op, opts, args = parse_args()
+ if len(args) == 0:
+ op.print_usage(stderr)
+ exit(1)
+
+ ## Sort out the message.
+ message = args[0]
+ buttons = args[1:]
+ if message.startswith('!'):
+ message = message[1:]
+ elif message == '-':
+ message = stdin.read()
+
+ ## Display it and retrieve and answer.
+ try:
+ msg = XT.Message(title = opts.title,
+ type = opts.type,
+ message = message,
+ headline = opts.headline,
+ buttons = buttons,
+ markupp = opts.markupp)
+ except ValueError, err:
+ op.error(err[0])
+ result = msg.ask()
+
+ ## Done.
+ if result != True:
+ print result
+ exit(0)
+
+if __name__ == '__main__':
+ main()
+
+###----- That's all, folks --------------------------------------------------
--- /dev/null
+### -*-python-*-
+###
+### Utility module for xtoys Python programs
+###
+### (c) 2007 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Edgeware X tools collection.
+###
+### X tools is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### X tools is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with X tools; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import os as OS
+import optparse as O
+from sys import stdin, stdout, exit, argv
+
+import pygtk
+pygtk.require('2.0')
+import gtk as GTK
+GDK = GTK.gdk
+import gobject as GO
+del pygtk
+
+###--------------------------------------------------------------------------
+### Reasons for living.
+
+_reasons = 0
+
+def addreason():
+ """Add a reason."""
+ global _reasons
+ _reasons += 1
+
+def delreason():
+ """Drop a reason. When reasons reach zero, the main loop stops."""
+ global _reasons
+ _reasons -= 1
+ if _reasons == 0:
+ GTK.main_quit()
+
+###--------------------------------------------------------------------------
+### General utilities.
+
+def make_optparse(options, **kw):
+ """
+ Construct an option parser object.
+
+ The KW are keyword arguments to be passed to OptionParser. The OPTIONS are
+ a list of (SHORT, LONG, KW) triples; the SHORT and LONG strings do /not/
+ have leading dashes.
+ """
+ op = O.OptionParser(**kw)
+ for short, long, kw in options:
+ names = ['--%s' % long]
+ if short is not None:
+ names.append('-%s' % short)
+ op.add_option(*names, **kw)
+ return op
+
+###--------------------------------------------------------------------------
+### Message boxes.
+
+class MessageButton (object):
+ """
+ An object storing information about a button in a Message.
+ """
+
+ def __init__(me, label, value = None, *options):
+ """
+ Initialize a button definition.
+
+ If LABEL is a tuple, then it should have the form
+ (LABEL, VALUE, OPTIONS...); the initialization parser is applied to its
+ contents. This is not done recursively.
+
+ If LABEL is a string, and VALUE and OPTIONS are omitted, then it is
+ parsed as OPT:OPT:...:LABEL, where OPT is either an option (see below) or
+ `=VALUE'. Only one VALUE may be given.
+
+ The LABEL is the label to put on the button, or the GTK stock id. The
+ VALUE is the value to return from Message.ask if the button is chosen.
+ The OPTIONS are:
+
+ * 'default': this is the default button
+ * 'cancel': this is the cancel button
+ """
+ if value is not None or len(options) > 0:
+ me._doinit(label, value, *options)
+ elif isinstance(label, tuple):
+ me._doinit(*label)
+ else:
+ i = 0
+ options = []
+ while 0 <= i < len(label):
+ if label[i] == '!':
+ i += 1
+ break
+ j = label.find(':', i)
+ if j < 0:
+ break
+ if label[i] == '=':
+ if value is not None:
+ raise ValueError, 'Duplicate value in button spec %s' % label
+ value = label[i + 1:j]
+ else:
+ options.append(label[i:j])
+ i = j + 1
+ label = label[i:]
+ me._doinit(label, value, *options)
+
+ def _doinit(me, label, value = None, *options):
+ """
+ Does the work of processing the initialization parameters.
+ """
+ me.label = label
+ if value is None:
+ me.value = label
+ else:
+ me.value = value
+ me.defaultp = me.cancelp = False
+ for opt in options:
+ if opt == 'default':
+ me.defaultp = True
+ elif opt == 'cancel':
+ me.cancelp = True
+ else:
+ raise ValueError, 'unknown button option %s' % opt
+
+class Message (GTK.MessageDialog):
+ """
+ A simple message-box window: contains text and some buttons.
+
+ See __init__ for the usage instructions.
+ """
+
+ ## Mapping from Pythonic strings to GTK constants.
+ dboxtype = {'info': GTK.MESSAGE_INFO,
+ 'warning': GTK.MESSAGE_WARNING,
+ 'question': GTK.MESSAGE_QUESTION,
+ 'error': GTK.MESSAGE_ERROR}
+
+ def __init__(me,
+ title = None,
+ type = 'info',
+ message = '',
+ headline = None,
+ buttons = [],
+ markupp = False):
+ """
+ Report a message to the user and get a response back.
+
+ The TITLE is placed in the window's title bar.
+
+ The TYPE controls what kind of icon is placed in the window; it should be
+ one of 'info', 'warning', 'question' or 'error'.
+
+ The MESSAGE is the string which should be displayed. It may have
+ multiple lines. There may also be a HEADLINE message. The messages are
+ parsed for Pango markup if MARKUPP is set.
+
+ The BUTTONS are a list of buttons to show, right to left. Each one
+ should be a string which may either be a GTK stock tag or a plain label
+ string. This may be prefixed, optionally, by `!' to ignore other prefix
+ characters, `+' to make this button the default, or `:' to make this the
+ `cancel' button, chosen by pressing escape. If no button is marked as
+ cancel, we just use the first.
+ """
+
+ ## Initialize superclasses.
+ GTK.MessageDialog.__init__(me, type = me.dboxtype.get(type))
+
+ ## Set the title.
+ if title is None:
+ title = OS.path.basename(argv[0])
+ me.set_title(title)
+
+ ## Add the message strings.
+ me.set_property('use-markup', markupp)
+ if headline is None:
+ me.set_property('text', message)
+ else:
+ me.set_property('text', headline)
+ me.set_property('secondary-text', message)
+ me.set_property('secondary-use-markup', markupp)
+
+ ## Include the buttons.
+ if len(buttons) == 0:
+ buttons = [MessageButton('gtk-ok', True, 'default', 'cancel')]
+ else:
+ buttons = buttons[:]
+ buttons.reverse()
+ me.buttons = [isinstance(b, MessageButton) and b or MessageButton(b)
+ for b in buttons]
+ cancel = -1
+ default = -1
+ for i in xrange(len(buttons)):
+ button = me.buttons[i]
+ label = button.label
+ if GTK.stock_lookup(label) is None:
+ b = GTK.Button(label = label)
+ else:
+ b = GTK.Button(stock = label)
+ if button.defaultp:
+ default = i
+ if button.cancelp:
+ cancel = i
+ b.set_flags(b.flags() | GTK.CAN_DEFAULT)
+ button.widget = b
+ me.add_action_widget(b, i)
+
+ ## Choose default buttons.
+ if cancel == -1:
+ cancel = 0
+ if default == -1:
+ default = len(me.buttons) - 1
+ me.cancel = cancel
+ me.default = default
+
+ ## Connect up the handlers and go.
+ me.connect('key-press-event', me.keypress, me.buttons[cancel])
+ me.buttons[default].widget.grab_default()
+
+ def keypress(me, win, event, cancel):
+ """
+ Handle key-press events.
+
+ If the EVENT was an escape-press, then activate the CANCEL button.
+ """
+ key = GDK.keyval_name(event.keyval)
+ if key != 'Escape':
+ return False
+ cancel.widget.activate()
+ return True
+
+ def ask(me):
+ """
+ Display the message, and wait for a response.
+
+ The return value is the label of the button which was chosen.
+ """
+ me.show_all()
+ r = me.run()
+ if r < 0:
+ r = me.cancel
+ me.hide()
+ return me.buttons[r].value
+
+###----- That's all, folks --------------------------------------------------