First commit

This commit is contained in:
James Dugan 2025-02-04 21:06:01 -07:00
commit 847c9e64c0
51 changed files with 4058 additions and 0 deletions

63
.config/hilbish/carapace.lua Executable file
View file

@ -0,0 +1,63 @@
local json = require "json"
local lunacolors = require "lunacolors"
local M = {}
M.fmt = function(str, style)
local styles = style:split(" ")
local formatStr = ""
for _, v in pairs(styles) do
formatStr = formatStr .. "{" .. v .. "}"
end
formatStr = formatStr .. str
return lunacolors.format(formatStr)
end
M.complete_func = function(query, ctx, fields)
local carapace_arg = ctx
-- If the ctx ends in a space, we need to append a "" to the carapace command
if ctx:sub(-1, -1):match("%s") ~= nil then
carapace_arg = carapace_arg .. '""'
end
-- Run carapace
local carapace_cmd = "carapace " .. fields[1] .. " export " .. carapace_arg
local res = io.popen(carapace_cmd)
local out = res:read("*all")
-- Extract completion items
local out_table = json.decode(out)
local values = {}
if out_table["values"] ~= nil then
for _, v in pairs(out_table["values"]) do
-- Some completion items are styled; some aren't.
if v["style"] ~= nil then
if v["description"] ~= nil then
values[v["value"]] = { M.fmt(v["description"], v["style"]) }
else
table.insert(values, v["value"])
end
else
if v["description"] ~= nil then
values[v["value"]] = { v["description"] }
else
table.insert(values, v["value"])
end
end
end
end
-- Construct completion items table
local comp = {
{
items = values,
type = "list"
}
}
return comp, query
end
return M

View file

