1
0
mirror of https://github.com/jiriks74/presence.nvim synced 2024-11-23 20:37:50 +01:00
presence.nvim/lua/presence/init.lua
Andrew Kwon 66bd159dd3 Allow cancelling presence on remote nvim instances
- Add msgpack lib
- Restructure to have lib and deps folders
- Add cancellation methods and invoke remote cancellation on update
2021-01-10 22:28:01 -08:00

342 lines
10 KiB
Lua

local Presence = {}
local Log = require("lib.log")
local files = require("presence.files")
local msgpack = require("deps.msgpack")
local DiscordRPC = require("presence.discord")
function Presence:setup(options)
options = options or {}
self.options = options
-- Initialize logger with provided options
self.log = Log {
level = options.log_level or vim.g.presence_log_level,
}
self.log:debug("Setting up plugin...")
-- Warn on any duplicate user-defined options
self:check_dup_options("auto_update")
self:check_dup_options("log_level")
-- Ensure auto-update config is reflected in its global var setting
if options.auto_update ~= nil or not vim.g.presence_auto_update then
local should_auto_update = options.auto_update ~= nil
and (options.auto_update and 1 or 0)
or (vim.g.presence_auto_update or 1)
vim.api.nvim_set_var("presence_auto_update", should_auto_update)
end
-- Set autocommands
vim.fn["presence#SetAutoCmds"]()
-- Internal state
self.is_connected = false
self.is_authorized = false
-- Use the default or user-defined client id if provided
self.client_id = "793271441293967371"
if options.client_id then
self.log:debug("Using user-defined Discord client id")
self.client_id = options.client_id
end
self.discord = DiscordRPC:new({
client_id = self.client_id,
logger = self.log,
})
self.log:info("Completed plugin setup")
-- Set global variable to indicate plugin has been set up
vim.api.nvim_set_var("presence_has_setup", 1)
return self
end
-- Send a nil activity to unset the presence
function Presence:cancel_presence()
self.log:debug("Nullifying Discord presence...")
if not self.discord:is_connected() then
return
end
self.discord:set_activity(nil, function(err)
if err then
self.log:error("Failed to set nil activity in Discord: "..err)
return
end
self.log:info("Sent nil activity to Discord")
end)
end
-- Send command to cancel the presence for all other remote Neovim instances
function Presence:cancel_all_remote_presences()
self:get_nvim_socket_addrs(function(sockets)
for i = 1, #sockets do
local nvim_socket = sockets[i]
-- Skip if the nvim socket is the current instance
if nvim_socket ~= vim.v.servername then
local command = "lua package.loaded.presence:cancel_presence()"
self:call_remote_nvim_instance(nvim_socket, command)
end
end
end)
end
-- Call a command on a remote Neovim instance at the provided IPC path
function Presence:call_remote_nvim_instance(ipc_path, command)
local remote_nvim_instance = vim.loop.new_pipe(true)
remote_nvim_instance:connect(ipc_path, function()
self.log:debug(string.format("Connected to remote nvim instance at %s", ipc_path))
local packed = msgpack.pack({ 0, 0, "nvim_command", { command } })
remote_nvim_instance:write(packed, function()
self.log:debug(string.format("Wrote to remote nvim instance: %s", ipc_path))
remote_nvim_instance:shutdown()
remote_nvim_instance:close()
end)
end)
end
-- Check and warn for duplicate user-defined options
function Presence:check_dup_options(option)
local g_variable = "presence_"..option
if self.options[option] ~= nil and vim.g[g_variable] ~= nil then
local warning_fmt = "Duplicate options set: `g:%s` and setup option `%s`"
local warning_msg = string.format(warning_fmt, g_variable, option)
self.log:warn(warning_msg)
end
end
function Presence:connect(on_done)
self.log:debug("Connecting to Discord...")
self.discord:connect(function(err)
-- Handle known connection errors
if err == "EISCONN" then
self.log:info("Already connected to Discord")
elseif err == "ECONNREFUSED" then
self.log:warn("Failed to connect to Discord: "..err.." (is Discord running?)")
return
elseif err then
self.log:error("Failed to connect to Discord: "..err)
return
end
self.log:info("Connected to Discord")
self.is_connected = true
if on_done then on_done() end
end)
end
function Presence:authorize(on_done)
self.log:debug("Authorizing with Discord...")
self.discord:authorize(function(err, response)
if err and err:find(".*already did handshake.*") then
self.log:info("Already authorized with Discord")
self.is_authorized = true
return on_done()
elseif err then
self.log:error("Failed to authorize with Discord: "..err)
self.is_authorized = false
return
end
self.log:info("Authorized with Discord for "..response.data.user.username)
self.is_authorized = true
if on_done then on_done() end
end)
end
-- Gets the file path of the current vim buffer
function Presence.get_current_buffer(on_buffer)
vim.schedule(function()
local current_buffer = vim.api.nvim_get_current_buf()
local buffer = vim.api.nvim_buf_get_name(current_buffer)
on_buffer(buffer)
end)
end
-- Gets the current project name
function Presence:get_project_name(file_path)
-- TODO: Only checks for a git repository, could add more checks here
-- Might want to run this in a background process depending on performance
local project_path_cmd = "git rev-parse --show-toplevel"
project_path_cmd = file_path
and string.format("cd %s && %s", file_path, project_path_cmd)
or project_path_cmd
local project_path = vim.fn.system(project_path_cmd)
project_path = vim.trim(project_path)
if #project_path == 0 or project_path:find("fatal.*") then
return nil
end
return self.get_filename(project_path)
end
-- Get the name of the parent directory for the given path
function Presence.get_dir_path(path)
return path:match("^(.+/.+)/.*$")
end
-- Get the name of the file for the given path
function Presence.get_filename(path)
return path:match("^.+/(.+)$")
end
-- Get the file extension for the given filename
function Presence.get_file_extension(path)
return path:match("^.+%.(.+)$")
end
-- Get all active local nvim unix domain socket addresses
function Presence:get_nvim_socket_addrs(on_done)
-- TODO: Find a better way to get paths of remote Neovim sockets lol
local cmd = [[netstat -u | grep --color=never "nvim.*/0" | awk -F "[ :]+" '{print $9}' | uniq]]
local sockets = {}
local function handle_data(_, data)
if not data then return end
for i = 1, #data do
local socket = data[i]
if socket ~= "" and socket ~= vim.v.servername then
table.insert(sockets, socket)
end
end
end
local function handle_error(_, data)
if not data then return end
if data[1] ~= "" then
self.log:error(data[1])
end
end
local function handle_exit()
on_done(sockets)
end
vim.fn.jobstart(cmd, {
on_stdout = handle_data,
on_stderr = handle_error,
on_exit = handle_exit,
})
end
-- Wrap calls to Discord that require prior connection and authorization
function Presence.discord_event(on_ready)
return function(self, ...)
local args = {...}
local callback = function() on_ready(self, unpack(args)) end
if self.is_connected and self.is_authorized then
return callback()
end
if self.is_connected and not self.is_authorized then
return self:authorize(callback)
end
self:connect(function()
if self.is_authorized then
return callback()
end
self:authorize(callback)
end)
end
end
-- Update Rich Presence for the provided vim buffer
function Presence:update_for_buffer(buffer)
self.log:debug(string.format("Setting activity for %s...", buffer))
-- Send command to cancel presence for all remote Neovim instances
self:cancel_all_remote_presences()
-- Parse vim buffer
local filename = self.get_filename(buffer)
local extension = self.get_file_extension(filename)
local parent_dirpath = self.get_dir_path(buffer)
-- Determine image text and asset key
local name = filename
local asset_key = "file"
local description = filename
if files[extension] then
name, asset_key, description = unpack(files[extension])
end
-- TODO: Update timestamp to be workspace-specific
local started_at = os.time()
local activity = {
state = string.format("Editing %s", filename),
assets = {
large_image = "neovim",
large_text = "The One True Text Editor",
small_image = asset_key,
small_text = description or name,
},
timestamps = {
start = started_at
},
}
-- Include project details if available
local project_name = self:get_project_name(parent_dirpath)
if project_name then
self.log:debug(string.format("Detected project: %s", project_name))
activity.details = string.format("Working on %s", project_name)
else
self.log:debug("No project detected")
end
self.discord:set_activity(activity, function(err)
if err then
self.log:error("Failed to set activity in Discord: "..err)
return
end
self.log:info(string.format("Set activity in Discord for %s", filename))
end)
end
-- Update Rich Presence for the current or provided vim buffer for an authorized connection
Presence.update = Presence.discord_event(function(self, buffer)
if buffer then
self:update_for_buffer(buffer)
else
self.get_current_buffer(function(current_buffer)
self:update_for_buffer(current_buffer)
end)
end
end)
function Presence:stop()
self.log:debug("Disconnecting from Discord...")
self.discord:disconnect(function()
self.log:info("Disconnected from Discord")
end)
end
return Presence