neovide.neovide/lua/init.lua
Alexsander Falcucci 09ba278bcd
fix: don't clear hidden window titles (#3403)
First, we now set a sane default window title

we already handle title updates from nvim, but the default startup
behavior was still effectively "call everything Neovide" until
otherwise.

that's not very useful.

now, if the user hasn't configured either 'title' or 'titlestring', we enable
titles and use "%F" so the window follows the current path. *but we don't
override user configuration. if either option was already set, we leave it alone.

---

The second, "title-hidden" means "don't show the title", not "make the window
stop having one"

On macOS we got that wrong. when title hiding was enabled, we hid the
title visually and then also **cleared** the underlying NSWindow title by
setting it to an empty string.

That broke the native bookkeeping for secondary windows. The window itself
was created fine and the native tab showed up, but AppKit no longer had
a real title to use for the Window menu entry. So the second window
ended up effectively unnamed there.

we fix it by leaving the NSWindow title alone. keep the title hidden from
the titlebar, but preserve it for the platform APIs that actually need
it.
2026-03-15 21:06:07 +01:00

415 lines
12 KiB
Lua

---@class Args
---@field neovide_channel_id integer
---@field neovide_version string
---@field config_path string
---@field register_clipboard boolean
---@field register_right_click boolean
---@field remote boolean
---@field enable_focus_command boolean
---@field global_variable_settings string[]
---@field option_settings string[]
---@type Args
local args = ...
local M = {}
M.private = {}
vim.g.neovide_channel_id = args.neovide_channel_id
vim.g.neovide_version = args.neovide_version
-- Set some basic rendering options.
vim.o.lazyredraw = false
vim.o.termguicolors = true
local function set_default_window_title()
local title_info = vim.api.nvim_get_option_info2("title", {})
local titlestring_info = vim.api.nvim_get_option_info2("titlestring", {})
if title_info.was_set or titlestring_info.was_set then
return
end
vim.o.title = true
vim.o.titlestring = "%F"
end
set_default_window_title()
local function rpcnotify(method, ...)
vim.rpcnotify(vim.g.neovide_channel_id, method, ...)
end
local function rpcrequest(method, ...)
return vim.rpcrequest(vim.g.neovide_channel_id, method, ...)
end
local function set_clipboard(register)
return function(lines)
rpcrequest("neovide.set_clipboard", lines, register)
end
end
local function get_clipboard(register)
return function()
return rpcrequest("neovide.get_clipboard", register)
end
end
-- Quit when Command+Q is pressed on macOS
if vim.fn.has("macunix") then
vim.keymap.set({ "n", "i", "c", "v", "o", "t", "l" }, "<D-q>", function()
rpcnotify("neovide.exec_detach_handler")
end)
end
if args.register_clipboard and not vim.g.neovide_no_custom_clipboard then
vim.g.clipboard = {
name = "neovide",
copy = {
["+"] = set_clipboard("+"),
["*"] = set_clipboard("*"),
},
paste = {
["+"] = get_clipboard("+"),
["*"] = get_clipboard("*"),
},
cache_enabled = false,
}
vim.g.loaded_clipboard_provider = nil
vim.cmd.runtime("autoload/provider/clipboard.vim")
end
if args.register_right_click then
vim.api.nvim_create_user_command("NeovideRegisterRightClick", function()
rpcnotify("neovide.register_right_click")
end, {})
vim.api.nvim_create_user_command("NeovideUnregisterRightClick", function()
rpcnotify("neovide.unregister_right_click")
end, {})
end
vim.api.nvim_create_user_command("NeovideFocus", function()
rpcnotify("neovide.focus_window")
end, {})
if vim.fn.has("mac") == 1 then
local URL_PATTERN = "https?://[%w-_%.]+%.%w[%w-_%.%%%?%.:/+=&%%[%]#]*"
local compat_unpack = table.unpack or unpack
local function find_span(line, cursor_column, pattern, plain)
local start_pos = 1
local search_limit = #line + 1
for _ = 1, search_limit do
local match_start, match_end = line:find(pattern, start_pos, plain)
if not match_start then
break
end
if cursor_column >= match_start and cursor_column <= match_end then
return match_start, match_end
end
start_pos = match_end + 1
end
return nil, nil
end
local function detect_url(line, cursor_column)
local url_start, url_end = find_span(line, cursor_column, URL_PATTERN, false)
if not url_start then
return nil
end
return {
entity = line:sub(url_start, url_end),
col = url_start - 1,
kind = "url",
}
end
local function detect_file(line, cursor_column)
local cfile = vim.fn.expand("<cfile>")
if not cfile or cfile == "" then
return nil
end
local literal_start = find_span(line, cursor_column, cfile, true)
if not literal_start then
return nil
end
local absolute_path = vim.fn.fnamemodify(cfile, ":p")
if absolute_path == "" or not vim.loop.fs_stat(absolute_path) then
return nil
end
return {
entity = absolute_path,
col = literal_start - 1,
kind = "file",
}
end
local function detect_word(line, cursor_column)
local word, word_col =
compat_unpack(vim.fn.matchstrpos(line, [[\k*\%]] .. cursor_column .. [[c\k*]]))
if not word or word == "" then
return nil
end
return {
entity = word,
col = word_col,
kind = "text",
}
end
local ENTITY_DETECTORS = { detect_url, detect_file, detect_word }
local function detect_entity(line, cursor_col)
local cursor_column = cursor_col + 1
for _, detector in ipairs(ENTITY_DETECTORS) do
local match = detector(line, cursor_column)
if match then
return match
end
end
return {
entity = "",
col = cursor_col,
kind = "text",
}
end
local function take_entity_under_cursor()
local mouse_pos = vim.fn.getmousepos()
local guifont = vim.api.nvim_get_option("guifont")
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local match = detect_entity(line, cursor[2])
local screenpos = vim.fn.screenpos(mouse_pos.winid, cursor[1], match.col + 1)
local screen_row = math.max(screenpos.row - 1, 0)
local screen_col = math.max(screenpos.col - 1, 0)
return screen_col, screen_row, match.entity, guifont, match.kind
end
vim.api.nvim_create_user_command("NeovideForceClick", function()
local col, row, entity, guifont, entity_kind = take_entity_under_cursor()
rpcnotify("neovide.force_click", col, row, entity, guifont, entity_kind)
end, {})
end
vim.api.nvim_exec2(
[[
function! WatchGlobal(variable, callback)
call dictwatcheradd(g:, a:variable, a:callback)
endfunction
]],
{ output = false }
)
for _, global_variable_setting in ipairs(args.global_variable_settings) do
local callback = function()
rpcnotify("setting_changed", global_variable_setting, vim.g["neovide_" .. global_variable_setting])
end
vim.fn.WatchGlobal("neovide_" .. global_variable_setting, callback)
end
for _, option_setting in ipairs(args.option_settings) do
vim.api.nvim_create_autocmd({ "OptionSet" }, {
pattern = option_setting,
once = false,
nested = true,
callback = function()
rpcnotify("option_changed", option_setting, vim.o[option_setting])
end,
})
end
-- Ignore initial values of lines and columns because they are set by neovim directly.
-- See https://github.com/neovide/neovide/issues/2300
vim.api.nvim_create_autocmd({ "VimEnter" }, {
once = true,
nested = true,
callback = function()
for _, option_setting in ipairs(args.option_settings) do
if option_setting ~= "lines" and option_setting ~= "columns" then
rpcnotify("option_changed", option_setting, vim.o[option_setting])
end
end
end,
})
-- Create auto command for retrieving exit code from neovim on quit.
vim.api.nvim_create_autocmd({ "VimLeavePre" }, {
pattern = "*",
once = true,
nested = true,
callback = function()
rpcrequest("neovide.quit", vim.v.exiting)
end,
})
M.private.dropfile = function(filename, tabs)
vim.api.nvim_cmd({
cmd = "drop",
args = { vim.fn.fnameescape(filename) },
-- Always open as the last tabpage
mods = tabs and { tab = #vim.api.nvim_list_tabpages() } or {},
}, {})
end
M.private.can_set_background = function()
local info = vim.api.nvim_get_option_info2("background", {})
-- Don't change the background if someone else has set it
if info.was_set and info.last_set_chan ~= args.neovide_channel_id then
return false
else
return true
end
end
M.disable_redraw = function()
-- Wrap inside pcall to avoid errors if Neovide disconnects
pcall(rpcnotify, "neovide.set_redraw", false)
end
M.enable_redraw = function()
-- Wrap inside pcall to avoid errors if Neovide disconnects
pcall(rpcnotify, "neovide.set_redraw", true)
end
vim.api.nvim_create_user_command("NeovideConfig", function()
if args.remote then
if vim.fn.filereadable(args.config_path) ~= 0 then
vim.notify(
"Neovide is running as a remote server. So the config file may not be on this machine.\n"
.. "Open it manually if you intend to edit the file anyway.\n"
.. "Config file location: "
.. args.config_path,
vim.log.levels.WARN,
{ title = "Neovide" }
)
end
else
vim.cmd('edit ' .. vim.fn.fnameescape(args.config_path))
end
end, {})
local function progress_bar(data)
-- Wrap inside pcall to avoid errors if Neovide disconnects.
pcall(rpcnotify, "neovide.progress_bar", data)
end
local function buffer_is_empty(bufnr)
if vim.api.nvim_buf_line_count(bufnr) ~= 1 then
return false
end
local first_line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, true)[1]
return first_line == nil or first_line == ""
end
local function current_window_is_only_normal()
local current = vim.api.nvim_get_current_win()
local found
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local config = vim.api.nvim_win_get_config(win)
if config.relative == "" then
if found ~= nil then
return false
end
found = win
end
end
return found ~= nil and found == current
end
local function shortmess_allows_intro()
return not string.find(vim.o.shortmess or "", "I", 1, true)
end
local function should_show_intro_banner()
local current_buffer = vim.api.nvim_get_current_buf()
local buffer_name = vim.api.nvim_buf_get_name(current_buffer)
local current_window = vim.api.nvim_get_current_win()
return buffer_is_empty(current_buffer)
and buffer_name == ""
and current_buffer == 1
and current_window == 1000
and current_window_is_only_normal()
and shortmess_allows_intro()
end
local intro_banner_state
local function notify_intro_state()
local allowed = false
local ok, result = pcall(should_show_intro_banner)
if ok then
allowed = result
end
if intro_banner_state ~= allowed then
intro_banner_state = allowed
pcall(rpcnotify, "neovide.intro_banner_allowed", allowed)
end
end
local function schedule_intro_state_check()
vim.schedule(notify_intro_state)
end
local intro_group = vim.api.nvim_create_augroup("NeovideIntroBanner", { clear = true })
vim.api.nvim_create_autocmd({
"VimEnter",
"BufEnter",
"BufFilePost",
"BufNewFile",
"BufReadPost",
"TextChanged",
"TextChangedI",
"WinEnter",
"WinNew",
"WinClosed",
"TabEnter",
"TabClosed",
}, {
group = intro_group,
callback = schedule_intro_state_check,
})
vim.api.nvim_create_autocmd("OptionSet", {
group = intro_group,
pattern = "shortmess",
callback = schedule_intro_state_check,
})
notify_intro_state()
pcall(vim.api.nvim_create_autocmd, 'Progress', {
group = vim.api.nvim_create_augroup('NeovideProgressBar', { clear = true }),
desc = 'Forward progress events to Neovide',
callback = function(ev)
if ev.data and ev.data.status == 'running' then
progress_bar({
percent = ev.data.percent or 0,
title = ev.data.title or "",
message = ev.data.message or "",
})
else
progress_bar({ percent = 100 })
end
end,
})
---@param preedit_raw_text string
---@param cursor_offset_start integer This values show the cursor begin position. The position is byte-wise indexed.
---@param cursor_offset_end integer This values show the cursor end position. The position is byte-wise indexed.
M.preedit_handler = function(preedit_raw_text, cursor_offset_start, cursor_offset_end) end
---@param commit_raw_text string
---@param commit_formatted_text string It's escaped.
M.commit_handler = function(commit_raw_text, commit_formatted_text)
vim.api.nvim_input(commit_formatted_text)
end
_G["neovide"] = M