UNPKG

hw-server

Version:
716 lines (681 loc) 24.9 kB
/*global module,require,process*/ const utils = require("./utils.js"); const Rule = require("./rule.js"); const Log = require("./log.js"); const readline = require("readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer: completer }); var currentPrompt; function promptify(p) { const zeroify = (s) => { const str = s + ""; return "00".substr(0, 2 - str.length) + str; }; const d = new Date(); const fmt = zeroify(d.getHours()) + ":" + zeroify(d.getMinutes()) + "." + zeroify(d.getSeconds()); return fmt + " " + p; } function prompt() { rl.setPrompt(promptify(currentPrompt)); rl.prompt(); } function setPrompt(p) { currentPrompt = p; rl.setPrompt(promptify(p)); } function lineFixup(line) { // replace all consecutive spaces with one space return line.replace(/\s+/g, " "); } function construct(constructor, args) { function F() { return constructor.apply(this, args); } F.prototype = constructor.prototype; try { var f = new F(); } catch (e) { Console.log("Couldn't create", e); return undefined; } return f; } const data = { state: [], homework: undefined, LogOut: undefined, LogError: undefined, get currentState() { return this.state[this.state.length - 1]; }, get prompt() { return this.currentState.prompt; }, set prompt(p) { var s = this.currentState; s.prompt = p; setPrompt(s.prompt); }, applyState: function() { var s = this.currentState; setPrompt(s.prompt); }, gotoState: function(pos) { while (this.state.length > pos) this.state.pop(); } }; utils.onify(data); data._initOns(); const states = { default: { prompt: "homework> ", completions: ["rule ", "shutdown", "device", "eval", "timers", "variables"], apply: function(line) { if (line === "shutdown") { data._emit("shutdown"); return; } var elems = line.split(' ').filter((e) => { return e.length > 0; }); switch (elems[0]) { case "rule": if (elems.length < 2) { console.log("rule needs a name"); return; } data.state.push(states.rule); states.rule._name = elems[1]; data.applyState(); break; case "device": data.state.push(states.device); states.device._device = undefined; data.applyState(); break; case "eval": data.state.push(states.eval); data.applyState(); break; case "timers": data.state.push(states.timers); data.applyState(); break; case "variables": data.state.push(states.variables); data.applyState(); break; default: if (typeof states.default._extraCommands === "object" && elems[0] in states.default._extraCommands) { data.state.push(states.default._extraCommands[elems[0]]); data.applyState(); break; } console.log("unknown command", elems[0]); break; } } }, rule: { prompt: "rule> ", completions: ["if", "home", "shutdown"], apply: function(line) { switch (line) { case "if": data.state.push(states.ruleif); data.applyState(); break; case "home": data.gotoState(1); data.applyState(); break; case "shutdown": data._emit("shutdown"); break; } }, _name: undefined, _mode: "event", _events: [[]], _actions: [], _clear: function() { this._name = undefined; this._mode = "event"; this._events = [[]]; this._actions = []; }, _currentEvent: function() { return this._events[this._events.length - 1]; }, _createEvent: function() { this._events.push([]); return this._currentEvent(); } }, ruleif: { prompt: "rule if> ", completions: function(line) { var candidates = ["home"], event; const elems = line ? line.split(' ') : []; if (states.rule._mode !== "event" && states.rule._currentEvent().length > 0) { candidates = ["and", "or", "then"]; if (!elems.length) return candidates; return candidates.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); } const events = data.homework.events; if (!line) { for (event in events) { candidates.push(event + " "); } } else { if (elems.length === 1) { // make sure we have a complete keyword for (event in events) { candidates.push(event + " "); } candidates = candidates.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0 && c.length > elems[elems.length - 1].length + 1; }); if (candidates.length > 0) return candidates; } if (elems[0] in events) { event = events[elems[0]]; var args = elems.slice(0); args.splice(0, 1); if (args.length === 1 || (args.length > 1 && args[args.length - 1] !== "")) args.splice(args.length - 1, 1); args = args.map((e) => { return e.replace(/_/g, " "); }); var eventcomps = event.completion.apply(null, args).values.map((e) => { return e.replace(/ /g, "_") + " "; }); if (!(eventcomps instanceof Array)) return eventcomps; if (elems.length === 1) { candidates = eventcomps; } else { candidates = eventcomps.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); } } } return candidates; }, apply: function(line) { const events = data.homework.events; var elems = line.split(' ').filter((e) => { return e.length > 0; }); if (states.rule._currentEvent().length > 0) { if (elems[0] === "and") { states.rule._mode = "event"; data.state.push(states.ruleand); data.applyState(); return; } if (elems[0] === "or") { states.rule._mode = "event"; states.rule._createEvent(); data.state.push(states.ruleor); data.applyState(); return; } if (elems[0] === "then") { // console.log(states.rule._events); states.rule._mode = "then"; data.state.push(states.rulethen); data.applyState(); return; } } if (elems[0] === "save") { console.log("can only save in state then"); return; } if (elems[0] === "home") { states.rule._clear(); data.gotoState(1); data.applyState(); return; } if (elems[0] in events) { const event = events[elems[0]]; elems.splice(0, 1); const created = construct(event.ctor, elems.map((e) => { return e.replace(/_/g, " "); })); if (!created) return; //console.log("got event", created); states.rule._currentEvent().push(created); } else { console.log("no such event", elems[0]); } states.rule._mode = "next"; } }, rulethen: { prompt: "rule then> ", completions: function(line) { var candidates = ["home"], action; if (states.rule._actions.length > 0) { candidates.push("save"); } const actions = data.homework.actions; if (!line) { for (action in actions) { candidates.push(action + " "); } } else { const elems = line.split(' '); if (elems.length === 1) { // make sure we have a complete keyword for (action in actions) { candidates.push(action + " "); } candidates = candidates.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0 && c.length > elems[elems.length - 1].length + 1; }); if (candidates.length > 0) return candidates; } if (elems[0] in actions) { action = actions[elems[0]]; var args = elems.slice(0); args.splice(0, 1); if (args.length === 1 || (args.length > 1 && args[args.length - 1] !== "")) args.splice(args.length - 1, 1); args = args.map((e) => { return e.replace(/_/g, " "); }); var actioncomps = action.completion.apply(null, args).values.map((e) => { return e.replace(/ /g, "_") + " "; }); if (!(actioncomps instanceof Array)) return actioncomps; if (elems.length === 1) { candidates = actioncomps; } else { candidates = actioncomps.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); } } } return candidates; }, apply: function(line) { const actions = data.homework.actions; var elems = line.split(' ').filter((e) => { return e.length > 0; }); if (elems[0] === "save") { var rule = new Rule(states.rule._name); rule.then.apply(rule, states.rule._actions); for (var i = 0; i < states.rule._events.length; ++i) { rule.and.apply(rule, states.rule._events[i]); } data.homework.addRule(rule); data.homework.saveRules(); states.rule._clear(); data.gotoState(1); data.applyState(); return; } if (elems[0] === "home") { states.rule._clear(); data.gotoState(1); data.applyState(); return; } if (elems[0] in actions) { const action = actions[elems[0]]; elems.splice(0, 1); const created = construct(action.ctor, elems.map((e) => { return e.replace(/_/g, " "); })); if (!created) return; //console.log("got action", created); states.rule._actions.push(created); } else { console.log("no such action", elems[0]); } } }, device: { prompt: "device> ", completions: function(line) { var candidates = []; const elems = line ? line.split(' ') : []; if (states.device._value !== undefined && elems.length > 1 && elems[0] === "set") { var vs = states.device._value.values; candidates = (typeof vs === "object") ? Object.keys(vs) : []; } else { candidates.push("home"); const devices = data.homework.devices; if (states.device._device === undefined) { // list all devices for (var i = 0; i < devices.length; ++i) { candidates.push(devices[i].name.replace(/ /g, "_")); } } else { candidates.push("back"); if (states.device._value === undefined) { // list all values for (var k in states.device._device.values) { candidates.push(k.replace(/ /g, "_")); } candidates.push("rename "); } else { candidates.push("get"); candidates.push("set "); } } } if (elems.length > 0) return candidates.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); return candidates; }, apply: function(line) { const devices = data.homework.devices; const elems = line ? line.split(' ').filter((e) => { return e.length > 0; }) : []; var state; if (!elems.length) return; if (elems[0] === "home") { states.device._clear(); data.gotoState(1); data.applyState(); return; } if (states.device._device === undefined) { // find and set the device for (var i = 0; i < devices.length; ++i) { if (devices[i].name.replace(/ /g, "_") === elems[0]) { states.device._device = devices[i]; state = { prompt: "device " + devices[i].name + "> ", completions: states.device.completions, apply: states.device.apply }; data.state.push(state); data.applyState(); } } } else { if (elems[0] === "back") { var ok = false; if (states.device._value !== undefined) { states.device._value = undefined; ok = true; } else if (states.device._device !== undefined) { states.device._device = undefined; ok = true; } if (ok) { data.state.pop(); data.applyState(); } return; } else if (elems[0] === "rename" && states.device._device !== undefined) { var newname = /rename\s+([^\s].*)/.exec(line); if (newname instanceof Array && newname.length === 2) { states.device._device.name = newname[1]; data.prompt = "device " + newname[1] + "> "; data.homework.saveDevices(); } } if (states.device._value === undefined) { // find and set the value const values = states.device._device.values; const vn = elems[0].replace(/_/g, " "); if (vn in values) { states.device._value = values[vn]; state = { prompt: "device " + states.device._device.name + " " + states.device._value.name + "> ", completions: states.device.completions, apply: states.device.apply }; data.state.push(state); data.applyState(); } } else { if (elems[0] === "set") { if (elems.length === 1) { var val = parseInt(elems[1]); if (val + "" === elems[1]) states.device._value.value = val; } else { var newval = /set\s+([^\s].*)/.exec(line); if (newval instanceof Array && newval.length === 2) { states.device._value.value = newval[1]; } else { Console.log("couldn't set value"); } } } else if (elems[0] === "get") { Console.log(states.device._value.value); } } } }, _device: undefined, _value: undefined, _clear: function() { this._device = undefined; this._value = undefined; } }, eval: { prompt: "eval> ", completions: function(line) { return []; }, apply: function(line) { if (line === "back" || line === "home") { states.device._clear(); data.gotoState(1); data.applyState(); return; } try { eval(line); } catch (e) { Console.error("eval error", e); } } }, timers: { prompt: "timers> ", completions: function(line) { var candidates = [["home", "create ", "destroy "], ["timeout ", "interval ", "schedule "]]; var ret = []; const elems = line ? lineFixup(line).split(' ') : []; if (elems.length === 0) { return candidates[0]; } else if (elems.length <= candidates.length) { // verify that we have a full word ret = candidates[elems.length - 1].filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0 && c.length > elems[elems.length - 1].length + 1; }); if (ret.length > 0) return ret; } var stripped = utils.strip(elems); if ((stripped.length === 2 || stripped.length === 3) && stripped[0] === "destroy") { // complete on timer names ret = data.homework.Timer.names(stripped[1]); } return ret.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); }, apply: function(line) { if (line === "back" || line === "home") { data.gotoState(1); data.applyState(); return; } const elems = line ? line.split(' ').filter((e) => { return e.length > 0; }) : []; if (!elems.length) return; if (elems.length < 3) { Console.error("Need at least 3 arguments"); return; } if (elems[0] === "create") { try { if (data.homework.Timer.create.apply(data.homework.Timer, elems.slice(1))) { Console.log("created"); } else { Console.log("couldn't create"); } } catch (e) { Console.error("Couldn't create timer", e); } } else if (elems[0] === "destroy") { data.homework.Timer.destroy(elems[1], elems[2]); } } }, variables: { prompt: "variables> ", completions: function(line) { var candidates = ["home", "create ", "destroy "]; var ret = []; const elems = line ? lineFixup(line).split(' ') : []; if (elems.length === 0) { return candidates; } else if (elems.length === 1) { // verify that we have a full word ret = candidates.filter((c) => { return c.indexOf(elems[0]) === 0 && c.length > elems[0].length + 1; }); if (ret.length > 0) return ret; } var stripped = utils.strip(elems); if ((stripped.length === 1 || stripped.length === 2) && stripped[0] === "destroy") { // complete on variable names ret = data.homework.Variable.names(); } return ret.filter((c) => { return c.indexOf(elems[elems.length - 1]) === 0; }); }, apply: function(line) { if (line === "back" || line === "home") { data.gotoState(1); data.applyState(); return; } const elems = line ? line.split(' ').filter((e) => { return e.length > 0; }) : []; if (!elems.length) return; if (elems.length < 2) { Console.error("Need at least 2 arguments"); return; } if (elems[0] === "create") { try { if (data.homework.Variable.create(elems[1])) { Console.log("created"); } else { Console.log("couldn't create"); } } catch (e) { Console.error("Couldn't create variable", e); } } else if (elems[0] === "destroy") { data.homework.Variable.destroy(elems[1]); } } } }; states.ruleand = { prompt: "rule and> ", completions: states.ruleif.completions, apply: states.ruleif.apply }; states.ruleor = { prompt: "rule or> ", completions: states.ruleif.completions, apply: states.ruleif.apply }; function completer(partial, callback) { //callback(null, [['123'], partial]); const state = data.currentState; if (partial.length === 0) { const comps = (typeof state.completions === "function") ? state.completions() : state.completions; callback(null, [comps, partial]); return; } var hits; const what = partial.split(' '); const last = what[what.length - 1]; if (typeof state.completions === "function") hits = state.completions(partial); else { if (last !== "") hits = state.completions.filter((c) => { return c.indexOf(partial) === 0; }); else hits = []; } callback(null, [hits, last]); } function Console() { } Console.init = function(homework) { data.homework = homework; data.LogOut = new Log(console.log); data.LogOut.on("log", function(l) { data._emit("logOut", l); }); data.LogError = new Log(console.error); data.LogError.on("log", function(l) { data._emit("logError", l); }); rl.on("line", (line) => { // console.log(`hey ${line}`); data.currentState.apply(line); prompt(); }); rl.on("SIGINT", () => { data._emit("shutdown"); }); data.state.push(states.default); data.applyState(); prompt(); }; Console.logs = function(type) { switch (type) { case "out": return data.LogOut.logs; case "error": return data.LogError.logs; } return undefined; }; Console.cleanup = function() { data.LogOut.close(); data.LogError.close(); }; Console.log = function() { data.LogOut.log.apply(data.LogOut, arguments); prompt(); }; Console.error = function() { data.LogError.log.apply(data.LogError, arguments); prompt(); }; Console.registerCommand = function(parent, name, section) { states[parent].completions.push(name); if (!states[parent]._extraCommands) states[parent]._extraCommands = Object.create(null); states[parent]._extraCommands[name] = section; states[name] = section; }; Console.home = function() { data.gotoState(1); data.applyState(); }; Console.on = data.on.bind(data); module.exports = Console;