--- love_file_handler.lua — LÖVE2D file handler for logger.lua -- -- Uses love.filesystem to write logs into LÖVE's save directory, which works -- correctly on every platform LÖVE supports (Windows, macOS, Linux, Android). -- -- Requires: -- logger.lua in the same directory (or on package.path) -- LÖVE2D 11.x+ (love.filesystem must be available) -- -- Usage inside main.lua / conf.lua: -- -- local Logger = require("logger") -- local LoveHandler = require("love_file_handler") -- -- local log = Logger.new("game", { -- handlers = { -- Logger.handlers.ConsoleHandler(), -- print() fallback -- LoveHandler.new("logs/game.log"), -- love.filesystem -- }, -- }) -- -- function love.load() -- log:info("Game started — LÖVE %s", love.getVersion()) -- end -- ── Sanity check ────────────────────────────────────────────────────────── assert(love and love.filesystem, "love_file_handler requires the LÖVE2D runtime (love.filesystem)") -- ── Module table ────────────────────────────────────────────────────────── local LoveHandler = {} -- ── Helpers ─────────────────────────────────────────────────────────────── --- Ensure every directory component in `path` exists. -- love.filesystem.createDirectory is recursive on LÖVE 11+. local function ensure_dir(path) local dir = path:match("^(.*)/[^/]+$") if dir and dir ~= "" then love.filesystem.createDirectory(dir) end end --- Return current file size in bytes, or 0 on error / missing file. local function file_size(path) local info = love.filesystem.getInfo(path) return info and info.size or 0 end --- Shift rotated files: .4→.5, .3→.4, …, base→.1 local function rotate(path, max_files) for i = max_files - 1, 1, -1 do local src = path .. "." .. i local dest = path .. "." .. (i + 1) if love.filesystem.getInfo(src) then -- love.filesystem has no rename; copy + delete local data = love.filesystem.read(src) if data then love.filesystem.write(dest, data) love.filesystem.remove(src) end end end -- move base → .1 if love.filesystem.getInfo(path) then local data = love.filesystem.read(path) if data then love.filesystem.write(path .. ".1", data) love.filesystem.remove(path) end end end -- ── Constructor ─────────────────────────────────────────────────────────── --- Create a new LÖVE2D file handler. -- -- @param path string Path inside the LÖVE save directory. -- Subdirectories are created automatically. -- Default: "logs/game.log" -- -- @param opts table Optional settings: -- max_size (number) Rotate when the log file reaches this many bytes. -- Default: 1 MB (1 048 576). Set to 0 to disable. -- max_files (number) How many rotated files to keep. Default: 5. -- buffered (bool) Accumulate lines in memory and flush in batches. -- Reduces I/O overhead on mobile / web exports. -- Default: false. -- flush_size (number) Lines to buffer before auto-flushing (buffered mode). -- Default: 20. -- on_error (func) Called with (err_string) on any I/O error. -- Default: prints a warning to love.graphics console. -- -- @return handler, control_table -- handler — function accepted by Logger:add_handler() -- control — table with :flush(), :close(), :path(), :size() methods function LoveHandler.new(path, opts) path = path or "logs/game.log" opts = opts or {} local max_size = (opts.max_size == nil) and (1024 * 1024) or opts.max_size local max_files = opts.max_files or 5 local buffered = opts.buffered or false local flush_size = opts.flush_size or 20 local on_error = opts.on_error or function(err) if love.graphics and love.graphics.print then print("[love_file_handler] I/O error: " .. tostring(err)) end end -- State local buffer = {} -- pending lines (buffered mode) local closed = false ensure_dir(path) -- ── Internal write ──────────────────────────────────────────────────── local function raw_write(lines) if #lines == 0 then return end local text = table.concat(lines, "\n") .. "\n" -- Rotate if needed if max_size and max_size > 0 and file_size(path) + #text >= max_size then rotate(path, max_files) end -- Append local ok, err = love.filesystem.append(path, text) if not ok then on_error(tostring(err)) end end -- ── Public control table ────────────────────────────────────────────── local control = {} --- Flush buffered lines to disk immediately. function control:flush() if #buffer > 0 then raw_write(buffer) buffer = {} end end --- Close the handler (flushes first). After close() the handler is a no-op. function control:close() self:flush() closed = true end --- Full path as seen by love.filesystem. function control:path() return path end --- Real filesystem path to the save directory file (for external tools). function control:real_path() local save_dir = love.filesystem.getSaveDirectory() return save_dir .. "/" .. path end --- Current log file size in bytes. function control:size() return file_size(path) end --- List rotated files that exist (e.g. {"logs/game.log.1", ...}) function control:rotated_files() local result = {} for i = 1, max_files do local p = path .. "." .. i if love.filesystem.getInfo(p) then table.insert(result, p) end end return result end --- Read the entire current log file as a string (nil on error). function control:read() self:flush() return love.filesystem.read(path) end -- ── Handler function ────────────────────────────────────────────────── local function handler(record) if closed then return end if buffered then table.insert(buffer, record.formatted) if #buffer >= flush_size then raw_write(buffer) buffer = {} end else raw_write({ record.formatted }) end end return handler, control end -- ── Convenience: hook love.quit to flush ────────────────────────────────── --- Wrap love.quit so a control table's flush() is called automatically. -- Call once per handler after Logger.new(); safe to call multiple times. -- -- @param control The control table returned by LoveHandler.new() function LoveHandler.auto_flush_on_quit(control) local original_quit = love.quit or function() end love.quit = function() control:flush() return original_quit() end end return LoveHandler