@ -0,0 +1,54 @@
local M = {}
M.fish_completer = function(query, ctx, fields)
-- Ensure any quotes in input are properly escaped
ctx = ctx:gsub('"', '\\"')
ctx = ctx:gsub("'", "\\'")
-- Run fish
local cmd = "fish --command 'complete \"--do-complete=" .. ctx .. "\"'"
local res = io.popen(cmd)
local raw_str = res:read('*all')
-- Extract completion items
-- local values = table.map(string.split(raw_str, "\n"), function(v)
-- return string.split(v, "\t")
-- end)
local values = string.split(raw_str, "\n")
local comp_items = {}
for _, v in pairs(values) do
local _, count = string.gsub(v, " ", "")
if count > 0 then
local split = string.split(v, "\t")
comp_items[split[1]] = { split[2] }
else
comp_items[#comp_items+1] = v
end
end
-- for _, v in pairs(string.split(raw_str, "\n")) do
-- local comp_val = string.split(v, "\t")
-- values[comp_val[1]] = comp_val[2]
-- end
-- Only extract completions that start with given prefix
-- (fish doesn't consistently do this it seems)
-- if query ~= "" then
-- for i = #values,1,-1 do
-- if values[i]:find(query, 1, True) ~= 1 then
-- table.remove(values, i)
-- end
-- end
-- end
-- Construct completion items table
local comp = {
{
items = comp_items,
type = 'list'
}
}
return comp, query
end
return M

20
.config/hilbish/hinter.lua Executable file
View file

@ -0,0 +1,20 @@
-- Basic shell history based hinter.
-- It's a bit buggy, but it works (TODO: fix)
local hilbish = require "hilbish"
local M = {}
M.hinter = function(line)
local his = hilbish.history.all()
local his_vals = table.filter(his, function(v)
return v:find(line, 1, true) == 1
end)
if #his_vals ~= 0 then
local base_val = his_vals[#his_vals]
return string.sub(base_val, string.len(line) + 1, -1)
end
return ''
end
return M

9
.config/hilbish/homebrew.lua Executable file
View file

@ -0,0 +1,9 @@
-- Homebrew support
local hilbish = require("hilbish")
os.setenv("HOMEBREW_PREFIX", "/opt/homebrew")
os.setenv("HOMEBREW_REPOSITORY", "/opt/homebrew")
os.setenv("HOMEBREW_CELLAR", "/opt/homebrew")
hilbish.appendPath("/opt/homebrew/bin")
hilbish.appendPath("/opt/homebrew/sbin")

77
.config/hilbish/init.lua Executable file
View file

@ -0,0 +1,77 @@
local bait = require('bait')
local ansikit = require('ansikit')
local hilbish = require('hilbish')
-- For some reason, on MacOS Hilbish doesn't find lua modules correctly.
-- So, we need to tell it where to find stuff.
local home_dir = os.getenv("HOME")
package.path = home_dir .. "/.config/hilbish/?.lua;" .. package.path
package.path = home_dir .. "/.config/hilbish/vendor/?.lua;" .. package.path
-- Path
hilbish.appendPath('~/bin')
hilbish.appendPath('~/.cargo/bin')
hilbish.appendPath('~/.local/bin')
hilbish.appendPath('~/.nimble/bin')
hilbish.appendPath('~/.luarocks/bin')
hilbish.appendPath('~/.rye/shims')
hilbish.appendPath('/usr/local/bin')
hilbish.appendPath('/opt/local/bin')
local hinter = require('.hinter')
local syntax = require('.syntax')
local fish_completer = require('.fish_completer')
--local carapace_completer = require('.carapace')
local starship = require('.starship')
require('homebrew')
-- Shut up, hilbish
hilbish.opts['greeting'] = ''
hilbish.opts['motd'] = false
hilbish.opts['tips'] = false
-- Prompt (starship)
starship.setup()
-- Hooks
bait.catch('hilbish.vimMode', function(mode)
if mode ~= 'insert' then
ansikit.cursorStyle(ansikit.blockCursor)
else
ansikit.cursorStyle(ansikit.lineCursor)
end
end)
-- Vim mode
hilbish.inputMode("vim")
-- Aliases
hilbish.aliases.add('ls', 'lsd')
hilbish.aliases.add('la', 'exa --icons --all')
hilbish.aliases.add('ll', 'exa --long')
hilbish.aliases.add('lla', 'exa --long --all')
hilbish.aliases.add('lt', 'exa --icons --tree')
hilbish.aliases.add('lta', 'exa --icons --tree --all')
-- dotfile management
hilbish.aliases.add('dotfiles', 'git --git-dir=' .. home_dir .. '/workspace/dippin-dotfiles/ --work-tree=' .. home_dir .. '/')
-- Other environment variables
os.setenv("KAKOUNE_POSIX_SHELL", "/bin/dash")
os.setenv("CARAPACE_BRIDGES", "zsh,fish,bash,inshellisense")
-- Syntax highlighting
function hilbish.highlighter(line)
return syntax.sh(line)
end
-- Hinter
function hilbish.hinter(line, _)
return hinter.hinter(line)
end
-- Completers
hilbish.complete('command.git', fish_completer.fish_completer)
hilbish.complete('command.pacman', fish_completer.fish_completer)

64
.config/hilbish/starship.lua Executable file
View file

@ -0,0 +1,64 @@
-- Starship.rs support for Hilbish
-- Supports the following features:
-- * exit codes
-- * command duration (kind of - it usually works but is not 100% perfect)
-- * number of jobs
-- * continuation prompt (which some shells like fish don't even support!)
-- * anything else inherently handled by starship (git, username, etc)
local bait = require 'bait'
local hilbish = require 'hilbish'
local M = {}
M.execTime = {}
function M.doPrompt(status, num_jobs, num_secs)
-- Since lua by default only has second-resolution with its os.time() function,
-- we need to multiply by 1000 to get milliseconds elapsed.
-- This, of course, means we don't actually get millisecond-level resolution, but whatever - we work with what we got.
local time_ms = num_secs * 1000
local cmd = "starship prompt --status " .. status .. " --jobs " .. num_jobs .. " --cmd-duration " .. time_ms
local res = io.popen(cmd)
hilbish.prompt(res:read("*all"))
end
function M.doCommandExit(code, cmdStr)
local cur_time = os.time()
local time_elapsed = 0
if cmdStr ~= nil then
if M.execTime[cmdStr] ~= nil then
time_elapsed = cur_time - M.execTime[cmdStr]
end
M.execTime[cmdStr] = nil
end
local jobs = hilbish.jobs.all()
M.doPrompt(code, #jobs, time_elapsed)
end
function M.doCommandPreExec(input)
if input ~= nil then
M.execTime[input] = os.time()
end
end
function M.setup()
local cmd = "starship prompt --continuation"
local res = io.popen(cmd)
hilbish.multiprompt(res:read("*all"))
M.doPrompt(0, 0, 0)
bait.catch("command.preexec", function(input, _)
M.doCommandPreExec(input)
end)
bait.catch('command.exit', function(code, cmdStr)
M.doCommandExit(code, cmdStr)
end)
end
return M

192
.config/hilbish/syntax.lua Executable file
View file

@ -0,0 +1,192 @@
-- Syntax highlighting implementation for Hilbish
local colors = require "lunacolors"
local hilbish = require "hilbish"
local fs = require "fs"
local utils = require ".utils"
local M = {}
local SQ = 0x27 -- U+0027 APORSTROPHE
local DQ = 0x22 -- U+0022 QUOTATION MARK
local SP = 0x20 -- U+0020 SPACE
local HT = 0x09 -- U+0009 CHARACTER TABULATION
local LF = 0x0A -- U+000A LINE FEED (LF)
local CR = 0x0D -- U+000D CARRIAGE RETURN (CR)
local BS = 0x5C -- U+005C BACKSLASH
-- sh builtins
local sh_builtins = {
"admin", "aliasar", "asa", "at", "awk", "basename", "batch", "bc", "bg", "c99", "cal", "cat", "cd", "cflow", "chgrp",
"chmod",
"chown", "cksum", "cmp", "comm", "command", "compress", "cp", "crontab", "csplit", "ctags", "cut", "cxref", "date",
"dd",
"delta", "df", "diff", "dirname", "du", "echo", "ed", "env", "ex", "expand", "expr", "false", "fc", "fg", "file",
"find", "fold",
"fort77", "fuser", "gencat", "get", "getconf", "getopts", "grep", "hash", "head", "iconv", "id", "ipcrm", "ipcs",
"jobs",
"join", "kill", "lex", "link", "ln", "locale", "localedef", "logger", "logname", "lp", "ls", "m4", "mailx", "make",
"man",
"mesg", "mkdir", "mkfifo", "more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste", "patch", "pathchk",
"pax",
"pr", "printf", "prs", "ps", "pwd", "qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls", "qselect", "qsig",
"qstat", "qsub", "read", "renice", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "sh", "sleep", "sort", "split",
"strings", "strip", "stty", "tabs", "tail", "talk", "tee", "test", "time", "touch", "tput", "tr", "true", "tsort",
"tty", "type", "ulimit", "umask", "unalias", "uname", "uncompress", "unexpand", "unget", "uniq", "unlink",
"uucp", "uudecode", "uuencode", "uustat", "uux", "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc",
"zcat"
}
-- sh keywords
local sh_keywords = {
"if", "then", "else", "elif", "fi", "case", "esac", "for", "select", "while", "until", "do", "done", "in", "function",
"time", "coproc"
}
-- Determine if this string is a significant keyword, and if so, what it is
function M.is_cmd(str)
if utils.contains(sh_builtins, str) then
return "builtin"
elseif utils.contains(sh_keywords, str) then
return "keyword"
elseif hilbish.which(str) ~= nil then
return "cmd"
else
-- Try to use this as a filesystem glob
-- This may fail with an error, so we need to make sure that
-- doesn't happen first.
str = str:gsub("~", os.getenv("HOME"))
if pcall(fs.glob, str) then
local globs = fs.glob(str)
if #globs > 0 then
return "file"
end
end
end
return nil
end
-- Crude sh lexer
function M.lex_sh(s)
local state = nil
local escape = false
local result = {}
local point = 0
local index = 0
-- Go through the string
for i = 1, #s do
index = i
local c = s:byte(i)
local v = string.char(c)
-- Single quoted string state
if state == SQ then
if c == SQ then
state = nil
result[#result + 1] = { string.sub(s, point, i), "string" }
point = i + 1
end
-- Double quoted string state
elseif state == DQ then
if escape then
escape = false
else
if c == DQ then
state = nil
result[#result + 1] = { string.sub(s, point, i), "string" }
point = i + 1
elseif c == BS then
escape = true
end
end
-- Default state
else
if escape then
escape = false
elseif c == SP or c == HT or c == LF or c == CR then
if point ~= i then
result[#result + 1] = { string.sub(s, point, i - 1), "other" }
end
result[#result + 1] = { v, "whitespace" }
point = i + 1
elseif c == DQ then
state = DQ
if point ~= i then
result[#result + 1] = { string.sub(s, point, i - 1), "other" }
end
point = i
elseif c == SQ then
state = SQ
if point ~= i then
result[#result + 1] = { string.sub(s, point, i - 1), "other" }
end
point = i
elseif c == BS then
escape = true
end
end
end
-- If we are still in a string then say as such
if state ~= nil then
result[#result + 1] = { string.sub(s, point, index), "string" }
-- Otherwise, return the rest as a regular token
else
if point ~= index then
result[#result + 1] = { string.sub(s, point, index), "other" }
else
local c = s:byte(index)
-- If the final character isn't a space, render it anyway
-- This is a side effect of the space handling code. TODO: fix.
if c ~= SP and c ~= HT and c ~= LF and c ~= CR then
result[#result + 1] = { string.sub(s, point, index), "other" }
end
end
end
return result
end
-- Sort the table's key pairs
function M.pairsByKeys(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0
local iter = function()
i = i + 1
if a[i] == nil then
return nil
else
return a[i], t[a[i]]
end
end
return iter
end
-- sh highlighter
function M.sh(str)
-- Parse the string
local table = M.lex_sh(str)
-- Construct the format string
local format_str = ""
for _, v in M.pairsByKeys(table) do
if v[2] == "string" then
format_str = format_str .. "{red}" .. v[1]
else
local cmd = M.is_cmd(v[1])
if cmd == "cmd" then
format_str = format_str .. "{bold}{brightGreen}" .. v[1] .. "{reset}"
elseif cmd == "builtin" then
format_str = format_str .. "{bold}{brightBlue}" .. v[1] .. "{reset}"
elseif cmd == "keyword" then
format_str = format_str .. "{bold}{brightMagenta}" .. v[1] .. "{reset}"
elseif cmd == "file" then
format_str = format_str .. "{bold}{brightYellow}" .. v[1] .. "{reset}"
else
format_str = format_str .. "{brightWhite}" .. v[1]
end
end
end
return colors.format(format_str)
end
return M

20
.config/hilbish/utils.lua Executable file
View file

@ -0,0 +1,20 @@
-- Various utility functions.
local utils = {}
utils.contains = function(table, element)
for key, value in pairs(table) do
if type(key) == type(element) then
if key == element then
return true
end
else
if value == element then
return true
end
end
end
return false
end
return utils

388
.config/hilbish/vendor/json.lua vendored Executable file
View file

@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json