Serge Bazanski | 18c1a26 | 2022-07-07 14:24:53 +0200 | [diff] [blame] | 1 | -- |
| 2 | -- lurker |
| 3 | -- |
| 4 | -- Copyright (c) 2018 rxi |
| 5 | -- |
| 6 | -- This library is free software; you can redistribute it and/or modify it |
| 7 | -- under the terms of the MIT license. See LICENSE for details. |
| 8 | -- |
| 9 | |
| 10 | -- Assumes lume is in the same directory as this file if it does not exist |
| 11 | -- as a global |
| 12 | local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume")) |
| 13 | |
| 14 | local lurker = { _version = "1.0.1" } |
| 15 | |
| 16 | |
| 17 | local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems |
| 18 | local time = love.timer.getTime or os.time |
| 19 | |
| 20 | local function isdir(path) |
| 21 | local info = love.filesystem.getInfo(path) |
| 22 | return info.type == "directory" |
| 23 | end |
| 24 | |
| 25 | local function lastmodified(path) |
| 26 | local info = love.filesystem.getInfo(path, "file") |
| 27 | return info.modtime |
| 28 | end |
| 29 | |
| 30 | local lovecallbacknames = { |
| 31 | "update", |
| 32 | "load", |
| 33 | "draw", |
| 34 | "mousepressed", |
| 35 | "mousereleased", |
| 36 | "keypressed", |
| 37 | "keyreleased", |
| 38 | "focus", |
| 39 | "quit", |
| 40 | } |
| 41 | |
| 42 | |
| 43 | function lurker.init() |
| 44 | lurker.print("Initing lurker") |
| 45 | lurker.path = "." |
| 46 | lurker.preswap = function() end |
| 47 | lurker.postswap = function() end |
| 48 | lurker.interval = .5 |
| 49 | lurker.protected = true |
| 50 | lurker.quiet = false |
| 51 | lurker.lastscan = 0 |
| 52 | lurker.lasterrorfile = nil |
| 53 | lurker.files = {} |
| 54 | lurker.funcwrappers = {} |
| 55 | lurker.lovefuncs = {} |
| 56 | lurker.state = "init" |
| 57 | lume.each(lurker.getchanged(), lurker.resetfile) |
| 58 | return lurker |
| 59 | end |
| 60 | |
| 61 | |
| 62 | function lurker.print(...) |
| 63 | print("[lurker] " .. lume.format(...)) |
| 64 | end |
| 65 | |
| 66 | |
| 67 | function lurker.listdir(path, recursive, skipdotfiles) |
| 68 | path = (path == ".") and "" or path |
| 69 | local function fullpath(x) return path .. "/" .. x end |
| 70 | local t = {} |
| 71 | for _, f in pairs(lume.map(dir(path), fullpath)) do |
| 72 | if not skipdotfiles or not f:match("/%.[^/]*$") then |
| 73 | if recursive and isdir(f) then |
| 74 | t = lume.concat(t, lurker.listdir(f, true, true)) |
| 75 | else |
| 76 | table.insert(t, lume.trim(f, "/")) |
| 77 | end |
| 78 | end |
| 79 | end |
| 80 | return t |
| 81 | end |
| 82 | |
| 83 | |
| 84 | function lurker.initwrappers() |
| 85 | for _, v in pairs(lovecallbacknames) do |
| 86 | lurker.funcwrappers[v] = function(...) |
| 87 | local args = {...} |
| 88 | xpcall(function() |
| 89 | return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args)) |
| 90 | end, lurker.onerror) |
| 91 | end |
| 92 | lurker.lovefuncs[v] = love[v] |
| 93 | end |
| 94 | lurker.updatewrappers() |
| 95 | end |
| 96 | |
| 97 | |
| 98 | function lurker.updatewrappers() |
| 99 | for _, v in pairs(lovecallbacknames) do |
| 100 | if love[v] ~= lurker.funcwrappers[v] then |
| 101 | lurker.lovefuncs[v] = love[v] |
| 102 | love[v] = lurker.funcwrappers[v] |
| 103 | end |
| 104 | end |
| 105 | end |
| 106 | |
| 107 | |
| 108 | function lurker.onerror(e, nostacktrace) |
| 109 | lurker.print("An error occurred; switching to error state") |
| 110 | lurker.state = "error" |
| 111 | |
| 112 | -- Release mouse |
| 113 | local setgrab = love.mouse.setGrab or love.mouse.setGrabbed |
| 114 | setgrab(false) |
| 115 | |
| 116 | -- Set up callbacks |
| 117 | for _, v in pairs(lovecallbacknames) do |
| 118 | love[v] = function() end |
| 119 | end |
| 120 | |
| 121 | love.update = lurker.update |
| 122 | |
| 123 | love.keypressed = function(k) |
| 124 | if k == "escape" then |
| 125 | lurker.print("Exiting...") |
| 126 | love.event.quit() |
| 127 | end |
| 128 | end |
| 129 | |
| 130 | local stacktrace = nostacktrace and "" or |
| 131 | lume.trim((debug.traceback("", 2):gsub("\t", ""))) |
| 132 | local msg = lume.format("{1}\n\n{2}", {e, stacktrace}) |
| 133 | local colors = { |
| 134 | { lume.color("#1e1e2c", 256) }, |
| 135 | { lume.color("#f0a3a3", 256) }, |
| 136 | { lume.color("#92b5b0", 256) }, |
| 137 | { lume.color("#66666a", 256) }, |
| 138 | { lume.color("#cdcdcd", 256) }, |
| 139 | } |
| 140 | love.graphics.reset() |
| 141 | love.graphics.setFont(love.graphics.newFont(12)) |
| 142 | |
| 143 | love.draw = function() |
| 144 | local pad = 25 |
| 145 | local width = love.graphics.getWidth() |
| 146 | |
| 147 | local function drawhr(pos, color1, color2) |
| 148 | local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time())) |
| 149 | if color1 then love.graphics.setColor(color1) end |
| 150 | love.graphics.rectangle("fill", pad, pos, width - pad*2, 1) |
| 151 | if color2 then love.graphics.setColor(color2) end |
| 152 | love.graphics.rectangle("fill", animpos, pos, 8, 1) |
| 153 | end |
| 154 | |
| 155 | local function drawtext(str, x, y, color, limit) |
| 156 | love.graphics.setColor(color) |
| 157 | love.graphics[limit and "printf" or "print"](str, x, y, limit) |
| 158 | end |
| 159 | |
| 160 | love.graphics.setBackgroundColor(colors[1]) |
| 161 | love.graphics.clear() |
| 162 | |
| 163 | drawtext("An error has occurred", pad, pad, colors[2]) |
| 164 | drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") - |
| 165 | pad, pad, colors[4]) |
| 166 | drawhr(pad + 32, colors[4], colors[5]) |
| 167 | drawtext("If you fix the problem and update the file the program will " .. |
| 168 | "resume", pad, pad + 46, colors[3]) |
| 169 | drawhr(pad + 72, colors[4], colors[5]) |
| 170 | drawtext(msg, pad, pad + 90, colors[5], width - pad * 2) |
| 171 | |
| 172 | love.graphics.reset() |
| 173 | end |
| 174 | end |
| 175 | |
| 176 | |
| 177 | function lurker.exitinitstate() |
| 178 | lurker.state = "normal" |
| 179 | if lurker.protected then |
| 180 | lurker.initwrappers() |
| 181 | end |
| 182 | end |
| 183 | |
| 184 | |
| 185 | function lurker.exiterrorstate() |
| 186 | lurker.state = "normal" |
| 187 | for _, v in pairs(lovecallbacknames) do |
| 188 | love[v] = lurker.funcwrappers[v] |
| 189 | end |
| 190 | end |
| 191 | |
| 192 | |
| 193 | function lurker.update() |
| 194 | if lurker.state == "init" then |
| 195 | lurker.exitinitstate() |
| 196 | end |
| 197 | local diff = time() - lurker.lastscan |
| 198 | if diff > lurker.interval then |
| 199 | lurker.lastscan = lurker.lastscan + diff |
| 200 | local changed = lurker.scan() |
| 201 | if #changed > 0 and lurker.lasterrorfile then |
| 202 | local f = lurker.lasterrorfile |
| 203 | lurker.lasterrorfile = nil |
| 204 | lurker.hotswapfile(f) |
| 205 | end |
| 206 | end |
| 207 | end |
| 208 | |
| 209 | |
| 210 | function lurker.getchanged() |
| 211 | local function fn(f) |
| 212 | return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f) |
| 213 | end |
| 214 | return lume.filter(lurker.listdir(lurker.path, true, true), fn) |
| 215 | end |
| 216 | |
| 217 | |
| 218 | function lurker.modname(f) |
| 219 | return (f:gsub("%.lua$", ""):gsub("[/\\]", ".")) |
| 220 | end |
| 221 | |
| 222 | |
| 223 | function lurker.resetfile(f) |
| 224 | lurker.files[f] = lastmodified(f) |
| 225 | end |
| 226 | |
| 227 | |
| 228 | function lurker.hotswapfile(f) |
| 229 | lurker.print("Hotswapping '{1}'...", {f}) |
| 230 | if lurker.state == "error" then |
| 231 | lurker.exiterrorstate() |
| 232 | end |
| 233 | if lurker.preswap(f) then |
| 234 | lurker.print("Hotswap of '{1}' aborted by preswap", {f}) |
| 235 | lurker.resetfile(f) |
| 236 | return |
| 237 | end |
| 238 | local modname = lurker.modname(f) |
| 239 | local t, ok, err = lume.time(lume.hotswap, modname) |
| 240 | if ok then |
| 241 | lurker.print("Swapped '{1}' in {2} secs", {f, t}) |
| 242 | else |
| 243 | lurker.print("Failed to swap '{1}' : {2}", {f, err}) |
| 244 | if not lurker.quiet and lurker.protected then |
| 245 | lurker.lasterrorfile = f |
| 246 | lurker.onerror(err, true) |
| 247 | lurker.resetfile(f) |
| 248 | return |
| 249 | end |
| 250 | end |
| 251 | lurker.resetfile(f) |
| 252 | lurker.postswap(f) |
| 253 | if lurker.protected then |
| 254 | lurker.updatewrappers() |
| 255 | end |
| 256 | end |
| 257 | |
| 258 | |
| 259 | function lurker.scan() |
| 260 | if lurker.state == "init" then |
| 261 | lurker.exitinitstate() |
| 262 | end |
| 263 | local changed = lurker.getchanged() |
| 264 | lume.each(changed, lurker.hotswapfile) |
| 265 | return changed |
| 266 | end |
| 267 | |
| 268 | |
| 269 | return lurker.init() |