diff --git a/.github/wiki/Build.md b/.github/wiki/Build.md
index dbc55ae6..257d6d9e 100644
--- a/.github/wiki/Build.md
+++ b/.github/wiki/Build.md
@@ -7,13 +7,4 @@
```bash
git clone https://github.com/vyfor/cord.nvim
```
-2. Build the server binary and place it under the Neovim data directory
- **Linux:**
- ```bash
- cargo install --path . --root ~/.local/share/nvim/cord --force
- ```
-
- **Windows (PowerShell):**
- ```powershell
- cargo install --path . --root $env:LOCALAPPDATA/nvim-data/cord --force
- ```
\ No newline at end of file
+2. Build the server binary using `:Cord update build` which will place the built binary under the Neovim data directory
\ No newline at end of file
diff --git a/.github/wiki/Configuration.md b/.github/wiki/Configuration.md
index 34b09298..be8d627b 100644
--- a/.github/wiki/Configuration.md
+++ b/.github/wiki/Configuration.md
@@ -47,6 +47,7 @@ If you want a fresh start, you can copy the default config and tweak it. I sugge
display = {
theme = 'default',
flavor = 'dark',
+ view = 'full',
swap_fields = false,
swap_icons = false,
},
@@ -118,6 +119,7 @@ If you want a fresh start, you can copy the default config and tweak it. I sugge
timeout = 300000,
},
discord = {
+ pipe_paths = nil,
reconnect = {
enabled = false,
interval = 5000,
@@ -166,12 +168,20 @@ With this option set to true, the plugin **will not start automatically**. Inste
| --------------------- | --------- | --------- | --------------------------------------------------------------------------- |
| `display.theme` | `string` | `default` | Choose between different icon themes; 'default', 'atom', 'catppuccin' |
| `display.flavor` | `string` | `dark` | Choose between different theme flavors; typically 'dark', 'light', 'accent' |
+| `display.view` | `string` | `full` | Control what shows up as the large and small images |
| `display.swap_fields` | `boolean` | `false` | Show workspace name before filename |
| `display.swap_icons` | `boolean` | `false` | Use editor icon as large image |
->[!TIP]
+> [!TIP]
> Check out our icon [showcase](https://github.com/vyfor/icons#showcase)!
+### View
+
+- `full` - always shows both of the icons
+- `editor` - only shows the editor icon
+- `asset` - only shows the asset (language, file browser, plugin manager, etc.) icon
+- `auto` - shows both icons, but drops the language icon if in a new/empty buffer
+
## ⏰ Timestamp
| Option | Type | Default | Description |
@@ -190,7 +200,7 @@ With this option set to true, the plugin **will not start automatically**. Inste
| `idle.show_status` | `boolean` | `true` | Show idle status in presence, or hide the presence if `false` |
| `idle.ignore_focus` | `boolean` | `true` | Show idle despite Neovim having focus |
| `idle.unidle_on_focus` | `boolean` | `true` | Unidle the session when Neovim gains focus |
-| `idle.smart_idle` | `boolean` | `true` | Enable [smart idle](#smart-idle) feature |
+| `idle.smart_idle` | `boolean` | `true` | Enable smart idle feature. See below |
| `idle.details` | `string \| function(opts)` | `'Idling'` | Details shown when idle |
| `idle.state` | `string \| function(opts)` | `nil` | State shown when idle |
| `idle.tooltip` | `string \| function(opts)` | `'💤'` | Tooltip shown when hovering over idle icon |
@@ -376,20 +386,38 @@ require('cord').setup {
## ⚙️ Advanced
-| Option | Type | Default | Description |
-| ------------------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `advanced.plugin.autocmds` | `boolean` | `true` | Enable autocmds |
-| `advanced.plugin.cursor_update` | `string` | `'on_hold'` | When to update cursor position: `'on_move'`, `'on_hold'`, or `'none'`. See [Cursor Update Mode](#cursor-update-mode) |
-| `advanced.plugin.match_in_mappings` | `boolean` | `true` | Whether to match against file extensions in mappings |
-| `advanced.server.update` | `string` | `'fetch'` | Default way to acquire the server executable either if the executable is not found or a manual update is requested: `'fetch'` - fetch from GitHub, `'build'` - build from source, `'none'` - no-op |
-| `advanced.server.pipe_path` | `string \| nil` | `nil` | Custom IPC pipe path |
-| `advanced.server.executable_path` | `string \| nil` | `nil` | Custom server executable path |
-| `advanced.server.timeout` | `number` | `300000` | Server shutdown timeout (ms) |
-| `advanced.discord.reconnect.enabled` | `boolean` | `false` | Whether reconnection is enabled. Has minimal impact on performance |
-| `advanced.discord.reconnect.interval` | `number` | `5000` | Reconnection interval in milliseconds, 0 to disable |
-| `advanced.discord.reconnect.initial` | `boolean` | `true` | Whether to reconnect if initial connection fails |
-| `advanced.workspace.root_markers` | `string[]` | `string[]` | List of root markers to use when determining the workspace directory |
-| `advanced.workspace.limit_to_cwd` | `boolean` | `false` | Limits workspace detection to the working directory (vim.fn.getcwd()). When true, workspace detection stops at the CWD if no marker is found, making the search more efficient |
+### Plugin Settings
+
+| Option | Type | Default | Description |
+| ----------------------------------- | --------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
+| `advanced.plugin.autocmds` | `boolean` | `true` | Enable autocmds |
+| `advanced.plugin.cursor_update` | `string` | `'on_hold'` | When to update cursor position: `'on_move'`, `'on_hold'`, or `'none'`. See [Cursor Update Mode](#cursor-update-mode) |
+| `advanced.plugin.match_in_mappings` | `boolean` | `true` | Whether to match against file extensions in mappings |
+
+### Server Settings
+
+| Option | Type | Default | Description |
+| --------------------------------- | --------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `advanced.server.update` | `string` | `'fetch'` | Default way to acquire the server executable either if the executable is not found or a manual update is requested: `'fetch'` - fetch from GitHub, `'install'` - install from crates.io, `'build'` - build from source, `'none'` - no-op |
+| `advanced.server.pipe_path` | `string \| nil` | `nil` | Custom IPC pipe path |
+| `advanced.server.executable_path` | `string \| nil` | `nil` | Custom server executable path |
+| `advanced.server.timeout` | `number` | `300000` | Server shutdown timeout (ms) |
+
+### Discord Settings
+
+| Option | Type | Default | Description |
+| ------------------------------------- | ---------- | ------- | ------------------------------------------------------------------ |
+| `advanced.discord.pipe_paths` | `string[]` | `nil` | Custom IPC pipe paths to use when connecting to Discord |
+| `advanced.discord.reconnect.enabled` | `boolean` | `false` | Whether reconnection is enabled. Has minimal impact on performance |
+| `advanced.discord.reconnect.interval` | `number` | `5000` | Reconnection interval in milliseconds, 0 to disable |
+| `advanced.discord.reconnect.initial` | `boolean` | `true` | Whether to reconnect if initial connection fails |
+
+### Workspace Settings
+
+| Option | Type | Default | Description |
+| --------------------------------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `advanced.workspace.root_markers` | `string[]` | `string[]` | List of root markers to use when determining the workspace directory |
+| `advanced.workspace.limit_to_cwd` | `boolean` | `false` | Limits workspace detection to the working directory (vim.fn.getcwd()). When true, workspace detection stops at the CWD if no marker is found, making the search more efficient |
### Cursor Update Mode
@@ -424,7 +452,8 @@ The `advanced.cursor_update_mode` option controls how cursor position updates ar
- `:Cord update` - Update the server executable using the configured update mode (fetch by default)
- `:Cord update check` - Check for server updates
- `:Cord update fetch` - Fetch the server executable from GitHub using `curl`
- - `:Cord update build` - Build the server executable using `cargo`
+ - `:Cord update install` - Install the server executable from crates.io using `cargo`
+ - `:Cord update build` - Build the server executable locally using `cargo`
- `:Cord status` - Show connection status
- `:Cord version` - Show current server version
- `:Cord restart` - Restart the server
diff --git a/.github/wiki/FAQ.md b/.github/wiki/FAQ.md
index 88399000..b6fb261d 100644
--- a/.github/wiki/FAQ.md
+++ b/.github/wiki/FAQ.md
@@ -22,7 +22,7 @@ require 'cord'.setup {
}
```
-2. Set the `CORD_LOG_FILE` environment variable to a file path. This will redirect all logs to that file, but note that doing so will bypass the `log_level` setting and log everything. This is useful for debugging as trace and debug logs can be very verbose and overwhelming in the editor.
+1. Set the `CORD_LOG_FILE` environment variable to a file path. This will redirect all logs to that file. This is useful for debugging as trace and debug logs can be very verbose and overwhelming in the editor.
> ### Q: Can I use a custom name in my Rich Presence?
@@ -44,7 +44,7 @@ Cord's server keeps running intentionally. In fact, this is one of the key desig
> ### Q: I'm using a custom Discord client. Will Cord work with it?
-Yes, although we do not endorse custom clients, and cannot guarantee that they will work. The main issue is that custom clients often cannot/do not expose the IPC pipe at the same path as the official client, so you might need to create a symlink to make it work.
+Yes, although we do not endorse custom clients, and cannot guarantee that they will work. The main issue is that custom clients often cannot/do not expose the IPC pipe at the same path as the official client, so you might need to create a symlink to make it work. You can also override the defaults by setting the `advanced.discord.pipe_paths` field to a list of absolute paths to use when connecting to Discord.
> ### Q: Is X plugin or X language supported?
diff --git a/README.md b/README.md
index 87a7539e..b6974a0b 100644
--- a/README.md
+++ b/README.md
@@ -28,11 +28,11 @@
> Cord no longer requires Rust to be installed. Server component will be automatically downloaded from GitHub Releases.
## 💎 Features
-- ⚡ Fast, lightweight, and batteries-included.
-- 🚀 Event-based architecture with instant presence updates.
+- ⚡ Fast, lightweight, and batteries included.
+- 🚀 Event-driven architecture with instant presence updates.
- 🎨 Dynamic string templates with custom variables.
- 🗃️ Customizable assets for any file/buffer type.
-- 🔧 Flexible configuration with rich API, function-based fields, hook system and user commands.
+- 🔧 Flexible configuration with rich API, function-based fields, hooks, user commands and support for custom IPC paths.
- 🔌 Plugin system of its own, with many plugins out-of-the-box.
- 🛠️ Finds repositories and workspaces based on VCS files without relying on command-line tools.
- 🧠 Manages activities across all instances with a single connection to Discord.
@@ -121,9 +121,15 @@ Invoke `:Cord update` whenever the plugin is updated.
Considerations
-Cord requires the server executables to be present. By default, the plugin automatically fetches them from GitHub, which requires a dependency on [**`curl`**](https://curl.se). Alternatively, either:
-- **Download from GitHub**: Get latest release from https://github.com/vyfor/cord.nvim/releases/latest, rename it to cord[.exe] and place it under `nvim-data-dir/cord/bin`
-- [**Build from source**](https://github.com/vyfor/cord.nvim/wiki/Build)
+Cord requires the **server executable** to be present. By default, the plugin automatically fetches it from GitHub, which requires a dependency on [**`curl`**](https://curl.se).
+
+Alternatively, you can provide the executable manually:
+
+- **Download from GitHub:**
+ Get the latest release from [https://github.com/vyfor/cord.nvim/releases/latest](https://github.com/vyfor/cord.nvim/releases/latest), rename it to `cord` (or `cord.exe` on Windows), and place it under `nvim-data-dir/cord/bin`. This is essentially what `:Cord update fetch` does.
+
+- **Build from source:**
+ Run `:Cord update install` to install from crates.io or `:Cord update build` to build from source locally.
diff --git a/lua/cord/api/command.lua b/lua/cord/api/command.lua
index 18a74d06..9df82095 100644
--- a/lua/cord/api/command.lua
+++ b/lua/cord/api/command.lua
@@ -1,16 +1,21 @@
local M = {}
-M.build = function()
- require('cord.core.async').run(function() require('cord.server.update').build():await() end)
+M.install = function()
+ require('cord.core.async').run(function() require('cord.server.update').install():await() end)
end
M.fetch = function()
require('cord.core.async').run(function() require('cord.server.update').fetch():await() end)
end
+M.build = function()
+ require('cord.core.async').run(function() require('cord.server.update').build():await() end)
+end
M.update = function()
local mode = require('cord.plugin.config').advanced.server.update
if mode == 'fetch' then
M.fetch()
+ elseif mode == 'install' then
+ M.install()
elseif mode == 'build' then
M.build()
elseif mode ~= 'none' then
@@ -198,6 +203,7 @@ M.commands = {
subcommands = {
check = M.check,
fetch = M.fetch,
+ install = M.install,
build = M.build,
},
},
diff --git a/lua/cord/api/config.lua b/lua/cord/api/config.lua
index 3bc82136..313f0c4b 100644
--- a/lua/cord/api/config.lua
+++ b/lua/cord/api/config.lua
@@ -15,6 +15,7 @@ local validation_rules = {
['display'] = { 'table' },
['display.theme'] = { 'string' },
['display.flavor'] = { 'string' },
+ ['display.view'] = { 'string' },
['display.swap_fields'] = { 'boolean' },
['display.swap_icons'] = { 'boolean' },
@@ -81,6 +82,7 @@ local validation_rules = {
['advanced.server.executable_path'] = { 'string' },
['advanced.server.timeout'] = { 'number' },
['advanced.discord'] = { 'table' },
+ ['advanced.discord.pipe_paths'] = { 'table' },
['advanced.discord.reconnect'] = { 'table' },
['advanced.discord.reconnect.enabled'] = { 'boolean' },
['advanced.discord.reconnect.interval'] = { 'number' },
@@ -93,6 +95,7 @@ local validation_rules = {
local array_paths = {
['buttons'] = true,
['plugins'] = true,
+ ['advanced.discord.pipe_paths'] = true,
['advanced.workspace.root_markers'] = true,
}
diff --git a/lua/cord/plugin/activity/init.lua b/lua/cord/plugin/activity/init.lua
index ec8d52ca..fdca4290 100644
--- a/lua/cord/plugin/activity/init.lua
+++ b/lua/cord/plugin/activity/init.lua
@@ -91,19 +91,43 @@ local function build_activity(opts)
end
local large_image, large_text, small_image, small_text
- if opts.filetype == 'Cord.new' then
- large_image = config.editor.icon
- large_text = config_utils.get(config.editor.tooltip, opts)
- elseif config.display.swap_icons then
+
+ local function set_editor_only()
large_image = config.editor.icon
large_text = config_utils.get(config.editor.tooltip, opts)
- small_image = opts.icon
- small_text = opts.tooltip or opts.filetype
- else
+ end
+
+ local function set_asset_only()
large_image = opts.icon
large_text = opts.tooltip or opts.filetype
- small_image = config.editor.icon
- small_text = config_utils.get(config.editor.tooltip, opts)
+ end
+
+ local function set_full()
+ if config.display.swap_icons then
+ large_image = config.editor.icon
+ large_text = config_utils.get(config.editor.tooltip, opts)
+ small_image = opts.icon
+ small_text = opts.tooltip or opts.filetype
+ else
+ large_image = opts.icon
+ large_text = opts.tooltip or opts.filetype
+ small_image = config.editor.icon
+ small_text = config_utils.get(config.editor.tooltip, opts)
+ end
+ end
+
+ if config.display.view == 'auto' then
+ if opts.filetype == 'Cord.new' then
+ set_editor_only()
+ else
+ set_full()
+ end
+ elseif config.display.view == 'editor' then
+ set_editor_only()
+ elseif config.display.view == 'asset' then
+ set_asset_only()
+ elseif config.display.view == 'full' or config.display.view == 'auto' then
+ set_full()
end
return {
@@ -134,16 +158,37 @@ local function build_idle_activity(opts)
end
local large_image, large_text, small_image, small_text
- if config.display.swap_icons then
+
+ local function set_editor_only()
large_image = config.editor.icon
large_text = config_utils.get(config.editor.tooltip, opts)
- small_image = config_utils.get(config.idle.icon, opts)
- small_text = config_utils.get(config.idle.tooltip, opts)
- else
+ end
+
+ local function set_asset_only()
large_image = config_utils.get(config.idle.icon, opts)
large_text = config_utils.get(config.idle.tooltip, opts)
- small_image = config.editor.icon
- small_text = config_utils.get(config.editor.tooltip, opts)
+ end
+
+ local function set_full()
+ if config.display.swap_icons then
+ large_image = config.editor.icon
+ large_text = config_utils.get(config.editor.tooltip, opts)
+ small_image = config_utils.get(config.idle.icon, opts)
+ small_text = config_utils.get(config.idle.tooltip, opts)
+ else
+ large_image = config_utils.get(config.idle.icon, opts)
+ large_text = config_utils.get(config.idle.tooltip, opts)
+ small_image = config.editor.icon
+ small_text = config_utils.get(config.editor.tooltip, opts)
+ end
+ end
+
+ if config.display.view == 'editor' then
+ set_editor_only()
+ elseif config.display.view == 'asset' then
+ set_asset_only()
+ elseif config.display.view == 'full' or config.display.view == 'auto' then
+ set_full()
end
return {
diff --git a/lua/cord/plugin/config/init.lua b/lua/cord/plugin/config/init.lua
index ed150c3c..83a40297 100644
--- a/lua/cord/plugin/config/init.lua
+++ b/lua/cord/plugin/config/init.lua
@@ -12,6 +12,7 @@
---@class CordDisplayConfig
---@field theme? 'default'|'atom'|'catppuccin'|string Set icon theme
---@field flavor? 'dark'|'light'|'accent'|string Set icon theme flavor
+---@field view? 'full'|'editor'|'asset'|'auto'|string Control what shows up as the large and small images
---@field swap_fields? boolean Whether to swap activity fields
---@field swap_icons? boolean Whether to swap activity icons
@@ -86,12 +87,13 @@
---@field match_in_mappings? boolean Whether to match against file extensions in mappings
---@class CordAdvancedServerConfig
----@field update? 'fetch'|'build'|'none'|string How to acquire the server executable: 'fetch' or 'build' or 'none'
+---@field update? 'fetch'|'install'|'build'|'none'|string How to acquire the server executable: 'fetch' or 'install' or 'build' or 'none'
---@field pipe_path? string Path to the server's pipe
---@field executable_path? string Path to the server's executable
---@field timeout? integer Timeout in milliseconds
---@class CordAdvancedDiscordConfig
+---@field pipe_paths? string Pipe paths to use when connecting to Discord
---@field reconnect? CordAdvancedDiscordReconnectConfig Reconnection configuration
---@class CordAdvancedDiscordReconnectConfig
@@ -130,6 +132,7 @@ local defaults = {
display = {
theme = 'default',
flavor = 'dark',
+ view = 'full',
swap_fields = false,
swap_icons = false,
},
@@ -195,6 +198,7 @@ local defaults = {
timeout = 300000,
},
discord = {
+ pipe_paths = nil,
reconnect = {
enabled = false,
interval = 5000,
diff --git a/lua/cord/plugin/config/util.lua b/lua/cord/plugin/config/util.lua
index 786ad35e..f669b298 100644
--- a/lua/cord/plugin/config/util.lua
+++ b/lua/cord/plugin/config/util.lua
@@ -27,6 +27,11 @@ function M.validate(new_config)
logger.set_level(log_level)
icons.set(final_config.display.theme, final_config.display.flavor)
+ if not vim.tbl_contains({ 'auto', 'editor', 'asset', 'full' }, final_config.display.view) then
+ logger.log_raw(vim.log.levels.ERROR, 'View must be one of `auto`, `editor`, `asset`, or `full`')
+ return
+ end
+
if final_config.buttons then
if #final_config.buttons > 2 then
logger.log_raw(vim.log.levels.ERROR, 'There cannot be more than 2 buttons')
diff --git a/lua/cord/plugin/log/file.lua b/lua/cord/plugin/log/file.lua
index 6fa647c6..bc937ce0 100644
--- a/lua/cord/plugin/log/file.lua
+++ b/lua/cord/plugin/log/file.lua
@@ -2,11 +2,14 @@ local levels = vim.log.levels
local fs = require 'cord.core.uv.fs'
local Async = require 'cord.core.async'
+local log_level = levels.TRACE
local queue = {}
local queue_start, queue_end = 1, 0
local flushing = false
local fd
+local function set_level(level) log_level = level end
+
local function log_notify(msg, level)
if vim.in_fast_event and vim.in_fast_event() then
vim.schedule(function() vim.notify(msg, level) end)
@@ -67,7 +70,7 @@ end
local function format_message(entry, message)
local ts = os.date '%Y-%m-%d %H:%M:%S'
local level_name = level_names[entry.level] or tostring(entry.level)
- return string.format('[%s] [cord.nvim] [%s] %s', ts, level_name, tostring(message))
+ return string.format('[%s] [%s] %s', ts, level_name, tostring(message))
end
local function flush()
@@ -113,7 +116,7 @@ local function flush()
end
local function log(level, msg)
- if not level then return end
+ if not level or level < log_level then return end
Async.run(function()
enqueue(level, msg)
flush()
@@ -128,9 +131,6 @@ local function log_raw(level, msg)
end)
end
--- no-op
-local function set_level(_level) end
-
local function error(msg) log(levels.ERROR, msg) end
local function warn(msg) log(levels.WARN, msg) end
local function info(msg) log(levels.INFO, msg) end
diff --git a/lua/cord/server/event/sender.lua b/lua/cord/server/event/sender.lua
index 2257fc3a..ad655a8a 100644
--- a/lua/cord/server/event/sender.lua
+++ b/lua/cord/server/event/sender.lua
@@ -24,6 +24,11 @@ function Producer:initialize(config)
timestamp = {
shared = config.timestamp.shared,
},
+ advanced = {
+ discord = {
+ pipe_paths = config.advanced.discord.pipe_paths,
+ },
+ },
})
end
diff --git a/lua/cord/server/fs/init.lua b/lua/cord/server/fs/init.lua
index 4f40d04c..e5bc824f 100644
--- a/lua/cord/server/fs/init.lua
+++ b/lua/cord/server/fs/init.lua
@@ -2,7 +2,7 @@ local M = {}
function M.get_plugin_root()
local source = debug.getinfo(1, 'S').source:sub(2)
- return vim.fn.fnamemodify(source, ':h:h:h:h')
+ return vim.fn.fnamemodify(source, ':h:h:h:h:h')
end
function M.get_data_path() return vim.fn.stdpath 'data' .. '/cord' end
diff --git a/lua/cord/server/init.lua b/lua/cord/server/init.lua
index ae5e72d7..37ef997d 100644
--- a/lua/cord/server/init.lua
+++ b/lua/cord/server/init.lua
@@ -54,6 +54,8 @@ function M:run()
return async.wrap(function()
M.tx = require('cord.server.event.sender').new(M.client)
M.rx = require('cord.server.event.receiver').new(M.client)
+ logger.debug 'Server: sending initialize event'
+ M.tx:initialize(config.get())
logger.debug 'Server: registering ready handler'
M.rx:register(
'ready',
@@ -62,7 +64,6 @@ function M:run()
self.status = 'ready'
async.run(function()
logger.info 'Connected to Discord'
- M.tx:initialize(config.get())
local ActivityManager = require 'cord.plugin.activity.manager'
local manager, err = ActivityManager.new({ tx = M.tx }):get()
diff --git a/lua/cord/server/spawn/init.lua b/lua/cord/server/spawn/init.lua
index d7c719ce..abb31ceb 100644
--- a/lua/cord/server/spawn/init.lua
+++ b/lua/cord/server/spawn/init.lua
@@ -15,6 +15,8 @@ M.spawn = async.wrap(function(config, pipe_path)
logger.debug('Spawn: executable missing at ' .. tostring(exec_path))
if update_strategy == 'fetch' then
require('cord.server.update').fetch():await()
+ elseif update_strategy == 'install' then
+ require('cord.server.update').install():await()
elseif update_strategy == 'build' then
require('cord.server.update').build():await()
else
diff --git a/lua/cord/server/update/init.lua b/lua/cord/server/update/init.lua
index c318ebbc..edd75482 100644
--- a/lua/cord/server/update/init.lua
+++ b/lua/cord/server/update/init.lua
@@ -3,7 +3,7 @@ local logger = require 'cord.plugin.log'
local M = {}
-M.build = async.wrap(function()
+M.install = async.wrap(function()
local server = require 'cord.server'
if server.is_updating then return end
server.is_updating = true
@@ -69,6 +69,73 @@ M.build = async.wrap(function()
end)
end)
+M.build = async.wrap(function()
+ local server = require 'cord.server'
+ if server.is_updating then return end
+ server.is_updating = true
+
+ if not vim.fn.executable 'cargo' then
+ error 'cargo is not installed or not in PATH'
+ return
+ end
+
+ logger.info 'Building executable locally...'
+
+ vim.schedule(function()
+ local cord = require 'cord.server'
+ local function initialize()
+ local process = require 'cord.core.uv.process'
+
+ async.run(function()
+ process
+ .spawn({
+ cmd = 'cargo',
+ args = {
+ 'install',
+ '--path',
+ require('cord.server.fs').get_plugin_root(),
+ '--force',
+ '--root',
+ require('cord.server.fs').get_data_path(),
+ },
+ })
+ :and_then(function(res)
+ if res.code ~= 0 then
+ server.is_updating = false
+ logger.error 'Failed to build executable'
+ if res.stderr then logger.error('cargo\'s stderr: ' .. res.stderr) end
+ return
+ end
+ logger.log_raw(vim.log.levels.INFO, 'Successfully built executable. Restarting...')
+
+ async.run(function()
+ server.is_updating = false
+ cord:initialize()
+ end)
+ end, function(err)
+ server.is_updating = false
+
+ logger.error(err)
+ end)
+ end)
+ end
+
+ if cord.manager then cord.manager:cleanup() end
+ if not cord.tx then return initialize() end
+ if not cord.client then return initialize() end
+ if cord.client:is_closing() then return initialize() end
+
+ if cord.client.on_close then cord.client.on_close() end
+
+ cord.client.on_close = function()
+ cord.client.on_close = nil
+ initialize()
+ end
+
+ cord.tx:shutdown()
+ end)
+end)
+
local function get_local_version()
local process = require 'cord.core.uv.process'
local executable_path =
diff --git a/src/cord.rs b/src/cord.rs
index dfad139a..5c620870 100644
--- a/src/cord.rs
+++ b/src/cord.rs
@@ -2,7 +2,6 @@ use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, RwLock};
use std::time::Duration;
-use crate::error::CordErrorKind;
use crate::ipc::discord::client::{Connection, RichClient};
use crate::ipc::pipe::PipeServerImpl;
use crate::ipc::pipe::platform::server::PipeServer;
@@ -12,7 +11,7 @@ use crate::messages::message::Message;
use crate::protocol::msgpack::MsgPack;
use crate::session::SessionManager;
use crate::util::lockfile::ServerLock;
-use crate::util::logger::{LogLevel, Logger};
+use crate::util::logger::{self, LogLevel, Logger};
pub const VERSION: &str = include_str!("../.github/server-version.txt");
@@ -33,7 +32,6 @@ pub struct Cord {
pub pipe: PipeServer,
pub tx: Sender,
pub rx: Receiver,
- pub logger: Arc,
_lock: ServerLock,
}
@@ -44,36 +42,14 @@ impl Cord {
let (tx, rx) = mpsc::channel::();
let session_manager = Arc::new(SessionManager::default());
- let logger = Arc::new(Logger::new(tx.clone(), LogLevel::Off));
+ let _ = logger::INSTANCE
+ .set(RwLock::new(Logger::new(tx.clone(), LogLevel::Off)));
let rich_client =
- Arc::new(RwLock::new(RichClient::new(config.client_id)));
- {
- let mut client = rich_client.write().unwrap();
- if client.connect().is_err() {
- if config.reconnect_interval > 0 && config.initial_reconnect {
- drop(client);
- let client_clone = rich_client.clone();
- let tx = tx.clone();
-
- std::thread::spawn(move || {
- let mut client = client_clone.write().unwrap();
- client.reconnect(config.reconnect_interval, tx.clone());
- });
- } else {
- return Err(crate::error::CordError::new(
- CordErrorKind::Io,
- "Failed to connect to Discord",
- ));
- }
- } else {
- client.handshake()?;
- client.start_read_thread(tx.clone())?;
- }
- }
+ Arc::new(RwLock::new(RichClient::new(config.client_id, vec![])));
let server = PipeServer::new(
- &config.pipe_name,
+ &config.server_pipe,
tx.clone(),
Arc::clone(&session_manager),
);
@@ -85,7 +61,6 @@ impl Cord {
pipe: server,
tx,
rx,
- logger,
_lock: lock,
})
}
@@ -152,7 +127,7 @@ impl Cord {
/// Manages application settings required for initialization.
pub struct Config {
- pub pipe_name: String,
+ pub server_pipe: String,
pub client_id: u64,
pub timeout: u64,
pub reconnect_interval: u64,
@@ -163,7 +138,7 @@ pub struct Config {
impl Config {
/// Creates a new configuration.
pub fn new(
- pipe_name: String,
+ server_pipe: String,
client_id: u64,
timeout: u64,
reconnect_interval: u64,
@@ -171,7 +146,7 @@ impl Config {
shared_timestamps: bool,
) -> Self {
Self {
- pipe_name,
+ server_pipe,
client_id,
timeout,
reconnect_interval,
diff --git a/src/ipc/discord/client.rs b/src/ipc/discord/client.rs
index 58c569c2..f44461a5 100644
--- a/src/ipc/discord/client.rs
+++ b/src/ipc/discord/client.rs
@@ -1,9 +1,10 @@
use std::sync::Arc;
-use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::atomic::AtomicBool;
use std::sync::mpsc::Sender;
use std::thread::JoinHandle;
use std::time::Duration;
+use crate::ipc::discord::error::DiscordError;
use crate::messages::message::Message;
use crate::presence::packet::Packet;
use crate::protocol::json::Json;
@@ -17,6 +18,7 @@ use crate::protocol::json::Json;
/// * `is_ready`: Indicates if the client is ready.
pub struct RichClient {
pub client_id: u64,
+ pub pipe_paths: Vec,
#[cfg(target_os = "windows")]
pub pipe: Option>,
#[cfg(not(target_os = "windows"))]
@@ -26,25 +28,26 @@ pub struct RichClient {
pub pid: u32,
pub is_ready: Arc,
pub thread_handle: Option>,
- pub is_reconnecting: Arc,
+ pub is_reconnecting: bool,
}
/// Defines methods for connecting and closing the client.
pub trait Connection {
- /// Connects to Discord using the given client ID.
- fn connect(&mut self) -> crate::Result<()>;
- /// Closes the connection to Discord.
+ /// Connects to the given pipe.
+ fn try_connect(&mut self, pipe: &str) -> crate::Result;
+ /// Closes the connection to the pipe.
fn close(&mut self);
- /// Start reading from Discord in a separate thread
+ /// Start reading from the pipe in a separate thread.
fn start_read_thread(&mut self, tx: Sender) -> crate::Result<()>;
- /// Write data to Discord
+ /// Write data to the pipe.
fn write(&self, opcode: u32, data: Option<&[u8]>) -> crate::Result<()>;
}
impl RichClient {
- pub fn new(client_id: u64) -> Self {
+ pub fn new(client_id: u64, pipe_paths: Vec) -> Self {
Self {
client_id,
+ pipe_paths,
#[cfg(target_os = "windows")]
pipe: None,
#[cfg(not(target_os = "windows"))]
@@ -54,11 +57,35 @@ impl RichClient {
pid: std::process::id(),
is_ready: Arc::new(AtomicBool::new(false)),
thread_handle: None,
- is_reconnecting: Arc::new(AtomicBool::new(false)),
+ is_reconnecting: false,
}
}
/// Establishes a connection with Discord.
+ pub fn connect(&mut self) -> crate::Result<()> {
+ if self.pipe_paths.is_empty() {
+ for pipe in get_dirs() {
+ if self.try_connect(&pipe)? {
+ return Ok(());
+ }
+ }
+ } else {
+ let pipes = std::mem::take(&mut self.pipe_paths);
+
+ for pipe in &pipes {
+ if self.try_connect(pipe)? {
+ self.pipe_paths = pipes;
+ return Ok(());
+ }
+ }
+
+ self.pipe_paths = pipes;
+ }
+
+ Err(DiscordError::PipeNotFound.into())
+ }
+
+ /// Sends a handshake packet to Discord.
pub fn handshake(&self) -> crate::Result<()> {
self.write(
0,
@@ -91,19 +118,17 @@ impl RichClient {
/// Reconnects to Discord with exponential backoff.
pub fn reconnect(&mut self, interval: u64, tx: Sender) {
- self.is_reconnecting.store(true, Ordering::SeqCst);
+ self.is_reconnecting = true;
self.close();
std::thread::sleep(Duration::from_millis(500));
- let mut client = Self::new(self.client_id);
- while self.is_reconnecting.load(Ordering::SeqCst) {
+ let mut client = Self::new(self.client_id, self.pipe_paths.clone());
+ while self.is_reconnecting {
if client.connect().is_ok() {
if client.handshake().is_ok() {
*self = client;
- if self.start_read_thread(tx).is_err() {
- self.is_reconnecting.store(false, Ordering::SeqCst);
- };
+ let _ = self.start_read_thread(tx);
break;
} else {
@@ -114,6 +139,22 @@ impl RichClient {
std::thread::sleep(Duration::from_millis(interval));
}
- self.is_reconnecting.store(false, Ordering::SeqCst);
+ self.is_reconnecting = false;
}
}
+
+#[cfg(target_os = "windows")]
+fn get_dirs() -> impl Iterator- {
+ (0..=10).map(|i| format!(r"\\.\pipe\discord-ipc-{}", i))
+}
+
+#[cfg(not(target_os = "windows"))]
+fn get_dirs() -> impl Iterator
- {
+ ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"]
+ .iter()
+ .filter_map(|&dir| std::env::var(dir).ok())
+ .chain(["/tmp".to_string()])
+ .flat_map(|base| {
+ (0..10).map(move |i| format!("{}/discord-ipc-{}", base, i))
+ })
+}
diff --git a/src/ipc/discord/platform/unix.rs b/src/ipc/discord/platform/unix.rs
index aeaff327..96fe7d42 100644
--- a/src/ipc/discord/platform/unix.rs
+++ b/src/ipc/discord/platform/unix.rs
@@ -1,4 +1,3 @@
-use std::env::var;
use std::io::{self, Read, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
@@ -11,10 +10,10 @@ use crate::ipc::discord::opcodes::Opcode;
use crate::ipc::discord::utils;
use crate::messages::events::local::ErrorEvent;
use crate::messages::message::Message;
-use crate::{local_event, server_event};
+use crate::{local_event, server_event, trace};
impl Connection for RichClient {
- /// Pipe path can be in any of the following directories:
+ /// Pipe can be in any of the following directories:
/// * `XDG_RUNTIME_DIR`
/// * `TMPDIR`
/// * `TMP`
@@ -27,38 +26,21 @@ impl Connection for RichClient {
///
/// Followed by:
/// * `/discord-ipc-{i}` - where `i` is a number from 0 to 9
- fn connect(&mut self) -> crate::Result<()> {
- let dirs = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"]
- .iter()
- .filter_map(|&dir| var(dir).ok())
- .chain(["/tmp".to_string()])
- .flat_map(|base| {
- [
- base.to_string(),
- format!("{}/app/com.discordapp.Discord", base),
- format!("{}/snap.discord", base),
- ]
- });
-
- for dir in dirs {
- for i in 0..10 {
- match UnixStream::connect(format!("{dir}/discord-ipc-{i}")) {
- Ok(pipe) => {
- let read_pipe =
- pipe.try_clone().map_err(DiscordError::Io)?;
- self.read_pipe = Some(read_pipe);
- self.write_pipe = Some(pipe);
- return Ok(());
- }
- Err(e) => match e.kind() {
- io::ErrorKind::NotFound => continue,
- _ => return Err(DiscordError::Io(e).into()),
- },
- }
+ fn try_connect(&mut self, pipe: &str) -> crate::Result {
+ match UnixStream::connect(pipe) {
+ Ok(pipe) => {
+ let read_pipe = pipe.try_clone().map_err(DiscordError::Io)?;
+ self.read_pipe = Some(read_pipe);
+ self.write_pipe = Some(pipe);
+ return Ok(true);
+ }
+ Err(e) => {
+ return match e.kind() {
+ io::ErrorKind::NotFound => Ok(false),
+ _ => Err(DiscordError::Io(e).into()),
+ };
}
}
-
- Err(DiscordError::PipeNotFound.into())
}
fn close(&mut self) {
@@ -103,6 +85,10 @@ impl Connection for RichClient {
let data = &buf[8..8 + size as usize];
let data_str =
String::from_utf8_lossy(data);
+ trace!(
+ "Received message from Discord: opcode={}, data={}",
+ opcode, data_str
+ );
match Opcode::from(opcode) {
Opcode::Frame => {
diff --git a/src/ipc/discord/platform/windows.rs b/src/ipc/discord/platform/windows.rs
index f5f3ebe7..7313ca8b 100644
--- a/src/ipc/discord/platform/windows.rs
+++ b/src/ipc/discord/platform/windows.rs
@@ -17,45 +17,37 @@ use crate::ipc::discord::opcodes::Opcode;
use crate::ipc::discord::utils;
use crate::messages::events::local::ErrorEvent;
use crate::messages::message::Message;
-use crate::{local_event, server_event};
+use crate::{local_event, server_event, trace};
impl Connection for RichClient {
- /// Pipe path can be under the directory `\\\\.\\pipe\\discord-ipc-{i}` where `i` is a number from 0 to 9.
- fn connect(&mut self) -> crate::Result<()> {
- for i in 0..10 {
- let pipe_name = format!("\\\\.\\pipe\\discord-ipc-{i}");
- let wide_name: Vec = OsStr::new(&pipe_name)
- .encode_wide()
- .chain(Some(0))
- .collect();
-
- unsafe {
- let handle = CreateFileW(
- wide_name.as_ptr(),
- GENERIC_READ | GENERIC_WRITE,
- 0,
- ptr::null_mut(),
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- 0 as _,
- );
-
- if handle == INVALID_HANDLE_VALUE {
- let error = io::Error::last_os_error();
- match error.kind() {
- io::ErrorKind::NotFound => continue,
- _ => return Err(DiscordError::Io(error).into()),
- }
- }
-
- let pipe = File::from_raw_handle(handle);
- self.pipe = Some(pipe.into());
-
- return Ok(());
+ /// Pipe can be under the path `\\\\.\\pipe\\discord-ipc-{i}` where `i` is a number from 0 to 9.
+ fn try_connect(&mut self, pipe_name: &str) -> crate::Result {
+ let wide_name: Vec =
+ OsStr::new(pipe_name).encode_wide().chain(Some(0)).collect();
+
+ unsafe {
+ let handle = CreateFileW(
+ wide_name.as_ptr(),
+ GENERIC_READ | GENERIC_WRITE,
+ 0,
+ ptr::null_mut(),
+ OPEN_EXISTING,
+ FILE_FLAG_OVERLAPPED,
+ 0 as _,
+ );
+
+ if handle == INVALID_HANDLE_VALUE {
+ let error = io::Error::last_os_error();
+ return match error.kind() {
+ io::ErrorKind::NotFound => Ok(false),
+ _ => Err(DiscordError::Io(error).into()),
+ };
}
- }
- Err(DiscordError::PipeNotFound.into())
+ let pipe = File::from_raw_handle(handle);
+ self.pipe = Some(pipe.into());
+ Ok(true)
+ }
}
fn close(&mut self) {
@@ -142,6 +134,10 @@ impl Connection for RichClient {
{
let data = &buf[8..8 + size as usize];
let data_str = String::from_utf8_lossy(data);
+ trace!(
+ "Received message from Discord: opcode={}, data={}",
+ opcode, data_str
+ );
match Opcode::from(opcode) {
Opcode::Frame => {
diff --git a/src/messages/events/client/connect.rs b/src/messages/events/client/connect.rs
index 1ba1da91..90a20c28 100644
--- a/src/messages/events/client/connect.rs
+++ b/src/messages/events/client/connect.rs
@@ -10,14 +10,15 @@ use crate::protocol::msgpack::MsgPack;
impl OnEvent for ConnectEvent {
fn on_event(self, ctx: &mut EventContext) -> crate::Result<()> {
- if ctx
+ let is_ready = ctx
.cord
.rich_client
.read()
.unwrap()
.is_ready
- .load(Ordering::SeqCst)
- {
+ .load(Ordering::SeqCst);
+
+ if is_ready {
ctx.cord
.pipe
.write_to(ctx.client_id, &MsgPack::serialize(&ReadyEvent)?)?;
diff --git a/src/messages/events/client/initialize.rs b/src/messages/events/client/initialize.rs
index cdbd8b19..83279b58 100644
--- a/src/messages/events/client/initialize.rs
+++ b/src/messages/events/client/initialize.rs
@@ -1,9 +1,10 @@
-use std::sync::Arc;
use std::sync::atomic::Ordering;
+use crate::error::CordErrorKind;
+use crate::ipc::discord::client::Connection;
use crate::messages::events::event::{EventContext, OnEvent};
use crate::types::config::PluginConfig;
-use crate::util::now;
+use crate::util::{logger, now};
#[derive(Debug)]
pub struct InitializeEvent {
@@ -18,8 +19,8 @@ impl InitializeEvent {
impl OnEvent for InitializeEvent {
fn on_event(self, ctx: &mut EventContext) -> crate::Result<()> {
- if let Some(logger) = Arc::get_mut(&mut ctx.cord.logger) {
- logger.set_level(self.config.log_level);
+ if let Some(logger) = logger::INSTANCE.get() {
+ logger.write().unwrap().set_level(self.config.log_level);
}
ctx.cord.config.shared_timestamps = self.config.timestamp.shared;
@@ -32,6 +33,39 @@ impl OnEvent for InitializeEvent {
);
}
+ let rich_client = &ctx.cord.rich_client;
+ let mut client = rich_client.write().unwrap();
+ if !self.config.advanced.discord.pipe_paths.is_empty()
+ && client.pipe_paths.is_empty()
+ {
+ client.pipe_paths = self.config.advanced.discord.pipe_paths.clone();
+ }
+
+ let config = &ctx.cord.config;
+ if !client.is_ready.load(Ordering::SeqCst) {
+ if client.connect().is_err() {
+ if config.reconnect_interval > 0 && config.initial_reconnect {
+ drop(client);
+ let client_clone = rich_client.clone();
+ let tx = ctx.cord.tx.clone();
+
+ let reconnect_interval = config.reconnect_interval;
+ std::thread::spawn(move || {
+ let mut client = client_clone.write().unwrap();
+ client.reconnect(reconnect_interval, tx.clone());
+ });
+ } else {
+ return Err(crate::error::CordError::new(
+ CordErrorKind::Io,
+ "Failed to connect to Discord",
+ ));
+ }
+ } else {
+ client.handshake()?;
+ client.start_read_thread(ctx.cord.tx.clone())?;
+ }
+ }
+
if let Some(mut session) =
ctx.cord.session_manager.get_session_mut(ctx.client_id)
{
diff --git a/src/messages/events/local/error.rs b/src/messages/events/local/error.rs
index a543d666..7721ea32 100644
--- a/src/messages/events/local/error.rs
+++ b/src/messages/events/local/error.rs
@@ -3,7 +3,7 @@ use crate::ipc::pipe::PipeServerImpl;
use crate::messages::events::event::{EventContext, OnEvent};
use crate::messages::events::server::DisconnectEvent;
use crate::protocol::msgpack::MsgPack;
-use crate::util::logger::LogLevel;
+use crate::{debug, error};
type Error = Box;
@@ -46,22 +46,14 @@ impl OnEvent for ErrorEvent {
.reconnect(reconnect_interval, tx);
});
- ctx.cord.logger.log(
- LogLevel::Debug,
- "Discord closed the connection".into(),
- 0,
- );
+ debug!("Discord closed the connection");
return Ok(());
}
_ => (),
}
}
- ctx.cord.logger.log(
- LogLevel::Error,
- self.error.to_string().into(),
- ctx.client_id,
- );
+ error!(ctx.client_id, "{}", self.error);
Ok(())
}
diff --git a/src/types/config.rs b/src/types/config.rs
index 7d17d30e..119d9ac7 100644
--- a/src/types/config.rs
+++ b/src/types/config.rs
@@ -2,16 +2,17 @@
use crate::protocol::msgpack::Value;
use crate::protocol::msgpack::deserialize::Deserialize;
-use crate::remove_field;
use crate::util::logger::LogLevel;
+use crate::{remove_field, remove_field_or_none};
#[derive(Debug, Clone)]
pub struct PluginConfig {
pub log_level: LogLevel,
pub timestamp: TimestampConfig,
+ pub advanced: AdvancedConfig,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
pub struct TimestampConfig {
pub shared: bool,
}
@@ -26,6 +27,49 @@ impl Deserialize for TimestampConfig {
}
}
+#[derive(Debug, Clone, Default)]
+pub struct AdvancedConfig {
+ pub discord: AdvancedDiscordConfig,
+}
+
+impl Deserialize for AdvancedConfig {
+ fn deserialize<'a>(input: Value) -> crate::Result {
+ let mut input = input.take_map().ok_or("Invalid config")?;
+
+ let discord = remove_field!(input, "discord", |v| {
+ AdvancedDiscordConfig::deserialize(v).ok()
+ });
+
+ Ok(AdvancedConfig { discord })
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AdvancedDiscordConfig {
+ pub pipe_paths: Vec,
+}
+
+impl Deserialize for AdvancedDiscordConfig {
+ fn deserialize<'a>(input: Value) -> crate::Result {
+ let mut input = input.take_map().ok_or("Invalid config")?;
+
+ let pipe_paths = remove_field_or_none!(input, "pipe_paths", |v| {
+ v.take_array().and_then(|arr| {
+ arr.into_iter()
+ .map(|v| {
+ v.take_string()
+ .ok_or("Invalid discord pipe path: not a string")
+ })
+ .collect::, _>>()
+ .ok()
+ })
+ })
+ .unwrap_or_default();
+
+ Ok(AdvancedDiscordConfig { pipe_paths })
+ }
+}
+
impl Deserialize for PluginConfig {
fn deserialize<'a>(input: Value) -> crate::Result {
let mut input = input.take_map().ok_or("Invalid config")?;
@@ -33,13 +77,19 @@ impl Deserialize for PluginConfig {
let log_level = remove_field!(input, "log_level", |v| v.as_uinteger())
.try_into()
.map_err(|_| "Invalid log level")?;
- let timestamp = remove_field!(input, "timestamp", |v| {
+ let timestamp = remove_field_or_none!(input, "timestamp", |v| {
TimestampConfig::deserialize(v).ok()
- });
+ })
+ .unwrap_or_default();
+ let advanced = remove_field_or_none!(input, "advanced", |v| {
+ AdvancedConfig::deserialize(v).ok()
+ })
+ .unwrap_or_default();
Ok(PluginConfig {
log_level,
timestamp,
+ advanced,
})
}
}
diff --git a/src/util/logger.rs b/src/util/logger.rs
index 4d39ce12..3bac969a 100644
--- a/src/util/logger.rs
+++ b/src/util/logger.rs
@@ -1,10 +1,13 @@
-use std::borrow::Cow;
+#![allow(unused)]
use std::sync::mpsc::Sender;
+use std::sync::{OnceLock, RwLock};
use crate::messages::events::server::LogEvent;
use crate::messages::message::Message;
use crate::server_event;
+pub static INSTANCE: OnceLock> = OnceLock::new();
+
pub struct Logger {
tx: Sender,
level: LogLevel,
@@ -26,24 +29,247 @@ impl Logger {
Logger { tx, level }
}
- pub fn log(&self, level: LogLevel, message: Cow, client_id: u32) {
+ pub fn set_level(&mut self, level: LogLevel) {
+ self.level = level;
+ }
+
+ #[inline(always)]
+ pub fn log(
+ &self,
+ level: LogLevel,
+ message: impl Into,
+ client_id: u32,
+ ) {
if level >= self.level && level != LogLevel::Off {
self.tx
.send(server_event!(
client_id,
Log,
- LogEvent::new(message.into_owned(), level)
+ LogEvent::new(message.into(), level)
))
.ok();
}
}
- #[inline]
- pub fn set_level(&mut self, level: LogLevel) {
- self.level = level;
+ #[inline(always)]
+ pub fn log_cb(
+ &self,
+ level: LogLevel,
+ client_id: u32,
+ cb: impl FnOnce() -> String,
+ ) {
+ if level >= self.level && level != LogLevel::Off {
+ self.tx
+ .send(server_event!(client_id, Log, LogEvent::new(cb(), level)))
+ .ok();
+ }
+ }
+
+ #[inline(always)]
+ pub fn log_raw(
+ &self,
+ level: LogLevel,
+ message: impl Into,
+ client_id: u32,
+ ) {
+ self.tx
+ .send(server_event!(
+ client_id,
+ Log,
+ LogEvent::new(message.into(), level)
+ ))
+ .ok();
+ }
+
+ #[inline(always)]
+ pub fn log_raw_cb(
+ &self,
+ level: LogLevel,
+ client_id: u32,
+ cb: impl FnOnce() -> String,
+ ) {
+ self.tx
+ .send(server_event!(client_id, Log, LogEvent::new(cb(), level)))
+ .ok();
}
}
+#[macro_export]
+macro_rules! log {
+ ($level:expr, $msg:expr, $client_id:expr) => {{
+ let logger = $crate::util::logger::INSTANCE
+ .get()
+ .expect("Logger not initialized")
+ .read()
+ .unwrap();
+
+ logger.log($level, $msg, $client_id);
+ }};
+ ($level:expr, $msg:expr) => {
+ $crate::log!($level, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! log_raw {
+ ($level:expr, $msg:expr, $client_id:expr) => {{
+ let logger = $crate::util::logger::INSTANCE
+ .get()
+ .expect("Logger not initialized")
+ .read()
+ .unwrap();
+
+ logger.log_raw($level, $msg, $client_id);
+ }};
+ ($level:expr, $msg:expr) => {
+ $crate::log_raw!($level, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! log_cb {
+ ($level:expr, $cb:expr, $client_id:expr) => {{
+ let logger = $crate::util::logger::INSTANCE
+ .get()
+ .expect("Logger not initialized")
+ .read()
+ .unwrap();
+
+ logger.log_cb($level, $client_id, $cb);
+ }};
+ ($level:expr, $cb:expr) => {
+ $crate::log_cb!($level, $cb, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! log_raw_cb {
+ ($level:expr, $cb:expr, $client_id:expr) => {{
+ let logger = $crate::util::logger::INSTANCE
+ .get()
+ .expect("Logger not initialized")
+ .read()
+ .unwrap();
+
+ logger.log_raw_cb($level, $client_id, $cb);
+ }};
+ ($level:expr, $cb:expr) => {
+ $crate::log_raw_cb!($level, $cb, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! trace {
+ // Pattern: trace!(client_id, "format", args...)
+ ($client_id:expr, $fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Trace, $client_id, format!($fmt, $($args),*))
+ };
+ // Pattern: trace!("format", args...)
+ ($fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Trace, 0, format!($fmt, $($args),*))
+ };
+ // Pattern: trace!(client_id, msg)
+ ($client_id:literal, $msg:expr) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Trace, $client_id, $msg)
+ };
+ // Pattern: trace!(msg)
+ ($msg:expr) => {
+ $crate::log!($crate::util::logger::LogLevel::Trace, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! debug {
+ // Pattern: debug!(client_id, "format", args...)
+ ($client_id:expr, $fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Debug, $client_id, format!($fmt, $($args),*))
+ };
+ // Pattern: debug!("format", args...)
+ ($fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Debug, 0, format!($fmt, $($args),*))
+ };
+ // Pattern: debug!(client_id, msg)
+ ($client_id:literal, $msg:expr) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Debug, $client_id, $msg)
+ };
+ // Pattern: debug!(msg)
+ ($msg:expr) => {
+ $crate::log!($crate::util::logger::LogLevel::Debug, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! info {
+ // Pattern: info!(client_id, "format", args...)
+ ($client_id:expr, $fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Info, $client_id, format!($fmt, $($args),*))
+ };
+ // Pattern: info!("format", args...)
+ ($fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Info, 0, format!($fmt, $($args),*))
+ };
+ // Pattern: info!(client_id, msg)
+ ($client_id:literal, $msg:expr) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Info, $client_id, $msg)
+ };
+ // Pattern: info!(msg)
+ ($msg:expr) => {
+ $crate::log!($crate::util::logger::LogLevel::Info, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! warn {
+ // Pattern: warn!(client_id, "format", args...)
+ ($client_id:expr, $fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Warn, $client_id, format!($fmt, $($args),*))
+ };
+ // Pattern: warn!("format", args...)
+ ($fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Warn, 0, format!($fmt, $($args),*))
+ };
+ // Pattern: warn!(client_id, msg)
+ ($client_id:literal, $msg:expr) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Warn, $client_id, $msg)
+ };
+ // Pattern: warn!(msg)
+ ($msg:expr) => {
+ $crate::log!($crate::util::logger::LogLevel::Warn, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! error {
+ // Pattern: error!(client_id, "format", args...)
+ ($client_id:expr, $fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Error, $client_id, format!($fmt, $($args),*))
+ };
+ // Pattern: error!("format", args...)
+ ($fmt:literal, $($args:expr),* $(,)?) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Error, 0, format!($fmt, $($args),*))
+ };
+ // Pattern: error!(client_id, msg)
+ ($client_id:literal, $msg:expr) => {
+ $crate::__log_with_client_id!($crate::util::logger::LogLevel::Error, $client_id, $msg)
+ };
+ // Pattern: error!(msg)
+ ($msg:expr) => {
+ $crate::log!($crate::util::logger::LogLevel::Error, $msg, 0)
+ };
+}
+
+#[macro_export]
+macro_rules! __log_with_client_id {
+ ($level:expr, $client_id:expr, $msg:expr) => {{
+ let logger = $crate::util::logger::INSTANCE
+ .get()
+ .expect("Logger not initialized")
+ .read()
+ .unwrap();
+ logger.log($level, $msg, $client_id);
+ }};
+}
+
impl From for LogLevel {
fn from(value: u8) -> Self {
match value {
diff --git a/src/util/macros.rs b/src/util/macros.rs
index 62e94984..37d07abe 100644
--- a/src/util/macros.rs
+++ b/src/util/macros.rs
@@ -70,7 +70,7 @@ macro_rules! echoln {
/// Prints a message to stderr without panicking.
#[macro_export]
-macro_rules! error {
+macro_rules! errorln {
($($arg:tt)*) => {{
use std::io::{self, Write};
let stderr = io::stderr();