-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
516 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,329 @@ | ||
#!/bin/sh | ||
# Call the shell's exec command to run this script with python3. We | ||
# actually call a symlink named SageMath. This is done so that the | ||
# app name will be SageMath rather than python. | ||
"exec" "`dirname $0`/../Frameworks/Sage.framework/Versions/Current/local/bin/SageMath" "$0" "$@" | ||
|
||
import sys | ||
import re | ||
import os | ||
from os.path import pardir, abspath, join as path_join | ||
import subprocess | ||
import signal | ||
import json | ||
import time | ||
import tkinter | ||
from tkinter import ttk | ||
from tkinter.font import Font | ||
from tkinter.simpledialog import Dialog | ||
from tkinter.filedialog import askdirectory | ||
from tkinter.messagebox import showerror, showwarning, askyesno | ||
jupyter_id = re.compile('nbserver-([0-9]+)-open.html') | ||
jupyter_runtime_dir = path_join(os.environ['HOME'], 'Library', 'Application Support', | ||
'SageMath', 'Jupyter') | ||
|
||
class PopupMenu(ttk.Menubutton): | ||
def __init__(self, parent, variable, values): | ||
ttk.Menubutton.__init__(self, parent, textvariable=variable, | ||
direction='flush') | ||
self.parent = parent | ||
self.variable = variable | ||
self.update(values) | ||
|
||
def update(self, values): | ||
self.variable.set(values[0]) | ||
self.menu = tkinter.Menu(self.parent, tearoff=False) | ||
for value in values: | ||
self.menu.add_radiobutton(label=value, variable=self.variable) | ||
self.config(menu=self.menu) | ||
|
||
class Launcher: | ||
framework_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Frameworks')) | ||
sage_executable = path_join(framework_dir, 'Sage.framework', 'Versions', 'Current', | ||
'local', 'bin', 'sage') | ||
sage_cmd = 'clear ; %s ; exit'%sage_executable | ||
terminal_script = """ | ||
set command to "%s" | ||
tell application "System Events" | ||
set terminalProcesses to application processes whose name is "Terminal" | ||
end tell | ||
if terminalProcesses is {} then | ||
set terminalIsRunning to false | ||
else | ||
set terminalIsRunning to true | ||
end if | ||
if terminalIsRunning then | ||
tell application "Terminal" | ||
activate | ||
do script command | ||
end tell | ||
else | ||
-- avoid opening two windows | ||
tell application "Terminal" | ||
activate | ||
do script command in window 1 | ||
end tell | ||
end if | ||
"""%sage_cmd | ||
|
||
iterm_script = """ | ||
set sageCommand to "/bin/bash -c '%s'" | ||
tell application "iTerm" | ||
set sageWindow to (create window with default profile command sageCommand) | ||
select sageWindow | ||
end tell | ||
"""%sage_cmd | ||
|
||
find_app_script = """ | ||
set appExists to false | ||
try | ||
tell application "Finder" to get application file id "%s" | ||
set appExists to true | ||
end try | ||
return appExists | ||
""" | ||
|
||
def launch_terminal(self, app): | ||
if app == 'Terminal.app': | ||
subprocess.run(['osascript', '-'], input=self.terminal_script, text=True, | ||
capture_output=True) | ||
elif app == 'iTerm.app': | ||
subprocess.run(['open', '-a', 'iTerm'], capture_output=True) | ||
subprocess.run(['osascript', '-'], input=self.iterm_script, text=True, | ||
capture_output=True) | ||
return True | ||
|
||
def launch_notebook(self, url=None): | ||
environ = {'JUPYTER_RUNTIME_DIR': jupyter_runtime_dir} | ||
environ.update(os.environ) | ||
if url is None: | ||
if not self.check_notebook_dir(): | ||
return False | ||
nb_pref_file = path_join(jupyter_runtime_dir, 'notebook_dir') | ||
jupyter_notebook_dir = self.notebooks.get() | ||
if not jupyter_notebook_dir: | ||
jupyter_notebook_dir = os.environ['HOME'] | ||
else: | ||
with open(nb_pref_file, 'w') as output: | ||
output.write('%s\n'%jupyter_notebook_dir) | ||
subprocess.Popen([self.sage_executable, '--jupyter', 'notebook', | ||
'--notebook-dir=%s'%jupyter_notebook_dir], env=environ) | ||
else: | ||
subprocess.run(['open', url], env=environ, capture_output=True) | ||
return True | ||
|
||
def find_app(self, bundle_id): | ||
script = self.find_app_script%bundle_id | ||
result = subprocess.run(['osascript', '-'], input=script, text=True, | ||
capture_output=True) | ||
return result.stdout.strip() == 'true' | ||
|
||
class LaunchWindow(tkinter.Toplevel, Launcher): | ||
def __init__(self, root): | ||
nb_pref_file = path_join(jupyter_runtime_dir, 'notebook_dir') | ||
if os.path.exists(nb_pref_file): | ||
with open(nb_pref_file) as infile: | ||
notebook_dir = infile.read().strip() | ||
else: | ||
notebook_dir = '' | ||
self.root = root | ||
tkinter.Toplevel.__init__(self) | ||
self.tk.call('::tk::unsupported::MacWindowStyle', 'style', self._w, | ||
'document', 'closeBox') | ||
self.protocol("WM_DELETE_WINDOW", self.quit) | ||
self.title('SageMath') | ||
self.columnconfigure(0, weight=1) | ||
frame = ttk.Frame(self, padding=10, width=300) | ||
frame.columnconfigure(0, weight=1) | ||
frame.grid(row=0, column=0, sticky=tkinter.NSEW) | ||
self.update_idletasks() | ||
# Logo | ||
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources')) | ||
logo_file = path_join(resource_dir, 'sage_logo_256.png') | ||
try: | ||
self.logo_image = tkinter.PhotoImage(file=logo_file) | ||
logo = ttk.Label(frame, image=self.logo_image) | ||
except tkinter.TclError: | ||
logo = ttk.Label(frame, text='Logo Here') | ||
# Interfaces | ||
checks = ttk.Labelframe(frame, text="Available User Interfaces", padding=10) | ||
self.radio_var = radio_var = tkinter.Variable(checks, 'cli') | ||
self.use_cli = ttk.Radiobutton(checks, text="Command line", variable=radio_var, | ||
value='cli', command=self.update_radio_buttons) | ||
self.terminals = ['Terminal.app'] | ||
if self.find_app('com.googlecode.iterm2'): | ||
self.terminals.append('iTerm.app') | ||
self.terminal_var = tkinter.Variable(self, self.terminals[0]) | ||
self.terminal_option = PopupMenu(checks, self.terminal_var, self.terminals) | ||
self.use_jupyter = ttk.Radiobutton(checks, text="Jupyter notebook from folder:", | ||
variable=radio_var, value='nb', command=self.update_radio_buttons) | ||
notebook_frame = ttk.Frame(checks) | ||
self.notebooks = ttk.Entry(notebook_frame, width=24) | ||
self.notebooks.insert(tkinter.END, notebook_dir) | ||
self.notebooks.config(state='readonly') | ||
self.browse = ttk.Button(notebook_frame, text='Select ...', padding=(-8, 0), | ||
command=self.browse_notebook_dir, state=tkinter.DISABLED) | ||
self.notebooks.grid(row=0, column=0) | ||
self.browse.grid(row=0, column=1) | ||
# Launch button | ||
self.launch = ttk.Button(frame, text="Launch", command=self.launch_sage) | ||
# Build the interfaces frame | ||
self.use_cli.grid(row=0, column=0, sticky=tkinter.W, pady=5) | ||
self.terminal_option.grid(row=1, column=0, sticky=tkinter.W, padx=10, pady=5) | ||
self.use_jupyter.grid(row=2, column=0, sticky=tkinter.W, pady=5) | ||
notebook_frame.grid(row=3, column=0, sticky=tkinter.W, pady=5) | ||
# Build the window | ||
logo.grid(row=0, column=0, pady=5) | ||
checks.grid(row=1, column=0, padx=10, pady=10, sticky=tkinter.EW) | ||
self.launch.grid(row=2, column=0) | ||
self.geometry('380x350+400+400') | ||
|
||
def quit(self): | ||
self.destroy() | ||
self.root.destroy() | ||
|
||
def update_radio_buttons(self): | ||
radio = self.radio_var.get() | ||
if radio == 'cli': | ||
self.notebooks.config(state=tkinter.DISABLED) | ||
self.browse.config(state=tkinter.DISABLED) | ||
self.terminal_option.config(state=tkinter.NORMAL) | ||
elif radio == 'nb': | ||
self.notebooks.config(state='readonly') | ||
self.browse.config(state=tkinter.NORMAL) | ||
self.terminal_option.config(state=tkinter.DISABLED) | ||
|
||
def launch_sage(self): | ||
interface = self.radio_var.get() | ||
if interface == 'cli': | ||
launched = self.launch_terminal(app=self.terminal_var.get()) | ||
elif interface == 'nb': | ||
jupyter_openers = [f for f in os.listdir(jupyter_runtime_dir) | ||
if f[-4:] == 'html'] | ||
if not jupyter_openers: | ||
launched = self.launch_notebook(None) | ||
else: | ||
html_file = path_join(jupyter_runtime_dir, jupyter_openers[0]) | ||
launched = self.launch_notebook(html_file) | ||
if launched: | ||
self.quit() | ||
|
||
def check_notebook_dir(self): | ||
notebook_dir = self.notebooks.get() | ||
if not notebook_dir.strip(): | ||
showwarning(parent=self, | ||
message="Please choose or create a folder for your Jupyter notebooks.") | ||
return False | ||
if not os.path.exists(notebook_dir): | ||
answer = askyesno(message='May we create the folder %s?'%notebook_dir) | ||
if answer == tkinter.YES: | ||
os.makedirs(notebook_dir, exist_ok=True) | ||
else: | ||
return False | ||
try: | ||
os.listdir(notebook_dir) | ||
except: | ||
showerror(message='Sorry. We do not have permission to read %s'%directory) | ||
return False | ||
return True | ||
|
||
def browse_notebook_dir(self): | ||
json_files = [filename for filename in os.listdir(jupyter_runtime_dir) | ||
if os.path.splitext(filename)[1] == '.json'] | ||
if json_files: | ||
answer = askyesno(message='You already have a Jupyter server running with ' | ||
'the notebook directory shown. Do you want to stop ' | ||
'that server and start a new one?') | ||
if answer == tkinter.YES: | ||
for json_file in json_files: | ||
with open(os.path.join(jupyter_runtime_dir, json_file)) as in_file: | ||
try: | ||
pid = int(json.load(in_file)['pid']) | ||
os.kill(pid, signal.SIGINT) | ||
time.sleep(2) | ||
os.kill(pid, signal.SIGINT) | ||
except: | ||
pass | ||
else: | ||
return | ||
directory = askdirectory(parent=self, | ||
message='Choose or create a folder for Jupyter notebooks') | ||
if directory: | ||
self.notebooks.config(state=tkinter.NORMAL) | ||
self.notebooks.delete(0, tkinter.END) | ||
self.notebooks.insert(tkinter.END, directory) | ||
self.notebooks.config(state='readonly') | ||
|
||
class AboutDialog(Dialog): | ||
def __init__(self, master, title='', content=''): | ||
self.content = content | ||
self.style = ttk.Style(master) | ||
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources')) | ||
logo_file = path_join(resource_dir, 'sage_logo_256.png') | ||
try: | ||
self.logo_image = tkinter.PhotoImage(file=logo_file) | ||
except tkinter.TclError: | ||
self.logo_image = None | ||
Dialog.__init__(self, master, title=title) | ||
|
||
def body(self, master): | ||
self.resizable(False, False) | ||
frame = ttk.Frame(self) | ||
if self.logo_image: | ||
logo = ttk.Label(frame, image=self.logo_image) | ||
else: | ||
logo = ttk.Label(frame, text='Logo Here') | ||
logo.grid(row=0, column=0, padx=20, pady=20, sticky=tkinter.N) | ||
message = tkinter.Message(frame, text=self.content) | ||
message.grid(row=1, column=0, padx=20, sticky=tkinter.EW) | ||
frame.pack() | ||
|
||
def buttonbox(self): | ||
frame = ttk.Frame(self, padding=(0, 0, 0, 20)) | ||
ok = ttk.Button(frame, text="OK", width=10, command=self.ok, | ||
default=tkinter.ACTIVE) | ||
ok.grid(row=2, column=0, padx=5, pady=5) | ||
self.bind("<Return>", self.ok) | ||
self.bind("<Escape>", self.ok) | ||
frame.pack() | ||
|
||
class SageApp(Launcher): | ||
resource_dir = abspath(path_join(sys.argv[0], pardir, pardir, 'Resources')) | ||
icon_file = abspath(path_join(resource_dir, 'sage_icon_1024.png')) | ||
about = """ | ||
SageMath is a free open-source mathematics software system licensed under the GPL. Please visit sagemath.org for more information about SageMath. | ||
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 | ||
https://github.com/3-manifolds/Sage_macOS/releases. | ||
The app is copyright © 2021 by Marc Culler, Nathan Dunfield, Matthias Gӧrner and others. | ||
""" | ||
|
||
def __init__(self): | ||
os.chdir(os.environ['HOME']) | ||
os.makedirs(jupyter_runtime_dir, mode=0o755, exist_ok=True) | ||
self.root_window = root = tkinter.Tk() | ||
self.icon = tkinter.Image("photo", file=self.icon_file) | ||
root.tk.call('wm','iconphoto', root._w, self.icon) | ||
self.menubar = menubar = tkinter.Menu(root) | ||
apple_menu = tkinter.Menu(menubar, name="apple") | ||
apple_menu.add_command(label='About SageMath ...', command=self.about_sagemath) | ||
menubar.add_cascade(menu=apple_menu) | ||
root.config(menu=menubar) | ||
ttk.Label(root, text="SageMath 9.2").pack(padx=20, pady=20) | ||
root.withdraw() | ||
|
||
def about_sagemath(self): | ||
AboutDialog(self.root_window, 'SageMath', self.about) | ||
|
||
def launch(self): | ||
self.launcher.deiconify() | ||
|
||
def run(self): | ||
self.launcher = LaunchWindow(root=self.root_window) | ||
self.root_window.tk.call('proc', '::tk::mac::ReopenApplication', '', | ||
'wm deiconify %s ; event generate '%self.launcher._w) | ||
self.root_window.mainloop() | ||
|
||
if __name__ == '__main__': | ||
SageApp().run() |
Oops, something went wrong.