Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/transcript.lua

Raw
local FILETYPE = 'pivi'

local buffers = {}
local setups = {}

--- Registers a listener for a transcript buffer being created.
--- Buffer-local behavior belongs to the layers that define it, so no global
--- mapping is ever claimed on the user's behalf.
local on_buffer = function( listener )
  table.insert( setups, listener )
end

local configure = function( bufnr, name )
  vim.bo[bufnr].buftype = 'nofile'
  vim.bo[bufnr].bufhidden = 'hide'
  vim.bo[bufnr].swapfile = false
  vim.bo[bufnr].filetype = FILETYPE
  vim.bo[bufnr].modifiable = false
  pcall( vim.api.nvim_buf_set_name, bufnr, 'pivi://' .. name )
  for _, listener in ipairs( setups ) do
    pcall( listener, bufnr )
  end
end

--- Returns the transcript buffer for a name, creating it when missing.
local ensure = function( name )
  vim.validate( 'name', name, 'string' )
  local bufnr = buffers[name]
  if bufnr and vim.api.nvim_buf_is_valid( bufnr ) then return bufnr end
  bufnr = vim.api.nvim_create_buf( false, true )
  configure( bufnr, name )
  buffers[name] = bufnr
  return bufnr
end

local resets = {}

--- Registers a listener for transcript content being discarded.
--- Presentation layers own state keyed by buffer, and that state is only
--- correct while the text it describes still exists.
local on_reset = function( listener )
  table.insert( resets, listener )
end

local announce_reset = function( bufnr )
  for _, listener in ipairs( resets ) do
    pcall( listener, bufnr )
  end
end

local windows = {}

--- Registers a listener for a window becoming a pivi window.
--- Window-local presentation belongs to the layers that define it, not to the
--- buffer that happens to be shown.
local on_window = function( listener )
  table.insert( windows, listener )
end

local writable = function( bufnr, action )
  vim.bo[bufnr].modifiable = true
  local ok, failure = pcall( action )
  vim.bo[bufnr].modifiable = false
  if not ok then error( failure ) end
end

local follow_windows = function( bufnr )
  local last = vim.api.nvim_buf_line_count( bufnr )
  for _, win in ipairs( vim.api.nvim_list_wins() ) do
    if vim.api.nvim_win_get_buf( win ) == bufnr then
      pcall( vim.api.nvim_win_set_cursor, win, { last, 0, } )
    end
  end
end

--- Appends raw text, continuing the final line when it does not start with a newline.
local append = function( bufnr, text )
  vim.validate( 'bufnr', bufnr, 'number' )
  vim.validate( 'text', text, 'string' )
  if text == '' then return end
  if not vim.api.nvim_buf_is_valid( bufnr ) then return end

  local chunks = vim.split( text, '\n', { plain = true, } )
  writable( bufnr, function()
    local count = vim.api.nvim_buf_line_count( bufnr )
    local last = vim.api.nvim_buf_get_lines( bufnr, count - 1, count, false )[1] or ''
    local lines = { last .. chunks[1], }
    for offset = 2, #chunks do
      table.insert( lines, chunks[offset] )
    end
    vim.api.nvim_buf_set_lines( bufnr, count - 1, count, false, lines )
  end )
  follow_windows( bufnr )
end

--- Appends complete lines.
local line = function( bufnr, text )
  vim.validate( 'bufnr', bufnr, 'number' )
  vim.validate( 'text', text, 'string' )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return end
  append( bufnr, text .. '\n' )
end

--- Replaces a range of rows, leaving everything around it alone.
--- Rows are zero based and the end row is exclusive, as in the buffer API.
local replace = function( bufnr, first, last, text )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return end
  writable( bufnr, function()
    vim.api.nvim_buf_set_lines( bufnr, first, last, false, text )
  end )
  follow_windows( bufnr )
end

local lines = function( bufnr )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return {} end
  return vim.api.nvim_buf_get_lines( bufnr, 0, -1, false )
end

local clear = function( bufnr )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return end
  announce_reset( bufnr )
  writable( bufnr, function()
    vim.api.nvim_buf_set_lines( bufnr, 0, -1, false, { '', } )
  end )
end

--- Window-local identity for a pivi window.
--- Only pivi's own windows are decorated; user-owned windows are untouched.
local decorate = function( win, name )
  vim.wo[win].number = false
  vim.wo[win].relativenumber = false
  vim.wo[win].signcolumn = 'no'
  vim.wo[win].foldcolumn = '0'
  vim.wo[win].cursorline = false
  vim.wo[win].wrap = true
  vim.wo[win].linebreak = true
  vim.wo[win].breakindent = true
  vim.wo[win].list = false
  vim.wo[win].spell = false
  vim.wo[win].winbar = '%#Title#π ' .. name .. '%*'
  for _, listener in ipairs( windows ) do
    pcall( listener, win )
  end
end

--- Shows what a pivi window is doing right now, in its own winbar.
local set_state = function( bufnr, name, state )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return end
  local text = '%#Title#π ' .. name .. '%*'
  if state and state ~= '' then text = text .. '  %#Comment#' .. state .. '%*' end
  for _, win in ipairs( vim.api.nvim_list_wins() ) do
    if vim.api.nvim_win_get_buf( win ) == bufnr then vim.wo[win].winbar = text end
  end
end

local name_of = function( bufnr )
  for name, candidate in pairs( buffers ) do
    if candidate == bufnr then return name end
  end
  return 'pi'
end

--- Shows the transcript buffer without stealing the current window.
local show = function( bufnr )
  if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
  for _, win in ipairs( vim.api.nvim_list_wins() ) do
    if vim.api.nvim_win_get_buf( win ) == bufnr then return win end
  end
  local win = require( 'bugabinga.pivi.layout' ).open( bufnr )
  if not win then return nil end
  decorate( win, name_of( bufnr ) )
  return win
end

local forget = function( name )
  local bufnr = buffers[name]
  buffers[name] = nil
  if bufnr then announce_reset( bufnr ) end
  if bufnr and vim.api.nvim_buf_is_valid( bufnr ) then
    pcall( vim.api.nvim_buf_delete, bufnr, { force = true, } )
  end
end

local forget_all = function()
  for name in pairs( buffers ) do
    forget( name )
  end
end

return {
  filetype = FILETYPE,
  ensure = ensure,
  on_reset = on_reset,
  on_window = on_window,
  on_buffer = on_buffer,
  decorate = decorate,
  set_state = set_state,
  append = append,
  line = line,
  replace = replace,
  lines = lines,
  clear = clear,
  show = show,
  forget = forget,
  forget_all = forget_all,
}