Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/loadout-overlay/src/bun/native/deck-hidraw-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ export async function startDeckHidrawWatcher(
// button map per frame is unnecessary on the hot path — chase the one
// bit and bail. (Less GC pressure than building a Map every report.)
if (bound) {
const cur = (report[bound.byte] & (1 << bound.bit)) !== 0;
// splitReports only yields full REPORT_LEN (64-byte) frames and
// bound.byte is always < 64, so this index is provably in-bounds.
const cur = (report[bound.byte]! & (1 << bound.bit)) !== 0;
if (cur && !lastBitValue && !suppressNextEdge) {
opts.onWake("QamToggle");
}
Expand Down
3 changes: 2 additions & 1 deletion apps/loadout-overlay/src/bun/native/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ export function hasCapability(caps: Uint8Array, code: number): boolean {
const byteIdx = code >> 3;
const bitIdx = code & 0x07;
if (byteIdx >= caps.length) return false;
return (caps[byteIdx] & (1 << bitIdx)) !== 0;
// Guarded above: byteIdx < caps.length, so this index is in-bounds.
return (caps[byteIdx]! & (1 << bitIdx)) !== 0;
}

/**
Expand Down
11 changes: 8 additions & 3 deletions apps/loadout-overlay/src/bun/native/gamescope-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class GamescopeAtoms {
for (const line of lines) {
if (!line.startsWith(prefix + "(")) continue;
const m = line.match(/=\s*"((?:[^"\\]|\\.)*)"/);
if (m) return m[1];
if (m) return m[1] ?? null;
}
}
return null;
Expand All @@ -311,6 +311,9 @@ export class GamescopeAtoms {
managed.push(id);
}
}
// The sole caller only invokes this with a non-empty candidate set
// (findSteamWindow returns early when allIds is empty), so pool is
// guaranteed non-empty and pool[0] below is always defined.
const pool = managed.length > 0 ? managed : candidates;

// Prefer a pool window currently claiming overlay/focus.
Expand All @@ -326,7 +329,7 @@ export class GamescopeAtoms {
return id;
}
}
return pool[0];
return pool[0]!;
}

/** True if `atom` is set on `windowId`. Uses xprop output to
Expand Down Expand Up @@ -851,7 +854,9 @@ export class GamescopeAtoms {
const m = line.match(/^(\w+)\([^)]+\)\s*=\s*(-?\d+)/);
if (!m) continue;
const n = Number(m[2]);
if (Number.isFinite(n)) out.set(m[1], n);
// Both capture groups are mandatory in the regex, so on a match
// m[1] is always present.
if (Number.isFinite(n)) out.set(m[1]!, n);
}
} catch (err) {
console.warn(
Expand Down
9 changes: 7 additions & 2 deletions apps/loadout-overlay/src/bun/native/input-intercept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ function buildBitmask(codes: readonly number[]): Uint8Array {
if (byteLen < 8) byteLen = 8;
const bits = new Uint8Array(byteLen);
for (const code of codes) {
bits[code >> 3] |= 1 << (code & 7);
// byteLen covers (maxCode >> 3) + 1 bytes and code <= maxCode, so
// code >> 3 is always in-bounds.
bits[code >> 3]! |= 1 << (code & 7);
}
return bits;
}
Expand Down Expand Up @@ -411,7 +413,9 @@ function readModifierState(fd: number): { guide: boolean; select: boolean } {
const buf = new Uint8Array(96);
const rc = libc.symbols.ioctl(fd, eviocgkey(buf.length), ptr(buf));
if (rc < 0) return { guide: false, select: false };
const bit = (code: number) => (buf[code >> 3] & (1 << (code & 7))) !== 0;
// The 96-byte buffer covers codes 0..767, past every code we query, so
// code >> 3 is always in-bounds.
const bit = (code: number) => (buf[code >> 3]! & (1 << (code & 7))) !== 0;
return { guide: bit(BTN_MODE), select: bit(BTN_SELECT) };
}

Expand Down Expand Up @@ -979,6 +983,7 @@ export async function startInputIntercept(
const idx = tracked.findIndex((t) => t.path === eventPath);
if (idx < 0) return;
const [t] = tracked.splice(idx, 1);
if (!t) return;
// Drop any deferred-grab entry before closing the fd: a stale entry would
// make pollOnce ioctl a closed (possibly OS-reused) fd next tick.
pendingGrabs.delete(t);
Expand Down
6 changes: 4 additions & 2 deletions apps/loadout-overlay/src/bun/native/ip-intercept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,11 @@ export function parseInputEventLine(
): { cap: string; value: number } | null {
const m = line.match(/\.InputEvent\s+\('([^']+)',\s*([-0-9.eE]+)\)/);
if (!m) return null;
const value = Number.parseFloat(m[2]);
// Both capture groups are mandatory in the regex, so on a match m[1]
// and m[2] are always present.
const value = Number.parseFloat(m[2]!);
if (Number.isNaN(value)) return null;
return { cap: m[1], value };
return { cap: m[1]!, value };
}

// ---- Public API ------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion apps/loadout-overlay/src/overlay/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ function SettingsInner({
: "bg-base-300/70 text-base-content/50"
}`}
>
{(plugin.icon ?? plugin.name)[0].toUpperCase()}
{(plugin.icon ?? plugin.name).charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-base-content truncate">
Expand Down
5 changes: 3 additions & 2 deletions apps/loadout-overlay/src/overlay/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export function Sidebar({
}
bucket[cat].push(p);
}
const result = order.map((name) => ({ name, items: bucket[name] }));
// Every name in `order` was pushed alongside its bucket entry above.
const result = order.map((name) => ({ name, items: bucket[name] ?? [] }));
if (favSet.size > 0) {
const favPlugins = favorites
.map((id) => plugins.find((p) => p.id === id))
Expand Down Expand Up @@ -263,7 +264,7 @@ export function Sidebar({
aria-hidden
/>
) : (
<span>{(plugin.icon ?? plugin.name)[0].toUpperCase()}</span>
<span>{(plugin.icon ?? plugin.name).charAt(0).toUpperCase()}</span>
)
}
label={plugin.name}
Expand Down
8 changes: 5 additions & 3 deletions apps/loadout-overlay/src/overlay/components/WelcomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export function WelcomeScreen({
const scrollRef = useRef<HTMLDivElement>(null);

const [stepIndex, setStepIndex] = useState(0);
const stepId = STEPS[stepIndex].id;
// stepIndex is clamped to [0, STEPS.length) by the nav handlers.
const stepId = STEPS[stepIndex]!.id;

// Theme — controlled locally during the flow so navigating around
// applies the preview immediately, then we persist on completion.
Expand Down Expand Up @@ -423,7 +424,8 @@ function StepHeader({
sub: "Press Open Loadout to head to your home dashboard. Everything below can be tweaked from the Settings page.",
},
};
const stepId = STEPS[stepIndex].id;
// stepIndex is clamped to [0, STEPS.length) by the nav handlers.
const stepId = STEPS[stepIndex]!.id;
const h = headers[stepId];
return (
<div className="px-9 pt-8 pb-4">
Expand Down Expand Up @@ -1375,7 +1377,7 @@ function StepPlugins({
: "bg-base-300/70 text-base-content/50"
}`}
>
{(plugin.icon ?? plugin.name)[0].toUpperCase()}
{(plugin.icon ?? plugin.name).charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-base-content truncate">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function WidgetPicker({
{!probing &&
availablePlugins.map((plugin) => {
const isAdded = favorites.includes(plugin.id);
const initial = (plugin.icon ?? plugin.name)[0].toUpperCase();
const initial = (plugin.icon ?? plugin.name).charAt(0).toUpperCase();
return (
<Focusable
key={plugin.id}
Expand Down
13 changes: 8 additions & 5 deletions apps/loadout-overlay/src/overlay/hooks/useGamepadInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ export function useGamepadInput(onBack?: () => void) {

// Left stick → arrow keys
if (gp.axes.length >= 2) {
const lx = gp.axes[0];
const ly = gp.axes[1];
// Guarded by length >= 2, so axes[0] and axes[1] are present.
const lx = gp.axes[0]!;
const ly = gp.axes[1]!;
handlePress("stick-left", "ArrowLeft", lx < -STICK_DEADZONE, now);
handlePress("stick-right", "ArrowRight", lx > STICK_DEADZONE, now);
handlePress("stick-up", "ArrowUp", ly < -STICK_DEADZONE, now);
Expand All @@ -165,8 +166,9 @@ export function useGamepadInput(onBack?: () => void) {
// pairs beyond the two sticks (axes 0-3).
for (let ai = 4; ai < gp.axes.length; ai += 2) {
if (ai + 1 >= gp.axes.length) break;
const dx = gp.axes[ai];
const dy = gp.axes[ai + 1];
// Guarded above: ai and ai + 1 are both < axes.length.
const dx = gp.axes[ai]!;
const dy = gp.axes[ai + 1]!;
handlePress(`daxis${ai}-left`, "ArrowLeft", dx < -STICK_DEADZONE, now);
handlePress(`daxis${ai}-right`, "ArrowRight", dx > STICK_DEADZONE, now);
handlePress(`daxis${ai}-up`, "ArrowUp", dy < -STICK_DEADZONE, now);
Expand All @@ -175,7 +177,8 @@ export function useGamepadInput(onBack?: () => void) {

// Right stick → smooth analog scroll with momentum
if (gp.axes.length >= 4) {
const ry = gp.axes[3];
// Guarded by length >= 4, so axes[3] is present.
const ry = gp.axes[3]!;
if (Math.abs(ry) > RIGHT_STICK_DEADZONE) {
scrollVelocity = ry * RIGHT_STICK_SPEED;
} else {
Expand Down
2 changes: 1 addition & 1 deletion apps/loadout/src/injector/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ export class SteamInjector {
menuPlugins.push({
pluginId: plugin.id,
title: t.title ?? plugin.name,
route: t.route ?? (plugin.routes ? Object.keys(plugin.routes)[0] : ""),
route: t.route ?? (plugin.routes ? (Object.keys(plugin.routes)[0] ?? "") : ""),
position: typeof t.position === "number" ? t.position : undefined,
icon: t.icon,
});
Expand Down
3 changes: 2 additions & 1 deletion apps/loadout/src/injector/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,6 @@ export async function findQuickAccessTab(options: GetTabsOptions): Promise<CEFTa
}

// Return the first one — typically the Big Picture Mode QAM
return qaTabs[0];
// Non-null: qaTabs.length was checked > 0 above.
return qaTabs[0]!;
}
2 changes: 1 addition & 1 deletion apps/loadout/src/loader/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function extractToken(req: Request): string | null {
if (authHeader) {
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (match) {
return match[1];
return match[1] ?? null;
}
}

Expand Down
3 changes: 2 additions & 1 deletion apps/loadout/src/loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ async function compileTsx(
});

if (result.success && result.outputs.length > 0) {
const code = await result.outputs[0].text();
// Non-null: outputs.length was checked > 0 above.
const code = await result.outputs[0]!.text();
return { code, ok: true };
}

Expand Down
17 changes: 11 additions & 6 deletions apps/loadout/src/loader/inject-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ import * as ReactDOMClient from "react-dom/client";
target: "browser",
});
if (vendorResult.success && vendorResult.outputs.length > 0) {
vendorBundle = await Bun.file(vendorResult.outputs[0].path).text();
// Non-null: outputs.length was checked > 0 above.
vendorBundle = await Bun.file(vendorResult.outputs[0]!.path).text();
} else {
console.error("Failed to build vendor bundle:", vendorResult.logs);
}
Expand Down Expand Up @@ -169,7 +170,8 @@ import * as ReactDOMClient from "react-dom/client";
});

if (sdkResult.success && sdkResult.outputs.length > 0) {
const sdkCode = await Bun.file(sdkResult.outputs[0].path).text();
// Non-null: outputs.length was checked > 0 above.
const sdkCode = await Bun.file(sdkResult.outputs[0]!.path).text();
sdkBundle = transformToIIFE(sdkCode, "__LOADOUT_SDK");
} else {
console.error("Failed to build inject SDK:", sdkResult.logs);
Expand Down Expand Up @@ -205,17 +207,20 @@ function transformToIIFE(esmCode: string, globalName: string): string {
coreCode = esmCode.slice(0, exportMatch.index).trimEnd();

// Parse export names (handle `name as alias` syntax)
const names = exportMatch[1].split(",").map((s) => s.trim());
// Non-null: exportMatch is truthy, group 1 is a required capture.
const names = exportMatch[1]!.split(",").map((s) => s.trim());
for (const name of names) {
const parts = name.split(/\s+as\s+/);
// Use the original name (before "as"), which is the local variable
exportNames.push(parts[0].trim());
// Use the original name (before "as"), which is the local variable.
// Non-null: split always yields at least one element.
exportNames.push(parts[0]!.trim());
}
}

if (defaultExportMatch && !exportMatch) {
coreCode = esmCode.slice(0, defaultExportMatch.index).trimEnd();
exportNames.push(defaultExportMatch[1]);
// Non-null: defaultExportMatch is truthy, group 1 is a required capture.
exportNames.push(defaultExportMatch[1]!);
}

// Build the assignments
Expand Down
8 changes: 5 additions & 3 deletions apps/loadout/src/loader/routes/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const pluginAppBundleRoute: RouteHandler = {
match: (_req, url) => APP_BUNDLE_PATTERN.test(url.pathname),
async handle(_req, url, ctx) {
const match = url.pathname.match(APP_BUNDLE_PATTERN)!;
const pluginId = match[1];
// Non-null: group 1 is a required capture in APP_BUNDLE_PATTERN.
const pluginId = match[1]!;
let code = ctx.bundleCache.get(pluginId);
if (!code) {
const appPath = join(ctx.pluginsDir, pluginId, "app.tsx");
Expand All @@ -66,8 +67,9 @@ export const pluginAssetRoute: RouteHandler = {
match: (_req, url) => ASSET_PATTERN.test(url.pathname),
async handle(_req, url, ctx) {
const match = url.pathname.match(ASSET_PATTERN)!;
const pluginId = match[1];
const relPath = decodeURIComponent(match[2]);
// Non-null: groups 1 and 2 are required captures in ASSET_PATTERN.
const pluginId = match[1]!;
const relPath = decodeURIComponent(match[2]!);
if (!ctx.plugins.has(pluginId)) {
return new Response("Not Found", { status: 404 });
}
Expand Down
10 changes: 10 additions & 0 deletions apps/loadout/src/loader/routes/steam-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ export const steamGridRoute: RouteHandler = {
const perUser = url.pathname.match(STEAM_GRID_PATTERN);
if (perUser) {
const [, idPart, userIdPart, type] = perUser;
if (
idPart === undefined ||
userIdPart === undefined ||
type === undefined
) {
return new Response("not found", { status: 404 });
}
const t = type as SteamGridType;
const candidates = steamGridCandidates(idPart, t);
const gridDir = join(
Expand Down Expand Up @@ -238,6 +245,9 @@ export const steamGridRoute: RouteHandler = {
const auto = url.pathname.match(STEAM_GRID_AUTO_PATTERN);
if (!auto) return new Response("not found", { status: 404 });
const [, idPart, type] = auto;
if (idPart === undefined || type === undefined) {
return new Response("not found", { status: 404 });
}
const t = type as SteamGridType;
const candidates = steamGridCandidates(idPart, t);
const userdata = getUserdataDir();
Expand Down
3 changes: 2 additions & 1 deletion apps/loadout/src/loader/services/game-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export class GameDetectionService implements PluginBackend {
const endTime = Date.now();
const idx = this.recent.findIndex((s) => s.appId === appId && s.endTime === undefined);
if (idx >= 0) {
this.recent[idx] = { ...this.recent[idx], endTime };
// Non-null: idx comes from findIndex and is >= 0, so it is in bounds.
this.recent[idx] = { ...this.recent[idx]!, endTime };
}
if (this.current && this.current.appId === appId) {
this.current = null;
Expand Down
3 changes: 2 additions & 1 deletion apps/loadout/src/loader/target-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export function resolveUser(
const uid = Number(f[2]);
const gid = Number(f[3]);
if (Number.isFinite(uid) && Number.isFinite(gid)) {
return { uid, gid, home: f[5] };
// Non-null: f.length was checked >= 6 above.
return { uid, gid, home: f[5]! };
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions packages/deck-hid/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,17 @@ export function parseHidUEvent(content: string): HidUEvent {
// Format: BUS:VENDOR:PRODUCT, all hex, vendor/product zero-padded to 8.
const parts = value.split(":");
if (parts.length === 3) {
out.bus = parseInt(parts[0], 16);
out.vendor = parseInt(parts[1], 16);
out.product = parseInt(parts[2], 16);
// Non-null: parts.length was checked === 3 above.
out.bus = parseInt(parts[0]!, 16);
out.vendor = parseInt(parts[1]!, 16);
out.product = parseInt(parts[2]!, 16);
}
} else if (key === "HID_PHYS") {
out.hidPhys = value;
// Tail "/inputN" — the kernel encodes the USB interface number here.
const m = value.match(/\/input(\d+)\s*$/);
if (m) out.interfaceNum = parseInt(m[1], 10);
// Non-null: m is truthy, group 1 is a required capture.
if (m) out.interfaceNum = parseInt(m[1]!, 10);
}
}
return out;
Expand Down Expand Up @@ -199,7 +201,7 @@ export function decodeButtons(report: Buffer): Map<string, boolean> | null {
if (report[0] !== REPORT_ID_INPUT) return null;
const out = new Map<string, boolean>();
for (const [byteIdx, defs] of DECK_BUTTONS_BY_BYTE) {
const v = report[byteIdx];
const v = report[byteIdx] ?? 0;
for (const { bit, name } of defs) {
out.set(name, (v & (1 << bit)) !== 0);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/game-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ async function readUserCollections(
const match = text.match(/"user-collections"\s*"((?:[^"\\]|\\.)*)"/);
if (!match) return new Map();

const escaped = match[1];
const escaped = match[1] ?? "";
const unescaped = escaped.replace(/\\\\/g, "\\").replace(/\\"/g, '"');

let collections: Record<string, { id?: string; added?: number[] }>;
Expand Down Expand Up @@ -190,7 +190,7 @@ async function readUserCollections(
function parseVdfValue(content: string, key: string): string | null {
const regex = new RegExp(`"${key}"\\s+"([^"]*)"`, "i");
const match = content.match(regex);
return match ? match[1] : null;
return match ? (match[1] ?? null) : null;
}

/**
Expand Down
Loading
Loading