Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use new neovim apis, add :healthcheck, fix sudo query_command #35

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions lua/auto-dark-mode/health.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 = 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

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

-- 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()
health.start("auto-dark-mode.nvim")

if adm.state.setup_correct then
health.ok("Setup is correct")
else
health.error("Setup is incorrect")
end

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)
health.info(
string.format("Benchmark: %.2fms avg / %.2fms min / %.2fms max", benchmark.avg, benchmark.min, benchmark.max)
)

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",
update_interval
)

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

return M
254 changes: 111 additions & 143 deletions lua/auto-dark-mode/init.lua
Original file line number Diff line number Diff line change
@@ -1,100 +1,79 @@
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 = {}

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

-- Optional. Specifies the `update_interval` milliseconds a theme check will be performed.
---@type number?
update_interval = 3000,
}

local function validate_options(options)
vim.validate({
fallback = {
options.fallback,
function(opt)
return opt == "dark" or opt == "light"
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

---@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()
---@class AutoDarkModeState
M.state = {
---@type boolean
setup_correct = false,
---@type DetectedOS
system = nil,
---@type table
query_command = {},
}

M.init = function()
local os_uname = uv.os_uname()

if string.match(os_uname.release, "WSL") then
M.state.system = "WSL"
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"
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 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`.
]])
if M.state.system == "Darwin" then
M.state.query_command = { "defaults", "read", "-g", "AppleInterfaceStyle" }
elseif M.state.system == "Linux" then
if vim.fn.executable("dbus-send") == 0 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`."
)
end

query_command = {
M.state.query_command = {
"dbus-send",
"--session",
"--print-reply=literal",
Expand All @@ -105,10 +84,27 @@ 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 = {
"reg.exe",
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 vim.fn.executable("reg.exe") == 0 then
local assumed_path = "/mnt/c/Windows/system32/reg.exe"

if vim.fn.filereadable(assumed_path) == 1 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,
"Query",
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v",
Expand All @@ -118,66 +114,38 @@ 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 uv.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([[
auto-dark-mode.nvim:
Running as `root`, but `$SUDO_USER` is not set.
Please open an issue to add support for your system.
]])
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

if type(set_dark_mode) ~= "function" or type(set_light_mode) ~= "function" then
error([[
local interval = require("auto-dark-mode.interval")

Call `setup` first:
interval.start(M.options, M.state)

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

check_is_dark_mode(change_theme_if_needed)
start_check_timer()
end

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
Loading