Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
3-manifolds
GitHub Repository: 3-manifolds/Sage_macOS
Path: blob/main/main.py
169 views
1
import sys
2
import re
3
import os
4
from os.path import pardir, abspath, join as path_join
5
import subprocess
6
import signal
7
import json
8
import time
9
import plistlib
10
import webbrowser
11
import tkinter
12
from tkinter import ttk
13
from tkinter.font import Font
14
from tkinter.simpledialog import Dialog, askstring
15
from tkinter.filedialog import askdirectory
16
from tkinter.messagebox import showerror, showwarning, askyesno, askokcancel
17
from tkinter.scrolledtext import ScrolledText
18
from sage.version import version as sage_version
19
import os
20
import plistlib
21
import platform
22
import certifi
23
24
this_python = 'python' + '.'.join(platform.python_version_tuple()[:2])
25
contents_dir = abspath(path_join(sys.argv[0], pardir, pardir))
26
frameworks_dir = path_join(contents_dir, 'Frameworks')
27
info_plist = path_join(contents_dir, 'Info.plist')
28
current = path_join(frameworks_dir, 'Sage.framework', 'Versions', 'Current')
29
30
def get_version():
31
with open(info_plist, 'rb') as plist_file:
32
info = plistlib.load(plist_file)
33
return info['CFBundleShortVersionString']
34
35
sagemath_version = get_version()
36
app_name = 'SageMath-%s' % sagemath_version.replace('.', '-')
37
app_support_dir = path_join(os.environ['HOME'], 'Library', app_name)
38
settings_path = path_join(app_support_dir, 'Settings.plist')
39
jupyter_runtime_dir = os.path.join(app_support_dir, 'Jupyter', 'runtime')
40
sage_userbase = app_support_dir
41
sage_executable = path_join(current, 'venv', 'bin', 'sage')
42
sage_jupyter_path = path_join(current, 'venv', 'share', 'jupyter')
43
44
jp_pid_re = re.compile('jpserver-([0-9]*).*')
45
46
class PopupMenu(ttk.Menubutton):
47
def __init__(self, parent, variable, values):
48
ttk.Menubutton.__init__(self, parent, textvariable=variable,
49
direction='flush')
50
self.parent = parent
51
self.variable = variable
52
self.update(values)
53
54
def update(self, values):
55
self.variable.set(values[0])
56
self.menu = tkinter.Menu(self.parent, tearoff=False)
57
for value in values:
58
self.menu.add_radiobutton(label=value, variable=self.variable)
59
self.config(menu=self.menu)
60
61
class Launcher:
62
jp_json_re = re.compile(r'jpserver-[0-9]*\.json')
63
url_fmt = 'http://localhost:{port}/{nb_type}?token={token}'
64
terminal_script = """
65
set command to "%s"
66
tell application "System Events"
67
set terminalProcesses to application processes whose name is "Terminal"
68
end tell
69
if terminalProcesses is {} then
70
set terminalIsRunning to false
71
else
72
set terminalIsRunning to true
73
end if
74
if terminalIsRunning then
75
tell application "Terminal"
76
activate
77
do script command
78
end tell
79
else
80
-- avoid opening two windows
81
tell application "Terminal"
82
activate
83
do script command in window 1
84
end tell
85
end if
86
"""
87
88
iterm_script = """
89
set sageCommand to "/bin/bash -c \\"%s\\""
90
tell application "iTerm"
91
set sageWindow to (create window with default profile command sageCommand)
92
select sageWindow
93
end tell
94
"""
95
96
find_app_script = """
97
set appExists to false
98
try
99
tell application "Finder" to get application file id "%s"
100
set appExists to true
101
end try
102
return appExists
103
"""
104
105
def check_notebook_dir(self):
106
notebook_dir = self.notebook_dir.get()
107
if not notebook_dir.strip():
108
showwarning(parent=self,
109
message="Please choose or create a folder for your Jupyter notebooks.")
110
return False
111
if not os.path.exists(notebook_dir):
112
answer = askyesno(message='May we create the folder %s?'%notebook_dir)
113
if answer == tkinter.YES:
114
os.makedirs(notebook_dir, exist_ok=True)
115
else:
116
return False
117
try:
118
os.listdir(notebook_dir)
119
except:
120
showerror(message='Sorry. We do not have permission to read %s'%directory)
121
return False
122
return True
123
124
def find_app(self, bundle_id):
125
script = self.find_app_script%bundle_id
126
result = subprocess.run(['osascript', '-'], input=script, text=True,
127
capture_output=True)
128
return result.stdout.strip() == 'true'
129
130
def launch_terminal(self, app):
131
env = dict(self.environment)
132
env['PYTHONUSERBASE'] = sage_userbase
133
env['SSL_CERT_FILE'] = certifi.where()
134
env_str = " ".join(rf"{key}='{value}'" for key, value in env.items())
135
if app == 'Terminal.app':
136
sage_cmd = 'clear ; /usr/bin/env %s %s ; exit' % (env_str, sage_executable)
137
script = self.terminal_script % sage_cmd
138
subprocess.run(['osascript', '-'], input=script, text=True,
139
capture_output=True)
140
elif app == 'iTerm.app':
141
subprocess.run(['open', '-a', 'iTerm'], capture_output=True)
142
sage_cmd = '/usr/bin/env %s %s' % (env_str, sage_executable)
143
script = self.iterm_script % sage_cmd
144
subprocess.run(['osascript', '-'], input=script, text=True,
145
capture_output=True)
146
return True
147
148
def launch_notebook(self, notebook_type):
149
if not self.check_notebook_dir():
150
return False
151
notebook_dir = self.notebook_dir.get()
152
environ = {
153
'JUPYTER_RUNTIME_DIR': jupyter_runtime_dir,
154
'JUPYTER_PATH': sage_jupyter_path,
155
'PYTHONUSERBASE': sage_userbase,
156
'SSL_CERT_FILE': certifi.where()
157
}
158
environ.update(os.environ)
159
json_files = [f for f in os.listdir(jupyter_runtime_dir)
160
if self.jp_json_re.match(f)]
161
if json_files:
162
filename = os.path.join(jupyter_runtime_dir, json_files[0])
163
with open(filename) as json_file:
164
info = json.load(json_file)
165
info['nb_type'] = 'lab' if notebook_type=='jupyterlab' else 'tree'
166
if info['root_dir'] == notebook_dir:
167
url = self.url_fmt.format(**info)
168
subprocess.run(['open', url], env=environ)
169
return True
170
sage_executable = path_join(frameworks_dir, 'sage.framework', 'Versions',
171
'Current', 'venv', 'bin', 'sage')
172
subprocess.Popen([sage_executable, '-n', notebook_type,
173
'--notebook-dir=%s'%notebook_dir], env=environ)
174
return True
175
176
class LaunchWindow(tkinter.Toplevel, Launcher):
177
178
def __init__(self, root):
179
Launcher.__init__(self)
180
self.get_settings()
181
self.root = root
182
tkinter.Toplevel.__init__(self)
183
self.tk.call('::tk::unsupported::MacWindowStyle', 'style', self._w,
184
'document', 'closeBox')
185
self.protocol("WM_DELETE_WINDOW", self.close)
186
self.title('SageMath')
187
self.columnconfigure(0, weight=1)
188
frame = ttk.Frame(self, padding=10, width=300)
189
frame.columnconfigure(0, weight=1)
190
frame.grid(row=0, column=0, sticky='nsew')
191
self.update_idletasks()
192
# Logo
193
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir,
194
'Resources'))
195
logo_file = path_join(resource_dir, 'sage_logo_256.png')
196
try:
197
self.logo_image = tkinter.PhotoImage(file=logo_file)
198
logo = ttk.Label(frame, image=self.logo_image)
199
except tkinter.TclError:
200
logo = ttk.Label(frame, text='Logo Here')
201
# Interfaces
202
interfaces = ttk.Labelframe(frame, text="Available User Interfaces",
203
padding=10)
204
self.radio_var = radio_var = tkinter.Variable(interfaces,
205
self.settings['state']['interface_type'])
206
self.use_cli = ttk.Radiobutton(interfaces, text="Command line",
207
variable=radio_var, value='cli',
208
command=self.update_radio_buttons)
209
self.terminals = ['Terminal.app']
210
if self.find_app('com.googlecode.iterm2'):
211
if self.settings['state']['terminal_app'] == 'iTerm.app':
212
self.terminals.insert(0, 'iTerm.app')
213
else:
214
self.terminals.append('iTerm.app')
215
self.terminal_var = tkinter.Variable(self, self.terminals[0])
216
self.terminal_option = PopupMenu(interfaces,
217
self.terminal_var, self.terminals)
218
self.use_jupyter = ttk.Radiobutton(interfaces, text="Notebook",
219
variable=radio_var, value='nb', command=self.update_radio_buttons)
220
self.notebook_types = ['Jupyter Notebook', 'JupyterLab']
221
favorite = self.settings['state']['notebook_type']
222
if favorite != 'Jupyter Notebook' and favorite in self.notebook_types:
223
self.notebook_types.remove(favorite)
224
self.notebook_types.insert(0, favorite)
225
self.nb_var = tkinter.Variable(self, self.notebook_types[0])
226
self.notebook_option = PopupMenu(interfaces, self.nb_var,
227
self.notebook_types)
228
notebook_dir_frame = ttk.Frame(interfaces)
229
ttk.Label(notebook_dir_frame, text='Using notebooks from:').grid(
230
row=0, column=0, sticky='w', padx=12)
231
self.notebook_dir = ttk.Entry(notebook_dir_frame, width=24)
232
self.notebook_dir.insert(tkinter.END, self.settings['state']['notebook_dir'])
233
self.notebook_dir.config(state='readonly')
234
self.browse = ttk.Button(notebook_dir_frame, text='Select ...', padding=(-8, 0),
235
command=self.browse_notebook_dir, state=tkinter.DISABLED)
236
self.notebook_dir.grid(row=1, column=0, padx=8)
237
self.browse.grid(row=1, column=1)
238
# Build the interfaces frame
239
self.use_cli.grid(row=0, column=0, sticky='w', pady=5)
240
self.terminal_option.grid(row=1, column=0, sticky='w', padx=10, pady=5)
241
self.use_jupyter.grid(row=2, column=0, sticky='w', pady=5)
242
self.notebook_option.grid(row=3, column=0, sticky='w', padx=10, pady=5)
243
notebook_dir_frame.grid(row=4, column=0, sticky='w', pady=5)
244
# Launch button
245
launch_frame = ttk.Frame(frame)
246
self.launch = ttk.Button(launch_frame, text="Launch", command=self.launch_sage)
247
self.launch.pack()
248
# Build the window
249
logo.grid(row=0, column=0, pady=5)
250
interfaces.grid(row=2, column=0, padx=10, pady=10, sticky='ew')
251
launch_frame.grid(row=3, column=0)
252
self.geometry('380x390+400+400')
253
self.update_radio_buttons()
254
self.update_idletasks()
255
256
def close(self):
257
self.withdraw()
258
259
default_settings = {
260
'environment': {
261
},
262
'state': {
263
'interface_type': 'cli',
264
'terminal_app': 'Terminal.app',
265
'notebook_type': 'Jupyter Notebook',
266
'notebook_dir': '',
267
},
268
}
269
270
def get_settings(self):
271
# The settings are described by a dict with dict values.
272
settings = self.default_settings.copy()
273
try:
274
with open(settings_path, 'rb') as settings_file:
275
saved_settings = plistlib.load(settings_file)
276
except:
277
#settings file missing or corrupt
278
saved_settings = None
279
if saved_settings:
280
for key in settings:
281
settings[key].update(saved_settings.get(key, {}))
282
self.settings = settings
283
284
def save_settings(self):
285
self.get_settings()
286
self.settings['state'].update(
287
{
288
'interface_type': self.radio_var.get(),
289
'terminal_app': self.terminal_var.get(),
290
'notebook_type': self.nb_var.get(),
291
'notebook_dir': self.notebook_dir.get(),
292
}
293
)
294
try:
295
with open(settings_path, 'wb') as settings_file:
296
plistlib.dump(self.settings, settings_file)
297
except:
298
pass
299
300
def update_radio_buttons(self):
301
radio = self.radio_var.get()
302
if radio == 'cli':
303
self.notebook_dir.config(state=tkinter.DISABLED)
304
self.browse.config(state=tkinter.DISABLED)
305
self.terminal_option.config(state=tkinter.NORMAL)
306
self.notebook_option.config(state=tkinter.DISABLED)
307
elif radio == 'nb':
308
self.notebook_dir.config(state='readonly')
309
self.browse.config(state=tkinter.NORMAL)
310
self.notebook_option.config(state=tkinter.NORMAL)
311
self.terminal_option.config(state=tkinter.DISABLED)
312
313
def update_environment(self):
314
required_paths = [
315
'/var/tmp/sage-10.3-current/local/bin',
316
'/var/tmp/sage-10.3-current/venv/bin',
317
'/bin',
318
'/usr/bin',
319
'/usr/local/bin',
320
'/Library/TeX/texbin'
321
]
322
try:
323
with open(settings_path, 'rb') as settings_file:
324
settings = plistlib.load(settings_file)
325
environment = settings.get('environment', {})
326
except:
327
environment = {}
328
# Try to prevent users from crippling Sage with a weird PATH.
329
user_paths = environment.get('PATH', '').split(':')
330
# Avoid including the empty path.
331
paths = [path for path in user_paths if path] + required_paths
332
unique_paths = list(dict.fromkeys(paths))
333
environment['PATH'] = ':'.join(unique_paths)
334
self.environment = environment
335
336
def launch_sage(self):
337
self.update_environment()
338
interface = self.radio_var.get()
339
if interface == 'cli':
340
launched = self.launch_terminal(app=self.terminal_var.get())
341
elif interface == 'nb':
342
app = self.nb_var.get()
343
if not app in self.notebook_types:
344
app = 'Jupyter Notebook'
345
self.nb_var.set(app)
346
if app == 'JupyterLab':
347
launched = self.launch_notebook('jupyterlab')
348
else:
349
launched = self.launch_notebook('jupyter')
350
if launched:
351
self.save_settings()
352
self.close()
353
354
def browse_notebook_dir(self):
355
directory = askdirectory(parent=self, initialdir=os.environ['HOME'],
356
message='Choose or create a folder for Jupyter notebooks')
357
if directory:
358
self.notebook_dir.config(state=tkinter.NORMAL)
359
self.notebook_dir.delete(0, tkinter.END)
360
self.notebook_dir.insert(tkinter.END, directory)
361
self.notebook_dir.config(state='readonly')
362
363
class AboutDialog(Dialog):
364
def __init__(self, parent, title='', content=''):
365
self.content = content
366
self.style = ttk.Style(parent)
367
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources'))
368
logo_file = path_join(resource_dir, 'sage_logo_256.png')
369
try:
370
self.logo_image = tkinter.PhotoImage(file=logo_file)
371
except tkinter.TclError:
372
self.logo_image = None
373
Dialog.__init__(self, parent, title=title)
374
375
def body(self, parent):
376
self.resizable(False, False)
377
frame = ttk.Frame(self)
378
if self.logo_image:
379
logo = ttk.Label(frame, image=self.logo_image)
380
else:
381
logo = ttk.Label(frame, text='Logo Here')
382
logo.grid(row=0, column=0, padx=20, pady=20, sticky='n')
383
message = tkinter.Message(frame, text=self.content)
384
message.grid(row=1, column=0, padx=20, sticky='ew')
385
frame.pack()
386
387
def buttonbox(self):
388
frame = ttk.Frame(self, padding=(0, 0, 0, 20))
389
ok = ttk.Button(frame, text="OK", width=10, command=self.ok,
390
default=tkinter.ACTIVE)
391
ok.grid(row=2, column=0, padx=5, pady=5)
392
self.bind("<Return>", self.ok)
393
self.bind("<Escape>", self.ok)
394
frame.pack()
395
396
class InfoDialog(Dialog):
397
def __init__(self, parent, title='', message='',
398
text_width=40, text_height=12, font_size=16):
399
self.message = message
400
self.text_width, self.text_height = text_width, text_height
401
self.text_font = tkinter.font.Font()
402
self.text_font.config(size=font_size)
403
self.style = ttk.Style(parent)
404
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources'))
405
logo_file = path_join(resource_dir, 'sage_logo_256.png')
406
try:
407
self.logo_image = tkinter.PhotoImage(file=logo_file)
408
except tkinter.TclError:
409
self.logo_image = None
410
Dialog.__init__(self, parent, title=title)
411
412
def body(self, parent):
413
self.resizable(False, False)
414
frame = ttk.Frame(self)
415
if self.logo_image:
416
logo = ttk.Label(frame, image=self.logo_image)
417
else:
418
logo = ttk.Label(frame, text='Logo Here')
419
logo.grid(row=0, column=0, padx=20, pady=20, sticky='n')
420
font = tkinter.font.Font()
421
font.config(size=18)
422
text = tkinter.Text(frame, wrap=tkinter.WORD, bd=0,
423
highlightthickness=0, bg='SystemWindowBackgroundColor',
424
width=self.text_width, height=self.text_height,
425
font=self.text_font)
426
text.grid(row=1, column=0, padx=20, sticky='ew')
427
text.insert(tkinter.INSERT, self.message)
428
text.config(state=tkinter.DISABLED)
429
frame.pack()
430
431
def buttonbox(self):
432
frame = ttk.Frame(self, padding=(0, 0, 0, 20))
433
ok = ttk.Button(frame, text="OK", width=10, command=self.ok,
434
default=tkinter.ACTIVE)
435
ok.grid(row=2, column=0, padx=5, pady=5)
436
self.bind("<Return>", self.ok)
437
self.bind("<Escape>", self.ok)
438
frame.pack()
439
440
class EnvironmentEditor(tkinter.Toplevel):
441
def __init__(self, parent):
442
tkinter.Toplevel.__init__(self, parent)
443
self.parent = parent
444
self.wm_protocol('WM_DELETE_WINDOW', self.close)
445
self.title('Sage Environment')
446
home = os.environ['HOME']
447
self.load_settings()
448
self.environment = self.settings.get('environment', {})
449
self.varlist = list(self.environment.keys())
450
self.add = tkinter.Image('nsimage', name='add', source='NSAddTemplate',
451
width=20, height=20)
452
self.remove = tkinter.Image('nsimage', name='remove', source='NSRemoveTemplate',
453
width=20, height=4)
454
self.left = ttk.Frame(self, padding=0)
455
ttk.Label(self, text = 'Variable').grid(row=0, column=0, padx=10, sticky='w')
456
ttk.Label(self, text = 'Value').grid(row=0, column=1, sticky='w')
457
self.varnames = tkinter.StringVar(self)
458
if self.varlist:
459
self.varnames.set(self.varlist)
460
self.listbox = tkinter.Listbox(self.left, selectmode='browse',
461
listvariable=self.varnames, height=19)
462
self.listbox.grid(row=1, column=0, columnspan=2, sticky='nsew')
463
button_frame = ttk.Frame(self.left, padding=(0, 4, 0, 10))
464
ttk.Button(button_frame, style="GradientButton", image='add',
465
command=self.add_var).grid(row=0, column=0, sticky='nw')
466
ttk.Button(button_frame, style="GradientButton", image='remove',
467
padding=(0,8), command=self.remove_var).grid(row=0, column=1, sticky='nw')
468
button_frame.grid(row=2, column=0, sticky='nw')
469
self.columnconfigure(1, weight=1)
470
self.rowconfigure(1, weight=1)
471
self.left.grid(row=1, rowspan=2, column=0, sticky='nsw', padx=10, pady=10)
472
self.text = ScrolledText(self)
473
self.text.frame.grid(row=1, column=1, pady=10, sticky='nsew')
474
ttk.Button(self, text='Done', command = self.done).grid(
475
row=2, column=1, pady=20, padx=20, sticky='es')
476
self.listbox.bind("<<ListboxSelect>>",
477
lambda e: self.update())
478
self.selected = None
479
if self.varlist:
480
self.listbox.selection_set(0)
481
self.update()
482
483
def update(self):
484
if self.selected is not None:
485
current_value = self.text.get('0.0', 'end').strip()
486
self.environment[self.listbox.get(self.selected)] = current_value
487
selection = self.listbox.curselection()
488
if not selection:
489
return
490
selection = selection[0]
491
self.selected = selection
492
self.text.delete('0.0', 'end')
493
var = self.listbox.get(selection).strip()
494
value = self.environment.get(var, '')
495
if value:
496
self.text.insert('0.0', value)
497
498
def add_var(self):
499
self.update()
500
new_var = askstring('New Variable', 'Variable Name:')
501
self.environment[new_var] = ''
502
self.text.delete('0.0', 'end')
503
self.selected = len(self.varlist)
504
self.varlist.append(new_var)
505
self.listbox.insert('end', new_var)
506
self.listbox.selection_clear(0, 'end')
507
self.listbox.selection_set(self.selected)
508
self.listbox.see(self.selected)
509
self.text.focus_set()
510
511
def remove_var(self):
512
selection = self.listbox.curselection()
513
if not selection:
514
return
515
var = self.listbox.get(selection[0])
516
self.varlist.remove(var)
517
self.environment.pop(var)
518
self.text.delete('0.0', 'end')
519
self.varnames.set(self.varlist)
520
if '' in self.environment:
521
self.environment.pop('')
522
523
def go(self):
524
self.transient(self.parent)
525
self.grab_set()
526
self.wait_window(self)
527
528
def load_settings(self):
529
if os.path.exists(settings_path):
530
try:
531
with open(settings_path, 'rb') as settings_file:
532
self.settings = plistlib.load(settings_file)
533
except plistlib.InvalidFileException:
534
os.unlink(settings_path)
535
self.settings = {}
536
else:
537
self.settings = {}
538
539
def done(self):
540
self.update()
541
if '' in self.environment:
542
self.environment.pop('')
543
self.settings['environment'] = self.environment
544
with open(settings_path, 'wb') as settings_file:
545
plistlib.dump(self.settings, settings_file)
546
self.destroy()
547
548
def close(self):
549
if askokcancel(message=''
550
'Closing the window will cause your changes to be lost.'):
551
self.destroy()
552
553
class SageApp(Launcher):
554
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources'))
555
icon_file = abspath(path_join(resource_dir, 'sage_icon_1024.png'))
556
about = """
557
SageMath is a free open-source mathematics software system licensed under the GPL. Please visit sagemath.org for more information about SageMath.
558
559
This SageMath app contains a subset of the SageMath binary distribution available from sagemath.org. It is packaged as a component of the 3-manifolds project by Marc Culler, Nathan Dunfield, and Matthias Gӧrner. It is licensed under the GPL License, version 2 or later, and can be downloaded from
560
https://github.com/3-manifolds/Sage_macOS/releases.
561
562
The app is copyright © 2021 by Marc Culler, Nathan Dunfield, Matthias Gӧrner and others.
563
"""
564
565
def __init__(self):
566
os.makedirs(jupyter_runtime_dir, exist_ok=True)
567
os.makedirs(sage_userbase, exist_ok=True)
568
self.root_window = root = tkinter.Tk()
569
root.withdraw()
570
os.chdir(os.environ['HOME'])
571
self.icon = tkinter.Image("photo", file=self.icon_file)
572
root.tk.call('wm','iconphoto', root._w, self.icon)
573
self.menubar = menubar = tkinter.Menu(root)
574
root.createcommand('::tk::mac::ShowPreferences', self.edit_env)
575
apple_menu = tkinter.Menu(menubar, name="apple")
576
apple_menu.add_command(label='About SageMath ...',
577
command=self.about_sagemath)
578
menubar.add_cascade(menu=apple_menu)
579
root.config(menu=menubar)
580
ttk.Label(root, text="SageMath").pack(padx=20, pady=20)
581
582
def about_sagemath(self):
583
AboutDialog(self.root_window, 'SageMath', self.about)
584
585
def edit_env(self):
586
editor = EnvironmentEditor(self.launcher)
587
editor.go()
588
589
def run(self):
590
symlink = path_join(os.path.sep, 'var', 'tmp',
591
'sage-%s-current' % sagemath_version)
592
self.launcher = LaunchWindow(root=self.root_window)
593
if not os.path.islink(symlink):
594
try:
595
os.symlink(current, symlink)
596
except Exception as e:
597
showwarning(parent=self.root_window,
598
message="%s Cannot create %s; "
599
"SageMath must exit."%(e, symlink))
600
sys.exit(1)
601
self.root_window.createcommand('tk::mac::ReopenApplication',
602
self.launcher.deiconify)
603
self.root_window.createcommand('tk::mac::Quit', self.quit)
604
self.root_window.mainloop()
605
606
def shutdown_servers(self):
607
try:
608
jp_files = os.listdir(jupyter_runtime_dir)
609
except:
610
return
611
pids = set()
612
for filename in jp_files:
613
m = jp_pid_re.match(filename)
614
if m:
615
pids.add(m.groups()[0])
616
if pids:
617
answer = askokcancel(
618
message='Quitting the SageMath app will terminate '
619
'all notebooks. Unsaved changes will be lost.')
620
if answer == False:
621
return False
622
for pid in pids:
623
try:
624
os.kill(int(pid), signal.SIGTERM)
625
except:
626
pass
627
for filename in jp_files:
628
if filename == 'jupyter_cookie_secret':
629
continue
630
if os.path.exists(filename):
631
os.unlink(filename)
632
return True
633
634
def quit(self):
635
if self.shutdown_servers():
636
self.launcher.destroy()
637
self.root_window.destroy()
638
639
if __name__ == '__main__':
640
SageApp().run()
641
642