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