pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io Singleton { id: root enum Level { Debug, Info, Warn, Error, Fatal } readonly property int level: _parseLevel(Quickshell.env("DMS_LOG_LEVEL")) readonly property string levelName: _levelName(level) readonly property string _logFilePath: Quickshell.env("DMS_LOG_FILE") || "" readonly property bool _useColor: !Quickshell.env("NO_COLOR") && Quickshell.env("DMS_LOG_NO_COLOR") !== "1" function scoped(module) { return { debug: function () { root._emit(Log.Level.Debug, module, arguments); }, info: function () { root._emit(Log.Level.Info, module, arguments); }, warn: function () { root._emit(Log.Level.Warn, module, arguments); }, error: function () { root._emit(Log.Level.Error, module, arguments); }, fatal: function () { root._emit(Log.Level.Fatal, module, arguments); } }; } function debug() { _emit(Log.Level.Debug, "", arguments); } function info() { _emit(Log.Level.Info, "", arguments); } function warn() { _emit(Log.Level.Warn, "", arguments); } function error() { _emit(Log.Level.Error, "", arguments); } function fatal() { _emit(Log.Level.Fatal, "", arguments); } function callStack() { const trace = _captureStack(0).split("\n").map(l => l.trim()).filter(l => l.length > 0); _emit(Log.Level.Info, "Debug", ["--------------------------"]); _emit(Log.Level.Info, "Debug", ["Current call stack"]); for (const line of trace) _emit(Log.Level.Info, "Debug", ["- " + line]); _emit(Log.Level.Info, "Debug", ["--------------------------"]); } function _parseLevel(name) { switch ((name || "").toLowerCase()) { case "debug": return Log.Level.Debug; case "warn": case "warning": return Log.Level.Warn; case "error": return Log.Level.Error; case "fatal": return Log.Level.Fatal; default: return Log.Level.Info; } } function _levelName(lvl) { switch (lvl) { case Log.Level.Debug: return "debug"; case Log.Level.Info: return "info"; case Log.Level.Warn: return "warn"; case Log.Level.Error: return "error"; case Log.Level.Fatal: return "fatal"; } return "info"; } function _levelTag(lvl, color) { let tag, ansi; switch (lvl) { case Log.Level.Fatal: tag = " FATAL"; ansi = "\x1b[31m"; break; case Log.Level.Error: tag = " ERROR"; ansi = "\x1b[91m"; break; case Log.Level.Warn: tag = " WARN"; ansi = "\x1b[33m"; break; case Log.Level.Info: tag = " INFO"; ansi = "\x1b[32m"; break; case Log.Level.Debug: tag = " DEBUG"; ansi = "\x1b[34m"; break; default: return " INFO"; } if (!color) return tag; return ansi + tag + "\x1b[0m"; } function _stringify(v) { if (v === null) return "null"; if (v === undefined) return "undefined"; if (typeof v === "string") return v; if (v instanceof Error) return v.toString(); try { return JSON.stringify(v); } catch (e) { return String(v); } } function _captureStack(skip) { try { throw new Error(); } catch (e) { const lines = (e.stack || "").split("\n"); return lines.slice(1 + (skip || 0)).join("\n"); } } function _callerLocation() { const stack = _captureStack(2); const lines = stack.split("\n"); for (const line of lines) { const m = line.match(/([^/@\s]+\.qml):(\d+)/); if (!m) continue; if (m[1] === "Log.qml") continue; return { file: m[1], line: m[2] }; } return null; } function _emit(lvl, module, args) { if (lvl < root.level) return; const argList = Array.from(args); const loc = _callerLocation(); const msg = argList.map(_stringify).join(" "); let tag; if (module && loc && loc.file === module + ".qml") tag = "[" + module + ":" + loc.line + "] "; else if (module && loc) tag = "[" + module + "] (" + loc.file + ":" + loc.line + ") "; else if (module) tag = "[" + module + "] "; else if (loc) tag = "(" + loc.file + ":" + loc.line + ") "; else tag = ""; const body = tag + msg; switch (lvl) { case Log.Level.Debug: console.debug(body); break; case Log.Level.Info: console.info(body); break; case Log.Level.Warn: console.warn(body); break; case Log.Level.Error: case Log.Level.Fatal: console.error(body); break; } if (root._logFilePath && fileTee.running) fileTee.write(_levelTag(lvl, false) + " qml: " + body + "\n"); if (lvl === Log.Level.Fatal) Qt.callLater(() => Qt.exit(1)); } Process { id: fileTee command: ["sh", "-c", "exec tee -a \"$0\" >/dev/null", root._logFilePath] stdinEnabled: true running: root._logFilePath.length > 0 } }