2 # -*- coding: utf-8 -*-
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
28 self
.fork_event
= threading
.Event()
31 if self
.tail_pid
is None:
32 self
.fork_event
.wait()
33 if self
.tail_pid
is not None:
36 os
.kill(self
.tail_pid
, signal
.SIGINT
)
40 if self
.tail_pid
is None:
41 self
.fork_event
.wait()
43 os
.kill(self
.tail_pid
, signal
.SIGSTOP
)
45 if e
.errno
!= os
.errno
.ESRCH
:
49 if self
.tail_pid
is None:
50 self
.fork_event
.wait()
52 os
.kill(self
.tail_pid
, signal
.SIGCONT
)
58 n_lines
= spin
.get_text()
61 pipe_read
, pipe_write
= os
.pipe()
62 pipe_read
= os
.fdopen(pipe_read
, 'r')
63 pipe_write
= os
.fdopen(pipe_write
, 'w')
67 os
.dup2(pipe_write
.fileno(), sys
.stdout
.fileno())
68 #os.dup2(pipe_write.fileno(), sys.stderr.fileno())
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
)
80 textbuffer
.set_text('')
81 btn_conn
.set_active(True)
82 set_tooltip_btn_conn()
86 set_blocking(pipe_read
, True)
87 while waitpid_but_running(pid
, info
):
90 readable
, x
, x
= select
.select(inputs
, [], [], 1.0)
91 if pipe_read
in readable
:
93 # tail(1) streams line-by-line
94 line
= pipe_read
.readline()
102 append_lines(text_append
)
108 display_error("tail exited with code %s%s." %(info
['code'], (",\nkilled by signal %s" %(signame(info
['signal']),) if info
['signal'] else '.')))
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
121 for name
, num
in signal
.__dict
__.iteritems():
122 if name
.startswith('SIG') and num
== n
:
125 def set_blocking(fd
, on
):
126 fl
= fcntl
.fcntl(fd
, fcntl
.F_GETFL
)
128 mask
= fl
& ~os
.O_NONBLOCK
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
)
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:
147 Global
['Thread'].kill()
148 Global
['Thread'].join()
151 def start_tail_thread(wdg
=None):
154 for opt
in 'bytes', 'pid', 'quiet', 'retry', 'verbose':
155 val
= getattr(Args
, opt
)
157 tail_opts
.append('--'+opt
)
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())
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
):
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
)
186 Global
['Thread'].unpause()
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"))
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
)
205 vadjustment
.set_value(vadjustment
.get_upper() - vadjustment
.get_page_size())
207 def scroll_down_conditional(*x
):
208 if btn_auto
.get_active():
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
):
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):
230 elif type(e
) == type([]):
234 dlg
= gtk
.MessageDialog(win
, gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
, text
)
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])
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()]
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
):
261 def set_font_size_delta(wdg
, delta
):
262 pt
= Global
['Font']['Size'] + delta
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
)
272 win
.set_title('; '.join(Args
.FILE
))
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')
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
)
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)
341 frame
.pack_start(toolbar
, False, True, 0)
342 frame
.pack_start(scroll
, True, True, 0)
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
)
394 toolbar
.insert(ti_spin
, -1)