diff --git a/lua/auto-dark-mode/health.lua b/lua/auto-dark-mode/health.lua index b42d832..565c720 100644 --- a/lua/auto-dark-mode/health.lua +++ b/lua/auto-dark-mode/health.lua @@ -1,14 +1,20 @@ local M = {} +local uv = vim.uv or vim.loop + local adm = require("auto-dark-mode") +local interval = require("auto-dark-mode.interval") M.benchmark = function(iterations) local results = {} for _ = 1, iterations do - local _start = vim.uv.hrtime() - vim.system(adm.state.query_command, { text = true }):wait() - local _end = vim.uv.hrtime() + local _start = uv.hrtime() + -- by using an empty function, parsing the response is measured, but + -- actually syncing the vim theme isn't performed + interval.poll_dark_mode(function() end) + local _end = uv.hrtime() + table.insert(results, (_end - _start) / 1000000) end @@ -24,37 +30,45 @@ M.benchmark = function(iterations) return { avg = sum / #results, max = max, min = min } end +-- support for neovim < 0.9.0 +local H = vim.health +local health = {} +health.start = H.start or H.report_start +health.ok = H.ok or H.report_ok +health.info = H.info or H.report_info +health.error = H.error or H.report_error + M.check = function() - vim.health.start("auto-dark-mode.nvim") + health.start("auto-dark-mode.nvim") if adm.state.setup_correct then - vim.health.ok("Setup is correct") + health.ok("Setup is correct") else - vim.health.error("Setup is incorrect") + health.error("Setup is incorrect") end - vim.health.info("Detected operating system: " .. adm.state.system) - vim.health.info("Using query command: `" .. table.concat(adm.state.query_command, " ") .. "`") + health.info(string.format("Detected operating system: %s", adm.state.system)) + health.info(string.format("Using query command: `%s`", table.concat(adm.state.query_command, " "))) local benchmark = M.benchmark(30) - vim.health.info( + health.info( string.format("Benchmark: %.2fms avg / %.2fms min / %.2fms max", benchmark.avg, benchmark.min, benchmark.max) ) - local interval = adm.options.update_interval - local ratio = interval / benchmark.avg - local info = string.format("Update interval (%dms) is %.2fx the average query time", interval, ratio) + local update_interval = adm.options.update_interval + local ratio = update_interval / benchmark.avg + local info = string.format("Update interval (%dms) is %.2fx the average query time", update_interval, ratio) local error = string.format( "Update interval (%dms) seems too short compared to current benchmarks, consider increasing it", - interval + update_interval ) if ratio > 30 then - vim.health.ok(info) + health.ok(info) elseif ratio > 5 then - vim.health.warn(info) + health.warn(info) else - vim.health.error(error) + health.error(error) end end diff --git a/lua/auto-dark-mode/init.lua b/lua/auto-dark-mode/init.lua index 607bae4..eb564d2 100644 --- a/lua/auto-dark-mode/init.lua +++ b/lua/auto-dark-mode/init.lua @@ -1,10 +1,17 @@ local M = {} +local uv = vim.uv or vim.loop + ---@alias Appearance "light" | "dark" ---@alias DetectedOS "Linux" | "Darwin" | "Windows_NT" | "WSL" ---@class AutoDarkModeOptions local default_options = { + -- Optional. Fallback theme to use if the system theme can't be detected. + -- Useful for linux and environments without a desktop manager. + ---@type Appearance? + fallback = "dark", + -- Optional. If not provided, `vim.api.nvim_set_option_value('background', 'dark', {})` will be used. ---@type fun(): nil | nil set_dark_mode = function() @@ -17,21 +24,13 @@ local default_options = { vim.api.nvim_set_option_value("background", "light", {}) end, - -- Every `update_interval` milliseconds a theme check will be performed. + -- Optional. Specifies the `update_interval` milliseconds a theme check will be performed. ---@type number? update_interval = 3000, - - -- Optional. Fallback theme to use if the system theme can't be detected. - -- Useful for linux and environments without a desktop manager. - ---@type Appearance - fallback = "dark", } local function validate_options(options) vim.validate({ - set_dark_mode = { options.set_dark_mode, "function" }, - set_light_mode = { options.set_light_mode, "function" }, - update_interval = { options.update_interval, "number" }, fallback = { options.fallback, function(opt) @@ -39,6 +38,9 @@ local function validate_options(options) end, "`fallback` must be either 'light' or 'dark'", }, + set_dark_mode = { options.set_dark_mode, "function" }, + set_light_mode = { options.set_light_mode, "function" }, + update_interval = { options.update_interval, "number" }, }) M.state.setup_correct = true end @@ -53,21 +55,11 @@ M.state = { query_command = {}, } --- map the vim.loop functions to vim.uv if available -local getuid = vim.uv.getuid or vim.loop.getuid - M.init = function() - local os_uname = vim.uv.os_uname() or vim.loop.os_uname() + local os_uname = uv.os_uname() if string.match(os_uname.release, "WSL") then M.state.system = "WSL" - if not vim.fn.executable("reg.exe") then - error([[ - auto-dark-mode.nvim: - `reg.exe` is not available. To support syncing with the host system, - this plugin relies on `reg.exe` being on the `$PATH`. - ]]) - end else M.state.system = os_uname.sysname end @@ -77,9 +69,9 @@ M.init = function() elseif M.state.system == "Linux" then if not vim.fn.executable("dbus-send") then error([[ - auto-dark-mode.nvim: - `dbus-send` is not available. The Linux implementation of - auto-dark-mode.nvim relies on `dbus-send` being on the `$PATH`. + auto-dark-mode.nvim: + `dbus-send` is not available. The Linux implementation of + auto-dark-mode.nvim relies on `dbus-send` being on the `$PATH`. ]]) end @@ -95,8 +87,28 @@ M.init = function() "string:color-scheme", } elseif M.state.system == "Windows_NT" or M.state.system == "WSL" then + local reg = "reg.exe" + + -- on WSL, if `reg.exe` cannot be found on the `$PATH` + -- (see interop.appendWindowsPath https://learn.microsoft.com/en-us/windows/wsl/wsl-config), + -- assume that it's in the default location + if M.state.system == "WSL" and not vim.fn.executable("reg.exe") then + local assumed_path = "/mnt/c/Windows/system32/reg.exe" + + if vim.fn.filereadable(assumed_path) then + reg = assumed_path + else + -- `reg.exe` isn't on `$PATH` or in the default location, so throw an error + error([[ + auto-dark-mode.nvim: + `reg.exe` is not available. To support syncing with the host system, + this plugin relies on `reg.exe` being on the `$PATH`. + ]]) + end + end + M.state.query_command = { - "reg.exe", + reg, "Query", "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "/v", @@ -107,7 +119,7 @@ M.init = function() end -- when on a supported unix system, and the userid is root - if (M.state.system == "Darwin" or M.state.system == "Linux") and getuid() == 0 then + if (M.state.system == "Darwin" or M.state.system == "Linux") and uv.getuid() == 0 then local sudo_user = vim.env.SUDO_USER if sudo_user ~= nil then @@ -122,7 +134,7 @@ M.init = function() auto-dark-mode.nvim: Running as `root`, but `$SUDO_USER` is not set. Please open an issue to add support for your system. - ]]) + ]]) end end diff --git a/lua/auto-dark-mode/interval.lua b/lua/auto-dark-mode/interval.lua index e1952a6..797bf4f 100644 --- a/lua/auto-dark-mode/interval.lua +++ b/lua/auto-dark-mode/interval.lua @@ -7,6 +7,8 @@ local M = { currently_in_dark_mode = nil, } +local uv = vim.uv or vim.loop + -- Parses the query response for each system, returning `true` if the system is -- in Dark mode, `false` when in Light mode. ---@param res string @@ -35,8 +37,10 @@ local function parse_query_response(res) return false end +-- Executes the `set_dark_mode` and `set_light_mode` hooks when needed, +-- otherwise it's a no-op. ---@param is_dark_mode boolean -local function change_theme_if_needed(is_dark_mode) +local function sync_theme(is_dark_mode) if is_dark_mode == M.currently_in_dark_mode then return end @@ -57,11 +61,17 @@ local function change_theme_if_needed(is_dark_mode) end end -M.poll_dark_mode = function() +---@param callback? fun(is_dark_mode: boolean): nil +M.poll_dark_mode = function(callback) + -- if no callback is provided, use a no-op + if callback == nil then + callback = function() end + end + if vim.system then vim.system(M.state.query_command, { text = true }, function(data) local is_dark_mode = parse_query_response(data.stdout) - change_theme_if_needed(is_dark_mode) + callback(is_dark_mode) end) else -- Legacy implementation using `vim.fn.jobstart` instead of `vim.system`, @@ -70,7 +80,7 @@ M.poll_dark_mode = function() stdout_buffered = true, on_stdout = function(_, data, _) local is_dark_mode = parse_query_response(table.concat(data, "\n")) - change_theme_if_needed(is_dark_mode) + callback(is_dark_mode) end, }) end @@ -80,17 +90,22 @@ M.start_timer = function() ---@type number local interval = M.options.update_interval - if vim.uv.new_timer or vim.loop.new_timer then - M.timer = vim.uv.new_timer() or vim.loop.new_timer() - M.timer:start(interval, interval, M.poll_dark_mode) + local timer_callback = function() + M.poll_dark_mode(sync_theme) + end + + -- needs to check for `vim.system` because the poll function depends on it + if uv and vim.system then + M.timer = uv.new_timer() + M.timer:start(interval, interval, timer_callback) else - M.timer_id = vim.fn.timer_start(interval, M.poll_dark_mode, { ["repeat"] = -1 }) + M.timer_id = vim.fn.timer_start(interval, timer_callback, { ["repeat"] = -1 }) end end M.stop_timer = function() - if vim.uv.timer_stop or vim.loop.timer_stop then - vim.uv.timer_stop(M.timer) + if uv.timer_stop then + uv.timer_stop(M.timer) else vim.fn.timer_stop(M.timer_id) end @@ -102,7 +117,7 @@ M.start = function(options, state) M.options = options M.state = state - M.poll_dark_mode() + M.poll_dark_mode(sync_theme) M.start_timer() end