Skip to content

Instantly share code, notes, and snippets.

@andreif
Last active January 20, 2020 15:00
Show Gist options
  • Save andreif/981ed24d5d10b60ed0c8 to your computer and use it in GitHub Desktop.
Save andreif/981ed24d5d10b60ed0c8 to your computer and use it in GitHub Desktop.

Simplest app can be created via https://gist.github.com/mathiasbynens/674099 e.g.

mkdir -p MyApp.app/Contents/MacOS
printf '#!/bin/bash\nsleep 5' > MyApp.app/Contents/MacOS/MyApp
chmod +x MyApp.app/Contents/MacOS/MyApp
echo "<plist><dict></dict></plist>" > MyApp.app/Contents/Info.plist

But more "appy" app can be done via the followning steps

  1. Create app dir structure

    mkdir -p MyApp.app/Contents/MacOS
    mkdir -p MyApp.app/Contents/Resources
  2. Create app property list MyApp.app/Contents/Info.plist

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>CFBundleExecutable</key>
    	<string>MyApp</string>
    	<key>CFBundleGetInfoString</key>
    	<string>MyApp</string>
    	<key>CFBundleIconFile</key>
    	<string>MyApp.icns</string>
    	<key>CFBundleName</key>
    	<string>MyApp</string>
    	<key>CFBundlePackageType</key>
    	<string>APPL</string>
    </dict>
    </plist>
  3. Create app executable MyApp.app/Contents/MacOS/MyApp

    #!/bin/bash
    /usr/bin/python $(cd "$(dirname "${BASH_SOURCE[0]}")/../Resources" && pwd)/main.py
  4. Create app icon MyApp.app/Contents/Resources/MyApp.icns, e.g. https://gist.github.com/andreif/1691ee0a22458e4a6589

  5. Create python app MyApp.app/Contents/Resources/main.py, e.g.

    # coding=utf-8
    # Source: http://www.tkdocs.com/tutorial/firstexample.html
    
    import sys
    py2 = sys.version_info.major == 2
    if py2:
        from Tkinter import *
        import Tkinter as ttk
    else:
        from tkinter import *
        import tkinter as ttk
    
    
    def calculate(*args):
       try:
           value = float(feet.get())
           meters.set((0.3048 * value * 10000.0 + 0.5)/10000.0)
       except ValueError:
           pass
    
    root = Tk()
    root.title("Feet to Meters")
    if py2:
        mainframe = ttk.Frame(root)
    else:
        mainframe = ttk.Frame(root, padding="3 3 12 12")
    mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
    mainframe.columnconfigure(0, weight=1)
    mainframe.rowconfigure(0, weight=1)
    
    feet = StringVar()
    meters = StringVar()
    
    feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet)
    feet_entry.grid(column=2, row=1, sticky=(W, E))
    
    ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E))
    ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W)
    
    ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W)
    ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E)
    ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W)
    
    for child in mainframe.winfo_children():
       child.grid_configure(padx=5, pady=5)
    
    feet_entry.focus()
    root.bind('<Return>', calculate)
    
    root.mainloop()
  6. Enjoy the result

    MyApp

  7. To change app name in the OS X menu bar from Python to e.g. MyApp, use pyobjc, which is pre-installed in the system Python (/System/Library/Frameworks/Python/...):

    from Foundation import NSBundle
    bundle = NSBundle.mainBundle()
    info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
    info['CFBundleName'] = 'MyApp'
  8. To prevent multiple instances of the same app:

    try:
        import fcntl
        lockfile = open('/tmp/myapp.lock', 'w')
        fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError as e:
        exit()
  9. To focus on the already running instance is a bit hard atm, so we focus on all Python.app instances, until there is a better solution available/known

    from Foundation import NSWorkspace
    from Cocoa import NSApplicationActivateAllWindows, NSApplicationActivateIgnoringOtherApps
    
    for app in NSWorkspace.sharedWorkspace().runningApplications():
        if app.bundleIdentifier() == 'org.python.python':
            app.activateWithOptions_(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)
  10. One could use a pid file as a temporary solution

    from Foundation import NSProcessInfo
    info = NSProcessInfo.processInfo()
    pid = info.processIdentifier()
    with open(PID_FILE, 'w+') as f:
        f.write(str(pid))
    try:
        with open(PID_FILE) as f:
            pid = int(f.read())
    except:
        pid = None
    
    for app in NSWorkspace.sharedWorkspace().runningApplications():
        if app.bundleIdentifier() == 'org.python.python':
            if not pid or pid == app.processIdentifier():
                app.activateWithOptions_(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment