Compare commits

...
2 Commits
42 changed files with 3748 additions and 1475 deletions

No files matched your search

+52 -1
View File
@@ -82,6 +82,40 @@ IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend.
BSP setup, and bumps the policy epoch so every AP re-applies its HWP BSP setup, and bumps the policy epoch so every AP re-applies its HWP
request (and re-checks C1E) on its next tick. request (and re-checks C1E) on its next tick.
### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`)
Battery state comes from the ACPI Control Method Battery
(`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`:
- `_STA` bit 4 says whether a pack is installed. A laptop with the pack
removed still has the device, which is why "has a battery device" and
"has a battery" are separate answers.
- `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh),
the design capacity, and the last full charge capacity.
- `_BST` gives the charge/discharge state, the present rate, the remaining
capacity and the present voltage.
- `_PSR` on the adapter says whether it is plugged in.
Percentage is remaining / last-full, clamped to 100 and falling back to the
design capacity when firmware reports last-full as zero. Results are cached
for two seconds so that a panel repaint does not turn into EC traffic.
Almost all of this reaches the CPU through the **embedded controller**.
`Hal::EmbeddedController` finds the EC's command/data port pair from the
ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09`
device, then drives the polled read/write protocol (`0x80`/`0x81` with
OBF/IBF handshaking). It is wired into the AML interpreter as the
`EmbeddedControl` region space, so any `OperationRegion` in the DSDT that
targets the EC now works -- battery methods being the first user.
Supporting `_BST` also needed real package element storage in the AML
interpreter. Firmware overwhelmingly builds its result package with
`Store(x, Index(BST0, n))`, which requires `Index()` to name a writable
element rather than yield a value. `AML::PackageStore` provides that: a
persistent region for packages declared with `Name()`, and a transient ring
rewound at each top-level evaluation for packages built inside a method, so
polling `_BST` cannot exhaust it.
## Diagnostics ## Diagnostics
`SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature, `SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature,
@@ -90,7 +124,15 @@ CPUID base/turbo frequency, the measured average active frequency
(APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint. (APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint.
Returns -1 when the CPU exposes no power features (e.g. under QEMU). Returns -1 when the CPU exposes no power features (e.g. under QEMU).
The `power` program (`programs/src/power`) prints it: `SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge
state, AC presence, capacities, present rate, voltage and an estimated time
remaining. It returns -1 on a machine with no battery device at all, which
is how the desktop panel decides not to draw an indicator. Like
`SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has
to work in an unprivileged session, which holds no device capability to
spend on it.
The `power` program (`programs/src/power`) prints both:
``` ```
power one-shot snapshot power one-shot snapshot
@@ -104,8 +146,17 @@ Boot log lines to look for (Kernel Log app):
[CpuPower] HWP enabled: perf range 4-54, EPP=0x80 [CpuPower] HWP enabled: perf range 4-54, EPP=0x80
[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ... [CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ...
[CpuIdle] AP deep idle: MWAIT hint 0x20 [CpuIdle] AP deep idle: MWAIT hint 0x20
[EC] Embedded Controller at cmd=66 data=62
[Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present)
``` ```
The desktop panel shows a battery icon left of the Wi-Fi and network icons,
using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per
10% step, reloaded only when the step changes). Clicking it opens a popout
with the percentage in numbers, the charge state, time remaining and raw
capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a
desktop machine shows nothing there.
## Tuning ## Tuning
The governor constants live at the top of `Hal/CpuPower.cpp` The governor constants live at the top of `Hal/CpuPower.cpp`
+568 -50
View File
@@ -6,10 +6,13 @@
#include "AmlInterpreter.hpp" #include "AmlInterpreter.hpp"
#include <ACPI/ACPI.hpp> #include <ACPI/ACPI.hpp>
#include <ACPI/EmbeddedController.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Io/IoPort.hpp> #include <Io/IoPort.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
using namespace Kt; using namespace Kt;
@@ -41,13 +44,78 @@ namespace Hal {
return IsLeadNameChar(c) || (c >= '0' && c <= '9'); return IsLeadNameChar(c) || (c >= '0' && c <= '9');
} }
// The interpreter runs on whatever stack its caller has -- the boot
// stack early on, a thread's 16 KiB kernel stack once userspace is up.
// m_stackFloor is anchored at the top-level entry; a frame below it has
// spent the budget and must unwind rather than keep recursing.
bool Interpreter::HaveStackHeadroom(const void* frame) const {
if (m_stackFloor == 0) return true; // no evaluation in progress
if ((uint64_t)frame > m_stackFloor) return true;
// Say so once. A method that quietly gives up here returns a
// default value that looks like a hardware answer -- a battery
// reporting itself absent, say -- and nothing upstream can tell
// the difference.
static bool warned = false;
if (!warned) {
warned = true;
KernelLogStream(WARNING, "AML")
<< "Evaluation stopped: spent the " << base::dec
<< (uint64_t)(StackBudget / 1024) << " KiB stack allowance";
}
return false;
}
void Interpreter::ReportDepthLimit() {
static bool warned = false;
if (!warned) {
warned = true;
KernelLogStream(WARNING, "AML") << "Evaluation stopped: nesting past "
<< base::dec << (uint64_t)MaxExprDepth << " levels";
}
}
// True when [address, address+bytes) is inside the contiguous HHDM
// mapping and so safe to touch at phys+hhdmBase.
static bool HhdmReadable(uint64_t address, uint32_t bytes) {
uint64_t limit = Memory::VMM::Paging::HhdmLimit();
if (limit == 0) return false;
if (address >= limit) return false;
return (limit - address) >= bytes;
}
// ============================================================================ // ============================================================================
// Constructor // Constructor
// ============================================================================ // ============================================================================
Interpreter::Interpreter() Interpreter::Interpreter()
: m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false) {} : m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false),
m_ctxPool(nullptr), m_ctxInUse(0), m_evalNesting(0), m_stackFloor(0),
m_traceBudget(0) {}
// ============================================================================
// Execution context pool
// ============================================================================
Interpreter::ExecContext* Interpreter::AcquireContext() {
if (!m_ctxPool) {
m_ctxPool = (ExecContext*)Memory::g_heap->Request(
sizeof(ExecContext) * (MaxCallDepth + 1));
if (!m_ctxPool) return nullptr;
}
if (m_ctxInUse > MaxCallDepth) return nullptr;
ExecContext* ctx = &m_ctxPool[m_ctxInUse++];
*ctx = ExecContext{};
return ctx;
}
void Interpreter::ReleaseContext() {
if (m_ctxInUse > 0) m_ctxInUse--;
}
// ============================================================================ // ============================================================================
@@ -148,7 +216,9 @@ namespace Hal {
// Reads a NameString (which may include root prefix, parent prefixes, // Reads a NameString (which may include root prefix, parent prefixes,
// dual/multi name prefix) and produces an absolute path. // dual/multi name prefix) and produces an absolute path.
int Interpreter::ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode, int Interpreter::ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode,
char* outPath, int maxLen) { char* outPath, int maxLen,
char* outRelative, int relLen,
bool* outSearchable) {
int pathPos = 0; int pathPos = 0;
bool isAbsolute = false; bool isAbsolute = false;
int parentPrefixCount = 0; int parentPrefixCount = 0;
@@ -220,6 +290,22 @@ namespace Hal {
} }
outPath[pathPos] = '\0'; outPath[pathPos] = '\0';
// Hand back the name as written, for callers that must apply the
// ACPI search rule when the rebased path does not exist.
if (outSearchable)
*outSearchable = !isAbsolute && parentPrefixCount == 0 && segCount > 0;
if (outRelative && relLen > 0) {
int rp = 0;
for (int i = 0; i < segCount; i++) {
if (i > 0 && rp < relLen - 1) outRelative[rp++] = '.';
for (int j = 0; j < 4 && rp < relLen - 1; j++)
outRelative[rp++] = segments[i][j];
}
outRelative[rp] = '\0';
}
return pathPos; return pathPos;
} }
@@ -582,13 +668,17 @@ namespace Hal {
uint32_t fieldEnd = pkgStart + pkgLen; uint32_t fieldEnd = pkgStart + pkgLen;
if (fieldEnd > endOffset) fieldEnd = endOffset; if (fieldEnd > endOffset) fieldEnd = endOffset;
// Region name // Region name. A Field almost always names a region
// declared in an enclosing scope, so the search rule is not
// optional here.
char regionPath[256]; char regionPath[256];
ReadNameString(aml, pos, scopeNode, regionPath, 256); char regionRel[64];
bool regionSearchable = false;
ReadNameString(aml, pos, scopeNode, regionPath, 256,
regionRel, sizeof(regionRel), &regionSearchable);
int32_t regionNode = m_ns.FindNode(regionPath); int32_t regionNode = m_ns.FindNode(regionPath);
if (regionNode < 0) { if (regionNode < 0 && regionSearchable)
regionNode = m_ns.ResolveName(regionPath, scopeNode); regionNode = m_ns.SearchUpward(regionRel, scopeNode);
}
// Field flags // Field flags
uint8_t fieldFlags = aml[*pos]; uint8_t fieldFlags = aml[*pos];
@@ -833,34 +923,74 @@ namespace Hal {
auto* nsNode = m_ns.GetNode(node); auto* nsNode = m_ns.GetNode(node);
if (!nsNode || nsNode->Obj.Type != ObjectType::Method) return false; if (!nsNode || nsNode->Obj.Type != ObjectType::Method) return false;
ExecContext ctx{}; // Packages built inside the method come out of the transient ring.
ctx.Aml = m_dsdt; // Rewinding it here bounds the store to one evaluation's worth,
ctx.AmlBase = nsNode->Obj.Method.AmlOffset; // which is what makes repeated polling (_BST every few seconds)
ctx.AmlLength = nsNode->Obj.Method.AmlLength; // safe. The caller must read the returned package before the next
ctx.ScopeNode = node; // evaluation, which every caller in the kernel does.
ctx.Returned = false; // Evaluation is serialized: the execution contexts come from a
ctx.Broken = false; // shared pool, and the battery indicator now polls from a user
ctx.Depth = 0; // thread while other subsystems may evaluate AML of their own.
bool topLevel = false;
m_evalLock.Acquire();
if (m_evalNesting == 0) {
topLevel = true;
PackageStore::ResetTransient();
// Anchor the stack budget to this frame. Everything below
// measures against it, so the same limit works whether we were
// called on the large boot stack or a thread's 16 KiB one.
char stackMarker = 0;
m_stackFloor = (uint64_t)&stackMarker - StackBudget;
}
m_evalNesting++;
ExecContext* ctx = AcquireContext();
if (!ctx) {
m_evalNesting--;
m_evalLock.Release();
return false;
}
ctx->Aml = m_dsdt;
ctx->AmlBase = nsNode->Obj.Method.AmlOffset;
ctx->AmlLength = nsNode->Obj.Method.AmlLength;
ctx->ScopeNode = node;
ctx->Returned = false;
ctx->Broken = false;
ctx->Depth = 0;
ctx->ExprDepth = 0;
// Set up arguments // Set up arguments
for (int i = 0; i < argCount && i < MaxMethodArgs; i++) { for (int i = 0; i < argCount && i < MaxMethodArgs; i++) {
ctx.Args[i] = args[i]; ctx->Args[i] = args[i];
} }
// Initialize locals to zero // Initialize locals to zero
for (int i = 0; i < MaxMethodLocals; i++) { for (int i = 0; i < MaxMethodLocals; i++) {
ctx.Locals[i].Type = ObjectType::Integer; ctx->Locals[i].Type = ObjectType::Integer;
ctx.Locals[i].Integer = 0; ctx->Locals[i].Integer = 0;
} }
bool ok = ExecuteBlock(ctx, ctx.AmlBase, ctx.AmlBase + ctx.AmlLength); bool ok = ExecuteBlock(*ctx, ctx->AmlBase, ctx->AmlBase + ctx->AmlLength);
result = ctx.ReturnValue; result = ctx->ReturnValue;
if (result.Type == ObjectType::None) { if (result.Type == ObjectType::None) {
result.Type = ObjectType::Integer; result.Type = ObjectType::Integer;
result.Integer = 0; result.Integer = 0;
} }
ReleaseContext();
m_evalNesting--;
// The floor refers to a frame on this call's stack. Leaving it set
// would have the next evaluation -- possibly on a different thread's
// stack entirely -- compare against an address that no longer means
// anything, and bail out for no reason.
if (topLevel) m_stackFloor = 0;
m_evalLock.Release();
return ok; return ok;
} }
@@ -870,6 +1000,12 @@ namespace Hal {
// ============================================================================ // ============================================================================
bool Interpreter::ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset) { bool Interpreter::ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset) {
char stackMarker = 0;
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker))
return false;
DepthGuard depthGuard(ctx.ExprDepth);
uint32_t pos = offset; uint32_t pos = offset;
while (pos < endOffset && !ctx.Returned && !ctx.Broken) { while (pos < endOffset && !ctx.Returned && !ctx.Broken) {
@@ -890,6 +1026,19 @@ namespace Hal {
uint8_t op = ctx.Aml[*pos]; uint8_t op = ctx.Aml[*pos];
if (m_traceBudget > 0) {
m_traceBudget--;
// Offsets are absolute into the DSDT, so they line up with a
// disassembly of the same table.
if (op == ExtOpPrefix && *pos + 1 < endOffset) {
KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos
<< " ext op " << (uint64_t)ctx.Aml[*pos + 1];
} else {
KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos
<< " op " << (uint64_t)op;
}
}
// Return // Return
if (op == ReturnOp) { if (op == ReturnOp) {
(*pos)++; (*pos)++;
@@ -988,8 +1137,11 @@ namespace Hal {
int32_t targetNode = -1; int32_t targetNode = -1;
bool isLocal = false, isArg = false; bool isLocal = false, isArg = false;
int localIdx = 0, argIdx = 0; int localIdx = 0, argIdx = 0;
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx); int32_t pkgStore = -1; int pkgElement = 0;
StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx); EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx,
pkgStore, pkgElement);
StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx,
pkgStore, pkgElement);
return true; return true;
} }
@@ -999,7 +1151,9 @@ namespace Hal {
int32_t targetNode = -1; int32_t targetNode = -1;
bool isLocal = false, isArg = false; bool isLocal = false, isArg = false;
int localIdx = 0, argIdx = 0; int localIdx = 0, argIdx = 0;
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx); int32_t pkgStore = -1; int pkgElement = 0;
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx,
pkgStore, pkgElement);
Object current{}; Object current{};
if (isLocal) current = ctx.Locals[localIdx]; if (isLocal) current = ctx.Locals[localIdx];
@@ -1012,7 +1166,8 @@ namespace Hal {
if (current.Type == ObjectType::Integer) { if (current.Type == ObjectType::Integer) {
current.Integer += (op == IncrementOp) ? 1 : -1; current.Integer += (op == IncrementOp) ? 1 : -1;
} }
StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx); StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx,
pkgStore, pkgElement);
return true; return true;
} }
@@ -1083,6 +1238,16 @@ namespace Hal {
// Evaluate an AML term that produces a value. // Evaluate an AML term that produces a value.
bool Interpreter::EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, bool Interpreter::EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
Object& result) { Object& result) {
char stackMarker = 0;
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) {
result = Object{};
result.Type = ObjectType::Integer;
result.Integer = 0;
return false;
}
DepthGuard depthGuard(ctx.ExprDepth);
if (*pos >= endOffset) return false; if (*pos >= endOffset) return false;
uint8_t op = ctx.Aml[*pos]; uint8_t op = ctx.Aml[*pos];
@@ -1136,6 +1301,14 @@ namespace Hal {
return true; return true;
} }
// DerefOf. Index() already yields the element's value here, so a
// dereference of one is just the value again; anything else passes
// through unchanged.
if (op == DerefOfOp) {
(*pos)++;
return EvalTerm(ctx, pos, endOffset, result);
}
// Package // Package
if (op == PackageOp) { if (op == PackageOp) {
(*pos)++; (*pos)++;
@@ -1144,13 +1317,38 @@ namespace Hal {
uint32_t pkgEnd = pkgStart + pkgLen; uint32_t pkgEnd = pkgStart + pkgLen;
if (pkgEnd > endOffset) pkgEnd = endOffset; if (pkgEnd > endOffset) pkgEnd = endOffset;
// Store raw package as buffer for now // Retain the raw bytes (constant elements are read straight
// out of them) and, in parallel, evaluate each element into the
// package store so computed ones -- Package(){Local0, Local1},
// the shape _BST and friends are built with -- survive.
result.Type = ObjectType::Package; result.Type = ObjectType::Package;
uint32_t dataLen = pkgEnd - *pos; uint32_t dataLen = pkgEnd - *pos;
if (dataLen > MaxBufferLen) dataLen = MaxBufferLen; if (dataLen > MaxBufferLen) dataLen = MaxBufferLen;
result.Buffer.Length = dataLen; result.Buffer.Length = dataLen;
memcpy(result.Buffer.Data, &ctx.Aml[*pos], dataLen); memcpy(result.Buffer.Data, &ctx.Aml[*pos], dataLen);
uint32_t elemPos = *pos;
int count = (elemPos < pkgEnd) ? (int)ctx.Aml[elemPos] : 0;
elemPos++;
if (count > MaxPackageElements) count = MaxPackageElements;
if (count > 0) {
int32_t store = PackageStore::AllocTransient(count);
if (store > 0) {
for (int i = 0; i < count && elemPos < pkgEnd; i++) {
Object elem{};
uint32_t before = elemPos;
if (!EvalTerm(ctx, &elemPos, pkgEnd, elem)) break;
// An element that consumed nothing would spin here
// forever on a stream we decoded wrongly.
if (elemPos <= before) break;
Object* slot = PackageStore::Element(store, i);
if (slot) *slot = elem;
}
result.PkgIndex = store;
}
}
*pos = pkgEnd; *pos = pkgEnd;
return true; return true;
} }
@@ -1259,14 +1457,16 @@ namespace Hal {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, result, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
} else if (IsLeadNameChar(next) || next == '\\' || next == '^') { } else if (IsLeadNameChar(next) || next == '\\' || next == '^') {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, result, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
} }
// ZeroOp as "no target" — skip // ZeroOp as "no target" — skip
else if (next == ZeroOp) { else if (next == ZeroOp) {
@@ -1289,8 +1489,9 @@ namespace Hal {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, result, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
} else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) { } else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) {
(*pos)++; (*pos)++;
} }
@@ -1321,16 +1522,18 @@ namespace Hal {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, remainder, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, remainder, tn, il, li, ia, ai, ps, pe);
} }
// Result target // Result target
if (*pos < endOffset) { if (*pos < endOffset) {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, result, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
} }
return true; return true;
} }
@@ -1345,6 +1548,8 @@ namespace Hal {
result.Integer = obj.String.Length; result.Integer = obj.String.Length;
else if (obj.Type == ObjectType::Buffer) else if (obj.Type == ObjectType::Buffer)
result.Integer = obj.Buffer.Length; result.Integer = obj.Buffer.Length;
else if (obj.Type == ObjectType::Package)
result.Integer = (uint64_t)GetPackageCount(obj);
else else
result.Integer = 0; result.Integer = 0;
return true; return true;
@@ -1357,12 +1562,23 @@ namespace Hal {
EvalTerm(ctx, pos, endOffset, source); EvalTerm(ctx, pos, endOffset, source);
EvalTerm(ctx, pos, endOffset, index); EvalTerm(ctx, pos, endOffset, index);
result.Type = ObjectType::Integer;
uint64_t idx = index.Integer; uint64_t idx = index.Integer;
if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) { if (source.Type == ObjectType::Package) {
// ACPI defines Index() as producing a reference; we hand
// back the element's value instead, which is what every
// operand context here goes on to use. Writes through
// Index() are handled separately, in EvalTarget.
if (!GetPackageElement(source, (int)idx, result)) {
result = Object{};
result.Type = ObjectType::Integer;
result.Integer = 0;
}
} else if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) {
result.Type = ObjectType::Integer;
result.Integer = source.Buffer.Data[idx]; result.Integer = source.Buffer.Data[idx];
} else { } else {
result.Type = ObjectType::Integer;
result.Integer = 0; result.Integer = 0;
} }
@@ -1371,8 +1587,9 @@ namespace Hal {
int32_t tn = -1; int32_t tn = -1;
bool il = false, ia = false; bool il = false, ia = false;
int li = 0, ai = 0; int li = 0, ai = 0;
EvalTarget(ctx, pos, tn, il, li, ia, ai); int32_t ps = -1; int pe = 0;
StoreToTarget(ctx, result, tn, il, li, ia, ai); EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
} else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) { } else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) {
(*pos)++; (*pos)++;
} }
@@ -1405,12 +1622,14 @@ namespace Hal {
if (IsLeadNameChar(op) || op == '\\' || op == '^' || if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
op == DualNamePrefix || op == MultiNamePrefix) { op == DualNamePrefix || op == MultiNamePrefix) {
char path[256]; char path[256];
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256); char relative[64];
bool searchable = false;
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
relative, sizeof(relative), &searchable);
int32_t node = m_ns.FindNode(path); int32_t node = m_ns.FindNode(path);
if (node < 0) { if (node < 0 && searchable)
node = m_ns.ResolveName(path, ctx.ScopeNode); node = m_ns.SearchUpward(relative, ctx.ScopeNode);
}
if (node >= 0) { if (node >= 0) {
auto* nsNode = m_ns.GetNode(node); auto* nsNode = m_ns.GetNode(node);
@@ -1430,7 +1649,13 @@ namespace Hal {
return true; return true;
} }
ExecContext subCtx{}; ExecContext* sub = AcquireContext();
if (!sub) {
result.Type = ObjectType::Integer;
result.Integer = 0;
return true;
}
ExecContext& subCtx = *sub;
subCtx.Aml = ctx.Aml; subCtx.Aml = ctx.Aml;
subCtx.AmlBase = nsNode->Obj.Method.AmlOffset; subCtx.AmlBase = nsNode->Obj.Method.AmlOffset;
subCtx.AmlLength = nsNode->Obj.Method.AmlLength; subCtx.AmlLength = nsNode->Obj.Method.AmlLength;
@@ -1438,6 +1663,7 @@ namespace Hal {
subCtx.Returned = false; subCtx.Returned = false;
subCtx.Broken = false; subCtx.Broken = false;
subCtx.Depth = ctx.Depth + 1; subCtx.Depth = ctx.Depth + 1;
subCtx.ExprDepth = ctx.ExprDepth;
for (int i = 0; i < argc; i++) for (int i = 0; i < argc; i++)
subCtx.Args[i] = args[i]; subCtx.Args[i] = args[i];
for (int i = 0; i < MaxMethodLocals; i++) { for (int i = 0; i < MaxMethodLocals; i++) {
@@ -1451,6 +1677,7 @@ namespace Hal {
result.Type = ObjectType::Integer; result.Type = ObjectType::Integer;
result.Integer = 0; result.Integer = 0;
} }
ReleaseContext();
} else if (nsNode->Obj.Type == ObjectType::Field) { } else if (nsNode->Obj.Type == ObjectType::Field) {
uint64_t val = 0; uint64_t val = 0;
ReadField(node, val); ReadField(node, val);
@@ -1483,12 +1710,25 @@ namespace Hal {
// ============================================================================ // ============================================================================
bool Interpreter::EvalTarget(ExecContext& ctx, uint32_t* pos, bool Interpreter::EvalTarget(ExecContext& ctx, uint32_t* pos,
int32_t& nodeIndex, bool& isLocal, int& localIdx, int32_t& nodeIndex, bool& isLocal, int& localIdx,
bool& isArg, int& argIdx) { bool& isArg, int& argIdx,
int32_t& pkgStore, int& pkgElement) {
nodeIndex = -1; nodeIndex = -1;
char stackMarker = 0;
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) {
isLocal = false; isArg = false;
localIdx = 0; argIdx = 0;
pkgStore = -1; pkgElement = 0;
return false;
}
DepthGuard depthGuard(ctx.ExprDepth);
isLocal = false; isLocal = false;
isArg = false; isArg = false;
localIdx = 0; localIdx = 0;
argIdx = 0; argIdx = 0;
pkgStore = -1;
pkgElement = 0;
if (*pos >= ctx.AmlBase + ctx.AmlLength) return false; if (*pos >= ctx.AmlBase + ctx.AmlLength) return false;
@@ -1520,10 +1760,73 @@ namespace Hal {
if (IsLeadNameChar(op) || op == '\\' || op == '^' || if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
op == DualNamePrefix || op == MultiNamePrefix) { op == DualNamePrefix || op == MultiNamePrefix) {
char path[256]; char path[256];
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256); char relative[64];
bool searchable = false;
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
relative, sizeof(relative), &searchable);
nodeIndex = m_ns.FindNode(path); nodeIndex = m_ns.FindNode(path);
if (nodeIndex < 0) if (nodeIndex < 0 && searchable)
nodeIndex = m_ns.ResolveName(path, ctx.ScopeNode); nodeIndex = m_ns.SearchUpward(relative, ctx.ScopeNode);
return true;
}
// Index(source, n) names a package element. This is how firmware
// fills a result package -- Store(x, Index(BST0, 1)) -- so the
// element has to be addressable, not just readable.
if (op == IndexOp) {
(*pos)++;
// Resolve the source by name where we can, so the write lands
// in the namespace object rather than in a copy of it.
int32_t srcNode = -1;
uint8_t srcOp = (*pos < ctx.AmlBase + ctx.AmlLength) ? ctx.Aml[*pos] : 0;
if (IsLeadNameChar(srcOp) || srcOp == '\\' || srcOp == '^' ||
srcOp == DualNamePrefix || srcOp == MultiNamePrefix) {
char path[256];
char relative[64];
bool searchable = false;
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
relative, sizeof(relative), &searchable);
srcNode = m_ns.FindNode(path);
if (srcNode < 0 && searchable)
srcNode = m_ns.SearchUpward(relative, ctx.ScopeNode);
} else {
// Locals and arguments can hold a package too.
Object src{};
EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, src);
if (src.Type == ObjectType::Package && src.PkgIndex > 0)
pkgStore = src.PkgIndex;
}
Object idx{};
EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, idx);
pkgElement = (int)idx.Integer;
if (srcNode >= 0) {
auto* srcObj = m_ns.GetNode(srcNode);
if (srcObj && srcObj->Obj.Type == ObjectType::Package)
pkgStore = MaterializeNodePackage(srcNode);
}
// Index() always carries a target operand of its own -- ZeroOp
// when there is none. It has to be consumed either way, or the
// rest of the method decodes off by however long it was.
if (*pos < ctx.AmlBase + ctx.AmlLength) {
int32_t innerNode = -1;
bool innerLocal = false, innerArg = false;
int innerLocalIdx = 0, innerArgIdx = 0;
int32_t innerStore = -1; int innerElement = 0;
EvalTarget(ctx, pos, innerNode, innerLocal, innerLocalIdx,
innerArg, innerArgIdx, innerStore, innerElement);
}
if (pkgStore <= 0) {
// Not a package we can write through (a buffer, or an
// unresolvable source): discard the store rather than
// corrupting an unrelated object.
pkgStore = -1;
nodeIndex = -1;
}
return true; return true;
} }
@@ -1539,7 +1842,14 @@ namespace Hal {
// ============================================================================ // ============================================================================
void Interpreter::StoreToTarget(ExecContext& ctx, const Object& value, void Interpreter::StoreToTarget(ExecContext& ctx, const Object& value,
int32_t nodeIndex, bool isLocal, int localIdx, int32_t nodeIndex, bool isLocal, int localIdx,
bool isArg, int argIdx) { bool isArg, int argIdx,
int32_t pkgStore, int pkgElement) {
if (pkgStore > 0) {
Object* slot = PackageStore::Element(pkgStore, pkgElement);
if (slot) *slot = value;
return;
}
if (isLocal && localIdx >= 0 && localIdx < MaxMethodLocals) { if (isLocal && localIdx >= 0 && localIdx < MaxMethodLocals) {
ctx.Locals[localIdx] = value; ctx.Locals[localIdx] = value;
} else if (isArg && argIdx >= 0 && argIdx < MaxMethodArgs) { } else if (isArg && argIdx >= 0 && argIdx < MaxMethodArgs) {
@@ -1558,6 +1868,171 @@ namespace Hal {
// ============================================================================ // ============================================================================
// Package elements
// ============================================================================
bool Interpreter::DecodeConstElement(const uint8_t* data, uint32_t length,
uint32_t* pos, Object& out) {
out = Object{};
if (*pos >= length) return false;
uint8_t op = data[(*pos)++];
switch (op) {
case ZeroOp:
out.Type = ObjectType::Integer;
out.Integer = 0;
return true;
case OneOp:
out.Type = ObjectType::Integer;
out.Integer = 1;
return true;
case OnesOp:
out.Type = ObjectType::Integer;
out.Integer = ~0ULL;
return true;
default:
break;
}
// Fixed-width integer prefixes
int width = 0;
if (op == BytePrefix) width = 1;
else if (op == WordPrefix) width = 2;
else if (op == DWordPrefix) width = 4;
else if (op == QWordPrefix) width = 8;
if (width > 0) {
if (*pos + (uint32_t)width > length) return false;
uint64_t value = 0;
for (int i = 0; i < width; i++)
value |= (uint64_t)data[*pos + i] << (i * 8);
*pos += (uint32_t)width;
out.Type = ObjectType::Integer;
out.Integer = value;
return true;
}
if (op == StringPrefix) {
out.Type = ObjectType::String;
uint16_t n = 0;
while (*pos < length && data[*pos] != 0) {
if (n < MaxStringLen - 1)
out.String.Data[n++] = (char)data[*pos];
(*pos)++;
}
if (*pos < length) (*pos)++; // trailing NUL
out.String.Data[n] = '\0';
out.String.Length = n;
return true;
}
if (op == BufferOp || op == PackageOp || op == VarPackageOp) {
uint32_t start = *pos;
uint32_t pkgLen = DecodePkgLength(data, pos);
uint32_t end = start + pkgLen;
if (end > length) end = length;
if (end < *pos) return false;
uint32_t dataLen = end - *pos;
if (dataLen > MaxBufferLen) dataLen = MaxBufferLen;
out.Type = (op == BufferOp) ? ObjectType::Buffer : ObjectType::Package;
out.Buffer.Length = dataLen;
memcpy(out.Buffer.Data, &data[*pos], dataLen);
*pos = end;
return true;
}
// A name reference, local, or anything else we cannot resolve from
// the bytes alone. Skip it and hand back zero.
if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
op == DualNamePrefix || op == MultiNamePrefix) {
(*pos)--;
uint32_t p = *pos;
// Walk the NameString by hand: prefixes, then 4-char segments.
while (p < length && (data[p] == '\\' || data[p] == '^')) p++;
if (p < length && data[p] == DualNamePrefix) {
p += 1 + 8;
} else if (p < length && data[p] == MultiNamePrefix) {
uint8_t segs = (p + 1 < length) ? data[p + 1] : 0;
p += 2 + (uint32_t)segs * 4;
} else if (p < length && data[p] == 0x00) {
p += 1; // NullName
} else {
p += 4;
}
*pos = (p > length) ? length : p;
}
out.Type = ObjectType::Integer;
out.Integer = 0;
return true;
}
int Interpreter::GetPackageCount(const Object& package) {
if (package.Type != ObjectType::Package) return 0;
if (package.PkgIndex > 0)
return PackageStore::Count(package.PkgIndex);
// Raw form: the first byte of the retained AML is NumElements.
if (package.Buffer.Length < 1) return 0;
return (int)package.Buffer.Data[0];
}
bool Interpreter::GetPackageElement(const Object& package, int index, Object& result) {
result = Object{};
if (package.Type != ObjectType::Package || index < 0) return false;
if (package.PkgIndex > 0) {
Object* elem = PackageStore::Element(package.PkgIndex, index);
if (!elem) return false;
result = *elem;
return true;
}
uint32_t length = package.Buffer.Length;
if (length < 1) return false;
if (index >= (int)package.Buffer.Data[0]) return false;
uint32_t pos = 1; // past NumElements
for (int i = 0; i <= index; i++) {
if (!DecodeConstElement(package.Buffer.Data, length, &pos, result))
return false;
}
return true;
}
int32_t Interpreter::MaterializeNodePackage(int32_t nodeIndex) {
auto* node = m_ns.GetNode(nodeIndex);
if (!node || node->Obj.Type != ObjectType::Package) return -1;
if (node->Obj.PkgIndex > 0) return node->Obj.PkgIndex;
int count = GetPackageCount(node->Obj);
if (count <= 0) return -1;
if (count > MaxPackageElements) count = MaxPackageElements;
int32_t store = PackageStore::AllocPersistent(count);
if (store < 0) return -1;
// Seed the elements from the constant bytes the package was
// declared with, so a package that is only partly written through
// Index() keeps its declared values everywhere else.
uint32_t pos = 1;
for (int i = 0; i < count; i++) {
Object elem{};
if (!DecodeConstElement(node->Obj.Buffer.Data, node->Obj.Buffer.Length,
&pos, elem))
break;
Object* slot = PackageStore::Element(store, i);
if (slot) *slot = elem;
}
node->Obj.PkgIndex = store;
return store;
}
// ============================================================================
// ReadField // ReadField
// ============================================================================ // ============================================================================
@@ -1578,7 +2053,19 @@ namespace Hal {
uint64_t byteAddr = regBase + (bitOff / 8); uint64_t byteAddr = regBase + (bitOff / 8);
uint32_t bitShift = bitOff % 8; uint32_t bitShift = bitOff % 8;
return ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, value); uint64_t raw = 0;
if (!ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, raw))
return false;
// ReadRegion hands back whole access units starting at byteAddr;
// narrow that to the field's own bits so sub-byte flags (common in
// EC and PM register layouts) don't carry their neighbours along.
raw >>= bitShift;
if (bitLen < 64)
raw &= (1ULL << bitLen) - 1;
value = raw;
return true;
} }
// ============================================================================ // ============================================================================
@@ -1639,6 +2126,13 @@ namespace Hal {
} }
if (space == RegionSpace::SystemMemory) { if (space == RegionSpace::SystemMemory) {
// The offset comes from firmware AML and may be nonsense: a
// region we mis-decoded, or one whose base is computed by code
// we do not fully execute. Dereferencing that through the HHDM
// is how a bad table takes the kernel down with a #GP, so the
// address has to be inside what Init() actually mapped.
if (!HhdmReadable(address, accessBytes)) return false;
volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address); volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address);
if (accessBytes <= 1) if (accessBytes <= 1)
value = *mmio; value = *mmio;
@@ -1651,6 +2145,19 @@ namespace Hal {
return true; return true;
} }
if (space == RegionSpace::EmbeddedControl) {
// The EC address space is a flat 256-byte array reached one
// byte at a time over the command/data port pair, so a wider
// field is assembled little-endian from consecutive reads.
for (uint32_t i = 0; i < accessBytes; i++) {
uint8_t byte = 0;
if (!EmbeddedController::Read((uint8_t)(address + i), byte))
return false;
value |= (uint64_t)byte << (i * 8);
}
return true;
}
if (space == RegionSpace::PciConfig) { if (space == RegionSpace::PciConfig) {
// PCI config space access -- not yet implemented // PCI config space access -- not yet implemented
return false; return false;
@@ -1681,6 +2188,8 @@ namespace Hal {
} }
if (space == RegionSpace::SystemMemory) { if (space == RegionSpace::SystemMemory) {
if (!HhdmReadable(address, accessBytes)) return false;
volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address); volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address);
if (accessBytes <= 1) if (accessBytes <= 1)
*mmio = (uint8_t)value; *mmio = (uint8_t)value;
@@ -1693,6 +2202,15 @@ namespace Hal {
return true; return true;
} }
if (space == RegionSpace::EmbeddedControl) {
for (uint32_t i = 0; i < accessBytes; i++) {
if (!EmbeddedController::Write((uint8_t)(address + i),
(uint8_t)(value >> (i * 8))))
return false;
}
return true;
}
return false; return false;
} }
+100 -4
View File
@@ -7,6 +7,7 @@
#pragma once #pragma once
#include "AmlNamespace.hpp" #include "AmlNamespace.hpp"
#include <CppLib/Spinlock.hpp>
#include <cstdint> #include <cstdint>
namespace Hal { namespace Hal {
@@ -105,6 +106,24 @@ namespace Hal {
static constexpr int MaxCallDepth = 16; static constexpr int MaxCallDepth = 16;
static constexpr int MaxLoopIterations = 1024; static constexpr int MaxLoopIterations = 1024;
// Backstop cap on evaluation nesting. This counter is shared by
// expression evaluation AND block execution, and block nesting is
// legitimately deep -- every If/Else/While body is another level, so a
// long firmware method reaches well past a handful. The real protection
// against running off the stack is HaveStackHeadroom(), which measures
// actual usage; this only catches a decode that recurses without
// consuming input.
static constexpr int MaxExprDepth = 48;
// How much stack the interpreter may consume before it gives up. Note
// this is an ALLOWANCE, not a reserve: frames here are expensive (a
// name-resolving EvalTerm frame carries a 256-byte path buffer plus
// 272-byte Object temporaries, so ~800 bytes each), and a real
// firmware _BIF nests far enough to need tens of them. Sized against
// Sched::StackPages -- keep well under it, since the syscall path below
// has already spent some of that stack.
static constexpr uint64_t StackBudget = 32768;
// ============================================================================ // ============================================================================
// Interpreter // Interpreter
@@ -138,6 +157,31 @@ namespace Hal {
// Check if the interpreter has been initialized // Check if the interpreter has been initialized
bool IsInitialized() const { return m_initialized; } bool IsInitialized() const { return m_initialized; }
// Log every opcode executed, with its offset, for the next
// `budget` opcodes. The only way to see where a method that runs to
// completion without returning a value actually goes.
void SetTrace(int budget) { m_traceBudget = budget; }
// ============================================================================
// Packages
// ============================================================================
// Read one element of a Package object. Works for both a package
// whose elements were computed at run time (materialized in the
// package store) and one that is still just the constant AML bytes
// it was declared with. Returns false when the object is not a
// package or the index is out of range.
bool GetPackageElement(const Object& package, int index, Object& result);
// Number of elements a Package declares.
int GetPackageCount(const Object& package);
// Give a package stored in a namespace node real per-element
// storage so Index() can be written through. Returns the store
// index, or -1 if the node does not hold a package.
int32_t MaterializeNodePackage(int32_t nodeIndex);
private: private:
// ============================================================================ // ============================================================================
// Parsing (table load) // Parsing (table load)
@@ -158,8 +202,17 @@ namespace Hal {
// ============================================================================ // ============================================================================
// Read a NameString from AML and produce an absolute path. // Read a NameString from AML and produce an absolute path.
// Advances *pos past the name. // Advances *pos past the name.
// Produces the absolute path (rebased onto scopeNode for a
// relative name). outRelative, when given, receives the name as it
// actually appeared in the AML, and *outSearchable says whether the
// ACPI search rule applies to it -- true only when the name carried
// no root or parent prefix. Resolution must fall back to
// Namespace::SearchUpward() in that case: the absolute path is a
// guess at one scope, and the object usually lives in a parent.
int ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode, int ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode,
char* outPath, int maxLen); char* outPath, int maxLen,
char* outRelative = nullptr, int relLen = 0,
bool* outSearchable = nullptr);
// Read a single 4-char NameSeg from AML. Advances *pos. // Read a single 4-char NameSeg from AML. Advances *pos.
void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg); void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg);
@@ -170,6 +223,13 @@ namespace Hal {
// ============================================================================ // ============================================================================
uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos); uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos);
// Decode one constant package element out of raw AML bytes and
// advance *pos past it. Non-constant elements (name references,
// locals) decode to integer 0, which is what they were worth
// before the package store existed.
bool DecodeConstElement(const uint8_t* data, uint32_t length,
uint32_t* pos, Object& out);
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos); uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
// ============================================================================ // ============================================================================
@@ -187,7 +247,16 @@ namespace Hal {
Object ReturnValue; Object ReturnValue;
bool Returned; bool Returned;
bool Broken; bool Broken;
int Depth; int Depth; // method-call nesting
int ExprDepth; // expression/block nesting
};
// Increments a depth counter for the duration of a scope; pairs
// with the MaxExprDepth check at each recursive entry point.
struct DepthGuard {
int& Value;
explicit DepthGuard(int& v) : Value(v) { Value++; }
~DepthGuard() { Value--; }
}; };
bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset); bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset);
@@ -199,14 +268,19 @@ namespace Hal {
// Evaluate a "SuperName" target for Store operations. // Evaluate a "SuperName" target for Store operations.
// Returns the node index for named targets, or handles locals/args. // Returns the node index for named targets, or handles locals/args.
// If isLocal/isArg is set, localIdx/argIdx contains the index. // If isLocal/isArg is set, localIdx/argIdx contains the index.
// A target may also be Index(package, n), which names a package
// element rather than a node, local or arg. pkgStore/pkgElement
// carry that case (pkgStore > 0 when it applies).
bool EvalTarget(ExecContext& ctx, uint32_t* pos, bool EvalTarget(ExecContext& ctx, uint32_t* pos,
int32_t& nodeIndex, bool& isLocal, int& localIdx, int32_t& nodeIndex, bool& isLocal, int& localIdx,
bool& isArg, int& argIdx); bool& isArg, int& argIdx,
int32_t& pkgStore, int& pkgElement);
// Store a value to a target (node, local, or arg). // Store a value to a target (node, local, or arg).
void StoreToTarget(ExecContext& ctx, const Object& value, void StoreToTarget(ExecContext& ctx, const Object& value,
int32_t nodeIndex, bool isLocal, int localIdx, int32_t nodeIndex, bool isLocal, int localIdx,
bool isArg, int argIdx); bool isArg, int argIdx,
int32_t pkgStore = -1, int pkgElement = 0);
// ============================================================================ // ============================================================================
@@ -221,10 +295,32 @@ namespace Hal {
// State // State
// ============================================================================ // ============================================================================
// ExecContext is ~4.4 KB (sixteen Objects, each carrying a
// 256-byte buffer inline). One per nested method call on a 16 KiB
// kernel stack overflows after three levels, so they come from a
// pool instead. The pool is only safe because evaluation is
// serialized -- see m_evalLock.
// Stack-exhaustion check; `frame` is the address of a local in
// the calling frame.
bool HaveStackHeadroom(const void* frame) const;
// One-shot warning when the nesting backstop trips.
static void ReportDepthLimit();
ExecContext* AcquireContext();
void ReleaseContext();
Namespace m_ns; Namespace m_ns;
const uint8_t* m_dsdt; const uint8_t* m_dsdt;
uint32_t m_dsdtLength; uint32_t m_dsdtLength;
bool m_initialized; bool m_initialized;
ExecContext* m_ctxPool; // MaxCallDepth + 1 entries, heap
int m_ctxInUse;
int m_evalNesting; // top-level evaluations in progress
uint64_t m_stackFloor; // stack address the interpreter stops at
int m_traceBudget; // opcodes left to log, 0 = off
kcp::Mutex m_evalLock;
}; };
// ============================================================================ // ============================================================================
+196 -6
View File
@@ -7,10 +7,114 @@
#include "AmlNamespace.hpp" #include "AmlNamespace.hpp"
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Memory/Heap.hpp> #include <Memory/Heap.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Hal { namespace Hal {
namespace AML { namespace AML {
// ============================================================================
// Package element store
// ============================================================================
namespace PackageStore {
struct Entry {
Object* Elements;
int Count;
};
static Entry s_persistent[MaxPersistent];
static Entry s_transient[MaxTransient];
static int s_persistentUsed = 0;
static int s_transientNext = 0;
// Indices handed out are 1-based so that a zeroed Object (nodes are
// memset before Clear() runs, and callers zero-init freely) never
// looks like a reference to entry 0.
static constexpr int32_t TransientBase = MaxPersistent;
static Object* AllocElements(int count) {
auto* mem = (Object*)Memory::g_heap->Request(sizeof(Object) * count);
if (!mem) return nullptr;
for (int i = 0; i < count; i++)
mem[i] = Object{};
return mem;
}
int32_t AllocPersistent(int count) {
if (count <= 0 || count > MaxPackageElements) return -1;
if (s_persistentUsed >= MaxPersistent) return -1;
Entry& e = s_persistent[s_persistentUsed];
e.Elements = AllocElements(count);
if (!e.Elements) return -1;
e.Count = count;
return (int32_t)(s_persistentUsed++) + 1;
}
int32_t AllocTransient(int count) {
if (count <= 0 || count > MaxPackageElements) return -1;
int slot = s_transientNext;
if (slot >= MaxTransient) {
// The ring is only rewound between top-level evaluations, so
// a method that builds more packages than this reuses the
// oldest slots rather than failing outright.
slot = 0;
s_transientNext = 0;
}
s_transientNext++;
Entry& e = s_transient[slot];
// Element arrays are sized on first use and reused afterwards;
// a larger request reallocates.
if (e.Elements == nullptr || e.Count < count) {
Object* mem = AllocElements(count);
if (!mem) return -1;
if (e.Elements) Memory::g_heap->Free(e.Elements);
e.Elements = mem;
} else {
for (int i = 0; i < count; i++)
e.Elements[i] = Object{};
}
e.Count = count;
return TransientBase + slot + 1;
}
void ResetTransient() {
s_transientNext = 0;
}
static Entry* Lookup(int32_t index) {
if (index <= 0) return nullptr;
int32_t i = index - 1;
if (i < MaxPersistent) {
if (i >= s_persistentUsed) return nullptr;
return s_persistent[i].Elements ? &s_persistent[i] : nullptr;
}
i -= TransientBase;
if (i < 0 || i >= MaxTransient) return nullptr;
return s_transient[i].Elements ? &s_transient[i] : nullptr;
}
Object* Element(int32_t index, int element) {
Entry* e = Lookup(index);
if (!e || element < 0 || element >= e->Count) return nullptr;
return &e->Elements[element];
}
int Count(int32_t index) {
Entry* e = Lookup(index);
return e ? e->Count : 0;
}
};
Namespace::Namespace() : m_chunkCount(0), m_nodeCount(0) { Namespace::Namespace() : m_chunkCount(0), m_nodeCount(0) {
for (int i = 0; i < MaxChunks; i++) for (int i = 0; i < MaxChunks; i++)
m_chunks[i] = nullptr; m_chunks[i] = nullptr;
@@ -30,7 +134,17 @@ namespace Hal {
} }
bool Namespace::AllocChunk() { bool Namespace::AllocChunk() {
if (m_chunkCount >= MaxChunks) return false; if (m_chunkCount >= MaxChunks) {
static bool warned = false;
if (!warned) {
warned = true;
KernelLogStream(WARNING, "AML")
<< "Namespace node pool full at " << base::dec
<< (uint64_t)(MaxChunks * NodesPerChunk)
<< " nodes; part of the namespace will be missing";
}
return false;
}
auto* chunk = (NamespaceNode*)Memory::g_heap->Request( auto* chunk = (NamespaceNode*)Memory::g_heap->Request(
sizeof(NamespaceNode) * NodesPerChunk); sizeof(NamespaceNode) * NodesPerChunk);
@@ -45,6 +159,41 @@ namespace Hal {
return true; return true;
} }
bool Namespace::AppendChild(NamespaceNode* parent, int32_t child) {
if (parent->ChildCount >= parent->ChildCapacity) {
int32_t newCap = parent->ChildCapacity ? parent->ChildCapacity * 2
: InitialChildren;
auto* grown = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * newCap);
if (!grown) {
// Losing a child silently costs whole devices, so say it
// once rather than leaving a hole nobody can see.
static bool warned = false;
if (!warned) {
warned = true;
KernelLogStream(WARNING, "AML")
<< "Out of heap growing a namespace node's child list;"
<< " part of the namespace will be missing";
}
return false;
}
for (int32_t i = 0; i < parent->ChildCount; i++)
grown[i] = parent->ChildIndices[i];
for (int32_t i = parent->ChildCount; i < newCap; i++)
grown[i] = -1;
if (parent->ChildIndices)
Memory::g_heap->Free(parent->ChildIndices);
parent->ChildIndices = grown;
parent->ChildCapacity = newCap;
}
parent->ChildIndices[parent->ChildCount++] = child;
return true;
}
int32_t Namespace::AllocNode() { int32_t Namespace::AllocNode() {
// Check if we need a new chunk // Check if we need a new chunk
int32_t capacity = m_chunkCount * NodesPerChunk; int32_t capacity = m_chunkCount * NodesPerChunk;
@@ -165,11 +314,7 @@ namespace Hal {
// Add to parent's children // Add to parent's children
auto* parent = GetNode(current); auto* parent = GetNode(current);
if (!parent) return -1; if (!parent) return -1;
if (parent->ChildCount < MaxChildren) { if (!AppendChild(parent, child)) return -1;
parent->ChildIndices[parent->ChildCount++] = child;
} else {
return -1; // too many children
}
} }
current = child; current = child;
} }
@@ -188,6 +333,51 @@ namespace Hal {
return current; return current;
} }
int32_t Namespace::SearchUpward(const char* relative, int32_t scopeNodeIndex) const {
char segments[MaxPathDepth][MaxNameSegLen + 1];
int segCount = 0;
// Split on '.', padding each segment to four characters the way
// node names are stored.
int i = 0;
while (relative[i] && segCount < MaxPathDepth) {
char seg[MaxNameSegLen + 1];
int n = 0;
while (relative[i] && relative[i] != '.' && n < MaxNameSegLen)
seg[n++] = relative[i++];
seg[n] = '\0';
while (relative[i] && relative[i] != '.') i++; // overlong segment
if (relative[i] == '.') i++;
PadSegment(seg, segments[segCount]);
segCount++;
}
if (segCount == 0) return -1;
// Walk up from the current scope looking for the first segment.
int32_t scope = scopeNodeIndex;
while (scope >= 0) {
int32_t found = FindChildByName(scope, segments[0]);
if (found >= 0) {
// Descend the rest. A scope that matches the first segment
// but not the remainder does not win -- keep walking up.
int32_t cur = found;
bool ok = true;
for (int k = 1; k < segCount; k++) {
cur = FindChildByName(cur, segments[k]);
if (cur < 0) { ok = false; break; }
}
if (ok) return cur;
}
auto* node = GetNode(scope);
scope = node ? node->ParentIndex : -1;
}
return -1;
}
int32_t Namespace::ResolveName(const char* name, int32_t scopeNodeIndex) const { int32_t Namespace::ResolveName(const char* name, int32_t scopeNodeIndex) const {
// If it starts with '\', it's absolute // If it starts with '\', it's absolute
if (name[0] == '\\') if (name[0] == '\\')
+64 -6
View File
@@ -55,10 +55,15 @@ namespace Hal {
// ============================================================================ // ============================================================================
static constexpr int MaxNameSegLen = 4; static constexpr int MaxNameSegLen = 4;
static constexpr int MaxPathDepth = 16; static constexpr int MaxPathDepth = 16;
static constexpr int MaxChildren = 64; // Children are held in a heap array that grows on demand, starting at
// this many entries. A fixed ceiling here is not survivable: an EC or
// GPE scope in a modern laptop DSDT carries well over a hundred named
// objects, and dropping the overflow loses whole devices (the battery
// among them) with no way to tell from the outside.
static constexpr int InitialChildren = 8;
static constexpr int MaxStringLen = 64; static constexpr int MaxStringLen = 64;
static constexpr int MaxBufferLen = 256; static constexpr int MaxBufferLen = 256;
static constexpr int MaxPackageElements = 16; static constexpr int MaxPackageElements = 32;
static constexpr int MaxMethodArgs = 7; static constexpr int MaxMethodArgs = 7;
static constexpr int MaxMethodLocals = 8; static constexpr int MaxMethodLocals = 8;
@@ -76,6 +81,14 @@ namespace Hal {
struct Object { struct Object {
ObjectType Type = ObjectType::None; ObjectType Type = ObjectType::None;
// Packages keep their raw AML bytes in Buffer (constant elements can
// be decoded straight from there), but a package whose elements are
// computed or written through Index() needs real per-element
// storage. PkgIndex names that storage once it is materialized;
// -1 means "not materialized yet". It lives outside the union so a
// package can carry both representations at once.
int32_t PkgIndex = -1;
union { union {
uint64_t Integer; uint64_t Integer;
@@ -116,7 +129,36 @@ namespace Hal {
} Processor; } Processor;
}; };
Object() : Type(ObjectType::None), Integer(0) {} Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {}
};
// ============================================================================
// Package element store
// ============================================================================
// Element arrays for materialized packages, kept outside Object so the
// tagged union stays small. Entries are split in two: a persistent
// region for packages declared with Name() (materialized once and kept
// for the life of the namespace) and a transient ring for packages
// built inside a method, which is rewound at each top-level evaluation
// so repeated calls -- polling _BST, say -- cannot exhaust it.
namespace PackageStore {
static constexpr int MaxPersistent = 32;
static constexpr int MaxTransient = 32;
// Allocate an element array of `count` objects. Returns -1 when the
// store is full or the count is out of range.
int32_t AllocPersistent(int count);
int32_t AllocTransient(int count);
// Rewind the transient ring. Called once per top-level evaluation.
void ResetTransient();
// Element access. Returns nullptr for an unknown entry or an
// out-of-range element.
Object* Element(int32_t index, int element);
int Count(int32_t index);
}; };
// ============================================================================ // ============================================================================
@@ -129,16 +171,19 @@ namespace Hal {
char Name[MaxNameSegLen + 1]; // null-terminated 4-char segment char Name[MaxNameSegLen + 1]; // null-terminated 4-char segment
Object Obj; Object Obj;
int32_t ParentIndex; // -1 for root int32_t ParentIndex; // -1 for root
int32_t ChildIndices[MaxChildren]; int32_t* ChildIndices; // heap array, ChildCapacity long
int32_t ChildCount; int32_t ChildCount;
int32_t ChildCapacity;
// Only ever called on a freshly allocated (zeroed) node, so there is
// no child array to release here.
void Clear() { void Clear() {
Name[0] = 0; Name[0] = 0;
Obj = Object{}; Obj = Object{};
ParentIndex = -1; ParentIndex = -1;
ChildIndices = nullptr;
ChildCount = 0; ChildCount = 0;
for (int i = 0; i < MaxChildren; i++) ChildCapacity = 0;
ChildIndices[i] = -1;
} }
}; };
@@ -166,6 +211,14 @@ namespace Hal {
// 3. Root scope // 3. Root scope
int32_t ResolveName(const char* name, int32_t scopeNodeIndex) const; int32_t ResolveName(const char* name, int32_t scopeNodeIndex) const;
// The ACPI name search rule: for a name written WITHOUT a root or
// parent prefix, search the current scope for its first segment,
// then each parent scope up to the root, and descend the remaining
// segments from wherever it matched. `relative` is the name as it
// appeared in the AML ("PBIF", "BAT0.PBIF"), never a path already
// rebased onto a scope. Returns -1 if no scope contains it.
int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const;
// Get a node by index. // Get a node by index.
NamespaceNode* GetNode(int32_t index); NamespaceNode* GetNode(int32_t index);
const NamespaceNode* GetNode(int32_t index) const; const NamespaceNode* GetNode(int32_t index) const;
@@ -217,6 +270,11 @@ namespace Hal {
private: private:
int32_t AllocNode(); int32_t AllocNode();
bool AllocChunk(); bool AllocChunk();
// Append a child index to a parent, growing its array as needed.
// Returns false only when the heap is exhausted.
bool AppendChild(NamespaceNode* parent, int32_t child);
void EnsureRoot(); void EnsureRoot();
int32_t FindChildByName(int32_t parentIndex, const char* seg) const; int32_t FindChildByName(int32_t parentIndex, const char* seg) const;
+67 -22
View File
@@ -319,6 +319,58 @@ namespace Hal {
// EnumerateAll // EnumerateAll
// ============================================================================
// Fill one DeviceInfo from a namespace node.
static void DescribeDevice(AML::Namespace& ns, int32_t idx, DeviceInfo& dev) {
dev.NodeIndex = idx;
ns.GetNodePath(idx, dev.Path, 128);
dev.Status = EvaluateSta(idx);
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
if (!EvaluateHid(idx, dev.HardwareId, 16))
dev.HardwareId[0] = '\0';
if (!EvaluateUid(idx, dev.UniqueId, 16))
dev.UniqueId[0] = '\0';
dev.Address = EvaluateAdr(idx);
}
// ============================================================================
// FindDeviceByHid
// ============================================================================
bool FindDeviceByHid(const char* hid, DeviceInfo& out) {
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return false;
auto& ns = interp.GetNamespace();
int32_t found = -1;
// Only _HID is evaluated per candidate; the full description is
// built once, for the match. WalkDescendants has no early exit, so
// the first match wins and later ones are ignored.
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
if (found >= 0) return;
char candidate[16];
if (!EvaluateHid(idx, candidate, 16)) return;
if (StrEqual(candidate, hid)) found = idx;
});
if (found < 0) return false;
out = DeviceInfo{};
DescribeDevice(ns, found, out);
return true;
}
// ============================================================================
// EnumerateAll
// ============================================================================ // ============================================================================
void EnumerateAll(DeviceList& result) { void EnumerateAll(DeviceList& result) {
auto& interp = AML::GetInterpreter(); auto& interp = AML::GetInterpreter();
@@ -326,39 +378,32 @@ namespace Hal {
auto& ns = interp.GetNamespace(); auto& ns = interp.GetNamespace();
result.Count = 0; result.Count = 0;
result.Truncated = false;
// Walk all namespace nodes looking for Device objects // Walk all namespace nodes looking for Device objects
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device, ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) { [&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
if (result.Count >= MaxDevices) return; if (result.Count >= MaxDevices) {
result.Truncated = true;
auto& dev = result.Devices[result.Count]; return;
dev.NodeIndex = idx; }
// Get the path
ns.GetNodePath(idx, dev.Path, 128);
// Evaluate _STA
dev.Status = EvaluateSta(idx);
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
// Evaluate _HID
if (!EvaluateHid(idx, dev.HardwareId, 16))
dev.HardwareId[0] = '\0';
// Evaluate _UID
if (!EvaluateUid(idx, dev.UniqueId, 16))
dev.UniqueId[0] = '\0';
// Evaluate _ADR
dev.Address = EvaluateAdr(idx);
DescribeDevice(ns, idx, result.Devices[result.Count]);
result.Count++; result.Count++;
}); });
KernelLogStream(OK, "ACPI") << "Enumerated " << base::dec KernelLogStream(OK, "ACPI") << "Enumerated " << base::dec
<< (uint64_t)result.Count << " ACPI devices"; << (uint64_t)result.Count << " ACPI devices";
// The list is a fixed-size stack object, so a large namespace runs
// past it. Say so: a device missing from the log below may simply
// be one that did not fit, and looking it up by HID still works.
if (result.Truncated) {
KernelLogStream(WARNING, "ACPI") << "Device list truncated at "
<< base::dec << (uint64_t)MaxDevices
<< "; use FindDeviceByHid to reach the rest";
}
// Log discovered devices // Log discovered devices
for (int i = 0; i < result.Count; i++) { for (int i = 0; i < result.Count; i++) {
auto& dev = result.Devices[i]; auto& dev = result.Devices[i];
+8 -1
View File
@@ -45,8 +45,9 @@ namespace Hal {
struct DeviceList { struct DeviceList {
DeviceInfo Devices[MaxDevices]; DeviceInfo Devices[MaxDevices];
int Count; int Count;
bool Truncated; // the namespace held more than MaxDevices
DeviceList() : Count(0) {} DeviceList() : Count(0), Truncated(false) {}
// Find a device by HID. Returns nullptr if not found. // Find a device by HID. Returns nullptr if not found.
const DeviceInfo* FindByHid(const char* hid) const; const DeviceInfo* FindByHid(const char* hid) const;
@@ -64,6 +65,12 @@ namespace Hal {
// The interpreter must be initialized first (LoadTable called). // The interpreter must be initialized first (LoadTable called).
void EnumerateAll(DeviceList& result); void EnumerateAll(DeviceList& result);
// Find the first device with the given _HID, walking the namespace
// directly. Unlike EnumerateAll this has no device-count ceiling and
// needs no multi-kilobyte list on the caller's stack, so it is the way
// to reach a specific device on a machine whose namespace is large.
bool FindDeviceByHid(const char* hid, DeviceInfo& out);
// Evaluate _STA for a device node. Returns STA_DEFAULT if no _STA exists. // Evaluate _STA for a device node. Returns STA_DEFAULT if no _STA exists.
uint32_t EvaluateSta(int32_t deviceNodeIndex); uint32_t EvaluateSta(int32_t deviceNodeIndex);
+422
View File
@@ -0,0 +1,422 @@
/*
* Battery.cpp
* ACPI Control Method Battery: _STA / _BIF / _BIX / _BST, plus _PSR on the
* AC adapter
* Copyright (c) 2026 Daniel Hammer
*/
#include "Battery.hpp"
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Hal {
namespace Battery {
// ============================================================================
// State
// ============================================================================
// ACPI reports an unreadable field as all-ones rather than zero.
static constexpr uint32_t Unknown32 = 0xFFFFFFFFu;
// The EC transactions behind _BST take a few milliseconds, and the
// value only moves on a scale of minutes, so a short cache keeps a
// one-second panel repaint from becoming a bus workload.
static constexpr uint64_t CacheMs = 2000;
static bool g_initialized = false;
static bool g_haveBattery = false;
static bool g_haveAdapter = false;
static char g_batteryPath[128];
static char g_adapterPath[128];
static int32_t g_batteryNode = -1;
static kcp::Mutex g_lock;
// Set for the boot-path probe only, so the per-method tracing does not
// repeat on every poll once the system is up.
static bool g_tracing = false;
static bool Refresh(Status& out);
static void Probe();
static Status g_cached{};
static bool g_cacheValid = false;
static uint64_t g_cacheStamp = 0;
// ============================================================================
// Path helpers
// ============================================================================
static void AppendMethod(const char* devicePath, const char* method, char* out, int maxLen) {
int i = 0;
while (devicePath[i] && i < maxLen - 6) {
out[i] = devicePath[i];
i++;
}
// The root scope is written "\" with no separator before the first
// segment; every other path takes a dot.
if (!(i == 1 && out[0] == '\\'))
out[i++] = '.';
for (int j = 0; j < 4 && i < maxLen - 1; j++)
out[i++] = method[j];
out[i] = '\0';
}
static void CopyPath(char* dst, const char* src, int maxLen) {
int i = 0;
while (src[i] && i < maxLen - 1) {
dst[i] = src[i];
i++;
}
dst[i] = '\0';
}
// ============================================================================
// Initialize
// ============================================================================
void Initialize() {
if (g_initialized) return;
g_initialized = true;
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return;
// Searched by HID rather than off an enumerated list: a laptop
// namespace runs well past the list's fixed size, and the battery
// sits deep under the EC where it would be cut off.
AcpiDevices::DeviceInfo bat{};
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_BATTERY, bat)) {
CopyPath(g_batteryPath, bat.Path, sizeof(g_batteryPath));
g_batteryNode = bat.NodeIndex;
g_haveBattery = true;
}
AcpiDevices::DeviceInfo ac{};
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_AC_ADAPTER, ac)) {
CopyPath(g_adapterPath, ac.Path, sizeof(g_adapterPath));
g_haveAdapter = true;
}
if (!g_haveBattery) {
// Distinguish "this machine has no battery" from "the namespace
// walk found nothing at all", which would point at the walk
// rather than the hardware.
auto& ns = interp.GetNamespace();
int deviceCount = 0;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t, const AML::NamespaceNode*) { deviceCount++; });
KernelLogStream(INFO, "Battery") << "No ACPI battery device (PNP0C0A) among "
<< base::dec << (uint64_t)deviceCount << " ACPI devices";
return;
}
KernelLogStream(OK, "Battery") << "ACPI battery at " << g_batteryPath
<< (g_haveAdapter ? " (AC adapter present)" : " (no AC adapter device)");
Probe();
}
// Evaluate the battery methods once, here on the boot path, announcing
// each before it runs. These are the first firmware methods this kernel
// executes that reach the embedded controller, and if one of them takes
// the machine down the last line printed says which -- far better than
// discovering it when the desktop makes its first query.
static void Probe() {
KernelLogStream(INFO, "Battery") << "Probing battery methods";
g_tracing = true;
Status s{};
bool ok = Refresh(s);
g_tracing = false;
if (!ok) {
KernelLogStream(WARNING, "Battery") << "Probe failed; battery status unavailable";
g_haveBattery = false;
return;
}
g_cached = s;
g_cacheStamp = 0; // let the first real query refresh
g_cacheValid = true;
if (!s.Present) {
KernelLogStream(INFO, "Battery") << "Battery slot empty"
<< (s.AcOnline ? " (on AC)" : "");
return;
}
KernelLogStream(OK, "Battery") << "Battery at " << base::dec
<< (uint64_t)s.Percent << "% ("
<< (s.Charging ? "charging" : (s.Discharging ? "discharging" : "idle"))
<< ", " << (uint64_t)s.RemainingCapacity << " of "
<< (uint64_t)s.FullCapacity << (s.PowerUnitMilliAmps ? " mAh)" : " mWh)");
}
bool IsPresent() {
return g_haveBattery;
}
// ============================================================================
// Method evaluation
// ============================================================================
static const char* TypeName(AML::ObjectType t) {
switch (t) {
case AML::ObjectType::None: return "none";
case AML::ObjectType::Integer: return "integer";
case AML::ObjectType::String: return "string";
case AML::ObjectType::Buffer: return "buffer";
case AML::ObjectType::Package: return "package";
case AML::ObjectType::Device: return "device";
case AML::ObjectType::Method: return "method";
case AML::ObjectType::OperationRegion: return "opregion";
case AML::ObjectType::Field: return "field";
default: return "other";
}
}
static bool EvalPackage(const char* devicePath, const char* method, AML::Object& out) {
char path[160];
AppendMethod(devicePath, method, path, sizeof(path));
auto& interp = AML::GetInterpreter();
// Separate "the method is not there" from "it ran and gave us
// something that is not a package": those need opposite fixes, and
// the failure looks identical from the caller.
int32_t node = interp.GetNamespace().FindNode(path);
if (node < 0) {
if (g_tracing)
KernelLogStream(INFO, "Battery") << " " << path << ": absent";
return false;
}
// Trace the opcodes of the one method whose behaviour we cannot
// otherwise explain: it runs to completion, trips no limit, and
// still yields no value.
bool traceThis = g_tracing && method[1] == 'B';
if (traceThis) interp.SetTrace(120);
bool ok = interp.EvaluateObject(path, out);
if (traceThis) interp.SetTrace(0);
if (g_tracing) {
KernelLogStream(INFO, "Battery") << " " << path << ": "
<< (ok ? "ok" : "FAILED") << ", returned " << TypeName(out.Type)
<< " with " << base::dec
<< (uint64_t)interp.GetPackageCount(out) << " element(s)";
}
return ok && out.Type == AML::ObjectType::Package;
}
static uint32_t ElementInteger(const AML::Object& pkg, int index) {
AML::Object elem{};
if (!AML::GetInterpreter().GetPackageElement(pkg, index, elem)) return Unknown32;
if (elem.Type != AML::ObjectType::Integer) return Unknown32;
return (uint32_t)elem.Integer;
}
// _BIX supersedes _BIF and prepends a Revision field, shifting every
// shared field by one. Both are read into the same three numbers.
struct BatteryInfo {
bool Valid;
bool PowerUnitMilliAmps;
uint32_t DesignCapacity;
uint32_t FullCapacity;
};
static bool ReadInfo(BatteryInfo& info) {
info = BatteryInfo{};
AML::Object pkg{};
int base = 0;
if (EvalPackage(g_batteryPath, "_BIX", pkg)) {
// _BIX revision 0 starts with Revision; later revisions keep it
// in the same place, so the offset is fixed at one.
base = 1;
} else if (EvalPackage(g_batteryPath, "_BIF", pkg)) {
base = 0;
} else {
return false;
}
uint32_t unit = ElementInteger(pkg, base + 0);
uint32_t design = ElementInteger(pkg, base + 1);
uint32_t full = ElementInteger(pkg, base + 2);
info.PowerUnitMilliAmps = (unit != Unknown32) && (unit & 1);
info.DesignCapacity = (design == Unknown32) ? 0 : design;
info.FullCapacity = (full == Unknown32) ? 0 : full;
info.Valid = true;
return true;
}
static bool AcOnline() {
if (!g_haveAdapter) return false;
char path[160];
AppendMethod(g_adapterPath, "_PSR", path, sizeof(path));
if (g_tracing)
KernelLogStream(INFO, "Battery") << " evaluating " << path;
AML::Object result{};
if (!AML::GetInterpreter().EvaluateObject(path, result)) return false;
return result.Type == AML::ObjectType::Integer && result.Integer != 0;
}
// ============================================================================
// GetStatus
// ============================================================================
static bool Refresh(Status& out) {
out = Status{};
out.MinutesRemaining = -1;
out.HasAcAdapter = g_haveAdapter;
// _STA bit 4 says whether a battery is actually in the slot. A
// laptop with the pack removed still has the device.
uint32_t sta = (g_batteryNode >= 0)
? AcpiDevices::EvaluateSta(g_batteryNode)
: AcpiDevices::STA_DEFAULT;
// Bit 4 is the one that decides everything below, and firmware
// clears it for reasons that have nothing to do with the pack
// (an EC that has not been told its region handler is live, most
// often), so log the raw value rather than just the verdict.
if (g_tracing) {
KernelLogStream(INFO, "Battery") << " _STA = " << base::hex
<< (uint64_t)sta << (((sta & AcpiDevices::STA_BATTERY) != 0)
? " (battery present)" : " (battery bit clear)");
}
out.AcOnline = AcOnline();
// _STA bit 4 is the spec's answer for "is a pack installed", but
// it is only as good as the firmware path behind it. Read the
// battery methods either way and let the capacity data have the
// final say -- see the staSaysAbsent handling below.
bool staSaysAbsent = (sta & AcpiDevices::STA_BATTERY) == 0;
BatteryInfo info{};
if (!ReadInfo(info)) {
if (g_tracing)
KernelLogStream(INFO, "Battery") << " no _BIX/_BIF package";
return true;
}
AML::Object bst{};
if (!EvalPackage(g_batteryPath, "_BST", bst)) {
if (g_tracing)
KernelLogStream(INFO, "Battery") << " no _BST package";
return true;
}
uint32_t state = ElementInteger(bst, 0);
uint32_t rate = ElementInteger(bst, 1);
uint32_t remaining = ElementInteger(bst, 2);
uint32_t voltage = ElementInteger(bst, 3);
if (g_tracing) {
KernelLogStream(INFO, "Battery") << " _BIF/_BIX: design="
<< base::dec << (uint64_t)info.DesignCapacity
<< " lastFull=" << (uint64_t)info.FullCapacity
<< (info.PowerUnitMilliAmps ? " (mAh)" : " (mWh)");
KernelLogStream(INFO, "Battery") << " _BST: state=" << base::hex
<< (uint64_t)state << " rate=" << base::dec << (uint64_t)rate
<< " remaining=" << (uint64_t)remaining
<< " voltage=" << (uint64_t)voltage << "mV";
}
// Trust real capacity data over a status bit that says otherwise:
// firmware that reports a pack's charge is describing a pack that
// is there.
if (staSaysAbsent) {
bool looksReal = info.FullCapacity > 0 && info.FullCapacity != Unknown32
&& remaining != Unknown32;
if (!looksReal) {
if (g_tracing)
KernelLogStream(INFO, "Battery")
<< " _STA says absent and the methods agree";
return true;
}
if (g_tracing)
KernelLogStream(WARNING, "Battery")
<< " _STA says absent but _BST reports charge; trusting _BST";
}
if (state == Unknown32) state = 0;
out.Present = true;
out.Charging = (state & StateCharging) != 0;
out.Discharging = (state & StateDischarging) != 0;
out.Critical = (state & StateCritical) != 0;
out.PowerUnitMilliAmps = info.PowerUnitMilliAmps;
out.DesignCapacity = info.DesignCapacity;
out.FullCapacity = info.FullCapacity;
out.RemainingCapacity = (remaining == Unknown32) ? 0 : remaining;
out.PresentRate = (rate == Unknown32) ? 0 : rate;
out.VoltageMv = (voltage == Unknown32) ? 0 : voltage;
// Some firmware leaves last-full at zero or below the current
// reading after a swap; fall back to the design capacity, and
// clamp so the panel can never show 143%.
uint32_t full = out.FullCapacity;
if (full == 0) full = out.DesignCapacity;
if (full > 0 && out.RemainingCapacity > 0) {
uint64_t pct = ((uint64_t)out.RemainingCapacity * 100) / full;
out.Percent = (pct > 100) ? 100 : (uint8_t)pct;
}
if (out.PresentRate > 0 && out.Discharging) {
out.MinutesRemaining =
(int32_t)(((uint64_t)out.RemainingCapacity * 60) / out.PresentRate);
}
return true;
}
bool GetStatus(Status& out) {
if (!g_haveBattery) return false;
g_lock.Acquire();
uint64_t now = Timekeeping::GetMilliseconds();
if (g_cacheValid && now - g_cacheStamp < CacheMs) {
out = g_cached;
g_lock.Release();
return true;
}
Status fresh{};
bool ok = Refresh(fresh);
if (ok) {
g_cached = fresh;
g_cacheStamp = now;
g_cacheValid = true;
}
out = ok ? fresh : g_cached;
g_lock.Release();
return ok || g_cacheValid;
}
};
};
+62
View File
@@ -0,0 +1,62 @@
/*
* Battery.hpp
* ACPI Control Method Battery (PNP0C0A) and AC adapter (ACPI0003)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace Battery {
// ============================================================================
// Battery state
// ============================================================================
// _BST battery state bits.
static constexpr uint32_t StateDischarging = (1 << 0);
static constexpr uint32_t StateCharging = (1 << 1);
static constexpr uint32_t StateCritical = (1 << 2);
struct Status {
bool Present; // a battery slot exists and reports a battery in it
bool Charging;
bool Discharging;
bool Critical;
bool AcOnline; // AC adapter connected (false when no adapter device)
bool HasAcAdapter; // an ACPI0003 device exists
uint8_t Percent; // 0..100, clamped; 0 when capacity is unknown
uint32_t RemainingCapacity; // mWh or mAh, per PowerUnitMilliAmps
uint32_t FullCapacity; // last full charge capacity, same unit
uint32_t DesignCapacity;
uint32_t PresentRate; // mW or mA, 0 when unknown
uint32_t VoltageMv; // present voltage in mV, 0 when unknown
bool PowerUnitMilliAmps; // capacities are mAh/mA rather than mWh/mW
// Minutes of runtime left at the present rate, or -1 when the rate
// is unknown/zero (which includes every charging battery).
int32_t MinutesRemaining;
};
// ============================================================================
// API
// ============================================================================
// Discover battery and AC adapter devices in the ACPI namespace. Safe to
// call more than once; only the first call does work.
void Initialize();
// True when at least one battery device was found, whether or not a
// battery is currently installed in it.
bool IsPresent();
// Read the current status. Results are cached briefly so that polling
// from a panel indicator does not hammer the embedded controller.
// Returns false when the machine has no battery device at all.
bool GetStatus(Status& out);
};
};
+326
View File
@@ -0,0 +1,326 @@
/*
* EmbeddedController.cpp
* ACPI Embedded Controller transport (polled, no burst mode)
* Copyright (c) 2026 Daniel Hammer
*/
#include "EmbeddedController.hpp"
#include "ACPI.hpp"
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <Io/IoPort.hpp>
#include <Memory/HHDM.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Hal {
namespace EmbeddedController {
// ============================================================================
// EC register protocol
// ============================================================================
// EC_SC (status, read from the command port)
static constexpr uint8_t SC_OBF = (1 << 0); // output buffer full: data is readable
static constexpr uint8_t SC_IBF = (1 << 1); // input buffer full: EC has not consumed our byte
// EC_SC commands (written to the command port)
static constexpr uint8_t CMD_READ = 0x80;
static constexpr uint8_t CMD_WRITE = 0x81;
// The EC is a slow microcontroller; a byte transaction is typically a
// few hundred microseconds but firmware is allowed to take much longer.
// One "tick" is a port-wait (~1us), so this caps a wait near 500ms.
static constexpr uint32_t PollTicks = 500000;
// ============================================================================
// State
// ============================================================================
static uint16_t g_dataPort = 0;
static uint16_t g_cmdPort = 0;
static bool g_available = false;
static bool g_initialized = false;
// A Mutex, not a Spinlock: EC transactions run only from process and
// syscall context, and can spin for milliseconds waiting on the
// controller. A Spinlock would hold interrupts off for all of it.
static kcp::Mutex g_lock;
// ============================================================================
// ECDT
// ============================================================================
// The ECDT gives the EC's ports without needing the AML namespace. It is
// optional, and some firmware ships one with zeroed ports, so treat a
// zero port as "not described here" and fall through to the namespace.
struct EcdtTable {
ACPI::CommonSDTHeader Header;
uint8_t ControlGas[12];
uint8_t DataGas[12];
uint32_t Uid;
uint8_t GpeBit;
// followed by a null-terminated namespace path
} __attribute__((packed));
static bool SignatureMatch(const char* sig, const char* target) {
for (int i = 0; i < 4; i++) {
if (sig[i] != target[i]) return false;
}
return true;
}
// Decode a Generic Address Structure that must describe a 16-bit I/O port.
static uint16_t GasIoPort(const uint8_t gas[12]) {
if (gas[0] != 1) return 0; // AddressSpaceId 1 == SystemIO
uint64_t address = 0;
for (int i = 0; i < 8; i++)
address |= (uint64_t)gas[4 + i] << (i * 8);
return address <= 0xFFFF ? (uint16_t)address : 0;
}
static bool FromEcdt(ACPI::CommonSDTHeader* xsdt) {
if (xsdt == nullptr) return false;
uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8;
uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader));
for (uint32_t i = 0; i < entryCount; i++) {
auto* header = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
if (!SignatureMatch(header->Signature, "ECDT")) continue;
if (header->Length < sizeof(EcdtTable)) continue;
auto* ecdt = (EcdtTable*)header;
uint16_t cmd = GasIoPort(ecdt->ControlGas);
uint16_t data = GasIoPort(ecdt->DataGas);
if (cmd == 0 || data == 0) continue;
g_cmdPort = cmd;
g_dataPort = data;
return true;
}
return false;
}
// ============================================================================
// Namespace fallback
// ============================================================================
// _CRS on an EC device lists two I/O resources in a fixed order: data
// register first, then command/status. Anything else is not an EC we
// know how to drive.
static bool FromNamespace() {
if (!AML::GetInterpreter().IsInitialized()) return false;
AcpiDevices::DeviceInfo ec{};
if (!AcpiDevices::FindDeviceByHid(AcpiDevices::HID_EC, ec)) return false;
AML::ResourceList crs{};
if (!AcpiDevices::EvaluateCrs(ec.NodeIndex, crs)) return false;
uint16_t ports[2] = {0, 0};
int found = 0;
for (int i = 0; i < crs.Count && found < 2; i++) {
if (crs.Resources[i].Type == AML::ResourceType::IoPort)
ports[found++] = crs.Resources[i].IoPort.Base;
else if (crs.Resources[i].Type == AML::ResourceType::FixedIoPort)
ports[found++] = crs.Resources[i].FixedIoPort.Base;
}
if (found < 2 || ports[0] == 0 || ports[1] == 0) return false;
g_dataPort = ports[0];
g_cmdPort = ports[1];
return true;
}
// ============================================================================
// Initialize
// ============================================================================
void Initialize(void* xsdt) {
if (g_initialized) return;
g_initialized = true;
if (!FromEcdt((ACPI::CommonSDTHeader*)xsdt) && !FromNamespace()) {
KernelLogStream(INFO, "EC") << "No ACPI Embedded Controller present";
return;
}
g_available = true;
KernelLogStream(OK, "EC") << "Embedded Controller at cmd=" << base::hex
<< (uint64_t)g_cmdPort << " data=" << (uint64_t)g_dataPort;
RunRegMethods();
KernelLogStream(INFO, "EC") << "EC space dump:";
DumpSpace(0x00, 32);
}
// ============================================================================
// _REG
// ============================================================================
void RunRegMethods() {
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return;
auto& ns = interp.GetNamespace();
// Collect first, evaluate second. Executing AML can create namespace
// nodes, which reallocates a parent's child array -- walking the
// tree while methods run would be iterating a freed array.
static constexpr int MaxRegMethods = 64;
int32_t nodes[MaxRegMethods];
int count = 0;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
[&](int32_t idx, const AML::NamespaceNode* node) {
if (count >= MaxRegMethods) return;
if (node->Name[0] == '_' && node->Name[1] == 'R' &&
node->Name[2] == 'E' && node->Name[3] == 'G')
nodes[count++] = idx;
});
// Arg0 = region space (3, EmbeddedControl), Arg1 = 1 (handler is
// connected). A _REG that handles some other space checks Arg0 and
// ignores the call, so this is safe to issue namespace-wide.
AML::Object args[2]{};
args[0].Type = AML::ObjectType::Integer;
args[0].Integer = (uint64_t)AML::RegionSpace::EmbeddedControl;
args[1].Type = AML::ObjectType::Integer;
args[1].Integer = 1;
int ran = 0;
for (int i = 0; i < count; i++) {
char path[256];
ns.GetNodePath(nodes[i], path, sizeof(path));
AML::Object result{};
bool ok = interp.EvaluateMethod(path, args, 2, result);
if (ok) ran++;
KernelLogStream(INFO, "EC") << " _REG " << path
<< (ok ? " ok" : " FAILED");
}
KernelLogStream(OK, "EC") << "Ran " << base::dec << (uint64_t)ran
<< " of " << (uint64_t)count << " _REG method(s)";
}
// ============================================================================
// Diagnostics
// ============================================================================
void DumpSpace(uint8_t base_, int bytes) {
if (!g_available) return;
// Whether the transport works at all is not otherwise observable:
// a failed read and a genuinely zero register look identical by the
// time firmware AML has turned them into a status value. All-00 or
// all-FF here means the handshake is wrong, not that the battery is
// missing.
for (int row = 0; row < bytes; row += 16) {
char line[64];
int n = 0;
bool anyFailed = false;
for (int i = 0; i < 16 && (row + i) < bytes; i++) {
uint8_t v = 0;
if (!Read((uint8_t)(base_ + row + i), v)) anyFailed = true;
static const char hex[] = "0123456789ABCDEF";
line[n++] = hex[(v >> 4) & 0xF];
line[n++] = hex[v & 0xF];
line[n++] = ' ';
}
line[n] = '\0';
KernelLogStream(INFO, "EC") << " " << base::hex
<< (uint64_t)(base_ + row) << ": " << line
<< (anyFailed ? " (read timeout)" : "");
}
}
bool IsAvailable() {
return g_available;
}
// ============================================================================
// Transport
// ============================================================================
// Spin until the named status bit reaches the wanted state.
static bool WaitStatus(uint8_t mask, bool wantSet) {
for (uint32_t i = 0; i < PollTicks; i++) {
uint8_t sc = Io::In8(g_cmdPort);
if (((sc & mask) != 0) == wantSet) return true;
Io::IoPortWait();
}
return false;
}
// Drain a byte the EC left in the output buffer from an earlier aborted
// transaction. Without this the next read returns the stale value.
static void DrainOutput() {
if (Io::In8(g_cmdPort) & SC_OBF)
(void)Io::In8(g_dataPort);
}
bool Read(uint8_t address, uint8_t& value) {
if (!g_available) return false;
g_lock.Acquire();
DrainOutput();
bool ok = false;
if (WaitStatus(SC_IBF, false)) {
Io::Out8(CMD_READ, g_cmdPort);
if (WaitStatus(SC_IBF, false)) {
Io::Out8(address, g_dataPort);
if (WaitStatus(SC_OBF, true)) {
value = Io::In8(g_dataPort);
ok = true;
}
}
}
g_lock.Release();
return ok;
}
bool Write(uint8_t address, uint8_t value) {
if (!g_available) return false;
g_lock.Acquire();
DrainOutput();
bool ok = false;
if (WaitStatus(SC_IBF, false)) {
Io::Out8(CMD_WRITE, g_cmdPort);
if (WaitStatus(SC_IBF, false)) {
Io::Out8(address, g_dataPort);
if (WaitStatus(SC_IBF, false)) {
Io::Out8(value, g_dataPort);
ok = WaitStatus(SC_IBF, false);
}
}
}
g_lock.Release();
return ok;
}
};
};
+50
View File
@@ -0,0 +1,50 @@
/*
* EmbeddedController.hpp
* ACPI Embedded Controller (PNP0C09) transport
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace EmbeddedController {
// ============================================================================
// Bring-up
// ============================================================================
// Locate the EC in the ACPI namespace and latch its command/data ports.
// Prefers the ECDT table when present (it is valid before the namespace
// is loaded); otherwise walks the namespace for PNP0C09 and reads _CRS.
// Safe to call more than once; only the first call does work.
void Initialize(void* xsdt);
// True once a usable command/data port pair has been found.
bool IsAvailable();
// Run the namespace's _REG(EmbeddedControl, 1) methods, telling firmware
// that EC region access is live. ACPI requires this once a handler for
// an address space is installed, and firmware leans on it hard: EC
// methods are typically guarded by a flag that _REG sets, and until it
// is set they return safe do-nothing answers (a battery reports itself
// absent, a thermal zone reports no temperature). Called by Initialize.
void RunRegMethods();
// Log `bytes` bytes of EC address space starting at `base_`. Purely a
// diagnostic: it is the only way to tell a broken handshake from a
// register that genuinely reads zero.
void DumpSpace(uint8_t base_, int bytes);
// ============================================================================
// Transport
// ============================================================================
// Read/write one byte of the EC's 256-byte address space. Both return
// false on timeout or when no EC is present, leaving *value untouched.
bool Read(uint8_t address, uint8_t& value);
bool Write(uint8_t address, uint8_t value);
};
};
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 184 #define MONTAUK_BUILD_NUMBER 220
+27
View File
@@ -13,6 +13,7 @@
#include <ACPI/AcpiShutdown.hpp> #include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiSleep.hpp> #include <ACPI/AcpiSleep.hpp>
#include <ACPI/CpuIdle.hpp> #include <ACPI/CpuIdle.hpp>
#include <ACPI/Battery.hpp>
#include <Hal/CpuPower.hpp> #include <Hal/CpuPower.hpp>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
@@ -114,4 +115,30 @@ namespace montauk::abi {
out->apIdleHint = Hal::CpuIdle::GetApIdleHint(); out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
return 0; return 0;
} }
// SYS_BATTERYINFO. Fills a battery snapshot. Returns -1 on a machine with
// no ACPI battery device, which is how a desktop-class caller learns not to
// draw an indicator at all.
static int64_t Sys_BatteryInfo(BatteryInfo* out) {
Hal::Battery::Status s{};
if (!Hal::Battery::GetStatus(s)) {
return -1;
}
out->present = s.Present ? 1 : 0;
out->charging = s.Charging ? 1 : 0;
out->discharging = s.Discharging ? 1 : 0;
out->critical = s.Critical ? 1 : 0;
out->acOnline = s.AcOnline ? 1 : 0;
out->hasAcAdapter = s.HasAcAdapter ? 1 : 0;
out->percent = s.Percent;
out->powerUnitMilliAmps = s.PowerUnitMilliAmps ? 1 : 0;
out->remainingCapacity = s.RemainingCapacity;
out->fullCapacity = s.FullCapacity;
out->designCapacity = s.DesignCapacity;
out->presentRate = s.PresentRate;
out->voltageMv = s.VoltageMv;
out->minutesRemaining = s.MinutesRemaining;
return 0;
}
}; };
+6
View File
@@ -504,6 +504,12 @@ namespace montauk::abi {
case SYS_POWERINFO: case SYS_POWERINFO:
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1; if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1;
return Sys_PowerInfo((PowerInfo*)frame->arg1); return Sys_PowerInfo((PowerInfo*)frame->arg1);
case SYS_BATTERYINFO:
// Read-only power telemetry, ungated like SYS_POWERINFO: the
// panel indicator has to work in an unprivileged session, which
// holds no device capability to spend on it.
if (!UserMemory::Writable<BatteryInfo>(frame->arg1)) return -1;
return Sys_BatteryInfo((BatteryInfo*)frame->arg1);
case SYS_THREAD_SPAWN: case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT: case SYS_THREAD_EXIT:
+23
View File
@@ -343,6 +343,9 @@ namespace montauk::abi {
touch in full (image decode, heap slabs). */ touch in full (image decode, heap slabs). */
static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure
/* Battery.hpp -- ACPI battery status */
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
/* Kernel-owned process capabilities. User identities may namespace /* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */ per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -881,6 +884,26 @@ namespace montauk::abi {
uint32_t apIdleHint; // MWAIT hint used for AP deep idle uint32_t apIdleHint; // MWAIT hint used for AP deep idle
}; };
// Battery status (returned by SYS_BATTERYINFO). A machine with no battery
// device fails the call outright; `present` distinguishes a laptop with the
// pack removed from one that is running on it.
struct BatteryInfo {
uint8_t present; // a battery is installed
uint8_t charging;
uint8_t discharging;
uint8_t critical;
uint8_t acOnline; // AC adapter connected
uint8_t hasAcAdapter; // an AC adapter device exists at all
uint8_t percent; // 0..100, 0 when capacity is unknown
uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW
uint32_t remainingCapacity;
uint32_t fullCapacity; // last full charge capacity
uint32_t designCapacity;
uint32_t presentRate; // mW or mA, 0 when unknown
uint32_t voltageMv;
int32_t minutesRemaining; // -1 when not estimable (e.g. charging)
};
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT) // Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
struct CrashReportInfo { struct CrashReportInfo {
int pid; int pid;
+8
View File
@@ -25,6 +25,8 @@
#include <ACPI/CpuIdle.hpp> #include <ACPI/CpuIdle.hpp>
#include <ACPI/AcpiShutdown.hpp> #include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiEvents.hpp> #include <ACPI/AcpiEvents.hpp>
#include <ACPI/EmbeddedController.hpp>
#include <ACPI/Battery.hpp>
#include <Hal/Apic/ApicInit.hpp> #include <Hal/Apic/ApicInit.hpp>
#include <Pci/Pci.hpp> #include <Pci/Pci.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/ApicTimer.hpp>
@@ -131,6 +133,12 @@ extern "C" void kmain() {
#if defined (__x86_64__) #if defined (__x86_64__)
if (g_acpi.GetXSDT() != nullptr) { if (g_acpi.GetXSDT() != nullptr) {
Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT()); Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT());
// The EC is the transport behind battery (and most thermal) methods, so
// it has to be latched before anything evaluates them.
Hal::EmbeddedController::Initialize(g_acpi.GetXSDT());
Hal::Battery::Initialize();
Hal::CpuIdle::Initialize(g_acpi.GetXSDT()); Hal::CpuIdle::Initialize(g_acpi.GetXSDT());
Hal::ApicInitialize(g_acpi.GetXSDT()); Hal::ApicInitialize(g_acpi.GetXSDT());
+11
View File
@@ -42,10 +42,21 @@ static void PrintGPFInfo(System::PanicFrame*& frame) {
PrintBoxedDec(kerr, "Error Code", gpf->GeneralProtectionFaultError, BoxWidth); PrintBoxedDec(kerr, "Error Code", gpf->GeneralProtectionFaultError, BoxWidth);
} }
// Link-time base of the kernel image (see linker-x86_64.ld).
static constexpr uint64_t KernelBaseVirt = 0xFFFFFFFF80000000ULL;
static void PrintRegisters(System::PanicFrame* frame) { static void PrintRegisters(System::PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth); PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "Registers:", BoxWidth, true); PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
PrintBoxedHex(kerr, "Instruction Pointer", frame->IP, BoxWidth); PrintBoxedHex(kerr, "Instruction Pointer", frame->IP, BoxWidth);
// The absolute address means nothing without the link map. Its offset from
// the kernel's load base is what `nm bin-x86_64/kernel` can be searched
// for directly, so print that next to it.
if (frame->IP >= KernelBaseVirt)
PrintBoxedHex(kerr, " kernel+", frame->IP - KernelBaseVirt, BoxWidth);
PrintBoxedHex(kerr, "Code Segment", frame->CS, BoxWidth); PrintBoxedHex(kerr, "Code Segment", frame->CS, BoxWidth);
PrintBoxedHex(kerr, "Flags", frame->Flags, BoxWidth); PrintBoxedHex(kerr, "Flags", frame->Flags, BoxWidth);
PrintBoxedHex(kerr, "Stack Pointer", frame->SP, BoxWidth); PrintBoxedHex(kerr, "Stack Pointer", frame->SP, BoxWidth);
+9
View File
@@ -5,6 +5,14 @@
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
namespace Memory::VMM { namespace Memory::VMM {
// Top of the contiguous HHDM mapping, published by Init() for callers that
// must validate a firmware-supplied physical address before dereferencing
// it through the HHDM.
static std::uint64_t g_hhdmLimit = 0;
std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; }
Paging* g_paging = nullptr; Paging* g_paging = nullptr;
static constexpr uint64_t LeafAddressMask = 0x000FFFFFFFFFF000ULL; static constexpr uint64_t LeafAddressMask = 0x000FFFFFFFFFF000ULL;
@@ -88,6 +96,7 @@ namespace Memory::VMM {
if (entryEnd > maxPhysAddr) maxPhysAddr = entryEnd; if (entryEnd > maxPhysAddr) maxPhysAddr = entryEnd;
} }
maxPhysAddr = (maxPhysAddr + 0xFFF) & ~0xFFFULL; maxPhysAddr = (maxPhysAddr + 0xFFF) & ~0xFFFULL;
g_hhdmLimit = maxPhysAddr;
for (uint64_t pageAddr = 0; pageAddr < maxPhysAddr; pageAddr += 0x1000) { for (uint64_t pageAddr = 0; pageAddr < maxPhysAddr; pageAddr += 0x1000) {
Map(pageAddr, HHDM(pageAddr)); Map(pageAddr, HHDM(pageAddr));
+8
View File
@@ -90,6 +90,14 @@ public:
Paging(); Paging();
void Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap, void Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
const montauk::boot::Framebuffer& framebuffer); const montauk::boot::Framebuffer& framebuffer);
// Top of the contiguous HHDM mapping built by Init(). A physical
// address at or above this is NOT reachable at phys+hhdmBase, and
// dereferencing it there faults (non-canonical addresses raise #GP
// rather than #PF). Anything that turns firmware-supplied numbers into
// HHDM pointers -- the AML interpreter's SystemMemory regions above all
// -- must check against this first.
static std::uint64_t HhdmLimit();
void Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress);
void MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress);
void MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress);
+6 -1
View File
@@ -18,7 +18,12 @@ namespace Sched {
// every slot uniformly -- the primary/sibling distinction matters only // every slot uniformly -- the primary/sibling distinction matters only
// for resource ownership on exit. // for resource ownership on exit.
static constexpr int MaxProcesses = 256; static constexpr int MaxProcesses = 256;
static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per thread // 64 KiB kernel stack per thread. The AML interpreter is the driver of
// this size: it runs on the calling thread's stack when a syscall asks for
// battery status, and firmware methods nest deeply through frames that
// carry 256-byte path buffers and 272-byte AML objects. See
// AML::StackBudget, which must stay comfortably under this.
static constexpr uint64_t StackPages = 16;
static constexpr uint64_t StackSize = StackPages * 0x1000; static constexpr uint64_t StackSize = StackPages * 0x1000;
static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread, eagerly mapped) static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread, eagerly mapped)
static constexpr uint64_t UserStackSize = UserStackPages * 0x1000; static constexpr uint64_t UserStackSize = UserStackPages * 0x1000;
+23
View File
@@ -256,6 +256,9 @@ namespace montauk::abi {
// in one at a time. For buffers the caller is about to touch in full. // in one at a time. For buffers the caller is about to touch in full.
static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure
/* Battery.hpp -- ACPI battery status */
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
/* Kernel-owned process capabilities. User identities may namespace /* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */ per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -808,6 +811,26 @@ namespace montauk::abi {
uint64_t pageSize; uint64_t pageSize;
}; };
// Battery status (returned by SYS_BATTERYINFO). A machine with no battery
// device fails the call outright; `present` distinguishes a laptop with the
// pack removed from one that is running on it.
struct BatteryInfo {
uint8_t present; // a battery is installed
uint8_t charging;
uint8_t discharging;
uint8_t critical;
uint8_t acOnline; // AC adapter connected
uint8_t hasAcAdapter; // an AC adapter device exists at all
uint8_t percent; // 0..100, 0 when capacity is unknown
uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW
uint32_t remainingCapacity;
uint32_t fullCapacity; // last full charge capacity
uint32_t designCapacity;
uint32_t presentRate; // mW or mA, 0 when unknown
uint32_t voltageMv;
int32_t minutesRemaining; // -1 when not estimable (e.g. charging)
};
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT) // Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
struct CrashReportInfo { struct CrashReportInfo {
int pid; int pid;
-680
View File
@@ -1,680 +0,0 @@
/*
* syntax_ansi.hpp
* ANSI terminal syntax highlighting for C, C++ and Lua
* Used by the MontaukOS CLI text editor
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <stdint.h>
// ============================================================================
// Token types
// ============================================================================
enum SynLanguage : uint8_t {
SYN_LANG_NONE,
SYN_LANG_C,
SYN_LANG_CPP,
SYN_LANG_LUA,
};
enum SynToken : uint8_t {
SYN_NORMAL,
SYN_KEYWORD,
SYN_TYPE,
SYN_PREPROCESSOR,
SYN_STRING,
SYN_CHAR,
SYN_COMMENT,
SYN_NUMBER,
SYN_OPERATOR,
};
#define SYN_RAW_DELIM_MAX 16
struct SynState {
int in_block_comment; // 0 or 1 (bool stored as int for freestanding)
SynToken long_token;
int long_bracket_eqs;
bool in_raw_string;
char raw_delim[SYN_RAW_DELIM_MAX];
int raw_delim_len;
};
// ============================================================================
// Helpers
// ============================================================================
inline bool syn_is_alpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
inline bool syn_is_alnum(char c) {
return syn_is_alpha(c) || (c >= '0' && c <= '9');
}
inline bool syn_is_digit(char c) {
return c >= '0' && c <= '9';
}
inline bool syn_is_hex(char c) {
return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
inline void syn_set_token(SynToken* out, int out_len, int idx, SynToken tok) {
if (out && idx >= 0 && idx < out_len)
out[idx] = tok;
}
inline void syn_fill_tokens(SynToken* out, int out_len, int start, int end, SynToken tok) {
if (!out) return;
if (start < 0) start = 0;
if (end > out_len) end = out_len;
for (int i = start; i < end; i++)
out[i] = tok;
}
inline bool syn_streq(const char* buf, int len, const char* kw) {
int i = 0;
while (i < len && kw[i]) {
if (buf[i] != kw[i]) return false;
i++;
}
return i == len && kw[i] == '\0';
}
// ============================================================================
// Keyword / type classification
// ============================================================================
inline SynToken syn_classify_c_word(const char* buf, int len) {
static const char* keywords[] = {
"auto", "break", "case", "const", "continue", "default", "do",
"else", "enum", "extern", "for", "goto", "if", "inline",
"register", "restrict", "return", "sizeof", "static", "struct",
"switch", "typedef", "union", "volatile", "while",
"NULL", "true", "false", "nullptr",
};
static const char* types[] = {
"void", "char", "short", "int", "long", "float", "double",
"signed", "unsigned", "bool", "_Bool",
"int8_t", "int16_t", "int32_t", "int64_t",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
"FILE",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_lua_word(const char* buf, int len) {
static const char* keywords[] = {
"and", "break", "do", "else", "elseif", "end", "false",
"for", "function", "goto", "if", "in", "local", "nil",
"not", "or", "repeat", "return", "then", "true",
"until", "while",
};
static const char* builtins[] = {
"_ENV", "_G", "_VERSION",
"assert", "collectgarbage", "coroutine", "debug", "dofile",
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
"math", "next", "os", "package", "pairs", "pcall",
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
"select", "setmetatable", "string", "table", "tonumber",
"tostring", "type", "utf8", "warn", "xpcall",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) {
if (syn_streq(buf, len, builtins[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
// C++ keywords (on top of the C set, which is checked first)
static const char* keywords[] = {
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
"catch", "class", "compl", "concept", "consteval", "constexpr",
"constinit", "const_cast", "co_await", "co_return", "co_yield",
"decltype", "delete", "dynamic_cast", "explicit", "export",
"final", "friend", "mutable", "namespace", "new", "noexcept",
"not", "not_eq", "operator", "or", "or_eq", "override",
"private", "protected", "public", "reinterpret_cast", "requires",
"static_assert", "static_cast", "template", "this", "thread_local",
"throw", "try", "typeid", "typename", "using", "virtual",
"xor", "xor_eq",
};
// C++ library / builtin types
static const char* types[] = {
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
"std", "string", "string_view", "vector", "array", "span",
"map", "set", "unordered_map", "unordered_set", "deque", "list",
"pair", "tuple", "optional", "variant", "function",
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
};
SynToken c = syn_classify_c_word(buf, len);
if (c != SYN_NORMAL) return c;
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len) {
switch (lang) {
case SYN_LANG_C: return syn_classify_c_word(buf, len);
case SYN_LANG_CPP: return syn_classify_cpp_word(buf, len);
case SYN_LANG_LUA: return syn_classify_lua_word(buf, len);
default: return SYN_NORMAL;
}
}
inline SynState syn_make_state() {
SynState state = {};
state.in_block_comment = 0;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
state.in_raw_string = false;
state.raw_delim_len = 0;
return state;
}
// ============================================================================
// Lua long bracket matching
// ============================================================================
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
int& eqs, int& span) {
if (i >= len || line[i] != '[') return false;
int j = i + 1;
while (j < len && line[j] == '=') j++;
if (j < len && line[j] == '[') {
eqs = j - i - 1;
span = j - i + 1;
return true;
}
return false;
}
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
int eqs, int& span) {
if (i >= len || line[i] != ']') return false;
int j = i + 1;
for (int k = 0; k < eqs; k++) {
if (j >= len || line[j] != '=') return false;
j++;
}
if (j < len && line[j] == ']') {
span = j - i + 1;
return true;
}
return false;
}
// ============================================================================
// Number consumption
// ============================================================================
inline void syn_scan_digits(const char* line, int len, int& i, bool sep, bool hex) {
while (i < len) {
if (hex ? syn_is_hex(line[i]) : syn_is_digit(line[i])) { i++; continue; }
// C++14 digit separator: a quote wedged between two digits
if (sep && line[i] == '\'' && i + 1 < len &&
(hex ? syn_is_hex(line[i + 1]) : syn_is_digit(line[i + 1]))) {
i += 2;
continue;
}
break;
}
}
inline void syn_consume_number(const char* line, int len, int& i,
SynToken* out, int out_len, bool c_style_suffixes,
bool digit_sep = false) {
int start = i;
if (line[i] == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) {
i += 2;
syn_scan_digits(line, len, i, digit_sep, true);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, true);
}
if (i < len && (line[i] == 'p' || line[i] == 'P')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
} else {
if (line[i] == '.') i++;
syn_scan_digits(line, len, i, digit_sep, false);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, false);
}
if (i < len && (line[i] == 'e' || line[i] == 'E')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
}
if (c_style_suffixes) {
while (i < len && (line[i] == 'u' || line[i] == 'U' ||
line[i] == 'l' || line[i] == 'L' ||
line[i] == 'f' || line[i] == 'F' ||
line[i] == 'z' || line[i] == 'Z'))
i++;
}
syn_fill_tokens(out, out_len, start, i, SYN_NUMBER);
}
// ============================================================================
// C highlighting
// ============================================================================
// C / C++ raw string helpers
// ============================================================================
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
syn_streq(buf, len, "u8R");
}
inline bool syn_is_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
}
// Colors a raw-string body from `i` up to and including the closing )delim".
// If the terminator is not on this line, leaves state.in_raw_string set so the
// next line continues the literal.
inline void syn_consume_raw_body(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
while (i < len) {
if (line[i] == ')') {
int j = i + 1;
int k = 0;
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
j++;
k++;
}
if (k == state.raw_delim_len && j < len && line[j] == '"') {
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
i = j + 1;
state.in_raw_string = false;
state.raw_delim_len = 0;
return;
}
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
}
// Enters a raw string; `line[i]` must be the opening quote.
inline void syn_consume_raw_string(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.raw_delim_len = 0;
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
state.raw_delim[state.raw_delim_len++] = line[i];
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
if (i < len && line[i] == '(') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.in_raw_string = true;
syn_consume_raw_body(line, len, i, out, out_len, state);
return;
}
// Malformed (or an over-long delimiter): treat the rest of the line as string
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
i = len;
}
// ============================================================================
// C / C++ highlighting
// ============================================================================
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
SynState& state, SynLanguage lang = SYN_LANG_C) {
const bool cpp = (lang == SYN_LANG_CPP);
int i = 0;
while (i < len) {
// ---- Raw string continuation ----
if (state.in_raw_string) {
syn_consume_raw_body(line, len, i, out, out_len, state);
continue;
}
// ---- Block comment continuation ----
if (state.in_block_comment) {
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
char c = line[i];
// ---- Line comment ----
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// ---- Block comment start ----
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
continue;
}
// ---- Preprocessor directive ----
if (c == '#') {
// Check that only whitespace precedes the #
bool is_pp = true;
for (int j = 0; j < i; j++) {
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
}
if (is_pp) {
while (i < len) {
// Handle line-comment inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// Handle block comment start inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
// Continue consuming as comment
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
i++;
}
continue;
}
}
// ---- String literal ----
if (c == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
if (line[i] == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
continue;
}
// ---- Character literal ----
if (c == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_CHAR);
syn_set_token(out, out_len, i + 1, SYN_CHAR);
i += 2;
continue;
}
if (line[i] == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
}
continue;
}
// ---- Numbers ----
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, true, cpp);
continue;
}
// ---- Identifiers / keywords / types / literal prefixes ----
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
if (cpp && i < len && line[i] == '"' &&
syn_is_raw_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
syn_consume_raw_string(line, len, i, out, out_len, state);
continue;
}
if (cpp && i < len && line[i] == '"' &&
syn_is_string_prefix(line + start, i - start)) {
// The quote itself is handled on the next iteration
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
continue;
}
if (cpp && i < len && line[i] == '\'' &&
syn_is_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
continue;
}
SynToken tok = syn_classify_word(lang, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
// ---- Operators ----
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '=' || c == '!' || c == '<' || c == '>' || c == '&' ||
c == '|' || c == '^' || c == '~' || c == '?' || c == ':') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
// ---- Everything else (whitespace, braces, parens, etc.) ----
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
// ============================================================================
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
SynState& state) {
int i = 0;
while (i < len) {
if (state.long_token != SYN_NORMAL) {
int span = 0;
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
i += span;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
continue;
}
syn_set_token(out, out_len, i, state.long_token);
i++;
continue;
}
char c = line[i];
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
int eqs = 0;
int span = 0;
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
i += 2 + span;
state.long_token = SYN_COMMENT;
state.long_bracket_eqs = eqs;
continue;
}
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
if (c == '"' || c == '\'') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
break;
}
i++;
}
continue;
}
int eqs = 0;
int span = 0;
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
i += span;
state.long_token = SYN_STRING;
state.long_bracket_eqs = eqs;
continue;
}
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, 0);
continue;
}
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '^' || c == '#' || c == '=' || c == '<' || c == '>' ||
c == '~' || c == ':' || c == '.' || c == ',' || c == ';' ||
c == '(' || c == ')' || c == '{' || c == '}' ||
c == '[' || c == ']') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
// ============================================================================
// Per-line highlighter
// ============================================================================
inline void syn_highlight_line(const char* line, int len, SynToken* out, int out_len,
SynLanguage lang, SynState& state) {
if (!line || len <= 0) return;
switch (lang) {
case SYN_LANG_C:
case SYN_LANG_CPP:
syn_highlight_line_c(line, len, out, out_len, state, lang);
break;
case SYN_LANG_LUA:
syn_highlight_line_lua(line, len, out, out_len, state);
break;
default:
syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL);
break;
}
}
// ============================================================================
// Language detection
// ============================================================================
inline bool syn_path_ends_with(const char* path, const char* suffix) {
if (!path || !suffix) return false;
int path_len = 0;
while (path[path_len]) path_len++;
int suffix_len = 0;
while (suffix[suffix_len]) suffix_len++;
if (path_len < suffix_len) return false;
for (int i = 0; i < suffix_len; i++) {
if (path[path_len - suffix_len + i] != suffix[i])
return false;
}
return true;
}
inline SynLanguage syn_detect_language(const char* filepath) {
if (!filepath || filepath[0] == '\0') return SYN_LANG_NONE;
if (syn_path_ends_with(filepath, ".cpp") || syn_path_ends_with(filepath, ".cc") ||
syn_path_ends_with(filepath, ".cxx") || syn_path_ends_with(filepath, ".c++") ||
syn_path_ends_with(filepath, ".hpp") || syn_path_ends_with(filepath, ".hh") ||
syn_path_ends_with(filepath, ".hxx") || syn_path_ends_with(filepath, ".h++") ||
syn_path_ends_with(filepath, ".ipp") || syn_path_ends_with(filepath, ".tpp") ||
syn_path_ends_with(filepath, ".inl"))
return SYN_LANG_CPP;
if (syn_path_ends_with(filepath, ".c") || syn_path_ends_with(filepath, ".h"))
return SYN_LANG_C;
if (syn_path_ends_with(filepath, ".lua"))
return SYN_LANG_LUA;
return SYN_LANG_NONE;
}
+10
View File
@@ -217,6 +217,16 @@ struct DesktopState {
uint64_t wifi_status_time; uint64_t wifi_status_time;
// Battery indicator. Absent entirely on a machine with no ACPI battery
// device, which is how a desktop keeps a clean panel.
SvgIcon icon_battery;
char battery_icon_name[64]; // which glyph icon_battery holds
bool battery_present; // a battery device exists
bool battery_popup_open;
Rect battery_icon_rect;
montauk::abi::BatteryInfo battery_info;
uint64_t battery_last_poll;
bool vol_popup_open; bool vol_popup_open;
Rect vol_icon_rect; Rect vol_icon_rect;
int vol_level; // 0-100 int vol_level; // 0-100
+1
View File
@@ -211,6 +211,7 @@ extern "C" {
#define MTK_SYS_SPAWN_CAPS 185 #define MTK_SYS_SPAWN_CAPS 185
#define MTK_SYS_SPAWN_REDIR_CAPS 186 #define MTK_SYS_SPAWN_REDIR_CAPS 186
#define MTK_SYS_ALLOC_EAGER 187 #define MTK_SYS_ALLOC_EAGER 187
#define MTK_SYS_BATTERYINFO 188
/* @SYSCALLS-END */ /* @SYSCALLS-END */
#define MTK_SOCK_TCP 1 #define MTK_SOCK_TCP 1
+7
View File
@@ -639,6 +639,13 @@ namespace montauk {
return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf); return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf);
} }
// ACPI battery status. Returns 0 on success, or -1 on a machine with no
// battery device at all -- which is the signal not to draw an indicator.
// A laptop with the pack removed still returns 0, with present == 0.
inline int battery_info(montauk::abi::BatteryInfo* out) {
return (int)syscall1(montauk::abi::SYS_BATTERYINFO, (uint64_t)out);
}
// Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or // Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or
// timeoutMs elapses, then fills buf with the networks seen; it returns the // timeoutMs elapses, then fills buf with the networks seen; it returns the
// number of entries written, or -1 when no adapter is ready. // number of entries written, or -1 when no adapter is ready.
+321
View File
@@ -0,0 +1,321 @@
/*
* lang_c.hpp
* C and C++ highlighting
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
* Copyright (c) 2026 Daniel Hammer
*/
// Included by <syntax/syntax.hpp>; not standalone.
#pragma once
// ============================================================================
// Keyword / type classification
// ============================================================================
inline SynToken syn_classify_c_word(const char* buf, int len) {
static const char* keywords[] = {
"auto", "break", "case", "const", "continue", "default", "do",
"else", "enum", "extern", "for", "goto", "if", "inline",
"register", "restrict", "return", "sizeof", "static", "struct",
"switch", "typedef", "union", "volatile", "while",
"NULL", "true", "false", "nullptr",
};
static const char* types[] = {
"void", "char", "short", "int", "long", "float", "double",
"signed", "unsigned", "bool", "_Bool",
"int8_t", "int16_t", "int32_t", "int64_t",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
"FILE",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
// C++ keywords (on top of the C set, which is checked first)
static const char* keywords[] = {
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
"catch", "class", "compl", "concept", "consteval", "constexpr",
"constinit", "const_cast", "co_await", "co_return", "co_yield",
"decltype", "delete", "dynamic_cast", "explicit", "export",
"final", "friend", "mutable", "namespace", "new", "noexcept",
"not", "not_eq", "operator", "or", "or_eq", "override",
"private", "protected", "public", "reinterpret_cast", "requires",
"static_assert", "static_cast", "template", "this", "thread_local",
"throw", "try", "typeid", "typename", "using", "virtual",
"xor", "xor_eq",
};
// C++ library / builtin types
static const char* types[] = {
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
"std", "string", "string_view", "vector", "array", "span",
"map", "set", "unordered_map", "unordered_set", "deque", "list",
"pair", "tuple", "optional", "variant", "function",
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
};
SynToken c = syn_classify_c_word(buf, len);
if (c != SYN_NORMAL) return c;
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
// ============================================================================
// C highlighting
// ============================================================================
// C / C++ raw string helpers
// ============================================================================
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
syn_streq(buf, len, "u8R");
}
inline bool syn_is_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
}
// Colors a raw-string body from `i` up to and including the closing )delim".
// If the terminator is not on this line, leaves state.in_raw_string set so the
// next line continues the literal.
inline void syn_consume_raw_body(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
while (i < len) {
if (line[i] == ')') {
int j = i + 1;
int k = 0;
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
j++;
k++;
}
if (k == state.raw_delim_len && j < len && line[j] == '"') {
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
i = j + 1;
state.in_raw_string = false;
state.raw_delim_len = 0;
return;
}
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
}
// Enters a raw string; `line[i]` must be the opening quote.
inline void syn_consume_raw_string(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.raw_delim_len = 0;
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
state.raw_delim[state.raw_delim_len++] = line[i];
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
if (i < len && line[i] == '(') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.in_raw_string = true;
syn_consume_raw_body(line, len, i, out, out_len, state);
return;
}
// Malformed (or an over-long delimiter): treat the rest of the line as string
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
i = len;
}
// ============================================================================
// C / C++ highlighting
// ============================================================================
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
SynState& state, SynLanguage lang = SYN_LANG_C) {
const bool cpp = (lang == SYN_LANG_CPP);
int i = 0;
while (i < len) {
// ---- Raw string continuation ----
if (state.in_raw_string) {
syn_consume_raw_body(line, len, i, out, out_len, state);
continue;
}
// ---- Block comment continuation ----
if (state.in_block_comment) {
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
char c = line[i];
// ---- Line comment ----
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// ---- Block comment start ----
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
continue;
}
// ---- Preprocessor directive ----
if (c == '#') {
// Check that only whitespace precedes the #
bool is_pp = true;
for (int j = 0; j < i; j++) {
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
}
if (is_pp) {
while (i < len) {
// Handle line-comment inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// Handle block comment start inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
// Continue consuming as comment
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
i++;
}
continue;
}
}
// ---- String literal ----
if (c == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
if (line[i] == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
continue;
}
// ---- Character literal ----
if (c == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_CHAR);
syn_set_token(out, out_len, i + 1, SYN_CHAR);
i += 2;
continue;
}
if (line[i] == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
}
continue;
}
// ---- Numbers ----
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, true, cpp);
continue;
}
// ---- Identifiers / keywords / types / literal prefixes ----
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
if (cpp && i < len && line[i] == '"' &&
syn_is_raw_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
syn_consume_raw_string(line, len, i, out, out_len, state);
continue;
}
if (cpp && i < len && line[i] == '"' &&
syn_is_string_prefix(line + start, i - start)) {
// The quote itself is handled on the next iteration
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
continue;
}
if (cpp && i < len && line[i] == '\'' &&
syn_is_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
continue;
}
SynToken tok = syn_classify_word(lang, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
// ---- Operators ----
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '=' || c == '!' || c == '<' || c == '>' || c == '&' ||
c == '|' || c == '^' || c == '~' || c == '?' || c == ':') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
// ---- Everything else (whitespace, braces, parens, etc.) ----
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
+475
View File
@@ -0,0 +1,475 @@
/*
* lang_html.hpp
* HTML highlighting, with embedded CSS and JS
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
* Copyright (c) 2026 Daniel Hammer
*/
// Included by <syntax/syntax.hpp>; not standalone.
#pragma once
// ============================================================================
// JavaScript keyword / builtin classification
// ============================================================================
inline SynToken syn_classify_js_word(const char* buf, int len) {
static const char* keywords[] = {
"async", "await", "break", "case", "catch", "class", "const",
"continue", "debugger", "default", "delete", "do", "else",
"export", "extends", "finally", "for", "function", "if",
"import", "in", "instanceof", "let", "new", "of", "return",
"static", "super", "switch", "this", "throw", "try", "typeof",
"var", "void", "while", "with", "yield",
"true", "false", "null", "undefined",
};
static const char* builtins[] = {
"Array", "Boolean", "Date", "Error", "Infinity", "JSON", "Map",
"Math", "NaN", "Number", "Object", "Promise", "Proxy", "RegExp",
"Set", "String", "Symbol", "WeakMap", "WeakSet",
"console", "document", "globalThis", "localStorage", "navigator",
"window", "fetch", "setTimeout", "setInterval",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) {
if (syn_streq(buf, len, builtins[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
// ============================================================================
// JavaScript (also used for <script> bodies inside HTML)
// ============================================================================
//
// Highlights line[i .. limit) - `limit` is the end of the JS region on this
// line, which for an embedded script is where the closing </script> begins.
inline void syn_highlight_js_range(const char* line, int limit, int& i,
SynToken* out, int out_len, SynState& state) {
while (i < limit) {
// ---- Block comment continuation ----
if (state.in_block_comment) {
while (i < limit) {
if (i + 1 < limit && line[i] == '*' && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
char c = line[i];
if (c == '/' && i + 1 < limit && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, limit, SYN_COMMENT);
i = limit;
break;
}
if (c == '/' && i + 1 < limit && line[i + 1] == '*') {
state.in_block_comment = true;
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
i += 2;
continue;
}
// ---- String / template literal (single line only) ----
if (c == '"' || c == '\'' || c == '`') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < limit) {
if (line[i] == '\\' && i + 1 < limit) {
syn_fill_tokens(out, out_len, i, i + 2, SYN_STRING);
i += 2;
continue;
}
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
break;
}
i++;
}
continue;
}
if (syn_is_digit(c) || (c == '.' && i + 1 < limit && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, limit, i, out, out_len, false);
continue;
}
if (syn_is_alpha(c) || c == '$') {
int start = i;
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '$')) i++;
syn_fill_tokens(out, out_len, start, i,
syn_classify_js_word(line + start, i - start));
continue;
}
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '=' || c == '!' || c == '<' || c == '>' || c == '&' ||
c == '|' || c == '^' || c == '~' || c == '?' || c == ':') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
// ============================================================================
// CSS (also used for <style> bodies inside HTML)
// ============================================================================
inline void syn_highlight_css_range(const char* line, int limit, int& i,
SynToken* out, int out_len, SynState& state) {
while (i < limit) {
// ---- Block comment continuation ----
if (state.in_block_comment) {
while (i < limit) {
if (i + 1 < limit && line[i] == '*' && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
char c = line[i];
if (c == '/' && i + 1 < limit && line[i + 1] == '*') {
state.in_block_comment = true;
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
i += 2;
continue;
}
if (c == '"' || c == '\'') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < limit) {
if (line[i] == '\\' && i + 1 < limit) {
syn_fill_tokens(out, out_len, i, i + 2, SYN_STRING);
i += 2;
continue;
}
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
break;
}
i++;
}
continue;
}
// ---- At-rule (@media, @import, ...) ----
if (c == '@') {
int start = i;
i++;
while (i < limit && syn_is_alnum(line[i])) i++;
syn_fill_tokens(out, out_len, start, i, SYN_PREPROCESSOR);
continue;
}
// ---- Hex color inside a declaration, id selector outside ----
if (c == '#') {
int start = i;
i++;
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
syn_fill_tokens(out, out_len, start, i,
state.css_in_value ? SYN_NUMBER : SYN_TYPE);
continue;
}
// ---- Class selector ----
if (c == '.' && !state.css_in_value && i + 1 < limit && syn_is_alpha(line[i + 1])) {
int start = i;
i++;
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
syn_fill_tokens(out, out_len, start, i, SYN_TYPE);
continue;
}
if (syn_is_digit(c) || (c == '.' && i + 1 < limit && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, limit, i, out, out_len, false);
// Unit suffix (px, em, %, ...)
int unit = i;
while (i < limit && (syn_is_alpha(line[i]) || line[i] == '%')) i++;
syn_fill_tokens(out, out_len, unit, i, SYN_NUMBER);
continue;
}
if (syn_is_alpha(c) || c == '-') {
int start = i;
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
SynToken tok;
if (state.css_in_value) {
tok = SYN_NORMAL;
} else if (state.css_depth > 0) {
tok = SYN_KEYWORD; // property name
} else {
tok = SYN_TYPE; // element selector
}
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
if (c == '{') {
if (state.css_depth < 255) state.css_depth++;
state.css_in_value = 0;
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
if (c == '}') {
if (state.css_depth > 0) state.css_depth--;
state.css_in_value = 0;
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
if (c == ':') {
// Inside a block this starts a value; outside it is a pseudo-class
if (state.css_depth > 0) state.css_in_value = 1;
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
if (c == ';') {
state.css_in_value = 0;
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
if (c == '>' || c == '~' || c == '+' || c == '*' || c == ',' || c == '=') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
// ============================================================================
// HTML
// ============================================================================
//
// Tag names are keywords, attribute names types, attribute values strings,
// entities numbers, and <!DOCTYPE>/processing instructions preprocessor.
// <script> and <style> bodies are handed to the JS and CSS highlighters.
inline void syn_highlight_line_html(const char* line, int len, SynToken* out, int out_len,
SynState& state) {
int i = 0;
while (i < len) {
// ---- Comment continuation ----
if (state.html_ctx == SYN_HTML_COMMENT) {
while (i < len) {
if (syn_match_ci(line, len, i, "-->")) {
syn_fill_tokens(out, out_len, i, i + 3, SYN_COMMENT);
i += 3;
state.html_ctx = SYN_HTML_TEXT;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
// ---- Raw text body: <script> / <style> ----
if (state.html_ctx == SYN_HTML_SCRIPT || state.html_ctx == SYN_HTML_STYLE) {
const bool script = (state.html_ctx == SYN_HTML_SCRIPT);
const char* endTag = script ? "</script" : "</style";
int limit = i;
while (limit < len && !syn_match_ci(line, len, limit, endTag)) limit++;
if (script)
syn_highlight_js_range(line, limit, i, out, out_len, state);
else
syn_highlight_css_range(line, limit, i, out, out_len, state);
i = limit;
if (i < len) {
// Closing tag reached: back to ordinary tag parsing
state.in_block_comment = false;
state.css_in_value = 0;
state.css_depth = 0;
state.html_ctx = SYN_HTML_TAG;
state.html_pending = SYN_HTML_TEXT;
syn_fill_tokens(out, out_len, i, i + 2, SYN_OPERATOR);
i += 2;
int start = i;
while (i < len && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
syn_fill_tokens(out, out_len, start, i, SYN_KEYWORD);
}
continue;
}
// ---- Inside a tag: attributes ----
if (state.html_ctx == SYN_HTML_TAG) {
// Attribute value continued from a previous line
if (state.html_quote) {
while (i < len) {
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == state.html_quote) {
state.html_quote = 0;
i++;
break;
}
i++;
}
continue;
}
char c = line[i];
if (c == '>') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
state.html_ctx = state.html_pending ? (SynHtmlCtx)state.html_pending
: SYN_HTML_TEXT;
state.html_pending = SYN_HTML_TEXT;
continue;
}
if (c == '/' && i + 1 < len && line[i + 1] == '>') {
syn_fill_tokens(out, out_len, i, i + 2, SYN_OPERATOR);
i += 2;
state.html_ctx = SYN_HTML_TEXT;
state.html_pending = SYN_HTML_TEXT;
continue;
}
if (c == '=') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
if (c == '"' || c == '\'') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
bool closed = false;
while (i < len) {
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
closed = true;
break;
}
i++;
}
if (!closed) state.html_quote = quote;
continue;
}
if (syn_is_alpha(c) || c == '-' || c == ':' || c == '@') {
int start = i;
i++;
while (i < len && (syn_is_alnum(line[i]) || line[i] == '-' ||
line[i] == ':' || line[i] == '.'))
i++;
syn_fill_tokens(out, out_len, start, i, SYN_TYPE);
continue;
}
// Unquoted attribute value or stray punctuation
if (c != ' ' && c != '\t') {
int start = i;
while (i < len && line[i] != ' ' && line[i] != '\t' &&
line[i] != '>' && line[i] != '"' && line[i] != '\'')
i++;
if (i == start) i++;
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
continue;
}
// ---- Text content ----
char c = line[i];
if (c == '<') {
if (syn_match_ci(line, len, i, "<!--")) {
state.html_ctx = SYN_HTML_COMMENT;
syn_fill_tokens(out, out_len, i, i + 4, SYN_COMMENT);
i += 4;
continue;
}
if (i + 1 < len && (line[i + 1] == '!' || line[i + 1] == '?')) {
// <!DOCTYPE ...> or a processing instruction
int start = i;
while (i < len && line[i] != '>') i++;
if (i < len) i++;
syn_fill_tokens(out, out_len, start, i, SYN_PREPROCESSOR);
continue;
}
int op_end = i + 1;
bool closing = false;
if (op_end < len && line[op_end] == '/') {
closing = true;
op_end++;
}
syn_fill_tokens(out, out_len, i, op_end, SYN_OPERATOR);
int start = op_end;
int j = op_end;
while (j < len && (syn_is_alnum(line[j]) || line[j] == '-' || line[j] == ':')) j++;
if (j == start) {
// A bare '<' in text, not a tag
i = op_end;
continue;
}
syn_fill_tokens(out, out_len, start, j, SYN_KEYWORD);
state.html_ctx = SYN_HTML_TAG;
state.html_pending = SYN_HTML_TEXT;
if (!closing) {
if (syn_streq(line + start, j - start, "script") ||
syn_streq(line + start, j - start, "SCRIPT"))
state.html_pending = SYN_HTML_SCRIPT;
else if (syn_streq(line + start, j - start, "style") ||
syn_streq(line + start, j - start, "STYLE"))
state.html_pending = SYN_HTML_STYLE;
}
i = j;
continue;
}
// ---- Entity reference ----
if (c == '&') {
int j = i + 1;
if (j < len && line[j] == '#') j++;
int nameStart = j;
while (j < len && syn_is_alnum(line[j])) j++;
if (j > nameStart && j < len && line[j] == ';') {
syn_fill_tokens(out, out_len, i, j + 1, SYN_NUMBER);
i = j + 1;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
+160
View File
@@ -0,0 +1,160 @@
/*
* lang_lua.hpp
* Lua highlighting
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
* Copyright (c) 2026 Daniel Hammer
*/
// Included by <syntax/syntax.hpp>; not standalone.
#pragma once
// ============================================================================
// Keyword / builtin classification
// ============================================================================
inline SynToken syn_classify_lua_word(const char* buf, int len) {
static const char* keywords[] = {
"and", "break", "do", "else", "elseif", "end", "false",
"for", "function", "goto", "if", "in", "local", "nil",
"not", "or", "repeat", "return", "then", "true",
"until", "while",
};
static const char* builtins[] = {
"_ENV", "_G", "_VERSION",
"assert", "collectgarbage", "coroutine", "debug", "dofile",
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
"math", "next", "os", "package", "pairs", "pcall",
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
"select", "setmetatable", "string", "table", "tonumber",
"tostring", "type", "utf8", "warn", "xpcall",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) {
if (syn_streq(buf, len, builtins[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
// ============================================================================
// Lua long bracket matching
// ============================================================================
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
int& eqs, int& span) {
if (i >= len || line[i] != '[') return false;
int j = i + 1;
while (j < len && line[j] == '=') j++;
if (j < len && line[j] == '[') {
eqs = j - i - 1;
span = j - i + 1;
return true;
}
return false;
}
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
int eqs, int& span) {
if (i >= len || line[i] != ']') return false;
int j = i + 1;
for (int k = 0; k < eqs; k++) {
if (j >= len || line[j] != '=') return false;
j++;
}
if (j < len && line[j] == ']') {
span = j - i + 1;
return true;
}
return false;
}
// ============================================================================
// Per-line highlighter
// ============================================================================
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
SynState& state) {
int i = 0;
while (i < len) {
if (state.long_token != SYN_NORMAL) {
int span = 0;
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
i += span;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
continue;
}
syn_set_token(out, out_len, i, state.long_token);
i++;
continue;
}
char c = line[i];
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
int eqs = 0;
int span = 0;
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
i += 2 + span;
state.long_token = SYN_COMMENT;
state.long_bracket_eqs = eqs;
continue;
}
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
if (c == '"' || c == '\'') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
break;
}
i++;
}
continue;
}
int eqs = 0;
int span = 0;
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
i += span;
state.long_token = SYN_STRING;
state.long_bracket_eqs = eqs;
continue;
}
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, 0);
continue;
}
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '^' || c == '#' || c == '=' || c == '<' || c == '>' ||
c == '~' || c == ':' || c == '.' || c == ',' || c == ';' ||
c == '(' || c == ')' || c == '{' || c == '}' ||
c == '[' || c == ']') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
+299
View File
@@ -0,0 +1,299 @@
/*
* syntax.hpp
* Syntax highlighting core: token types, state, shared scanners
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
* Copyright (c) 2026 Daniel Hammer
*/
// The highlighter turns one line of text into a SynToken per character while
// carrying multi-line state (block comments, HTML tag context, ...) forward
// in a SynState. Frontends map SynToken to their own colors: the GUI editor
// to a Color, edit.elf to an ANSI escape.
//
// Callers include THIS header only; the per-language lang_*.hpp files below
// are not standalone (they rely on the declarations above their include).
#pragma once
#include <stdint.h>
// ============================================================================
// Token types
// ============================================================================
enum SynLanguage : uint8_t {
SYN_LANG_NONE,
SYN_LANG_C,
SYN_LANG_CPP,
SYN_LANG_LUA,
SYN_LANG_HTML,
SYN_LANG_CSS,
SYN_LANG_JS,
};
enum SynToken : uint8_t {
SYN_NORMAL,
SYN_KEYWORD,
SYN_TYPE,
SYN_PREPROCESSOR,
SYN_STRING,
SYN_CHAR,
SYN_COMMENT,
SYN_NUMBER,
SYN_OPERATOR,
};
#define SYN_RAW_DELIM_MAX 16
// Where an HTML line starts: plain text, inside a tag, inside a comment, or
// inside a <script>/<style> raw-text body.
enum SynHtmlCtx : uint8_t {
SYN_HTML_TEXT,
SYN_HTML_TAG,
SYN_HTML_COMMENT,
SYN_HTML_SCRIPT,
SYN_HTML_STYLE,
};
struct SynState {
int in_block_comment; // 0 or 1 (bool stored as int for freestanding)
SynToken long_token;
int long_bracket_eqs;
bool in_raw_string;
char raw_delim[SYN_RAW_DELIM_MAX];
int raw_delim_len;
uint8_t html_ctx; // SynHtmlCtx
uint8_t html_pending; // context to enter once the open tag closes
char html_quote; // attribute value quote left open on this line
uint8_t css_depth; // brace nesting, to tell selectors from properties
uint8_t css_in_value; // between ':' and ';' inside a declaration
};
// ============================================================================
// Helpers
// ============================================================================
inline bool syn_is_alpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
inline bool syn_is_alnum(char c) {
return syn_is_alpha(c) || (c >= '0' && c <= '9');
}
inline bool syn_is_digit(char c) {
return c >= '0' && c <= '9';
}
inline bool syn_is_hex(char c) {
return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
inline void syn_set_token(SynToken* out, int out_len, int idx, SynToken tok) {
if (out && idx >= 0 && idx < out_len)
out[idx] = tok;
}
inline void syn_fill_tokens(SynToken* out, int out_len, int start, int end, SynToken tok) {
if (!out) return;
if (start < 0) start = 0;
if (end > out_len) end = out_len;
for (int i = start; i < end; i++)
out[i] = tok;
}
inline bool syn_streq(const char* buf, int len, const char* kw) {
int i = 0;
while (i < len && kw[i]) {
if (buf[i] != kw[i]) return false;
i++;
}
return i == len && kw[i] == '\0';
}
inline char syn_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
// Case-insensitive literal match at line[i]
inline bool syn_match_ci(const char* line, int len, int i, const char* s) {
int k = 0;
while (s[k]) {
if (i + k >= len) return false;
if (syn_lower(line[i + k]) != syn_lower(s[k])) return false;
k++;
}
return true;
}
inline SynState syn_make_state() {
SynState state = {};
state.in_block_comment = 0;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
state.in_raw_string = false;
state.raw_delim_len = 0;
state.html_ctx = SYN_HTML_TEXT;
state.html_pending = SYN_HTML_TEXT;
state.html_quote = 0;
state.css_depth = 0;
state.css_in_value = 0;
return state;
}
// ============================================================================
// Number consumption
// ============================================================================
inline void syn_scan_digits(const char* line, int len, int& i, bool sep, bool hex) {
while (i < len) {
if (hex ? syn_is_hex(line[i]) : syn_is_digit(line[i])) { i++; continue; }
// C++14 digit separator: a quote wedged between two digits
if (sep && line[i] == '\'' && i + 1 < len &&
(hex ? syn_is_hex(line[i + 1]) : syn_is_digit(line[i + 1]))) {
i += 2;
continue;
}
break;
}
}
inline void syn_consume_number(const char* line, int len, int& i,
SynToken* out, int out_len, bool c_style_suffixes,
bool digit_sep = false) {
int start = i;
if (line[i] == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) {
i += 2;
syn_scan_digits(line, len, i, digit_sep, true);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, true);
}
if (i < len && (line[i] == 'p' || line[i] == 'P')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
} else {
if (line[i] == '.') i++;
syn_scan_digits(line, len, i, digit_sep, false);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, false);
}
if (i < len && (line[i] == 'e' || line[i] == 'E')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
}
if (c_style_suffixes) {
while (i < len && (line[i] == 'u' || line[i] == 'U' ||
line[i] == 'l' || line[i] == 'L' ||
line[i] == 'f' || line[i] == 'F' ||
line[i] == 'z' || line[i] == 'Z'))
i++;
}
syn_fill_tokens(out, out_len, start, i, SYN_NUMBER);
}
// Defined below, once every language's classifier is in scope
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len);
#include <syntax/lang_c.hpp>
#include <syntax/lang_lua.hpp>
#include <syntax/lang_html.hpp>
// ============================================================================
// Word classification
// ============================================================================
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len) {
switch (lang) {
case SYN_LANG_C: return syn_classify_c_word(buf, len);
case SYN_LANG_CPP: return syn_classify_cpp_word(buf, len);
case SYN_LANG_LUA: return syn_classify_lua_word(buf, len);
case SYN_LANG_JS: return syn_classify_js_word(buf, len);
default: return SYN_NORMAL;
}
}
inline void syn_highlight_line(const char* line, int len, SynToken* out, int out_len,
SynLanguage lang, SynState& state) {
if (!line || len <= 0) return;
switch (lang) {
case SYN_LANG_C:
case SYN_LANG_CPP:
syn_highlight_line_c(line, len, out, out_len, state, lang);
break;
case SYN_LANG_LUA:
syn_highlight_line_lua(line, len, out, out_len, state);
break;
case SYN_LANG_HTML:
syn_highlight_line_html(line, len, out, out_len, state);
break;
case SYN_LANG_CSS: {
int i = 0;
syn_highlight_css_range(line, len, i, out, out_len, state);
break;
}
case SYN_LANG_JS: {
int i = 0;
syn_highlight_js_range(line, len, i, out, out_len, state);
break;
}
default:
syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL);
break;
}
}
// ============================================================================
// Language detection
// ============================================================================
inline bool syn_path_ends_with(const char* path, const char* suffix) {
if (!path || !suffix) return false;
int path_len = 0;
while (path[path_len]) path_len++;
int suffix_len = 0;
while (suffix[suffix_len]) suffix_len++;
if (path_len < suffix_len) return false;
for (int i = 0; i < suffix_len; i++) {
if (path[path_len - suffix_len + i] != suffix[i])
return false;
}
return true;
}
inline SynLanguage syn_detect_language(const char* filepath) {
if (!filepath || filepath[0] == '\0') return SYN_LANG_NONE;
if (syn_path_ends_with(filepath, ".cpp") || syn_path_ends_with(filepath, ".cc") ||
syn_path_ends_with(filepath, ".cxx") || syn_path_ends_with(filepath, ".c++") ||
syn_path_ends_with(filepath, ".hpp") || syn_path_ends_with(filepath, ".hh") ||
syn_path_ends_with(filepath, ".hxx") || syn_path_ends_with(filepath, ".h++") ||
syn_path_ends_with(filepath, ".ipp") || syn_path_ends_with(filepath, ".tpp") ||
syn_path_ends_with(filepath, ".inl"))
return SYN_LANG_CPP;
if (syn_path_ends_with(filepath, ".c") || syn_path_ends_with(filepath, ".h"))
return SYN_LANG_C;
if (syn_path_ends_with(filepath, ".lua"))
return SYN_LANG_LUA;
if (syn_path_ends_with(filepath, ".html") || syn_path_ends_with(filepath, ".htm") ||
syn_path_ends_with(filepath, ".xhtml"))
return SYN_LANG_HTML;
if (syn_path_ends_with(filepath, ".css"))
return SYN_LANG_CSS;
if (syn_path_ends_with(filepath, ".js") || syn_path_ends_with(filepath, ".mjs"))
return SYN_LANG_JS;
return SYN_LANG_NONE;
}
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ---- # ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print)) APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS) SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+238
View File
@@ -0,0 +1,238 @@
/*
* battery.cpp
* Panel battery indicator: the icon that only appears on a machine with a
* battery, and the popout that puts a number on it.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <gui/mtk.hpp>
using namespace gui;
// ============================================================================
// Geometry
// ============================================================================
static constexpr int BATT_POPUP_W = 210;
static constexpr int BATT_POLL_MS = 5000; // the kernel caches for 2s anyway
static int battery_popup_x(const DesktopState* ds) {
int x = ds->battery_icon_rect.x + ds->battery_icon_rect.w - BATT_POPUP_W;
return x < 4 ? 4 : x;
}
// Rows below the headline: state, then whatever of capacity/time we can say.
static int battery_detail_rows(const DesktopState* ds) {
const montauk::abi::BatteryInfo& b = ds->battery_info;
if (!b.present) return 0;
int rows = 2; // "Charge", then "State"
if (b.minutesRemaining >= 0) rows++; // "Time left"
if (b.fullCapacity > 0) rows++; // "Capacity"
return rows;
}
static int battery_popup_h(const DesktopState* ds) {
int fh = system_font_height();
int header = fh + 10; // title row
int rows = battery_detail_rows(ds) * (fh + 6);
return 10 + header + rows + 12;
}
static Rect battery_popup_rect(const DesktopState* ds) {
return {battery_popup_x(ds), PANEL_HEIGHT + 2, BATT_POPUP_W, battery_popup_h(ds)};
}
// ============================================================================
// Icon selection
// ============================================================================
// The icon pack ships one glyph per 10% step. Round to the nearest step so the
// drawn level matches the number in the popout rather than always sitting a
// bucket low.
static int battery_bucket(int percent) {
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
int bucket = ((percent + 5) / 10) * 10;
return bucket > 100 ? 100 : bucket;
}
static void battery_icon_path(const DesktopState* ds, char* out, int cap) {
const montauk::abi::BatteryInfo& b = ds->battery_info;
if (!b.present) {
montauk::strncpy(out, "0:/icons/battery-missing-symbolic.svg", cap);
return;
}
int bucket = battery_bucket(b.percent);
// A full battery on the charger gets the "charged" glyph; the pack has no
// 100% charging variant, and "charged" is what that state means anyway.
if (b.charging && bucket >= 100) {
montauk::strncpy(out, "0:/icons/battery-level-100-charged-symbolic.svg", cap);
return;
}
snprintf(out, cap, "0:/icons/battery-level-%d%s-symbolic.svg",
bucket, b.charging ? "-charging" : "");
}
// Reload the glyph only when the one we want changes: at a 5 second poll a
// laptop crosses a bucket boundary every few minutes at most, and each load
// rasterises an SVG.
static bool battery_refresh_icon(DesktopState* ds) {
char path[64];
battery_icon_path(ds, path, sizeof(path));
if (ds->icon_battery.pixels && montauk::streq(path, ds->battery_icon_name))
return false;
SvgIcon loaded = svg_load(path, 16, 16, colors::PANEL_TEXT);
if (!loaded.pixels) return false;
svg_free(ds->icon_battery);
ds->icon_battery = loaded;
montauk::strncpy(ds->battery_icon_name, path, sizeof(ds->battery_icon_name));
return true;
}
// ============================================================================
// Startup and polling
// ============================================================================
void desktop_battery_init(DesktopState* ds) {
ds->icon_battery = {nullptr, 0, 0};
ds->battery_icon_name[0] = '\0';
ds->battery_popup_open = false;
ds->battery_icon_rect = {0, 0, 0, 0};
ds->battery_last_poll = 0;
montauk::memset(&ds->battery_info, 0, sizeof(ds->battery_info));
// A machine with no ACPI battery device fails the call outright, and the
// indicator never appears. This does not change while we are running, so
// unlike Wi-Fi there is nothing to keep looking for.
ds->battery_present = montauk::battery_info(&ds->battery_info) == 0;
if (ds->battery_present) battery_refresh_icon(ds);
}
bool desktop_battery_poll(DesktopState* ds, uint64_t now) {
if (!ds->battery_present) return false;
if (ds->battery_last_poll != 0 && now - ds->battery_last_poll < BATT_POLL_MS)
return false;
ds->battery_last_poll = now;
montauk::abi::BatteryInfo fresh;
if (montauk::battery_info(&fresh) != 0) return false;
bool changed = fresh.percent != ds->battery_info.percent
|| fresh.present != ds->battery_info.present
|| fresh.charging != ds->battery_info.charging
|| fresh.acOnline != ds->battery_info.acOnline
|| fresh.minutesRemaining != ds->battery_info.minutesRemaining;
ds->battery_info = fresh;
if (battery_refresh_icon(ds)) changed = true;
// While the popout is up its numbers are on screen, so any change at all
// has to be repainted.
return changed || ds->battery_popup_open;
}
// ============================================================================
// Popout
// ============================================================================
void desktop_draw_battery_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
const montauk::abi::BatteryInfo& b = ds->battery_info;
Rect popup = battery_popup_rect(ds);
int fh = system_font_height();
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
int lx = popup.x + 14;
int vx = popup.x + 92;
int ty = popup.y + 10;
draw_text(fb, lx, ty, "Battery", colors::TEXT_COLOR);
// A coloured dot mirrors the Wi-Fi popup: green on the charger, amber when
// the charge is getting short, red once the firmware calls it critical.
const char* status_str;
Color dot_color;
if (!b.present) {
status_str = "Not present";
dot_color = Color::from_rgb(0x88, 0x88, 0x88);
} else if (b.charging) {
status_str = "Charging";
dot_color = Color::from_rgb(0x4C, 0xAF, 0x50);
} else if (b.critical || b.percent <= 10) {
status_str = "Low";
dot_color = Color::from_rgb(0xCC, 0x33, 0x33);
} else if (b.acOnline) {
status_str = "Plugged in";
dot_color = Color::from_rgb(0x4C, 0xAF, 0x50);
} else {
status_str = "On battery";
dot_color = (b.percent <= 25) ? Color::from_rgb(0xD0, 0x9A, 0x2E)
: Color::from_rgb(0x66, 0x66, 0x66);
}
int sw = text_width(status_str);
int status_x = popup.x + popup.w - 14 - sw;
fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color);
draw_text(fb, status_x, ty, status_str, dim);
ty += fh + 10;
if (!b.present) return;
char value[40];
snprintf(value, sizeof(value), "%d%%", (int)b.percent);
draw_text(fb, lx, ty, "Charge", dim);
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
ty += fh + 6;
snprintf(value, sizeof(value), "%s",
b.charging ? "Charging" : (b.acOnline ? "Not charging" : "Discharging"));
draw_text(fb, lx, ty, "State", dim);
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
ty += fh + 6;
if (b.minutesRemaining >= 0) {
int mins = b.minutesRemaining;
if (mins >= 60) snprintf(value, sizeof(value), "%dh %02dm", mins / 60, mins % 60);
else snprintf(value, sizeof(value), "%dm", mins);
draw_text(fb, lx, ty, "Time left", dim);
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
ty += fh + 6;
}
if (b.fullCapacity > 0) {
const char* unit = b.powerUnitMilliAmps ? "mAh" : "mWh";
snprintf(value, sizeof(value), "%u/%u %s",
(unsigned)b.remainingCapacity, (unsigned)b.fullCapacity, unit);
draw_text(fb, lx, ty, "Capacity", dim);
draw_text(fb, vx, ty, value, colors::TEXT_COLOR);
}
}
// A click anywhere outside the popout closes it; the popout itself has nothing
// to click, so it only has to swallow the event.
bool desktop_battery_handle_mouse(DesktopState* ds, int mx, int my) {
if (!ds->battery_popup_open) return false;
if (battery_popup_rect(ds).contains(mx, my)) return true;
if (!ds->battery_icon_rect.contains(mx, my))
ds->battery_popup_open = false;
return false;
}
+5
View File
@@ -268,6 +268,11 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_wifi_popup(ds); desktop_draw_wifi_popup(ds);
} }
// Draw battery popup if open
if (ds->battery_popup_open) {
desktop_draw_battery_popup(ds);
}
// Draw volume popup if open // Draw volume popup if open
if (ds->vol_popup_open) { if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds); desktop_draw_vol_popup(ds);
@@ -195,6 +195,12 @@ void desktop_draw_wifi_popup(gui::DesktopState* ds);
bool desktop_wifi_handle_mouse(gui::DesktopState* ds, int mx, int my, bool desktop_wifi_handle_mouse(gui::DesktopState* ds, int mx, int my,
bool left_pressed, int scroll); bool left_pressed, int scroll);
// battery.cpp
void desktop_battery_init(gui::DesktopState* ds);
bool desktop_battery_poll(gui::DesktopState* ds, uint64_t now);
void desktop_draw_battery_popup(gui::DesktopState* ds);
bool desktop_battery_handle_mouse(gui::DesktopState* ds, int mx, int my);
// compose.cpp // compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds); void desktop_draw_lock_screen(gui::DesktopState* ds);
void desktop_mark_background_dirty(gui::DesktopState* ds); void desktop_mark_background_dirty(gui::DesktopState* ds);
+27
View File
@@ -189,6 +189,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
return; return;
} }
@@ -529,6 +530,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return; return;
} }
// Battery popout: it has no controls, so it only swallows clicks that land
// inside it and closes on anything else.
if (ds->battery_popup_open && left_pressed) {
if (desktop_battery_handle_mouse(ds, mx, my))
return;
}
// Handle net popup clicks // Handle net popup clicks
if (ds->net_popup_open && left_pressed) { if (ds->net_popup_open && left_pressed) {
int popup_w = 220; int popup_w = 220;
@@ -545,6 +553,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
} else if (!ds->net_icon_rect.contains(mx, my)) { } else if (!ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
} }
} }
@@ -555,6 +564,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = !ds->app_menu_open; ds->app_menu_open = !ds->app_menu_open;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
@@ -571,6 +581,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
@@ -583,6 +594,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
} }
@@ -593,6 +605,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
} }
@@ -603,9 +616,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
} }
// Battery icon
if (ds->battery_icon_rect.w > 0 && ds->battery_icon_rect.contains(mx, my)) {
ds->battery_popup_open = !ds->battery_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
return;
}
// Window indicator buttons // Window indicator buttons
int indicator_x = 40; int indicator_x = 40;
for (int i = 0; i < ds->window_count; i++) { for (int i = 0; i < ds->window_count; i++) {
@@ -833,6 +859,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
} }
} }
+2
View File
@@ -392,6 +392,7 @@ void gui::desktop_init(DesktopState* ds) {
desktop_refresh_netifs(ds); desktop_refresh_netifs(ds);
desktop_wifi_init(ds); desktop_wifi_init(ds);
desktop_battery_init(ds);
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->vol_icon_rect = {0, 0, 0, 0}; ds->vol_icon_rect = {0, 0, 0, 0};
@@ -518,6 +519,7 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
} }
if (desktop_wifi_poll(ds, now)) changed = true; if (desktop_wifi_poll(ds, now)) changed = true;
if (desktop_battery_poll(ds, now)) changed = true;
// Event-driven sync: the kernel mixer bumps a serial on every state // Event-driven sync: the kernel mixer bumps a serial on every state
// change. Reading it is one cheap syscall; refresh only when the serial // change. Reading it is one cheap syscall; refresh only when the serial
+27
View File
@@ -210,6 +210,33 @@ void gui::desktop_draw_panel(DesktopState* ds) {
} else { } else {
ds->wifi_icon_rect = {0, 0, 0, 0}; ds->wifi_icon_rect = {0, 0, 0, 0};
} }
// Battery, leftmost of the status icons. Only a machine that reported a
// battery device at startup gets one; a desktop shows nothing here.
if (ds->battery_present && ds->icon_battery.pixels) {
ds->battery_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
// The glyph already encodes level and charging state, and the charging
// variants carry their own green, so it is blitted as loaded. A
// critical battery is the one case worth shouting about.
if (ds->battery_info.present && ds->battery_info.critical
&& !ds->battery_info.charging) {
uint32_t* src = ds->icon_battery.pixels;
uint32_t tinted[256];
for (int p = 0; p < 16 * 16; p++) {
uint8_t a = (src[p] >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x00CC3333;
}
fb.blit_alpha(ds->battery_icon_rect.x, icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(ds->battery_icon_rect.x, icon_y,
ds->icon_battery.width, ds->icon_battery.height,
ds->icon_battery.pixels);
}
} else {
ds->battery_icon_rect = {0, 0, 0, 0};
}
} }
// ============================================================================ // ============================================================================
+28 -13
View File
@@ -12,7 +12,7 @@ using montauk::slen;
using montauk::memcpy; using montauk::memcpy;
using montauk::memmove; using montauk::memmove;
#include <edit/syntax_ansi.hpp> #include <syntax/syntax.hpp>
// ---- Integer to string ---- // ---- Integer to string ----
@@ -66,6 +66,8 @@ static void dim_text() { esc("2m"); }
// ---- Syntax highlighting state ---- // ---- Syntax highlighting state ----
static SynState synState = {}; static SynState synState = {};
#define SYN_TOKEN_MAX 1024
static SynLanguage synLang = SYN_LANG_NONE; static SynLanguage synLang = SYN_LANG_NONE;
// ANSI color for token type (must be called before each colored character) // ANSI color for token type (must be called before each colored character)
@@ -513,22 +515,27 @@ static void draw_line(int screenRow, int docLine) {
if (visibleLen > maxChars) visibleLen = maxChars; if (visibleLen > maxChars) visibleLen = maxChars;
} }
if (synLang != SYN_LANG_NONE && visibleLen > 0) { if (synLang != SYN_LANG_NONE) {
// Tokenize visible portion for syntax highlighting // Tokenize the WHOLE line, not just the visible window: the
static SynToken tokens[256]; // highlighter carries multi-line state (comments, HTML tags,
int tokCount = visibleLen; // script bodies) that only stays correct if every character of
if (tokCount > 256) tokCount = 256; // every line is seen in order.
static SynToken tokens[SYN_TOKEN_MAX];
int tokCount = ln->len;
if (tokCount > SYN_TOKEN_MAX) tokCount = SYN_TOKEN_MAX;
syn_fill_tokens(tokens, tokCount, 0, tokCount, SYN_NORMAL); syn_fill_tokens(tokens, tokCount, 0, tokCount, SYN_NORMAL);
syn_highlight_line(ln->data + startCol, visibleLen, tokens, tokCount, synLang, synState); syn_highlight_line(ln->data, ln->len, tokens, tokCount, synLang, synState);
// Output with ANSI colors // Output the visible window with ANSI colors
SynToken prevTok = SYN_NORMAL; SynToken prevTok = SYN_NORMAL;
for (int c = 0; c < tokCount; c++) { for (int c = 0; c < visibleLen; c++) {
if (c == 0 || tokens[c] != prevTok) { int ti = startCol + c;
syn_ansi_color(tokens[c]); SynToken tok = (ti < tokCount) ? tokens[ti] : SYN_NORMAL;
prevTok = tokens[c]; if (c == 0 || tok != prevTok) {
syn_ansi_color(tok);
prevTok = tok;
} }
putch(ln->data[startCol + c]); putch(ln->data[ti]);
} }
syn_ansi_reset(); syn_ansi_reset();
} else { } else {
@@ -552,6 +559,14 @@ static void render() {
draw_status_bar(); draw_status_bar();
if (fullRedraw) { if (fullRedraw) {
// Replay the lines above the viewport so multi-line syntax state
// (block comments, HTML tags, <script> bodies) is correct at topLine
synState = syn_make_state();
if (synLang != SYN_LANG_NONE) {
for (int i = 0; i < topLine && i < numLines; i++)
syn_highlight_line(lines[i].data, lines[i].len, nullptr, 0,
synLang, synState);
}
for (int i = 0; i < editorRows; i++) { for (int i = 0; i < editorRows; i++) {
draw_line(i, topLine + i); draw_line(i, topLine + i);
} }
+14 -6
View File
@@ -186,18 +186,26 @@ static bool path_is_file(const char* path) {
#define LOG_READ_SIZE 65536 #define LOG_READ_SIZE 65536
// Write the system log to a file on disk // Write the system log to a file on disk.
//
// 0:/state is the system's mutable runtime state: generated by the system,
// kept across boots, and never part of the system image. The log used to live
// in 0:/os/logs, which the protected-path table covers as CAP_SYSTEM_IMAGE
// (Fs/ProtectedPaths.cpp) -- nothing is ever granted that capability, so every
// create, write and delete below failed silently and the log froze at whatever
// an earlier build had written.
void stage_save_log() { void stage_save_log() {
// Edge case - 0:/os/logs exists and is a file // Edge cases - 0:/state or 0:/state/logs exists and is a file
if (path_is_file("0:/os/logs")) if (path_is_file("0:/state") || path_is_file("0:/state/logs"))
return; return;
montauk::fmkdir("0:/os/logs"); montauk::fmkdir("0:/state");
montauk::fmkdir("0:/state/logs");
// Clear any existing montaukos log // Clear any existing montaukos log
montauk::fdelete("0:/os/logs/montaukos"); montauk::fdelete("0:/state/logs/montaukos");
int handle = montauk::fcreate("0:/os/logs/montaukos"); int handle = montauk::fcreate("0:/state/logs/montaukos");
if (handle < 0) if (handle < 0)
return; return;
+61 -1
View File
@@ -95,6 +95,63 @@ static void print_snapshot(const montauk::abi::PowerInfo& info) {
print("\n"); print("\n");
} }
// Battery is reported independently of the CPU power features: a laptop under
// emulation has neither, but a real one may well expose a battery and no HWP.
static void print_battery() {
montauk::abi::BatteryInfo b{};
if (montauk::battery_info(&b) != 0) {
print("Battery: none (no ACPI battery device)\n");
return;
}
if (!b.present) {
print("Battery: slot present, no pack installed");
if (b.hasAcAdapter) print(b.acOnline ? " (on AC)" : " (no AC)");
print("\n");
return;
}
print("Battery: ");
put_u64(b.percent);
print("%");
if (b.charging) print(", charging");
else if (b.acOnline) print(", on AC, not charging");
else print(", discharging");
if (b.critical) print(", CRITICAL");
print("\n");
if (b.fullCapacity > 0) {
print(" Capacity: ");
put_u64(b.remainingCapacity);
print(" of ");
put_u64(b.fullCapacity);
print(b.powerUnitMilliAmps ? " mAh (design " : " mWh (design ");
put_u64(b.designCapacity);
print(")\n");
}
if (b.presentRate > 0) {
print(" Rate: ");
put_u64(b.presentRate);
print(b.powerUnitMilliAmps ? " mA\n" : " mW\n");
}
if (b.minutesRemaining >= 0) {
print(" Time left: ");
put_u64((uint64_t)(b.minutesRemaining / 60));
print("h ");
put_u64((uint64_t)(b.minutesRemaining % 60));
print("m\n");
}
if (b.voltageMv > 0) {
print(" Voltage: ");
put_u64(b.voltageMv);
print(" mV\n");
}
}
static void print_watch_line(const montauk::abi::PowerInfo& info, uint64_t t) { static void print_watch_line(const montauk::abi::PowerInfo& info, uint64_t t) {
put_u64(t); put_u64(t);
print("s "); print("s ");
@@ -124,13 +181,16 @@ extern "C" void _start() {
montauk::abi::PowerInfo info{}; montauk::abi::PowerInfo info{};
if (!read_info(info)) { if (!read_info(info)) {
print("power: no CPU power management available\n"); print("power: no CPU power management available\n");
print("(non-Intel CPU or running under emulation)\n"); print("(non-Intel CPU or running under emulation)\n\n");
print_battery();
montauk::exit(1); montauk::exit(1);
} }
bool watch = rest[0] == 'w'; bool watch = rest[0] == 'w';
if (!watch) { if (!watch) {
print_snapshot(info); print_snapshot(info);
print("\n");
print_battery();
montauk::exit(0); montauk::exit(0);
} }
+1 -1
View File
@@ -2,7 +2,7 @@
* main.cpp * main.cpp
* MontaukOS Text Editor - standalone Window Server app * MontaukOS Text Editor - standalone Window Server app
* Single-buffer text editor with line numbers, cursor, scrolling, file I/O, * Single-buffer text editor with line numbers, cursor, scrolling, file I/O,
* syntax highlighting for C, C++ and Lua files * syntax highlighting for C, C++, Lua, HTML, CSS and JS files
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
+3 -681
View File
@@ -1,50 +1,17 @@
/* /*
* syntax_highlight.hpp * syntax_highlight.hpp
* C, C++ and Lua syntax highlighting for the MontaukOS text editor * Token colors for the MontaukOS text editor
* Activated for .c/.h, C++ (.cpp/.hpp/...), and .lua files * The highlighters themselves live in <syntax/syntax.hpp>
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#pragma once #pragma once
#include <gui/gui.hpp> #include <gui/gui.hpp>
#include <syntax/syntax.hpp>
using namespace gui; using namespace gui;
enum SynLanguage : uint8_t {
SYN_LANG_NONE,
SYN_LANG_C,
SYN_LANG_CPP,
SYN_LANG_LUA,
};
// ============================================================================
// Token types
// ============================================================================
enum SynToken : uint8_t {
SYN_NORMAL,
SYN_KEYWORD,
SYN_TYPE,
SYN_PREPROCESSOR,
SYN_STRING,
SYN_CHAR,
SYN_COMMENT,
SYN_NUMBER,
SYN_OPERATOR,
};
#define SYN_RAW_DELIM_MAX 16
struct SynState {
bool in_block_comment;
SynToken long_token;
int long_bracket_eqs;
bool in_raw_string;
char raw_delim[SYN_RAW_DELIM_MAX];
int raw_delim_len;
};
// ============================================================================ // ============================================================================
// Colors for each token type // Colors for each token type
// ============================================================================ // ============================================================================
@@ -62,648 +29,3 @@ inline Color syn_color(SynToken tok) {
default: return Color::from_rgb(0x1E, 0x1E, 0x1E); // near-black default: return Color::from_rgb(0x1E, 0x1E, 0x1E); // near-black
} }
} }
// ============================================================================
// Keyword / type lookup
// ============================================================================
inline bool syn_is_alpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
inline bool syn_is_alnum(char c) {
return syn_is_alpha(c) || (c >= '0' && c <= '9');
}
inline bool syn_is_digit(char c) {
return c >= '0' && c <= '9';
}
inline bool syn_is_hex(char c) {
return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
inline void syn_set_token(SynToken* out, int out_len, int idx, SynToken tok) {
if (out && idx >= 0 && idx < out_len)
out[idx] = tok;
}
inline void syn_fill_tokens(SynToken* out, int out_len, int start, int end, SynToken tok) {
if (!out) return;
if (start < 0) start = 0;
if (end > out_len) end = out_len;
for (int i = start; i < end; i++)
out[i] = tok;
}
inline bool syn_streq(const char* buf, int len, const char* kw) {
int i = 0;
while (i < len && kw[i]) {
if (buf[i] != kw[i]) return false;
i++;
}
return i == len && kw[i] == '\0';
}
inline SynToken syn_classify_c_word(const char* buf, int len) {
// C keywords
static const char* keywords[] = {
"auto", "break", "case", "const", "continue", "default", "do",
"else", "enum", "extern", "for", "goto", "if", "inline",
"register", "restrict", "return", "sizeof", "static", "struct",
"switch", "typedef", "union", "volatile", "while",
"NULL", "true", "false", "nullptr",
};
// C types
static const char* types[] = {
"void", "char", "short", "int", "long", "float", "double",
"signed", "unsigned", "bool", "_Bool",
"int8_t", "int16_t", "int32_t", "int64_t",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
"FILE",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_lua_word(const char* buf, int len) {
static const char* keywords[] = {
"and", "break", "do", "else", "elseif", "end", "false",
"for", "function", "goto", "if", "in", "local", "nil",
"not", "or", "repeat", "return", "then", "true",
"until", "while",
};
static const char* builtins[] = {
"_ENV", "_G", "_VERSION",
"assert", "collectgarbage", "coroutine", "debug", "dofile",
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
"math", "next", "os", "package", "pairs", "pcall",
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
"select", "setmetatable", "string", "table", "tonumber",
"tostring", "type", "utf8", "warn", "xpcall",
};
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) {
if (syn_streq(buf, len, builtins[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
// C++ keywords (on top of the C set, which is checked first)
static const char* keywords[] = {
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
"catch", "class", "compl", "concept", "consteval", "constexpr",
"constinit", "const_cast", "co_await", "co_return", "co_yield",
"decltype", "delete", "dynamic_cast", "explicit", "export",
"final", "friend", "mutable", "namespace", "new", "noexcept",
"not", "not_eq", "operator", "or", "or_eq", "override",
"private", "protected", "public", "reinterpret_cast", "requires",
"static_assert", "static_cast", "template", "this", "thread_local",
"throw", "try", "typeid", "typename", "using", "virtual",
"xor", "xor_eq",
};
// C++ library / builtin types
static const char* types[] = {
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
"std", "string", "string_view", "vector", "array", "span",
"map", "set", "unordered_map", "unordered_set", "deque", "list",
"pair", "tuple", "optional", "variant", "function",
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
};
SynToken c = syn_classify_c_word(buf, len);
if (c != SYN_NORMAL) return c;
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
}
for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) {
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
}
return SYN_NORMAL;
}
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len) {
switch (lang) {
case SYN_LANG_C: return syn_classify_c_word(buf, len);
case SYN_LANG_CPP: return syn_classify_cpp_word(buf, len);
case SYN_LANG_LUA: return syn_classify_lua_word(buf, len);
default: return SYN_NORMAL;
}
}
inline SynState syn_make_state() {
SynState state = {};
state.in_block_comment = false;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
state.in_raw_string = false;
state.raw_delim_len = 0;
return state;
}
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
int& eqs, int& span) {
if (i >= len || line[i] != '[') return false;
int j = i + 1;
while (j < len && line[j] == '=') j++;
if (j < len && line[j] == '[') {
eqs = j - i - 1;
span = j - i + 1;
return true;
}
return false;
}
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
int eqs, int& span) {
if (i >= len || line[i] != ']') return false;
int j = i + 1;
for (int k = 0; k < eqs; k++) {
if (j >= len || line[j] != '=') return false;
j++;
}
if (j < len && line[j] == ']') {
span = j - i + 1;
return true;
}
return false;
}
inline void syn_scan_digits(const char* line, int len, int& i, bool sep, bool hex) {
while (i < len) {
if (hex ? syn_is_hex(line[i]) : syn_is_digit(line[i])) { i++; continue; }
// C++14 digit separator: a quote wedged between two digits
if (sep && line[i] == '\'' && i + 1 < len &&
(hex ? syn_is_hex(line[i + 1]) : syn_is_digit(line[i + 1]))) {
i += 2;
continue;
}
break;
}
}
inline void syn_consume_number(const char* line, int len, int& i,
SynToken* out, int out_len, bool c_style_suffixes,
bool digit_sep = false) {
int start = i;
if (line[i] == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) {
i += 2;
syn_scan_digits(line, len, i, digit_sep, true);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, true);
}
if (i < len && (line[i] == 'p' || line[i] == 'P')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
} else {
if (line[i] == '.') i++;
syn_scan_digits(line, len, i, digit_sep, false);
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
i++;
syn_scan_digits(line, len, i, digit_sep, false);
}
if (i < len && (line[i] == 'e' || line[i] == 'E')) {
int exp = i + 1;
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
if (exp < len && syn_is_digit(line[exp])) {
i = exp;
syn_scan_digits(line, len, i, digit_sep, false);
}
}
}
if (c_style_suffixes) {
while (i < len && (line[i] == 'u' || line[i] == 'U' ||
line[i] == 'l' || line[i] == 'L' ||
line[i] == 'f' || line[i] == 'F' ||
line[i] == 'z' || line[i] == 'Z'))
i++;
}
syn_fill_tokens(out, out_len, start, i, SYN_NUMBER);
}
// ============================================================================
// Per-line highlighter
// ============================================================================
//
// Fills `out[]` with SynToken values for each character in the line.
// `state` carries the multi-line syntax state across lines:
// - pass in the state from the previous line
// - on return, updated for the next line
//
// `line` points to the first char of the line, `len` is its length
// (excluding the newline). `out` can be null if only state tracking is needed.
// ============================================================================
// C / C++ raw string helpers
// ============================================================================
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
syn_streq(buf, len, "u8R");
}
inline bool syn_is_string_prefix(const char* buf, int len) {
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
}
// Colors a raw-string body from `i` up to and including the closing )delim".
// If the terminator is not on this line, leaves state.in_raw_string set so the
// next line continues the literal.
inline void syn_consume_raw_body(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
while (i < len) {
if (line[i] == ')') {
int j = i + 1;
int k = 0;
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
j++;
k++;
}
if (k == state.raw_delim_len && j < len && line[j] == '"') {
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
i = j + 1;
state.in_raw_string = false;
state.raw_delim_len = 0;
return;
}
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
}
// Enters a raw string; `line[i]` must be the opening quote.
inline void syn_consume_raw_string(const char* line, int len, int& i,
SynToken* out, int out_len, SynState& state) {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.raw_delim_len = 0;
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
state.raw_delim[state.raw_delim_len++] = line[i];
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
if (i < len && line[i] == '(') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
state.in_raw_string = true;
syn_consume_raw_body(line, len, i, out, out_len, state);
return;
}
// Malformed (or an over-long delimiter): treat the rest of the line as string
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
i = len;
}
// ============================================================================
// C / C++ highlighting
// ============================================================================
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
SynState& state, SynLanguage lang = SYN_LANG_C) {
const bool cpp = (lang == SYN_LANG_CPP);
int i = 0;
while (i < len) {
// ---- Raw string continuation ----
if (state.in_raw_string) {
syn_consume_raw_body(line, len, i, out, out_len, state);
continue;
}
// ---- Block comment continuation ----
if (state.in_block_comment) {
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
char c = line[i];
// ---- Line comment ----
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// ---- Block comment start ----
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
continue;
}
// ---- Preprocessor directive ----
if (c == '#') {
// Check that only whitespace precedes the #
bool is_pp = true;
for (int j = 0; j < i; j++) {
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
}
if (is_pp) {
while (i < len) {
// Handle line-comment inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
// Handle block comment start inside preprocessor
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
state.in_block_comment = true;
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
// Continue consuming as comment
while (i < len) {
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
syn_set_token(out, out_len, i, SYN_COMMENT);
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
i += 2;
state.in_block_comment = false;
break;
}
syn_set_token(out, out_len, i, SYN_COMMENT);
i++;
}
continue;
}
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
i++;
}
continue;
}
}
// ---- String literal ----
if (c == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
if (line[i] == '"') {
syn_set_token(out, out_len, i, SYN_STRING);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_STRING);
i++;
}
continue;
}
// ---- Character literal ----
if (c == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_CHAR);
syn_set_token(out, out_len, i + 1, SYN_CHAR);
i += 2;
continue;
}
if (line[i] == '\'') {
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
break;
}
syn_set_token(out, out_len, i, SYN_CHAR);
i++;
}
continue;
}
// ---- Numbers ----
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, true, cpp);
continue;
}
// ---- Identifiers / keywords / types / literal prefixes ----
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
if (cpp && i < len && line[i] == '"' &&
syn_is_raw_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
syn_consume_raw_string(line, len, i, out, out_len, state);
continue;
}
if (cpp && i < len && line[i] == '"' &&
syn_is_string_prefix(line + start, i - start)) {
// The quote itself is handled on the next iteration
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
continue;
}
if (cpp && i < len && line[i] == '\'' &&
syn_is_string_prefix(line + start, i - start)) {
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
continue;
}
SynToken tok = syn_classify_word(lang, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
// ---- Operators ----
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '=' || c == '!' || c == '<' || c == '>' || c == '&' ||
c == '|' || c == '^' || c == '~' || c == '?' || c == ':') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
// ---- Everything else (whitespace, braces, parens, etc.) ----
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
SynState& state) {
int i = 0;
while (i < len) {
if (state.long_token != SYN_NORMAL) {
int span = 0;
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
i += span;
state.long_token = SYN_NORMAL;
state.long_bracket_eqs = -1;
continue;
}
syn_set_token(out, out_len, i, state.long_token);
i++;
continue;
}
char c = line[i];
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
int eqs = 0;
int span = 0;
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
i += 2 + span;
state.long_token = SYN_COMMENT;
state.long_bracket_eqs = eqs;
continue;
}
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
break;
}
if (c == '"' || c == '\'') {
char quote = c;
syn_set_token(out, out_len, i, SYN_STRING);
i++;
while (i < len) {
if (line[i] == '\\' && i + 1 < len) {
syn_set_token(out, out_len, i, SYN_STRING);
syn_set_token(out, out_len, i + 1, SYN_STRING);
i += 2;
continue;
}
syn_set_token(out, out_len, i, SYN_STRING);
if (line[i] == quote) {
i++;
break;
}
i++;
}
continue;
}
int eqs = 0;
int span = 0;
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
i += span;
state.long_token = SYN_STRING;
state.long_bracket_eqs = eqs;
continue;
}
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
syn_consume_number(line, len, i, out, out_len, false);
continue;
}
if (syn_is_alpha(c)) {
int start = i;
while (i < len && syn_is_alnum(line[i])) i++;
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
syn_fill_tokens(out, out_len, start, i, tok);
continue;
}
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '^' || c == '#' || c == '=' || c == '<' || c == '>' ||
c == '~' || c == ':' || c == '.' || c == ',' || c == ';' ||
c == '(' || c == ')' || c == '{' || c == '}' ||
c == '[' || c == ']') {
syn_set_token(out, out_len, i, SYN_OPERATOR);
i++;
continue;
}
syn_set_token(out, out_len, i, SYN_NORMAL);
i++;
}
}
inline void syn_highlight_line(const char* line, int len, SynToken* out, int out_len,
SynLanguage lang, SynState& state) {
if (!line || len <= 0) return;
switch (lang) {
case SYN_LANG_C:
case SYN_LANG_CPP:
syn_highlight_line_c(line, len, out, out_len, state, lang);
break;
case SYN_LANG_LUA:
syn_highlight_line_lua(line, len, out, out_len, state);
break;
default:
syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL);
break;
}
}
// ============================================================================
// Extension check
// ============================================================================
inline bool syn_path_ends_with(const char* path, const char* suffix) {
if (!path || !suffix) return false;
int path_len = 0;
while (path[path_len]) path_len++;
int suffix_len = 0;
while (suffix[suffix_len]) suffix_len++;
if (path_len < suffix_len) return false;
for (int i = 0; i < suffix_len; i++) {
if (path[path_len - suffix_len + i] != suffix[i])
return false;
}
return true;
}
inline SynLanguage syn_detect_language(const char* filepath) {
if (!filepath || filepath[0] == '\0') return SYN_LANG_NONE;
if (syn_path_ends_with(filepath, ".cpp") || syn_path_ends_with(filepath, ".cc") ||
syn_path_ends_with(filepath, ".cxx") || syn_path_ends_with(filepath, ".c++") ||
syn_path_ends_with(filepath, ".hpp") || syn_path_ends_with(filepath, ".hh") ||
syn_path_ends_with(filepath, ".hxx") || syn_path_ends_with(filepath, ".h++") ||
syn_path_ends_with(filepath, ".ipp") || syn_path_ends_with(filepath, ".tpp") ||
syn_path_ends_with(filepath, ".inl"))
return SYN_LANG_CPP;
if (syn_path_ends_with(filepath, ".c") || syn_path_ends_with(filepath, ".h"))
return SYN_LANG_C;
if (syn_path_ends_with(filepath, ".lua"))
return SYN_LANG_LUA;
return SYN_LANG_NONE;
}
+25
View File
@@ -48,6 +48,31 @@ ICONS=(
"devices/symbolic/computer-symbolic.svg" "devices/symbolic/computer-symbolic.svg"
"devices/symbolic/network-wired-symbolic.svg" "devices/symbolic/network-wired-symbolic.svg"
"devices/symbolic/network-wireless-symbolic.svg" "devices/symbolic/network-wireless-symbolic.svg"
# Panel battery indicator: one icon per 10% bucket, plus the
# charging variants, the charged state and the empty-slot glyph.
"status/symbolic/battery-level-0-symbolic.svg"
"status/symbolic/battery-level-0-charging-symbolic.svg"
"status/symbolic/battery-level-10-symbolic.svg"
"status/symbolic/battery-level-10-charging-symbolic.svg"
"status/symbolic/battery-level-20-symbolic.svg"
"status/symbolic/battery-level-20-charging-symbolic.svg"
"status/symbolic/battery-level-30-symbolic.svg"
"status/symbolic/battery-level-30-charging-symbolic.svg"
"status/symbolic/battery-level-40-symbolic.svg"
"status/symbolic/battery-level-40-charging-symbolic.svg"
"status/symbolic/battery-level-50-symbolic.svg"
"status/symbolic/battery-level-50-charging-symbolic.svg"
"status/symbolic/battery-level-60-symbolic.svg"
"status/symbolic/battery-level-60-charging-symbolic.svg"
"status/symbolic/battery-level-70-symbolic.svg"
"status/symbolic/battery-level-70-charging-symbolic.svg"
"status/symbolic/battery-level-80-symbolic.svg"
"status/symbolic/battery-level-80-charging-symbolic.svg"
"status/symbolic/battery-level-90-symbolic.svg"
"status/symbolic/battery-level-90-charging-symbolic.svg"
"status/symbolic/battery-level-100-symbolic.svg"
"status/symbolic/battery-level-100-charged-symbolic.svg"
"status/symbolic/battery-missing-symbolic.svg"
"apps/symbolic/web-browser-symbolic.svg" "apps/symbolic/web-browser-symbolic.svg"
# Scalable (colorful) icons for app menu # Scalable (colorful) icons for app menu
"apps/scalable/utilities-terminal.svg" "apps/scalable/utilities-terminal.svg"