diff --git a/sdcard/3ds/ctruLua/editor/color.lua b/sdcard/3ds/ctruLua/editor/color.lua new file mode 100644 index 0000000..c162160 --- /dev/null +++ b/sdcard/3ds/ctruLua/editor/color.lua @@ -0,0 +1,6 @@ +-- Colors based on the Monokai theme +return { + background = 0x272822FF, + default = 0xF8F8F2FF, + cursor = 0xFF0000FF +} \ No newline at end of file diff --git a/sdcard/3ds/ctruLua/editor/main.lua b/sdcard/3ds/ctruLua/editor/main.lua new file mode 100644 index 0000000..1bf70ed --- /dev/null +++ b/sdcard/3ds/ctruLua/editor/main.lua @@ -0,0 +1,153 @@ +local ctr = require("ctr") +local hid = require("ctr.hid") +local gfx = require("ctr.gfx") + +-- Open libs +local keyboard = dofile("sdmc:/3ds/ctruLua/keyboard.lua") +local openfile = dofile("sdmc:/3ds/ctruLua/openfile.lua") +local color = dofile("sdmc:/3ds/ctruLua/editor/color.lua") + +-- Open file +local path, status = openfile("Choose a file to edit", "/3ds/ctruLua/", nil, "any") +if not path then return end +local lines = {} +if status == "exist" then + for line in io.lines(path) do table.insert(lines, line) end +else + lines = { "" } +end + +-- Variables +local lineHeight = 10 +local cursorX, cursorY = 1, 1 +local scrollX, scrollY = 0, 0 + +-- Set defaults +gfx.set3D(false) +gfx.color.setDefault(color.default) +gfx.color.setBackground(color.background) + +while ctr.run() do + hid.read() + local keys = hid.keys() + + -- Keys input + if keys.down.start then return end + + if keys.down.dRight then + cursorX = cursorX + 1 + if cursorX > utf8.len(lines[cursorY])+1 then + if cursorY < #lines then + cursorX, cursorY = 1, cursorY + 1 + else + cursorX = cursorX - 1 + end + end + end + if keys.down.dLeft then + if cursorX > utf8.len(lines[cursorY])+1 then cursorX = utf8.len(lines[cursorY])+1 end + cursorX = cursorX - 1 + if cursorX < 1 then + if cursorY > 1 then + cursorX, cursorY = utf8.len(lines[cursorY-1])+1, cursorY - 1 + else + cursorX = 1 + end + end + end + if keys.down.dUp and cursorY > 1 then cursorY = cursorY - 1 end + if keys.down.dDown and cursorY < #lines then cursorY = cursorY + 1 end + + if keys.held.cpadRight or keys.held.a then scrollX = scrollX + 3 end + if keys.held.cpadLeft or keys.held.y then scrollX = scrollX - 3 end + if keys.held.cpadUp or keys.held.x then scrollY = scrollY - 3 end + if keys.held.cpadDown or keys.held.b then scrollY = scrollY + 3 end + + if keys.down.select then + local file = io.open(path, "w") + if not file then + local t = os.time() + repeat + gfx.startFrame(gfx.GFX_TOP) + gfx.text(3, 3, "Can't open file in write mode") + gfx.endFrame() + gfx.render() + until t + 5 < os.time() + else + for i = 1, #lines, 1 do + file:write(lines[i].."\n") + gfx.startFrame(gfx.GFX_TOP) + gfx.rectangle(0, 0, math.ceil(i/#lines*gfx.TOP_WIDTH), gfx.TOP_HEIGHT, 0, 0xFFFFFFFF) + gfx.color.setDefault(0x000000FF) + gfx.text(gfx.TOP_WIDTH/2, gfx.TOP_HEIGHT/2, math.ceil(i/#lines*100).."%") + gfx.color.setDefault(color.default) + gfx.endFrame() + gfx.render() + end + file:flush() + file:close() + end + end + + -- Keyboard input + local input = keyboard.read() + if input then + if input == "BACK" then + if cursorX > utf8.len(lines[cursorY])+1 then cursorX = utf8.len(lines[cursorY])+1 end + if cursorX > 1 then + lines[cursorY] = lines[cursorY]:sub(1, utf8.offset(lines[cursorY], cursorX-1)-1).. + lines[cursorY]:sub(utf8.offset(lines[cursorY], cursorX), -1) + cursorX = cursorX - 1 + elseif cursorY > 1 then + cursorX, cursorY = utf8.len(lines[cursorY-1])+1, cursorY - 1 + lines[cursorY] = lines[cursorY]..lines[cursorY+1] + table.remove(lines, cursorY+1) + end + elseif input == "\n" then + local newline = lines[cursorY]:sub(utf8.offset(lines[cursorY], cursorX), -1) + local whitespace = lines[cursorY]:match("^%s+") + if whitespace then newline = whitespace .. newline end + + lines[cursorY] = lines[cursorY]:sub(1, utf8.offset(lines[cursorY], cursorX)-1) + table.insert(lines, cursorY + 1, newline) + + cursorX, cursorY = whitespace and #whitespace+1 or 1, cursorY + 1 + else + lines[cursorY] = lines[cursorY]:sub(1, utf8.offset(lines[cursorY], cursorX)-1)..input.. + lines[cursorY]:sub(utf8.offset(lines[cursorY], cursorX), -1) + cursorX = cursorX + 1 + end + end + + -- Draw + gfx.startFrame(gfx.GFX_TOP) + + local sI = math.floor(scrollY / lineHeight) + if sI < 1 then sI = 1 end + + local eI = math.ceil((scrollY + gfx.TOP_HEIGHT) / lineHeight) + if eI > #lines then eI = #lines end + + for i = sI, eI, 1 do + local line = lines[i] + local y = -scrollY+ (i-1)*lineHeight + + if cursorY == i then + gfx.color.setDefault(color.cursor) + gfx.text(-scrollX, y, line:sub(1, (utf8.offset(line, cursorX) or 0)-1):gsub("\t", " ").."|", nil) -- TODO: color doesn't work + gfx.color.setDefault(color.default) + end + + gfx.text(-scrollX, y, line:gsub("\t", " "), nil) + end + + gfx.endFrame() + + gfx.startFrame(gfx.GFX_BOTTOM) + + keyboard.draw(5, 115) + + gfx.endFrame() + + gfx.render() +end diff --git a/sdcard/3ds/ctruLua/keyboard.lua b/sdcard/3ds/ctruLua/keyboard.lua new file mode 100644 index 0000000..e97b9c5 --- /dev/null +++ b/sdcard/3ds/ctruLua/keyboard.lua @@ -0,0 +1,106 @@ +local hid = require("ctr.hid") +local gfx = require("ctr.gfx") + +-- Options +local keyWidth, keyHeight = 25, 25 +local layout = { + ["default"] = { + { "&", "é", "\"", "'", "(", "-", "è", "_", "ç", "à", ")", "=", "Back" }, + { "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "$", "Enter" }, + { "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "ù", "*", "Enter" }, + { "Shift", "<", "w", "x", "c", "v", "b", "n", ",", ";", ":", "!", "Tab" }, + { "CpLck", ">", "+", "/", " ", " ", " ", " ", " ", "{", "}", ".", "AltGr" } + }, + ["Shift"] = { + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "°", "+", "Back" }, + { "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", "¨", "£", "Enter" }, + { "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", "%", "µ", "Enter" }, + { "Shift", ">", "W", "X", "C", "V", "B", "N", "?", ".", "/", "§", "Tab" }, + { "CpLck", "~", "#", "[", " ", " ", " ", " ", " ", "]", "|", "@", "AltGr" } + }, + ["AltGr"] = { + { "²", "~", "#", "{", "[", "|", "`", "\\", "^", "@", "]", "}", "Back" }, + { "a", "z", "€", "r", "t", "y", "u", "i", "o", "p", "", "¤", "Enter" }, + { "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "", "", "Enter" }, + { "Shift", "", "w", "x", "c", "v", "b", "n", "", "", "", "", "Tab" }, + { "CpLck", "", "", "", " ", " ", " ", " ", " ", "", "", "", "AltGr" } + }, +} +local alias = { + ["Tab"] = "\t", + ["Enter"] = "\n", + ["Back"] = "BACK" +} +local sticky = { + ["CpLck"] = "Shift" +} +local keys = { + ["l"] = "Shift", + ["r"] = "Shift" +} + +-- Variables +local currentModifier = { "default", "sticky" } +local buffer = "" + +return { + draw = function(x, y) + local hidKeys = hid.keys() + + local xTouch, yTouch + if hidKeys.down.touch then xTouch, yTouch = hid.touch() end + + for key, modifier in pairs(keys) do + if hidKeys.down[key] then + currentModifier = { modifier, "key" } + elseif hidKeys.up[key] and currentModifier[2] == "key" and currentModifier[1] == modifier then + currentModifier = { "default", "sticky" } + end + end + + for row, rowKeys in pairs(layout[currentModifier[1]]) do + for column, key in pairs(rowKeys) do + local xKey, yKey = x + (column-1)*(keyWidth-1), y + (row-1)*(keyHeight-1) + + gfx.rectangle(xKey, yKey, keyWidth, keyHeight, 0, 0xFFFFFFFF) + gfx.rectangle(xKey + 1, yKey + 1, keyWidth - 2, keyHeight - 2, 0, 0x000000FF) + gfx.text(xKey + 2, yKey + 2, key) + + if xTouch then + if xTouch > xKey and xTouch < xKey + keyWidth then + if yTouch > yKey and yTouch < yKey + keyHeight then + gfx.rectangle(xKey, yKey, keyWidth, keyHeight, 0, 0xFFFFFFDD) + + local k = alias[key] or key + if sticky[k] and layout[sticky[k]] then + if currentModifier[1] == sticky[k] and currentModifier[2] == "sticky" then + currentModifier = { "default", "sticky" } + else + currentModifier = { sticky[k], "sticky" } + end + elseif layout[k] then + if currentModifier[1] == k and currentModifier[2] == "normal" then + currentModifier = { "default", "sticky" } + else + currentModifier = { k, "normal" } + end + else + buffer = buffer .. k + if currentModifier[1] ~= "default" and currentModifier[2] == "normal" then + currentModifier = { "default", "sticky" } + end + end + end + end + end + end + end + end, + + read = function() + local ret = buffer + buffer = "" + + return ret ~= "" and ret or nil + end +} \ No newline at end of file diff --git a/sdcard/3ds/ctruLua/main.lua b/sdcard/3ds/ctruLua/main.lua index 3bdca31..c4cffda 100644 --- a/sdcard/3ds/ctruLua/main.lua +++ b/sdcard/3ds/ctruLua/main.lua @@ -1,64 +1,4 @@ -local ctr = require("ctr") -local gfx = require("ctr.gfx") - -local sel = 1 -local scroll = 0 -local curdir = "/" -local files = ctr.fs.list(curdir) - -while ctr.run() do - ctr.hid.read() - local keys = ctr.hid.keys() - if keys.down.start then break end - - if keys.down.down and sel < #files then - sel = sel + 1 - if sel > scroll + 14 then scroll = scroll + 1 end - elseif keys.down.up and sel > 1 then - sel = sel - 1 - if sel <= scroll then scroll = scroll - 1 end - end - - if keys.down.a then - local f = files[sel] - - if f.isDirectory then - if f.name == ".." then curdir = curdir:gsub("[^/]+/$", "") - else curdir = curdir..f.name.."/" end - - sel = 1 - scroll = 0 - files = ctr.fs.list(curdir) - - if curdir ~= "/" then - table.insert(files, 1, { name = "..", isDirectory = true, fileSize = "parent directory" }) - end - else - if f.name:match("%..+$") == ".lua" then - dofile(curdir..f.name) - -- reset things the script could have changed - gfx.set3D(false) - gfx.color.setDefault(0xFFFFFFFF) - gfx.color.setBackground(0x000000FF) - end - end - end - - gfx.startFrame(gfx.GFX_TOP) - - gfx.rectangle(0, 10+(sel-scroll)*15, gfx.TOP_WIDTH, 15, 0, gfx.color.RGBA8(0, 0, 200)) - - for i = scroll+1, scroll+14, 1 do - local f = files[i] - if not f then break end - local name = f.isDirectory and "["..f.name.."]" or f.name.." ("..f.fileSize.."b)" - if not f.isHidden then gfx.text(5, 12+(i-scroll)*15, name) end - end - - gfx.rectangle(0, 0, gfx.TOP_WIDTH, 25, 0, gfx.color.RGBA8(200, 200, 200)) - gfx.text(3, 3, curdir, 13, gfx.color.RGBA8(0, 0, 0)) - - gfx.endFrame() - - gfx.render() -end \ No newline at end of file +repeat + local file = dofile("sdmc:/3ds/ctruLua/openfile.lua")("Choose a Lua file to execute", "/3ds/ctruLua/", ".lua", "exist") + if file then dofile(file) end +until not file \ No newline at end of file diff --git a/sdcard/3ds/ctruLua/openfile.lua b/sdcard/3ds/ctruLua/openfile.lua new file mode 100644 index 0000000..82a04a4 --- /dev/null +++ b/sdcard/3ds/ctruLua/openfile.lua @@ -0,0 +1,139 @@ +--- Open a file explorer to select a file. +-- string title: title of the file explorer. +-- string curdir: the directory to initially open the file explorer in. +-- string exts: the file extensions the user can select, separated by ";". If nil, all extensions are accepted. +-- string type: "exist" to select an existing file, "new" to select an non-existing file or "any" to select a existing +-- or non-existing file name. If nil, defaults to "exist". +-- returns string: the file the user has selected, or nil if the explorer was closed without selecting a file. +-- string: "exist" if the file exist or "new" if it doesn't +return function(title, curdir, exts, type) + -- Open libs + local ctr = require("ctr") + local gfx = require("ctr.gfx") + + local keyboard = dofile("sdmc:/3ds/ctruLua/keyboard.lua") + + -- Arguments + local type = type or "exist" + + -- Variables + local sel = 1 + local scroll = 0 + local files = ctr.fs.list(curdir) + if curdir ~= "/" then table.insert(files, 1, { name = "..", isDirectory = true }) end + local newFileName = "" + + local ret = nil + + -- Remember and set defaults + --local was3D = gfx.get3D() TODO: implement this thing in ctruLua + local wasDefault = gfx.color.getDefault() + local wasBackground = gfx.color.getBackground() + gfx.set3D(false) + gfx.color.setDefault(0xFFFFFFFF) + gfx.color.setBackground(0x000000FF) + + while ctr.run() do + ctr.hid.read() + local keys = ctr.hid.keys() + if keys.down.start then break end + + -- Keys input + if keys.down.down and sel < #files then + sel = sel + 1 + if sel > scroll + 14 then scroll = scroll + 1 end + elseif keys.down.up and sel > 1 then + sel = sel - 1 + if sel <= scroll then scroll = scroll - 1 end + end + + if keys.down.a then + local f = files[sel] + + if f.isDirectory then + if f.name == ".." then curdir = curdir:gsub("[^/]+/$", "") + else curdir = curdir..f.name.."/" end + + sel = 1 + scroll = 0 + files = ctr.fs.list(curdir) + + if curdir ~= "/" then + table.insert(files, 1, { name = "..", isDirectory = true }) + end + elseif type == "exist" or type == "any" then + if exts then + for ext in (exts..";"):gmatch("[^;]+") do + if f.name:match("%..+$") == ext then + ret = { curdir..f.name, "exist" } + break + end + end + else + ret = { curdir..f.name, "exist" } + end + if ret then break end + end + end + + -- Keyboard input + if type == "new" or type == "any" then + local input = keyboard.read() + if input then + if input == "BACK" then + newFileName = newFileName:sub(1, (utf8.offset(newFileName, -1) or 0)-1) + elseif input == "\n" then + local fileStatus = "new" + local f = io.open(curdir..newFileName) + if f then fileStatus = "exist" f:close() end + ret = { curdir..newFileName, fileStatus } + break + else + newFileName = newFileName..input + end + end + end + + -- Draw + gfx.startFrame(gfx.GFX_TOP) + + gfx.rectangle(0, 10+(sel-scroll)*15, gfx.TOP_WIDTH, 15, 0, gfx.color.RGBA8(0, 0, 200)) + + for i = scroll+1, scroll+14, 1 do + local f = files[i] + if not f then break end + local name = f.isDirectory and "["..f.name.."]" or f.name.." ("..f.fileSize.."b)" + if not f.isHidden then gfx.text(5, 12+(i-scroll)*15, name) end + end + + gfx.rectangle(0, 0, gfx.TOP_WIDTH, 25, 0, gfx.color.RGBA8(200, 200, 200)) + gfx.text(3, 3, curdir, 13, gfx.color.RGBA8(0, 0, 0)) + + gfx.endFrame() + + gfx.startFrame(gfx.GFX_BOTTOM) + + gfx.text(5, 5, title) + gfx.text(5, 20, "Accepted file extensions: "..(exts or "all")) + + if type == "new" or type == "any" then + gfx.text(5, 90, newFileName) + keyboard.draw(5, 115) + end + + gfx.endFrame() + + gfx.render() + end + + -- Reset defaults + --gfx.set3D(was3D) + gfx.color.setDefault(wasDefault) + gfx.color.setBackground(wasBackground) + + if ret then + return table.unpack(ret) + else + return ret + end +end