Release 1.6.0.
[xtoys] / xgetline.in
CommitLineData
bce8c6ee
MW
1#! @PYTHON@
2### -*-python-*-
3###
4### Utility module for xtoys Python programs
5###
6### (c) 2007 Straylight/Edgeware
7###
8
9###----- Licensing notice ---------------------------------------------------
10###
11### This file is part of the Edgeware XT tools collection.
12###
13### XT tools is free software; you can redistribute it and/or modify
14### it under the terms of the GNU General Public License as published by
15### the Free Software Foundation; either version 2 of the License, or
16### (at your option) any later version.
17###
18### XT tools is distributed in the hope that it will be useful,
19### but WITHOUT ANY WARRANTY; without even the implied warranty of
20### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21### GNU General Public License for more details.
22###
23### You should have received a copy of the GNU General Public License
24### along with XT tools; if not, write to the Free Software Foundation,
25### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
27VERSION = '@VERSION@'
28
29###--------------------------------------------------------------------------
30### External dependencies.
31
32import optparse as O
33from sys import stdout, stderr, exit
34import os as OS
35import errno as E
36
37import xtoys as XT
38GTK, GDK, GO = XT.GTK, XT.GDK, XT.GO
39
40###--------------------------------------------------------------------------
41### Entry classes.
42
43### These package up the mess involved with the different kinds of dialogue
44### box xgetline can show. The common interface is informal, but looks like
45### this:
46###
47### ready(): prepare the widget for action
48### value(): extract the value the user entered
49### setvalue(VALUE): show VALUE as the existing value in the widget
50### sethistory(HISTORY): store the HISTORY in the widget's history list
51### gethistory(): extract the history list back out again
52
53def setup_entry(entry):
54 """Standard things to do to an entry widget."""
55 entry.grab_focus()
56 entry.set_activates_default(True)
57
58class SimpleEntry (GTK.Entry):
59 """A plain old Entry widget with no bells or whistles."""
60 def setvalue(me, value):
61 me.set_text(value)
62 def ready(me):
63 setup_entry(me)
64 def value(me):
65 return me.get_text()
66 def gethistory(me):
67 return ()
68
69class ModelMixin (object):
70 """
71 A helper mixin for classes which make use of a TreeModel.
72
73 It provides the sethistory and gethistory methods for the common widget
74 interface, and can produce a CellRenderer for stuffing into the viewer
75 widget, whatever that might be.
76 """
77
78 def __init__(me):
79 """Initialize the ModelMixin."""
80 me.model = GTK.ListStore(GO.TYPE_STRING)
81 me.set_model(me.model)
82
83 def setlayout(me):
84 """Insert a CellRenderer for displaying history items into the widget."""
85 cell = GTK.CellRendererText()
86 me.pack_start(cell)
87 me.set_attributes(cell, text = 0)
88
89 def sethistory(me, history):
90 """Write the HISTORY into the model."""
91 for line in history:
92 me.model.append([line])
93
94 def gethistory(me):
95 """Extract the history from the model."""
96 return (l[0] for l in me.model)
97
98class Combo (ModelMixin, GTK.ComboBox):
99 """
100 A widget which uses a non-editable Combo box for entry.
101 """
102
103 def __init__(me):
104 """Initialize the widget."""
105 GTK.ComboBox.__init__(me)
106 ModelMixin.__init__(me)
107 me.setlayout()
108
109 def sethistory(me, history):
110 """
111 Insert the HISTORY.
112
113 We have to select some item, so it might as well be the first one.
114 """
115 ModelMixin.sethistory(me, history)
116 me.set_active(0)
117
118 def ready(me):
119 """Nothing special needed to make us ready."""
120 pass
121
122 def setvalue(me, value):
123 """
124 Store a value in the widget.
125
126 This involves finding it in the list and setting it by index. I suppose
127 I could keep a dictionary, but it seems bad to have so many copies.
128 """
129 for i in xrange(len(me.model)):
130 if me.model[i][0] == value:
131 me.set_active(i)
132
133 def value(me):
134 """Extract the current selection."""
135 return me.model[me.get_active()][0]
136
137class ComboEntry (ModelMixin, GTK.ComboBoxEntry):
138 """
139 A widget which uses an editable combo box.
140 """
141
142 def __init__(me):
143 """
144 Initialize the widget.
145 """
146 GTK.ComboBoxEntry.__init__(me)
147 ModelMixin.__init__(me)
148 me.set_text_column(0)
149
150 def ready(me):
151 """
152 Set up the entry widget.
153
154 We grab the arrow keys to step through the history.
155 """
156 setup_entry(me.child)
157 me.child.connect('key-press-event', me.press)
158
159 def press(me, _, event):
160 """
161 Handle key-press events.
162
163 Specifically, up and down to move through the history.
164 """
165 if GDK.keyval_name(event.keyval) in ('Up', 'Down'):
166 me.popup()
167 return True
168 return False
169
170 def setvalue(me, value):
171 me.child.set_text(value)
172 def value(me):
173 return me.child.get_text()
174
175###--------------------------------------------------------------------------
176### Utility functions.
177
178def chomped(lines):
179 """For each line in LINES, generate a line without trailing newline."""
180 for line in lines:
181 if line != '' and line[-1] == '\n':
182 line = line[:-1]
183 yield line
184
185###--------------------------------------------------------------------------
186### Create the window.
187
188def escape(_, event, win):
189 """Key-press handler: on escape, destroy WIN."""
190 if GDK.keyval_name(event.keyval) == 'Escape':
191 win.destroy()
192 return True
193 return False
194
195def accept(_, entry, win):
196 """OK button handler: store user's value and end."""
197 global result
198 result = entry.value()
199 win.destroy()
200 return True
201
202def make_window(opts):
203 """
204 Make and return the main window.
205 """
206
207 ## Create the window.
208 win = GTK.Window(GTK.WINDOW_TOPLEVEL)
209 win.set_title(opts.title)
210 win.set_position(GTK.WIN_POS_MOUSE)
211 win.connect('destroy', lambda _: XT.delreason())
212
213 ## Make a horizontal box for the widgets.
214 box = GTK.HBox(spacing = 4)
215 box.set_border_width(4)
216 win.add(box)
217
bce8c6ee
MW
218 ## Choose the appropriate widget.
219 if opts.file is None:
220 entry = SimpleEntry()
221 opts.history = False
222 if opts.invisible:
223 entry.set_visibility(False)
224 else:
225 if opts.nochoice:
226 entry = Combo()
227 else:
228 entry = ComboEntry()
229 try:
230 entry.sethistory(chomped(open(opts.file, 'r')))
231 except IOError, error:
232 if error.errno == E.ENOENT and opts.history:
233 pass
234 else:
235 raise
236
583efdd3
MW
237 ## If we have a prompt, insert it.
238 if opts.prompt is not None:
239 label = GTK.Label(opts.prompt)
240 label.set_properties(mnemonic_widget = entry,
241 use_underline = True)
242 box.pack_start(label, False)
243
bce8c6ee
MW
244 ## Insert the widget and configure it.
245 box.pack_start(entry, True)
246 if opts.default == '@':
247 try:
248 entry.setvalue(entry.gethistory.__iter__.next())
249 except StopIteration:
250 pass
251 elif opts.default is not None:
252 entry.setvalue(opts.default)
253 entry.ready()
254
255 ## Sort out the OK button.
256 ok = GTK.Button('OK')
257 ok.set_flags(ok.flags() | GTK.CAN_DEFAULT)
258 box.pack_start(ok, False)
259 ok.connect('clicked', accept, entry, win)
260 ok.grab_default()
261
262 ## Handle escape.
263 win.connect('key-press-event', escape, win)
264
265 ## Done.
266 win.show_all()
267 return entry
268
269###--------------------------------------------------------------------------
270### Option parsing.
271
272def parse_args():
273 """
274 Parse the command line, returning a triple (PARSER, OPTS, ARGS).
275 """
276
277 op = XT.make_optparse \
278 ([('H', 'history',
279 {'action': 'store_true', 'dest': 'history',
280 'help': "With `--list', update with new string."}),
281 ('M', 'history-max',
282 {'type': 'int', 'dest': 'histmax',
283 'help': "Maximum number of items written back to file."}),
284 ('d', 'default',
285 {'dest': 'default',
286 'help': "Set the default entry."}),
287 ('i', 'invisible',
288 {'action': 'store_true', 'dest': 'invisible',
289 'help': "Don't show the user's string as it's typed."}),
290 ('l', 'list',
291 {'dest': 'file',
292 'help': "Read FILE into a drop-down list."}),
293 ('n', 'no-choice',
294 {'action': 'store_true', 'dest': 'nochoice',
295 'help': "No free text input: user must choose item from list."}),
296 ('p', 'prompt',
297 {'dest': 'prompt',
298 'help': "Set the window's prompt string."}),
299 ('t', 'title',
300 {'dest': 'title',
301 'help': "Set the window's title string."})],
302 version = VERSION,
303 usage = '%prog [-Hin] [-M HISTMAX] [-p PROMPT] [-l FILE] [-t TITLE]')
304
305 op.set_defaults(title = 'Input request',
306 invisible = False,
307 prompt = None,
308 file = None,
309 nochoice = False,
310 history = False,
311 histmax = 20)
312
313 opts, args = op.parse_args()
314 return op, opts, args
315
316###--------------------------------------------------------------------------
317### Main program.
318
319result = None
320
321def main():
322
323 ## Startup.
324 op, opts, args = parse_args()
325 if len(args) > 0:
326 op.print_usage(stderr)
327 exit(1)
328 entry = make_window(opts)
329 XT.addreason()
330 GTK.main()
331
332 ## Closedown.
333 if result is None:
334 exit(1)
335 if opts.history:
336 try:
337 new = '%s.new' % opts.file
338 out = open(new, 'w')
339 print >>out, result
340 i = 0
341 for l in entry.gethistory():
342 if opts.histmax != 0 and i >= opts.histmax:
343 break
344 if l != result:
345 print >>out, l
346 i += 1
347 out.close()
348 OS.rename(new, opts.file)
349 finally:
350 try:
351 OS.unlink(new)
352 except OSError, err:
353 if err.errno != E.ENOENT:
354 raise
355 print result
356 exit(0)
357
358if __name__ == '__main__':
359 main()
360
361###----- That's all, folks --------------------------------------------------