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