blob: 9200ee7443e92012c3dd890e95ab86f5595c854e [file] [log] [blame]
Serge Bazanski18c1a262022-07-07 14:24:53 +02001--
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
12local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume"))
13
14local lurker = { _version = "1.0.1" }
15
16
17local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems
18local time = love.timer.getTime or os.time
19
20local function isdir(path)
21 local info = love.filesystem.getInfo(path)
22 return info.type == "directory"
23end
24
25local function lastmodified(path)
26 local info = love.filesystem.getInfo(path, "file")
27 return info.modtime
28end
29
30local lovecallbacknames = {
31 "update",
32 "load",
33 "draw",
34 "mousepressed",
35 "mousereleased",
36 "keypressed",
37 "keyreleased",
38 "focus",
39 "quit",
40}
41
42
43function 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
59end
60
61
62function lurker.print(...)
63 print("[lurker] " .. lume.format(...))
64end
65
66
67function 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
81end
82
83
84function 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()
95end
96
97
98function 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
105end
106
107
108function 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
174end
175
176
177function lurker.exitinitstate()
178 lurker.state = "normal"
179 if lurker.protected then
180 lurker.initwrappers()
181 end
182end
183
184
185function lurker.exiterrorstate()
186 lurker.state = "normal"
187 for _, v in pairs(lovecallbacknames) do
188 love[v] = lurker.funcwrappers[v]
189 end
190end
191
192
193function 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
207end
208
209
210function 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)
215end
216
217
218function lurker.modname(f)
219 return (f:gsub("%.lua$", ""):gsub("[/\\]", "."))
220end
221
222
223function lurker.resetfile(f)
224 lurker.files[f] = lastmodified(f)
225end
226
227
228function 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
256end
257
258
259function 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
266end
267
268
269return lurker.init()