Standardize on single quotes
[wifi-radar.git] / wifiradar / gui / g2 / prefs.py
blob14cf86e75c32a4514ac7c043792be0aee3b4a466
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # gui/g2/prefs.py - preferences editor, etc. for PyGTK UI
6 # Part of WiFi Radar: A utility for managing WiFi profiles on GNU/Linux.
8 # Copyright (C) 2004-2005 Ahmad Baitalmal <ahmad@baitalmal.com>
9 # Copyright (C) 2005 Nicolas Brouard <nicolas.brouard@mandrake.org>
10 # Copyright (C) 2005-2009 Brian Elliott Finley <brian@thefinleys.com>
11 # Copyright (C) 2006 David Decotigny <com.d2@free.fr>
12 # Copyright (C) 2006 Simon Gerber <gesimu@gmail.com>
13 # Copyright (C) 2006-2007 Joey Hurst <jhurst@lucubrate.org>
14 # Copyright (C) 2012 Anari Jalakas <anari.jalakas@gmail.com>
15 # Copyright (C) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
16 # Copyright (C) 2009-2010,2014 Sean Robinson <robinson@tuxfamily.org>
17 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
19 # This program is free software; you can redistribute it and/or modify
20 # it under the terms of the GNU General Public License as published by
21 # the Free Software Foundation; version 2 of the License.
23 # This program is distributed in the hope that it will be useful,
24 # but WITHOUT ANY WARRANTY; without even the implied warranty of
25 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 # GNU General Public License in LICENSE.GPL for more details.
28 # You should have received a copy of the GNU General Public License
29 # along with this program; if not, write to:
30 # Free Software Foundation, Inc.
31 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
35 from __future__ import unicode_literals
37 import errno
38 import gtk
39 import logging
40 from subprocess import call, Popen, PIPE, STDOUT
42 from wifiradar.misc import _
43 from . import transients
45 # create a logger
46 logger = logging.getLogger(__name__)
49 class PreferencesEditor(gtk.Dialog, object):
50 """ The preferences dialog. Edits non-profile sections of the config file.
51 """
52 def __init__(self, parent, config):
53 """ Create a new PreferencesEditor, with :data:`parent` window,
54 to edit :data:`config`.
55 """
56 gtk.Dialog.__init__(self, _('WiFi Radar Preferences'), parent,
57 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
58 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
59 gtk.STOCK_SAVE, gtk.RESPONSE_APPLY))
61 self.config = config
62 self.icon = gtk.gdk.pixbuf_new_from_file('pixmaps/wifi-radar.png')
63 self.set_icon(self.icon)
64 self.set_resizable(True)
66 # set up preferences widgets
68 # build everything in a tabbed notebook
69 self.prefs_notebook = gtk.Notebook()
71 ### General tab
72 self.general_page = gtk.VBox()
73 # auto detect wireless device
74 self.w_auto_detect = gtk.CheckButton(_('Auto-detect wireless device'))
76 self.w_auto_detect.set_active(self.config.get_opt('DEFAULT', 'interface') == _('auto_detect'))
77 self.w_auto_detect.connect('toggled', self.toggle_auto_detect)
78 self.w_auto_detect.set_tooltip_text(_('Automatically select wireless '
79 'device to configure'))
80 self.general_page.pack_start(self.w_auto_detect, False, False, 5)
82 # network interface selecter
83 self.w_interface = gtk.combo_box_entry_new_text()
84 try:
85 iwconfig_info = Popen([self.config.get_opt('DEFAULT', 'iwconfig_command')], stdout=PIPE, stderr=STDOUT).stdout
86 except OSError as e:
87 if e.errno == 2:
88 logger.critical(_('iwconfig command not found, please set '
89 'this in the preferences.'))
90 iwconfig_info = ''
91 wireless_devices = [(x[0:x.find(' ')]) for x in iwconfig_info if ('ESSID' in x)]
92 for device in wireless_devices:
93 if device != self.config.get_opt('DEFAULT', 'interface'):
94 self.w_interface.append_text(device)
95 if self.config.get_opt('DEFAULT', 'interface') != _('auto_detect'):
96 self.w_interface.prepend_text(self.config.get_opt('DEFAULT', 'interface'))
97 self.w_interface.set_active(0)
98 self.w_interface_label = gtk.Label(_('Wireless device'))
99 self.w_hbox1 = gtk.HBox(False, 0)
100 self.w_hbox1.pack_start(self.w_interface_label, False, False, 5)
101 self.w_hbox1.pack_start(self.w_interface, True, True, 0)
102 self.w_interface.set_sensitive(self.config.get_opt('DEFAULT', 'interface') != _('auto_detect'))
103 self.general_page.pack_start(self.w_hbox1, False, False, 5)
105 # scan timeout (spin button of integers from 1 to 100)
106 #self.time_in_seconds = gtk.Adjustment(self.config.get_opt_as_int('DEFAULT', 'scan_timeout'),1,100,1,1,0)
107 #self.w_scan_timeout = gtk.SpinButton(self.time_in_seconds, 1, 0)
108 #self.w_scan_timeout.set_update_policy(gtk.UPDATE_IF_VALID)
109 #self.w_scan_timeout.set_numeric(True)
110 #self.w_scan_timeout.set_snap_to_ticks(True)
111 #self.w_scan_timeout.set_wrap(False)
112 #self.w_scan_timeout.set_tooltip_text(_('How long should WiFi Radar scan for access points?'))
113 #self.w_scan_timeout_label = gtk.Label(_('Scan timeout (seconds)'))
114 #self.w_hbox2 = gtk.HBox(False, 0)
115 #self.w_hbox2.pack_start(self.w_scan_timeout_label, False, False, 5)
116 #self.w_hbox2.pack_start(self.w_scan_timeout, True, True, 0)
117 #self.general_page.pack_start(self.w_hbox2, False, False, 5)
119 # commit required
120 self.w_commit_required = gtk.CheckButton(_('Commit required'))
121 self.w_commit_required.set_active(self.config.get_opt_as_bool('DEFAULT', 'commit_required'))
122 self.w_commit_required.set_tooltip_text(_('Check this box if your '
123 'card requires a "commit" command with iwconfig'))
124 self.general_page.pack_start(self.w_commit_required, False, False, 5)
126 # ifup required
127 self.w_ifup_required = gtk.CheckButton(_('Ifup required'))
128 self.w_ifup_required.set_active(self.config.get_opt_as_bool('DEFAULT', 'ifup_required'))
129 self.w_ifup_required.set_tooltip_text(_('Check this box if your '
130 'system requires the interface to be brought up first'))
131 self.general_page.pack_start(self.w_ifup_required, False, False, 5)
133 self.prefs_notebook.append_page(self.general_page, gtk.Label(_('General')))
134 ### End of General tab
136 ### Advanced tab
137 # table to use for layout of following command configurations
138 self.cmds_table = gtk.Table()
140 # ifconfig command
141 self.ifconfig_cmd = gtk.Entry()
142 self.ifconfig_cmd.set_width_chars(32)
143 self.ifconfig_cmd.set_tooltip_text(_('The command to use to configure '
144 'the network card'))
145 self.ifconfig_cmd.set_text(self.config.get_opt('DEFAULT', 'ifconfig_command'))
146 self.ifconfig_cmd_label = gtk.Label(_('Network interface configure command'))
147 self.ifconfig_cmd_button = transients.FileBrowseButton(self, self.ifconfig_cmd)
148 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
149 self.cmds_table.attach(self.ifconfig_cmd_label, 1, 2, 1, 2, gtk.FILL|gtk.EXPAND, 0, 5, 0)
150 self.cmds_table.attach(self.ifconfig_cmd, 2, 3, 1, 2, gtk.FILL|gtk.EXPAND, 0, 0, 0)
151 self.cmds_table.attach(self.ifconfig_cmd_button, 3, 4, 1, 2, gtk.FILL|gtk.EXPAND, 0, 0, 0)
153 # iwconfig command
154 self.iwconfig_cmd = gtk.Entry()
155 self.iwconfig_cmd.set_width_chars(32)
156 self.iwconfig_cmd.set_tooltip_text(_('The command to use to configure '
157 'the wireless connection'))
158 self.iwconfig_cmd.set_text(self.config.get_opt('DEFAULT', 'iwconfig_command'))
159 self.iwconfig_cmd_label = gtk.Label(_('Wireless connection configure command'))
160 self.iwconfig_cmd_button = transients.FileBrowseButton(self, self.iwconfig_cmd)
161 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
162 self.cmds_table.attach(self.iwconfig_cmd_label, 1, 2, 2, 3, gtk.FILL|gtk.EXPAND, 0, 5, 0)
163 self.cmds_table.attach(self.iwconfig_cmd, 2, 3, 2, 3, gtk.FILL|gtk.EXPAND, 0, 0, 0)
164 self.cmds_table.attach(self.iwconfig_cmd_button, 3, 4, 2, 3, gtk.FILL|gtk.EXPAND, 0, 0, 0)
166 # iwlist command
167 self.iwlist_cmd = gtk.Entry()
168 self.iwlist_cmd.set_width_chars(32)
169 self.iwlist_cmd.set_tooltip_text(_('The command to use to scan for '
170 'access points'))
171 self.iwlist_cmd.set_text(self.config.get_opt('DEFAULT', 'iwlist_command'))
172 self.iwlist_cmd_label = gtk.Label(_('Wireless scanning command'))
173 self.iwlist_cmd_button = transients.FileBrowseButton(self, self.iwlist_cmd)
174 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
175 self.cmds_table.attach(self.iwlist_cmd_label, 1, 2, 3, 4, gtk.FILL|gtk.EXPAND, 0, 5, 0)
176 self.cmds_table.attach(self.iwlist_cmd, 2, 3, 3, 4, gtk.FILL|gtk.EXPAND, 0, 0, 0)
177 self.cmds_table.attach(self.iwlist_cmd_button, 3, 4, 3, 4, gtk.FILL|gtk.EXPAND, 0, 0, 0)
179 # route command
180 self.route_cmd = gtk.Entry()
181 self.route_cmd.set_width_chars(32)
182 self.route_cmd.set_tooltip_text(_('The command to use to configure '
183 'the network routing'))
184 self.route_cmd.set_text(self.config.get_opt('DEFAULT', 'route_command'))
185 self.route_cmd_label = gtk.Label(_('Network route configure command'))
186 self.route_cmd_button = transients.FileBrowseButton(self, self.route_cmd)
187 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
188 self.cmds_table.attach(self.route_cmd_label, 1, 2, 4, 5, gtk.FILL|gtk.EXPAND, 0, 5, 0)
189 self.cmds_table.attach(self.route_cmd, 2, 3, 4, 5, gtk.FILL|gtk.EXPAND, 0, 0, 0)
190 self.cmds_table.attach(self.route_cmd_button, 3, 4, 4, 5, gtk.FILL|gtk.EXPAND, 0, 0, 0)
192 # log file
193 self.logfile_entry = gtk.Entry()
194 self.logfile_entry.set_width_chars(32)
195 self.logfile_entry.set_tooltip_text(_('The file in which to save '
196 'logging info'))
197 self.logfile_entry.set_text(self.config.get_opt('DEFAULT', 'logfile'))
198 self.logfile_label = gtk.Label(_('Log file'))
199 self.logfile_button = transients.FileBrowseButton(self, self.logfile_entry)
200 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
201 self.cmds_table.attach(self.logfile_label, 1, 2, 5, 6, gtk.FILL|gtk.EXPAND, 0, 5, 0)
202 self.cmds_table.attach(self.logfile_entry, 2, 3, 5, 6, gtk.FILL|gtk.EXPAND, 0, 0, 0)
203 self.cmds_table.attach(self.logfile_button, 3, 4, 5, 6, gtk.FILL|gtk.EXPAND, 0, 0, 0)
205 # log level (spin button of integers from 0 to 50 by 5's)
206 self.loglevel = gtk.SpinButton(gtk.Adjustment(self.config.get_opt_as_int('DEFAULT', 'loglevel'), 1, 50, 5, 5, 0), 1, 0)
207 self.loglevel.set_update_policy(gtk.UPDATE_IF_VALID)
208 self.loglevel.set_numeric(True)
209 self.loglevel.set_snap_to_ticks(True)
210 self.loglevel.set_wrap(False)
211 self.loglevel.set_tooltip_text(_('How much detail to save in the log '
212 'file. Larger numbers provide less detail and smaller numbers, '
213 'more detail.'))
214 self.loglevel.set_text(self.config.get_opt('DEFAULT', 'loglevel'))
215 self.loglevel_label = gtk.Label(_('Log level'))
216 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
217 self.cmds_table.attach(self.loglevel_label, 1, 2, 6, 7, gtk.FILL|gtk.EXPAND, 0, 5, 0)
218 self.cmds_table.attach(self.loglevel, 2, 3, 6, 7, gtk.FILL|gtk.EXPAND, 0, 0, 0)
220 self.prefs_notebook.append_page(self.cmds_table, gtk.Label(_('Advanced')))
221 ### End of Advanced tab
223 ### DHCP tab
224 # table to use for layout of DHCP prefs
225 self.dhcp_table = gtk.Table()
227 self.dhcp_cmd = gtk.Entry()
228 self.dhcp_cmd.set_width_chars(32)
229 self.dhcp_cmd.set_tooltip_text(_('The command to use for automatic '
230 'network configuration'))
231 self.dhcp_cmd.set_text(self.config.get_opt('DHCP', 'command'))
232 self.dhcp_cmd_label = gtk.Label(_('Command'))
233 self.dhcp_cmd_button = transients.FileBrowseButton(self, self.dhcp_cmd)
234 self.dhcp_table.attach(self.dhcp_cmd_label, 1, 2, 1, 2, True, False, 5, 0)
235 self.dhcp_table.attach(self.dhcp_cmd, 2, 3, 1, 2, True, False, 0, 0)
236 self.dhcp_table.attach(self.dhcp_cmd_button, 3, 4, 1, 2, False, False, 0, 0)
238 self.dhcp_args = gtk.Entry()
239 self.dhcp_args.set_width_chars(32)
240 self.dhcp_args.set_tooltip_text(_('The start-up arguments to the '
241 'DHCP command'))
242 self.dhcp_args.set_text(self.config.get_opt('DHCP', 'args'))
243 self.dhcp_args_label = gtk.Label(_('Arguments'))
244 self.dhcp_table.attach(self.dhcp_args_label, 1, 2, 2, 3, True, False, 5, 0)
245 self.dhcp_table.attach(self.dhcp_args, 2, 3, 2, 3, True, False, 0, 0)
247 self.dhcp_kill_args = gtk.Entry()
248 self.dhcp_kill_args.set_width_chars(32)
249 self.dhcp_kill_args.set_tooltip_text(_('The shutdown arguments to '
250 'the DHCP command'))
251 self.dhcp_kill_args.set_text(self.config.get_opt('DHCP', 'kill_args'))
252 self.dhcp_kill_args_label = gtk.Label(_('Kill arguments'))
253 self.dhcp_table.attach(self.dhcp_kill_args_label, 1, 2, 3, 4, True, False, 5, 0)
254 self.dhcp_table.attach(self.dhcp_kill_args, 2, 3, 3, 4, True, False, 0, 0)
256 self.dhcp_timeout = gtk.Entry()
257 self.dhcp_timeout.set_width_chars(32)
258 self.dhcp_timeout.set_tooltip_text(_('The amount of time DHCP will '
259 'spend trying to connect'))
260 self.dhcp_timeout.set_text(self.config.get_opt('DHCP', 'timeout'))
261 self.dhcp_timeout_label = gtk.Label(_('DHCP connect timeout (seconds)'))
262 self.dhcp_table.attach(self.dhcp_timeout_label, 1, 2, 4, 5, True, False, 5, 0)
263 self.dhcp_table.attach(self.dhcp_timeout, 2, 3, 4, 5, True, False, 0, 0)
265 self.dhcp_pidfile = gtk.Entry()
266 self.dhcp_pidfile.set_width_chars(32)
267 self.dhcp_pidfile.set_tooltip_text(_('The file DHCP uses to store '
268 'its PID'))
269 self.dhcp_pidfile.set_text(self.config.get_opt('DHCP', 'pidfile'))
270 self.dhcp_pidfile_label = gtk.Label(_('PID file'))
271 self.dhcp_table.attach(self.dhcp_pidfile_label, 1, 2, 5, 6, True, False, 5, 0)
272 self.dhcp_table.attach(self.dhcp_pidfile, 2, 3, 5, 6, True, False, 0, 0)
274 self.prefs_notebook.append_page(self.dhcp_table, gtk.Label(_('DHCP')))
275 ### End of DHCP tab
277 ### WPA tab
278 # table to use for layout of DHCP prefs
279 self.wpa_table = gtk.Table()
281 self.wpa_cmd = gtk.Entry()
282 self.wpa_cmd.set_width_chars(32)
283 self.wpa_cmd.set_tooltip_text(_('The command to use for WPA encrypted '
284 'connections'))
285 self.wpa_cmd.set_text(self.config.get_opt('WPA', 'command'))
286 self.wpa_cmd_label = gtk.Label(_('Command'))
287 self.wpa_cmd_button = transients.FileBrowseButton(self, self.wpa_cmd)
288 self.wpa_table.attach(self.wpa_cmd_label, 1, 2, 1, 2, True, False, 5, 0)
289 self.wpa_table.attach(self.wpa_cmd, 2, 3, 1, 2, True, False, 0, 0)
290 self.wpa_table.attach(self.wpa_cmd_button, 3, 4, 1, 2, False, False, 0, 0)
292 self.wpa_args = gtk.Entry()
293 self.wpa_args.set_width_chars(32)
294 self.wpa_args.set_tooltip_text(_('The start-up arguments to the WPA '
295 'command'))
296 self.wpa_args.set_text(self.config.get_opt('WPA', 'args'))
297 self.wpa_args_label = gtk.Label(_('Arguments'))
298 self.wpa_table.attach(self.wpa_args_label, 1, 2, 2, 3, True, False, 5, 0)
299 self.wpa_table.attach(self.wpa_args, 2, 3, 2, 3, True, False, 0, 0)
301 self.wpa_kill_args = gtk.Entry()
302 self.wpa_kill_args.set_width_chars(32)
303 self.wpa_kill_args.set_tooltip_text(_('The shutdown arguments to the '
304 'WPA command'))
305 self.wpa_kill_args.set_text(self.config.get_opt('WPA', 'kill_command'))
306 self.wpa_kill_args_label = gtk.Label(_('Kill command'))
307 self.wpa_table.attach(self.wpa_kill_args_label, 1, 2, 3, 4, True, False, 5, 0)
308 self.wpa_table.attach(self.wpa_kill_args, 2, 3, 3, 4, True, False, 0, 0)
310 self.wpa_config = gtk.Entry()
311 self.wpa_config.set_width_chars(32)
312 self.wpa_config.set_tooltip_text(_('The WPA configuration file to use'))
313 self.wpa_config.set_text(self.config.get_opt('WPA', 'configuration'))
314 self.wpa_config_label = gtk.Label(_('Configuration file'))
315 self.wpa_table.attach(self.wpa_config_label, 1, 2, 4, 5, True, False, 5, 0)
316 self.wpa_table.attach(self.wpa_config, 2, 3, 4, 5, True, False, 0, 0)
318 self.wpa_driver = gtk.Entry()
319 self.wpa_driver.set_width_chars(32)
320 self.wpa_driver.set_tooltip_text(_('The WPA driver to use'))
321 self.wpa_driver.set_text(self.config.get_opt('WPA', 'driver'))
322 self.wpa_driver_label = gtk.Label(_('Driver'))
323 self.wpa_table.attach(self.wpa_driver_label, 1, 2, 5, 6, True, False, 5, 0)
324 self.wpa_table.attach(self.wpa_driver, 2, 3, 5, 6, True, False, 0, 0)
326 self.wpa_pidfile = gtk.Entry()
327 self.wpa_pidfile.set_width_chars(32)
328 self.wpa_pidfile.set_tooltip_text(_('The file WPA uses to store its '
329 'PID'))
330 self.wpa_pidfile.set_text(self.config.get_opt('WPA', 'pidfile'))
331 self.wpa_pidfile_label = gtk.Label(_('PID file'))
332 self.wpa_table.attach(self.wpa_pidfile_label, 1, 2, 6, 7, True, False, 5, 0)
333 self.wpa_table.attach(self.wpa_pidfile, 2, 3, 6, 7, True, False, 0, 0)
335 self.prefs_notebook.append_page(self.wpa_table, gtk.Label(_('WPA')))
336 ### End of WPA tab
338 self.vbox.pack_start(self.prefs_notebook, False, False, 5)
340 def toggle_auto_detect(self, auto_detect_toggle, data=None):
341 """ Respond to Auto-detect checkbox toggle by activating or
342 de-activating the interface ComboBox. :data:`auto_detect_toggle`
343 is the checkbox sending the signal. :data:`data` is a list
344 of arbitrary arguments, it is ignored.
346 self.w_interface.set_sensitive(not auto_detect_toggle.get_active())
348 def run(self):
349 """ Display preferences dialog and operate until canceled or okayed.
350 :func:`run` returns a tuple with a :data:`GTK Response Type Constant`
351 and the possibly added :class:`ConfigManger`.
353 self.show_all()
354 response = gtk.Dialog.run(self)
355 if response == gtk.RESPONSE_APPLY:
356 self.apply()
357 return (response, self.config)
359 def apply(self):
360 """ Apply the values in the UI to the configuration object.
362 if self.w_auto_detect.get_active():
363 self.config.set_opt('DEFAULT', 'interface', _('auto_detect'))
364 else:
365 interface = _('auto_detect')
366 if self.w_interface.get_active_text() != '':
367 interface = self.w_interface.get_active_text()
368 self.config.set_opt('DEFAULT', 'interface', interface)
369 #self.config.set_float_opt('DEFAULT', 'scan_timeout',
370 #self.w_scan_timeout.get_value().strip())
371 self.config.set_bool_opt('DEFAULT', 'commit_required',
372 self.w_commit_required.get_active())
373 self.config.set_bool_opt('DEFAULT', 'ifup_required',
374 self.w_ifup_required.get_active())
375 self.config.set_opt('DEFAULT', 'ifconfig_command',
376 self.ifconfig_cmd.get_text().strip())
377 self.config.set_opt('DEFAULT', 'iwconfig_command',
378 self.iwconfig_cmd.get_text().strip())
379 self.config.set_opt('DEFAULT', 'iwlist_command',
380 self.iwlist_cmd.get_text().strip())
381 self.config.set_opt('DEFAULT', 'route_command',
382 self.route_cmd.get_text().strip())
383 self.config.set_opt('DEFAULT', 'logfile',
384 self.logfile_entry.get_text().strip())
385 self.config.set_int_opt('DEFAULT', 'loglevel',
386 int(self.loglevel.get_value()))
387 self.config.set_opt('DHCP', 'command',
388 self.dhcp_cmd.get_text().strip())
389 self.config.set_opt('DHCP', 'args',
390 self.dhcp_args.get_text().strip())
391 self.config.set_opt('DHCP', 'kill_args',
392 self.dhcp_kill_args.get_text().strip())
393 self.config.set_opt('DHCP', 'timeout',
394 self.dhcp_timeout.get_text().strip())
395 self.config.set_opt('DHCP', 'pidfile',
396 self.dhcp_pidfile.get_text().strip())
397 self.config.set_opt('WPA', 'command',
398 self.wpa_cmd.get_text().strip())
399 self.config.set_opt('WPA', 'args',
400 self.wpa_args.get_text().strip())
401 self.config.set_opt('WPA', 'kill_command',
402 self.wpa_kill_args.get_text().strip())
403 self.config.set_opt('WPA', 'configuration',
404 self.wpa_config.get_text().strip())
405 self.config.set_opt('WPA', 'driver',
406 self.wpa_driver.get_text().strip())
407 self.config.set_opt('WPA', 'pidfile',
408 self.wpa_pidfile.get_text().strip())
411 # Make so we can be imported
412 if __name__ == '__main__':
413 pass