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();