lurker.lua 6.4 KB

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