Skip to content

Commit

Permalink
feat: use new neovim apis, add :healthcheck, fix sudo query_command
Browse files Browse the repository at this point in the history
Uses `vim.uv` over `vim.loop`, `vim.system` over `vim.fn.jobstart`, adds
a `:checkhealth` hook with a query_command benchmark, and fixes the
`SUDO_USER` query_command.
  • Loading branch information
nekowinston committed Oct 10, 2024
1 parent d365bec commit b221547
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 177 deletions.
61 changes: 61 additions & 0 deletions lua/auto-dark-mode/health.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
local M = {}

local adm = require("auto-dark-mode")

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()
table.insert(results, (_end - _start) / 1000000)
end

local max = 0
local min = math.huge
local sum = 0
for _, v in pairs(results) do
max = max > v and max or v
min = min < v and min or v
sum = sum + v
end

return { avg = sum / #results, max = max, min = min }
end

M.check = function()
vim.health.start("auto-dark-mode.nvim")

if adm.state.setup_correct then
vim.health.ok("Setup is correct")
else
vim.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, " ") .. "`")

local benchmark = M.benchmark(30)
vim.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 error = string.format(
"Update interval (%dms) seems too short compared to current benchmarks, consider increasing it",
interval
)

if ratio > 30 then
vim.health.ok(info)
elseif ratio > 5 then
vim.health.warn(info)
else
vim.health.error(error)
end
end

return M
236 changes: 99 additions & 137 deletions lua/auto-dark-mode/init.lua
Original file line number Diff line number Diff line change
@@ -1,100 +1,89 @@
local utils = require("auto-dark-mode.utils")

---@type number
local timer_id
---@type boolean
local is_currently_dark_mode

---@type fun(): nil | nil
local set_dark_mode
---@type fun(): nil | nil
local set_light_mode

---@type number
local update_interval

---@type table
local query_command
---@type "Linux" | "Darwin" | "Windows_NT" | "WSL"
local system

---@type "light" | "dark"
local fallback

-- Parses the query response for each system
---@param res table
---@return boolean
local function parse_query_response(res)
if system == "Linux" then
-- https://github.com/flatpak/xdg-desktop-portal/blob/c0f0eb103effdcf3701a1bf53f12fe953fbf0b75/data/org.freedesktop.impl.portal.Settings.xml#L32-L46
-- 0: no preference
-- 1: dark
-- 2: light
if string.match(res[1], "uint32 1") ~= nil then
return true
elseif string.match(res[1], "uint32 2") ~= nil then
return false
else
return fallback == "dark"
end
elseif system == "Darwin" then
return res[1] == "Dark"
elseif system == "Windows_NT" or system == "WSL" then
-- AppsUseLightTheme REG_DWORD 0x0 : dark
-- AppsUseLightTheme REG_DWORD 0x1 : light
return string.match(res[3], "0x1") == nil
end
return false
end

