repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/layout.lua
Raw--- How pivi sizes its own windows.
--- Every number is a readability judgement rather than a taste: prose needs a
--- certain width before it stops being a column of fragments, and code needs
--- one before it starts wrapping.
local DEFAULTS = {
transcript_share = 0.4,
transcript_min = 60,
transcript_max = 100,
source_min = 50,
stacked_share = 0.45,
stacked_min = 12,
prompt_share = 0.15,
prompt_min = 3,
prompt_max = 10,
}
local settings = function()
local chosen = vim.g.pivi_layout
if type( chosen ) ~= 'table' then return DEFAULTS end
return vim.tbl_extend( 'force', DEFAULTS, chosen )
end
local clamp = function( value, low, high )
return math.max( low, math.min( high, value ) )
end
--- Decides where the transcript goes and how large it is.
--- Beside the source while both still fit, stacked under it when they do not.
--- @param columns number editor width
--- @param lines number editor height
--- @return table plan orientation, size, and prompt height
local plan = function( columns, lines, chosen )
chosen = chosen or settings()
local wanted = clamp(
math.floor( columns * chosen.transcript_share + 0.5 ),
chosen.transcript_min,
chosen.transcript_max
)
local prompt = clamp(
math.floor( lines * chosen.prompt_share + 0.5 ),
chosen.prompt_min,
math.min( chosen.prompt_max, math.max( chosen.prompt_min, lines - 4 ) )
)
-- One column is spent on the separator between two side-by-side windows.
if columns - wanted - 1 >= chosen.source_min then
return { orientation = 'beside', width = wanted, prompt = prompt, }
end
local height = clamp(
math.floor( lines * chosen.stacked_share + 0.5 ),
math.min( chosen.stacked_min, math.max( 1, lines - 4 ) ),
math.max( 1, lines - 4 )
)
return { orientation = 'below', height = height, prompt = prompt, }
end
local current = function()
return plan( vim.o.columns, vim.o.lines )
end
local size = function( win, chosen )
if not vim.api.nvim_win_is_valid( win ) then return end
if chosen.orientation == 'beside' then
pcall( vim.api.nvim_win_set_width, win, chosen.width )
vim.wo[win].winfixwidth = true
else
pcall( vim.api.nvim_win_set_height, win, chosen.height )
vim.wo[win].winfixheight = true
end
end
local owned = function( win )
if not vim.api.nvim_win_is_valid( win ) then return false end
local filetype = vim.bo[vim.api.nvim_win_get_buf( win )].filetype
return filetype == 'pivi' or filetype == 'pivi-input'
end
--- The window a new pivi window should be carved out of.
--- Splitting a pivi window would stack pivi on pivi, so the widest window the
--- user actually works in is preferred.
local host = function()
local win = vim.api.nvim_get_current_win()
if not owned( win ) then return win end
local widest, area = nil, -1
for _, candidate in ipairs( vim.api.nvim_tabpage_list_wins( 0 ) ) do
if not owned( candidate ) and vim.api.nvim_win_get_config( candidate ).relative == '' then
local measure = vim.api.nvim_win_get_width( candidate ) * vim.api.nvim_win_get_height( candidate )
if measure > area then widest, area = candidate, measure end
end
end
return widest or win
end
--- Opens a window for the transcript, leaving the working window focused.
--- The split direction follows the user's own `splitright` and `splitbelow`.
--- @return number? win
local open = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
local working = vim.api.nvim_get_current_win()
local chosen = current()
local into = host()
local win
local opened = pcall( vim.api.nvim_win_call, into, function()
vim.cmd( chosen.orientation == 'beside' and 'vsplit' or 'split' )
win = vim.api.nvim_get_current_win()
end )
if not opened or not win or not vim.api.nvim_win_is_valid( win ) then return nil end
vim.api.nvim_win_set_buf( win, bufnr )
size( win, chosen )
if vim.api.nvim_win_is_valid( working ) then
vim.api.nvim_set_current_win( working )
end
return win
end
--- Opens the prompt under the transcript when there is one, so the two stay
--- in the same column instead of carving up the source window.
--- @return number? win
local open_prompt = function( bufnr, beside )
if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
local chosen = current()
local into = beside and vim.api.nvim_win_is_valid( beside ) and beside or host()
local win
local opened = pcall( vim.api.nvim_win_call, into, function()
vim.cmd 'split'
win = vim.api.nvim_get_current_win()
end )
if not opened or not win or not vim.api.nvim_win_is_valid( win ) then return nil end
vim.api.nvim_win_set_buf( win, bufnr )
pcall( vim.api.nvim_win_set_height, win, chosen.prompt )
vim.wo[win].winfixheight = true
vim.api.nvim_set_current_win( win )
return win
end
local windows = function()
local found = { transcript = {}, prompt = {}, }
for _, win in ipairs( vim.api.nvim_list_wins() ) do
local bufnr = vim.api.nvim_win_get_buf( win )
local filetype = vim.bo[bufnr].filetype
if filetype == 'pivi' then
table.insert( found.transcript, win )
elseif filetype == 'pivi-input' then
table.insert( found.prompt, win )
end
end
return found
end
--- Brings existing pivi windows back to the size the editor now deserves.
local apply = function()
local chosen = current()
local found = windows()
for _, win in ipairs( found.transcript ) do
size( win, chosen )
end
for _, win in ipairs( found.prompt ) do
pcall( vim.api.nvim_win_set_height, win, chosen.prompt )
end
end
return {
defaults = DEFAULTS,
settings = settings,
plan = plan,
current = current,
host = host,
open = open,
open_prompt = open_prompt,
windows = windows,
apply = apply,
}