-- license:MIT -- copyright-holders:Carl, Patrick Rapin, Reuben Thomas -- completion from https://github.com/rrthomas/lua-rlcompleter local exports = {} exports.name = "console" exports.version = "0.0.1" exports.description = "Console plugin" exports.license = "BSD-3-Clause" exports.author = { name = "Carl" } local console = exports local history_file = "console_history" local history_fullpath = nil function console.startplugin() local conth = emu.thread() local ln_started = false local started = false local stopped = false local ln = require("linenoise") local preload = false local matches = {} local lastindex = 0 local consolebuf _G.history = function (index) local history = ln.historyget() if index then ln.preload(history[index]) return end for num, line in ipairs(history) do print(num, line) end end print(" /| /| /| /| /| _______") print(" / | / | / | / | / | / /") print(" / |/ | / | / |/ | / ____/ ") print(" / | / | / | / /_ ") print(" / |/ | / |/ __/ ") print(" / /| /| /| |/ /| /| /____ ") print(" / / | / | / | / | / | / ") print("/ _/ |/ / / |___/ |/ /_______/ ") print(" / / ") print(" / _/ \n") print(emu.app_name() .. " " .. emu.app_version(), "\nCopyright (C) Nicola Salmoria and the MAME team\n"); print(_VERSION, "\nCopyright (C) Lua.org, PUC-Rio\n"); -- linenoise isn't thread safe but that means history can handled here -- that also means that bad things will happen if anything outside lua tries to use it -- especially the completion callback ln.historysetmaxlen(50) local scr = [[ local ln = require('linenoise') ln.setcompletion(function(c, str, pos) status = str .. "\x01" .. tostring(pos) yield() ln.addcompletion(c, status:match("([^\x01]*)\x01(.*)")) end) local ret = ln.linenoise('$PROMPT') if ret == nil then return "\n" end return ret ]] local keywords = { 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while' } local cmdbuf = "" -- Main completion function. It evaluates the current sub-expression -- to determine its type. Currently supports tables fields, global -- variables and function prototype completion. local function contextual_list(expr, sep, str, word, strs) local function add(value) value = tostring(value) if value:match("^" .. word) then matches[#matches + 1] = value end end -- This function is called in a context where a keyword or a global -- variable can be inserted. Local variables cannot be listed! local function add_globals() for _, k in ipairs(keywords) do add(k) end for k in pairs(_G) do add(k) end end if expr and expr ~= "" then local v = load("local STRING = {'" .. table.concat(strs,"','") .. "'} return " .. expr) if v then err, v = pcall(v) if (not err) or (not v) then add_globals() return end local t = type(v) if sep == '.' or sep == ':' then if t == 'table' then for k, v in pairs(v) do if type(k) == 'string' and (sep ~= ':' or type(v) == "function") then add(k) end end elseif t == 'userdata' then for k, v in pairs(getmetatable(v)) do if type(k) == 'string' and (sep ~= ':' or type(v) == "function") then add(k) end end end elseif sep == '[' then if t == 'table' then for k in pairs(v) do if type(k) == 'number' then add(k .. "]") end end if word ~= "" then add_globals() end end end end end if #matches == 0 then add_globals() end end local function find_unmatch(str, openpar, pair) local done = false if not str:match(openpar) then return str end local tmp = str:gsub(pair, "") if not tmp:match(openpar) then return str end repeat str = str:gsub(".-" .. openpar .. "(.*)", function (s) tmp = s:gsub(pair, "") if not tmp:match(openpar) then done = true end return s end) until done or str == "" return str end -- This complex function tries to simplify the input line, by removing -- literal strings, full table constructors and balanced groups of -- parentheses. Returns the sub-expression preceding the word, the -- separator item ( '.', ':', '[', '(' ) and the current string in case -- of an unfinished string literal. local function simplify_expression(expr, word) local strs = {} -- Replace annoying sequences \' and \" inside literal strings expr = expr:gsub("\\(['\"])", function (c) return string.format("\\%03d", string.byte(c)) end) local curstring -- Remove (finished and unfinished) literal strings while true do local idx1, _, equals = expr:find("%[(=*)%[") local idx2, _, sign = expr:find("(['\"])") if idx1 == nil and idx2 == nil then break end local idx, startpat, endpat if (idx1 or math.huge) < (idx2 or math.huge) then idx, startpat, endpat = idx1, "%[" .. equals .. "%[", "%]" .. equals .. "%]" else idx, startpat, endpat = idx2, sign, sign end if expr:sub(idx):find("^" .. startpat .. ".-" .. endpat) then expr = expr:gsub(startpat .. "(.-)" .. endpat, function (str) strs[#strs + 1] = str return " STRING[" .. #strs .. "] " end) else expr = expr:gsub(startpat .. "(.*)", function (str) curstring = str return "(CURSTRING " end) end end -- crop string at unmatched open paran expr = find_unmatch(expr, "%(", "%b()") expr = find_unmatch(expr, "%[", "%b[]") --expr = expr:gsub("%b()"," PAREN ") -- Remove groups of parentheses expr = expr:gsub("%b{}"," TABLE ") -- Remove table constructors -- Avoid two consecutive words without operator expr = expr:gsub("(%w)%s+(%w)","%1|%2") expr = expr:gsub("%s+", "") -- Remove now useless spaces -- This main regular expression looks for table indexes and function calls. return curstring, strs, expr:match("([%.:%w%(%)%[%]_]-)([:%.%[%(])" .. word .. "$") end local function get_completions(line, endpos) matches = {} local endstr = line:sub(endpos + 1, -1) line = line:sub(1, endpos) endstr = endstr or "" local start, word = line:match("^(.*[ \t\n\"\\'><=;:%+%-%*/%%^~#{}%(%)%[%].,])(.-)$") if not start then start = "" word = word or line else word = word or "" end local str, strs, expr, sep = simplify_expression(line, word) contextual_list(expr, sep, str, word, strs) if #matches > 1 then print("\n") for k, v in pairs(matches) do print(v) end return "\x01" .. "-1" elseif #matches == 1 then return start .. matches[1] .. endstr .. "\x01" .. (#start + #matches[1]) end return "\x01" .. "-1" end emu.register_start(function() if not consolebuf and manager.machine.debugger then consolebuf = manager.machine.debugger.consolelog lastindex = 0 end end) emu.register_stop(function() consolebuf = nil end) emu.register_periodic(function() if stopped then return end if (not started) then -- options are not available in startplugin, so we load the history here local homepath = emu.subst_env(manager.options.entries.homepath:value():match("([^;]+)")) history_fullpath = homepath .. '/' .. history_file ln.loadhistory(history_fullpath) started = true end local prompt = "\x1b[1;36m[MAME]\x1b[0m> " if consolebuf and (#consolebuf > lastindex) then local last = #consolebuf print("\n") while lastindex < last do lastindex = lastindex + 1 print(consolebuf[lastindex]) end ln.refresh() end if conth.yield then conth:continue(get_completions(conth.result:match("([^\x01]*)\x01(.*)"))) return elseif conth.busy then return elseif ln_started then local cmd = conth.result if cmd == "\n" then stopped = true return elseif cmd == "" then if cmdbuf ~= "" then print("Incomplete command") cmdbuf = "" end else cmdbuf = cmdbuf .. "\n" .. cmd local func, err = load(cmdbuf) if not func then if err:match("") then prompt = "\x1b[1;36m[MAME]\x1b[0m>> " else print("error: ", err) cmdbuf = "" end else local status status, err = pcall(func) if not status then print("error: ", err) end cmdbuf = "" end ln.historyadd(cmd) end end conth:start(scr:gsub("$PROMPT", prompt)) ln_started = true end) end setmetatable(console, { __gc = function () if history_fullpath then local ln = require("linenoise") ln.savehistory(history_fullpath) end end}) return exports