---@param callback fun(is_dark_mode: boolean)
local function check_is_dark_mode(callback)
utils.start_job(query_command, {
on_stdout = function(data)
local is_dark_mode = parse_query_response(data)
callback(is_dark_mode)
end,
local M = {}

---@alias Appearance "light" | "dark"
---@alias DetectedOS "Linux" | "Darwin" | "Windows_NT" | "WSL"

---@class AutoDarkModeOptions
local default_options = {
-- Optional. If not provided, `vim.api.nvim_set_option_value('background', 'dark', {})` will be used.
---@type fun(): nil | nil
set_dark_mode = function()
vim.api.nvim_set_option_value("background", "dark", {})
end,

-- Optional. If not provided, `vim.api.nvim_set_option_value('background', 'light', {})` will be used.
---@type fun(): nil | nil
set_light_mode = function()
vim.api.nvim_set_option_value("background", "light", {})
end,

-- Every `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)
return opt == "dark" or opt == "light"
end,
"`fallback` must be either 'light' or 'dark'",
},
})
M.state.setup_correct = true
end

---@param is_dark_mode boolean
local function change_theme_if_needed(is_dark_mode)
if is_dark_mode == is_currently_dark_mode then
return
end

is_currently_dark_mode = is_dark_mode
if is_currently_dark_mode then
set_dark_mode()
else
set_light_mode()
end
end

local function start_check_timer()
timer_id = vim.fn.timer_start(update_interval, function()
check_is_dark_mode(change_theme_if_needed)
end, { ["repeat"] = -1 })
end

local function init()
if string.match(vim.loop.os_uname().release, "WSL") then
system = "WSL"
---@class AutoDarkModeState
M.state = {
---@type boolean
setup_correct = false,
---@type DetectedOS
system = nil,
---@type table
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()

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
system = vim.loop.os_uname().sysname
M.state.system = os_uname.sysname
end

if system == "Darwin" then
query_command = { "defaults", "read", "-g", "AppleInterfaceStyle" }
elseif system == "Linux" then
if M.state.system == "Darwin" then
M.state.query_command = { "defaults", "read", "-g", "AppleInterfaceStyle" }
elseif M.state.system == "Linux" then
if not vim.fn.executable("dbus-send") then
error([[
`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

query_command = {
M.state.query_command = {
"dbus-send",
"--session",
"--print-reply=literal",
Expand All @@ -105,9 +94,8 @@ local function init()
"string:org.freedesktop.appearance",
"string:color-scheme",
}
elseif system == "Windows_NT" or system == "WSL" then
-- Don't swap the quotes; it breaks the code
query_command = {
elseif M.state.system == "Windows_NT" or M.state.system == "WSL" then
M.state.query_command = {
"reg.exe",
"Query",
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
Expand All @@ -118,66 +106,40 @@ local function init()
return
end

if vim.fn.has("unix") ~= 0 then
if vim.loop.getuid() == 0 then
local sudo_user = vim.env.SUDO_USER
-- 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
local sudo_user = vim.env.SUDO_USER

if sudo_user ~= nil then
query_command = vim.tbl_extend("keep", { "su", "-", sudo_user, "-c" }, query_command)
else
error([[
if sudo_user ~= nil then
-- prepend the command with `su - $SUDO_USER -c`
local extra_args = { "su", "-", sudo_user, "-c" }
for _, v in pairs(M.state.query_command) do
table.insert(extra_args, v)
end
M.state.query_command = extra_args
else
error([[
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
end

if type(set_dark_mode) ~= "function" or type(set_light_mode) ~= "function" then
error([[
Call `setup` first:
require('auto-dark-mode').setup({
set_dark_mode=function()
vim.api.nvim_set_option_value('background', 'dark')
vim.cmd('colorscheme gruvbox')
end,
set_light_mode=function()
vim.api.nvim_set_option_value('background', 'light')
end,
})
]])
end
local interval = require("auto-dark-mode.interval")

check_is_dark_mode(change_theme_if_needed)
start_check_timer()
end
interval.start(M.options, M.state)

local function disable()
vim.fn.timer_stop(timer_id)
-- expose the previous `require("auto-dark-mode").disable()` function
M.disable = interval.stop_timer
end

---@param options AutoDarkModeOptions
local function setup(options)
options = options or {}

---@param background string
local function set_background(background)
vim.api.nvim_set_option_value("background", background, {})
end

set_dark_mode = options.set_dark_mode or function()
set_background("dark")
end
set_light_mode = options.set_light_mode or function()
set_background("light")
end
update_interval = options.update_interval or 3000
fallback = options.fallback or "dark"
M.setup = function(options)
M.options = vim.tbl_deep_extend("keep", options or {}, default_options)
validate_options(M.options)

init()
M.init()
end

return { setup = setup, init = init, disable = disable }
return M
Loading

0 comments on commit b221547

Please sign in to comment.