aboutsummaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorJefferson González <jgmdev@gmail.com>2022-10-02 14:42:25 -0400
committerGitHub <noreply@github.com>2022-10-02 14:42:25 -0400
commita19c1a7b13b3712f3473fe8024ccfaf24f3f9004 (patch)
tree20a4453a253ee6808918af8fc04398e3e6543cd9 /plugins
parent82e4650234fd45225f86958b21b04f2a9a9b864a (diff)
downloadlite-xl-plugins-a19c1a7b13b3712f3473fe8024ccfaf24f3f9004.tar.gz
lite-xl-plugins-a19c1a7b13b3712f3473fe8024ccfaf24f3f9004.zip
add inter-process communication (ipc) plugin (#127)
* adds single instance support and basic file dragging from instance to instance
Diffstat (limited to 'plugins')
-rw-r--r--plugins/ipc.lua1026
1 files changed, 1026 insertions, 0 deletions
diff --git a/plugins/ipc.lua b/plugins/ipc.lua
new file mode 100644
index 0000000..128e39c
--- /dev/null
+++ b/plugins/ipc.lua
@@ -0,0 +1,1026 @@
+-- mod-version:3
+--
+-- Crossplatform file based IPC system for lite-xl.
+-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
+-- @license MIT
+--
+local core = require "core"
+local config = require "core.config"
+local common = require "core.common"
+local command = require "core.command"
+local Object = require "core.object"
+local RootView = require "core.rootview"
+local settings_found, settings = pcall(require, "plugins.settings")
+
+---The maximum amount of seconds a message will be broadcasted.
+---@type integer
+local MESSAGE_EXPIRATION=3
+
+---@class config.plugins.ipc
+---@field single_instance boolean
+---@field dirs_instance boolean
+config.plugins.ipc = common.merge({
+ single_instance = true,
+ dirs_instance = true,
+ -- The config specification used by the settings gui
+ config_spec = {
+ name = "Inter-process communication",
+ {
+ label = "Single Instance",
+ description = "Run a single instance of lite-xl.",
+ path = "single_instance",
+ type = "toggle",
+ default = true
+ },
+ {
+ label = "Directories Instance",
+ description = "Open a new instance for directories.",
+ path = "dirs_instance",
+ type = "toggle",
+ default = true
+ }
+ }
+}, config.plugins.ipc)
+
+---@alias plugins.ipc.onmessageread fun(message: plugins.ipc.message) | nil
+---@alias plugins.ipc.onreplyread fun(reply: plugins.ipc.reply) | nil
+---@alias plugins.ipc.onmessage fun(message: plugins.ipc.message, reply: plugins.ipc.reply) | nil
+---@alias plugins.ipc.onreply fun(reply: plugins.ipc.reply) | nil
+---@alias plugins.ipc.function fun(...)
+
+---@alias plugins.ipc.messagetype
+---| '"message"'
+---| '"method"'
+---| '"signal"'
+
+---@class plugins.ipc.message
+---@field id string
+---@field sender string
+---@field name string
+---@field type plugins.ipc.messagetype | string
+---@field destinations table<integer,string>
+---@field data table<string,any>
+---@field timestamp number
+---@field on_read plugins.ipc.onmessageread
+---@field on_reply plugins.ipc.onreply
+---@field replies plugins.ipc.reply[]
+local IPCMessage = {
+ ---Id of the message
+ id = "",
+ ---The id of process that sent the message
+ sender = "",
+ ---Name of the message
+ name = "",
+ ---Type of message.
+ type = "",
+ ---List with id of the instance that should receive the message.
+ destinations = {},
+ ---A list of named values sent to receivers.
+ data = {},
+ ---Time in seconds when the message was sent for automatic expiration purposes.
+ timestamp = 0,
+ ---Optional callback executed by the receiver when the message is read.
+ on_read = function(message) end,
+ ---Optional callback executed when a reply to the message is received.
+ on_reply = function(reply) end,
+ ---The received replies for the message.
+ replies = {}
+}
+
+---@class plugins.ipc.reply
+---@field id string
+---@field sender string
+---@field replier string
+---@field data table<string,any>
+---@field timestamp number
+---@field on_read plugins.ipc.onreplyread
+local IPCReply = {
+ ---Id of the message
+ id = "",
+ ---The id of process that sent the message
+ sender = "",
+ ---The id of the replier
+ replier = "",
+ ---A list of named values sent back to sender.
+ data = {},
+ ---Time in seconds when the reply was sent for automatic expiration purposes.
+ timestamp = 0,
+ ---Optional callback executed by the sender when the reply is read.
+ on_read = function(reply) end
+}
+
+---@class plugins.ipc.instance
+---@field id string
+---@field position integer
+---@field last_update integer
+---@field messages plugins.ipc.message[]
+---@field replies plugins.ipc.reply[]
+---@field properties table
+local IPCInstance = {
+ ---Process id of the instance.
+ id = "",
+ ---The position in which the instance was launched.
+ position = 0,
+ ---Flag that indicates if this instance was the first started.
+ primary = false,
+ ---Indicates the last time this instance updated its session file.
+ last_update = 0,
+ ---The messages been broadcasted.
+ messages = {},
+ ---The replies been broadcasted.
+ replies = {},
+ ---Table of properties associated with the instance.
+ properties = {}
+}
+
+---@class core.ipc : core.object
+---@field private id string
+---@field private user_dir string
+---@field private running boolean
+---@field private file string
+---@field private primary boolean
+---@field private position integer
+---@field private messages plugins.ipc.message[]
+---@field private replies plugins.ipc.reply[]
+---@field private listeners table<string,table<integer,plugins.ipc.onmessage>>
+---@field private signals table<string,integer>
+---@field private methods table<string,integer>
+---@field private signal_definitions table<integer,string>
+---@field private method_definitions table<integer,string>
+local IPC = Object:extend()
+
+---@class plugins.ipc.threads
+---@field cr thread
+---@field wake number
+
+---List of threads belonging to all instantiated IPC objects.
+---@type plugins.ipc.threads[]
+local threads = {}
+
+---Register a new thread to be run on the background.
+---@param f function
+local function add_thread(f)
+ local key = #threads + 1
+ threads[key] = { cr = coroutine.create(f), wake = 0 }
+ return key
+end
+
+---Updates the session file of an IPC object.
+---@param self core.ipc
+local function update_file(self)
+ local file, errmsg = io.open(self.file, "w+")
+
+ if file then
+ local output = "-- Warning: Generated by IPC system do not edit manually!\n"
+ .. "return " .. common.serialize(
+ {
+ id = self.id,
+ primary = self.primary,
+ position = self.position,
+ last_update = os.time(),
+ messages = self.messages,
+ replies = self.replies,
+ signals = self.signal_definitions,
+ methods = self.method_definitions
+ },
+ {
+ pretty = true
+ }
+ )
+
+ output = output:gsub("%s+%[\"on_reply\"%].-\n", "")
+
+ file:write(output)
+ file:close()
+ else
+ core.error("IPC Error: failed updating status (%s)", errmsg)
+ end
+end
+
+---Constructor
+---@param id? string Defaults to current lite-xl process id.
+function IPC:new(id)
+ self.id = id or tostring(system.get_process_id())
+ self.user_dir = USERDIR .. "/ipc"
+ self.file = self.user_dir .. "/" .. self.id .. ".lua"
+ self.primary = false
+ self.running = false
+ self.messages = {}
+ self.replies = {}
+ self.listeners = {}
+ self.signals = {}
+ self.methods = {}
+ self.signal_definitions = {}
+ self.method_definitions = {}
+
+ local ipc_dir_status = system.get_file_info(self.user_dir)
+
+ if not ipc_dir_status then
+ local created, errmsg = common.mkdirp(self.user_dir)
+ if not created then
+ core.error("Error initializing IPC system: %s", errmsg)
+ return
+ end
+ end
+
+ local file, errmsg = io.open(self.file, "w+")
+
+ if not file then
+ core.error("Error initializing IPC system: %s", errmsg)
+ return
+ else
+ file:close()
+ os.remove(self.file)
+ end
+
+ -- Execute to set the instance position and primary attribute if no other running.
+ local instances = self:get_instances()
+ self.primary = #instances == 0 and true or false
+ self.position = #instances + 1
+
+ self:start()
+end
+
+---Starts and registers the ipc session and monitoring.
+function IPC:start()
+ if not self.running then
+ self.running = true
+
+ update_file(self)
+
+ local wait_time = 0.3
+
+ local this = self
+ self.coroutine_key = add_thread(function()
+ coroutine.yield(wait_time)
+ while(true) do
+ this:read_messages()
+ this:read_replies()
+ update_file(this)
+ coroutine.yield(wait_time)
+ end
+ end)
+ end
+end
+
+---Stop and unregister the ipc session and monitoring.
+function IPC:stop()
+ self.running = false
+ table.remove(threads, self.coroutine_key)
+ os.remove(self.file)
+end
+
+---Get a list of running lite-xl instances.
+---@return plugins.ipc.instance[]
+function IPC:get_instances()
+ ---@type plugins.ipc.instance[]
+ local instances = {}
+
+ local files, errmsg = system.list_dir(self.user_dir)
+
+ if files then
+ for _, file in ipairs(files) do
+ if string.match(file, "^%d+%.lua$") then
+ local path = self.user_dir .. "/" .. file
+ local file_info = system.get_file_info(path)
+ if file_info and file_info.type == "file" then
+ ::read_instance_file::
+ ---@type plugins.ipc.instance
+ local instance = dofile(path)
+ if instance and instance.id ~= self.id then
+ if instance.last_update + 2 > os.time() then
+ table.insert(instances, instance)
+ else
+ -- Delete expired instance session maybe result of a crash
+ os.remove(path)
+ end
+ elseif not instance and path ~= self.file then
+ --We retry reading the file since it was been modified
+ --by its owner instance.
+ goto read_instance_file
+ end
+ end
+ end
+ end
+ else
+ core.error("IPC Error: failed getting running instances (%s)", errmsg)
+ end
+
+ local instances_count = #instances
+
+ if instances_count > 0 then
+ table.sort(instances, function(ia, ib)
+ return ia.position < ib.position
+ end)
+ end
+
+ if not self.primary and self.position then
+ if instances_count == 0 or instances[1].position > self.position then
+ self.primary = true
+ end
+ end
+
+ return instances
+end
+
+---@class plugins.ipc.vardecl
+---@field name string
+---@field type string
+---@field optional boolean
+
+---Generate a string representation of a function
+---@param name string
+---@param params? plugins.ipc.vardecl[]
+---@param returns? plugins.ipc.vardecl[]
+---@return string function_definition
+local function generate_definition(name, params, returns)
+ local declaration = name .. "("
+
+ if params and #params > 0 then
+ local params_string = ""
+ for _, param in ipairs(params) do
+ params_string = params_string .. param.name
+ if param.optional then
+ params_string = params_string .. "?: "
+ else
+ params_string = params_string .. ": "
+ end
+ params_string = params_string .. param.type .. ", "
+ end
+ local params_stripped = params_string:gsub(", $", "")
+ declaration = declaration .. params_stripped
+ end
+
+ declaration = declaration .. ")"
+
+ if returns and #returns > 0 then
+ declaration = declaration .. " -> "
+ local returns_string = ""
+ for _, ret in ipairs(returns) do
+ if ret.name then
+ returns_string = returns_string .. ret.name .. ": "
+ end
+ returns_string = returns_string .. ret.type
+ if ret.optional then
+ returns_string = returns_string .. "?, "
+ else
+ returns_string = returns_string .. ", "
+ end
+ end
+ local returns_stripped = returns_string:gsub(", $", "")
+ declaration = declaration .. returns_stripped
+ end
+
+ return declaration
+end
+
+---Retrieve the id of the primary instance if found.
+---@return string | nil
+function IPC:get_primary_instance()
+ local instances = self:get_instances()
+ for _, instance in ipairs(instances) do
+ if instance.primary then
+ return instance.id
+ end
+ end
+ return nil
+end
+
+---Get a queued message.
+---@param message_id string
+---@return plugins.ipc.message | nil
+function IPC:get_message(message_id)
+ for _, message in ipairs(self.messages) do
+ if message.id == message_id then
+ return message
+ end
+ end
+ return nil
+end
+
+---Remove a message from the queue.
+---@param message_id string
+function IPC:remove_message(message_id)
+ for m, message in ipairs(self.messages) do
+ if message.id == message_id then
+ table.remove(self.messages, m)
+ break
+ end
+ end
+end
+
+---Get the reply sent to a specific message.
+---@param message_id string
+---@return plugins.ipc.reply | nil
+function IPC:get_reply(message_id)
+ for _, reply in ipairs(self.replies) do
+ if reply.id == message_id then
+ return reply
+ end
+ end
+ return nil
+end
+
+---Verify all the messages sent by running instances, read those directed
+---to the currently running instance and reply to them.
+function IPC:read_messages()
+ local instances = self:get_instances()
+
+ local awaiting_replies = {}
+
+ for _, instance in ipairs(instances) do
+ for _, message in ipairs(instance.messages) do
+ for _, destination in ipairs(message.destinations) do
+ if destination == self.id then
+ local reply = self:get_reply(message.id)
+
+ if not reply then
+ if message.on_read then
+ local on_read, errmsg = load(message.on_read)
+ if on_read then
+ local executed = core.try(function() on_read(message) end)
+ if not executed then
+ core.error(
+ "IPC Error: could not run message on_read\n"
+ .. "Message: %s\n",
+ common.serialize(message, {pretty = true})
+ )
+ end
+ else
+ core.error(
+ "IPC Error: could not run message on_read (%s)\n"
+ .. "Message: %s\n",
+ errmsg,
+ common.serialize(message, {pretty = true})
+ )
+ end
+ end
+
+ ---@type plugins.ipc.reply
+ reply = {}
+ reply.id = message.id
+ reply.sender = message.sender
+ reply.replier = self.id
+ reply.data = {}
+ reply.on_read = nil
+
+ local type_name = message.type .. "." .. message.name
+
+ -- Allow listeners to react to message and modify reply
+ if self.listeners[type_name] and #self.listeners[type_name] > 0 then
+ for _, on_message in ipairs(self.listeners[type_name]) do
+ on_message(message, reply)
+ end
+ end
+
+ if reply.on_read then
+ reply.on_read = string.dump(reply.on_read)
+ end
+
+ reply.timestamp = os.time()
+ end
+
+ table.insert(awaiting_replies, reply)
+ break
+ end
+ end
+ end
+ end
+
+ self.replies = awaiting_replies
+end
+
+---Reads replies directed to messages sent by the currently running instance
+---and if any returns them.
+---@return plugins.ipc.reply[] | nil
+function IPC:read_replies()
+ if #self.messages == 0 then
+ return
+ end
+
+ local instances = self:get_instances()
+
+ local replies = {}
+
+ local messages_removed = 0;
+ for m=1, #self.messages do
+ local message = self.messages[m-messages_removed]
+ local message_removed = false
+
+ local destinations_removed = 0
+ for d=1, #message.destinations do
+ local destination = message.destinations[d-destinations_removed]
+
+ local found = false
+ for _, instance in ipairs(instances) do
+ if instance.id == destination then
+ found = true
+ for _, reply in ipairs(instance.replies) do
+ if reply.id == message.id then
+ local reply_registered = false
+ for _, message_reply in ipairs(message.replies) do
+ if message_reply.replier == instance.id then
+ reply_registered = true
+ break
+ end
+ end
+ if not reply_registered then
+ if message.on_reply then
+ message.on_reply(reply)
+ end
+
+ if reply.on_read then
+ local on_read, errmsg = load(reply.on_read)
+ if on_read then
+ local executed = core.try(function() on_read(reply) end)
+ if not executed then
+ core.error(
+ "IPC Error: could not run reply on_read\n"
+ .. "Message: %s\n"
+ .. "Reply: %s",
+ common.serialize(message, {pretty = true}),
+ common.serialize(reply, {pretty = true})
+ )
+ end
+ else
+ core.error(
+ "IPC Error: could not run reply on_read (%s)\n"
+ .. "Message: %s\n"
+ .. "Reply: %s",
+ errmsg,
+ common.serialize(message, {pretty = true}),
+ common.serialize(reply, {pretty = true})
+ )
+ end
+ end
+
+ table.insert(replies, reply)
+ table.insert(message.replies, reply)
+ end
+ end
+ end
+ break
+ end
+ end
+ if not found then
+ table.remove(message.destinations, d-destinations_removed)
+ destinations_removed = destinations_removed + 1
+ if #message.destinations == 0 then
+ table.remove(self.messages, m-messages_removed)
+ messages_removed = messages_removed + 1
+ message_removed = true
+ end
+ end
+ end
+ if
+ not message_removed
+ and
+ (
+ #message.replies == #message.destinations
+ or
+ message.timestamp + MESSAGE_EXPIRATION < os.time()
+ )
+ then
+ table.remove(self.messages, m-messages_removed)
+ messages_removed = messages_removed + 1
+ end
+ end
+
+ return replies
+end
+
+---Blocks execution of current instance to wait for all replies by the
+---specified message and when finished returns them.
+---@param message_id string
+---@return plugins.ipc.reply[] | nil
+function IPC:wait_for_replies(message_id)
+ local message_data = self:get_message(message_id)
+
+ update_file(self)
+
+ if message_data then
+ self:read_replies()
+ while true do
+ if
+ message_data.replies
+ and
+ #message_data.replies == #message_data.destinations
+ then
+ return message_data.replies
+ elseif not self:get_message(message_id) then
+ return message_data.replies
+ end
+ self:read_replies()
+ end
+ end
+ return nil
+end
+
+---Blocks execution of current instance to wait for all messages to
+---be replied to.
+function IPC:wait_for_messages()
+ update_file(self)
+ while #self.messages > 0 do
+ self:read_replies()
+ system.sleep(0.1)
+ end
+end
+
+---@class plugins.ipc.sendmessageoptions
+---@field data table<string,any> @Optional data given to the receiver.
+---@field on_reply plugins.ipc.onreply @Callback that allows monitoring all the replies received for this message.
+---@field on_read plugins.ipc.onmessage @Function executed by the message receiver.
+---@field destinations string | table<integer,string> | nil @Id of the running instances to receive the message, if not set all running instances will receive the message.
+
+---Queue a new message to be sent to other lite-xl instances.
+---@param name string
+---@param options? plugins.ipc.sendmessageoptions
+---@param message_type? plugins.ipc.messagetype
+---@return string | nil message_id
+function IPC:send_message(name, options, message_type)
+ options = options or {}
+
+ local found_destinations = {}
+ local instances = self:get_instances()
+ local destinations = options.destinations
+
+ if type(destinations) == "string" then
+ destinations = { destinations }
+ end
+
+ if not destinations then
+ for _, instance in ipairs(instances) do
+ table.insert(found_destinations, instance.id)
+ end
+ else
+ for _, destination in ipairs(destinations) do
+ for _, instance in ipairs(instances) do
+ if instance.id == destination then
+ table.insert(found_destinations, destination)
+ end
+ end
+ end
+ end
+
+ if #found_destinations <= 0 then
+ return nil
+ end
+
+ ---@type plugins.ipc.message
+ local message = {}
+ message.id = self.id .. "." .. tostring(system.get_time())
+ message.name = name
+ message.type = message_type or "message"
+ message.sender = self.id
+ message.data = options.data or {}
+ message.destinations = found_destinations
+ message.timestamp = os.time()
+ message.on_reply = options.on_reply or nil
+ message.on_read = options.on_read and string.dump(options.on_read) or nil
+ message.replies = {}
+
+ table.insert(self.messages, message)
+
+ update_file(self)
+
+ return message.id
+end
+
+---Add a listener for a given type of message.
+---@param name string
+---@param callback plugins.ipc.onmessage
+---@param message_type? plugins.ipc.messagetype
+---@return integer listener_position
+function IPC:listen_message(name, callback, message_type)
+ message_type = message_type or "message"
+
+ local type_name = message_type .. "." .. name
+ if not self.listeners[type_name] then
+ self.listeners[type_name] = {}
+ end
+
+ table.insert(self.listeners[type_name], callback)
+
+ return #self.listeners[type_name]
+end
+
+---Listen for a given signal.
+---@param name string
+---@param callback plugins.ipc.function
+---@return integer listener_position
+function IPC:listen_signal(name, callback)
+ local signal_cb = function(message)
+ callback(table.unpack(message.data))
+ end
+ return self:listen_message(name, signal_cb, "signal")
+end
+
+---Add a new signal that can be sent to other instances.
+---@param name string A unique name for the signal.
+---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into callback.
+function IPC:register_signal(name, params)
+ if self.signals[name] then
+ core.log_quiet("IPC: Overriding signal '%s'", name)
+ table.remove(self.signal_definitions, self.signals[name])
+ end
+
+ self.signals[name] = table.insert(
+ self.signal_definitions,
+ generate_definition(name, params)
+ )
+
+ table.sort(self.signal_definitions)
+end
+
+---Add a new method that can be invoked from other instances.
+---@param name string A unique name for the method.
+---@param method fun(...) Function invoked when the method is called.
+---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into method.
+---@param returns? plugins.ipc.vardecl[] Return values of the method.
+function IPC:register_method(name, method, params, returns)
+ if self.methods[name] then
+ core.log_quiet("IPC: Overriding method '%s'", name)
+ table.remove(self.method_definitions, self.methods[name])
+ end
+
+ self.methods[name] = table.insert(
+ self.method_definitions,
+ generate_definition(name, params, returns)
+ )
+
+ table.sort(self.method_definitions)
+
+ self:listen_message(name, function(message, reply)
+ local ret = table.pack(method(table.unpack(message.data)))
+ reply.data = ret
+ end, "method")
+end
+
+---Broadcast a signal to running instances.
+---@param destinations string | table<integer, string> | nil
+---@param name string
+---@vararg any signal_parameters
+function IPC:signal(destinations, name, ...)
+ self:send_message(name, {
+ destinations = destinations,
+ data = table.pack(self.id, ...)
+ }, "signal")
+end
+
+---Call a method on another instance and wait for reply.
+---@param destinations string | table<integer, string> | nil
+---@param name string
+---@return any | table<string,table> return_of_called_method
+function IPC:call(destinations, name, ...)
+ local message_id = self:send_message(name, {
+ destinations = destinations,
+ data = table.pack(...)
+ }, "method")
+
+ local ret = nil
+
+ if message_id then
+ local replies = self:wait_for_replies(message_id)
+ if replies and #replies > 1 then
+ ret = {}
+ for _, reply in ipairs(replies) do
+ ret[reply.replier] = reply.data
+ end
+ elseif replies and #replies > 0 then
+ return table.unpack(replies[1].data)
+ end
+ else
+ core.error("IPC Error: could not make call to '%s'", name)
+ end
+
+ return ret
+end
+
+---Call a method on another instance asynchronously waiting for the replies.
+---@param destinations string | table<integer, string> | nil
+---@param name string
+---@param callback fun(id: string, ret: table) | nil Called with the returned values
+---@return string | nil message_id
+function IPC:call_async(destinations, name, callback, ...)
+ return self:send_message(name, {
+ destinations = destinations,
+ data = table.pack(...),
+ on_reply = callback and function(reply)
+ callback(reply.replier, reply.data)
+ end or nil
+ }, "method")
+end
+
+---Main ipc session for current instance.
+---@type core.ipc
+local ipc = IPC()
+
+---Get the IPC session for the running lite-xl instance.
+---@return core.ipc
+function IPC.current()
+ return ipc
+end
+
+--------------------------------------------------------------------------------
+-- Override system.wait_event to allow ipc monitoring on the background.
+--------------------------------------------------------------------------------
+local system_wait_event = system.wait_event
+
+local run_threads = coroutine.wrap(function()
+ while true do
+ for k, thread in pairs(threads) do
+ if thread.wake < system.get_time() then
+ local _, wait = assert(coroutine.resume(thread.cr))
+ if coroutine.status(thread.cr) == "dead" then
+ table.remove(threads, k)
+ elseif wait then
+ thread.wake = system.get_time() + wait
+ end
+ end
+ coroutine.yield()
+ end
+ end
+end)
+
+system.wait_event = function(timeout)
+ run_threads()
+
+ if not timeout then
+ if not system.window_has_focus() then
+ local t = system.get_time()
+ local h = 0.5 / 2
+ local dt = math.ceil(t / h) * h - t
+
+ system_wait_event(dt + 1 / config.fps)
+ else
+ system_wait_event()
+ end
+ else
+ system_wait_event(timeout)
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Override system.show_fatal_error to be able and destroy session file on crash.
+--------------------------------------------------------------------------------
+local system_show_fatal_error = system.show_fatal_error
+
+system.show_fatal_error = function(title, message)
+ if title == "Lite XL internal error" then
+ ipc:stop()
+ end
+ system_show_fatal_error(title, message)
+end
+
+--------------------------------------------------------------------------------
+-- Override core.run to destroy ipc session file on exit.
+--------------------------------------------------------------------------------
+local core_run = core.run
+
+core.run = function()
+ core_run()
+ ipc:stop()
+end
+
+--------------------------------------------------------------------------------
+-- Override system.get_time temporarily as first function called on core.run
+-- to allow settings gui to properly load ipc config options.
+--------------------------------------------------------------------------------
+local system_get_time = system.get_time
+
+system.get_time = function()
+ if settings_found and not settings.ui then
+ return system_get_time()
+ end
+
+ if config.plugins.ipc.single_instance then
+ system.get_time = system_get_time
+
+ local primary_instance = ipc:get_primary_instance()
+ if primary_instance and ARGS[2] then
+ local open_directory = false
+ for i=2, #ARGS do
+ local path = system.absolute_path(ARGS[i])
+
+ if path then
+ local path_info = system.get_file_info(path)
+ if path_info then
+ if path_info.type == "file" then
+ ipc:call_async(primary_instance, "core.open_file", nil, path)
+ else
+ if not config.plugins.ipc.dirs_instance then
+ ipc:call_async(primary_instance, "core.open_directory", nil, path)
+ else
+ if #ARGS > 2 then
+ system.exec(string.format("%q %q", EXEFILE, path))
+ else
+ open_directory = true
+ end
+ end
+ end
+ end
+ end
+ end
+ ipc:wait_for_messages()
+ if not open_directory then
+ os.exit()
+ end
+ end
+ else
+ system.get_time = system_get_time
+ end
+
+ return system_get_time()
+end
+
+--------------------------------------------------------------------------------
+-- Register methods for opening files and directories.
+--------------------------------------------------------------------------------
+ipc:register_method("core.open_file", function(file)
+ if system.get_file_info(file) then
+ if system.raise_window then system.raise_window() end
+ core.root_view:open_doc(core.open_doc(file))
+ end
+end, {{name = "file", type = "string"}})
+
+ipc:register_method("core.open_directory", function(directory)
+ if system.get_file_info(directory) then
+ if system.raise_window then system.raise_window() end
+ core.add_project_directory(directory)
+ end
+end, {{name = "directory", type = "string"}})
+
+--------------------------------------------------------------------------------
+-- Register file dragging signals from instance to instance
+--------------------------------------------------------------------------------
+ipc:register_signal("core.tab_drag_start", {{name = "file", type = "string"}})
+ipc:register_signal("core.tab_drag_stop")
+ipc:register_signal("core.tab_drag_received", {{name = "file", type = "string"}})
+
+local rootview_tab_dragging = false
+local rootview_dragged_node = nil
+local rootview_waiting_drop_file = ""
+local rootview_waiting_drop_instance = ""
+
+local rootview_on_mouse_moved = RootView.on_mouse_moved
+function RootView:on_mouse_moved(x, y, dx, dy)
+ rootview_on_mouse_moved(self, x, y, dx, dy)
+ if
+ self.dragged_node and self.dragged_node.dragging
+ and
+ not rootview_tab_dragging
+ then
+ ---@type core.doc
+ local doc = core.active_view.doc
+ if doc and doc.abs_filename then
+ rootview_tab_dragging = true
+ ipc:signal(nil, "core.tab_drag_start", doc.abs_filename)
+ rootview_dragged_node = self.dragged_node
+ end
+ elseif rootview_dragged_node then
+ local w, h, wx, wy = system.get_window_size()
+ if x < 0 or x > w or y < 0 or y > h then
+ self.dragged_node = nil
+ self:set_show_overlay(self.drag_overlay, false)
+ elseif not self.dragged_node then
+ self.dragged_node = rootview_dragged_node
+ self:set_show_overlay(self.drag_overlay, true)
+ end
+ core.request_cursor("hand")
+ elseif rootview_waiting_drop_file ~= "" then
+ ipc:signal(
+ rootview_waiting_drop_instance,
+ "core.tab_drag_received",
+ rootview_waiting_drop_file
+ )
+ core.root_view:open_doc(core.open_doc(rootview_waiting_drop_file))
+ rootview_waiting_drop_file = ""
+ end
+end
+
+local rootview_on_mouse_released = RootView.on_mouse_released
+function RootView:on_mouse_released(button, x, y, ...)
+ rootview_on_mouse_released(self, button, x, y, ...)
+ if rootview_tab_dragging then
+ rootview_tab_dragging = false
+ rootview_dragged_node = nil
+ ipc:signal(nil, "core.tab_drag_stop")
+ end
+end
+
+ipc:listen_signal("core.tab_drag_start", function(instance, file)
+ rootview_waiting_drop_instance = instance
+ rootview_waiting_drop_file = file
+end)
+
+ipc:listen_signal("core.tab_drag_stop", function()
+ rootview_waiting_drop_instance = ""
+ rootview_waiting_drop_file = ""
+end)
+
+ipc:listen_signal("core.tab_drag_received", function()
+ command.perform("root:close")
+end)
+
+
+return IPC