signage: bring in from external repo

This is b28e6f07aa48f1e2f01eb37bffa180f97a7b03bd from
https://code.hackerspace.pl/q3k/love2d-signage/. We only keep code
commited by inf and q3k, and we're both now licensing this code under
the ISC license, as per COPYING in the root of hscloud.

Change-Id: Ibeee2e6923605e4b1a17a1d295867c056863ef59
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1335
Reviewed-by: informatic <informatic@hackerspace.pl>
Reviewed-by: q3k <q3k@hackerspace.pl>
diff --git a/hswaw/signage/vendor/lurker.lua b/hswaw/signage/vendor/lurker.lua
new file mode 100644
index 0000000..9200ee7
--- /dev/null
+++ b/hswaw/signage/vendor/lurker.lua
@@ -0,0 +1,269 @@
+--
+-- lurker
+--
+-- Copyright (c) 2018 rxi
+--
+-- This library is free software; you can redistribute it and/or modify it
+-- under the terms of the MIT license. See LICENSE for details.
+--
+
+-- Assumes lume is in the same directory as this file if it does not exist
+-- as a global
+local lume = rawget(_G, "lume") or require((...):gsub("[^/.\\]+$", "lume"))
+
+local lurker = { _version = "1.0.1" }
+
+
+local dir = love.filesystem.enumerate or love.filesystem.getDirectoryItems
+local time = love.timer.getTime or os.time
+
+local function isdir(path)
+    local info = love.filesystem.getInfo(path)
+    return info.type == "directory"
+end
+
+local function lastmodified(path)
+    local info = love.filesystem.getInfo(path, "file")
+    return info.modtime
+end
+
+local lovecallbacknames = {
+  "update",
+  "load",
+  "draw",
+  "mousepressed",
+  "mousereleased",
+  "keypressed",
+  "keyreleased",
+  "focus",
+  "quit",
+}
+
+
+function lurker.init()
+  lurker.print("Initing lurker")
+  lurker.path = "."
+  lurker.preswap = function() end
+  lurker.postswap = function() end
+  lurker.interval = .5
+  lurker.protected = true
+  lurker.quiet = false
+  lurker.lastscan = 0
+  lurker.lasterrorfile = nil
+  lurker.files = {}
+  lurker.funcwrappers = {}
+  lurker.lovefuncs = {}
+  lurker.state = "init"
+  lume.each(lurker.getchanged(), lurker.resetfile)
+  return lurker
+end
+
+
+function lurker.print(...)
+  print("[lurker] " .. lume.format(...))
+end
+
+
+function lurker.listdir(path, recursive, skipdotfiles)
+  path = (path == ".") and "" or path
+  local function fullpath(x) return path .. "/" .. x end
+  local t = {}
+  for _, f in pairs(lume.map(dir(path), fullpath)) do
+    if not skipdotfiles or not f:match("/%.[^/]*$") then
+      if recursive and isdir(f) then
+        t = lume.concat(t, lurker.listdir(f, true, true))
+      else
+        table.insert(t, lume.trim(f, "/"))
+      end
+    end
+  end
+  return t
+end
+
+
+function lurker.initwrappers()
+  for _, v in pairs(lovecallbacknames) do
+    lurker.funcwrappers[v] = function(...)
+      local args = {...}
+      xpcall(function()
+        return lurker.lovefuncs[v] and lurker.lovefuncs[v](unpack(args))
+      end, lurker.onerror)
+    end
+    lurker.lovefuncs[v] = love[v]
+  end
+  lurker.updatewrappers()
+end
+
+
+function lurker.updatewrappers()
+  for _, v in pairs(lovecallbacknames) do
+    if love[v] ~= lurker.funcwrappers[v] then
+      lurker.lovefuncs[v] = love[v]
+      love[v] = lurker.funcwrappers[v]
+    end
+  end
+end
+
+
+function lurker.onerror(e, nostacktrace)
+  lurker.print("An error occurred; switching to error state")
+  lurker.state = "error"
+
+  -- Release mouse
+  local setgrab = love.mouse.setGrab or love.mouse.setGrabbed
+  setgrab(false)
+
+  -- Set up callbacks
+  for _, v in pairs(lovecallbacknames) do
+    love[v] = function() end
+  end
+
+  love.update = lurker.update
+
+  love.keypressed = function(k)
+    if k == "escape" then
+      lurker.print("Exiting...")
+      love.event.quit()
+    end
+  end
+
+  local stacktrace = nostacktrace and "" or
+                     lume.trim((debug.traceback("", 2):gsub("\t", "")))
+  local msg = lume.format("{1}\n\n{2}", {e, stacktrace})
+  local colors = {
+    { lume.color("#1e1e2c", 256) },
+    { lume.color("#f0a3a3", 256) },
+    { lume.color("#92b5b0", 256) },
+    { lume.color("#66666a", 256) },
+    { lume.color("#cdcdcd", 256) },
+  }
+  love.graphics.reset()
+  love.graphics.setFont(love.graphics.newFont(12))
+
+  love.draw = function()
+    local pad = 25
+    local width = love.graphics.getWidth()
+
+    local function drawhr(pos, color1, color2)
+      local animpos = lume.smooth(pad, width - pad - 8, lume.pingpong(time()))
+      if color1 then love.graphics.setColor(color1) end
+      love.graphics.rectangle("fill", pad, pos, width - pad*2, 1)
+      if color2 then love.graphics.setColor(color2) end
+      love.graphics.rectangle("fill", animpos, pos, 8, 1)
+    end
+
+    local function drawtext(str, x, y, color, limit)
+      love.graphics.setColor(color)
+      love.graphics[limit and "printf" or "print"](str, x, y, limit)
+    end
+
+    love.graphics.setBackgroundColor(colors[1])
+    love.graphics.clear()
+
+    drawtext("An error has occurred", pad, pad, colors[2])
+    drawtext("lurker", width - love.graphics.getFont():getWidth("lurker") -
+             pad, pad, colors[4])
+    drawhr(pad + 32, colors[4], colors[5])
+    drawtext("If you fix the problem and update the file the program will " ..
+             "resume", pad, pad + 46, colors[3])
+    drawhr(pad + 72, colors[4], colors[5])
+    drawtext(msg, pad, pad + 90, colors[5], width - pad * 2)
+
+    love.graphics.reset()
+  end
+end
+
+
+function lurker.exitinitstate()
+  lurker.state = "normal"
+  if lurker.protected then
+    lurker.initwrappers()
+  end
+end
+
+
+function lurker.exiterrorstate()
+  lurker.state = "normal"
+  for _, v in pairs(lovecallbacknames) do
+    love[v] = lurker.funcwrappers[v]
+  end
+end
+
+
+function lurker.update()
+  if lurker.state == "init" then
+    lurker.exitinitstate()
+  end
+  local diff = time() - lurker.lastscan
+  if diff > lurker.interval then
+    lurker.lastscan = lurker.lastscan + diff
+    local changed = lurker.scan()
+    if #changed > 0 and lurker.lasterrorfile then
+      local f = lurker.lasterrorfile
+      lurker.lasterrorfile = nil
+      lurker.hotswapfile(f)
+    end
+  end
+end
+
+
+function lurker.getchanged()
+  local function fn(f)
+    return f:match("%.lua$") and lurker.files[f] ~= lastmodified(f)
+  end
+  return lume.filter(lurker.listdir(lurker.path, true, true), fn)
+end
+
+
+function lurker.modname(f)
+  return (f:gsub("%.lua$", ""):gsub("[/\\]", "."))
+end
+
+
+function lurker.resetfile(f)
+  lurker.files[f] = lastmodified(f)
+end
+
+
+function lurker.hotswapfile(f)
+  lurker.print("Hotswapping '{1}'...", {f})
+  if lurker.state == "error" then
+    lurker.exiterrorstate()
+  end
+  if lurker.preswap(f) then
+    lurker.print("Hotswap of '{1}' aborted by preswap", {f})
+    lurker.resetfile(f)
+    return
+  end
+  local modname = lurker.modname(f)
+  local t, ok, err = lume.time(lume.hotswap, modname)
+  if ok then
+    lurker.print("Swapped '{1}' in {2} secs", {f, t})
+  else
+    lurker.print("Failed to swap '{1}' : {2}", {f, err})
+    if not lurker.quiet and lurker.protected then
+      lurker.lasterrorfile = f
+      lurker.onerror(err, true)
+      lurker.resetfile(f)
+      return
+    end
+  end
+  lurker.resetfile(f)
+  lurker.postswap(f)
+  if lurker.protected then
+    lurker.updatewrappers()
+  end
+end
+
+
+function lurker.scan()
+  if lurker.state == "init" then
+    lurker.exitinitstate()
+  end
+  local changed = lurker.getchanged()
+  lume.each(changed, lurker.hotswapfile)
+  return changed
+end
+
+
+return lurker.init()