--- 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, }