mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-08 20:46:16 +08:00
Convert Mac launcher to a Cocoa app (#1263)
1. Add `:event_handler` app option. It is a process that will receive the app events, the `:open_app`, `{:open_url, url}`, and `{:open_file, path}` messages. 2. Add `AppBuilder.init/0`. This reads the `APP_BUILDER_INPUT` env variable which contains the app event the release was started with. 3. Use <https://github.com/wojtekmach/otp/tree/wm-WX_MACOS_NON_GUI_APP> branch which contains wx fix that hasn't been merged yet.
This commit is contained in:
parent
f9156e7a9f
commit
789a44a7da
13 changed files with 290 additions and 340 deletions
7
.github/scripts/app/bootstrap_mac.sh
vendored
7
.github/scripts/app/bootstrap_mac.sh
vendored
|
@ -6,8 +6,8 @@ main() {
|
||||||
|
|
||||||
wxwidgets_repo="wxWidgets/wxWidgets"
|
wxwidgets_repo="wxWidgets/wxWidgets"
|
||||||
wxwidgets_ref="v3.1.7"
|
wxwidgets_ref="v3.1.7"
|
||||||
otp_repo="erlang/otp"
|
otp_repo="wojtekmach/otp"
|
||||||
otp_ref="OTP-25.0.2"
|
otp_ref="wm-WX_MACOS_NON_GUI_APP"
|
||||||
elixir_vsn="1.13.4"
|
elixir_vsn="1.13.4"
|
||||||
|
|
||||||
target=$(target)
|
target=$(target)
|
||||||
|
@ -78,7 +78,8 @@ build_wxwidgets() {
|
||||||
--with-libpng=builtin \
|
--with-libpng=builtin \
|
||||||
--with-liblzma=builtin \
|
--with-liblzma=builtin \
|
||||||
--with-zlib=builtin \
|
--with-zlib=builtin \
|
||||||
--with-expat=builtin
|
--with-expat=builtin \
|
||||||
|
--with-regex=builtin
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
cd -
|
cd -
|
||||||
|
|
|
@ -16,38 +16,21 @@ end
|
||||||
|
|
||||||
defmodule WxDemo.Window do
|
defmodule WxDemo.Window do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
use GenServer, restart: :transient
|
||||||
|
|
||||||
@behaviour :wx_object
|
def start_link(arg) do
|
||||||
|
GenServer.start_link(__MODULE__, arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
|
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
|
||||||
@wx_id_exit 5006
|
@wx_id_exit 5006
|
||||||
@wx_id_osx_hide 5250
|
@wx_id_osx_hide 5250
|
||||||
|
|
||||||
def start_link(_) do
|
|
||||||
{:wx_ref, _, _, pid} = :wx_object.start_link({:local, __MODULE__}, __MODULE__, [], [])
|
|
||||||
{:ok, pid}
|
|
||||||
end
|
|
||||||
|
|
||||||
def child_spec(init_arg) do
|
|
||||||
%{
|
|
||||||
id: __MODULE__,
|
|
||||||
start: {__MODULE__, :start_link, [init_arg]},
|
|
||||||
restart: :transient
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def windows_connected(url) do
|
|
||||||
url
|
|
||||||
|> String.trim()
|
|
||||||
|> String.trim_leading("\"")
|
|
||||||
|> String.trim_trailing("\"")
|
|
||||||
|> windows_to_wx()
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_) do
|
def init(_) do
|
||||||
|
AppBuilder.init()
|
||||||
app_name = "WxDemo"
|
app_name = "WxDemo"
|
||||||
os = os()
|
os = AppBuilder.os()
|
||||||
wx = :wx.new()
|
wx = :wx.new()
|
||||||
frame = :wxFrame.new(wx, -1, app_name, size: {400, 400})
|
frame = :wxFrame.new(wx, -1, app_name, size: {400, 400})
|
||||||
|
|
||||||
|
@ -59,33 +42,24 @@ defmodule WxDemo.Window do
|
||||||
:wxFrame.connect(frame, :command_menu_selected, skip: true)
|
:wxFrame.connect(frame, :command_menu_selected, skip: true)
|
||||||
:wxFrame.connect(frame, :close_window, skip: true)
|
:wxFrame.connect(frame, :close_window, skip: true)
|
||||||
|
|
||||||
case os do
|
{:ok, %{frame: frame}}
|
||||||
:macos ->
|
|
||||||
:wx.subscribe_events()
|
|
||||||
|
|
||||||
:windows ->
|
|
||||||
windows_to_wx(System.get_env("WXDEMO_URL") || "")
|
|
||||||
end
|
|
||||||
|
|
||||||
state = %{frame: frame}
|
|
||||||
{frame, state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
|
def handle_info({:wx, @wx_id_exit, _, _, _}, state) do
|
||||||
:init.stop()
|
:init.stop()
|
||||||
{:stop, :shutdown, state}
|
{:stop, :shutdown, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event({:wx, _, _, _, {:wxClose, :close_window}}, state) do
|
def handle_info({:wx, _, _, _, {:wxClose, :close_window}}, state) do
|
||||||
:init.stop()
|
:init.stop()
|
||||||
{:stop, :shutdown, state}
|
{:stop, :shutdown, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# ignore other menu events
|
# ignore other menu events
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event({:wx, _, _, _, {:wxCommand, :command_menu_selected, _, _, _}}, state) do
|
def handle_info({:wx, _, _, _, {:wxCommand, :command_menu_selected, _, _, _}}, state) do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -118,23 +92,4 @@ defmodule WxDemo.Window do
|
||||||
|> :wxMenu.findItem(@wx_id_exit)
|
|> :wxMenu.findItem(@wx_id_exit)
|
||||||
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp os() do
|
|
||||||
case :os.type() do
|
|
||||||
{:unix, :darwin} -> :macos
|
|
||||||
{:win32, _} -> :windows
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp windows_to_wx("") do
|
|
||||||
send(__MODULE__, {:new_file, ''})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp windows_to_wx("wxdemo://" <> _ = url) do
|
|
||||||
send(__MODULE__, {:open_url, String.to_charlist(url)})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp windows_to_wx(path) do
|
|
||||||
send(__MODULE__, {:open_file, String.to_charlist(path)})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,12 +46,12 @@ defmodule WxDemo.MixProject do
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
event_handler: WxDemo.Window,
|
||||||
macos: [
|
macos: [
|
||||||
build_dmg: macos_notarization != nil,
|
build_dmg: macos_notarization != nil,
|
||||||
notarization: macos_notarization
|
notarization: macos_notarization
|
||||||
],
|
],
|
||||||
windows: [
|
windows: [
|
||||||
server: WxDemo,
|
|
||||||
build_installer: true
|
build_installer: true
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,6 +18,36 @@ defmodule AppBuilder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def init do
|
||||||
|
if input = System.get_env("APP_BUILDER_INPUT") do
|
||||||
|
__rpc__(self(), input)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def __rpc__(event_handler) do
|
||||||
|
input = IO.read(:line) |> String.trim()
|
||||||
|
__rpc__(event_handler, input)
|
||||||
|
end
|
||||||
|
|
||||||
|
def __rpc__(event_handler, "open_app") do
|
||||||
|
send(event_handler, :open_app)
|
||||||
|
end
|
||||||
|
|
||||||
|
def __rpc__(event_handler, "open_url:" <> url) do
|
||||||
|
send(event_handler, {:open_url, url})
|
||||||
|
end
|
||||||
|
|
||||||
|
def __rpc__(event_handler, "open_file:" <> path) do
|
||||||
|
path =
|
||||||
|
if os() == :windows do
|
||||||
|
String.replace(path, "\\", "/")
|
||||||
|
else
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
send(event_handler, {:open_file, path})
|
||||||
|
end
|
||||||
|
|
||||||
defp validate_options(options) do
|
defp validate_options(options) do
|
||||||
os = os()
|
os = os()
|
||||||
|
|
||||||
|
@ -25,6 +55,7 @@ defmodule AppBuilder do
|
||||||
all: [
|
all: [
|
||||||
:name,
|
:name,
|
||||||
:icon_path,
|
:icon_path,
|
||||||
|
:event_handler,
|
||||||
url_schemes: [],
|
url_schemes: [],
|
||||||
document_types: [],
|
document_types: [],
|
||||||
additional_paths: []
|
additional_paths: []
|
||||||
|
|
|
@ -53,27 +53,14 @@ defmodule AppBuilder.Windows do
|
||||||
release
|
release
|
||||||
end
|
end
|
||||||
|
|
||||||
def __send_events__(server, input)
|
def handle_event(module, input)
|
||||||
|
|
||||||
def __send_events__(server, "new_file") do
|
def handle_event(module, "open_url:" <> url) do
|
||||||
send(server, {:new_file, ''})
|
module.open_url(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def __send_events__(server, "reopen_app") do
|
def handle_event(module, "open_file:" <> path) do
|
||||||
send(server, {:reopen_app, ''})
|
module.open_file(String.replace(path, "\\", "/"))
|
||||||
end
|
|
||||||
|
|
||||||
def __send_events__(server, "open_url:" <> url) do
|
|
||||||
send(server, {:open_url, String.to_charlist(url)})
|
|
||||||
end
|
|
||||||
|
|
||||||
def __send_events__(server, "open_file:" <> path) do
|
|
||||||
path =
|
|
||||||
path
|
|
||||||
|> String.replace("\\", "/")
|
|
||||||
|> String.to_charlist()
|
|
||||||
|
|
||||||
send(server, {:open_file, path})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_vcredistx64 do
|
defp ensure_vcredistx64 do
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
defmodule AppBuilder.Wx do
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Subscribe the given `server` to application events.
|
|
||||||
|
|
||||||
The possible events are:
|
|
||||||
|
|
||||||
* `{:new_file, []}`
|
|
||||||
* `{:reopen_app, []}`
|
|
||||||
* `{:open_file, path}`
|
|
||||||
* `{:open_url, url}`
|
|
||||||
|
|
||||||
On macOS, we simply call `:wx.subscribe_events()`. On Windows,
|
|
||||||
we emulate it.
|
|
||||||
"""
|
|
||||||
def subscribe_to_app_events(server) do
|
|
||||||
unless Code.ensure_loaded?(:wx) do
|
|
||||||
Logger.error("""
|
|
||||||
wx is not available.
|
|
||||||
|
|
||||||
Please add it to your extra applications:
|
|
||||||
|
|
||||||
extra_applications: [:wx]
|
|
||||||
""")
|
|
||||||
|
|
||||||
raise "wx is not available"
|
|
||||||
end
|
|
||||||
|
|
||||||
case :os.type() do
|
|
||||||
{:unix, :darwin} ->
|
|
||||||
:wx.subscribe_events()
|
|
||||||
|
|
||||||
{:win32, _} ->
|
|
||||||
input = System.get_env("APP_BUILDER_INPUT", "new_file")
|
|
||||||
AppBuilder.Windows.__send_events__(server, input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -8,5 +8,7 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -1,17 +1,121 @@
|
||||||
<%
|
<%
|
||||||
|
|
||||||
additional_paths = [
|
event_handler = @app_options |> Keyword.fetch!(:event_handler) |> inspect()
|
||||||
"rel/erts-#{@release.erts_version}/bin"
|
|
||||||
] ++ @app_options[:additional_paths]
|
|
||||||
|
|
||||||
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)/#{&1}")
|
additional_paths =
|
||||||
|
["rel/erts-#{@release.erts_version}/bin"] ++ @app_options[:additional_paths]
|
||||||
|
|> Enum.map_join(":", &"\\(resourcePath)/#{&1}")
|
||||||
|
|
||||||
%>
|
%>
|
||||||
import Foundation
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
var releaseTask: Process!
|
||||||
|
var isRunning = false
|
||||||
|
var initialInput = "open_app"
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
if !isRunning {
|
||||||
|
releaseTask = startRelease(initialInput)
|
||||||
|
isRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ n: Notification) {
|
||||||
|
if (releaseTask.isRunning == true) {
|
||||||
|
log("terminating release task")
|
||||||
|
releaseTask.terminate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: NSApplication, open urls: [URL]) {
|
||||||
|
for url in urls {
|
||||||
|
var input : String
|
||||||
|
|
||||||
|
if url.isFileURL {
|
||||||
|
input = "open_file:\(url.path)"
|
||||||
|
} else {
|
||||||
|
input = "open_url:\(url)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRunning {
|
||||||
|
rpc(input)
|
||||||
|
} else {
|
||||||
|
initialInput = input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRelease(_ input : String) -> Process {
|
||||||
|
let task = buildReleaseTask()
|
||||||
|
task.environment!["APP_BUILDER_INPUT"] = input
|
||||||
|
task.arguments = ["start"]
|
||||||
|
|
||||||
|
task.terminationHandler = {(t: Process) in
|
||||||
|
if t.terminationStatus == 0 {
|
||||||
|
log("release exited with: \(t.terminationStatus)")
|
||||||
|
} else {
|
||||||
|
runAlert(messageText: "\(appName) exited with error status \(t.terminationStatus).")
|
||||||
|
}
|
||||||
|
|
||||||
|
NSApp.terminate(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
try! task.run()
|
||||||
|
log("release pid: \(task.processIdentifier)")
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInteractive).async {
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpc(_ event: String) {
|
||||||
|
let input = Pipe()
|
||||||
|
let task = buildReleaseTask()
|
||||||
|
task.standardInput = input
|
||||||
|
input.fileHandleForWriting.write("\(event)\n".data(using: .utf8)!)
|
||||||
|
task.arguments = ["rpc", "AppBuilder.__rpc__(<%= event_handler %>)"]
|
||||||
|
try! task.run()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
if task.terminationStatus != 0 {
|
||||||
|
runAlert(messageText: "Something went wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReleaseTask() -> Process {
|
||||||
|
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/<%= @release.name %>", ofType: "")!
|
||||||
|
let resourcePath = Bundle.main.resourcePath ?? ""
|
||||||
|
let additionalPaths = "<%= additional_paths %>"
|
||||||
|
|
||||||
|
var environment = ProcessInfo.processInfo.environment
|
||||||
|
let path = environment["PATH"] ?? ""
|
||||||
|
environment["PATH"] = "\(additionalPaths):\(path)"
|
||||||
|
|
||||||
|
let task = Process()
|
||||||
|
task.environment = environment
|
||||||
|
task.launchPath = releaseScriptPath
|
||||||
|
task.standardOutput = logFile
|
||||||
|
task.standardError = logFile
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAlert(messageText: String) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
alert.messageText = messageText
|
||||||
|
alert.informativeText = "Logs available at: \(logPath)"
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func log(_ line: String) {
|
func log(_ line: String) {
|
||||||
logFile.write("\(line)\n".data(using: .utf8)!)
|
logFile.write("[\(appName)Launcher] \(line)\n".data(using: .utf8)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
|
@ -22,33 +126,7 @@ if !fm.fileExists(atPath: logPath) { fm.createFile(atPath: logPath, contents: Da
|
||||||
let logFile = FileHandle(forUpdatingAtPath: logPath)!
|
let logFile = FileHandle(forUpdatingAtPath: logPath)!
|
||||||
logFile.seekToEndOfFile()
|
logFile.seekToEndOfFile()
|
||||||
|
|
||||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/<%= @release.name %>", ofType: "")!
|
let app = NSApplication.shared
|
||||||
|
let delegate = AppDelegate()
|
||||||
let resourcePath = Bundle.main.resourcePath ?? ""
|
app.delegate = delegate
|
||||||
let additionalPaths = "<%= additional_paths %>"
|
app.run()
|
||||||
|
|
||||||
var environment = ProcessInfo.processInfo.environment
|
|
||||||
let path = environment["PATH"] ?? ""
|
|
||||||
environment["PATH"] = "\(additionalPaths):\(path)"
|
|
||||||
|
|
||||||
let task = Process()
|
|
||||||
task.environment = environment
|
|
||||||
task.launchPath = releaseScriptPath
|
|
||||||
task.arguments = ["start"]
|
|
||||||
task.standardOutput = logFile
|
|
||||||
task.standardError = logFile
|
|
||||||
|
|
||||||
log("[\(appName)Launcher] starting release")
|
|
||||||
try task.run()
|
|
||||||
log("[\(appName)Launcher] pid: \(task.processIdentifier)")
|
|
||||||
|
|
||||||
task.waitUntilExit()
|
|
||||||
log("[\(appName)Launcher] release exited with \(task.terminationStatus)")
|
|
||||||
|
|
||||||
if task.terminationStatus != 0 {
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.messageText = "\(appName) exited with error status \(task.terminationStatus)."
|
|
||||||
alert.informativeText = "Logs available at: \(logPath)"
|
|
||||||
alert.runModal()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,48 +1,32 @@
|
||||||
' This vbs script avoids a flashing cmd window when launching the release bat file
|
<%
|
||||||
|
|
||||||
|
event_handler = @app_options |> Keyword.fetch!(:event_handler) |> inspect()
|
||||||
|
|
||||||
|
additional_paths =
|
||||||
|
["rel/erts-#{@release.erts_version}/bin"] ++ @app_options[:additional_paths]
|
||||||
|
|> Enum.map(&("root & \"" <> String.replace(&1, "/", "\\") <> ";\" & "))
|
||||||
|
|
||||||
|
%>
|
||||||
|
|
||||||
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
||||||
script = root & "rel\bin\<%= @release.name %>.bat"
|
script = root & "rel\bin\<%= @release.name %>.bat"
|
||||||
|
|
||||||
Set shell = CreateObject("WScript.Shell")
|
|
||||||
|
|
||||||
' Below we run two commands:
|
|
||||||
'
|
|
||||||
' 1. bin/release rpc
|
|
||||||
' 2. bin/release start
|
|
||||||
'
|
|
||||||
' The first one will only succeed when the app is already running. The second one when it is not.
|
|
||||||
' It's ok for either to fail because we run them asynchronously.
|
|
||||||
|
|
||||||
Set env = shell.Environment("Process")
|
|
||||||
env("PATH") = "<%= Enum.map_join(@app_options[:additional_paths], ";", &String.replace(&1, "/", "\\")) %>" & env("PATH")
|
|
||||||
|
|
||||||
If WScript.Arguments.Count > 0 Then
|
If WScript.Arguments.Count > 0 Then
|
||||||
input = WScript.Arguments(0)
|
input = WScript.Arguments(0)
|
||||||
Else
|
Else
|
||||||
input = "reopen_app"
|
input = "open_app"
|
||||||
End If
|
End If
|
||||||
|
|
||||||
' Below, we're basically doing:
|
Set shell = CreateObject("WScript.Shell")
|
||||||
'
|
Set env = shell.Environment("Process")
|
||||||
' $ bin/release rpc 'AppBuilder.Windows.__send_events__(MyApp, input)'
|
env("PATH") = <%= additional_paths %>env("PATH")
|
||||||
'
|
|
||||||
' We send the input through IO, as opposed using the rpc expression, to avoid RCE.
|
|
||||||
cmd = "echo " & input & " | """ & script & """ rpc ""AppBuilder.Windows.__send_events__(<%= inspect(@app_options[:server]) %>, String.trim(IO.read(:line)))"""
|
|
||||||
code = shell.Run("cmd /c " & cmd, 0)
|
|
||||||
|
|
||||||
' Below, we're basically doing:
|
' try release rpc, if release is down, this will fail but that's ok.
|
||||||
'
|
cmd = "echo " & input & " | """ & script & """ rpc ""AppBuilder.__rpc__(<%= event_handler %>)"""
|
||||||
' $ bin/release start
|
status = shell.Run("cmd /c " & cmd, 0, true)
|
||||||
'
|
|
||||||
' We send the input through the environment variable as we can't easily access argv
|
|
||||||
' when booting through the release script.
|
|
||||||
|
|
||||||
If WScript.Arguments.Count > 0 Then
|
|
||||||
env("APP_BUILDER_INPUT") = WScript.Arguments(0)
|
|
||||||
Else
|
|
||||||
env("APP_BUILDER_INPUT") = "new_file"
|
|
||||||
End If
|
|
||||||
|
|
||||||
|
' try release start, if release is up, this will fail but that's ok.
|
||||||
|
env("APP_BUILDER_INPUT") = input
|
||||||
cmd = """" & script & """ start"
|
cmd = """" & script & """ start"
|
||||||
code = shell.Run("cmd /c " & cmd & " >> " & root & "\Logs\<%= @app_options[:name] %>.log 2>&1", 0)
|
status = shell.Run("cmd /c " & cmd, 0)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,105 @@
|
||||||
if Mix.target() == :app do
|
if Mix.target() == :app do
|
||||||
|
defmodule LivebookApp do
|
||||||
|
@moduledoc false
|
||||||
|
@name __MODULE__
|
||||||
|
@wxID_OPEN 5000
|
||||||
|
@wxID_EXIT 5006
|
||||||
|
@wxBITMAP_TYPE_PNG 15
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
def start_link(arg) do
|
||||||
|
GenServer.start_link(__MODULE__, arg, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
taskbar_icon_path = "rel/app/icon.png"
|
||||||
|
@external_resource taskbar_icon_path
|
||||||
|
@taskbar_icon File.read!(taskbar_icon_path)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
AppBuilder.init()
|
||||||
|
os = AppBuilder.os()
|
||||||
|
:wx.new()
|
||||||
|
|
||||||
|
# TODO: instead of embedding the icon and copying to tmp, copy the file known location.
|
||||||
|
# It's a bit tricky because it needs to support all the ways of running the app:
|
||||||
|
# 1. MIX_TARGET=app mix phx.server
|
||||||
|
# 2. mix app
|
||||||
|
# 3. mix release app
|
||||||
|
taskbar_icon_path = Path.join(System.tmp_dir!(), "icon.png")
|
||||||
|
File.write!(taskbar_icon_path, @taskbar_icon)
|
||||||
|
icon = :wxIcon.new(taskbar_icon_path, type: @wxBITMAP_TYPE_PNG)
|
||||||
|
|
||||||
|
menu_items = [
|
||||||
|
{"Open Browser", key: "ctrl+o", id: @wxID_OPEN},
|
||||||
|
{"Quit", key: "ctrl+q", id: @wxID_EXIT}
|
||||||
|
]
|
||||||
|
|
||||||
|
taskbar = WxUtils.taskbar("Livebook", icon, menu_items)
|
||||||
|
|
||||||
|
if os == :windows do
|
||||||
|
:wxTaskBarIcon.connect(taskbar, :taskbar_left_down,
|
||||||
|
callback: fn _, _ ->
|
||||||
|
open_browser()
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:open_app, state) do
|
||||||
|
open_browser()
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:open_file, path}, state) do
|
||||||
|
path
|
||||||
|
|> Livebook.Utils.notebook_open_url()
|
||||||
|
|> open_browser()
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:open_url, "livebook://" <> rest}, state) do
|
||||||
|
"https://#{rest}"
|
||||||
|
|> Livebook.Utils.notebook_import_url()
|
||||||
|
|> open_browser()
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:wx, @wxID_EXIT, _, _, _}, _state) do
|
||||||
|
System.stop(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:wx, @wxID_OPEN, _, _, _}, state) do
|
||||||
|
open_browser()
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
if Mix.env() == :dev do
|
||||||
|
@impl true
|
||||||
|
def handle_info(event, state) do
|
||||||
|
IO.inspect(event)
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp open_browser(url \\ LivebookWeb.Endpoint.access_url()) do
|
||||||
|
Livebook.Utils.browser_open(url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defmodule WxUtils do
|
defmodule WxUtils do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
@wxID_ANY -1
|
||||||
defmacro wxID_ANY, do: -1
|
|
||||||
defmacro wxID_OPEN, do: 5000
|
|
||||||
defmacro wxID_EXIT, do: 5006
|
|
||||||
defmacro wxID_OSX_HIDE, do: 5250
|
|
||||||
defmacro wxBITMAP_TYPE_PNG, do: 15
|
|
||||||
|
|
||||||
def taskbar(title, icon, menu_items) do
|
def taskbar(title, icon, menu_items) do
|
||||||
pid = self()
|
pid = self()
|
||||||
|
@ -66,7 +159,7 @@ if Mix.target() == :app do
|
||||||
|
|
||||||
Enum.each(items, fn
|
Enum.each(items, fn
|
||||||
{title, options} ->
|
{title, options} ->
|
||||||
id = Keyword.get(options, :id, wxID_ANY())
|
id = Keyword.get(options, :id, @wxID_ANY)
|
||||||
|
|
||||||
title =
|
title =
|
||||||
case Keyword.fetch(options, :key) do
|
case Keyword.fetch(options, :key) do
|
||||||
|
@ -82,149 +175,5 @@ if Mix.target() == :app do
|
||||||
|
|
||||||
menu
|
menu
|
||||||
end
|
end
|
||||||
|
|
||||||
def menubar(app_name, menus) do
|
|
||||||
menubar = :wxMenuBar.new()
|
|
||||||
|
|
||||||
if AppBuilder.os() == :macos, do: fixup_macos_menubar(menubar, app_name)
|
|
||||||
|
|
||||||
for {title, menu_items} <- menus do
|
|
||||||
true = :wxMenuBar.append(menubar, menu(menu_items), title)
|
|
||||||
end
|
|
||||||
|
|
||||||
menubar
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fixup_macos_menubar(menubar, app_name) do
|
|
||||||
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
|
|
||||||
|
|
||||||
menu
|
|
||||||
|> :wxMenu.findItem(wxID_OSX_HIDE())
|
|
||||||
|> :wxMenuItem.setItemLabel("Hide #{app_name}\tCtrl+H")
|
|
||||||
|
|
||||||
menu
|
|
||||||
|> :wxMenu.findItem(wxID_EXIT())
|
|
||||||
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule LivebookApp do
|
|
||||||
@moduledoc false
|
|
||||||
@name __MODULE__
|
|
||||||
|
|
||||||
use GenServer
|
|
||||||
import WxUtils
|
|
||||||
|
|
||||||
def start_link(arg) do
|
|
||||||
GenServer.start_link(__MODULE__, arg, name: @name)
|
|
||||||
end
|
|
||||||
|
|
||||||
taskbar_icon_path = "rel/app/icon.png"
|
|
||||||
@external_resource taskbar_icon_path
|
|
||||||
@taskbar_icon File.read!(taskbar_icon_path)
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(_) do
|
|
||||||
os = AppBuilder.os()
|
|
||||||
wx = :wx.new()
|
|
||||||
AppBuilder.Wx.subscribe_to_app_events(@name)
|
|
||||||
|
|
||||||
menu_items = [
|
|
||||||
{"Open Browser", key: "ctrl+o", id: wxID_OPEN()},
|
|
||||||
{"Quit", key: "ctrl+q", id: wxID_EXIT()}
|
|
||||||
]
|
|
||||||
|
|
||||||
if os == :macos do
|
|
||||||
:wxMenuBar.setAutoWindowMenu(false)
|
|
||||||
|
|
||||||
menubar =
|
|
||||||
menubar("Livebook", [
|
|
||||||
{"File", menu_items}
|
|
||||||
])
|
|
||||||
|
|
||||||
:ok = :wxMenuBar.connect(menubar, :command_menu_selected, skip: true)
|
|
||||||
|
|
||||||
# TODO: use :wxMenuBar.macSetCommonMenuBar/1 when OTP 25 is out
|
|
||||||
frame = :wxFrame.new(wx, -1, "", size: {0, 0})
|
|
||||||
:wxFrame.show(frame)
|
|
||||||
:wxFrame.setMenuBar(frame, menubar)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: instead of embedding the icon and copying to tmp, copy the file known location.
|
|
||||||
# It's a bit tricky because it needs to support all the ways of running the app:
|
|
||||||
# 1. MIX_TARGET=app mix phx.server
|
|
||||||
# 2. mix app
|
|
||||||
# 3. mix release app
|
|
||||||
taskbar_icon_path = Path.join(System.tmp_dir!(), "icon.png")
|
|
||||||
File.write!(taskbar_icon_path, @taskbar_icon)
|
|
||||||
icon = :wxIcon.new(taskbar_icon_path, type: wxBITMAP_TYPE_PNG())
|
|
||||||
|
|
||||||
taskbar = taskbar("Livebook", icon, menu_items)
|
|
||||||
|
|
||||||
if os == :windows do
|
|
||||||
:wxTaskBarIcon.connect(taskbar, :taskbar_left_down,
|
|
||||||
callback: fn _, _ ->
|
|
||||||
open_browser()
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:wx, wxID_EXIT(), _, _, _}, _state) do
|
|
||||||
System.stop(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:wx, wxID_OPEN(), _, _, _}, state) do
|
|
||||||
open_browser()
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# This event is triggered when the application is opened for the first time
|
|
||||||
@impl true
|
|
||||||
def handle_info({:new_file, ''}, state) do
|
|
||||||
open_browser()
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:reopen_app, _}, state) do
|
|
||||||
open_browser()
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:open_file, path}, state) do
|
|
||||||
path
|
|
||||||
|> List.to_string()
|
|
||||||
|> Livebook.Utils.notebook_open_url()
|
|
||||||
|> open_browser()
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:open_url, 'livebook://' ++ rest}, state) do
|
|
||||||
"https://#{rest}"
|
|
||||||
|> Livebook.Utils.notebook_import_url()
|
|
||||||
|> open_browser()
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
if Mix.env() == :dev do
|
|
||||||
@impl true
|
|
||||||
def handle_info(event, state) do
|
|
||||||
IO.inspect(event)
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp open_browser(url \\ LivebookWeb.Endpoint.access_url()) do
|
|
||||||
Livebook.Utils.browser_open(url)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -160,8 +160,8 @@ defmodule Livebook.MixProject do
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
event_handler: LivebookApp,
|
||||||
additional_paths: [
|
additional_paths: [
|
||||||
"rel/erts-#{:erlang.system_info(:version)}/bin",
|
|
||||||
"rel/vendor/elixir/bin"
|
"rel/vendor/elixir/bin"
|
||||||
],
|
],
|
||||||
macos: [
|
macos: [
|
||||||
|
@ -171,7 +171,6 @@ defmodule Livebook.MixProject do
|
||||||
notarization: macos_notarization
|
notarization: macos_notarization
|
||||||
],
|
],
|
||||||
windows: [
|
windows: [
|
||||||
server: LivebookApp,
|
|
||||||
icon_path: "rel/app/icon.ico",
|
icon_path: "rel/app/icon.ico",
|
||||||
build_installer: true
|
build_installer: true
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
set RELEASE_NODE=livebook
|
||||||
set RELEASE_MODE=interactive
|
set RELEASE_MODE=interactive
|
||||||
set LIVEBOOK_SHUTDOWN_ENABLED=true
|
set LIVEBOOK_SHUTDOWN_ENABLED=true
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
export RELEASE_NODE=livebook
|
||||||
export RELEASE_MODE=interactive
|
export RELEASE_MODE=interactive
|
||||||
export LIVEBOOK_SHUTDOWN_ENABLED=true
|
export LIVEBOOK_SHUTDOWN_ENABLED=true
|
||||||
|
export WX_MACOS_NON_GUI_APP=1
|
||||||
|
|
||||||
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
cookie_path="${RELEASE_ROOT}/releases/COOKIE"
|
||||||
if [ ! -f $cookie_path ]; then
|
if [ ! -f $cookie_path ]; then
|
||||||
|
|
Loading…
Add table
Reference in a new issue