mirror of
https://github.com/jiriks74/presence.nvim
synced 2024-12-28 11:02:34 +01:00
Initial commit
This commit is contained in:
commit
a656ba5361
8 changed files with 720 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.DS_Store
|
3
.luacheckrc
Normal file
3
.luacheckrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
globals = {
|
||||||
|
'vim',
|
||||||
|
}
|
48
lua/log.lua
Normal file
48
lua/log.lua
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
local Log = {}
|
||||||
|
|
||||||
|
Log.codes = {}
|
||||||
|
|
||||||
|
Log.levels = {
|
||||||
|
{ "debug", "Comment" },
|
||||||
|
{ "info", "None" },
|
||||||
|
{ "warn", "WarningMsg" },
|
||||||
|
{ "error", "ErrorMsg" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function Log.new(options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
local logger = vim.deepcopy(Log)
|
||||||
|
logger.options = options
|
||||||
|
logger.options.level = options.level
|
||||||
|
|
||||||
|
return logger
|
||||||
|
end
|
||||||
|
|
||||||
|
setmetatable(Log, {
|
||||||
|
__call = function(_, ...)
|
||||||
|
return Log.new(...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Initialize logger with log functions for each level
|
||||||
|
for i = 1, #Log.levels do
|
||||||
|
local level, hl = unpack(Log.levels[i])
|
||||||
|
|
||||||
|
Log.codes[level] = i
|
||||||
|
|
||||||
|
Log[level] = function(self, message)
|
||||||
|
-- Skip if log level is not set or the log is below the configured or default level
|
||||||
|
if not self.options.level or self.codes[level] < self.codes[self.options.level] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(string.format("echohl %s", hl))
|
||||||
|
vim.cmd(string.format([[echom "[%s] %s"]], "presence.nvim", vim.fn.escape(message, '"')))
|
||||||
|
vim.cmd("echohl NONE")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Log
|
197
lua/presence/discord.lua
Normal file
197
lua/presence/discord.lua
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
local Discord = {}
|
||||||
|
|
||||||
|
Discord.opcodes = {
|
||||||
|
auth = 0,
|
||||||
|
frame = 1,
|
||||||
|
closed = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Discord RPC Subscription events
|
||||||
|
-- https://discord.com/developers/docs/topics/rpc#commands-and-events-rpc-events
|
||||||
|
-- Ready: https://discord.com/developers/docs/topics/rpc#ready
|
||||||
|
-- Error: https://discord.com/developers/docs/topics/rpc#error
|
||||||
|
Discord.events = {
|
||||||
|
READY = "READY",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
local struct = require("struct")
|
||||||
|
|
||||||
|
-- Initialize a new Discord RPC client
|
||||||
|
function Discord:new(options)
|
||||||
|
self.log = options.logger
|
||||||
|
self.client_id = options.client_id
|
||||||
|
|
||||||
|
self.ipc_path = self.find_ipc_path()
|
||||||
|
self.pipe = vim.loop.new_pipe(true)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Connect to the local Discord RPC socket
|
||||||
|
-- TODO Might need to check for pipes ranging from discord-ipc-0 to discord-ipc-9:
|
||||||
|
-- https://github.com/discord/discord-rpc/blob/master/documentation/hard-mode.md#notes
|
||||||
|
function Discord:connect(on_connect)
|
||||||
|
if self.pipe:is_closing() then
|
||||||
|
self.pipe = vim.loop.new_pipe(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.pipe:connect(self.ipc_path.."/discord-ipc-0", on_connect)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Discord:is_connected()
|
||||||
|
return self.pipe:is_active()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Disconnect from the local Discord RPC socket
|
||||||
|
function Discord:disconnect(on_close)
|
||||||
|
self.pipe:shutdown()
|
||||||
|
if not self.pipe:is_closing() then
|
||||||
|
self.pipe:close(on_close)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make a remote procedure call to Discord
|
||||||
|
-- Callback argument in format: on_response(error[, response_table])
|
||||||
|
function Discord:call(opcode, payload, on_response)
|
||||||
|
self.encode_json(payload, function(body)
|
||||||
|
-- Start reading for the response
|
||||||
|
self.pipe:read_start(function(...)
|
||||||
|
self:read_message(payload.nonce, on_response, ...)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Construct message denoting little endian, auth opcode, msg length
|
||||||
|
local message = struct.pack("<ii", opcode, #body)..body
|
||||||
|
|
||||||
|
-- Write the message to the pipe
|
||||||
|
self.pipe:write(message, function(err)
|
||||||
|
if err then
|
||||||
|
local err_format = "Pipe write error - %s"
|
||||||
|
local err_message = string.format(err_format, err)
|
||||||
|
|
||||||
|
on_response(err_message)
|
||||||
|
else
|
||||||
|
self.log:debug("Successfully wrote message to pipe")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read and handle socket messages
|
||||||
|
function Discord:read_message(nonce, on_response, err, chunk)
|
||||||
|
if err then
|
||||||
|
local err_format = "Pipe read error - %s"
|
||||||
|
local err_message = string.format(err_format, err)
|
||||||
|
|
||||||
|
on_response(err_message)
|
||||||
|
|
||||||
|
elseif chunk then
|
||||||
|
local header_size = 9
|
||||||
|
local message = chunk:sub(header_size)
|
||||||
|
local response_opcode = struct.unpack("<ii", chunk)
|
||||||
|
|
||||||
|
self.decode_json(message, function(response)
|
||||||
|
-- Check for a non-frame opcode in the response
|
||||||
|
if response_opcode ~= self.opcodes.frame then
|
||||||
|
local err_format = "Received unexpected opcode - %s (code %s)"
|
||||||
|
local err_message = string.format(err_format, response.message, response.code)
|
||||||
|
|
||||||
|
return on_response(err_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for an error event response
|
||||||
|
if response.evt == self.events.ERROR then
|
||||||
|
local data = response.data
|
||||||
|
local err_format = "Received error event - %s (code %s)"
|
||||||
|
local err_message = string.format(err_format, data.message, data.code)
|
||||||
|
|
||||||
|
return on_response(err_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check for a valid nonce value
|
||||||
|
if response.nonce and response.nonce ~= vim.NIL and response.nonce ~= nonce then
|
||||||
|
local err_format = "Received unexpected nonce - %s (expected %s)"
|
||||||
|
local err_message = string.format(err_format, response.nonce, nonce)
|
||||||
|
|
||||||
|
return on_response(err_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
on_response(nil, response)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
-- TODO: Handle when pipe is closed
|
||||||
|
self.log:warn("Pipe was closed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call to authorize the client connection with Discord
|
||||||
|
-- Callback argument in format: on_authorize(error[, response_table])
|
||||||
|
function Discord:authorize(on_authorize)
|
||||||
|
local payload = {
|
||||||
|
client_id = self.client_id,
|
||||||
|
v = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
self:call(self.opcodes.auth, payload, on_authorize)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call to set the Neovim activity to Discord
|
||||||
|
function Discord:set_activity(activity, on_response)
|
||||||
|
local payload = {
|
||||||
|
cmd = "SET_ACTIVITY",
|
||||||
|
nonce = self.generate_uuid(),
|
||||||
|
args = {
|
||||||
|
activity = activity,
|
||||||
|
pid = vim.loop:getpid(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self:call(self.opcodes.frame, payload, on_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the the IPC path in temporary runtime directories
|
||||||
|
function Discord.find_ipc_path()
|
||||||
|
local env_vars = {
|
||||||
|
'TEMP',
|
||||||
|
'TMP',
|
||||||
|
'TMPDIR',
|
||||||
|
'XDG_RUNTIME_DIR',
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 1, #env_vars do
|
||||||
|
local var = env_vars[i]
|
||||||
|
local path = vim.loop.os_getenv(var)
|
||||||
|
if path then
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Discord.generate_uuid()
|
||||||
|
local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
||||||
|
|
||||||
|
local uuid = template:gsub('[xy]', function(char)
|
||||||
|
local n = char == 'x'
|
||||||
|
and math.random(0, 0xf)
|
||||||
|
or math.random(8, 0xb)
|
||||||
|
return string.format('%x', n)
|
||||||
|
end)
|
||||||
|
|
||||||
|
return uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
function Discord.decode_json(t, on_done)
|
||||||
|
vim.schedule(function()
|
||||||
|
on_done(vim.fn.json_decode(t))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Discord.encode_json(t, on_done)
|
||||||
|
vim.schedule(function()
|
||||||
|
on_done(vim.fn.json_encode(t))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Discord
|
50
lua/presence/files.lua
Normal file
50
lua/presence/files.lua
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
-- Discord application asset file names (name, key[, description]) by file extension
|
||||||
|
return {
|
||||||
|
applescript = { "Applescript", "applescript" },
|
||||||
|
ahk = { "Autohotkey", "autohotkey" },
|
||||||
|
cs = { "C#", "c_sharp" },
|
||||||
|
cpp = { "C++", "c_plus_plus" },
|
||||||
|
c = { "C ", "c" },
|
||||||
|
css = { "CSS", "css" },
|
||||||
|
clj = { "Clojure", "clojure" },
|
||||||
|
cljs = { "ClojureScript", "clojurescript" },
|
||||||
|
coffee = { "CoffeeScript", "coffeescript" },
|
||||||
|
cr = { "Crystal", "crystal" },
|
||||||
|
dart = { "Dart", "dart" },
|
||||||
|
ex = { "Elixir", "elixir" },
|
||||||
|
elm = { "Elm", "elm" },
|
||||||
|
erl = { "Erlang", "erlang" },
|
||||||
|
fs = { "F#", "f_sharp" },
|
||||||
|
go = { "Go", "go" },
|
||||||
|
html = { "HTML", "html" },
|
||||||
|
hack = { "Hack", "hack" },
|
||||||
|
hs = { "Haskell", "haskell" },
|
||||||
|
hx = { "Haxe", "haxe" },
|
||||||
|
java = { "Java", "java" },
|
||||||
|
js = { "JavaScript", "javascript" },
|
||||||
|
jl = { "Julia", "julia" },
|
||||||
|
kt = { "Kotlin", "kotlin" },
|
||||||
|
lua = { "Lua", "lua" },
|
||||||
|
nim = { "Nim", "nim" },
|
||||||
|
nix = { "Nix", "nix" },
|
||||||
|
php = { "PHP", "php" },
|
||||||
|
pl = { "Perl", "perl" },
|
||||||
|
ps1 = { "PowerShell", "powershell" },
|
||||||
|
purs = { "PureScript", "purescript" },
|
||||||
|
py = { "Python", "python" },
|
||||||
|
r = { "R", "r" },
|
||||||
|
jsx = { "React", "react" },
|
||||||
|
tsx = { "React", "react" },
|
||||||
|
re = { "Reason", "reason" },
|
||||||
|
rb = { "Ruby", "ruby" },
|
||||||
|
rs = { "Rust", "rust" },
|
||||||
|
scala = { "Scala", "scala" },
|
||||||
|
sh = { "Shell", "shell" },
|
||||||
|
bash = { "Shell", "shell" },
|
||||||
|
swift = { "Swift", "swift" },
|
||||||
|
tex = { "TeX", "tex" },
|
||||||
|
ts = { "TypeScript", "typescript" },
|
||||||
|
vim = { "Vim", "vim" },
|
||||||
|
viml = { "Vim", "vim" },
|
||||||
|
vue = { "Vue", "vue" },
|
||||||
|
}
|
227
lua/presence/init.lua
Normal file
227
lua/presence/init.lua
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
local Presence = {}
|
||||||
|
|
||||||
|
local Log = require("log")
|
||||||
|
local files = require("presence.files")
|
||||||
|
local DiscordRPC = require("presence.discord")
|
||||||
|
|
||||||
|
function Presence:setup(options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- Initialize logger with provided options
|
||||||
|
self.log = Log {
|
||||||
|
level = options.log_level or vim.g.presence_log_level,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.log:debug("Setting up plugin...")
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-- 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))
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
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 = os.time(os.date("!*t"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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
|
179
lua/struct.lua
Normal file
179
lua/struct.lua
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
local struct = {}
|
||||||
|
|
||||||
|
function struct.pack(format, ...)
|
||||||
|
local stream = {}
|
||||||
|
local vars = {...}
|
||||||
|
local endianness = true
|
||||||
|
|
||||||
|
for i = 1, format:len() do
|
||||||
|
local opt = format:sub(i, i)
|
||||||
|
|
||||||
|
if opt == '<' then
|
||||||
|
endianness = true
|
||||||
|
elseif opt == '>' then
|
||||||
|
endianness = false
|
||||||
|
elseif opt:find('[bBhHiIlL]') then
|
||||||
|
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
|
||||||
|
local val = tonumber(table.remove(vars, 1))
|
||||||
|
|
||||||
|
local bytes = {}
|
||||||
|
for _ = 1, n do
|
||||||
|
table.insert(bytes, string.char(val % (2 ^ 8)))
|
||||||
|
val = math.floor(val / (2 ^ 8))
|
||||||
|
end
|
||||||
|
|
||||||
|
if not endianness then
|
||||||
|
table.insert(stream, string.reverse(table.concat(bytes)))
|
||||||
|
else
|
||||||
|
table.insert(stream, table.concat(bytes))
|
||||||
|
end
|
||||||
|
elseif opt:find('[fd]') then
|
||||||
|
local val = tonumber(table.remove(vars, 1))
|
||||||
|
local sign = 0
|
||||||
|
|
||||||
|
if val < 0 then
|
||||||
|
sign = 1
|
||||||
|
val = -val
|
||||||
|
end
|
||||||
|
|
||||||
|
local mantissa, exponent = math.frexp(val)
|
||||||
|
if val == 0 then
|
||||||
|
mantissa = 0
|
||||||
|
exponent = 0
|
||||||
|
else
|
||||||
|
mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24)
|
||||||
|
exponent = exponent + ((opt == 'd') and 1022 or 126)
|
||||||
|
end
|
||||||
|
|
||||||
|
local bytes = {}
|
||||||
|
if opt == 'd' then
|
||||||
|
val = mantissa
|
||||||
|
for _ = 1, 6 do
|
||||||
|
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
|
||||||
|
val = math.floor(val / (2 ^ 8))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8)))
|
||||||
|
val = math.floor(mantissa / (2 ^ 8))
|
||||||
|
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
|
||||||
|
val = math.floor(val / (2 ^ 8))
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8)))
|
||||||
|
val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8))
|
||||||
|
table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8)))
|
||||||
|
|
||||||
|
if not endianness then
|
||||||
|
table.insert(stream, string.reverse(table.concat(bytes)))
|
||||||
|
else
|
||||||
|
table.insert(stream, table.concat(bytes))
|
||||||
|
end
|
||||||
|
elseif opt == 's' then
|
||||||
|
table.insert(stream, tostring(table.remove(vars, 1)))
|
||||||
|
table.insert(stream, string.char(0))
|
||||||
|
elseif opt == 'c' then
|
||||||
|
local n = format:sub(i + 1):match('%d+')
|
||||||
|
local str = tostring(table.remove(vars, 1))
|
||||||
|
local len = tonumber(n)
|
||||||
|
if len <= 0 then
|
||||||
|
len = str:len()
|
||||||
|
end
|
||||||
|
if len - str:len() > 0 then
|
||||||
|
str = str .. string.rep(' ', len - str:len())
|
||||||
|
end
|
||||||
|
table.insert(stream, str:sub(1, len))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function struct.unpack(format, stream, pos)
|
||||||
|
local vars = {}
|
||||||
|
local iterator = pos or 1
|
||||||
|
local endianness = true
|
||||||
|
|
||||||
|
for i = 1, format:len() do
|
||||||
|
local opt = format:sub(i, i)
|
||||||
|
|
||||||
|
if opt == '<' then
|
||||||
|
endianness = true
|
||||||
|
elseif opt == '>' then
|
||||||
|
endianness = false
|
||||||
|
elseif opt:find('[bBhHiIlL]') then
|
||||||
|
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
|
||||||
|
local signed = opt:lower() == opt
|
||||||
|
|
||||||
|
local val = 0
|
||||||
|
for j = 1, n do
|
||||||
|
local byte = string.byte(stream:sub(iterator, iterator))
|
||||||
|
if endianness then
|
||||||
|
val = val + byte * (2 ^ ((j - 1) * 8))
|
||||||
|
else
|
||||||
|
val = val + byte * (2 ^ ((n - j) * 8))
|
||||||
|
end
|
||||||
|
iterator = iterator + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if signed and val >= 2 ^ (n * 8 - 1) then
|
||||||
|
val = val - 2 ^ (n * 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(vars, math.floor(val))
|
||||||
|
elseif opt:find('[fd]') then
|
||||||
|
local n = (opt == 'd') and 8 or 4
|
||||||
|
local x = stream:sub(iterator, iterator + n - 1)
|
||||||
|
iterator = iterator + n
|
||||||
|
|
||||||
|
if not endianness then
|
||||||
|
x = string.reverse(x)
|
||||||
|
end
|
||||||
|
|
||||||
|
local sign = 1
|
||||||
|
local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128)
|
||||||
|
for j = n - 2, 1, -1 do
|
||||||
|
mantissa = mantissa * (2 ^ 8) + string.byte(x, j)
|
||||||
|
end
|
||||||
|
|
||||||
|
if string.byte(x, n) > 127 then
|
||||||
|
sign = -1
|
||||||
|
end
|
||||||
|
|
||||||
|
local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) +
|
||||||
|
math.floor(string.byte(x, n - 1) /
|
||||||
|
((opt == 'd') and 16 or 128))
|
||||||
|
if exponent == 0 then
|
||||||
|
table.insert(vars, 0.0)
|
||||||
|
else
|
||||||
|
mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign
|
||||||
|
table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127)))
|
||||||
|
end
|
||||||
|
elseif opt == 's' then
|
||||||
|
local bytes = {}
|
||||||
|
for j = iterator, stream:len() do
|
||||||
|
if stream:sub(j,j) == string.char(0) or stream:sub(j) == '' then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(bytes, stream:sub(j, j))
|
||||||
|
end
|
||||||
|
|
||||||
|
local str = table.concat(bytes)
|
||||||
|
iterator = iterator + str:len() + 1
|
||||||
|
table.insert(vars, str)
|
||||||
|
elseif opt == 'c' then
|
||||||
|
local n = format:sub(i + 1):match('%d+')
|
||||||
|
local len = tonumber(n)
|
||||||
|
if len <= 0 then
|
||||||
|
len = table.remove(vars)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(vars, stream:sub(iterator, iterator + len - 1))
|
||||||
|
iterator = iterator + len
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unpack(vars)
|
||||||
|
end
|
||||||
|
|
||||||
|
return struct
|
15
plugin/presence.vim
Normal file
15
plugin/presence.vim
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
" Define autocommands to handle auto-update events
|
||||||
|
augroup presence_events
|
||||||
|
autocmd!
|
||||||
|
if g:presence_auto_update
|
||||||
|
autocmd BufRead * lua package.loaded.presence:update()
|
||||||
|
endif
|
||||||
|
augroup END
|
||||||
|
|
||||||
|
" Fallback to setting up the plugin automatically
|
||||||
|
if !exists("g:presence_has_setup")
|
||||||
|
lua << EOF
|
||||||
|
local Presence = require("presence"):setup()
|
||||||
|
Presence.log:debug("Custom setup not detected, plugin set up using defaults")
|
||||||
|
EOF
|
||||||
|
endif
|
Loading…
Reference in a new issue