fill-form accepts dot as a placeholder type-nothing parameter
[hband-tools.git] / xgui-tools / gtail
blob9a49b3576d0368209c494cf20d696f87400d3013
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 import os
5 import sys
6 import signal
7 import select
8 import fcntl
9 import gtk
10 import gobject
11 import pango
12 import gettext
13 _ = gettext.gettext
14 import threading
15 gtk.threads_init()
16 import argparse
17 import time
18 import glib
22 class TailThread(threading.Thread):
23 def __init__(self, filenames, tail_opts=[]):
24 threading.Thread.__init__(self)
25 self.filenames = filenames
26 self.tail_opts = tail_opts
27 self.tail_pid = None
28 self.fork_event = threading.Event()
29 self.exiting = False
30 def kill(self):
31 if self.tail_pid is None:
32 self.fork_event.wait()
33 if self.tail_pid is not None:
34 self.exiting = True
35 try:
36 os.kill(self.tail_pid, signal.SIGINT)
37 except OSError:
38 pass
39 def pause(self):
40 if self.tail_pid is None:
41 self.fork_event.wait()
42 try:
43 os.kill(self.tail_pid, signal.SIGSTOP)
44 except OSError, e:
45 if e.errno != os.errno.ESRCH:
46 # No such process
47 raise
48 def unpause(self):
49 if self.tail_pid is None:
50 self.fork_event.wait()
51 try:
52 os.kill(self.tail_pid, signal.SIGCONT)
53 except OSError, e:
54 display_error(e)
55 self.tail_pid = None
56 def run(self):
57 gtk.threads_enter()
58 n_lines = spin.get_text()
59 gtk.threads_leave()
61 pipe_read, pipe_write = os.pipe()
62 pipe_read = os.fdopen(pipe_read, 'r')
63 pipe_write = os.fdopen(pipe_write, 'w')
64 pid = os.fork()
65 if pid == 0:
66 pipe_read.close()
67 os.dup2(pipe_write.fileno(), sys.stdout.fileno())
68 #os.dup2(pipe_write.fileno(), sys.stderr.fileno())
69 try:
70 #sys.stderr.write(">> tail -f -F -n %s %s\n" % (n_lines, self.filename))
71 os.execvp('tail', ['-f', '-F', '-n', n_lines] + self.tail_opts + self.filenames)
72 except OSError, exc:
73 pass
74 os._exit(127)
75 else:
76 self.tail_pid = pid
77 self.fork_event.set()
78 pipe_write.close()
79 gtk.threads_enter()
80 textbuffer.set_text('')
81 btn_conn.set_active(True)
82 set_tooltip_btn_conn()
83 gtk.threads_leave()
85 info = {}
86 set_blocking(pipe_read, True)
87 while waitpid_but_running(pid, info):
88 inputs = [pipe_read]
89 text_append = ''
90 readable, x, x = select.select(inputs, [], [], 1.0)
91 if pipe_read in readable:
92 try:
93 # tail(1) streams line-by-line
94 line = pipe_read.readline()
95 if line == '': break
96 except IOError, exc:
97 self.kill()
98 break
99 text_append += line
100 if text_append:
101 gtk.threads_enter()
102 append_lines(text_append)
103 limit_lines()
104 gtk.threads_leave()
105 pipe_read.close()
106 if not self.exiting:
107 gtk.threads_enter()
108 display_error("tail exited with code %s%s." %(info['code'], (",\nkilled by signal %s" %(signame(info['signal']),) if info['signal'] else '.')))
109 gtk.threads_leave()
110 return
112 def waitpid_but_running(expect_pid, info):
113 pid, status = os.waitpid(expect_pid, os.WNOHANG)
114 if pid == expect_pid:
115 info['signal'] = status & 0xFF
116 info['code'] = status >> 8
117 return False
118 return True
120 def signame(n):
121 for name, num in signal.__dict__.iteritems():
122 if name.startswith('SIG') and num == n:
123 return name
125 def set_blocking(fd, on):
126 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
127 if on:
128 mask = fl & ~os.O_NONBLOCK
129 else:
130 mask = fl | os.O_NONBLOCK
131 fcntl.fcntl(fd, fcntl.F_SETFL, mask)
133 def main_win_show(widget, event):
134 win.disconnect(start_evt)
135 start_tail_thread()
136 win_set_title()
138 def add_key_binding(widget, keyname, callback):
139 accelgroup = gtk.AccelGroup()
140 key, modifier = gtk.accelerator_parse(keyname)
141 accelgroup.connect_group(key, modifier, gtk.ACCEL_VISIBLE, callback)
142 widget.add_accel_group(accelgroup)
144 def stop_tail_thread():
145 if Global['Thread'] is not None:
146 gtk.threads_leave()
147 Global['Thread'].kill()
148 Global['Thread'].join()
149 gtk.threads_enter()
151 def start_tail_thread(wdg=None):
152 stop_tail_thread()
153 tail_opts = []
154 for opt in 'bytes', 'pid', 'quiet', 'retry', 'verbose':
155 val = getattr(Args, opt)
156 if val:
157 tail_opts.append('--'+opt)
158 if val is not True:
159 tail_opts.append(str(val))
160 Global['Thread'] = TailThread(Args.FILE, tail_opts)
161 Global['Thread'].start()
163 def append_lines(text):
164 textbuffer.insert(textbuffer.get_end_iter(), text)
166 def limit_lines(corrig=0):
167 clns = textbuffer.get_line_count() + corrig
168 rlns = int(spin.get_text())
169 if clns > rlns:
170 vadjustment.handler_block(vadjustment.get_data('handler-value-changed'))
171 textbuffer.delete(textbuffer.get_start_iter(), textbuffer.get_iter_at_line(clns - rlns))
172 glib.idle_add(lambda: vadjustment.handler_unblock(vadjustment.get_data('handler-value-changed')), priority=glib.PRIORITY_DEFAULT_IDLE)
174 def lines_changed(wdg):
175 limit_lines(-1)
177 def lines_changed_restart(wdg):
178 timer = wdg.get_data('timer')
179 if timer is not None:
180 gobject.source_remove(timer)
181 timer = gobject.timeout_add(1000, start_tail_thread)
182 wdg.set_data('timer', timer)
184 def cb_connect(wdg):
185 if wdg.get_active():
186 Global['Thread'].unpause()
187 else:
188 Global['Thread'].pause()
189 set_tooltip_btn_conn()
191 def set_tooltip_btn_conn():
192 if btn_conn.get_active():
193 btn_conn.set_stock_id(gtk.STOCK_CONNECT)
194 btn_conn.set_tooltip_text(_("Pause"))
195 else:
196 btn_conn.set_stock_id(gtk.STOCK_DISCONNECT)
197 btn_conn.set_tooltip_text(_("Unpause and watch"))
199 def scrollbar_adjusted(vadj):
200 pos = vadj.get_value()
201 bottom = vadj.get_upper() - vadj.get_page_size()
202 btn_auto.set_active(pos >= bottom)
204 def scroll_down():
205 vadjustment.set_value(vadjustment.get_upper() - vadjustment.get_page_size())
207 def scroll_down_conditional(*x):
208 if btn_auto.get_active():
209 scroll_down()
211 def cb_click_follow(*ignore):
212 scroll_down_conditional()
214 def toggle_follow(*ignore):
215 btn_auto.set_active(not btn_auto.get_active())
216 scroll_down_conditional()
218 def do_connect(*ignore):
219 btn_conn.set_active(True)
221 def dont_connect(*ignore):
222 btn_conn.set_active(False)
224 def display_error(e):
225 text = None
226 if isinstance(e, OSError) or isinstance(e, IOError):
227 text = e.strerror + " (#" + str(e.errno) + ")\n" + str(e.filename)
228 elif isinstance(e, Exception):
229 text = e.message
230 elif type(e) == type([]):
231 text = ''.join(e)
232 if text is None:
233 text = str(e)
234 dlg = gtk.MessageDialog(win, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, text)
235 dlg.run()
236 dlg.destroy()
238 def open_new_file(*ignore):
239 dialog = gtk.FileChooserDialog(_("Select a file"), None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
240 dialog.set_action(gtk.FILE_CHOOSER_ACTION_OPEN)
241 if len(Args.FILE) == 1:
242 initdir = os.path.dirname(Args.FILE[0])
243 else:
244 initdir = os.getcwd()
245 dialog.set_current_folder(initdir)
246 response = dialog.run()
247 if response == gtk.RESPONSE_OK:
248 Args.FILE = [dialog.get_filename()]
249 start_tail_thread()
250 win_set_title()
251 dialog.destroy()
253 def on_button_press(wdg, event, spec):
254 if event.type == gtk.gdk.BUTTON_PRESS:
255 for num, key in (1, 'left'), (2, 'middle'), (3, 'right'):
256 if event.button == num and spec.has_key(key):
257 func = spec[key][0]
258 args = spec[key][1]
259 func(wdg, *args)
261 def set_font_size_delta(wdg, delta):
262 pt = Global['Font']['Size'] + delta
263 if pt < 1: pt = 1
264 set_font_size(wdg, pt)
266 def set_font_size(wdg, pt):
267 Global['Font']['Size'] = pt
268 fontdesc = pango.FontDescription('monospace %d' % Global['Font']['Size'])
269 textview.modify_font(fontdesc)
271 def win_set_title():
272 win.set_title('; '.join(Args.FILE))
274 def bye(*ignore):
275 stop_tail_thread()
276 gtk.main_quit()
278 def cb_lines_inc(*ignore):
279 adjus.set_value(adjus.get_value()+1)
280 def cb_lines_dec(*ignore):
281 adjus.set_value(adjus.get_value()-1)
282 def cb_font_enlarge(*ignore):
283 set_font_size_delta(None, +1)
284 def cb_font_minify(*ignore):
285 set_font_size_delta(None, -1)
286 def cb_font_default(*ignore):
287 set_font_size(None, 12)
291 Global = {'Thread': None, 'Font': {'Size': 12,},}
293 argparser = argparse.ArgumentParser(epilog=_("See tail(1), `tail --helpĀ“"))
294 argparser.add_argument('-c', '--bytes', metavar='BYTES', type=int)
295 argparser.add_argument('-n', '--lines', metavar='LINES', type=int, default=10)
296 argparser.add_argument('--pid', metavar='PID', type=int)
297 argparser.add_argument('-q', '--quiet', '--silent', action='store_true')
298 argparser.add_argument('--retry', action='store_true')
299 argparser.add_argument('-v', '--verbose', action='store_true')
300 argparser.add_argument('FILE', nargs='*', help=_("File(s) to watch"))
301 Args = argparser.parse_args()
302 if len(Args.FILE) < 1:
303 Args.FILE.append('/dev/stdin')
307 win = gtk.Window()
308 win.set_default_size(720, 350)
309 win.connect('delete-event', bye)
310 start_evt = win.connect('map-event', main_win_show)
311 add_key_binding(win, 'Escape', bye)
312 add_key_binding(win, '<Control>C', bye)
313 add_key_binding(win, '<Control>O', open_new_file)
314 add_key_binding(win, 'F', toggle_follow)
315 add_key_binding(win, '<Shift>F', toggle_follow)
316 add_key_binding(win, 'plus', cb_font_enlarge)
317 add_key_binding(win, 'minus', cb_font_minify)
318 add_key_binding(win, 'equal', cb_font_default)
319 add_key_binding(win, 'KP_Add', cb_font_enlarge)
320 add_key_binding(win, 'KP_Subtract', cb_font_minify)
321 add_key_binding(win, 'KP_Equal', cb_font_default)
322 add_key_binding(win, '<Control>S', dont_connect)
323 add_key_binding(win, '<Control>Q', do_connect)
325 frame = gtk.VBox()
326 toolbar = toolbar = gtk.Toolbar()
327 toolbar.set_style(gtk.TOOLBAR_ICONS)
328 scroll = gtk.ScrolledWindow()
329 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
330 vadjustment = scroll.get_vadjustment()
331 vadj_handler = vadjustment.connect('value-changed', scrollbar_adjusted)
332 vadjustment.set_data('handler-value-changed', vadj_handler)
334 textbuffer = gtk.TextBuffer()
335 textview = gtk.TextView(textbuffer)
336 textview.set_editable(False)
337 textview.connect('size-allocate', scroll_down_conditional)
338 set_font_size_delta(None, 0)
340 win.add(frame)
341 frame.pack_start(toolbar, False, True, 0)
342 frame.pack_start(scroll, True, True, 0)
343 scroll.add(textview)
346 btn_close = gtk.ToolButton(gtk.STOCK_CLOSE)
347 btn_close.set_tooltip_text(_("Close"))
348 btn_close.connect('clicked', bye)
349 toolbar.insert(btn_close, -1)
350 btn_open = gtk.ToolButton(gtk.STOCK_OPEN)
351 btn_open.set_tooltip_text(_("Open..."))
352 btn_open.connect('clicked', open_new_file)
353 toolbar.insert(btn_open, -1)
354 btn_conn = gtk.ToggleToolButton()
355 set_tooltip_btn_conn()
356 btn_conn.connect('toggled', cb_connect)
357 toolbar.insert(btn_conn, -1)
358 btn_auto = gtk.ToggleToolButton(gtk.STOCK_GOTO_BOTTOM)
359 btn_auto.set_active(True)
360 btn_auto.set_tooltip_text(_("Auto Scroll"))
361 btn_auto.connect('clicked', cb_click_follow)
362 toolbar.insert(btn_auto, -1)
363 toolbar.insert(gtk.SeparatorToolItem(), -1)
364 btn_size_plus = gtk.ToolButton(gtk.STOCK_ZOOM_IN)
365 btn_size_plus.set_tooltip_text(_("Bigger font"))
366 btn_size_plus.child.connect('button-press-event', on_button_press, {'left': (set_font_size_delta, [+1])})
367 toolbar.insert(btn_size_plus, -1)
368 btn_size_dflt = gtk.ToolButton(gtk.STOCK_ZOOM_100)
369 btn_size_dflt.set_tooltip_text(_("Default size"))
370 btn_size_dflt.child.connect('button-press-event', on_button_press, {'left': (set_font_size, [12])})
371 toolbar.insert(btn_size_dflt, -1)
372 btn_size_mins = gtk.ToolButton(gtk.STOCK_ZOOM_OUT)
373 btn_size_mins.set_tooltip_text(_("Smaller font"))
374 btn_size_mins.child.connect('button-press-event', on_button_press, {'left': (set_font_size_delta, [-1])})
375 toolbar.insert(btn_size_mins, -1)
377 ti_sep = gtk.SeparatorToolItem()
378 ti_sep.set_expand(True)
379 ti_sep.set_draw(False)
380 toolbar.insert(ti_sep, -1)
382 ti_label1 = gtk.ToolItem()
383 label1 = gtk.Label(_("Lines: "))
384 ti_label1.add(label1)
385 toolbar.insert(ti_label1, -1)
387 ti_spin = gtk.ToolItem()
388 adjus = gtk.Adjustment(value=Args.lines, lower=1, upper=9999, step_incr=1, page_incr=10)
389 spin = gtk.SpinButton(adjus)
390 spin.set_numeric(True)
391 spin.set_alignment(1)
392 spin.connect('value-changed', lines_changed)
393 ti_spin.add(spin)
394 toolbar.insert(ti_spin, -1)
397 win.show_all()
398 gtk.main()