diff --git a/esphomeyaml/dashboard/dashboard.py b/esphomeyaml/dashboard/dashboard.py index 676e6a7181..86f4c48b99 100644 --- a/esphomeyaml/dashboard/dashboard.py +++ b/esphomeyaml/dashboard/dashboard.py @@ -78,8 +78,6 @@ class EsphomeyamlCommandWebSocket(tornado.websocket.WebSocketHandler): data = yield self.proc.stdout.read_until_regex('[\n\r]') except tornado.iostream.StreamClosedError: break - if data.endswith('\r') and random.randrange(100) < 90: - continue try: data = data.replace('\033', '\\033') except UnicodeDecodeError: diff --git a/esphomeyaml/dashboard/static/esphomeyaml.css b/esphomeyaml/dashboard/static/esphomeyaml.css index 5443cf01bb..7693bab5eb 100644 --- a/esphomeyaml/dashboard/static/esphomeyaml.css +++ b/esphomeyaml/dashboard/static/esphomeyaml.css @@ -57,42 +57,27 @@ i.very-large { font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; } -.log.bold { - font-weight: bold; -} - -.log .v { - color: #888888; -} - -.log .d { - color: #00DDDD; -} - -.log .c { - color: magenta; -} - -.log .i { - color: limegreen; -} - -.log .w { - color: yellow; -} - -.log .e { - color: red; - font-weight: bold; -} - -.log .e { - color: red; -} - -.log .ww { - color: white; -} +.log-bold { font-weight: bold; } +.log-italic { font-style: italic; } +.log-underline { text-decoration: underline; } +.log-strikethrough { text-decoration: line-through; } +.log-underline.log-strikethrough { text-decoration: underline line-through; } +.log-fg-black { color: rgb(128,128,128); } +.log-fg-red { color: rgb(255,0,0); } +.log-fg-green { color: rgb(0,255,0); } +.log-fg-yellow { color: rgb(255,255,0); } +.log-fg-blue { color: rgb(0,0,255); } +.log-fg-magenta { color: rgb(255,0,255); } +.log-fg-cyan { color: rgb(0,255,255); } +.log-fg-white { background-color: rgb(255,255,255); } +.log-bg-black { background-color: rgb(0,0,0); } +.log-bg-red { background-color: rgb(255,0,0); } +.log-bg-green { background-color: rgb(0,255,0); } +.log-bg-yellow { background-color: rgb(255,255,0); } +.log-bg-blue { background-color: rgb(0,0,255); } +.log-bg-magenta { background-color: rgb(255,0,255); } +.log-bg-cyan { background-color: rgb(0,255,255); } +.log-bg-white { background-color: rgb(255,255,255); } .modal { width: 95%; diff --git a/esphomeyaml/dashboard/static/esphomeyaml.js b/esphomeyaml/dashboard/static/esphomeyaml.js index a14850fbc2..35fdc07909 100644 --- a/esphomeyaml/dashboard/static/esphomeyaml.js +++ b/esphomeyaml/dashboard/static/esphomeyaml.js @@ -1,26 +1,153 @@ document.addEventListener('DOMContentLoaded', () => { - M.AutoInit(document.body); - }); + M.AutoInit(document.body); +}); -const colorReplace = (input) => { - input = input.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); - input = input.replace(/\\033\[(?:0;)?31m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?31m/g, ''); - input = input.replace(/\\033\[(?:0;)?32m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?32m/g, ''); - input = input.replace(/\\033\[(?:0;)?33m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?33m/g, ''); - input = input.replace(/\\033\[(?:0;)?35m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?35m/g, ''); - input = input.replace(/\\033\[(?:0;)?36m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?36m/g, ''); - input = input.replace(/\\033\[(?:0;)?37m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?37m/g, ''); - input = input.replace(/\\033\[(?:0;)?38m/g, ''); - input = input.replace(/\\033\[(?:0?1;)?38m/g, ''); - input = input.replace(/\\033\[0m/g, ''); +const initializeColorState = () => { + return { + bold: false, + italic: false, + underline: false, + strikethrough: false, + foregroundColor: false, + backgroundColor: false, + carriageReturn: false, + }; +}; - return input; +const colorReplace = (pre, state, text) => { + const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; + let i = 0; + + if (state.carriageReturn) { + pre.removeChild(pre.lastChild); + state.carriageReturn = false; + } + + if (text.includes("\r")) { + state.carriageReturn = true; + } + + const lineSpan = document.createElement("span"); + lineSpan.classList.add("line"); + pre.appendChild(lineSpan); + + const addSpan = (content) => { + if (content === "") + return; + + const span = document.createElement("span"); + if (state.bold) span.classList.add("log-bold"); + if (state.italic) span.classList.add("log-italic"); + if (state.underline) span.classList.add("log-underline"); + if (state.strikethrough) span.classList.add("log-strikethrough"); + if (state.foregroundColor !== null) span.classList.add(`log-fg-${state.foregroundColor}`); + if (state.backgroundColor !== null) span.classList.add(`log-bg-${state.backgroundColor}`); + span.appendChild(document.createTextNode(content)); + lineSpan.appendChild(span); + }; + + + while (true) { + const match = re.exec(text); + if (match === null) + break; + + const j = match.index; + addSpan(text.substring(i, j)); + i = j + match[0].length; + + if (match[1] === undefined) continue; + + for (const colorCode of match[1].split(";")) { + switch (parseInt(colorCode)) { + case 0: + // reset + state.bold = false; + state.italic = false; + state.underline = false; + state.strikethrough = false; + state.foregroundColor = null; + state.backgroundColor = null; + break; + case 1: + state.bold = true; + break; + case 3: + state.italic = true; + break; + case 4: + state.underline = true; + break; + case 9: + state.strikethrough = true; + break; + case 22: + state.bold = false; + break; + case 23: + state.italic = false; + break; + case 24: + state.underline = false; + break; + case 29: + state.strikethrough = false; + break; + case 30: + state.foregroundColor = "black"; + break; + case 31: + state.foregroundColor = "red"; + state.bold = true; + break; + case 32: + state.foregroundColor = "green"; + break; + case 33: + state.foregroundColor = "yellow"; + break; + case 34: + state.foregroundColor = "blue"; + break; + case 35: + state.foregroundColor = "magenta"; + break; + case 36: + state.foregroundColor = "cyan"; + break; + case 37: + case 39: + state.foregroundColor = null; + break; + case 41: + state.backgroundColor = "red"; + break; + case 42: + state.backgroundColor = "green"; + break; + case 43: + state.backgroundColor = "yellow"; + break; + case 44: + state.backgroundColor = "blue"; + break; + case 45: + state.backgroundColor = "magenta"; + break; + case 46: + state.backgroundColor = "cyan"; + break; + case 47: + state.backgroundColor = "white"; + break; + case 40: + case 49: + state.backgroundColor = null; + break; + } + } + } + addSpan(text.substring(i)); }; const removeUpdateAvailable = (filename) => { @@ -144,6 +271,7 @@ document.querySelectorAll(".action-show-logs").forEach((showLogs) => { const modalInstance = M.Modal.getInstance(logsModalElem); const log = logsModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = logsModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -156,8 +284,7 @@ document.querySelectorAll(".action-show-logs").forEach((showLogs) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); @@ -192,6 +319,7 @@ document.querySelectorAll(".action-upload").forEach((upload) => { const modalInstance = M.Modal.getInstance(uploadModalElem); const log = uploadModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -204,8 +332,7 @@ document.querySelectorAll(".action-upload").forEach((upload) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); @@ -241,6 +368,7 @@ document.querySelectorAll(".action-validate").forEach((upload) => { const modalInstance = M.Modal.getInstance(validateModalElem); const log = validateModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = validateModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -253,8 +381,7 @@ document.querySelectorAll(".action-validate").forEach((upload) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({ @@ -296,6 +423,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => { const modalInstance = M.Modal.getInstance(compileModalElem); const log = compileModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = compileModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -310,8 +438,7 @@ document.querySelectorAll(".action-compile").forEach((upload) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); @@ -354,6 +481,7 @@ document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { const modalInstance = M.Modal.getInstance(cleanMqttModalElem); const log = cleanMqttModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -366,8 +494,7 @@ document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { stopLogsButton.innerHTML = "Close"; stopped = true; @@ -396,6 +523,7 @@ document.querySelectorAll(".action-clean").forEach((btn) => { const modalInstance = M.Modal.getInstance(cleanModalElem); const log = cleanModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = cleanModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -408,8 +536,7 @@ document.querySelectorAll(".action-clean").forEach((btn) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); @@ -444,6 +571,7 @@ document.querySelectorAll(".action-hass-config").forEach((btn) => { const modalInstance = M.Modal.getInstance(hassConfigModalElem); const log = hassConfigModalElem.querySelector(".log"); log.innerHTML = ""; + const colorState = initializeColorState(); const stopLogsButton = hassConfigModalElem.querySelector(".stop-logs"); let stopped = false; stopLogsButton.innerHTML = "Stop"; @@ -456,8 +584,7 @@ document.querySelectorAll(".action-hass-config").forEach((btn) => { logSocket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - const msg = data.data; - log.insertAdjacentHTML('beforeend', colorReplace(msg)); + colorReplace(log, colorState, data.data); } else if (data.event === "exit") { if (data.code === 0) { M.toast({html: "Program exited successfully."}); diff --git a/esphomeyaml/storage_json.py b/esphomeyaml/storage_json.py index 1fbc168946..d9b83170e2 100644 --- a/esphomeyaml/storage_json.py +++ b/esphomeyaml/storage_json.py @@ -253,7 +253,7 @@ class CheckForUpdateThread(threading.Thread): req = requests.get('{}/_static/version'.format(self.docs_base)) req.raise_for_status() - storage.remote_version = req.text + storage.remote_version = req.text.strip() storage.last_update_check = datetime.utcnow() storage.save(self._path) return storage diff --git a/esphomeyaml/util.py b/esphomeyaml/util.py index bb9f4b3e66..918bd8c2b0 100644 --- a/esphomeyaml/util.py +++ b/esphomeyaml/util.py @@ -5,6 +5,8 @@ import logging import re import sys +from esphomeyaml import core + _LOGGER = logging.getLogger(__name__) @@ -48,6 +50,21 @@ def shlex_quote(s): return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" +class RedirectText(object): + def __init__(self, out): + self._out = out + + def write(self, s): + s = s.replace('\033', '\\033') + self._out.write(s) + + def flush(self): + self._out.flush() + + def isatty(self): + return True + + def run_external_command(func, *cmd, **kwargs): def mock_exit(return_code): raise SystemExit(return_code) @@ -57,6 +74,10 @@ def run_external_command(func, *cmd, **kwargs): full_cmd = u' '.join(shlex_quote(x) for x in cmd) _LOGGER.info(u"Running: %s", full_cmd) + if core.FROM_DASHBOARD: + sys.stdout = RedirectText(sys.stdout) + sys.stderr = RedirectText(sys.stderr) + capture_stdout = kwargs.get('capture_stdout', False) if capture_stdout: sys.stdout = io.BytesIO() @@ -76,6 +97,11 @@ def run_external_command(func, *cmd, **kwargs): sys.argv = orig_argv sys.exit = orig_exit + if isinstance(sys.stdout, RedirectText): + sys.stdout = sys.__stdout__ + if isinstance(sys.stderr, RedirectText): + sys.stderr = sys.__stderr__ + if capture_stdout: # pylint: disable=lost-exception stdout = sys.stdout.getvalue()