adding missing libraries
This commit is contained in:
3
src/.gitignore
vendored
Normal file
3
src/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
!baton.lua
|
||||
!lovebird.lua
|
||||
!lurker.lua
|
374
src/baton.lua
Normal file
374
src/baton.lua
Normal file
@@ -0,0 +1,374 @@
|
||||
local baton = {
|
||||
_VERSION = 'Baton v1.0.2',
|
||||
_DESCRIPTION = 'Input library for LÖVE.',
|
||||
_URL = 'https://github.com/tesselode/baton',
|
||||
_LICENSE = [[
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Andrew Minnich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
-- string parsing functions --
|
||||
|
||||
-- splits a source definition into type and value
|
||||
-- example: 'button:a' -> 'button', 'a'
|
||||
local function parseSource(source)
|
||||
return source:match '(.+):(.+)'
|
||||
end
|
||||
|
||||
-- splits an axis value into axis and direction
|
||||
-- example: 'leftx-' -> 'leftx', '-'
|
||||
local function parseAxis(value)
|
||||
return value:match '(.+)([%+%-])'
|
||||
end
|
||||
|
||||
-- splits a joystick hat value into hat number and direction
|
||||
-- example: '2rd' -> '2', 'rd'
|
||||
local function parseHat(value)
|
||||
return value:match '(%d)(.+)'
|
||||
end
|
||||
|
||||
--[[
|
||||
-- source functions --
|
||||
|
||||
each source function checks the state of one type of input
|
||||
and returns a value from 0 to 1. for binary controls, such
|
||||
as keyboard keys and gamepad buttons, they return 1 if the
|
||||
input is held down and 0 if not. for analog controls, such
|
||||
as "leftx+" (the left analog stick held to the right), they
|
||||
return a number from 0 to 1.
|
||||
|
||||
source functions are split into keyboard/mouse functions
|
||||
and joystick/gamepad functions. baton treats these two
|
||||
categories slightly differently.
|
||||
]]
|
||||
|
||||
local sourceFunction = {keyboardMouse = {}, joystick = {}}
|
||||
|
||||
-- checks whether a keyboard key is down or not
|
||||
function sourceFunction.keyboardMouse.key(key)
|
||||
return love.keyboard.isDown(key) and 1 or 0
|
||||
end
|
||||
|
||||
-- checks whether a keyboard key is down or not,
|
||||
-- but it takes a scancode as an input
|
||||
function sourceFunction.keyboardMouse.sc(sc)
|
||||
return love.keyboard.isScancodeDown(sc) and 1 or 0
|
||||
end
|
||||
|
||||
-- checks whether a mouse buttons is down or not.
|
||||
-- note that baton doesn't detect mouse movement, just the buttons
|
||||
function sourceFunction.keyboardMouse.mouse(button)
|
||||
return love.mouse.isDown(tonumber(button)) and 1 or 0
|
||||
end
|
||||
|
||||
-- checks the position of a joystick axis
|
||||
function sourceFunction.joystick.axis(joystick, value)
|
||||
local axis, direction = parseAxis(value)
|
||||
-- "a and b or c" is ok here because b will never be boolean
|
||||
value = tonumber(axis) and joystick:getAxis(tonumber(axis))
|
||||
or joystick:getGamepadAxis(axis)
|
||||
if direction == '-' then value = -value end
|
||||
return value > 0 and value or 0
|
||||
end
|
||||
|
||||
-- checks whether a joystick button is held down or not
|
||||
-- can take a number or a GamepadButton string
|
||||
function sourceFunction.joystick.button(joystick, button)
|
||||
-- i'm intentionally not using the "a and b or c" idiom here
|
||||
-- because joystick.isDown returns a boolean
|
||||
if tonumber(button) then
|
||||
return joystick:isDown(tonumber(button)) and 1 or 0
|
||||
else
|
||||
return joystick:isGamepadDown(button) and 1 or 0
|
||||
end
|
||||
end
|
||||
|
||||
-- checks the direction of a joystick hat
|
||||
function sourceFunction.joystick.hat(joystick, value)
|
||||
local hat, direction = parseHat(value)
|
||||
return joystick:getHat(hat) == direction and 1 or 0
|
||||
end
|
||||
|
||||
--[[
|
||||
-- player class --
|
||||
|
||||
the player object takes a configuration table and handles input
|
||||
accordingly. it's called a "player" because it makes sense to use
|
||||
multiple of these for each player in a multiplayer game, but
|
||||
you can use separate player objects to organize inputs
|
||||
however you want.
|
||||
]]
|
||||
|
||||
local Player = {}
|
||||
Player.__index = Player
|
||||
|
||||
-- internal functions --
|
||||
|
||||
-- sets the player's config to a user-defined config table
|
||||
-- and sets some defaults if they're not already defined
|
||||
function Player:_loadConfig(config)
|
||||
if not config then
|
||||
error('No config table provided', 4)
|
||||
end
|
||||
if not config.controls then
|
||||
error('No controls specified', 4)
|
||||
end
|
||||
config.pairs = config.pairs or {}
|
||||
config.deadzone = config.deadzone or .5
|
||||
config.squareDeadzone = config.squareDeadzone or false
|
||||
self.config = config
|
||||
end
|
||||
|
||||
-- initializes a control object for each control defined in the config
|
||||
function Player:_initControls()
|
||||
self._controls = {}
|
||||
for controlName, sources in pairs(self.config.controls) do
|
||||
self._controls[controlName] = {
|
||||
sources = sources,
|
||||
rawValue = 0,
|
||||
value = 0,
|
||||
down = false,
|
||||
downPrevious = false,
|
||||
pressed = false,
|
||||
released = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- initializes an axis pair object for each axis pair defined in the config
|
||||
function Player:_initPairs()
|
||||
self._pairs = {}
|
||||
for pairName, controls in pairs(self.config.pairs) do
|
||||
self._pairs[pairName] = {
|
||||
controls = controls,
|
||||
rawX = 0,
|
||||
rawY = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
down = false,
|
||||
downPrevious = false,
|
||||
pressed = false,
|
||||
released = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function Player:_init(config)
|
||||
self:_loadConfig(config)
|
||||
self:_initControls()
|
||||
self:_initPairs()
|
||||
self._activeDevice = 'none'
|
||||
end
|
||||
|
||||
--[[
|
||||
detects the active device (keyboard/mouse or joystick).
|
||||
if the keyboard or mouse is currently being used, joystick
|
||||
inputs will be ignored. this is to prevent slight axis movements
|
||||
from adding errant inputs when someone's using the keyboard.
|
||||
|
||||
the active device is saved to player._activeDevice, which is then
|
||||
used throughout the rest of the update loop to check only
|
||||
keyboard or joystick inputs.
|
||||
]]
|
||||
function Player:_setActiveDevice()
|
||||
-- if the joystick is unset, then we should make sure _activeDevice
|
||||
-- isn't "joy" anymore, otherwise there will be an error later
|
||||
-- when we try to query a joystick that isn't there
|
||||
if self._activeDevice == 'joy' and not self.config.joystick then
|
||||
self._activeDevice = 'none'
|
||||
end
|
||||
for _, control in pairs(self._controls) do
|
||||
for _, source in ipairs(control.sources) do
|
||||
local type, value = parseSource(source)
|
||||
if sourceFunction.keyboardMouse[type] then
|
||||
if sourceFunction.keyboardMouse[type](value) > self.config.deadzone then
|
||||
self._activeDevice = 'kbm'
|
||||
return
|
||||
end
|
||||
elseif self.config.joystick and sourceFunction.joystick[type] then
|
||||
if sourceFunction.joystick[type](self.config.joystick, value) > self.config.deadzone then
|
||||
self._activeDevice = 'joy'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
gets the value of a control by running the appropriate source functions
|
||||
for all of its sources. does not apply deadzone.
|
||||
]]
|
||||
function Player:_getControlRawValue(control)
|
||||
local rawValue = 0
|
||||
for _, source in ipairs(control.sources) do
|
||||
local type, value = parseSource(source)
|
||||
if sourceFunction.keyboardMouse[type] and self._activeDevice == 'kbm' then
|
||||
if sourceFunction.keyboardMouse[type](value) == 1 then
|
||||
return 1
|
||||
end
|
||||
elseif sourceFunction.joystick[type] and self._activeDevice == 'joy' then
|
||||
rawValue = rawValue + sourceFunction.joystick[type](self.config.joystick, value)
|
||||
if rawValue >= 1 then
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return rawValue
|
||||
end
|
||||
|
||||
--[[
|
||||
updates each control in a player. saves the value with and without deadzone
|
||||
and the down/pressed/released state.
|
||||
]]
|
||||
function Player:_updateControls()
|
||||
for _, control in pairs(self._controls) do
|
||||
control.rawValue = self:_getControlRawValue(control)
|
||||
control.value = control.rawValue >= self.config.deadzone and control.rawValue or 0
|
||||
control.downPrevious = control.down
|
||||
control.down = control.value > 0
|
||||
control.pressed = control.down and not control.downPrevious
|
||||
control.released = control.downPrevious and not control.down
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
updates each axis pair in a player. saves the value with and without deadzone
|
||||
and the down/pressed/released state.
|
||||
]]
|
||||
function Player:_updatePairs()
|
||||
for _, pair in pairs(self._pairs) do
|
||||
-- get raw x and y
|
||||
local l = self._controls[pair.controls[1]].rawValue
|
||||
local r = self._controls[pair.controls[2]].rawValue
|
||||
local u = self._controls[pair.controls[3]].rawValue
|
||||
local d = self._controls[pair.controls[4]].rawValue
|
||||
pair.rawX, pair.rawY = r - l, d - u
|
||||
|
||||
-- limit to 1
|
||||
local len = math.sqrt(pair.rawX^2 + pair.rawY^2)
|
||||
if len > 1 then
|
||||
pair.rawX, pair.rawY = pair.rawX / len, pair.rawY / len
|
||||
end
|
||||
|
||||
-- deadzone
|
||||
if self.config.squareDeadzone then
|
||||
pair.x = math.abs(pair.rawX) > self.config.deadzone and pair.rawX or 0
|
||||
pair.y = math.abs(pair.rawY) > self.config.deadzone and pair.rawY or 0
|
||||
else
|
||||
pair.x = len > self.config.deadzone and pair.rawX or 0
|
||||
pair.y = len > self.config.deadzone and pair.rawY or 0
|
||||
end
|
||||
|
||||
-- down/pressed/released
|
||||
pair.downPrevious = pair.down
|
||||
pair.down = pair.x ~= 0 or pair.y ~= 0
|
||||
pair.pressed = pair.down and not pair.downPrevious
|
||||
pair.released = pair.downPrevious and not pair.down
|
||||
end
|
||||
end
|
||||
|
||||
-- public API --
|
||||
|
||||
-- checks for changes in inputs
|
||||
function Player:update()
|
||||
self:_setActiveDevice()
|
||||
self:_updateControls()
|
||||
self:_updatePairs()
|
||||
end
|
||||
|
||||
-- gets the value of a control or axis pair without deadzone applied
|
||||
function Player:getRaw(name)
|
||||
if self._pairs[name] then
|
||||
return self._pairs[name].rawX, self._pairs[name].rawY
|
||||
elseif self._controls[name] then
|
||||
return self._controls[name].rawValue
|
||||
else
|
||||
error('No control with name "' .. name .. '" defined', 3)
|
||||
end
|
||||
end
|
||||
|
||||
-- gets the value of a control or axis pair with deadzone applied
|
||||
function Player:get(name)
|
||||
if self._pairs[name] then
|
||||
return self._pairs[name].x, self._pairs[name].y
|
||||
elseif self._controls[name] then
|
||||
return self._controls[name].value
|
||||
else
|
||||
error('No control with name "' .. name .. '" defined', 3)
|
||||
end
|
||||
end
|
||||
|
||||
-- gets whether a control or axis pair is "held down"
|
||||
function Player:down(name)
|
||||
if self._pairs[name] then
|
||||
return self._pairs[name].down
|
||||
elseif self._controls[name] then
|
||||
return self._controls[name].down
|
||||
else
|
||||
error('No control with name "' .. name .. '" defined', 3)
|
||||
end
|
||||
end
|
||||
|
||||
-- gets whether a control or axis pair was pressed this frame
|
||||
function Player:pressed(name)
|
||||
if self._pairs[name] then
|
||||
return self._pairs[name].pressed
|
||||
elseif self._controls[name] then
|
||||
return self._controls[name].pressed
|
||||
else
|
||||
error('No control with name "' .. name .. '" defined', 3)
|
||||
end
|
||||
end
|
||||
|
||||
-- gets whether a control or axis pair was released this frame
|
||||
function Player:released(name)
|
||||
if self._pairs[name] then
|
||||
return self._pairs[name].released
|
||||
elseif self._controls[name] then
|
||||
return self._controls[name].released
|
||||
else
|
||||
error('No control with name "' .. name .. '" defined', 3)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
gets the currently active device (either "kbm", "joy", or "none").
|
||||
this is useful for displaying instructional text. you may have
|
||||
a menu that says "press ENTER to confirm" or "press A to confirm"
|
||||
depending on whether the player is using their keyboard or gamepad.
|
||||
this function allows you to detect which they used most recently.
|
||||
]]
|
||||
function Player:getActiveDevice()
|
||||
return self._activeDevice
|
||||
end
|
||||
|
||||
-- main functions --
|
||||
|
||||
-- creates a new player with the user-provided config table
|
||||
function baton.new(config)
|
||||
local player = setmetatable({}, Player)
|
||||
player:_init(config)
|
||||
return player
|
||||
end
|
||||
|
||||
return baton
|
737
src/lovebird.lua
Normal file
737
src/lovebird.lua
Normal file
@@ -0,0 +1,737 @@
|
||||
--
|
||||
-- lovebird
|
||||
--
|
||||
-- Copyright (c) 2017 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.
|
||||
--
|
||||
|
||||
local socket = require "socket"
|
||||
|
||||
local lovebird = { _version = "0.4.3" }
|
||||
|
||||
lovebird.loadstring = loadstring or load
|
||||
lovebird.inited = false
|
||||
lovebird.host = "*"
|
||||
lovebird.buffer = ""
|
||||
lovebird.lines = {}
|
||||
lovebird.connections = {}
|
||||
lovebird.pages = {}
|
||||
|
||||
lovebird.wrapprint = true
|
||||
lovebird.timestamp = true
|
||||
lovebird.allowhtml = false
|
||||
lovebird.echoinput = true
|
||||
lovebird.port = 8000
|
||||
lovebird.whitelist = { "127.0.0.1" }
|
||||
lovebird.maxlines = 200
|
||||
lovebird.updateinterval = .5
|
||||
|
||||
|
||||
lovebird.pages["index"] = [[
|
||||
<?lua
|
||||
-- Handle console input
|
||||
if req.parsedbody.input then
|
||||
local str = req.parsedbody.input
|
||||
if lovebird.echoinput then
|
||||
lovebird.pushline({ type = 'input', str = str })
|
||||
end
|
||||
if str:find("^=") then
|
||||
str = "print(" .. str:sub(2) .. ")"
|
||||
end
|
||||
xpcall(function() assert(lovebird.loadstring(str, "input"))() end,
|
||||
lovebird.onerror)
|
||||
end
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="x-ua-compatible" content="IE=Edge"/>
|
||||
<meta charset="utf-8">
|
||||
<title>lovebird</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
font-family: helvetica, verdana, sans;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
form {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.timestamp {
|
||||
color: #909090;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.repeatcount {
|
||||
color: #F0F0F0;
|
||||
background: #505050;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
border-radius: 7px;
|
||||
display: inline-block;
|
||||
}
|
||||
.errormarker {
|
||||
color: #F0F0F0;
|
||||
background: #8E0000;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
width: 17px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
.greybordered {
|
||||
margin: 12px;
|
||||
background: #F0F0F0;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.inputline {
|
||||
font-family: mono, courier;
|
||||
font-size: 13px;
|
||||
color: #606060;
|
||||
}
|
||||
.inputline:before {
|
||||
content: '\00B7\00B7\00B7';
|
||||
padding-right: 5px;
|
||||
}
|
||||
.errorline {
|
||||
color: #8E0000;
|
||||
}
|
||||
#header {
|
||||
background: #101010;
|
||||
height: 25px;
|
||||
color: #F0F0F0;
|
||||
padding: 9px
|
||||
}
|
||||
#title {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
}
|
||||
#title a {
|
||||
color: #F0F0F0;
|
||||
text-decoration: none;
|
||||
}
|
||||
#title a:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#version {
|
||||
font-size: 10px;
|
||||
}
|
||||
#status {
|
||||
float: right;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
#main a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
background: #E0E0E0;
|
||||
border: 1px solid #D0D0D0;
|
||||
border-radius: 3px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
#main a:hover {
|
||||
background: #D0D0D0;
|
||||
border: 1px solid #C0C0C0;
|
||||
}
|
||||
#console {
|
||||
position: absolute;
|
||||
top: 40px; bottom: 0px; left: 0px; right: 312px;
|
||||
}
|
||||
#input {
|
||||
position: absolute;
|
||||
margin: 10px;
|
||||
bottom: 0px; left: 0px; right: 0px;
|
||||
}
|
||||
#inputbox {
|
||||
width: 100%;
|
||||
font-family: mono, courier;
|
||||
font-size: 13px;
|
||||
}
|
||||
#output {
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
margin: 10px;
|
||||
line-height: 17px;
|
||||
top: 0px; bottom: 36px; left: 0px; right: 0px;
|
||||
}
|
||||
#env {
|
||||
position: absolute;
|
||||
top: 40px; bottom: 0px; right: 0px;
|
||||
width: 300px;
|
||||
}
|
||||
#envheader {
|
||||
padding: 5px;
|
||||
background: #E0E0E0;
|
||||
}
|
||||
#envvars {
|
||||
position: absolute;
|
||||
left: 0px; right: 0px; top: 25px; bottom: 0px;
|
||||
margin: 10px;
|
||||
overflow-y: scroll;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<div id="title">
|
||||
<a href="https://github.com/rxi/lovebird">lovebird</a>
|
||||
<span id="version"><?lua echo(lovebird._version) ?></span>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="console" class="greybordered">
|
||||
<div id="output"> <?lua echo(lovebird.buffer) ?> </div>
|
||||
<div id="input">
|
||||
<form method="post"
|
||||
onkeydown="return onInputKeyDown(event);"
|
||||
onsubmit="onInputSubmit(); return false;">
|
||||
<input id="inputbox" name="input" type="text"
|
||||
autocomplete="off"></input>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="env" class="greybordered">
|
||||
<div id="envheader"></div>
|
||||
<div id="envvars"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("inputbox").focus();
|
||||
|
||||
var changeFavicon = function(href) {
|
||||
var old = document.getElementById("favicon");
|
||||
if (old) document.head.removeChild(old);
|
||||
var link = document.createElement("link");
|
||||
link.id = "favicon";
|
||||
link.rel = "shortcut icon";
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
var truncate = function(str, len) {
|
||||
if (str.length <= len) return str;
|
||||
return str.substring(0, len - 3) + "...";
|
||||
}
|
||||
|
||||
var geturl = function(url, onComplete, onFail) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status == 200) {
|
||||
if (onComplete) onComplete(req.responseText);
|
||||
} else {
|
||||
if (onFail) onFail(req.responseText);
|
||||
}
|
||||
}
|
||||
url += (url.indexOf("?") > -1 ? "&_=" : "?_=") + Math.random();
|
||||
req.open("GET", url, true);
|
||||
req.send();
|
||||
}
|
||||
|
||||
var divContentCache = {}
|
||||
var updateDivContent = function(id, content) {
|
||||
if (divContentCache[id] != content) {
|
||||
document.getElementById(id).innerHTML = content;
|
||||
divContentCache[id] = content
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var onInputSubmit = function() {
|
||||
var b = document.getElementById("inputbox");
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", "/", true);
|
||||
req.send("input=" + encodeURIComponent(b.value));
|
||||
/* Do input history */
|
||||
if (b.value && inputHistory[0] != b.value) {
|
||||
inputHistory.unshift(b.value);
|
||||
}
|
||||
inputHistory.index = -1;
|
||||
/* Reset */
|
||||
b.value = "";
|
||||
refreshOutput();
|
||||
}
|
||||
|
||||
/* Input box history */
|
||||
var inputHistory = [];
|
||||
inputHistory.index = 0;
|
||||
var onInputKeyDown = function(e) {
|
||||
var key = e.which || e.keyCode;
|
||||
if (key != 38 && key != 40) return true;
|
||||
var b = document.getElementById("inputbox");
|
||||
if (key == 38 && inputHistory.index < inputHistory.length - 1) {
|
||||
/* Up key */
|
||||
inputHistory.index++;
|
||||
}
|
||||
if (key == 40 && inputHistory.index >= 0) {
|
||||
/* Down key */
|
||||
inputHistory.index--;
|
||||
}
|
||||
b.value = inputHistory[inputHistory.index] || "";
|
||||
b.selectionStart = b.value.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Output buffer and status */
|
||||
var refreshOutput = function() {
|
||||
geturl("/buffer", function(text) {
|
||||
updateDivContent("status", "connected ●");
|
||||
if (updateDivContent("output", text)) {
|
||||
var div = document.getElementById("output");
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
||||
/* Update favicon */
|
||||
changeFavicon("data:image/png;base64," +
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAP1BMVEUAAAAAAAAAAAD////19fUO"+
|
||||
"Dg7v7+/h4eGzs7MlJSUeHh7n5+fY2NjJycnGxsa3t7eioqKfn5+QkJCHh4d+fn7zU+b5AAAAAnRS"+
|
||||
"TlPlAFWaypEAAABRSURBVBjTfc9HDoAwDERRQ+w0ern/WQkZaUBC4e/mrWzppH9VJjbjZg1Ii2rM"+
|
||||
"DyR1JZ8J0dVWggIGggcEwgbYCRbuPRqgyjHNpzUP+39GPu9fgloC5L9DO0sAAAAASUVORK5CYII="
|
||||
);
|
||||
},
|
||||
function(text) {
|
||||
updateDivContent("status", "disconnected ○");
|
||||
/* Update favicon */
|
||||
changeFavicon("data:image/png;base64," +
|
||||
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAADZ2dm4uLgM"+
|
||||
"DAz29vbz8/Pv7+/h4eHIyMiwsLBtbW0lJSUeHh4QEBDn5+fS0tLDw8O0tLSioqKfn5+QkJCHh4d+"+
|
||||
"fn5ycnJmZmZgYGBXV1dLS0tFRUUGBgZ0He44AAAAAnRSTlPlAFWaypEAAABeSURBVBjTfY9HDoAw"+
|
||||
"DAQD6Z3ey/9/iXMxkVDYw0g7F3tJReosUKHnwY4pCM+EtOEVXrb7wVRA0dMbaAcUwiVeDQq1Jp4a"+
|
||||
"xUg5kE0ooqZu68Di2Tgbs/DiY/9jyGf+AyFKBAK7KD2TAAAAAElFTkSuQmCC"
|
||||
);
|
||||
});
|
||||
}
|
||||
setInterval(refreshOutput,
|
||||
<?lua echo(lovebird.updateinterval) ?> * 1000);
|
||||
|
||||
/* Environment variable view */
|
||||
var envPath = "";
|
||||
var refreshEnv = function() {
|
||||
geturl("/env.json?p=" + envPath, function(text) {
|
||||
var json = eval("(" + text + ")");
|
||||
|
||||
/* Header */
|
||||
var html = "<a href='#' onclick=\"setEnvPath('')\">env</a>";
|
||||
var acc = "";
|
||||
var p = json.path != "" ? json.path.split(".") : [];
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
acc += "." + p[i];
|
||||
html += " <a href='#' onclick=\"setEnvPath('" + acc + "')\">" +
|
||||
truncate(p[i], 10) + "</a>";
|
||||
}
|
||||
updateDivContent("envheader", html);
|
||||
|
||||
/* Handle invalid table path */
|
||||
if (!json.valid) {
|
||||
updateDivContent("envvars", "Bad path");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
var html = "<table>";
|
||||
for (var i = 0; json.vars[i]; i++) {
|
||||
var x = json.vars[i];
|
||||
var fullpath = (json.path + "." + x.key).replace(/^\./, "");
|
||||
var k = truncate(x.key, 15);
|
||||
if (x.type == "table") {
|
||||
k = "<a href='#' onclick=\"setEnvPath('" + fullpath + "')\">" +
|
||||
k + "</a>";
|
||||
}
|
||||
var v = "<a href='#' onclick=\"insertVar('" +
|
||||
fullpath.replace(/\.(-?[0-9]+)/g, "[$1]") +
|
||||
"');\">" + x.value + "</a>"
|
||||
html += "<tr><td>" + k + "</td><td>" + v + "</td></tr>";
|
||||
}
|
||||
html += "</table>";
|
||||
updateDivContent("envvars", html);
|
||||
});
|
||||
}
|
||||
var setEnvPath = function(p) {
|
||||
envPath = p;
|
||||
refreshEnv();
|
||||
}
|
||||
var insertVar = function(p) {
|
||||
var b = document.getElementById("inputbox");
|
||||
b.value += p;
|
||||
b.focus();
|
||||
}
|
||||
setInterval(refreshEnv, <?lua echo(lovebird.updateinterval) ?> * 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
]]
|
||||
|
||||
|
||||
lovebird.pages["buffer"] = [[ <?lua echo(lovebird.buffer) ?> ]]
|
||||
|
||||
|
||||
lovebird.pages["env.json"] = [[
|
||||
<?lua
|
||||
local t = _G
|
||||
local p = req.parsedurl.query.p or ""
|
||||
p = p:gsub("%.+", "."):match("^[%.]*(.*)[%.]*$")
|
||||
if p ~= "" then
|
||||
for x in p:gmatch("[^%.]+") do
|
||||
t = t[x] or t[tonumber(x)]
|
||||
-- Return early if path does not exist
|
||||
if type(t) ~= "table" then
|
||||
echo('{ "valid": false, "path": ' .. string.format("%q", p) .. ' }')
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
?>
|
||||
{
|
||||
"valid": true,
|
||||
"path": "<?lua echo(p) ?>",
|
||||
"vars": [
|
||||
<?lua
|
||||
local keys = {}
|
||||
for k in pairs(t) do
|
||||
if type(k) == "number" or type(k) == "string" then
|
||||
table.insert(keys, k)
|
||||
end
|
||||
end
|
||||
table.sort(keys, lovebird.compare)
|
||||
for _, k in pairs(keys) do
|
||||
local v = t[k]
|
||||
?>
|
||||
{
|
||||
"key": "<?lua echo(k) ?>",
|
||||
"value": <?lua echo(
|
||||
string.format("%q",
|
||||
lovebird.truncate(
|
||||
lovebird.htmlescape(
|
||||
tostring(v)), 26))) ?>,
|
||||
"type": "<?lua echo(type(v)) ?>",
|
||||
},
|
||||
<?lua end ?>
|
||||
]
|
||||
}
|
||||
]]
|
||||
|
||||
|
||||
|
||||
function lovebird.init()
|
||||
-- Init server
|
||||
lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
|
||||
lovebird.addr, lovebird.port = lovebird.server:getsockname()
|
||||
lovebird.server:settimeout(0)
|
||||
-- Wrap print
|
||||
lovebird.origprint = print
|
||||
if lovebird.wrapprint then
|
||||
local oldprint = print
|
||||
print = function(...)
|
||||
oldprint(...)
|
||||
lovebird.print(...)
|
||||
end
|
||||
end
|
||||
-- Compile page templates
|
||||
for k, page in pairs(lovebird.pages) do
|
||||
lovebird.pages[k] = lovebird.template(page, "lovebird, req",
|
||||
"pages." .. k)
|
||||
end
|
||||
lovebird.inited = true
|
||||
end
|
||||
|
||||
|
||||
function lovebird.template(str, params, chunkname)
|
||||
params = params and ("," .. params) or ""
|
||||
local f = function(x) return string.format(" echo(%q)", x) end
|
||||
str = ("?>"..str.."<?lua"):gsub("%?>(.-)<%?lua", f)
|
||||
str = "local echo " .. params .. " = ..." .. str
|
||||
local fn = assert(lovebird.loadstring(str, chunkname))
|
||||
return function(...)
|
||||
local output = {}
|
||||
local echo = function(str) table.insert(output, str) end
|
||||
fn(echo, ...)
|
||||
return table.concat(lovebird.map(output, tostring))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.map(t, fn)
|
||||
local res = {}
|
||||
for k, v in pairs(t) do res[k] = fn(v) end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
function lovebird.trace(...)
|
||||
local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
|
||||
print(str)
|
||||
if not lovebird.wrapprint then lovebird.print(str) end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.unescape(str)
|
||||
local f = function(x) return string.char(tonumber("0x"..x)) end
|
||||
return (str:gsub("%+", " "):gsub("%%(..)", f))
|
||||
end
|
||||
|
||||
|
||||
function lovebird.parseurl(url)
|
||||
local res = {}
|
||||
res.path, res.search = url:match("/([^%?]*)%??(.*)")
|
||||
res.query = {}
|
||||
for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
|
||||
res.query[k] = lovebird.unescape(v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local htmlescapemap = {
|
||||
["<"] = "<",
|
||||
["&"] = "&",
|
||||
['"'] = """,
|
||||
["'"] = "'",
|
||||
}
|
||||
|
||||
function lovebird.htmlescape(str)
|
||||
return ( str:gsub("[<&\"']", htmlescapemap) )
|
||||
end
|
||||
|
||||
|
||||
function lovebird.truncate(str, len)
|
||||
if #str <= len then
|
||||
return str
|
||||
end
|
||||
return str:sub(1, len - 3) .. "..."
|
||||
end
|
||||
|
||||
|
||||
function lovebird.compare(a, b)
|
||||
local na, nb = tonumber(a), tonumber(b)
|
||||
if na then
|
||||
if nb then return na < nb end
|
||||
return false
|
||||
elseif nb then
|
||||
return true
|
||||
end
|
||||
return tostring(a) < tostring(b)
|
||||
end
|
||||
|
||||
|
||||
function lovebird.checkwhitelist(addr)
|
||||
if lovebird.whitelist == nil then return true end
|
||||
for _, a in pairs(lovebird.whitelist) do
|
||||
local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
|
||||
if addr:match(ptn) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function lovebird.clear()
|
||||
lovebird.lines = {}
|
||||
lovebird.buffer = ""
|
||||
end
|
||||
|
||||
|
||||
function lovebird.pushline(line)
|
||||
line.time = os.time()
|
||||
line.count = 1
|
||||
table.insert(lovebird.lines, line)
|
||||
if #lovebird.lines > lovebird.maxlines then
|
||||
table.remove(lovebird.lines, 1)
|
||||
end
|
||||
lovebird.recalcbuffer()
|
||||
end
|
||||
|
||||
|
||||
function lovebird.recalcbuffer()
|
||||
local function doline(line)
|
||||
local str = line.str
|
||||
if not lovebird.allowhtml then
|
||||
str = lovebird.htmlescape(line.str):gsub("\n", "<br>")
|
||||
end
|
||||
if line.type == "input" then
|
||||
str = '<span class="inputline">' .. str .. '</span>'
|
||||
else
|
||||
if line.type == "error" then
|
||||
str = '<span class="errormarker">!</span> ' .. str
|
||||
str = '<span class="errorline">' .. str .. '</span>'
|
||||
end
|
||||
if line.count > 1 then
|
||||
str = '<span class="repeatcount">' .. line.count .. '</span> ' .. str
|
||||
end
|
||||
if lovebird.timestamp then
|
||||
str = os.date('<span class="timestamp">%H:%M:%S</span> ', line.time) ..
|
||||
str
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "<br>")
|
||||
end
|
||||
|
||||
|
||||
function lovebird.print(...)
|
||||
local t = {}
|
||||
for i = 1, select("#", ...) do
|
||||
table.insert(t, tostring(select(i, ...)))
|
||||
end
|
||||
local str = table.concat(t, " ")
|
||||
local last = lovebird.lines[#lovebird.lines]
|
||||
if last and str == last.str then
|
||||
-- Update last line if this line is a duplicate of it
|
||||
last.time = os.time()
|
||||
last.count = last.count + 1
|
||||
lovebird.recalcbuffer()
|
||||
else
|
||||
-- Create new line
|
||||
lovebird.pushline({ type = "output", str = str })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onerror(err)
|
||||
lovebird.pushline({ type = "error", str = err })
|
||||
if lovebird.wrapprint then
|
||||
lovebird.origprint("[lovebird] ERROR: " .. err)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onrequest(req, client)
|
||||
local page = req.parsedurl.path
|
||||
page = page ~= "" and page or "index"
|
||||
-- Handle "page not found"
|
||||
if not lovebird.pages[page] then
|
||||
return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
|
||||
end
|
||||
-- Handle page
|
||||
local str
|
||||
xpcall(function()
|
||||
local data = lovebird.pages[page](lovebird, req)
|
||||
local contenttype = "text/html"
|
||||
if string.match(page, "%.json$") then
|
||||
contenttype = "application/json"
|
||||
end
|
||||
str = "HTTP/1.1 200 OK\r\n" ..
|
||||
"Content-Type: " .. contenttype .. "\r\n" ..
|
||||
"Content-Length: " .. #data .. "\r\n" ..
|
||||
"\r\n" .. data
|
||||
end, lovebird.onerror)
|
||||
return str
|
||||
end
|
||||
|
||||
|
||||
function lovebird.receive(client, pattern)
|
||||
while 1 do
|
||||
local data, msg = client:receive(pattern)
|
||||
if not data then
|
||||
if msg == "timeout" then
|
||||
-- Wait for more data
|
||||
coroutine.yield(true)
|
||||
else
|
||||
-- Disconnected -- yielding nil means we're done
|
||||
coroutine.yield(nil)
|
||||
end
|
||||
else
|
||||
return data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.send(client, data)
|
||||
local idx = 1
|
||||
while idx < #data do
|
||||
local res, msg = client:send(data, idx)
|
||||
if not res and msg == "closed" then
|
||||
-- Handle disconnect
|
||||
coroutine.yield(nil)
|
||||
else
|
||||
idx = idx + res
|
||||
coroutine.yield(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function lovebird.onconnect(client)
|
||||
-- Create request table
|
||||
local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
|
||||
local req = {}
|
||||
req.socket = client
|
||||
req.addr, req.port = client:getsockname()
|
||||
req.request = lovebird.receive(client, "*l")
|
||||
req.method, req.url, req.proto = req.request:match(requestptn)
|
||||
req.headers = {}
|
||||
while 1 do
|
||||
local line, msg = lovebird.receive(client, "*l")
|
||||
if not line or #line == 0 then break end
|
||||
local k, v = line:match("(.-):%s*(.*)$")
|
||||
req.headers[k] = v
|
||||
end
|
||||
if req.headers["Content-Length"] then
|
||||
req.body = lovebird.receive(client, req.headers["Content-Length"])
|
||||
end
|
||||
-- Parse body
|
||||
req.parsedbody = {}
|
||||
if req.body then
|
||||
for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
|
||||
req.parsedbody[k] = lovebird.unescape(v)
|
||||
end
|
||||
end
|
||||
-- Parse request line's url
|
||||
req.parsedurl = lovebird.parseurl(req.url)
|
||||
-- Handle request; get data to send and send
|
||||
local data = lovebird.onrequest(req)
|
||||
lovebird.send(client, data)
|
||||
-- Clear up
|
||||
client:close()
|
||||
end
|
||||
|
||||
|
||||
function lovebird.update()
|
||||
if not lovebird.inited then lovebird.init() end
|
||||
-- Handle new connections
|
||||
while 1 do
|
||||
-- Accept new connections
|
||||
local client = lovebird.server:accept()
|
||||
if not client then break end
|
||||
client:settimeout(0)
|
||||
local addr = client:getsockname()
|
||||
if lovebird.checkwhitelist(addr) then
|
||||
-- Connection okay -- create and add coroutine to set
|
||||
local conn = coroutine.wrap(function()
|
||||
xpcall(function() lovebird.onconnect(client) end, function() end)
|
||||
end)
|
||||
lovebird.connections[conn] = true
|
||||
else
|
||||
-- Reject connection not on whitelist
|
||||
lovebird.trace("got non-whitelisted connection attempt: ", addr)
|
||||
client:close()
|
||||
end
|
||||
end
|
||||
-- Handle existing connections
|
||||
for conn in pairs(lovebird.connections) do
|
||||
-- Resume coroutine, remove if it has finished
|
||||
local status = conn()
|
||||
if status == nil then
|
||||
lovebird.connections[conn] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return lovebird
|
269
src/lurker.lua
Normal file
269
src/lurker.lua
Normal file
@@ -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()
|
Reference in New Issue
Block a user