2 ## src/history_manager.py
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
9 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26 ## NOTE: some method names may match those of logger.py but that's it
27 ## someday (TM) should have common class
28 ## that abstracts db connections and helpers on it
29 ## the same can be said for history_window.py
35 warnings
.filterwarnings(action
='ignore')
37 if os
.path
.isdir('gtk'):
38 # Used to create windows installer with GTK included
39 paths
= os
.environ
['PATH']
40 list_
= paths
.split(';')
43 if p
.find('gtk') < 0 and p
.find('GTK') < 0:
45 new_list
.insert(0, 'gtk/lib')
46 new_list
.insert(0, 'gtk/bin')
47 os
.environ
['PATH'] = ';'.join(new_list
)
48 os
.environ
['GTK_BASEPATH'] = 'gtk'
58 from common
import i18n
65 longargs
= 'help config_path='
66 opts
= getopt
.getopt(sys
.argv
[1:], shortargs
, longargs
.split())[0]
67 except getopt
.error
, msg
:
69 print 'for help use --help'
72 if o
in ('-h', '--help'):
73 print 'history_manager [--help] [--config-path]'
75 elif o
in ('-c', '--config-path'):
79 config_path
= parseOpts()
82 import common
.configpaths
83 common
.configpaths
.gajimpaths
.init(config_path
)
85 common
.configpaths
.gajimpaths
.init_profile()
86 from common
import exceptions
87 from common
import gajim
89 from common
.logger
import LOG_DB_PATH
, constants
91 #FIXME: constants should implement 2 way mappings
92 status
= dict((constants
.__dict
__[i
], i
[5:].lower()) for i
in \
93 constants
.__dict
__.keys() if i
.startswith('SHOW_'))
94 from common
import helpers
97 # time, message, subject
106 import sqlite3
as sqlite
109 class HistoryManager
:
111 pix
= gtkgui_helpers
.get_icon_pixmap('gajim')
112 # set the icon to all newly opened windows
113 gtk
.window_set_default_icon(pix
)
115 if not os
.path
.exists(LOG_DB_PATH
):
116 dialogs
.ErrorDialog(_('Cannot find history logs database'),
117 '%s does not exist.' % LOG_DB_PATH
)
120 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui')
121 self
.window
= xml
.get_object('history_manager_window')
122 self
.jids_listview
= xml
.get_object('jids_listview')
123 self
.logs_listview
= xml
.get_object('logs_listview')
124 self
.search_results_listview
= xml
.get_object('search_results_listview')
125 self
.search_entry
= xml
.get_object('search_entry')
126 self
.logs_scrolledwindow
= xml
.get_object('logs_scrolledwindow')
127 self
.search_results_scrolledwindow
= xml
.get_object(
128 'search_results_scrolledwindow')
129 self
.welcome_vbox
= xml
.get_object('welcome_vbox')
131 self
.jids_already_in
= [] # holds jids that we already have in DB
132 self
.AT_LEAST_ONE_DELETION_DONE
= False
134 self
.con
= sqlite
.connect(LOG_DB_PATH
, timeout
=20.0,
135 isolation_level
='IMMEDIATE')
136 self
.cur
= self
.con
.cursor()
138 self
._init
_jids
_listview
()
139 self
._init
_logs
_listview
()
140 self
._init
_search
_results
_listview
()
142 self
._fill
_jids
_listview
()
144 self
.search_entry
.grab_focus()
146 self
.window
.show_all()
148 xml
.connect_signals(self
)
150 def _init_jids_listview(self
):
151 self
.jids_liststore
= gtk
.ListStore(str, str) # jid, jid_id
152 self
.jids_listview
.set_model(self
.jids_liststore
)
153 self
.jids_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
155 renderer_text
= gtk
.CellRendererText() # holds jid
156 col
= gtk
.TreeViewColumn(_('Contacts'), renderer_text
, text
=0)
157 self
.jids_listview
.append_column(col
)
159 self
.jids_listview
.get_selection().connect('changed',
160 self
.on_jids_listview_selection_changed
)
162 def _init_logs_listview(self
):
163 # log_line_id(HIDDEN), jid_id(HIDDEN), time, message, subject, nickname
164 self
.logs_liststore
= gtk
.ListStore(str, str, str, str, str, str)
165 self
.logs_listview
.set_model(self
.logs_liststore
)
166 self
.logs_listview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
168 renderer_text
= gtk
.CellRendererText() # holds time
169 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
=C_UNIXTIME
)
170 # user can click this header and sort
171 col
.set_sort_column_id(C_UNIXTIME
)
172 col
.set_resizable(True)
173 self
.logs_listview
.append_column(col
)
175 renderer_text
= gtk
.CellRendererText() # holds nickname
176 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
=C_NICKNAME
)
177 # user can click this header and sort
178 col
.set_sort_column_id(C_NICKNAME
)
179 col
.set_resizable(True)
180 col
.set_visible(False)
181 self
.nickname_col_for_logs
= col
182 self
.logs_listview
.append_column(col
)
184 renderer_text
= gtk
.CellRendererText() # holds message
185 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, markup
=C_MESSAGE
)
186 # user can click this header and sort
187 col
.set_sort_column_id(C_MESSAGE
)
188 col
.set_resizable(True)
189 self
.message_col_for_logs
= col
190 self
.logs_listview
.append_column(col
)
192 renderer_text
= gtk
.CellRendererText() # holds subject
193 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
=C_SUBJECT
)
194 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
195 col
.set_resizable(True)
196 col
.set_visible(False)
197 self
.subject_col_for_logs
= col
198 self
.logs_listview
.append_column(col
)
200 def _init_search_results_listview(self
):
201 # log_line_id (HIDDEN), jid, time, message, subject, nickname
202 self
.search_results_liststore
= gtk
.ListStore(str, str, str, str, str,
204 self
.search_results_listview
.set_model(self
.search_results_liststore
)
206 renderer_text
= gtk
.CellRendererText() # holds JID (who said this)
207 col
= gtk
.TreeViewColumn(_('JID'), renderer_text
, text
=1)
208 col
.set_sort_column_id(1) # user can click this header and sort
209 col
.set_resizable(True)
210 self
.search_results_listview
.append_column(col
)
212 renderer_text
= gtk
.CellRendererText() # holds time
213 col
= gtk
.TreeViewColumn(_('Date'), renderer_text
, text
=C_UNIXTIME
)
214 # user can click this header and sort
215 col
.set_sort_column_id(C_UNIXTIME
)
216 col
.set_resizable(True)
217 self
.search_results_listview
.append_column(col
)
219 renderer_text
= gtk
.CellRendererText() # holds message
220 col
= gtk
.TreeViewColumn(_('Message'), renderer_text
, text
=C_MESSAGE
)
221 col
.set_sort_column_id(C_MESSAGE
) # user can click this header and sort
222 col
.set_resizable(True)
223 self
.search_results_listview
.append_column(col
)
225 renderer_text
= gtk
.CellRendererText() # holds subject
226 col
= gtk
.TreeViewColumn(_('Subject'), renderer_text
, text
=C_SUBJECT
)
227 col
.set_sort_column_id(C_SUBJECT
) # user can click this header and sort
228 col
.set_resizable(True)
229 self
.search_results_listview
.append_column(col
)
231 renderer_text
= gtk
.CellRendererText() # holds nickname
232 col
= gtk
.TreeViewColumn(_('Nickname'), renderer_text
, text
=C_NICKNAME
)
233 # user can click this header and sort
234 col
.set_sort_column_id(C_NICKNAME
)
235 col
.set_resizable(True)
236 self
.search_results_listview
.append_column(col
)
238 def on_history_manager_window_delete_event(self
, widget
, event
):
239 if not self
.AT_LEAST_ONE_DELETION_DONE
:
244 self
.cur
.execute('VACUUM')
251 dialog
= dialogs
.YesNoDialog(
252 _('Do you want to clean up the database? '
253 '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
254 _('Normally allocated database size will not be freed, '
255 'it will just become reusable. If you really want to reduce '
256 'database filesize, click YES, else click NO.'
257 '\n\nIn case you click YES, please wait...'),
258 on_response_yes
=on_yes
, on_response_no
=on_no
)
259 dialog
.set_title(_('Database Cleanup'))
260 button_box
= dialog
.get_children()[0].get_children()[1]
261 button_box
.get_children()[0].grab_focus()
263 def _fill_jids_listview(self
):
264 # get those jids that have at least one entry in logs
265 self
.cur
.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN ('
266 'SELECT distinct logs.jid_id FROM logs) ORDER BY jid')
267 # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
268 rows
= self
.cur
.fetchall()
270 self
.jids_already_in
.append(row
[0]) # jid
271 self
.jids_liststore
.append(row
) # jid, jid_id
273 def on_jids_listview_selection_changed(self
, widget
, data
=None):
274 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
276 paths_len
= len(list_of_paths
)
277 if paths_len
== 0: # nothing is selected
280 self
.logs_liststore
.clear() # clear the store
282 self
.welcome_vbox
.hide()
283 self
.search_results_scrolledwindow
.hide()
284 self
.logs_scrolledwindow
.show()
287 for path
in list_of_paths
: # make them treerowrefs (it's needed)
288 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
290 for rowref
in list_of_rowrefs
: # FILL THE STORE, for all rows selected
291 path
= rowref
.get_path()
294 jid
= liststore
[path
][0].decode('utf-8') # jid
295 self
._fill
_logs
_listview
(jid
)
297 def _get_jid_id(self
, jid
):
299 jids table has jid and jid_id
300 logs table has log_id, jid_id, contact_name, time, kind, show, message
302 So to ask logs we need jid_id that matches our jid in jids table this
303 method wants jid and returns the jid_id for later sql-ing on logs
305 if jid
.find('/') != -1: # if it has a /
306 jid_is_from_pm
= self
._jid
_is
_from
_pm
(jid
)
307 if not jid_is_from_pm
: # it's normal jid with resource
308 jid
= jid
.split('/', 1)[0] # remove the resource
309 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid
,))
310 jid_id
= self
.cur
.fetchone()[0]
313 def _get_jid_from_jid_id(self
, jid_id
):
315 jids table has jid and jid_id
317 This method accepts jid_id and returns the jid for later sql-ing on logs
319 self
.cur
.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id
,))
320 jid
= self
.cur
.fetchone()[0]
323 def _jid_is_from_pm(self
, jid
):
325 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
326 is not a normal guy and nkour is not his resource? We ask if gajim@conf
327 is already in jids (with type room jid). This fails if user disables
328 logging for room and only enables for pm (so higly unlikely) and if we
329 fail we do not go chaos (user will see the first pm as if it was message
330 in room's public chat) and after that everything is ok
332 possible_room_jid
= jid
.split('/', 1)[0]
334 self
.cur
.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
335 (possible_room_jid
, constants
.JID_ROOM_TYPE
))
336 row
= self
.cur
.fetchone()
342 def _jid_is_room_type(self
, jid
):
344 Return True/False if given id is room type or not eg. if it is room
346 self
.cur
.execute('SELECT type FROM jids WHERE jid = ?', (jid
,))
347 row
= self
.cur
.fetchone()
350 elif row
[0] == constants
.JID_ROOM_TYPE
:
355 def _fill_logs_listview(self
, jid
):
357 Fill the listview with all messages that user sent to or received from
360 # no need to lower jid in this context as jid is already lowered
361 # as we use those jids from db
362 jid_id
= self
._get
_jid
_id
(jid
)
364 SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
370 results
= self
.cur
.fetchall()
372 if self
._jid
_is
_room
_type
(jid
): # is it room?
373 self
.nickname_col_for_logs
.set_visible(True)
374 self
.subject_col_for_logs
.set_visible(False)
376 self
.nickname_col_for_logs
.set_visible(False)
377 self
.subject_col_for_logs
.set_visible(True)
380 # exposed in UI (TreeViewColumns) are only
381 # time, message, subject, nickname
382 # but store in liststore
383 # log_line_id, jid_id, time, message, subject, nickname
384 log_line_id
, jid_id
, time_
, kind
, message
, subject
, nickname
, \
387 time_
= time
.strftime('%x', time
.localtime(float(time_
))
388 ).decode(locale
.getpreferredencoding())
393 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
394 constants
.KIND_CHAT_MSG_RECV
, constants
.KIND_GC_MSG
):
395 # it is the other side
396 color
= gajim
.config
.get('inmsgcolor') # so incoming color
397 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
398 constants
.KIND_CHAT_MSG_SENT
): # it is us
399 color
= gajim
.config
.get('outmsgcolor') # so outgoing color
400 elif kind
in (constants
.KIND_STATUS
,
401 constants
.KIND_GCSTATUS
): # is is statuses
403 color
= gajim
.config
.get('statusmsgcolor')
404 # include status into (status) message
408 message
= ' : ' + message
409 message
= helpers
.get_uf_show(gajim
.SHOW_LIST
[show
]) + \
414 message_
+= ' foreground="%s"' % color
415 message_
+= '>%s</span>' % \
416 gobject
.markup_escape_text(message
)
417 self
.logs_liststore
.append((log_line_id
, jid_id
, time_
,
418 message_
, subject
, nickname
))
420 def _fill_search_results_listview(self
, text
):
422 Ask db and fill listview with results that match text
424 self
.search_results_liststore
.clear()
425 like_sql
= '%' + text
+ '%'
427 SELECT log_line_id, jid_id, time, message, subject, contact_name
429 WHERE message LIKE ? OR subject LIKE ?
431 ''', (like_sql
, like_sql
))
433 results
= self
.cur
.fetchall()
435 # exposed in UI (TreeViewColumns) are only
436 # JID, time, message, subject, nickname
437 # but store in liststore
438 # log_line_id, jid (from jid_id), time, message, subject, nickname
439 log_line_id
, jid_id
, time_
, message
, subject
, nickname
= row
441 time_
= time
.strftime('%x', time
.localtime(float(time_
))
442 ).decode(locale
.getpreferredencoding())
446 jid
= self
._get
_jid
_from
_jid
_id
(jid_id
)
448 self
.search_results_liststore
.append((log_line_id
, jid
, time_
,
449 message
, subject
, nickname
))
451 def on_logs_listview_key_press_event(self
, widget
, event
):
452 liststore
, list_of_paths
= self
.logs_listview
.get_selection()\
454 if event
.keyval
== gtk
.keysyms
.Delete
:
455 self
._delete
_logs
(liststore
, list_of_paths
)
457 def on_listview_button_press_event(self
, widget
, event
):
458 if event
.button
== 3: # right click
459 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui',
461 if widget
.name
!= 'jids_listview':
462 xml
.get_object('export_menuitem').hide()
463 xml
.get_object('delete_menuitem').connect('activate',
464 self
.on_delete_menuitem_activate
, widget
)
466 xml
.connect_signals(self
)
467 xml
.get_object('context_menu').popup(None, None, None,
468 event
.button
, event
.time
)
471 def on_export_menuitem_activate(self
, widget
):
472 xml
= gtkgui_helpers
.get_gtk_builder('history_manager.ui',
474 xml
.connect_signals(self
)
476 dlg
= xml
.get_object('filechooserdialog')
477 dlg
.set_title(_('Exporting History Logs...'))
478 dlg
.set_current_folder(gajim
.HOME_DIR
)
479 dlg
.props
.do_overwrite_confirmation
= True
482 if response
== gtk
.RESPONSE_OK
: # user want us to export ;)
483 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
485 path_to_file
= dlg
.get_filename()
486 self
._export
_jids
_logs
_to
_file
(liststore
, list_of_paths
,
491 def on_delete_menuitem_activate(self
, widget
, listview
):
492 widget_name
= gtk
.Buildable
.get_name(listview
)
493 liststore
, list_of_paths
= listview
.get_selection().get_selected_rows()
494 if widget_name
== 'jids_listview':
495 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
496 elif widget_name
in ('logs_listview', 'search_results_listview'):
497 self
._delete
_logs
(liststore
, list_of_paths
)
498 else: # Huh ? We don't know this widget
501 def on_jids_listview_key_press_event(self
, widget
, event
):
502 liststore
, list_of_paths
= self
.jids_listview
.get_selection()\
504 if event
.keyval
== gtk
.keysyms
.Delete
:
505 self
._delete
_jid
_logs
(liststore
, list_of_paths
)
507 def _export_jids_logs_to_file(self
, liststore
, list_of_paths
, path_to_file
):
508 paths_len
= len(list_of_paths
)
509 if paths_len
== 0: # nothing is selected
513 for path
in list_of_paths
: # make them treerowrefs (it's needed)
514 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
516 for rowref
in list_of_rowrefs
:
517 path
= rowref
.get_path()
520 jid_id
= liststore
[path
][1]
522 SELECT time, kind, message, contact_name FROM logs
527 # FIXME: we may have two contacts selected to export. fix that
528 # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
529 results
= self
.cur
.fetchall()
531 file_
= open(path_to_file
, 'w')
533 # in store: time, kind, message, contact_name FROM logs
534 # in text: JID or You or nickname (if it's gc_msg), time, message
535 time_
, kind
, message
, nickname
= row
536 if kind
in (constants
.KIND_SINGLE_MSG_RECV
,
537 constants
.KIND_CHAT_MSG_RECV
):
538 who
= self
._get
_jid
_from
_jid
_id
(jid_id
)
539 elif kind
in (constants
.KIND_SINGLE_MSG_SENT
,
540 constants
.KIND_CHAT_MSG_SENT
):
542 elif kind
== constants
.KIND_GC_MSG
:
544 else: # status or gc_status. do not save
549 time_
= time
.strftime('%c', time
.localtime(float(time_
))
550 ).decode(locale
.getpreferredencoding())
554 file_
.write(_('%(who)s on %(time)s said: %(message)s\n') % {
555 'who': who
, 'time': time_
, 'message': message
})
557 def _delete_jid_logs(self
, liststore
, list_of_paths
):
558 paths_len
= len(list_of_paths
)
559 if paths_len
== 0: # nothing is selected
562 def on_ok(liststore
, list_of_paths
):
563 # delete all rows from db that match jid_id
565 for path
in list_of_paths
: # make them treerowrefs (it's needed)
566 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
568 for rowref
in list_of_rowrefs
:
569 path
= rowref
.get_path()
572 jid_id
= liststore
[path
][1]
573 del liststore
[path
] # remove from UI
580 # now delete "jid, jid_id" row from jids table
588 self
.AT_LEAST_ONE_DELETION_DONE
= True
591 jid_id
= '<i>%s</i>' % liststore
[list_of_paths
[0]][0]
592 pri_text
= _('Do you really want to delete the logs of %(jid)s?') \
596 'Do you really want to delete logs of the selected contacts?')
597 dialog
= dialogs
.ConfirmationDialog('',
598 _('This is an irreversible operation.'), on_response_ok
=(on_ok
,
599 liststore
, list_of_paths
))
600 dialog
.set_title(_('Deletion Confirmation'))
601 dialog
.set_markup(pri_text
)
602 ok_button
= dialog
.get_children()[0].get_children()[1].get_children()[0]
603 ok_button
.grab_focus()
604 dialog
.set_transient_for(self
.window
)
606 def _delete_logs(self
, liststore
, list_of_paths
):
607 paths_len
= len(list_of_paths
)
608 if paths_len
== 0: # nothing is selected
611 def on_ok(liststore
, list_of_paths
):
612 # delete rows from db that match log_line_id
614 for path
in list_of_paths
: # make them treerowrefs (it's needed)
615 list_of_rowrefs
.append(gtk
.TreeRowReference(liststore
, path
))
617 for rowref
in list_of_rowrefs
:
618 path
= rowref
.get_path()
621 log_line_id
= liststore
[path
][0]
622 del liststore
[path
] # remove from UI
626 WHERE log_line_id = ?
631 self
.AT_LEAST_ONE_DELETION_DONE
= True
633 pri_text
= i18n
.ngettext(
634 'Do you really want to delete the selected message?',
635 'Do you really want to delete the selected messages?', paths_len
)
636 dialog
= dialogs
.ConfirmationDialog(pri_text
,
637 _('This is an irreversible operation.'), on_response_ok
=(on_ok
,
638 liststore
, list_of_paths
))
639 dialog
.set_title(_('Deletion Confirmation'))
640 ok_button
= dialog
.get_children()[0].get_children()[1].get_children()[0]
641 ok_button
.grab_focus()
642 dialog
.set_transient_for(self
.window
)
644 def on_search_db_button_clicked(self
, widget
):
645 text
= self
.search_entry
.get_text().decode('utf-8')
649 self
.welcome_vbox
.hide()
650 self
.logs_scrolledwindow
.hide()
651 self
.search_results_scrolledwindow
.show()
653 self
._fill
_search
_results
_listview
(text
)
655 def on_search_results_listview_row_activated(self
, widget
, path
, column
):
656 # get log_line_id, jid_id from row we double clicked
657 log_line_id
= self
.search_results_liststore
[path
][0]
658 jid
= self
.search_results_liststore
[path
][1].decode('utf-8')
659 # make it string as in gtk liststores I have them all as strings
660 # as this is what db returns so I don't have to fight with types
661 jid_id
= self
._get
_jid
_id
(jid
)
663 iter_
= self
.jids_liststore
.get_iter_root()
665 # self.jids_liststore[iter_][1] holds jid_ids
666 if self
.jids_liststore
[iter_
][1] == jid_id
:
668 iter_
= self
.jids_liststore
.iter_next(iter_
)
673 path
= self
.jids_liststore
.get_path(iter_
)
674 self
.jids_listview
.set_cursor(path
)
676 iter_
= self
.logs_liststore
.get_iter_root()
678 # self.logs_liststore[iter_][0] holds lon_line_ids
679 if self
.logs_liststore
[iter_
][0] == log_line_id
:
681 iter_
= self
.logs_liststore
.iter_next(iter_
)
683 path
= self
.logs_liststore
.get_path(iter_
)
684 self
.logs_listview
.scroll_to_cell(path
)
686 if __name__
== '__main__':
687 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
) # ^C exits the application