2 # -*- coding: utf-8 -*-
4 # gui/g2/profile.py - profile 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) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
15 # Copyright (C) 2009-2010,2014 Sean Robinson <seankrobinson@gmail.com>
16 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
18 # This program is free software; you can redistribute it and/or modify
19 # it under the terms of the GNU General Public License as published by
20 # the Free Software Foundation; version 2 of the License.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License in LICENSE.GPL for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
37 logger
= logging
.getLogger(__name__
)
40 class ProfileEditor(gtk
.Dialog
, object):
41 """ Edit and return an AP profile.
43 def __init__(self
, parent
, profile
):
44 """ Create a new :class:`ProfileEditor`, with :data:`parent` window,
45 to edit :data:`profile`.
47 gtk
.Dialog
.__init
__(self
, 'WiFi Profile', parent
,
48 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
49 (gtk
.STOCK_CANCEL
, False, gtk
.STOCK_SAVE
, True))
51 self
.profile
= profile
.copy()
52 self
.WIFI_MODES
= ['auto', 'Managed', 'Ad-Hoc', 'Master',
53 'Repeater', 'Secondary', 'Monitor']
54 self
.WEP_MODE
= ['open', 'restricted']
55 self
.WIFI_CHANNELS
= ['auto', '1', '2', '3', '4', '5', '6', '7',
56 '8', '9', '10', '11', '12', '13', '14']
58 self
.icon
= gtk
.gdk
.pixbuf_new_from_file("pixmaps/wifi-radar.png")
59 self
.set_icon(self
.icon
)
60 self
.set_resizable(False)
61 #self.set_size_request(400, 400)
63 # build everything in a tabbed notebook
64 self
.profile_notebook
= gtk
.Notebook()
66 self
.general_table
= gtk
.Table()
67 self
.general_table
.set_row_spacings(3)
68 self
.general_table
.set_col_spacings(3)
70 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
71 self
.general_table
.attach(gtk
.Label('Network Name'), 0, 1, 0, 1, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
73 self
.essid_entry
= gtk
.Entry(32)
74 self
.essid_entry
.set_text(self
.profile
['essid'])
75 self
.general_table
.attach(self
.essid_entry
, 1, 2, 0, 1, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
76 # Add the essid table to the dialog
77 self
.essid_entry
.set_tooltip_text("The name of the network with which to connect.")
80 self
.general_table
.attach(gtk
.Label('Network Address'), 0, 1, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
82 self
.bssid_entry
= gtk
.Entry(32)
83 self
.bssid_entry
.set_text(self
.profile
['bssid'])
84 self
.bssid_entry
.set_sensitive(not self
.profile
['roaming'])
85 # Add the bssid table to the dialog
86 self
.general_table
.attach(self
.bssid_entry
, 1, 2, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
87 self
.bssid_entry
.set_tooltip_text("The address of the network with which to connect.")
88 # Add the roaming checkbox
89 self
.roaming_cb
= gtk
.CheckButton('Roaming')
90 self
.roaming_cb
.set_active(self
.profile
['roaming'])
91 self
.roaming_cb
.connect("toggled", self
.toggle_roaming
)
92 self
.general_table
.attach(self
.roaming_cb
, 1, 2, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
93 self
.roaming_cb
.set_tooltip_text("Use the AP in this network which provides strongest signal?")
95 self
.general_table
.attach(gtk
.Label('Mode'), 0, 1, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
96 self
.mode_combo
= gtk
.combo_box_new_text()
97 for mode
in self
.WIFI_MODES
:
98 self
.mode_combo
.append_text(mode
)
100 self
.mode_combo
.set_active(self
.WIFI_MODES
.index(self
.profile
['mode']))
102 # If the preferred mode is not recognized,
103 # do not set an active item.
104 self
.mode_combo
.set_active(-1)
105 self
.general_table
.attach(self
.mode_combo
, 1, 2, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
106 self
.mode_combo
.set_tooltip_text("Method to use for connection. You probably want auto mode.")
108 self
.general_table
.attach(gtk
.Label('Channel'), 0, 1, 4, 5, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
109 self
.channel_combo
= gtk
.combo_box_new_text()
110 for channel
in self
.WIFI_CHANNELS
:
111 self
.channel_combo
.append_text(channel
)
113 self
.channel_combo
.set_active(self
.WIFI_CHANNELS
.index(self
.profile
['channel']))
115 # If the preferred channel is not recognized,
116 # do not set an active item.
117 self
.channel_combo
.set_active(-1)
118 self
.general_table
.attach(self
.channel_combo
, 1, 2, 4, 5, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
119 self
.channel_combo
.set_tooltip_text("Channel the network uses. You probably want auto mode.")
121 self
.general_table
.attach(gtk
.HSeparator(), 0, 2, 5, 6, gtk
.FILL|gtk
.EXPAND
, 0, 5, 10)
123 self
.security_setting
= gtk
.Label('Security: ' + self
.profile
['security'].upper())
124 self
.general_table
.attach(self
.security_setting
, 0, 1, 6, 7, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
125 if self
.profile
['use_dhcp']:
126 self
.ip_config_setting
= gtk
.Label('IP Configuration: DHCP')
128 self
.ip_config_setting
= gtk
.Label('IP Configuration: Manual')
129 self
.general_table
.attach(self
.ip_config_setting
, 1, 2, 6, 7, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
130 self
.profile_notebook
.append_page(self
.general_table
, gtk
.Label("General"))
132 # create the WiFi security page
133 self
.security_table
= gtk
.Table()
134 self
.security_table
.set_row_spacings(3)
135 self
.security_table
.set_col_spacings(3)
137 self
.security_radio_none
= gtk
.RadioButton(label
="None")
138 self
.security_radio_wep
= gtk
.RadioButton(self
.security_radio_none
, label
="WEP")
139 self
.security_radio_wpa
= gtk
.RadioButton(self
.security_radio_none
, label
="WPA")
140 self
.security_radio_none
.connect("toggled", self
.toggle_security
)
141 self
.security_radio_wep
.connect("toggled", self
.toggle_security
)
142 self
.security_radio_wpa
.connect("toggled", self
.toggle_security
)
143 if self
.profile
['security'] == 'none':
144 self
.security_radio_none
.set_active(True)
145 if self
.profile
['security'] == 'wep':
146 self
.security_radio_wep
.set_active(True)
147 if self
.profile
['security'] == 'wpa':
148 self
.security_radio_wpa
.set_active(True)
149 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
150 self
.security_table
.attach(self
.security_radio_none
, 0, 1, 0, 1, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
151 self
.security_table
.attach(self
.security_radio_wep
, 0, 1, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
153 # ToggleButton to select ASCII/hex WEP key type
154 self
.wep_key_cb
= gtk
.CheckButton('Hex Key')
155 if self
.profile
['key'].startswith('s:'):
156 self
.wep_key_cb
.set_active(False)
157 #self.wep_key_cb.set_label('ASCII Key')
158 self
.profile
['key'] = self
.profile
['key'][2:]
160 self
.wep_key_cb
.set_active(True)
161 self
.security_table
.attach(self
.wep_key_cb
, 1, 2, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
162 #self.wep_key_cb.connect("toggled", self.toggle_wep_key_type)
163 self
.key_entry
= gtk
.Entry(64)
164 self
.key_entry
.set_text(self
.profile
['key'])
165 self
.security_table
.attach(self
.key_entry
, 2, 3, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
166 self
.key_entry
.set_tooltip_text("WEP key: Plain language or hex string to use for encrypted communication with the network.")
168 self
.wep_mode_label
= gtk
.Label('WEP Mode')
169 self
.wep_mode_label
.set_alignment(1, 0.5)
170 self
.security_table
.attach(self
.wep_mode_label
, 1, 2, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
171 self
.wep_mode_combo
= gtk
.combo_box_new_text()
172 for security
in self
.WEP_MODE
:
173 self
.wep_mode_combo
.append_text(security
)
175 self
.wep_mode_combo
.set_active(self
.WEP_MODE
.index(self
.profile
['security']))
177 # If the preferred security mode is not recognized,
178 # do not set an active item.
179 self
.wep_mode_combo
.set_active(-1)
180 self
.security_table
.attach(self
.wep_mode_combo
, 2, 3, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
181 self
.wep_mode_combo
.set_tooltip_text("Use Open to allow unencrypted communication and Restricted to force encrypted-only communication.")
183 self
.security_table
.attach(self
.security_radio_wpa
, 0, 1, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
185 self
.psk_label
= gtk
.Label('PSK')
186 self
.psk_label
.set_alignment(1, 0.5)
187 self
.security_table
.attach(self
.psk_label
, 1, 2, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
189 self
.wpa_psk_entry
= gtk
.Entry()
190 self
.wpa_psk_entry
.set_text(self
.profile
['wpa_psk'])
191 self
.security_table
.attach(self
.wpa_psk_entry
, 2, 3, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
192 self
.profile_notebook
.append_page(self
.security_table
, gtk
.Label("Security"))
194 # create the network config page
195 self
.net_config_page
= gtk
.VBox()
197 self
.dhcp_cb
= gtk
.CheckButton('Automatic Network Configuration (DHCP)')
198 self
.dhcp_cb
.set_active(self
.profile
['use_dhcp'])
199 self
.dhcp_cb
.set_alignment(0.5, 0.5)
200 self
.dhcp_cb
.connect("toggled", self
.toggle_dhcp
)
201 self
.net_config_page
.pack_start(self
.dhcp_cb
, True, True, 5)
204 self
.ip_table
= gtk
.Table()
205 self
.ip_table
.set_row_spacings(3)
206 self
.ip_table
.set_col_spacings(3)
208 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
209 self
.ip_table
.attach(gtk
.Label('IP'), 0, 1, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
210 self
.ip_entry
= gtk
.Entry(15)
211 self
.ip_entry
.set_text(self
.profile
['ip'])
212 self
.ip_table
.attach(self
.ip_entry
, 1, 2, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
213 self
.ip_table
.attach(gtk
.Label('Netmask'), 0, 1, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
214 self
.netmask_entry
= gtk
.Entry(15)
215 self
.netmask_entry
.set_text(self
.profile
['netmask'])
216 self
.ip_table
.attach(self
.netmask_entry
, 1, 2, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
217 self
.ip_table
.attach(gtk
.Label('Gateway'), 0, 1, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
218 self
.gw_entry
= gtk
.Entry(15)
219 self
.gw_entry
.set_text(self
.profile
['gateway'])
220 self
.ip_table
.attach(self
.gw_entry
, 1, 2, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
221 self
.ip_table
.attach(gtk
.Label('Domain'), 0, 1, 4, 5, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
222 self
.domain_entry
= gtk
.Entry(32)
223 self
.domain_entry
.set_text(self
.profile
['domain'])
224 self
.ip_table
.attach(self
.domain_entry
, 1, 2, 4, 5, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
225 self
.ip_table
.attach(gtk
.Label('First DNS'), 0, 1, 5, 6, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
226 self
.dns1_entry
= gtk
.Entry(15)
227 self
.dns1_entry
.set_text(self
.profile
['dns1'])
228 self
.ip_table
.attach(self
.dns1_entry
, 1, 2, 5, 6, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
229 self
.ip_table
.attach(gtk
.Label('Second DNS'), 0, 1, 6, 7, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
230 self
.dns2_entry
= gtk
.Entry(15)
231 self
.dns2_entry
.set_text(self
.profile
['dns2'])
232 self
.ip_table
.attach(self
.dns2_entry
, 1, 2, 6, 7, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
234 self
.net_config_page
.pack_start(self
.ip_table
, False, False, 0)
235 self
.profile_notebook
.append_page(self
.net_config_page
, gtk
.Label("IP Configuration"))
237 # create the scripting page
238 con_pp_table
= gtk
.Table()
239 con_pp_table
.set_row_spacings(3)
240 con_pp_table
.set_col_spacings(3)
242 self
.conn_before
= gtk
.Label('Before Connection')
243 self
.conn_before
.set_alignment(1, 0.5)
244 # (widget, l, r, t, b, xopt, yopt, xpad, ypad)
245 con_pp_table
.attach(self
.conn_before
, 0, 1, 0, 1, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
246 self
.conn_after
= gtk
.Label('After Connection')
247 self
.conn_after
.set_alignment(1, 0.5)
248 con_pp_table
.attach(self
.conn_after
, 0, 1, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
250 self
.con_prescript_entry
= gtk
.Entry()
251 self
.con_prescript_entry
.set_text(self
.profile
['con_prescript'])
252 con_pp_table
.attach(self
.con_prescript_entry
, 1, 2, 0, 1, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
253 self
.con_prescript_entry
.set_tooltip_text("The local command to execute before trying to connect to the network.")
254 self
.con_postscript_entry
= gtk
.Entry()
255 self
.con_postscript_entry
.set_text(self
.profile
['con_postscript'])
256 con_pp_table
.attach(self
.con_postscript_entry
, 1, 2, 1, 2, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
257 self
.con_postscript_entry
.set_tooltip_text("The local command to execute after connecting to the network.")
259 self
.dis_before
= gtk
.Label('Before Disconnection')
260 self
.dis_before
.set_alignment(1, 0.5)
261 con_pp_table
.attach(self
.dis_before
, 0, 1, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
262 self
.dis_after
= gtk
.Label('After Disconnection')
263 self
.dis_after
.set_alignment(1, 0.5)
264 con_pp_table
.attach(self
.dis_after
, 0, 1, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
266 self
.dis_prescript_entry
= gtk
.Entry()
267 self
.dis_prescript_entry
.set_text(self
.profile
['dis_prescript'])
268 con_pp_table
.attach(self
.dis_prescript_entry
, 1, 2, 2, 3, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
269 self
.dis_prescript_entry
.set_tooltip_text("The local command to execute before disconnecting from the network.")
270 self
.dis_postscript_entry
= gtk
.Entry()
271 self
.dis_postscript_entry
.set_text(self
.profile
['dis_postscript'])
272 con_pp_table
.attach(self
.dis_postscript_entry
, 1, 2, 3, 4, gtk
.FILL|gtk
.EXPAND
, 0, 5, 0)
273 self
.dis_postscript_entry
.set_tooltip_text("The local command to execute after disconnecting from the network.")
274 self
.profile_notebook
.append_page(con_pp_table
, gtk
.Label("Scripting"))
276 self
.vbox
.pack_start(self
.profile_notebook
, False, False, 5)
279 """ Display profile dialog and return an edited profile or None.
281 self
.set_security_sensitivity(self
.profile
['security'])
282 self
.ip_table
.set_sensitive(not self
.profile
['use_dhcp'])
287 result
= gtk
.Dialog
.run(self
)
288 if result
is not None:
289 if self
.essid_entry
.get_text().strip() == "":
291 self
.profile
['known'] = True
292 self
.profile
['essid'] = self
.essid_entry
.get_text().strip()
293 if self
.roaming_cb
.get_active():
294 self
.profile
['bssid'] = ''
296 self
.profile
['bssid'] = self
.bssid_entry
.get_text().strip()
297 self
.profile
['roaming'] = self
.roaming_cb
.get_active()
298 if self
.wep_key_cb
.get_active():
299 self
.profile
['key'] = '' # hex WEP key
300 if re
.compile("[^0-9a-fA-F]").search(self
.key_entry
.get_text().strip()):
301 dlg
= gtk
.MessageDialog(self
, gtk
.DIALOG_DESTROY_WITH_PARENT | gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
, "Invalid hex WEP key.")
307 self
.profile
['key'] = 's:' # ASCII WEP key
308 self
.profile
['key'] = self
.profile
['key'] + self
.key_entry
.get_text().strip()
309 if self
.mode_combo
.get_active_text() is None:
310 self
.profile
['mode'] = 'auto'
312 self
.profile
['mode'] = self
.mode_combo
.get_active_text()
313 self
.profile
['encrypted'] = (self
.profile
['security'] != '')
314 if self
.channel_combo
.get_active_text() is None:
315 self
.profile
['channel'] = 'auto'
317 self
.profile
['channel'] = self
.channel_combo
.get_active_text()
318 self
.profile
['protocol'] = 'g'
319 self
.profile
['available'] = (self
.profile
['signal'] > 0)
320 self
.profile
['con_prescript'] = self
.con_prescript_entry
.get_text().strip()
321 self
.profile
['con_postscript'] = self
.con_postscript_entry
.get_text().strip()
322 self
.profile
['dis_prescript'] = self
.dis_prescript_entry
.get_text().strip()
323 self
.profile
['dis_postscript'] = self
.dis_postscript_entry
.get_text().strip()
325 if self
.wep_mode_combo
.get_active_text() is None:
326 self
.profile
['wep_mode'] = 'none'
328 self
.profile
['wep_mode'] = self
.wep_mode_combo
.get_active_text()
329 self
.profile
['wpa_psk'] = self
.wpa_psk_entry
.get_text().strip()
331 self
.profile
['use_dhcp'] = self
.dhcp_cb
.get_active()
332 self
.profile
['ip'] = self
.ip_entry
.get_text().strip()
333 self
.profile
['netmask'] = self
.netmask_entry
.get_text().strip()
334 self
.profile
['gateway'] = self
.gw_entry
.get_text().strip()
335 self
.profile
['domain'] = self
.domain_entry
.get_text().strip()
336 self
.profile
['dns1'] = self
.dns1_entry
.get_text().strip()
337 self
.profile
['dns2'] = self
.dns2_entry
.get_text().strip()
342 def toggle_roaming(self
, roaming_toggle
, data
=None):
343 """ Respond to a roaming checkbox toggle by activating or de-activating
344 the BSSID text entry. :data:`roaming_toggle` is the
345 :class:`gtk.CheckButton` sending the signal. :data:`data` is a
346 (not used) list of arbitrary arguments.
348 self
.bssid_entry
.set_sensitive(not roaming_toggle
.get_active())
350 def toggle_security(self
, security_radio
, data
=None):
351 """ Respond to security radio toggles by activating or de-activating
352 the various security text entries. :data:`security_radio` is the
353 :class:`gtk.RadioButton` sending the signal. :data:`data` is a
354 (not used) list of arbitrary arguments.
356 if security_radio
== self
.security_radio_none
:
357 self
.profile
['security'] = 'none'
358 elif security_radio
== self
.security_radio_wep
:
359 self
.profile
['security'] = 'wep'
360 elif security_radio
== self
.security_radio_wpa
:
361 self
.profile
['security'] = 'wpa'
362 self
.set_security_sensitivity(self
.profile
['security'])
363 self
.security_setting
.set_text('Security: ' + self
.profile
['security'].upper())
365 def set_security_sensitivity(self
, security_mode
):
366 """ Set the sensitivity of the various security text entries.
367 :data:`security_mode` is the security mode: 'none', 'wep', or
370 if security_mode
== 'none':
371 self
.wep_key_cb
.set_sensitive(False)
372 self
.key_entry
.set_sensitive(False)
373 self
.wep_mode_label
.set_sensitive(False)
374 self
.wep_mode_combo
.set_sensitive(False)
375 self
.psk_label
.set_sensitive(False)
376 self
.wpa_psk_entry
.set_sensitive(False)
377 elif security_mode
== 'wep':
378 self
.wep_key_cb
.set_sensitive(True)
379 self
.key_entry
.set_sensitive(True)
380 self
.wep_mode_label
.set_sensitive(True)
381 self
.wep_mode_combo
.set_sensitive(True)
382 self
.psk_label
.set_sensitive(False)
383 self
.wpa_psk_entry
.set_sensitive(False)
384 elif security_mode
== 'wpa':
385 self
.wep_key_cb
.set_sensitive(False)
386 self
.key_entry
.set_sensitive(False)
387 self
.wep_mode_label
.set_sensitive(False)
388 self
.wep_mode_combo
.set_sensitive(False)
389 self
.psk_label
.set_sensitive(True)
390 self
.wpa_psk_entry
.set_sensitive(True)
392 def toggle_dhcp(self
, widget
=None, data
=None):
393 """ Respond to clicking the DHCP check button. :data:`widget`
394 is the widget sending the signal and :data:`data` is a list
395 of arbitrary arguments, both are ignored.
397 self
.ip_table
.set_sensitive(not self
.dhcp_cb
.get_active())
400 # Make so we can be imported
401 if __name__
== "__main__":