Skip to content

Commit

Permalink
Version 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
culler committed Apr 9, 2021
1 parent aa71268 commit f6180a1
Show file tree
Hide file tree
Showing 3 changed files with 516 additions and 0 deletions.
329 changes: 329 additions & 0 deletions Sage_framework/files/SageMath
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()

0 comments on commit f6180a1

Please sign in to comment.