--[[ /*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2017 William Wilgus * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ ]] if ... == nil then rb.splash(rb.HZ * 3, "use 'require'") end require("printtable") local _clr = require("color") local _lcd = require("lcd") local _print = require("print") local _timer = require("timer") require("actions") local CANCEL_BUTTON = rb.actions.PLA_CANCEL -------------------------------------------------------------------------------- -- builds an index of byte position of every line at each bufsz increment -- in filename; bufsz == 1 would be every line; saves to filename.ext.idx_ext -- lnbyte should be nil for text files and number of bytes per line for binary local function build_file_index(filename, idx_ext, bufsz, lnbyte) if not filename then return end local file = io.open('/' .. filename, "r") --read if not file then _lcd:splashf(100, "Can't open %s", filename) return end local fsz = file:seek("end") local fsz_kb = fsz / 1024 local count local ltable = {0} --first index is the beginning of the file local timer = _timer() local fread _lcd:splashf(100, "Indexing file %d Kb", (fsz / 1024)) if lnbyte then fread = function(f) return f:read(lnbyte) end else lnbyte = -1 fread = function(f) return f:read("*l") end end file:seek("set", 0) for i = 1, fsz do if i % bufsz == 0 then local loc = file:seek() ltable[#ltable + 1] = loc _lcd:splashf(1, "Parsing %d of %d Kb", loc / 1024, fsz_kb) end if rb.get_plugin_action(0) == CANCEL_BUTTON then return end if not fread(file) then count = i break end end local fileidx = io.open('/' .. filename .. idx_ext, "w+") -- write/erase if fileidx then fileidx:write(fsz .. "\n") fileidx:write(count .. "\n") fileidx:write(bufsz .. "\n") fileidx:write(lnbyte .. "\n") fileidx:write(table.concat(ltable, "\n")) fileidx:close() _lcd:splashf(100, "Finished in %d seconds", timer.stop() / rb.HZ) collectgarbage("collect") else error("unable to save index file") end end -- build_file_index -------------------------------------------------------------------------------- --- returns size of original file, total lines buffersize, and table filled -- with line offsets in index file -> filename local function load_index_file(filename) local filesz, count, bufsz, lnbyte local ltable local fileidx = io.open('/' .. filename, "r") --read if fileidx then local idx = -3 ltable = {} fileidx:seek("set", 0) for line in fileidx:lines() do if idx == -3 then filesz = tonumber(line) elseif idx == -2 then count = tonumber(line) elseif idx == -1 then bufsz = tonumber(line) elseif idx == 0 then lnbyte = tonumber(line) else ltable[idx] = tonumber(line) end idx = idx + 1 end fileidx:close() end return lnbyte, filesz, count, bufsz, ltable end -- load_index_file -------------------------------------------------------------------------------- -- creates a fixed index with fixed line lengths, perfect for viewing hex files -- not so great for reading text files but works as a fallback local function load_fixed_index(bytesperline, filesz, bufsz) local lnbyte = bytesperline local count = (filesz + lnbyte - 1) / lnbyte + 1 local idx_t = {} -- build index for i = 0, filesz, bufsz do idx_t[#idx_t + 1] = lnbyte * i end return lnbyte, filesz, count, bufsz, idx_t end -- load_fixed_index -------------------------------------------------------------------------------- -- uses print_table to display a whole file function print_file(filename, maxlinelen, settings) if not filename then return end local file = io.open('/' .. filename or "", "r") --read if not file then _lcd:splashf(100, "Can't open %s", filename) return end maxlinelen = 33 local hstr = filename local ftable = {} table.insert(ftable, 1, hstr) local tline = #ftable + 1 local remln = maxlinelen local posln = 1 for line in file:lines() do if line then if maxlinelen then if line == "" then ftable[tline] = ftable[tline] or "" tline = tline + 1 remln = maxlinelen else line = line:match("%w.+") or "" end local linelen = line:len() while linelen > 0 do local fsp = line:find("%s", posln + remln - 5) or 0x0 fsp = fsp - (posln + remln) if fsp >= 0 then local fspr = fsp fsp = line:find("%s", posln + remln) or linelen fsp = fsp - (posln + remln) if math.abs(fspr) < fsp then fsp = fspr end end if fsp > 5 or fsp < -5 then fsp = 0 end local str = line:sub(posln, posln + remln + fsp) local slen = str:len() ftable[tline] = ftable[tline] or "" ftable[tline] = ftable[tline] .. str linelen = linelen - slen if linelen > 0 then tline = tline + 1 posln = posln + slen remln = maxlinelen --loop continues else ftable[tline] = ftable[tline] .. " " remln = maxlinelen - slen posln = 1 --loop ends end end else ftable[#ftable + 1] = line end end end file:close() _lcd:clear() _print.clear() if not settings then settings = {} settings.justify = "center" settings.wrap = true settings.msel = true end settings.hasheader = true settings.co_routine = nil local sel = print_table(ftable, #ftable, settings) _lcd:splashf(rb.HZ * 2, "%d items {%s}", #sel, table.concat(sel, ", ")) ftable = nil end -- print_file -------------------------------------------------------------------------------- -- uses print_table to display a portion of a file function print_file_increment(filename, settings) if not filename then return end local file = io.open('/' .. filename, "r") --read if not file then _lcd:splashf(100, "Can't open %s", filename) return end local fsz = file:seek("end") local bsz = 1023 --if small file do it the easier way and load whole file to table if fsz < 60 * 1024 then file:close() print_file(filename, settings) return end local ext = ".idx" local lnbyte, filesz, count, bufsz, idx_t = load_index_file(filename .. ext) if not idx_t or fsz ~= filesz then -- build file index build_file_index(filename, ext, bsz) lnbyte, filesz, count, bufsz, idx_t = load_index_file(filename .. ext) end -- if invalid or user canceled creation fallback to a fixed index if not idx_t or fsz ~= filesz or count <= 0 then _lcd:splashf(rb.HZ * 5, "Unable to read file index %s", filename .. ext) lnbyte, filesz, count, bufsz, idx_t = load_fixed_index(32, fsz, bsz) end if not idx_t or fsz ~= filesz or count <= 0 then _lcd:splashf(rb.HZ * 5, "Unable to load file %s", filename) return end local hstr = filename local file_t = setmetatable({},{__mode = "kv"}) --weak keys and values -- this allows them to be garbage collected as space is needed -- rebuilds when needed local ovf = 0 local lpos = 1 local timer = _timer() file:seek("set", 0) function print_co() while true do collectgarbage("step") file_t[1] = hstr --position 1 is ALWAYS header/title for i = 1, bufsz + ovf do file_t[lpos + i] = file:read ("*l") end ovf = 0 lpos = lpos + bufsz local bpos = coroutine.yield() if bpos <= lpos then -- roll over or scroll up bpos = (bpos - bufsz) + bpos % bufsz timer:check(true) end lpos = bpos - bpos % bufsz if lpos < 1 then lpos = 1 elseif lpos > count - bufsz then -- partial fill ovf = count - bufsz - lpos end --get position in file of the nearest indexed line file:seek("set", idx_t[bpos / bufsz + 1]) -- on really large files if it has been more than 10 minutes -- since the user scrolled up the screen wipe out the prior -- items to free memory if lpos % 5000 == 0 and timer:check() > rb.HZ * 600 then for i = 1, lpos - 100 do file_t[i] = nil end end end end co = coroutine.create(print_co) _lcd:clear() _print.clear() if not settings then settings = {} settings.justify = "center" settings.wrap = true end settings.hasheader = true settings.co_routine = co settings.msel = false table.insert(file_t, 1, hstr) --position 1 is header/title local sel = print_table(file_t, count, settings) file:close() idx_t = nil file_t = nil return sel end --print_file_increment -------------------------------------------------------------------------------- function print_file_hex(filename, bytesperline, settings) if not filename then return end local file = io.open('/' .. filename, "r") --read if not file then _lcd:splashf(100, "Can't open %s", filename) return end local hstr = filename local bpl = bytesperline local fsz = file:seek("end") --[[ local filesz = file:seek("end") local bufsz = 1023 local lnbyte = bytesperline local count = (filesz + lnbyte - 1) / lnbyte + 1 local idx_t = {} -- build index for i = 0, filesz, bufsz do idx_t[#idx_t + 1] = lnbyte * i end]] local lnbyte, filesz, count, bufsz, idx_t = load_fixed_index(bpl, fsz, 1023) local file_t = setmetatable({},{__mode = "kv"}) --weak keys and values -- this allows them to be garbage collected as space is needed -- rebuilds when needed local ovf = 0 local lpos = 1 local timer = _timer() file:seek("set", 0) function hex_co() while true do collectgarbage("step") file_t[1] = hstr --position 1 is ALWAYS header/title for i = 1, bufsz + ovf do local pos = file:seek() local s = file:read (lnbyte) if not s then -- EOF file_t[lpos + i] = "" break; end local s_len = s:len() if s_len > 0 then local fmt = "0x%04X: " .. string.rep("%02X ", s_len) local schrs = " " .. s:gsub("(%c)", " . ") file_t[lpos + i] = string.format(fmt, pos, s:byte(1, s_len)) .. schrs else file_t[lpos + i] = string.format("0x%04X: ", pos) end end ovf = 0 lpos = lpos + bufsz local bpos = coroutine.yield() if bpos < lpos then -- roll over or scroll up bpos = (bpos - bufsz) + bpos % bufsz timer:check(true) end lpos = bpos - bpos % bufsz if lpos < 1 then lpos = 1 elseif lpos > count - bufsz then -- partial fill ovf = count - bufsz - lpos end --get position in file of the nearest indexed line file:seek("set", idx_t[bpos / bufsz + 1]) -- on really large files if it has been more than 10 minutes -- since the user scrolled up the screen wipe out the prior -- items to free memory if lpos % 10000 == 0 and timer:check() > rb.HZ * 600 then for i = 1, lpos - 100 do file_t[i] = nil end end end end co = coroutine.create(hex_co) local function repl(char) local ret = "" if char:sub(1,2) == "0x" then return string.format("%dd:", tonumber(char:sub(3, -2), 16)) else return string.format("%03d ", tonumber(char, 16)) end end _lcd:clear() _print.clear() local sel, start, vcur = 1 table.insert(file_t, 1, hstr) --position 1 is header/title if not settings then settings = {} settings.justify = "left" settings.wrap = true settings.msel = false settings.hfgc = _clr.set( 0, 000, 000, 000) settings.hbgc = _clr.set(-1, 255, 255, 255) settings.ifgc = _clr.set(-1, 255, 255, 255) settings.ibgc = _clr.set( 0, 000, 000, 000) settings.iselc = _clr.set( 1, 000, 200, 100) end settings.hasheader = true settings.co_routine = co settings.start = start settings.curpos = vcur while sel > 0 do settings.start = start settings.curpos = vcur sel, start, vcur = print_table(file_t, count, settings) if sel > 1 and file_t[sel] then -- flips between hex and decimal local s = file_t[sel] if s:sub(-1) == "\b" then file_t[sel] = nil ovf = -(bufsz - 1) coroutine.resume(co, sel) --rebuild this item else s = s:gsub("(0x%x+:)", repl) .. "\b" file_t[sel] = s:gsub("(%x%x%s)", repl) .. "\b" end end end file:close() idx_t = nil file_t = nil return sel end -- print_file_hex --------------------------------------------------------------------------------