diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b82da..d8f5cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,22 @@ - `pipe` property to check if a sink with input is a pipe (like stdin) - Add fuzzy search to history search (enable via `hilbish.opts.fuzzy = true`) - Show indexes on cdr list +- Fix doc command not displaying correct subdocs when using shorthand api doc access (`doc api hilbish.jobs` as an example) - `hilbish.messages` interface (details in [#219]) - `hilbish.notification` signal when a message/notification is sent - `notifyJobFinish` opt to send a notification when background jobs are completed. +- Allow numbered arg substitutions in aliases. + - Example: `hilbish.alias('hello', 'echo %1 says hello')` allows the user to run `hello hilbish` + which will output `hilbish says hello`. +- Greenhouse + - Greenhouse is a pager library and program. Basic usage is `greenhouse ` + - Using this also brings enhancements to the `doc` command like easy + navigation of neighboring doc files. -[#219]: https://github.com/Rosettea/Hilbish/issues/219 ### Fixed +- Fix infinite loop when navigating history without any history. [#252](https://github.com/Rosettea/Hilbish/issues/252) +- Return the prefix when calling `hilbish.completions.call`. [#219](https://github.com/Rosettea/Hilbish/issues/219) - Replaced `sed` in-place editing with `grep` and `mv` for compatibility with BSD utils ## [2.1.2] - 2022-04-10 diff --git a/aliases.go b/aliases.go index bfacc43..8b815b3 100644 --- a/aliases.go +++ b/aliases.go @@ -1,6 +1,8 @@ package main import ( + "regexp" + "strconv" "strings" "sync" @@ -46,9 +48,32 @@ func (a *aliasModule) Resolve(cmdstr string) string { a.mu.RLock() defer a.mu.RUnlock() - args := strings.Split(cmdstr, " ") + arg, _ := regexp.Compile(`[\\]?%\d+`) + + args, _ := splitInput(cmdstr) + if len(args) == 0 { + // this shouldnt reach but...???? + return cmdstr + } + for a.aliases[args[0]] != "" { alias := a.aliases[args[0]] + alias = arg.ReplaceAllStringFunc(alias, func(a string) string { + idx, _ := strconv.Atoi(a[1:]) + if strings.HasPrefix(a, "\\") || idx == 0 { + return strings.TrimPrefix(a, "\\") + } + + if idx + 1 > len(args) { + return a + } + val := args[idx] + args = cut(args, idx) + cmdstr = strings.Join(args, " ") + + return val + }) + cmdstr = alias + strings.TrimPrefix(cmdstr, args[0]) cmdArgs, _ := splitInput(cmdstr) args = cmdArgs diff --git a/complete.go b/complete.go index 51b426f..0c70e07 100644 --- a/complete.go +++ b/complete.go @@ -253,15 +253,16 @@ func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } // we must keep the holy 80 cols - completerReturn, err := rt.Call1(l.MainThread(), - rt.FunctionValue(completecb), rt.StringValue(query), - rt.StringValue(ctx), rt.TableValue(fields)) + cont := c.Next() + err = rt.Call(l.MainThread(), rt.FunctionValue(completecb), + []rt.Value{rt.StringValue(query), rt.StringValue(ctx), rt.TableValue(fields)}, + cont) if err != nil { return nil, err } - return c.PushingNext1(t.Runtime, completerReturn), nil + return cont, nil } // #interface completions diff --git a/docs/api/hilbish/_index.md b/docs/api/hilbish/_index.md index 4cf0180..a683172 100644 --- a/docs/api/hilbish/_index.md +++ b/docs/api/hilbish/_index.md @@ -126,7 +126,10 @@ A call with no argument will toggle the value. Flush writes all buffered input to the sink. #### read() -> string -Reads input from the sink. +Reads a liine of input from the sink. + +#### readAll() -> string +Reads all input from the sink. #### write(str) Writes data to a sink. diff --git a/docs/api/hilbish/hilbish.editor.md b/docs/api/hilbish/hilbish.editor.md index 30a3842..d75d4c2 100644 --- a/docs/api/hilbish/hilbish.editor.md +++ b/docs/api/hilbish/hilbish.editor.md @@ -21,6 +21,10 @@ Returns the text that is at the register. ### insert(text) Inserts text into the line. +### getChar() -> string +Reads a keystroke from the user. This is in a format +of something like Ctrl-L.. + ### setVimRegister(register, text) Sets the vim register at `register` to hold the passed text. diff --git a/editor.go b/editor.go index 3038f07..d720a41 100644 --- a/editor.go +++ b/editor.go @@ -16,6 +16,7 @@ func editorLoader(rtm *rt.Runtime) *rt.Table { "setVimRegister": {editorSetRegister, 1, false}, "getVimRegister": {editorGetRegister, 2, false}, "getLine": {editorGetLine, 0, false}, + "readChar": {editorReadChar, 0, false}, } mod := rt.NewTable() @@ -94,3 +95,13 @@ func editorGetLine(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil } + +// #interface editor +// getChar() -> string +// Reads a keystroke from the user. This is in a format +// of something like Ctrl-L.. +func editorReadChar(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + buf := lr.rl.ReadChar() + + return c.PushingNext1(t.Runtime, rt.StringValue(string(buf))), nil +} diff --git a/emmyLuaDocs/hilbish.lua b/emmyLuaDocs/hilbish.lua index eb02a08..2ee93ed 100644 --- a/emmyLuaDocs/hilbish.lua +++ b/emmyLuaDocs/hilbish.lua @@ -40,6 +40,10 @@ function hilbish.editor.getVimRegister(register) end --- Inserts text into the line. function hilbish.editor.insert(text) end +--- Reads a keystroke from the user. This is in a format +--- of something like Ctrl-L.. +function hilbish.editor.getChar() end + --- Sets the vim register at `register` to hold the passed text. --- @param register string --- @param text string @@ -196,10 +200,14 @@ function hilbish:autoFlush(auto) end --- Flush writes all buffered input to the sink. function hilbish:flush() end ---- Reads input from the sink. +--- Reads a liine of input from the sink. --- @returns string function hilbish:read() end +--- Reads all input from the sink. +--- @returns string +function hilbish:readAll() end + --- Writes data to a sink. function hilbish:write(str) end diff --git a/main.go b/main.go index 300f333..90caa47 100644 --- a/main.go +++ b/main.go @@ -289,7 +289,7 @@ func removeDupes(slice []string) []string { func contains(s []string, e string) bool { for _, a := range s { - if a == e { + if strings.ToLower(a) == strings.ToLower(e) { return true } } @@ -324,3 +324,7 @@ func getVersion() string { return v.String() } + +func cut(slice []string, idx int) []string { + return append(slice[:idx], slice[idx + 1:]...) +} diff --git a/nature/commands/doc.lua b/nature/commands/doc.lua index d37e677..ee1e37c 100644 --- a/nature/commands/doc.lua +++ b/nature/commands/doc.lua @@ -1,6 +1,9 @@ +local ansikit = require 'ansikit' local commander = require 'commander' local fs = require 'fs' local lunacolors = require 'lunacolors' +local Greenhouse = require 'nature.greenhouse' +local Page = require 'nature.greenhouse.page' commander.register('doc', function(args, sinks) local moddocPath = hilbish.dataDir .. '/docs/' @@ -9,11 +12,6 @@ commander.register('doc', function(args, sinks) -- hilbish git moddocPath = './docs/' end - local apidocHeader = [[ -# %s -{grayBg} {white}{italic}%s {reset} - -]] local modules = table.map(fs.readdir(moddocPath), function(f) return lunacolors.underline(lunacolors.blue(string.gsub(f, '.md', ''))) @@ -25,47 +23,15 @@ to Hilbish. Usage: doc
[subdoc] Available sections: ]] .. table.concat(modules, ', ') - if #args > 0 then - local mod = args[1] - - local f = io.open(moddocPath .. mod .. '.md', 'rb') - local funcdocs = nil - local subdocName = args[2] - if not f then - -- assume subdir - -- dataDir/docs//.md - moddocPath = moddocPath .. mod .. '/' - if not subdocName then - subdocName = '_index' - end - f = io.open(moddocPath .. subdocName .. '.md', 'rb') - if not f then - f = io.open(moddocPath .. subdocName:match '%w+' .. '/' .. subdocName .. '.md', 'rb') - end - if not f then - moddocPath = moddocPath .. subdocName .. '/' - subdocName = args[3] or '_index' - f = io.open(moddocPath .. subdocName .. '.md', 'rb') - end - if not f then - sinks.out:writeln('No documentation found for ' .. mod .. '.') - return 1 - end - end - funcdocs = f:read '*a':gsub('-([%d]+)', '%1') - local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= '_index.md' and f ~= 'index.md' end) - local subdocs = table.map(moddocs, function(fname) - return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.md', ''))) - end) - if #moddocs ~= 0 then - funcdocs = funcdocs .. '\nSubdocs: ' .. table.concat(subdocs, ', ') - end - - local valsStr = funcdocs:match '%-%-%-\n([^%-%-%-]+)\n' + local f + local function handleYamlInfo(d) local vals = {} + local docs = d + + local valsStr = docs:match '%-%-%-\n([^%-%-%-]+)\n' + print(valsStr) if valsStr then - local _, endpos = funcdocs:find('---\n' .. valsStr .. '\n---\n\n', 1, true) - funcdocs = funcdocs:sub(endpos + 1, #funcdocs) + docs = docs:sub(valsStr:len() + 10, #docs) -- parse vals local lines = string.split(valsStr, '\n') @@ -78,23 +44,113 @@ Available sections: ]] .. table.concat(modules, ', ') end end end - if mod == 'api' then - funcdocs = string.format(apidocHeader, vals.title, vals.description or 'no description.') .. funcdocs - end - doc = funcdocs:sub(1, #funcdocs - 1) - f:close() + + --docs = docs:sub(1, #docs - 1) + return docs, vals end - local backtickOccurence = 0 - sinks.out:writeln(lunacolors.format(doc:gsub('`', function() - backtickOccurence = backtickOccurence + 1 - if backtickOccurence % 2 == 0 then - return '{reset}' - else - return '{underline}{green}' + if #args > 0 then + local mod = args[1] + + f = io.open(moddocPath .. mod .. '.md', 'rb') + local funcdocs = nil + local subdocName = args[2] + if not f then + moddocPath = moddocPath .. mod .. '/' + if not subdocName then + subdocName = '_index' + end + f = io.open(moddocPath .. subdocName .. '.md', 'rb') + local oldmoddocPath = moddocPath + if not f then + moddocPath = moddocPath .. subdocName:match '%w+' .. '/' + f = io.open(moddocPath .. subdocName .. '.md', 'rb') + end + if not f then + moddocPath = oldmoddocPath .. subdocName .. '/' + subdocName = args[3] or '_index' + f = io.open(moddocPath .. subdocName .. '.md', 'rb') + end + if not f then + sinks.out:writeln('No documentation found for ' .. mod .. '.') + return 1 + end end - end):gsub('\n#+.-\n', function(t) - local signature = t:gsub('<.->(.-)', '{underline}%1'):gsub('\\', '<') - return '{bold}{yellow}' .. signature .. '{reset}' - end))) + + end + + local moddocs = table.filter(fs.readdir(moddocPath), function(f) return f ~= '_index.md' and f ~= 'index.md' end) + local subdocs = table.map(moddocs, function(fname) + return lunacolors.underline(lunacolors.blue(string.gsub(fname, '.md', ''))) + end) + + local gh = Greenhouse(sinks.out) + function gh:resize() + local size = terminal.size() + self.region = { + width = size.width, + height = size.height - 3 + } + end + gh:resize() + + function gh:render() + local workingPage = self.pages[self.curPage] + local offset = self.offset + if self.isSpecial then + offset = self.specialOffset + workingPage = self.specialPage + end + + self.sink:write(ansikit.getCSI(self.region.height + 2 .. ';1', 'H')) + if not self.isSpecial then + if args[1] == 'api' then + self.sink:writeln(lunacolors.reset(string.format('%s', workingPage.title))) + self.sink:write(lunacolors.format(string.format('{grayBg} ↳ {white}{italic}%s {reset}', workingPage.description or 'No description.'))) + else + self.sink:write(lunacolors.reset(string.format('Viewing doc page %s', moddocPath))) + end + end + end + local backtickOccurence = 0 + local function formatDocText(d) + return lunacolors.format(d:gsub('`', function() + backtickOccurence = backtickOccurence + 1 + if backtickOccurence % 2 == 0 then + return '{reset}' + else + return '{underline}{green}' + end + end):gsub('\n#+.-\n', function(t) + local signature = t:gsub('<.->(.-)', '{underline}%1'):gsub('\\', '<') + return '{bold}{yellow}' .. signature .. '{reset}' + end)) + end + + + local doc, vals = handleYamlInfo(#args == 0 and doc or formatDocText(f:read '*a':gsub('-([%d]+)', '%1'))) + if #moddocs ~= 0 and f then + doc = doc .. '\nSubdocs: ' .. table.concat(subdocs, ', ') .. '\n\n' + end + if f then f:close() end + + local page = Page(vals.title, doc) + page.description = vals.description + gh:addPage(page) + + -- add subdoc pages + for _, sdName in ipairs(moddocs) do + local sdFile = fs.join(sdName, '_index.md') + if sdName:match '.md$' then + sdFile = sdName + end + + local f = io.open(moddocPath .. sdFile, 'rb') + local doc, vals = handleYamlInfo(f:read '*a':gsub('-([%d]+)', '%1')) + local page = Page(vals.title, formatDocText(doc)) + page.description = vals.description + gh:addPage(page) + end + ansikit.hideCursor() + gh:initUi() end) diff --git a/nature/commands/greenhouse.lua b/nature/commands/greenhouse.lua new file mode 100644 index 0000000..9c155b0 --- /dev/null +++ b/nature/commands/greenhouse.lua @@ -0,0 +1,124 @@ +local ansikit = require 'ansikit' +local bait = require 'bait' +local commander = require 'commander' +local hilbish = require 'hilbish' +local lunacolors = require 'lunacolors' +local terminal = require 'terminal' +local Greenhouse = require 'nature.greenhouse' +local Page = require 'nature.greenhouse.page' + +commander.register('greenhouse', function(args, sinks) + local gh = Greenhouse(sinks.out) + + local buffer = '' + local display = '' + local command = false + local commands = { + q = function() + gh.keybinds['Ctrl-D'](gh) + end, + ['goto'] = function(args) + if not args[1] then + return 'nuh uh' + end + gh:jump(tonumber(args[1])) + end + } + + function gh:resize() + local size = terminal.size() + self.region = { + width = size.width, + height = size.height - 2 + } + end + + function gh:render() + local workingPage = self.pages[self.curPage] + local offset = self.offset + if self.isSpecial then + offset = self.specialOffset + workingPage = self.specialPage + end + + self.sink:write(ansikit.getCSI(self.region.height + 1 .. ';1', 'H')) + if not self.isSpecial then + self.sink:writeln(lunacolors.format(string.format('{grayBg} ↳ Page %d%s{reset}', self.curPage, workingPage.title and ' — ' .. workingPage.title .. ' ' or ''))) + end + self.sink:write(buffer == '' and display or buffer) + end + function gh:input(c) + -- command handling + if c == ':' and not command then + command = true + end + if c == 'Escape' then + if command then + command = false + buffer = '' + else + if self.isSpecial then gh:special() end + end + elseif c == 'Backspace' then + buffer = buffer:sub(0, -2) + if buffer == '' then + command = false + else + goto update + end + end + + if command then + ansikit.showCursor() + if buffer:match '^:' then buffer = buffer .. c else buffer = c end + else + ansikit.hideCursor() + end + + ::update:: + gh:update() + end + gh:resize() + + gh:keybind('Enter', function(self) + if self.isSpecial then + self:jump(self.specialPageIdx) + self:special(false) + else + if buffer:len() < 2 then return end + + local splitBuf = string.split(buffer, " ") + local command = commands[splitBuf[1]:sub(2)] + if command then + table.remove(splitBuf, 1) + buffer = command(splitBuf) or '' + end + self:update() + end + end) + + if sinks['in'].pipe then + local page = Page('stdin', sinks['in']:readAll()) + gh:addPage(page) + end + + for _, name in ipairs(args) do + local f = io.open(name, 'r') + if not f then + sinks.err:writeln(string.format('could not open file %s', name)) + end + local page = Page(name, f:read '*a') + gh:addPage(page) + end + + if #gh.pages == 0 then + sinks.out:writeln [[greenhouse is the Hilbish pager library and command! +usage: greenhouse ... + +example: greenhouse hello.md]] + return 1 + end + + ansikit.hideCursor() + gh:initUi() +end) diff --git a/nature/greenhouse/init.lua b/nature/greenhouse/init.lua new file mode 100644 index 0000000..d5877e8 --- /dev/null +++ b/nature/greenhouse/init.lua @@ -0,0 +1,328 @@ +-- 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 + self.sink = sink + self.pages = {} + self.curPage = 1 + self.keybinds = { + ['Up'] = function(self) self:scroll 'up' end, + ['Down'] = function(self) self:scroll 'down' end, + ['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 + end + } + 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 + +local function sub(str, limit) + 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) + + return s:sub(0, limit + overhead) +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')) + + for i = offset, offset + self.region.height - 1 do + if i > #lines then break end + + local writer = self.sink.writeln + if i == offset + self.region.height - 1 then writer = self.sink.write end + + writer(self.sink, sub(lines[i]:gsub('\t', ' '), self.region.width)) + end + self:render() +end + +function Greenhouse:render() +end + +function Greenhouse:scroll(direction) + 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 + if direction == 'down' then + self.offset = math.min(self.offset + 1, math.max(1, #lines - self.region.height)) + elseif direction == 'up' then + self.offset = math.max(self.offset - 1, 1) + end + + if self.offset ~= oldOffset then self:draw() end +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() + + hilbish.goro(function() + while not done do + local c = read() + self:keybind('Ctrl-D', function() + done = true + end) + + if self.keybinds[c] then + self.keybinds[c](self) + else + self:input(c) + end + + --[[ + if c == 27 then + local c1 = read() + if c1 == 91 then + local c2 = read() + if c2 == 66 then -- arrow down + self:scroll 'down' + elseif c2 == 65 then -- arrow up + self:scroll 'up' + end + + if c2 == 49 then + local c3 = read() + if c3 == 59 then + local c4 = read() + if c4 == 53 then + local c5 = read() + if c5 == 67 then + self:next() + elseif c5 == 68 then + self:previous() + end + end + end + end + end + goto continue + end + ]]-- + + ::continue:: + end + end) + + while not done do + -- + end + ansikit.showCursor() + ansikit.screenMain() +end + +function read() + terminal.saveState() + terminal.setRaw() + local c = hilbish.editor.readChar() + + terminal.restoreState() + return c +end + +return Greenhouse diff --git a/nature/greenhouse/page.lua b/nature/greenhouse/page.lua new file mode 100644 index 0000000..51d1440 --- /dev/null +++ b/nature/greenhouse/page.lua @@ -0,0 +1,32 @@ +local Object = require 'nature.object' + +local Page = Object:extend() + +function Page:new(title, text) + self:setText(text) + self.title = title or 'Page' + self.lazy = false + self.loaded = true + self.children = {} +end + +function Page:setText(text) + self.lines = string.split(text, '\n') +end + +function Page:setTitle(title) + self.title = title +end + +function Page:dynamic(initializer) + self.initializer = initializer + self.lazy = true + self.loaded = false +end + +function Page:initialize() + self.initializer() + self.loaded = true +end + +return Page diff --git a/nature/object.lua b/nature/object.lua new file mode 100644 index 0000000..053be4a --- /dev/null +++ b/nature/object.lua @@ -0,0 +1,59 @@ +---@class nature.object +---@field super nature.object +local Object = {} +Object.__index = Object + +---Can be overrided by child objects to implement a constructor. +function Object:new() end + +---@return nature.object +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find("__") == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +---Check if the object is strictly of the given type. +---@param T any +---@return boolean +function Object:is(T) + return getmetatable(self) == T +end + +---Check if the object inherits from the given type. +---@param T any +---@return boolean +function Object:extends(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + +---Metamethod to get a string representation of an object. +---@return string +function Object:__tostring() + return "Object" +end + +---Methamethod to allow using the object call as a constructor. +---@return nature.object +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + + +return Object diff --git a/readline/codes.go b/readline/codes.go index 492bc72..dd8495d 100644 --- a/readline/codes.go +++ b/readline/codes.go @@ -1,5 +1,7 @@ package readline +import "os" + // Character codes const ( charCtrlA = iota + 1 @@ -134,3 +136,57 @@ const ( const ( seqCtermFg255 = "\033[48;5;255m" ) + +// TODO: return whether its actually a sequence or not +// remedies the edge case of someone literally typing Ctrl-A for example. +func (rl *Instance) ReadChar() string { + b := make([]byte, 1024) + i, _ := os.Stdin.Read(b) + r := []rune(string(b)) + s := string(r[:i]) + + switch b[0] { + case charCtrlA: return "Ctrl-A" + case charCtrlB: return "Ctrl-B" + case charCtrlC: return "Ctrl-C" + case charEOF: return "Ctrl-D" + case charCtrlE: return "Ctrl-E" + case charCtrlF: return "Ctrl-F" + case charCtrlG: return "Ctrl-G" + case charBackspace, charBackspace2: return "Backspace" + case charTab: return "Tab" + case charCtrlK: return "Ctrl-K" + case charCtrlL: return "Ctrl-L" + case charCtrlN: return "Ctrl-N" + case charCtrlO: return "Ctrl-O" + case charCtrlP: return "Ctrl-P" + case charCtrlQ: return "Ctrl-Q" + case charCtrlR: return "Ctrl-R" + case charCtrlS: return "Ctrl-S" + case charCtrlT: return "Ctrl-T" + case charCtrlU: return "Ctrl-U" + case charCtrlV: return "Ctrl-V" + case charCtrlW: return "Ctrl-W" + case charCtrlX: return "Ctrl-X" + case charCtrlY: return "Ctrl-Y" + case charCtrlZ: return "Ctrl-Z" + case '\r': fallthrough + case '\n': return "Enter" + case charEscape: + switch s { + case string(charEscape): return "Escape" + case seqUp: return "Up" + case seqDown: return "Down" + case seqBackwards: return "Left" + case seqForwards: return "Right" + case seqCtrlLeftArrow: return "Ctrl-Left" + case seqCtrlRightArrow: return "Ctrl-Right" + case seqCtrlDelete, seqCtrlDelete2: return "Ctrl-Delete" + case seqHome, seqHomeSc: return "Home" + case seqEnd, seqEndSc: return "End" + case seqDelete, seqDelete2: return "Delete" + } + } + + return s +} diff --git a/readline/history.go b/readline/history.go index f772813..e226b4d 100644 --- a/readline/history.go +++ b/readline/history.go @@ -156,8 +156,8 @@ func (rl *Instance) walkHistory(i int) { rl.updateHelpers() // In order to avoid having to type j/k twice each time for history navigation, - // we walk once again. This only ever happens when we aren't out of bounds. - if dedup && old == new { + // we walk once again. This only ever happens when we aren't out of bounds and the last history item was not a empty string. + if new != "" && dedup && old == new { rl.walkHistory(i) } } diff --git a/sink.go b/sink.go index 2ecc19d..3aa5507 100644 --- a/sink.go +++ b/sink.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "hilbish/util" @@ -31,6 +32,7 @@ func setupSinkType(rtm *rt.Runtime) { sinkFuncs := map[string]util.LuaExport{ "flush": {luaSinkFlush, 1, false}, "read": {luaSinkRead, 1, false}, + "readAll": {luaSinkReadAll, 1, false}, "autoFlush": {luaSinkAutoFlush, 2, false}, "write": {luaSinkWrite, 2, false}, "writeln": {luaSinkWriteln, 2, false}, @@ -65,10 +67,42 @@ func setupSinkType(rtm *rt.Runtime) { l.SetRegistry(sinkMetaKey, rt.TableValue(sinkMeta)) } + +// #member +// readAll() -> string +// --- @returns string +// Reads all input from the sink. +func luaSinkReadAll(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { + if err := c.Check1Arg(); err != nil { + return nil, err + } + + s, err := sinkArg(c, 0) + if err != nil { + return nil, err + } + + lines := []string{} + for { + line, err := s.reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + lines = append(lines, line) + } + + return c.PushingNext1(t.Runtime, rt.StringValue(strings.Join(lines, ""))), nil +} + // #member // read() -> string // --- @returns string -// Reads input from the sink. +// Reads a liine of input from the sink. func luaSinkRead(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err diff --git a/website/content/docs/features/runner-mode.md b/website/content/docs/features/runner-mode.md index 8774de9..58b55dd 100644 --- a/website/content/docs/features/runner-mode.md +++ b/website/content/docs/features/runner-mode.md @@ -13,8 +13,8 @@ is that it runs Lua first and then falls back to shell script. In some cases, someone might want to switch to just shell script to avoid it while interactive but still have a Lua config, or go full Lua to use -Hilbish as a REPL. This also allows users to add alternative languages, -instead of either like Fennel. +Hilbish as a REPL. This also allows users to add alternative languages like +Fennel as the interactive script runner. Runner mode can also be used to handle specific kinds of input before evaluating like normal, which is how [Link.hsh](https://github.com/TorchedSammy/Link.hsh)