diff --git a/flake.nix b/flake.nix index d98b289dea25d477977c82572923cb079d995c52..deb870346fd22976a555d10407a898f21416da8f 100644 --- a/flake.nix +++ b/flake.nix @@ -74,31 +74,11 @@ }; }); }) - # https://github.com/NixOS/nixpkgs/issues/476669#issuecomment-3707850520 - (_: prev: { - vesktop = prev.vesktop.overrideAttrs (_: { - preBuild = '' - cp -r ${prev.electron.dist} electron-dist - chmod -R u+w electron-dist - ''; - buildPhase = '' - runHook preBuild - - pnpm build - pnpm exec electron-builder \ - --dir \ - -c.asarUnpack="**/*.node" \ - -c.electronDist="electron-dist" \ - -c.electronVersion=${prev.electron.version} - - runHook postBuild - ''; - }); - }) # custom packages (_: pkgs: { autofmt = pkgs.callPackage ./packages/autofmt { }; + "jj-import-merge" = pkgs.callPackage ./packages/jj-import-merge { }; exiled_exchange_2 = pkgs.callPackage ./packages/exiled_exchange_2 { }; awakened_poe_trade = pkgs.callPackage ./packages/awakened_poe_trade { }; mtga-draft-17lands = pkgs.callPackage ./packages/17lands { }; @@ -133,6 +113,7 @@ nvim-natalie = mkNeovim "natalie" pkgs; inherit (pkgs) autofmt; + "jj-import-merge" = pkgs."jj-import-merge"; } // lib.optionalAttrs (system == "aarch64-darwin") { # "nix run .#darwin-rebuild" diff --git a/modules/home/default.nix b/modules/home/default.nix index fc538513d7e2b2bfb7916c71e2a97e1aa8bcc633..c137f374bdee0de3013b37b5eaaa00da07c061d7 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -149,6 +149,7 @@ in behavior = "drop"; }; git = { + colocate = false; sign-on-push = config.programs.git.signing.key != null; }; remotes.origin = { diff --git a/modules/macos/tiling/aerospace.nix b/modules/macos/tiling/aerospace.nix index b3acf3cd90601a1302a9a4a592abd653215d10e0..a18b269632c85ffed1cb14406c4c56406bd65a84 100644 --- a/modules/macos/tiling/aerospace.nix +++ b/modules/macos/tiling/aerospace.nix @@ -34,7 +34,7 @@ outer = { left = 0; bottom = 0; - top = 40; + top = 10; right = 0; }; }; diff --git a/packages/autofmt/autofmt.js b/packages/autofmt/autofmt.js index 7211f0b32c6e745f2c6807356e8d38b48cb79425..5bb0a0037307e72927401fa4f3db4859f9e45b26 100755 --- a/packages/autofmt/autofmt.js +++ b/packages/autofmt/autofmt.js @@ -100,12 +100,14 @@ const formatters = { }, prettier: { languages: ["javascript", "markdown", "json", "yaml", "mdx", "html", "css"], + mustMatchFile: true, files: ["node_modules/.bin/prettier"], cmd: ["node_modules/.bin/prettier", "--write", "--", "$files"], stdin: (file) => ["node_modules/.bin/prettier", "--stdin-filepath", file], }, biome: { languages: ["javascript", "json", "css"], + mustMatchFile: true, files: ["node_modules/.bin/biome"], cmd: ["node_modules/.bin/biome", "format", "--", "$files"], stdin: (file) => [ @@ -542,20 +544,22 @@ function pickFormatter(file) { : Object.entries(formatters[fmt].files) .map(([base, check]) => ({ base, check })); for (const { base, check } of filesToCheck) { + let fileMatches; if (base.includes("/")) { - matches = readDir(path.join(x, path.dirname(base))) // + fileMatches = readDir(path.join(x, path.dirname(base))) // .includes(path.basename(base)); } else { - matches = children.includes(base); + fileMatches = children.includes(base); } - if (matches && typeof check === "function") { + if (fileMatches && typeof check === "function") { let text; - matches = !!check({ + fileMatches = !!check({ get text() { return text ??= fs.readFileSync(path.join(x, base), "utf-8"); }, }); } + matches ||= fileMatches; } } if (matches) { diff --git a/packages/autofmt/default.nix b/packages/autofmt/default.nix index 45db7d83280d053850fcd1f77fc1a757e338ed38..64cebfb26f07b232564e8818e4732c0663c0dbd4 100644 --- a/packages/autofmt/default.nix +++ b/packages/autofmt/default.nix @@ -1,6 +1,5 @@ { pkgs, ... }: -pkgs.writeShellApplication { - name = "autofmt"; +let runtimeInputs = with pkgs; [ # include only a couple of formatters by default clang-tools @@ -11,5 +10,16 @@ pkgs.writeShellApplication { rustfmt zig ]; - text = ''exec ${pkgs.nodejs}/bin/node ${./autofmt.js} "$@"''; +in +pkgs.writeTextFile { + name = "autofmt"; + destination = "/bin/autofmt"; + executable = true; + text = builtins.replaceStrings + [ "#!/usr/bin/env node" ] + [ + ''#!${pkgs.nodejs}/bin/node + process.env.PATH = "${pkgs.lib.makeBinPath runtimeInputs}:" + (process.env.PATH ?? "");'' + ] + (builtins.readFile ./autofmt.js); } diff --git a/packages/jj-import-merge/default.nix b/packages/jj-import-merge/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..281bac05ddff1e3ccdab9d2dfc39e9144749f80f --- /dev/null +++ b/packages/jj-import-merge/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: +pkgs.writeShellApplication { + name = "jj-import-merge"; + runtimeInputs = with pkgs; [ + autofmt + nodejs + ]; + text = ''exec ${pkgs.nodejs}/bin/node ${./jj-import-merge.js} "$@"''; +} diff --git a/packages/jj-import-merge/jj-import-merge.js b/packages/jj-import-merge/jj-import-merge.js new file mode 100644 index 0000000000000000000000000000000000000000..df56d84e771df5b1f29c3de95638a1c4eb6c84f4 --- /dev/null +++ b/packages/jj-import-merge/jj-import-merge.js @@ -0,0 +1,871 @@ +#!/usr/bin/env node +// jj-import-merge +// Merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts. + +import * as child_process from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import process from "node:process"; + +const SUPPORTED_EXTENSIONS = new Set([ + ".cjs", + ".cts", + ".js", + ".jsx", + ".mjs", + ".mts", + ".ts", + ".tsx", +]); + +const CONFLICT_START_RE = /^<<<<<<< conflict \d+ of \d+/m; +const CONFLICT_END_RE = /^>>>>>>> conflict \d+ of \d+ ends$/m; + +main(); + +function main() { + const options = parseArgs(process.argv.slice(2)); + let files = options.files.map((file) => path.resolve(file)); + + if (options.allJj) { + files.push(...listJjConflictFiles().map((file) => path.resolve(file))); + } + + files = [...new Set(files)]; + if (files.length === 0) { + usage(1); + } + + let failed = false; + for (const file of files) { + try { + const result = processFile(file); + if (result.kind === "formatted") { + console.info(`formatted ${path.relative(process.cwd(), file)}`); + } else if (result.kind === "rewritten") { + console.info(`rewrote ${path.relative(process.cwd(), file)}`); + } + } catch (error) { + failed = true; + const message = error instanceof Error ? error.message : String(error); + console.error(`error: ${message}`); + } + } + + process.exit(failed ? 1 : 0); +} + +function parseArgs(argv) { + const options = { + allJj: false, + files: [], + }; + + while (argv.length > 0) { + const arg = argv.shift(); + if (arg === "-h" || arg === "--help") { + usage(0); + } else if (arg === "--all-jj") { + options.allJj = true; + } else if (arg === "--") { + options.files.push(...argv); + break; + } else if (arg.startsWith("-")) { + console.error(`error: unknown option ${JSON.stringify(arg)}`); + usage(1); + } else { + options.files.push(arg); + } + } + + return options; +} + +function usage(code) { + const exe = "jj-import-merge"; + const stream = code === 0 ? process.stdout : process.stderr; + stream.write(`usage: ${exe} [--all-jj] [--] ...\n`); + stream.write(`\n`); + stream.write(`merge top-of-file JS/TS imports from 2-sided Jujutsu conflicts\n`); + stream.write(`\n`); + stream.write(`options:\n`); + stream.write(` --all-jj resolve all conflicted JS/TS files from 'jj resolve --list'\n`); + stream.write(` -h show help\n`); + process.exit(code); +} + +function listJjConflictFiles() { + const result = child_process.spawnSync( + "jj", + ["--color=never", "--quiet", "resolve", "--list"], + { encoding: "utf8" }, + ); + + if (result.error) { + throw new Error(`failed to run jj: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(result.stderr.trim() || "jj resolve --list failed"); + } + + return result.stdout + .split(/\r?\n/) + .map((line) => line.trimEnd()) + .filter(Boolean) + .map((line) => line.match(/^(.*?)\s{2,}.+conflict$/)) + .filter(Boolean) + .map((match) => match[1]); +} + +function processFile(file) { + if (!SUPPORTED_EXTENSIONS.has(path.extname(file))) { + return { kind: "noop" }; + } + + let original; + try { + original = fs.readFileSync(file, "utf8"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`cannot read ${file}: ${message}`); + } + + if (!CONFLICT_START_RE.test(original)) { + return { kind: "noop" }; + } + + const parsed = parseConflictFile(original); + if (!parsed.supported) { + return { kind: "noop" }; + } + + const leftText = parsed.parts.map((part) => part.a).join(""); + const rightText = parsed.parts.map((part) => part.b).join(""); + const leftImports = extractTopImportBlock(leftText); + const rightImports = extractTopImportBlock(rightText); + if (!leftImports || !rightImports) { + return { kind: "noop" }; + } + + const leftPrelude = leftText.slice(0, leftImports.start); + const rightPrelude = rightText.slice(0, rightImports.start); + if (leftPrelude !== rightPrelude) { + return { kind: "noop" }; + } + + const start = projectBoundaryToOriginal( + parsed.parts, + leftImports.start, + rightImports.start, + ); + const end = projectBoundaryToOriginal(parsed.parts, leftImports.end, rightImports.end); + if (start == null || end == null || end < start) { + return { kind: "noop" }; + } + + const mergedImports = mergeImportStatements( + leftImports.statements, + rightImports.statements, + detectNewline(original), + ); + const updated = original.slice(0, start) + mergedImports + original.slice(end); + + if (updated === original) { + return { kind: "noop" }; + } + + try { + fs.writeFileSync(file, updated, "utf8"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`cannot write ${file}: ${message}`); + } + + if (hasConflictMarkers(updated)) { + return { kind: "rewritten" }; + } + + const formatResult = child_process.spawnSync("autofmt", [file], { + encoding: "utf8", + stdio: "inherit", + }); + + if (formatResult.error || formatResult.status !== 0) { + fs.writeFileSync(file, original, "utf8"); + if (formatResult.error) { + throw new Error(`autofmt failed for ${file}: ${formatResult.error.message}`); + } + throw new Error(`autofmt failed for ${file}`); + } + + return { kind: "formatted" }; +} + +function parseConflictFile(text) { + const lines = splitLines(text); + const parts = []; + + let index = 0; + while (index < lines.length) { + const line = lines[index].text; + if (!line.startsWith("<<<<<<< conflict ")) { + const start = index; + index += 1; + while ( + index < lines.length && + !lines[index].text.startsWith("<<<<<<< conflict ") + ) { + index += 1; + } + const raw = joinLines(lines, start, index); + parts.push({ type: "plain", orig: raw, a: raw, b: raw }); + continue; + } + + const block = parseConflictBlock(lines, index); + if (!block.supported) { + return { supported: false, parts: [] }; + } + parts.push(block.part); + index = block.nextIndex; + } + + return { supported: true, parts }; +} + +function parseConflictBlock(lines, startIndex) { + let index = startIndex + 1; + const entries = []; + + while (index < lines.length) { + const line = lines[index].text; + if (line.startsWith(">>>>>>> conflict ")) { + break; + } + if (line.startsWith("+++++++ ")) { + const contentStart = index + 1; + index += 1; + while ( + index < lines.length && + !isConflictEntryStart(lines[index].text) && + !lines[index].text.startsWith(">>>>>>> conflict ") + ) { + index += 1; + } + entries.push({ + kind: "explicit", + text: joinLines(lines, contentStart, index), + }); + continue; + } + if (line.startsWith("%%%%%%% diff from:")) { + if (index + 1 >= lines.length || !/^\\\\+\s*to:/.test(lines[index + 1].text)) { + return { supported: false, nextIndex: startIndex + 1 }; + } + index += 2; + let text = ""; + while ( + index < lines.length && + !isConflictEntryStart(lines[index].text) && + !lines[index].text.startsWith(">>>>>>> conflict ") + ) { + const diffLine = lines[index].text; + const prefix = diffLine[0]; + if (prefix === " " || prefix === "+") { + text += diffLine.slice(1); + } else if (prefix !== "-") { + return { supported: false, nextIndex: startIndex + 1 }; + } + index += 1; + } + entries.push({ kind: "diff", text }); + continue; + } + return { supported: false, nextIndex: startIndex + 1 }; + } + + if ( + index >= lines.length || + !lines[index].text.startsWith(">>>>>>> conflict ") || + entries.length !== 2 + ) { + return { supported: false, nextIndex: startIndex + 1 }; + } + + return { + supported: true, + nextIndex: index + 1, + part: { + type: "conflict", + orig: joinLines(lines, startIndex, index + 1), + a: entries[0].text, + b: entries[1].text, + }, + }; +} + +function isConflictEntryStart(line) { + return line.startsWith("+++++++ ") || line.startsWith("%%%%%%% diff from:"); +} + +function splitLines(text) { + const lines = []; + let start = 0; + + for (let index = 0; index < text.length; index += 1) { + if (text[index] !== "\n") continue; + lines.push({ + start, + end: index + 1, + text: text.slice(start, index + 1), + }); + start = index + 1; + } + + if (start < text.length) { + lines.push({ start, end: text.length, text: text.slice(start) }); + } + + return lines; +} + +function joinLines(lines, start, end) { + let text = ""; + for (let index = start; index < end; index += 1) { + text += lines[index].text; + } + return text; +} + +function extractTopImportBlock(text) { + const lines = splitLines(text); + if (lines.length === 0) return null; + + let index = 0; + if (lines[0]?.text.startsWith("#!")) { + index += 1; + } + + index = skipPrelude(lines, index); + if (index == null || index >= lines.length) { + return null; + } + + const first = parseImportStatement(lines, index); + if (!first) { + return null; + } + + const start = lines[index].start; + const statements = [first.statement]; + let end = offsetForLine(lines, first.nextIndex); + index = first.nextIndex; + + while (true) { + const afterTrivia = skipBlankAndComments(lines, index); + if (afterTrivia == null) { + return null; + } + + const next = parseImportStatement(lines, afterTrivia); + if (!next) { + break; + } + + statements.push(next.statement); + end = offsetForLine(lines, next.nextIndex); + index = next.nextIndex; + } + + return { start, end, statements }; +} + +function skipPrelude(lines, startIndex) { + let index = startIndex; + while (index < lines.length) { + const afterTrivia = skipBlankAndComments(lines, index); + if (afterTrivia == null) { + return null; + } + if (afterTrivia >= lines.length) { + return afterTrivia; + } + + const trimmed = lines[afterTrivia].text.trim(); + if (isDirectiveLine(trimmed)) { + index = afterTrivia + 1; + continue; + } + return afterTrivia; + } + return index; +} + +function skipBlankAndComments(lines, startIndex) { + let index = startIndex; + + while (index < lines.length) { + const trimmed = lines[index].text.trim(); + if (trimmed === "") { + index += 1; + continue; + } + if (trimmed.startsWith("//")) { + index += 1; + continue; + } + if (trimmed.startsWith("/*")) { + index = skipBlockComment(lines, index); + if (index == null) { + return null; + } + continue; + } + break; + } + + return index; +} + +function skipBlockComment(lines, startIndex) { + for (let index = startIndex; index < lines.length; index += 1) { + if (lines[index].text.includes("*/")) { + return index + 1; + } + } + return null; +} + +function isDirectiveLine(line) { + return /^["'][^"'\\]*(?:\\.[^"'\\]*)*["'];?$/.test(line); +} + +function parseImportStatement(lines, startIndex) { + if (startIndex >= lines.length) { + return null; + } + + const firstLine = lines[startIndex].text.trimStart(); + if (!firstLine.startsWith("import")) { + return null; + } + if (/^import(?:\s*\(|\s*\.)/.test(firstLine)) { + return null; + } + + let raw = ""; + const state = { + braceDepth: 0, + inBlockComment: false, + stringQuote: null, + }; + + for (let index = startIndex; index < lines.length; index += 1) { + raw += lines[index].text; + scanLine(lines[index].text, state); + + if (state.inBlockComment || state.braceDepth !== 0) { + continue; + } + + const analysis = parseImportSemantics(raw); + if (analysis) { + return { + nextIndex: index + 1, + statement: raw, + }; + } + } + + return null; +} + +function scanLine(line, state) { + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + const next = line[index + 1]; + + if (state.inBlockComment) { + if (char === "*" && next === "/") { + state.inBlockComment = false; + index += 1; + } + continue; + } + + if (state.stringQuote) { + if (char === "\\") { + index += 1; + continue; + } + if (char === state.stringQuote) { + state.stringQuote = null; + } + continue; + } + + if (char === "/" && next === "/") { + break; + } + if (char === "/" && next === "*") { + state.inBlockComment = true; + index += 1; + continue; + } + if (char === "'" || char === "\"" || char === "`") { + state.stringQuote = char; + continue; + } + if (char === "{") { + state.braceDepth += 1; + } else if (char === "}") { + state.braceDepth = Math.max(0, state.braceDepth - 1); + } + } +} + +function parseImportSemantics(raw) { + const normalized = collapseWhitespace(raw); + if (!normalized.startsWith("import ")) { + return null; + } + + const sideEffectMatch = normalized.match( + /^import\s+(["'])(.*?)\1(\s+with\s+\{.*\})?\s*;?$/, + ); + if (sideEffectMatch) { + return { + kind: "side-effect", + source: sideEffectMatch[2], + quote: sideEffectMatch[1], + attributes: sideEffectMatch[3] ?? "", + key: `side:${sideEffectMatch[2]}:${sideEffectMatch[3] ?? ""}`, + }; + } + + const bindingMatch = normalized.match( + /^import\s+(type\s+)?(.+?)\s+from\s+(["'])(.*?)\3(\s+with\s+\{.*\})?\s*;?$/, + ); + if (!bindingMatch) { + return null; + } + + const parsed = parseBindingSpec(bindingMatch[2].trim()); + if (!parsed) { + return null; + } + + const named = parsed.namedSpecifiers.join(","); + return { + kind: "binding", + isTypeImport: !!bindingMatch[1], + defaultBinding: parsed.defaultBinding, + namespaceImport: parsed.namespaceImport, + namedSpecifiers: parsed.namedSpecifiers, + source: bindingMatch[4], + quote: bindingMatch[3], + attributes: bindingMatch[5] ?? "", + key: [ + "binding", + bindingMatch[1] ? "type" : "value", + parsed.defaultBinding ?? "", + parsed.namespaceImport ?? "", + named, + bindingMatch[4], + bindingMatch[5] ?? "", + ].join(":"), + }; +} + +function parseBindingSpec(specPart) { + let defaultBinding = null; + let namespaceImport = null; + let namedSpecifiers = []; + + const braceStart = specPart.indexOf("{"); + const braceEnd = specPart.lastIndexOf("}"); + if (braceStart !== -1 || braceEnd !== -1) { + if (braceStart === -1 || braceEnd === -1 || braceEnd < braceStart) { + return null; + } + + const beforeBrace = specPart + .slice(0, braceStart) + .trim() + .replace(/,\s*$/, "") + .trim(); + const afterBrace = specPart.slice(braceEnd + 1).trim(); + if (afterBrace !== "") { + return null; + } + + const head = parseBindingHead(beforeBrace); + if (!head) { + return null; + } + defaultBinding = head.defaultBinding; + namespaceImport = head.namespaceImport; + namedSpecifiers = splitCommaList(specPart.slice(braceStart + 1, braceEnd)); + } else { + const head = parseBindingHead(specPart); + if (!head) { + return null; + } + defaultBinding = head.defaultBinding; + namespaceImport = head.namespaceImport; + } + + return { + defaultBinding, + namespaceImport, + namedSpecifiers, + }; +} + +function parseBindingHead(head) { + if (head === "") { + return { defaultBinding: null, namespaceImport: null }; + } + + if (head.startsWith("* as ")) { + return { defaultBinding: null, namespaceImport: head }; + } + + const parts = head.split(/\s*,\s*/).filter(Boolean); + if (parts.length === 1) { + return { + defaultBinding: parts[0], + namespaceImport: null, + }; + } + + if (parts.length === 2 && parts[1].startsWith("* as ")) { + return { + defaultBinding: parts[0], + namespaceImport: parts[1], + }; + } + + return null; +} + +function splitCommaList(text) { + return text + .split(",") + .map((part) => collapseWhitespace(part)) + .filter(Boolean); +} + +function mergeImportStatements(leftStatements, rightStatements, newline) { + const merged = []; + + for (const statement of [...leftStatements, ...rightStatements]) { + const analysis = parseImportSemantics(statement); + if (!analysis) { + const key = `raw:${collapseWhitespace(statement)}`; + if (!merged.some((entry) => entry.key === key)) { + merged.push({ + key, + raw: ensureTrailingNewline(statement.trim(), newline), + analysis: null, + }); + } + continue; + } + + const exactIndex = merged.findIndex((entry) => entry.key === analysis.key); + if (exactIndex !== -1) { + continue; + } + + const mergeIndex = merged.findIndex( + (entry) => entry.analysis && canMergeImports(entry.analysis, analysis), + ); + if (mergeIndex !== -1) { + const nextAnalysis = mergeImports(merged[mergeIndex].analysis, analysis); + merged[mergeIndex] = { + key: nextAnalysis.key, + raw: renderImport(nextAnalysis, newline), + analysis: nextAnalysis, + }; + continue; + } + + merged.push({ + key: analysis.key, + raw: renderImport(analysis, newline), + analysis, + }); + } + + return merged + .map((entry) => entry.raw.trimEnd()) + .join(newline) + newline; +} + +function canMergeImports(left, right) { + if (left.kind !== "binding" || right.kind !== "binding") { + return false; + } + if ( + left.source !== right.source || + left.attributes !== right.attributes || + left.isTypeImport !== right.isTypeImport + ) { + return false; + } + + const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); + const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport); + if (defaultBinding === undefined || namespaceImport === undefined) { + return false; + } + + const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]); + if (namespaceImport && namedSpecifiers.length > 0) { + return false; + } + + return true; +} + +function mergeImports(left, right) { + const defaultBinding = mergeField(left.defaultBinding, right.defaultBinding); + const namespaceImport = mergeField(left.namespaceImport, right.namespaceImport); + const namedSpecifiers = unique([...left.namedSpecifiers, ...right.namedSpecifiers]); + + return { + kind: "binding", + isTypeImport: left.isTypeImport, + defaultBinding, + namespaceImport, + namedSpecifiers, + source: left.source, + quote: left.quote, + attributes: left.attributes, + key: [ + "binding", + left.isTypeImport ? "type" : "value", + defaultBinding ?? "", + namespaceImport ?? "", + namedSpecifiers.join(","), + left.source, + left.attributes, + ].join(":"), + }; +} + +function mergeField(left, right) { + if (left == null) return right ?? null; + if (right == null) return left; + if (left === right) return left; + return undefined; +} + +function renderImport(analysis, newline) { + if (analysis.kind === "side-effect") { + return ensureTrailingNewline( + `import ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, + newline, + ); + } + + const pieces = []; + if (analysis.defaultBinding) { + pieces.push(analysis.defaultBinding); + } + if (analysis.namespaceImport) { + pieces.push(analysis.namespaceImport); + } + if (analysis.namedSpecifiers.length > 0) { + if (analysis.namedSpecifiers.length === 1) { + pieces.push(`{ ${analysis.namedSpecifiers[0]} }`); + } else { + pieces.push( + `{${newline}${analysis.namedSpecifiers.map((spec) => ` ${spec},`).join(newline)}${newline}}`, + ); + } + } + + return ensureTrailingNewline( + `import ${analysis.isTypeImport ? "type " : ""}${pieces.join(", ")} from ${analysis.quote}${analysis.source}${analysis.quote}${analysis.attributes};`, + newline, + ); +} + +function projectBoundaryToOriginal(parts, targetA, targetB) { + let offsetA = 0; + let offsetB = 0; + let offsetOrig = 0; + + if (targetA === 0 && targetB === 0) { + return 0; + } + + for (const part of parts) { + if (targetA === offsetA && targetB === offsetB) { + return offsetOrig; + } + + const nextA = offsetA + part.a.length; + const nextB = offsetB + part.b.length; + const nextOrig = offsetOrig + part.orig.length; + + if (part.type === "plain") { + if ( + targetA >= offsetA && + targetA <= nextA && + targetB >= offsetB && + targetB <= nextB + ) { + const deltaA = targetA - offsetA; + const deltaB = targetB - offsetB; + if (deltaA !== deltaB) { + return null; + } + return offsetOrig + deltaA; + } + } + + if (targetA === nextA && targetB === nextB) { + return nextOrig; + } + + offsetA = nextA; + offsetB = nextB; + offsetOrig = nextOrig; + } + + if (targetA === offsetA && targetB === offsetB) { + return offsetOrig; + } + + return null; +} + +function hasConflictMarkers(text) { + return CONFLICT_START_RE.test(text) || CONFLICT_END_RE.test(text); +} + +function detectNewline(text) { + return text.includes("\r\n") ? "\r\n" : "\n"; +} + +function ensureTrailingNewline(text, newline) { + return text.endsWith(newline) ? text : `${text}${newline}`; +} + +function offsetForLine(lines, lineIndex) { + if (lineIndex >= lines.length) { + return lines.length === 0 ? 0 : lines[lines.length - 1].end; + } + return lines[lineIndex].start; +} + +function collapseWhitespace(text) { + return text.replace(/\s+/g, " ").trim(); +} + +function unique(values) { + return [...new Set(values)]; +} diff --git a/users/clover/AGENTS.md b/users/clover/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..f3656226b2c41927d98de0484dd98df29191dca8 --- /dev/null +++ b/users/clover/AGENTS.md @@ -0,0 +1,55 @@ +# Clover's Preferences + +You are working with Clover, an intelligent web/systems engineer. Please avoid explaining things to her unless she asks and is curious. Her weak points are around writing good database queries and sometimes overcomplicating things. Make sure that she doesn't pull the session into an overcomplicated solution; not to say it's bad to do large reworks -- refactors can be complex but put the code at a simpler end state. You and her should plan strongly. + +## Responses + +- When writing output responses, please keep responses very dense. Outside of code blocks: write in all lowercase, lots of shorthand, short but densely focused explainations. Razor sharp accuracy. It's okay to mimic how Clover speaks to you. + - Terminology: something is "cooked" = bad, someone is "cooking" = good, gotta "lock in" = gotta focus, "locked in" = good, solid, or high quality, high/low "aura" = quality, "wtf" = what the fuck, "bait used to be believable" = when you find something surprising or wrong, that's "based" = good, "this is peak" / "peak design" / etc = good, "blow up" = remove / crash / errored, "nuke" = remove a large thing, "goated" = greatest of all time, "my live reaction" = my opinion, "disaster" = mistake, "holy shit" = surprise, "strat" = strategy, "propaganda" = fake information, "it's over" or "it's so over" = bad, "we're so back" = it's better now, "that's wraps" = it's even more over, "chat" = twitch live chat not chatgpt. + - Use the above terminologies ALL of the time, it's incredible. + - Verbiage that models tend to use; "[checking ground truth] instead of just guessing" -> "to make sure i'm not trolling" + - We have fun in our process, but write to the codebase professionally. +- Within `opencode`, everything is monospace, but markdown Headers, Bold, Italic, and inline/block Code blocks highlight different with different colors. Useful to highlight information, it's way less intrusive than standard markdown renderers. +- Never include a section on checks saying "pnpm test passed" or "checks passed" or whatever. Just make sure your validation is strong in the first place; You're expected to have that done. +- Feedback should be useful and actionable. A couple words is OK if there is genuinely nothing to add. + +## Comments + +Comments must always be placed and edited with intent to serialize codebase theory for future versions of ourselves and coworkers. They are professionally written, containing relevant context to the current iteration. Clover's comments are always timeless and rarely written from the first person (no `I`, `we`) -- refer directly to the code. Rewording comments this way helps solidify your own understanding, and can even reveal logic bugs in the designed systems. Prefer documentation comments over line comments. + +## Code Quality + +- Prefer fewer moving parts. Do not introduce helpers, methods, state fields, abstractions, or types unless they remove real duplication or encode a meaningful invariant. If a helper has one caller, inline it unless it makes a tricky operation substantially safer. +- Actively look for dead code and fake state while editing. If a value is always derived from another source, expose it as derived behavior instead of storing it. If a field is only written for type appeasement and never read, nuke it. +- Be suspicious of helper functions that hide one line of logic, especially wrappers around another object's property, trivial context builders, and "just in case" extension points. These are low aura unless they prevent a concrete bug. +- Before calling a change done, do one cleanup pass asking: can this be fewer names, fewer branches, fewer casts, fewer helper functions, or fewer public API promises? Prefer the smaller locked-in version. +- Type safety should constrain real runtime behavior. Avoid casts that paper over a mismatch; if a cast remains, it should be localized and justified by an actual API boundary. +- It is okay to spend more time to have a more correct response. + +## Jujutsu + +- My repos use Jujutsu and usually don't have a visible `.git` folder. Never run + `git`, and be cautious about jujutsu commands. Here are the standard commands. + By default, don't use any other mutating commands. + - `jj st` - show current commit + - `jj log -r @ -T description --no-graph` show the description for the current commit, which is important for review. consider running `jj st && jj log -r ...` to get both views in a single command. + - `jj diff --git`: diff current commit, can take a file/fileset - Drizzle + migrations are quite verbose. In alphaXiv (~/dev/1, ~/dev/2, etc), use + `jj diff '. ~ shared/db/migrations'` to subtract verbose migrations. + - `jj file show -r ` - read a file at a revision, such as + `main` or previous commit `@-` or change ID. + - `jj log` shows a set of commits +- When I say solve all merge conflicts, I mean: + 1. `jj st` to list files alongside the conflicts + 2. Resolve merge conflicts. Either edit manually or you could use + `jj restore --from ` to take one side. + 3. `jj st` to confirm no conflicts + project-specific checks + 4. You should resolve everything in the stack, check `jj log -r "@::"` to spot + future commits in the stack by me, then incrementally edit them with + `jj edit `. Always resolve bottom ones first, as that may + auto-resolve future merge conflicts. +- When I say "rebase ", I mean: + 1. `jj log -r ''`, observe what kind of commit. + 2. `jj git fetch`. If a parent commit was merged as a PR, then the parent will disappear and you'll have to rebase it onto main first or else you'll have incorrect conflicts. + 3. If it looks to be a pushed branch (clo/feature-name), then use `jj new main` to create a merge commit, otherwise just move the commit and its children with `jj rebase -s -d 'trunk()'` and `jj edit `. + 4. Solve merge conflicts if any arise following "solve all merge conflicts" rules. diff --git a/users/clover/home.nix b/users/clover/home.nix index 673e3b5cb39ed98b2ce92fea51331f2069b90d03..a6fc8a82376aaef174a2d556a500ef175b43c016 100644 --- a/users/clover/home.nix +++ b/users/clover/home.nix @@ -12,6 +12,7 @@ in stateVersion = "23.05"; # Please read the comment before changing. sessionPath = [ "$HOME/.vite-plus/bin" + "$HOME/.local/bin" ]; packages = with pkgs; @@ -23,8 +24,9 @@ in uv nh autofmt + pkgs."jj-import-merge" gnupg - nodejs_24 + just ]; # packages to install for desktop environments (non-server) desktop = [ @@ -39,6 +41,9 @@ in in all ++ (if host.darwin then darwin else linux) ++ (if hostServer then server else desktop); }; + + xdg.configFile."opencode/AGENTS.md".source = ./AGENTS.md; + programs = { # sort-lines:start # bat.enable = true; @@ -72,13 +77,29 @@ in jf = "jj git fetch"; e = "jj edit"; d = "jj desc"; - bup = "jj b m --from 'heads(::@- & bookmarks())' --to '@'"; - moveself = "jj rebase -r @ -d"; + bup = "jj b m --from 'heads(::@- & bookmarks()) ~ (master | bookmarks(glob:\"deployment*\"))' --to '@'"; + moveself = "jj rebase -s @ -d"; logall = "jj log -r 'all()'"; cherry = "jj git fetch && jj split -i -p -m 'SPLIT_CHANGES' && jj edit -r 'description(\"SPLIT_CHANGES\")' && jj desc -m '' --edit"; }; initContent = '' - export PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " + function _update_prompt_theme() { + local dark=false + if [[ "$OSTYPE" == darwin* ]]; then + [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null)" == "Dark" ]] && dark=true + else + local scheme + scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null) + [[ "$scheme" == *"dark"* ]] && dark=true + fi + if $dark; then + PS1="%{$(tput setaf 207)%}%m%{$(tput setaf 15)%}:%{$(tput setaf 87)%}%~ %{$(tput sgr0)%}$ " + else + PS1="%{$(tput setaf 168)%}%m%{$(tput setaf 238)%}:%{$(tput setaf 33)%}%~ %{$(tput sgr0)%}$ " + fi + } + precmd_functions+=(_update_prompt_theme) + _update_prompt_theme function python() { dirname=$(dirname $1 2>/dev/null) @@ -99,14 +120,45 @@ in fi } + function wrapWithAutoInstall() { + root="$(command jj workspace root)" + if [ -n "$root" ]; then + if [ -e "$root/bun.lock" ]; then + file="$root/bun.lock" + cmd="bun i -f" + elif [ -e "$root/package-lock.json" ]; then + file="$root/package-lock.json" + cmd="npm i" + elif [ -e "$root/pnpm-lock.yaml" ]; then + file="$root/pnpm-lock.yaml" + cmd="pnpm i" + elif [ -e "$root/Cargo.lock" ]; then + file="$root/Cargo.lock" + cmd="cargo fetch" + fi + fi + if [ -n "$file" ]; then + before="$(sha1sum "$file")" + fi + command "$@" + if [ -n "$file" ] && [ -e "$file" ]; then + after="$(sha1sum "$file")" + if [ "$after" != "$before" ]; then + eval "$cmd" + fi + fi + } + function jj() { if [[ "$*" = "git push"* ]] && [[ "$*" != *"--dry-run"* ]]; then - command jj "$@" --dry-run - printf "Press any key to continue... " - read -r - command jj "$@" - else - command jj "$@" + if command jj "$@" --dry-run; then + printf "Press any key to continue... " + read -r + command jj "$@" + fi + else if [[ "$*" != "undo" ]] || [[ "$*" != "redo" ]] + # auto-install deps when switching commits + wrapWithAutoInstall jj "$@" fi } ''; @@ -114,6 +166,7 @@ in git = { # use a git-specific email + ignores = [ "AGENTS.md" ]; settings.user.email = "git@paperclover.net"; settings.gpg.ssh.allowedSignersFile = "${pkgs.writeText "allowed_signers" '' git@paperclover.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAVa1aq1LEj/VXHJYlR5HJpK65lS/9jOL/dAB9ZS1chd @@ -142,15 +195,19 @@ in "working_copy remote_bookmarks" = colors."working_copy bookmark"; "author" = { - fg = "white"; + fg = "bright black"; bold = false; italic = true; }; "working_copy author" = colors.author; + committer = { + fg = "bright black"; + bold = false; + }; desc = { - bold = false; - fg = "bright white"; + bold = true; + fg = "default"; }; description = colors.desc; cc_wip = { @@ -160,7 +217,7 @@ in }; cc_header = { bold = true; - fg = "bright white"; + fg = "default"; }; cc_feat = "green"; cc_fix = "red"; @@ -179,6 +236,10 @@ in bold = true; reverse = true; }; + "change_id shortest rest" = { + fg = "bright black"; + bold = true; + }; conflict_marker = { fg = "bright red"; bold = true; @@ -186,6 +247,7 @@ in }; timestamp = "bright black"; + "committer timestamp" = colors.committer; commit_id = { fg = "bright black"; bold = false; diff --git a/users/clover/paperback/home.nix b/users/clover/paperback/home.nix index 6686bc71ada574880cc557bb0207d2d8cef33c11..26a293cf2031554ebe307f8b3f40f21595508ecd 100644 --- a/users/clover/paperback/home.nix +++ b/users/clover/paperback/home.nix @@ -1,23 +1,12 @@ { pkgs, ... }: { - # these programs are not globally installed to reduce distractions. - # most of these are needed for my work environment. - programs = { - zsh.profileExtra = '' - _bun() { - local context state line - _arguments -C '*:file:_files' - } - ''; - }; home.packages = with pkgs; [ - deno rustup typescript - pm2 pnpm yt-dlp spotdl zig + nodejs_24 ]; } diff --git a/users/clover/sandwich/home.nix b/users/clover/sandwich/home.nix new file mode 100644 index 0000000000000000000000000000000000000000..60fe8437d1309bd834f1e7b0f7852826c4f3285e --- /dev/null +++ b/users/clover/sandwich/home.nix @@ -0,0 +1,7 @@ +{ pkgs, ... }: +{ + + home.packages = with pkgs; [ + nodejs_24 + ]; +} diff --git a/users/clover/switch-admin.sh b/users/clover/switch-admin.sh new file mode 100755 index 0000000000000000000000000000000000000000..5ccb8d5aa54c63541bc4c965b07c8fcb219711da --- /dev/null +++ b/users/clover/switch-admin.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu + +cd /Users/clo/config + +target_host="${1:-}" +if [ -z "$target_host" ]; then + if [ -f .hostname ]; then + target_host="$(cat .hostname)" + else + target_host="$(hostname)" + fi +fi + +case "$target_host" in + '' | *[!A-Za-z0-9._-]*) + printf 'invalid host: %s\n' "$target_host" >&2 + exit 2 + ;; +esac + +if [ "$(uname -s)" != Darwin ]; then + exec ./switch "$target_host" +fi + +exec osascript -e "do shell script \"cd /Users/clo/config && /nix/var/nix/profiles/default/bin/nix run .#darwin-rebuild -- switch --flake .#$target_host\" with administrator privileges" diff --git a/users/clover/work/home.nix b/users/clover/work/home.nix index a6687ddcafbae66923c46d6cd449de1514f6eccc..14a2e1138b94fa2dd40cf806bc66e286aca82961 100644 --- a/users/clover/work/home.nix +++ b/users/clover/work/home.nix @@ -8,20 +8,24 @@ } ''; }; - programs.git.signing.key = "CE485807AE4068AF8508C8DC5CBB21841C7920AD"; home.packages = with pkgs; [ (bun.overrideAttrs (_: rec { - version = "1.3.0"; + version = "1.3.5"; src = fetchurl { url = "https://github.com/oven-sh/bun/releases/download/bun-v${version}/bun-darwin-aarch64.zip"; - hash = "sha256-hYSOP5ZIHvyr51pQD9O5S5u5VoaretCjiSl2x74VA2o="; + hash = "sha256-2xdYikrqiASFaCXUvq0/BeHzcnbKYG8342m09y810/s="; }; })) - doppler + awscli deno + doppler + nodejs_25 rustup - typescript ssm-session-manager-plugin - awscli + typescript + zig + ]; + home.sessionPath = [ + "$HOME/.opencode/bin" ]; } diff --git a/users/natalie/home.nix b/users/natalie/home.nix index 9b1db798d56f2814499e41da4ef75bdeb17e74a6..c59a8a0392a563ed6331e1976b54be3917bcfc32 100644 --- a/users/natalie/home.nix +++ b/users/natalie/home.nix @@ -17,12 +17,20 @@ fish.enable = true; man.generateCaches = false; lsd.enable = true; - android-sdk.enable = true; - # sort-lines:end + android-sdk.enable = false; ghostty = { enable = true; - settings.theme = "Catppuccin Mocha"; + package = null; + settings = { + theme = "light:catppuccin-latte,dark:catppuccin-macchiato"; + font-family = "AT Name Mono"; + adjust-cursor-thickness = 1; + minimum-contrast = 1.1; + background-opacity = 0.9; + background-blur = true; + }; }; + # sort-lines:end fish = { shellAliases = diff --git a/users/natalie/home/vicinae.nix b/users/natalie/home/vicinae.nix index 132ed839a7ca552bb1923bc5f6dbbe537c05cf3e..6dc91ed649bd1df50ebf4148fb369670b2623108 100644 --- a/users/natalie/home/vicinae.nix +++ b/users/natalie/home/vicinae.nix @@ -1,7 +1,13 @@ -{ inputs, pkgs, ... }: +{ + inputs, + pkgs, + lib, + host, + ... +}: { services.vicinae = { - enable = true; + enable = host.linux; systemd = { enable = true; autoStart = true; diff --git a/users/natalie/laptop/configuration.nix b/users/natalie/laptop/configuration.nix index 7e83946932a2f1cccc3d0c1a4e1e9b670eb7182b..6ad204a544a8351c5c28abedb90b896f59f8f803 100644 --- a/users/natalie/laptop/configuration.nix +++ b/users/natalie/laptop/configuration.nix @@ -7,7 +7,7 @@ # Custom configuration modules in "modules" are shared between users, # and can be configured in this "shared" namespace shared.darwin = { - macAppStoreApps = [ "wireguard" ]; +# macAppStoreApps = [ "wireguard" ]; }; system.defaults.NSGlobalDomain."com.apple.trackpad.scaling" = 1.0; @@ -40,12 +40,13 @@ casks = [ "battle-net" "stremio" - "alt-tab" +# "alt-tab" "legcord" "zulip" "zen" "supertuxkart" "sf-symbols" + "todoist" "mediosz/tap/swipeaerospace" ]; diff --git a/users/natalie/laptop/home.nix b/users/natalie/laptop/home.nix index 6974967997ae41ad67f2d9f53fe434226411dae3..8b2dd291b777b872af4efea6573f247a89a1efb6 100644 --- a/users/natalie/laptop/home.nix +++ b/users/natalie/laptop/home.nix @@ -8,6 +8,9 @@ ... }@args: { + programs.atuin = { + settings.data_dir = "/Users/${user.username}/.local/share/atuin"; + }; home = { stateVersion = "23.05"; # Please read the comment before changing. # shell = pkgs.fish; diff --git a/users/natalie/packages.nix b/users/natalie/packages.nix index 6352b5d863e0020d2e68f3cf5684bcd93ae80b29..78607611b27a1a0787fec2dac82ba22b9184a928 100644 --- a/users/natalie/packages.nix +++ b/users/natalie/packages.nix @@ -31,8 +31,6 @@ with pkgs; gtk4 #ides - zed-editor - emacs #virtualization qemu diff --git a/users/natalie/vim/languages.nix b/users/natalie/vim/languages.nix index 01029d427e0bba9193d8a17301d781a81e9c2d65..81a3e59daa0c66faa8d721b83e179fde15e8b911 100644 --- a/users/natalie/vim/languages.nix +++ b/users/natalie/vim/languages.nix @@ -106,6 +106,8 @@ scrollDocsDown = null; scrollDocsUp = null; }; + sourcePlugins = { + }; setupOpts = { keymap = { diff --git a/users/natalie/vim/latex.nix b/users/natalie/vim/latex.nix index 2d8dd7a728428b0b65c6c6de39d6aed8fb1091af..f6d735fe425a44541b5e75d8b82740cde31a4815 100644 --- a/users/natalie/vim/latex.nix +++ b/users/natalie/vim/latex.nix @@ -60,7 +60,9 @@ }; setupOpts = { sources = { - default = [ "vimtex" ]; + default = [ + "vimtex" + ]; providers = { vimtex = { name = "vimtex";