2023-10-25 04:41:53 +00:00
|
|
|
-- Greenhouse is a simple text scrolling handler for terminal programs.
|
|
|
|
-- The idea is that it can be set a region to do its scrolling and paging
|
|
|
|
-- job and then the user can draw whatever outside it.
|
|
|
|
-- This reduces code duplication for the message viewer
|
|
|
|
-- and flowerbook.
|
|
|
|
|
|
|
|
local ansikit = require 'ansikit'
|
|
|
|
local lunacolors = require 'lunacolors'
|
|
|
|
local terminal = require 'terminal'
|
|
|
|
local Page = require 'nature.greenhouse.page'
|
|
|
|
local Object = require 'nature.object'
|
|
|
|
|
|
|
|
local Greenhouse = Object:extend()
|
|
|
|
|
|
|
|
function Greenhouse:new(sink)
|
|
|
|
local size = terminal.size()
|
|
|
|
self.region = size
|
|
|
|
self.contents = nil -- or can be a table
|
|
|
|
self.start = 1 -- where to start drawing from (should replace with self.region.y)
|
|
|
|
self.offset = 1 -- vertical text offset
|
2023-12-26 03:08:29 +00:00
|
|
|
self.horizOffset = 1
|
2023-10-25 04:41:53 +00:00
|
|
|
self.sink = sink
|
|
|
|
self.pages = {}
|
|
|
|
self.curPage = 1
|
2023-12-26 03:08:29 +00:00
|
|
|
self.step = {
|
|
|
|
horizontal = 5,
|
|
|
|
vertical = 1
|
|
|
|
}
|
|
|
|
self.separator = '─'
|
2023-10-25 04:41:53 +00:00
|
|
|
self.keybinds = {
|
|
|
|
['Up'] = function(self) self:scroll 'up' end,
|
|
|
|
['Down'] = function(self) self:scroll 'down' end,
|
2023-12-26 03:08:29 +00:00
|
|
|
['Left'] = function(self) self:scroll 'left' end,
|
|
|
|
['Right'] = function(self) self:scroll 'right' end,
|
2023-10-25 04:41:53 +00:00
|
|
|
['Ctrl-Left'] = self.previous,
|
|
|
|
['Ctrl-Right'] = self.next,
|
|
|
|
['Ctrl-N'] = function(self) self:toc(true) end,
|
|
|
|
['Enter'] = function(self)
|
|
|
|
if self.isSpecial then
|
|
|
|
self:jump(self.specialPageIdx)
|
|
|
|
self:special(false)
|
|
|
|
end
|
2023-12-30 20:16:59 +00:00
|
|
|
end,
|
|
|
|
['Page-Down'] = function(self) self:scroll('down', {page = true}) end,
|
|
|
|
['Page-Up'] = function(self) self:scroll('up', {page = true}) end
|
2023-10-25 04:41:53 +00:00
|
|
|
}
|
|
|
|
self.isSpecial = false
|
|
|
|
self.specialPage = nil
|
|
|
|
self.specialPageIdx = 1
|
|
|
|
self.specialOffset = 1
|
|
|
|
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:addPage(page)
|
|
|
|
table.insert(self.pages, page)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:updateCurrentPage(text)
|
|
|
|
local page = self.pages[self.curPage]
|
|
|
|
page:setText(text)
|
|
|
|
end
|
|
|
|
|
2023-12-26 03:08:29 +00:00
|
|
|
function Greenhouse:sub(str, offset, limit)
|
2023-10-25 04:41:53 +00:00
|
|
|
local overhead = 0
|
|
|
|
local function addOverhead(s)
|
|
|
|
overhead = overhead + string.len(s)
|
|
|
|
end
|
|
|
|
|
|
|
|
local s = str:gsub('\x1b%[%d+;%d+;%d+;%d+;%d+%w', addOverhead)
|
|
|
|
:gsub('\x1b%[%d+;%d+;%d+;%d+%w', addOverhead)
|
|
|
|
:gsub('\x1b%[%d+;%d+;%d+%w',addOverhead)
|
|
|
|
:gsub('\x1b%[%d+;%d+%w', addOverhead)
|
|
|
|
:gsub('\x1b%[%d+%w', addOverhead)
|
|
|
|
|
2023-12-26 03:08:29 +00:00
|
|
|
return s:sub(offset, utf8.offset(str, limit + overhead) or limit + overhead)
|
|
|
|
--return s:sub(offset, limit + overhead)
|
2023-10-25 04:41:53 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:draw()
|
|
|
|
local workingPage = self.pages[self.curPage]
|
|
|
|
local offset = self.offset
|
|
|
|
if self.isSpecial then
|
|
|
|
offset = self.specialOffset
|
|
|
|
workingPage = self.specialPage
|
|
|
|
end
|
|
|
|
|
|
|
|
if workingPage.lazy and not workingPage.loaded then
|
|
|
|
workingPage.initialize()
|
|
|
|
end
|
|
|
|
|
|
|
|
local lines = workingPage.lines
|
|
|
|
self.sink:write(ansikit.getCSI(self.start .. ';1', 'H'))
|
|
|
|
self.sink:write(ansikit.getCSI(2, 'J'))
|
|
|
|
|
2023-12-26 03:08:29 +00:00
|
|
|
local writer = self.sink.writeln
|
2023-10-25 04:41:53 +00:00
|
|
|
for i = offset, offset + self.region.height - 1 do
|
|
|
|
if i > #lines then break end
|
|
|
|
|
|
|
|
if i == offset + self.region.height - 1 then writer = self.sink.write end
|
|
|
|
|
2023-12-26 03:08:29 +00:00
|
|
|
self.sink:write(ansikit.getCSI(self.start + i - offset .. ';1', 'H'))
|
|
|
|
local line = lines[i]:gsub('{separator}', function() return self.separator:rep(self.region.width - 1) end)
|
|
|
|
writer(self.sink, self:sub(line:gsub('\t', ' '), self.horizOffset, self.region.width))
|
2023-10-25 04:41:53 +00:00
|
|
|
end
|
2023-12-26 03:08:29 +00:00
|
|
|
writer(self.sink, '\27[0m')
|
2023-10-25 04:41:53 +00:00
|
|
|
self:render()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:render()
|
|
|
|
end
|
|
|
|
|
2023-12-30 20:16:59 +00:00
|
|
|
function Greenhouse:scroll(direction, opts)
|
|
|
|
opts = opts or {}
|
|
|
|
|
2023-10-25 04:41:53 +00:00
|
|
|
if self.isSpecial then
|
|
|
|
if direction == 'down' then
|
|
|
|
self:next(true)
|
|
|
|
elseif direction == 'up' then
|
|
|
|
self:previous(true)
|
|
|
|
end
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local lines = self.pages[self.curPage].lines
|
|
|
|
|
|
|
|
local oldOffset = self.offset
|
2023-12-26 03:08:29 +00:00
|
|
|
local oldHorizOffset = self.horizOffset
|
2023-12-30 20:16:59 +00:00
|
|
|
local amount = self.step.vertical
|
|
|
|
if opts.page then
|
|
|
|
amount = self.region.height
|
|
|
|
end
|
|
|
|
|
2023-10-25 04:41:53 +00:00
|
|
|
if direction == 'down' then
|
2023-12-30 20:16:59 +00:00
|
|
|
self.offset = math.min(self.offset + amount, math.max(1, #lines - self.region.height))
|
2023-10-25 04:41:53 +00:00
|
|
|
elseif direction == 'up' then
|
2023-12-30 20:16:59 +00:00
|
|
|
self.offset = math.max(self.offset - amount, 1)
|
2023-12-26 03:08:29 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
if direction == 'left' then
|
|
|
|
self.horizOffset = math.max(self.horizOffset - self.step.horizontal, 1)
|
|
|
|
elseif direction == 'right' then
|
|
|
|
self.horizOffset = self.horizOffset + self.step.horizontal
|
2023-10-25 04:41:53 +00:00
|
|
|
end
|
2023-12-26 03:08:29 +00:00
|
|
|
]]--
|
2023-10-25 04:41:53 +00:00
|
|
|
|
|
|
|
if self.offset ~= oldOffset then self:draw() end
|
2023-12-26 03:08:29 +00:00
|
|
|
if self.horizOffset ~= oldHorizOffset then self:draw() end
|
2023-10-25 04:41:53 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:update()
|
|
|
|
self:resize()
|
|
|
|
if self.isSpecial then
|
|
|
|
self:updateSpecial()
|
|
|
|
end
|
|
|
|
|
|
|
|
self:draw()
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function Greenhouse:special(val)
|
|
|
|
self.isSpecial = val
|
|
|
|
self:update()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:toggleSpecial()
|
|
|
|
self:special(not self.isSpecial)
|
|
|
|
end
|
|
|
|
|
|
|
|
--- This function will be called when the special page
|
|
|
|
--- is on and needs to be updated.
|
|
|
|
function Greenhouse:updateSpecial()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:contents()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:toc(toggle)
|
|
|
|
if not self.isSpecial then
|
|
|
|
self.specialPageIdx = self.curPage
|
|
|
|
end
|
|
|
|
if toggle then self.isSpecial = not self.isSpecial end
|
|
|
|
-- Generate a special page for our table of contents
|
|
|
|
local tocText = string.format([[
|
|
|
|
%s
|
|
|
|
|
|
|
|
]], lunacolors.cyan(lunacolors.bold '―― Table of Contents ――'))
|
|
|
|
|
|
|
|
local genericPageCount = 1
|
|
|
|
local contents = self:contents()
|
|
|
|
if contents then
|
|
|
|
for i, c in ipairs(contents) do
|
|
|
|
local title = c.title
|
|
|
|
if c.active then
|
|
|
|
title = lunacolors.invert(title)
|
|
|
|
end
|
|
|
|
|
|
|
|
tocText = tocText .. title .. '\n'
|
|
|
|
end
|
|
|
|
else
|
|
|
|
for i, page in ipairs(self.pages) do
|
|
|
|
local title = page.title
|
|
|
|
if title == 'Page' then
|
|
|
|
title = 'Page #' .. genericPageCount
|
|
|
|
genericPageCount = genericPageCount + 1
|
|
|
|
end
|
|
|
|
if i == self.specialPageIdx then
|
|
|
|
title = lunacolors.invert(title)
|
|
|
|
end
|
|
|
|
|
|
|
|
tocText = tocText .. title .. '\n'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
self.specialPage = Page('TOC', tocText)
|
|
|
|
function self:updateSpecial()
|
|
|
|
self:toc()
|
|
|
|
end
|
|
|
|
self:draw()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:resize()
|
|
|
|
local size = terminal.size()
|
|
|
|
self.region = size
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:next(special)
|
|
|
|
local oldCurrent = special and self.specialPageIdx or self.curPage
|
|
|
|
local pageIdx = math.min(oldCurrent + 1, #self.pages)
|
|
|
|
|
|
|
|
if special then
|
|
|
|
self.specialPageIdx = pageIdx
|
|
|
|
else
|
|
|
|
self.curPage = pageIdx
|
|
|
|
end
|
|
|
|
|
|
|
|
if pageIdx ~= oldCurrent then
|
|
|
|
self.offset = 1
|
|
|
|
self:update()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:previous(special)
|
|
|
|
local oldCurrent = special and self.specialPageIdx or self.curPage
|
|
|
|
local pageIdx = math.max(self.curPage - 1, 1)
|
|
|
|
|
|
|
|
if special then
|
|
|
|
self.specialPageIdx = pageIdx
|
|
|
|
else
|
|
|
|
self.curPage = pageIdx
|
|
|
|
end
|
|
|
|
|
|
|
|
if pageIdx ~= oldCurrent then
|
|
|
|
self.offset = 1
|
|
|
|
self:update()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:jump(idx)
|
|
|
|
if idx ~= self.curPage then
|
|
|
|
self.offset = 1
|
|
|
|
end
|
|
|
|
self.curPage = idx
|
|
|
|
self:update()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:keybind(key, callback)
|
|
|
|
self.keybinds[key] = callback
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:input(char)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Greenhouse:initUi()
|
|
|
|
local ansikit = require 'ansikit'
|
|
|
|
local bait = require 'bait'
|
|
|
|
local commander = require 'commander'
|
|
|
|
local hilbish = require 'hilbish'
|
|
|
|
local terminal = require 'terminal'
|
|
|
|
local Page = require 'nature.greenhouse.page'
|
|
|
|
local done = false
|
|
|
|
|
|
|
|
bait.catch('signal.sigint', function()
|
|
|
|
ansikit.clear()
|
|
|
|
done = true
|
|
|
|
end)
|
|
|
|
|
|
|
|
bait.catch('signal.resize', function()
|
|
|
|
self:update()
|
|
|
|
end)
|
|
|
|
|
|
|
|
ansikit.screenAlt()
|
|
|
|
ansikit.clear(true)
|
|
|
|
self:draw()
|
|
|
|
|
2023-12-26 16:52:39 +00:00
|
|
|
while not done do
|
|
|
|
local c = read()
|
|
|
|
self:keybind('Ctrl-Q', function()
|
|
|
|
done = true
|
|
|
|
end)
|
|
|
|
self:keybind('Ctrl-D', function()
|
|
|
|
done = true
|
|
|
|
end)
|
|
|
|
|
|
|
|
if self.keybinds[c] then
|
|
|
|
self.keybinds[c](self)
|
|
|
|
else
|
|
|
|
self:input(c)
|
2023-10-25 04:41:53 +00:00
|
|
|
end
|
|
|
|
end
|
2023-12-26 16:53:19 +00:00
|
|
|
|
2023-10-25 04:41:53 +00:00
|
|
|
ansikit.showCursor()
|
|
|
|
ansikit.screenMain()
|
|
|
|
end
|
|
|
|
|
|
|
|
function read()
|
|
|
|
terminal.saveState()
|
|
|
|
terminal.setRaw()
|
|
|
|
local c = hilbish.editor.readChar()
|
|
|
|
|
|
|
|
terminal.restoreState()
|
|
|
|
return c
|
|
|
|
end
|
|
|
|
|
|
|
|
return Greenhouse
|