@austinthesing/magic-shell 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -25
- package/dist/cli.js +1724 -194
- package/dist/index.js +2154 -400
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -518,22 +518,128 @@ function getPlatformPaths(platform) {
|
|
|
518
518
|
}
|
|
519
519
|
var init_shell = () => {};
|
|
520
520
|
|
|
521
|
+
// src/lib/repo-context.ts
|
|
522
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
523
|
+
import { join as join2 } from "path";
|
|
524
|
+
function detectRepoContext(cwd) {
|
|
525
|
+
const context = {
|
|
526
|
+
type: "unknown"
|
|
527
|
+
};
|
|
528
|
+
let detected = false;
|
|
529
|
+
if (existsSync3(join2(cwd, ".git"))) {
|
|
530
|
+
context.hasGit = true;
|
|
531
|
+
detected = true;
|
|
532
|
+
}
|
|
533
|
+
if (existsSync3(join2(cwd, "Dockerfile")) || existsSync3(join2(cwd, "docker-compose.yml")) || existsSync3(join2(cwd, "docker-compose.yaml"))) {
|
|
534
|
+
context.hasDocker = true;
|
|
535
|
+
detected = true;
|
|
536
|
+
}
|
|
537
|
+
const packageJsonPath = join2(cwd, "package.json");
|
|
538
|
+
if (existsSync3(packageJsonPath)) {
|
|
539
|
+
detected = true;
|
|
540
|
+
context.type = "node";
|
|
541
|
+
try {
|
|
542
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
543
|
+
if (existsSync3(join2(cwd, "bun.lockb")) || existsSync3(join2(cwd, "bun.lock"))) {
|
|
544
|
+
context.packageManager = "bun";
|
|
545
|
+
} else if (existsSync3(join2(cwd, "pnpm-lock.yaml"))) {
|
|
546
|
+
context.packageManager = "pnpm";
|
|
547
|
+
} else if (existsSync3(join2(cwd, "yarn.lock"))) {
|
|
548
|
+
context.packageManager = "yarn";
|
|
549
|
+
} else if (existsSync3(join2(cwd, "package-lock.json"))) {
|
|
550
|
+
context.packageManager = "npm";
|
|
551
|
+
} else if (packageJson.packageManager) {
|
|
552
|
+
const pm = packageJson.packageManager.split("@")[0];
|
|
553
|
+
context.packageManager = pm;
|
|
554
|
+
}
|
|
555
|
+
if (packageJson.scripts && typeof packageJson.scripts === "object") {
|
|
556
|
+
context.scripts = Object.keys(packageJson.scripts);
|
|
557
|
+
}
|
|
558
|
+
} catch {}
|
|
559
|
+
}
|
|
560
|
+
const makefilePath = join2(cwd, "Makefile");
|
|
561
|
+
if (existsSync3(makefilePath)) {
|
|
562
|
+
detected = true;
|
|
563
|
+
if (context.type === "unknown")
|
|
564
|
+
context.type = "make";
|
|
565
|
+
try {
|
|
566
|
+
const makefile = readFileSync2(makefilePath, "utf-8");
|
|
567
|
+
const targetRegex = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:/gm;
|
|
568
|
+
const targets = [];
|
|
569
|
+
let match;
|
|
570
|
+
while ((match = targetRegex.exec(makefile)) !== null) {
|
|
571
|
+
if (!match[1].startsWith(".") && !match[1].startsWith("_")) {
|
|
572
|
+
targets.push(match[1]);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (targets.length > 0) {
|
|
576
|
+
context.makeTargets = [...new Set(targets)];
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
}
|
|
580
|
+
if (existsSync3(join2(cwd, "Cargo.toml"))) {
|
|
581
|
+
detected = true;
|
|
582
|
+
context.type = "rust";
|
|
583
|
+
context.cargoCommands = ["build", "run", "test", "check", "clippy", "fmt", "doc"];
|
|
584
|
+
}
|
|
585
|
+
if (existsSync3(join2(cwd, "pyproject.toml")) || existsSync3(join2(cwd, "setup.py")) || existsSync3(join2(cwd, "requirements.txt"))) {
|
|
586
|
+
detected = true;
|
|
587
|
+
if (context.type === "unknown")
|
|
588
|
+
context.type = "python";
|
|
589
|
+
}
|
|
590
|
+
if (existsSync3(join2(cwd, "go.mod"))) {
|
|
591
|
+
detected = true;
|
|
592
|
+
if (context.type === "unknown")
|
|
593
|
+
context.type = "go";
|
|
594
|
+
}
|
|
595
|
+
return detected ? context : null;
|
|
596
|
+
}
|
|
597
|
+
function formatRepoContext(context) {
|
|
598
|
+
const lines = [];
|
|
599
|
+
lines.push(`Project type: ${context.type}`);
|
|
600
|
+
if (context.packageManager) {
|
|
601
|
+
lines.push(`Package manager: ${context.packageManager}`);
|
|
602
|
+
}
|
|
603
|
+
if (context.scripts && context.scripts.length > 0) {
|
|
604
|
+
const displayScripts = context.scripts.slice(0, 15);
|
|
605
|
+
const suffix = context.scripts.length > 15 ? ` (+${context.scripts.length - 15} more)` : "";
|
|
606
|
+
lines.push(`Available scripts: ${displayScripts.join(", ")}${suffix}`);
|
|
607
|
+
}
|
|
608
|
+
if (context.makeTargets && context.makeTargets.length > 0) {
|
|
609
|
+
const displayTargets = context.makeTargets.slice(0, 15);
|
|
610
|
+
const suffix = context.makeTargets.length > 15 ? ` (+${context.makeTargets.length - 15} more)` : "";
|
|
611
|
+
lines.push(`Make targets: ${displayTargets.join(", ")}${suffix}`);
|
|
612
|
+
}
|
|
613
|
+
if (context.cargoCommands) {
|
|
614
|
+
lines.push(`Cargo commands: ${context.cargoCommands.join(", ")}`);
|
|
615
|
+
}
|
|
616
|
+
if (context.hasDocker) {
|
|
617
|
+
lines.push(`Docker: available`);
|
|
618
|
+
}
|
|
619
|
+
if (context.hasGit) {
|
|
620
|
+
lines.push(`Git: initialized`);
|
|
621
|
+
}
|
|
622
|
+
return lines.join(`
|
|
623
|
+
`);
|
|
624
|
+
}
|
|
625
|
+
var init_repo_context = () => {};
|
|
626
|
+
|
|
521
627
|
// src/lib/config.ts
|
|
522
628
|
import { homedir as homedir3 } from "os";
|
|
523
|
-
import { join as
|
|
524
|
-
import { existsSync as
|
|
629
|
+
import { join as join3 } from "path";
|
|
630
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
525
631
|
function ensureConfigDir2() {
|
|
526
|
-
if (!
|
|
632
|
+
if (!existsSync4(CONFIG_DIR2)) {
|
|
527
633
|
mkdirSync2(CONFIG_DIR2, { recursive: true });
|
|
528
634
|
}
|
|
529
635
|
}
|
|
530
636
|
function loadConfig2() {
|
|
531
637
|
ensureConfigDir2();
|
|
532
|
-
if (!
|
|
638
|
+
if (!existsSync4(CONFIG_FILE2)) {
|
|
533
639
|
return { ...DEFAULT_CONFIG2 };
|
|
534
640
|
}
|
|
535
641
|
try {
|
|
536
|
-
const data =
|
|
642
|
+
const data = readFileSync3(CONFIG_FILE2, "utf-8");
|
|
537
643
|
const loaded = JSON.parse(data);
|
|
538
644
|
return { ...DEFAULT_CONFIG2, ...loaded };
|
|
539
645
|
} catch {
|
|
@@ -582,11 +688,11 @@ async function setApiKey2(provider, key) {
|
|
|
582
688
|
}
|
|
583
689
|
function loadHistory2() {
|
|
584
690
|
ensureConfigDir2();
|
|
585
|
-
if (!
|
|
691
|
+
if (!existsSync4(HISTORY_FILE2)) {
|
|
586
692
|
return [];
|
|
587
693
|
}
|
|
588
694
|
try {
|
|
589
|
-
const data =
|
|
695
|
+
const data = readFileSync3(HISTORY_FILE2, "utf-8");
|
|
590
696
|
return JSON.parse(data);
|
|
591
697
|
} catch {
|
|
592
698
|
return [];
|
|
@@ -605,14 +711,14 @@ function addToHistory(entry) {
|
|
|
605
711
|
var CONFIG_DIR2, CONFIG_FILE2, HISTORY_FILE2, KEYCHAIN_OPENROUTER2 = "openrouter-api-key", KEYCHAIN_OPENCODE_ZEN2 = "opencode-zen-api-key", DEFAULT_CONFIG2;
|
|
606
712
|
var init_config = __esm(() => {
|
|
607
713
|
init_keychain();
|
|
608
|
-
CONFIG_DIR2 =
|
|
609
|
-
CONFIG_FILE2 =
|
|
610
|
-
HISTORY_FILE2 =
|
|
714
|
+
CONFIG_DIR2 = join3(homedir3(), ".magic-shell");
|
|
715
|
+
CONFIG_FILE2 = join3(CONFIG_DIR2, "config.json");
|
|
716
|
+
HISTORY_FILE2 = join3(CONFIG_DIR2, "history.json");
|
|
611
717
|
DEFAULT_CONFIG2 = {
|
|
612
718
|
provider: "opencode-zen",
|
|
613
719
|
openrouterApiKey: "",
|
|
614
720
|
opencodeZenApiKey: "",
|
|
615
|
-
defaultModel: "
|
|
721
|
+
defaultModel: "gemini-3-flash",
|
|
616
722
|
safetyLevel: "moderate",
|
|
617
723
|
dryRunByDefault: false,
|
|
618
724
|
blockedCommands: [
|
|
@@ -623,7 +729,8 @@ var init_config = __esm(() => {
|
|
|
623
729
|
"chmod -R 777 /",
|
|
624
730
|
"chown -R"
|
|
625
731
|
],
|
|
626
|
-
confirmedDangerousPatterns: []
|
|
732
|
+
confirmedDangerousPatterns: [],
|
|
733
|
+
repoContext: false
|
|
627
734
|
};
|
|
628
735
|
});
|
|
629
736
|
|
|
@@ -679,8 +786,8 @@ import { EventEmitter as EventEmitter3 } from "events";
|
|
|
679
786
|
import { resolve, dirname } from "path";
|
|
680
787
|
import { fileURLToPath } from "url";
|
|
681
788
|
import { resolve as resolve2, isAbsolute, parse } from "path";
|
|
682
|
-
import { existsSync as
|
|
683
|
-
import { basename, join as
|
|
789
|
+
import { existsSync as existsSync6 } from "fs";
|
|
790
|
+
import { basename, join as join5 } from "path";
|
|
684
791
|
import os from "os";
|
|
685
792
|
import path from "path";
|
|
686
793
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
@@ -1430,6 +1537,13 @@ function t(strings, ...values) {
|
|
|
1430
1537
|
}
|
|
1431
1538
|
return new StyledText(chunks);
|
|
1432
1539
|
}
|
|
1540
|
+
|
|
1541
|
+
class LinearScrollAccel {
|
|
1542
|
+
tick(_now) {
|
|
1543
|
+
return 1;
|
|
1544
|
+
}
|
|
1545
|
+
reset() {}
|
|
1546
|
+
}
|
|
1433
1547
|
function isCompleteSequence(data) {
|
|
1434
1548
|
if (!data.startsWith(ESC)) {
|
|
1435
1549
|
return "not-escape";
|
|
@@ -2035,7 +2149,7 @@ function getBunfsRootPath() {
|
|
|
2035
2149
|
return process.platform === "win32" ? "B:\\~BUN\\root" : "/$bunfs/root";
|
|
2036
2150
|
}
|
|
2037
2151
|
function normalizeBunfsPath(fileName) {
|
|
2038
|
-
return
|
|
2152
|
+
return join5(getBunfsRootPath(), basename(fileName));
|
|
2039
2153
|
}
|
|
2040
2154
|
function isValidDirectoryName(name) {
|
|
2041
2155
|
if (!name || typeof name !== "string") {
|
|
@@ -11894,7 +12008,7 @@ var init_index_93qf6w1k = __esm(async () => {
|
|
|
11894
12008
|
worker_path = this.options.workerPath;
|
|
11895
12009
|
} else {
|
|
11896
12010
|
worker_path = new URL("./parser.worker.js", import.meta.url).href;
|
|
11897
|
-
if (!
|
|
12011
|
+
if (!existsSync6(resolve2(import.meta.dirname, "parser.worker.js"))) {
|
|
11898
12012
|
worker_path = new URL("./parser.worker.ts", import.meta.url).href;
|
|
11899
12013
|
}
|
|
11900
12014
|
}
|
|
@@ -17249,7 +17363,7 @@ function canonicalize(obj, stack, replacementStack, replacer, key) {
|
|
|
17249
17363
|
}
|
|
17250
17364
|
return canonicalizedObj;
|
|
17251
17365
|
}
|
|
17252
|
-
var EditBuffer, engine, BoxRenderable, TextBufferRenderable, BrandedTextNodeRenderable, TextNodeRenderable, RootTextNodeRenderable, CharacterDiff, characterDiff, extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}", tokenizeIncludingWhitespace, WordDiff, wordDiff, WordsWithSpaceDiff, wordsWithSpaceDiff, LineDiff, lineDiff, SentenceDiff, sentenceDiff, CssDiff, cssDiff, JsonDiff, jsonDiff, ArrayDiff, arrayDiff, TextRenderable, defaultInputKeybindings, InputRenderableEvents, InputRenderable, defaultThumbBackgroundColor, defaultTrackBackgroundColor, defaultSelectKeybindings, SelectRenderableEvents, SelectRenderable, TabSelectRenderableEvents, EditBufferRenderable;
|
|
17366
|
+
var EditBuffer, engine, BoxRenderable, TextBufferRenderable, BrandedTextNodeRenderable, TextNodeRenderable, RootTextNodeRenderable, CharacterDiff, characterDiff, extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}", tokenizeIncludingWhitespace, WordDiff, wordDiff, WordsWithSpaceDiff, wordsWithSpaceDiff, LineDiff, lineDiff, SentenceDiff, sentenceDiff, CssDiff, cssDiff, JsonDiff, jsonDiff, ArrayDiff, arrayDiff, TextRenderable, defaultInputKeybindings, InputRenderableEvents, InputRenderable, defaultThumbBackgroundColor, defaultTrackBackgroundColor, SliderRenderable, ScrollBarRenderable, ArrowRenderable, ContentRenderable, ScrollBoxRenderable, defaultSelectKeybindings, SelectRenderableEvents, SelectRenderable, TabSelectRenderableEvents, EditBufferRenderable;
|
|
17253
17367
|
var init_core = __esm(async () => {
|
|
17254
17368
|
await init_index_93qf6w1k();
|
|
17255
17369
|
EditBuffer = class EditBuffer extends EventEmitter10 {
|
|
@@ -18968,220 +19082,1360 @@ var init_core = __esm(async () => {
|
|
|
18968
19082
|
};
|
|
18969
19083
|
defaultThumbBackgroundColor = RGBA.fromHex("#9a9ea3");
|
|
18970
19084
|
defaultTrackBackgroundColor = RGBA.fromHex("#252527");
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
{ name: "down", shift: true, action: "move-down-fast" },
|
|
18978
|
-
{ name: "return", action: "select-current" },
|
|
18979
|
-
{ name: "linefeed", action: "select-current" }
|
|
18980
|
-
];
|
|
18981
|
-
((SelectRenderableEvents2) => {
|
|
18982
|
-
SelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
|
|
18983
|
-
SelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
|
|
18984
|
-
})(SelectRenderableEvents ||= {});
|
|
18985
|
-
SelectRenderable = class SelectRenderable extends Renderable {
|
|
18986
|
-
_focusable = true;
|
|
18987
|
-
_options = [];
|
|
18988
|
-
_selectedIndex = 0;
|
|
18989
|
-
scrollOffset = 0;
|
|
18990
|
-
maxVisibleItems;
|
|
19085
|
+
SliderRenderable = class SliderRenderable extends Renderable {
|
|
19086
|
+
orientation;
|
|
19087
|
+
_value;
|
|
19088
|
+
_min;
|
|
19089
|
+
_max;
|
|
19090
|
+
_viewPortSize;
|
|
18991
19091
|
_backgroundColor;
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
_focusedTextColor;
|
|
18995
|
-
_selectedBackgroundColor;
|
|
18996
|
-
_selectedTextColor;
|
|
18997
|
-
_descriptionColor;
|
|
18998
|
-
_selectedDescriptionColor;
|
|
18999
|
-
_showScrollIndicator;
|
|
19000
|
-
_wrapSelection;
|
|
19001
|
-
_showDescription;
|
|
19002
|
-
_font;
|
|
19003
|
-
_itemSpacing;
|
|
19004
|
-
linesPerItem;
|
|
19005
|
-
fontHeight;
|
|
19006
|
-
_fastScrollStep;
|
|
19007
|
-
_keyBindingsMap;
|
|
19008
|
-
_keyAliasMap;
|
|
19009
|
-
_keyBindings;
|
|
19010
|
-
_defaultOptions = {
|
|
19011
|
-
backgroundColor: "transparent",
|
|
19012
|
-
textColor: "#FFFFFF",
|
|
19013
|
-
focusedBackgroundColor: "#1a1a1a",
|
|
19014
|
-
focusedTextColor: "#FFFFFF",
|
|
19015
|
-
selectedBackgroundColor: "#334455",
|
|
19016
|
-
selectedTextColor: "#FFFF00",
|
|
19017
|
-
selectedIndex: 0,
|
|
19018
|
-
descriptionColor: "#888888",
|
|
19019
|
-
selectedDescriptionColor: "#CCCCCC",
|
|
19020
|
-
showScrollIndicator: false,
|
|
19021
|
-
wrapSelection: false,
|
|
19022
|
-
showDescription: true,
|
|
19023
|
-
itemSpacing: 0,
|
|
19024
|
-
fastScrollStep: 5
|
|
19025
|
-
};
|
|
19092
|
+
_foregroundColor;
|
|
19093
|
+
_onChange;
|
|
19026
19094
|
constructor(ctx, options) {
|
|
19027
|
-
super(ctx, { ...options
|
|
19028
|
-
this.
|
|
19029
|
-
|
|
19030
|
-
this.
|
|
19031
|
-
this.
|
|
19032
|
-
this.
|
|
19033
|
-
this.
|
|
19034
|
-
this.
|
|
19035
|
-
this.
|
|
19036
|
-
this.
|
|
19037
|
-
this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
|
|
19038
|
-
this._font = options.font;
|
|
19039
|
-
this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
|
|
19040
|
-
this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
|
|
19041
|
-
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
|
|
19042
|
-
this.linesPerItem += this._itemSpacing;
|
|
19043
|
-
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
|
|
19044
|
-
this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor);
|
|
19045
|
-
this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor);
|
|
19046
|
-
this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor);
|
|
19047
|
-
this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor);
|
|
19048
|
-
this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep;
|
|
19049
|
-
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
|
|
19050
|
-
this._keyBindings = options.keyBindings || [];
|
|
19051
|
-
const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
|
|
19052
|
-
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
|
|
19053
|
-
this.requestRender();
|
|
19095
|
+
super(ctx, { flexShrink: 0, ...options });
|
|
19096
|
+
this.orientation = options.orientation;
|
|
19097
|
+
this._min = options.min ?? 0;
|
|
19098
|
+
this._max = options.max ?? 100;
|
|
19099
|
+
this._value = options.value ?? this._min;
|
|
19100
|
+
this._viewPortSize = options.viewPortSize ?? Math.max(1, (this._max - this._min) * 0.1);
|
|
19101
|
+
this._onChange = options.onChange;
|
|
19102
|
+
this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : defaultTrackBackgroundColor;
|
|
19103
|
+
this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : defaultThumbBackgroundColor;
|
|
19104
|
+
this.setupMouseHandling();
|
|
19054
19105
|
}
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19106
|
+
get value() {
|
|
19107
|
+
return this._value;
|
|
19108
|
+
}
|
|
19109
|
+
set value(newValue) {
|
|
19110
|
+
const clamped = Math.max(this._min, Math.min(this._max, newValue));
|
|
19111
|
+
if (clamped !== this._value) {
|
|
19112
|
+
this._value = clamped;
|
|
19113
|
+
this._onChange?.(clamped);
|
|
19114
|
+
this.emit("change", { value: clamped });
|
|
19115
|
+
this.requestRender();
|
|
19060
19116
|
}
|
|
19061
19117
|
}
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
this.
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
const contentHeight = this.height;
|
|
19071
|
-
const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems);
|
|
19072
|
-
for (let i = 0;i < visibleOptions.length; i++) {
|
|
19073
|
-
const actualIndex = this.scrollOffset + i;
|
|
19074
|
-
const option = visibleOptions[i];
|
|
19075
|
-
const isSelected = actualIndex === this._selectedIndex;
|
|
19076
|
-
const itemY = contentY + i * this.linesPerItem;
|
|
19077
|
-
if (itemY + this.linesPerItem - 1 >= contentY + contentHeight)
|
|
19078
|
-
break;
|
|
19079
|
-
if (isSelected) {
|
|
19080
|
-
const contentHeight2 = this.linesPerItem - this._itemSpacing;
|
|
19081
|
-
this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
|
|
19082
|
-
}
|
|
19083
|
-
const nameContent = `${isSelected ? "▶ " : " "}${option.name}`;
|
|
19084
|
-
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
19085
|
-
const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
|
|
19086
|
-
let descX = contentX + 3;
|
|
19087
|
-
if (this._font) {
|
|
19088
|
-
const indicator = isSelected ? "▶ " : " ";
|
|
19089
|
-
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
|
|
19090
|
-
const indicatorWidth = 2;
|
|
19091
|
-
renderFontToFrameBuffer(this.frameBuffer, {
|
|
19092
|
-
text: option.name,
|
|
19093
|
-
x: contentX + 1 + indicatorWidth,
|
|
19094
|
-
y: itemY,
|
|
19095
|
-
color: nameColor,
|
|
19096
|
-
backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
|
|
19097
|
-
font: this._font
|
|
19098
|
-
});
|
|
19099
|
-
descX = contentX + 1 + indicatorWidth;
|
|
19100
|
-
} else {
|
|
19101
|
-
this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
|
|
19118
|
+
get min() {
|
|
19119
|
+
return this._min;
|
|
19120
|
+
}
|
|
19121
|
+
set min(newMin) {
|
|
19122
|
+
if (newMin !== this._min) {
|
|
19123
|
+
this._min = newMin;
|
|
19124
|
+
if (this._value < newMin) {
|
|
19125
|
+
this.value = newMin;
|
|
19102
19126
|
}
|
|
19103
|
-
|
|
19104
|
-
|
|
19105
|
-
|
|
19127
|
+
this.requestRender();
|
|
19128
|
+
}
|
|
19129
|
+
}
|
|
19130
|
+
get max() {
|
|
19131
|
+
return this._max;
|
|
19132
|
+
}
|
|
19133
|
+
set max(newMax) {
|
|
19134
|
+
if (newMax !== this._max) {
|
|
19135
|
+
this._max = newMax;
|
|
19136
|
+
if (this._value > newMax) {
|
|
19137
|
+
this.value = newMax;
|
|
19106
19138
|
}
|
|
19139
|
+
this.requestRender();
|
|
19107
19140
|
}
|
|
19108
|
-
|
|
19109
|
-
|
|
19141
|
+
}
|
|
19142
|
+
set viewPortSize(size) {
|
|
19143
|
+
const clampedSize = Math.max(0.01, Math.min(size, this._max - this._min));
|
|
19144
|
+
if (clampedSize !== this._viewPortSize) {
|
|
19145
|
+
this._viewPortSize = clampedSize;
|
|
19146
|
+
this.requestRender();
|
|
19110
19147
|
}
|
|
19111
19148
|
}
|
|
19112
|
-
|
|
19113
|
-
|
|
19114
|
-
return;
|
|
19115
|
-
const scrollPercent = this._selectedIndex / Math.max(1, this._options.length - 1);
|
|
19116
|
-
const indicatorHeight = Math.max(1, contentHeight - 2);
|
|
19117
|
-
const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight);
|
|
19118
|
-
const indicatorX = contentX + contentWidth - 1;
|
|
19119
|
-
this.frameBuffer.drawText("█", indicatorX, indicatorY, parseColor("#666666"));
|
|
19149
|
+
get viewPortSize() {
|
|
19150
|
+
return this._viewPortSize;
|
|
19120
19151
|
}
|
|
19121
|
-
get
|
|
19122
|
-
return this.
|
|
19152
|
+
get backgroundColor() {
|
|
19153
|
+
return this._backgroundColor;
|
|
19123
19154
|
}
|
|
19124
|
-
set
|
|
19125
|
-
this.
|
|
19126
|
-
this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1));
|
|
19127
|
-
this.updateScrollOffset();
|
|
19155
|
+
set backgroundColor(value) {
|
|
19156
|
+
this._backgroundColor = parseColor(value);
|
|
19128
19157
|
this.requestRender();
|
|
19129
19158
|
}
|
|
19130
|
-
|
|
19131
|
-
return this.
|
|
19159
|
+
get foregroundColor() {
|
|
19160
|
+
return this._foregroundColor;
|
|
19132
19161
|
}
|
|
19133
|
-
|
|
19134
|
-
|
|
19162
|
+
set foregroundColor(value) {
|
|
19163
|
+
this._foregroundColor = parseColor(value);
|
|
19164
|
+
this.requestRender();
|
|
19135
19165
|
}
|
|
19136
|
-
|
|
19137
|
-
const
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19166
|
+
calculateDragOffsetVirtual(event) {
|
|
19167
|
+
const trackStart = this.orientation === "vertical" ? this.y : this.x;
|
|
19168
|
+
const mousePos = (this.orientation === "vertical" ? event.y : event.x) - trackStart;
|
|
19169
|
+
const virtualMousePos = Math.max(0, Math.min((this.orientation === "vertical" ? this.height : this.width) * 2, mousePos * 2));
|
|
19170
|
+
const virtualThumbStart = this.getVirtualThumbStart();
|
|
19171
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19172
|
+
return Math.max(0, Math.min(virtualThumbSize, virtualMousePos - virtualThumbStart));
|
|
19173
|
+
}
|
|
19174
|
+
setupMouseHandling() {
|
|
19175
|
+
let isDragging = false;
|
|
19176
|
+
let dragOffsetVirtual = 0;
|
|
19177
|
+
this.onMouseDown = (event) => {
|
|
19178
|
+
event.stopPropagation();
|
|
19179
|
+
event.preventDefault();
|
|
19180
|
+
const thumb = this.getThumbRect();
|
|
19181
|
+
const inThumb = event.x >= thumb.x && event.x < thumb.x + thumb.width && event.y >= thumb.y && event.y < thumb.y + thumb.height;
|
|
19182
|
+
if (inThumb) {
|
|
19183
|
+
isDragging = true;
|
|
19184
|
+
dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
|
|
19185
|
+
} else {
|
|
19186
|
+
this.updateValueFromMouseDirect(event);
|
|
19187
|
+
isDragging = true;
|
|
19188
|
+
dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
|
|
19189
|
+
}
|
|
19190
|
+
};
|
|
19191
|
+
this.onMouseDrag = (event) => {
|
|
19192
|
+
if (!isDragging)
|
|
19193
|
+
return;
|
|
19194
|
+
event.stopPropagation();
|
|
19195
|
+
this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
|
|
19196
|
+
};
|
|
19197
|
+
this.onMouseUp = (event) => {
|
|
19198
|
+
if (isDragging) {
|
|
19199
|
+
this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
|
|
19200
|
+
}
|
|
19201
|
+
isDragging = false;
|
|
19202
|
+
};
|
|
19203
|
+
}
|
|
19204
|
+
updateValueFromMouseDirect(event) {
|
|
19205
|
+
const trackStart = this.orientation === "vertical" ? this.y : this.x;
|
|
19206
|
+
const trackSize = this.orientation === "vertical" ? this.height : this.width;
|
|
19207
|
+
const mousePos = this.orientation === "vertical" ? event.y : event.x;
|
|
19208
|
+
const relativeMousePos = mousePos - trackStart;
|
|
19209
|
+
const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
|
|
19210
|
+
const ratio = trackSize === 0 ? 0 : clampedMousePos / trackSize;
|
|
19211
|
+
const range = this._max - this._min;
|
|
19212
|
+
const newValue = this._min + ratio * range;
|
|
19213
|
+
this.value = newValue;
|
|
19214
|
+
}
|
|
19215
|
+
updateValueFromMouseWithOffset(event, offsetVirtual) {
|
|
19216
|
+
const trackStart = this.orientation === "vertical" ? this.y : this.x;
|
|
19217
|
+
const trackSize = this.orientation === "vertical" ? this.height : this.width;
|
|
19218
|
+
const mousePos = this.orientation === "vertical" ? event.y : event.x;
|
|
19219
|
+
const virtualTrackSize = trackSize * 2;
|
|
19220
|
+
const relativeMousePos = mousePos - trackStart;
|
|
19221
|
+
const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
|
|
19222
|
+
const virtualMousePos = clampedMousePos * 2;
|
|
19223
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19224
|
+
const maxThumbStart = Math.max(0, virtualTrackSize - virtualThumbSize);
|
|
19225
|
+
let desiredThumbStart = virtualMousePos - offsetVirtual;
|
|
19226
|
+
desiredThumbStart = Math.max(0, Math.min(maxThumbStart, desiredThumbStart));
|
|
19227
|
+
const ratio = maxThumbStart === 0 ? 0 : desiredThumbStart / maxThumbStart;
|
|
19228
|
+
const range = this._max - this._min;
|
|
19229
|
+
const newValue = this._min + ratio * range;
|
|
19230
|
+
this.value = newValue;
|
|
19231
|
+
}
|
|
19232
|
+
getThumbRect() {
|
|
19233
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19234
|
+
const virtualThumbStart = this.getVirtualThumbStart();
|
|
19235
|
+
const realThumbStart = Math.floor(virtualThumbStart / 2);
|
|
19236
|
+
const realThumbSize = Math.ceil((virtualThumbStart + virtualThumbSize) / 2) - realThumbStart;
|
|
19237
|
+
if (this.orientation === "vertical") {
|
|
19238
|
+
return {
|
|
19239
|
+
x: this.x,
|
|
19240
|
+
y: this.y + realThumbStart,
|
|
19241
|
+
width: this.width,
|
|
19242
|
+
height: Math.max(1, realThumbSize)
|
|
19243
|
+
};
|
|
19142
19244
|
} else {
|
|
19143
|
-
|
|
19245
|
+
return {
|
|
19246
|
+
x: this.x + realThumbStart,
|
|
19247
|
+
y: this.y,
|
|
19248
|
+
width: Math.max(1, realThumbSize),
|
|
19249
|
+
height: this.height
|
|
19250
|
+
};
|
|
19144
19251
|
}
|
|
19145
|
-
this.updateScrollOffset();
|
|
19146
|
-
this.requestRender();
|
|
19147
|
-
this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
|
|
19148
19252
|
}
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
this._selectedIndex = newIndex;
|
|
19153
|
-
} else if (this._wrapSelection && this._options.length > 0) {
|
|
19154
|
-
this._selectedIndex = 0;
|
|
19253
|
+
renderSelf(buffer) {
|
|
19254
|
+
if (this.orientation === "horizontal") {
|
|
19255
|
+
this.renderHorizontal(buffer);
|
|
19155
19256
|
} else {
|
|
19156
|
-
this.
|
|
19257
|
+
this.renderVertical(buffer);
|
|
19258
|
+
}
|
|
19259
|
+
}
|
|
19260
|
+
renderHorizontal(buffer) {
|
|
19261
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19262
|
+
const virtualThumbStart = this.getVirtualThumbStart();
|
|
19263
|
+
const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
|
|
19264
|
+
buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
|
|
19265
|
+
const realStartCell = Math.floor(virtualThumbStart / 2);
|
|
19266
|
+
const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
|
|
19267
|
+
const startX = Math.max(0, realStartCell);
|
|
19268
|
+
const endX = Math.min(this.width - 1, realEndCell);
|
|
19269
|
+
for (let realX = startX;realX <= endX; realX++) {
|
|
19270
|
+
const virtualCellStart = realX * 2;
|
|
19271
|
+
const virtualCellEnd = virtualCellStart + 2;
|
|
19272
|
+
const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
|
|
19273
|
+
const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
|
|
19274
|
+
const coverage = thumbEndInCell - thumbStartInCell;
|
|
19275
|
+
let char = " ";
|
|
19276
|
+
if (coverage >= 2) {
|
|
19277
|
+
char = "█";
|
|
19278
|
+
} else {
|
|
19279
|
+
const isLeftHalf = thumbStartInCell === virtualCellStart;
|
|
19280
|
+
if (isLeftHalf) {
|
|
19281
|
+
char = "▌";
|
|
19282
|
+
} else {
|
|
19283
|
+
char = "▐";
|
|
19284
|
+
}
|
|
19285
|
+
}
|
|
19286
|
+
for (let y = 0;y < this.height; y++) {
|
|
19287
|
+
buffer.setCellWithAlphaBlending(this.x + realX, this.y + y, char, this._foregroundColor, this._backgroundColor);
|
|
19288
|
+
}
|
|
19157
19289
|
}
|
|
19158
|
-
this.updateScrollOffset();
|
|
19159
|
-
this.requestRender();
|
|
19160
|
-
this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
|
|
19161
19290
|
}
|
|
19162
|
-
|
|
19163
|
-
const
|
|
19164
|
-
|
|
19165
|
-
|
|
19291
|
+
renderVertical(buffer) {
|
|
19292
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19293
|
+
const virtualThumbStart = this.getVirtualThumbStart();
|
|
19294
|
+
const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
|
|
19295
|
+
buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
|
|
19296
|
+
const realStartCell = Math.floor(virtualThumbStart / 2);
|
|
19297
|
+
const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
|
|
19298
|
+
const startY = Math.max(0, realStartCell);
|
|
19299
|
+
const endY = Math.min(this.height - 1, realEndCell);
|
|
19300
|
+
for (let realY = startY;realY <= endY; realY++) {
|
|
19301
|
+
const virtualCellStart = realY * 2;
|
|
19302
|
+
const virtualCellEnd = virtualCellStart + 2;
|
|
19303
|
+
const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
|
|
19304
|
+
const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
|
|
19305
|
+
const coverage = thumbEndInCell - thumbStartInCell;
|
|
19306
|
+
let char = " ";
|
|
19307
|
+
if (coverage >= 2) {
|
|
19308
|
+
char = "█";
|
|
19309
|
+
} else if (coverage > 0) {
|
|
19310
|
+
const virtualPositionInCell = thumbStartInCell - virtualCellStart;
|
|
19311
|
+
if (virtualPositionInCell === 0) {
|
|
19312
|
+
char = "▀";
|
|
19313
|
+
} else {
|
|
19314
|
+
char = "▄";
|
|
19315
|
+
}
|
|
19316
|
+
}
|
|
19317
|
+
for (let x = 0;x < this.width; x++) {
|
|
19318
|
+
buffer.setCellWithAlphaBlending(this.x + x, this.y + realY, char, this._foregroundColor, this._backgroundColor);
|
|
19319
|
+
}
|
|
19166
19320
|
}
|
|
19167
19321
|
}
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19173
|
-
|
|
19174
|
-
|
|
19322
|
+
getVirtualThumbSize() {
|
|
19323
|
+
const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
|
|
19324
|
+
const range = this._max - this._min;
|
|
19325
|
+
if (range === 0)
|
|
19326
|
+
return virtualTrackSize;
|
|
19327
|
+
const viewportSize = Math.max(1, this._viewPortSize);
|
|
19328
|
+
const contentSize = range + viewportSize;
|
|
19329
|
+
if (contentSize <= viewportSize)
|
|
19330
|
+
return virtualTrackSize;
|
|
19331
|
+
const thumbRatio = viewportSize / contentSize;
|
|
19332
|
+
const calculatedSize = Math.floor(virtualTrackSize * thumbRatio);
|
|
19333
|
+
return Math.max(1, Math.min(calculatedSize, virtualTrackSize));
|
|
19334
|
+
}
|
|
19335
|
+
getVirtualThumbStart() {
|
|
19336
|
+
const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
|
|
19337
|
+
const range = this._max - this._min;
|
|
19338
|
+
if (range === 0)
|
|
19339
|
+
return 0;
|
|
19340
|
+
const valueRatio = (this._value - this._min) / range;
|
|
19341
|
+
const virtualThumbSize = this.getVirtualThumbSize();
|
|
19342
|
+
return Math.round(valueRatio * (virtualTrackSize - virtualThumbSize));
|
|
19175
19343
|
}
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
|
|
19180
|
-
|
|
19181
|
-
|
|
19182
|
-
|
|
19183
|
-
|
|
19184
|
-
|
|
19344
|
+
};
|
|
19345
|
+
ScrollBarRenderable = class ScrollBarRenderable extends Renderable {
|
|
19346
|
+
slider;
|
|
19347
|
+
startArrow;
|
|
19348
|
+
endArrow;
|
|
19349
|
+
orientation;
|
|
19350
|
+
_focusable = true;
|
|
19351
|
+
_scrollSize = 0;
|
|
19352
|
+
_scrollPosition = 0;
|
|
19353
|
+
_viewportSize = 0;
|
|
19354
|
+
_showArrows = false;
|
|
19355
|
+
_manualVisibility = false;
|
|
19356
|
+
_onChange;
|
|
19357
|
+
scrollStep = null;
|
|
19358
|
+
get visible() {
|
|
19359
|
+
return super.visible;
|
|
19360
|
+
}
|
|
19361
|
+
set visible(value) {
|
|
19362
|
+
this._manualVisibility = true;
|
|
19363
|
+
super.visible = value;
|
|
19364
|
+
}
|
|
19365
|
+
resetVisibilityControl() {
|
|
19366
|
+
this._manualVisibility = false;
|
|
19367
|
+
this.recalculateVisibility();
|
|
19368
|
+
}
|
|
19369
|
+
get scrollSize() {
|
|
19370
|
+
return this._scrollSize;
|
|
19371
|
+
}
|
|
19372
|
+
get scrollPosition() {
|
|
19373
|
+
return this._scrollPosition;
|
|
19374
|
+
}
|
|
19375
|
+
get viewportSize() {
|
|
19376
|
+
return this._viewportSize;
|
|
19377
|
+
}
|
|
19378
|
+
set scrollSize(value) {
|
|
19379
|
+
if (value === this.scrollSize)
|
|
19380
|
+
return;
|
|
19381
|
+
this._scrollSize = value;
|
|
19382
|
+
this.recalculateVisibility();
|
|
19383
|
+
this.updateSliderFromScrollState();
|
|
19384
|
+
this.scrollPosition = this.scrollPosition;
|
|
19385
|
+
}
|
|
19386
|
+
set scrollPosition(value) {
|
|
19387
|
+
const newPosition = Math.round(Math.min(Math.max(0, value), this.scrollSize - this.viewportSize));
|
|
19388
|
+
if (newPosition !== this._scrollPosition) {
|
|
19389
|
+
this._scrollPosition = newPosition;
|
|
19390
|
+
this.updateSliderFromScrollState();
|
|
19391
|
+
}
|
|
19392
|
+
}
|
|
19393
|
+
set viewportSize(value) {
|
|
19394
|
+
if (value === this.viewportSize)
|
|
19395
|
+
return;
|
|
19396
|
+
this._viewportSize = value;
|
|
19397
|
+
this.slider.viewPortSize = Math.max(1, this._viewportSize);
|
|
19398
|
+
this.recalculateVisibility();
|
|
19399
|
+
this.updateSliderFromScrollState();
|
|
19400
|
+
this.scrollPosition = this.scrollPosition;
|
|
19401
|
+
}
|
|
19402
|
+
get showArrows() {
|
|
19403
|
+
return this._showArrows;
|
|
19404
|
+
}
|
|
19405
|
+
set showArrows(value) {
|
|
19406
|
+
if (value === this._showArrows)
|
|
19407
|
+
return;
|
|
19408
|
+
this._showArrows = value;
|
|
19409
|
+
this.startArrow.visible = value;
|
|
19410
|
+
this.endArrow.visible = value;
|
|
19411
|
+
}
|
|
19412
|
+
constructor(ctx, { trackOptions, arrowOptions, orientation, showArrows = false, ...options }) {
|
|
19413
|
+
super(ctx, {
|
|
19414
|
+
flexDirection: orientation === "vertical" ? "column" : "row",
|
|
19415
|
+
alignSelf: "stretch",
|
|
19416
|
+
alignItems: "stretch",
|
|
19417
|
+
...options
|
|
19418
|
+
});
|
|
19419
|
+
this._onChange = options.onChange;
|
|
19420
|
+
this.orientation = orientation;
|
|
19421
|
+
this._showArrows = showArrows;
|
|
19422
|
+
const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
|
|
19423
|
+
const defaultStepSize = Math.max(1, this._viewportSize);
|
|
19424
|
+
const stepSize = trackOptions?.viewPortSize ?? defaultStepSize;
|
|
19425
|
+
this.slider = new SliderRenderable(ctx, {
|
|
19426
|
+
orientation,
|
|
19427
|
+
min: 0,
|
|
19428
|
+
max: scrollRange,
|
|
19429
|
+
value: this._scrollPosition,
|
|
19430
|
+
viewPortSize: stepSize,
|
|
19431
|
+
onChange: (value) => {
|
|
19432
|
+
this._scrollPosition = Math.round(value);
|
|
19433
|
+
this._onChange?.(this._scrollPosition);
|
|
19434
|
+
this.emit("change", { position: this._scrollPosition });
|
|
19435
|
+
},
|
|
19436
|
+
...orientation === "vertical" ? {
|
|
19437
|
+
width: Math.max(1, Math.min(2, this.width)),
|
|
19438
|
+
height: "100%",
|
|
19439
|
+
marginLeft: "auto"
|
|
19440
|
+
} : {
|
|
19441
|
+
width: "100%",
|
|
19442
|
+
height: 1,
|
|
19443
|
+
marginTop: "auto"
|
|
19444
|
+
},
|
|
19445
|
+
flexGrow: 1,
|
|
19446
|
+
flexShrink: 1,
|
|
19447
|
+
...trackOptions
|
|
19448
|
+
});
|
|
19449
|
+
this.updateSliderFromScrollState();
|
|
19450
|
+
const arrowOpts = arrowOptions ? {
|
|
19451
|
+
foregroundColor: arrowOptions.backgroundColor,
|
|
19452
|
+
backgroundColor: arrowOptions.backgroundColor,
|
|
19453
|
+
attributes: arrowOptions.attributes,
|
|
19454
|
+
...arrowOptions
|
|
19455
|
+
} : {};
|
|
19456
|
+
this.startArrow = new ArrowRenderable(ctx, {
|
|
19457
|
+
alignSelf: "center",
|
|
19458
|
+
visible: this.showArrows,
|
|
19459
|
+
direction: this.orientation === "vertical" ? "up" : "left",
|
|
19460
|
+
height: this.orientation === "vertical" ? 1 : 1,
|
|
19461
|
+
...arrowOpts
|
|
19462
|
+
});
|
|
19463
|
+
this.endArrow = new ArrowRenderable(ctx, {
|
|
19464
|
+
alignSelf: "center",
|
|
19465
|
+
visible: this.showArrows,
|
|
19466
|
+
direction: this.orientation === "vertical" ? "down" : "right",
|
|
19467
|
+
height: this.orientation === "vertical" ? 1 : 1,
|
|
19468
|
+
...arrowOpts
|
|
19469
|
+
});
|
|
19470
|
+
this.add(this.startArrow);
|
|
19471
|
+
this.add(this.slider);
|
|
19472
|
+
this.add(this.endArrow);
|
|
19473
|
+
let startArrowMouseTimeout = undefined;
|
|
19474
|
+
let endArrowMouseTimeout = undefined;
|
|
19475
|
+
this.startArrow.onMouseDown = (event) => {
|
|
19476
|
+
event.stopPropagation();
|
|
19477
|
+
event.preventDefault();
|
|
19478
|
+
this.scrollBy(-0.5, "viewport");
|
|
19479
|
+
startArrowMouseTimeout = setTimeout(() => {
|
|
19480
|
+
this.scrollBy(-0.5, "viewport");
|
|
19481
|
+
startArrowMouseTimeout = setInterval(() => {
|
|
19482
|
+
this.scrollBy(-0.2, "viewport");
|
|
19483
|
+
}, 200);
|
|
19484
|
+
}, 500);
|
|
19485
|
+
};
|
|
19486
|
+
this.startArrow.onMouseUp = (event) => {
|
|
19487
|
+
event.stopPropagation();
|
|
19488
|
+
clearInterval(startArrowMouseTimeout);
|
|
19489
|
+
};
|
|
19490
|
+
this.endArrow.onMouseDown = (event) => {
|
|
19491
|
+
event.stopPropagation();
|
|
19492
|
+
event.preventDefault();
|
|
19493
|
+
this.scrollBy(0.5, "viewport");
|
|
19494
|
+
endArrowMouseTimeout = setTimeout(() => {
|
|
19495
|
+
this.scrollBy(0.5, "viewport");
|
|
19496
|
+
endArrowMouseTimeout = setInterval(() => {
|
|
19497
|
+
this.scrollBy(0.2, "viewport");
|
|
19498
|
+
}, 200);
|
|
19499
|
+
}, 500);
|
|
19500
|
+
};
|
|
19501
|
+
this.endArrow.onMouseUp = (event) => {
|
|
19502
|
+
event.stopPropagation();
|
|
19503
|
+
clearInterval(endArrowMouseTimeout);
|
|
19504
|
+
};
|
|
19505
|
+
}
|
|
19506
|
+
set arrowOptions(options) {
|
|
19507
|
+
Object.assign(this.startArrow, options);
|
|
19508
|
+
Object.assign(this.endArrow, options);
|
|
19509
|
+
this.requestRender();
|
|
19510
|
+
}
|
|
19511
|
+
set trackOptions(options) {
|
|
19512
|
+
Object.assign(this.slider, options);
|
|
19513
|
+
this.requestRender();
|
|
19514
|
+
}
|
|
19515
|
+
updateSliderFromScrollState() {
|
|
19516
|
+
const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
|
|
19517
|
+
this.slider.min = 0;
|
|
19518
|
+
this.slider.max = scrollRange;
|
|
19519
|
+
this.slider.value = Math.min(this._scrollPosition, scrollRange);
|
|
19520
|
+
}
|
|
19521
|
+
scrollBy(delta, unit = "absolute") {
|
|
19522
|
+
const multiplier = unit === "viewport" ? this.viewportSize : unit === "content" ? this.scrollSize : unit === "step" ? this.scrollStep ?? 1 : 1;
|
|
19523
|
+
const resolvedDelta = multiplier * delta;
|
|
19524
|
+
this.scrollPosition += resolvedDelta;
|
|
19525
|
+
}
|
|
19526
|
+
recalculateVisibility() {
|
|
19527
|
+
if (!this._manualVisibility) {
|
|
19528
|
+
const sizeRatio = this.scrollSize <= this.viewportSize ? 1 : this.viewportSize / this.scrollSize;
|
|
19529
|
+
super.visible = sizeRatio < 1;
|
|
19530
|
+
}
|
|
19531
|
+
}
|
|
19532
|
+
handleKeyPress(key) {
|
|
19533
|
+
switch (key.name) {
|
|
19534
|
+
case "left":
|
|
19535
|
+
case "h":
|
|
19536
|
+
if (this.orientation !== "horizontal")
|
|
19537
|
+
return false;
|
|
19538
|
+
this.scrollBy(-1 / 5, "viewport");
|
|
19539
|
+
return true;
|
|
19540
|
+
case "right":
|
|
19541
|
+
case "l":
|
|
19542
|
+
if (this.orientation !== "horizontal")
|
|
19543
|
+
return false;
|
|
19544
|
+
this.scrollBy(1 / 5, "viewport");
|
|
19545
|
+
return true;
|
|
19546
|
+
case "up":
|
|
19547
|
+
case "k":
|
|
19548
|
+
if (this.orientation !== "vertical")
|
|
19549
|
+
return false;
|
|
19550
|
+
this.scrollBy(-1 / 5, "viewport");
|
|
19551
|
+
return true;
|
|
19552
|
+
case "down":
|
|
19553
|
+
case "j":
|
|
19554
|
+
if (this.orientation !== "vertical")
|
|
19555
|
+
return false;
|
|
19556
|
+
this.scrollBy(1 / 5, "viewport");
|
|
19557
|
+
return true;
|
|
19558
|
+
case "pageup":
|
|
19559
|
+
this.scrollBy(-1 / 2, "viewport");
|
|
19560
|
+
return true;
|
|
19561
|
+
case "pagedown":
|
|
19562
|
+
this.scrollBy(1 / 2, "viewport");
|
|
19563
|
+
return true;
|
|
19564
|
+
case "home":
|
|
19565
|
+
this.scrollBy(-1, "content");
|
|
19566
|
+
return true;
|
|
19567
|
+
case "end":
|
|
19568
|
+
this.scrollBy(1, "content");
|
|
19569
|
+
return true;
|
|
19570
|
+
}
|
|
19571
|
+
return false;
|
|
19572
|
+
}
|
|
19573
|
+
};
|
|
19574
|
+
ArrowRenderable = class ArrowRenderable extends Renderable {
|
|
19575
|
+
_direction;
|
|
19576
|
+
_foregroundColor;
|
|
19577
|
+
_backgroundColor;
|
|
19578
|
+
_attributes;
|
|
19579
|
+
_arrowChars;
|
|
19580
|
+
constructor(ctx, options) {
|
|
19581
|
+
super(ctx, options);
|
|
19582
|
+
this._direction = options.direction;
|
|
19583
|
+
this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : RGBA.fromValues(1, 1, 1, 1);
|
|
19584
|
+
this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : RGBA.fromValues(0, 0, 0, 0);
|
|
19585
|
+
this._attributes = options.attributes ?? 0;
|
|
19586
|
+
this._arrowChars = {
|
|
19587
|
+
up: "▲",
|
|
19588
|
+
down: "▼",
|
|
19589
|
+
left: "◀",
|
|
19590
|
+
right: "▶",
|
|
19591
|
+
...options.arrowChars
|
|
19592
|
+
};
|
|
19593
|
+
if (!options.width) {
|
|
19594
|
+
this.width = Bun.stringWidth(this.getArrowChar());
|
|
19595
|
+
}
|
|
19596
|
+
}
|
|
19597
|
+
get direction() {
|
|
19598
|
+
return this._direction;
|
|
19599
|
+
}
|
|
19600
|
+
set direction(value) {
|
|
19601
|
+
if (this._direction !== value) {
|
|
19602
|
+
this._direction = value;
|
|
19603
|
+
this.requestRender();
|
|
19604
|
+
}
|
|
19605
|
+
}
|
|
19606
|
+
get foregroundColor() {
|
|
19607
|
+
return this._foregroundColor;
|
|
19608
|
+
}
|
|
19609
|
+
set foregroundColor(value) {
|
|
19610
|
+
if (this._foregroundColor !== value) {
|
|
19611
|
+
this._foregroundColor = parseColor(value);
|
|
19612
|
+
this.requestRender();
|
|
19613
|
+
}
|
|
19614
|
+
}
|
|
19615
|
+
get backgroundColor() {
|
|
19616
|
+
return this._backgroundColor;
|
|
19617
|
+
}
|
|
19618
|
+
set backgroundColor(value) {
|
|
19619
|
+
if (this._backgroundColor !== value) {
|
|
19620
|
+
this._backgroundColor = parseColor(value);
|
|
19621
|
+
this.requestRender();
|
|
19622
|
+
}
|
|
19623
|
+
}
|
|
19624
|
+
get attributes() {
|
|
19625
|
+
return this._attributes;
|
|
19626
|
+
}
|
|
19627
|
+
set attributes(value) {
|
|
19628
|
+
if (this._attributes !== value) {
|
|
19629
|
+
this._attributes = value;
|
|
19630
|
+
this.requestRender();
|
|
19631
|
+
}
|
|
19632
|
+
}
|
|
19633
|
+
set arrowChars(value) {
|
|
19634
|
+
this._arrowChars = {
|
|
19635
|
+
...this._arrowChars,
|
|
19636
|
+
...value
|
|
19637
|
+
};
|
|
19638
|
+
this.requestRender();
|
|
19639
|
+
}
|
|
19640
|
+
renderSelf(buffer) {
|
|
19641
|
+
const char = this.getArrowChar();
|
|
19642
|
+
buffer.drawText(char, this.x, this.y, this._foregroundColor, this._backgroundColor, this._attributes);
|
|
19643
|
+
}
|
|
19644
|
+
getArrowChar() {
|
|
19645
|
+
switch (this._direction) {
|
|
19646
|
+
case "up":
|
|
19647
|
+
return this._arrowChars.up;
|
|
19648
|
+
case "down":
|
|
19649
|
+
return this._arrowChars.down;
|
|
19650
|
+
case "left":
|
|
19651
|
+
return this._arrowChars.left;
|
|
19652
|
+
case "right":
|
|
19653
|
+
return this._arrowChars.right;
|
|
19654
|
+
default:
|
|
19655
|
+
return "?";
|
|
19656
|
+
}
|
|
19657
|
+
}
|
|
19658
|
+
};
|
|
19659
|
+
ContentRenderable = class ContentRenderable extends BoxRenderable {
|
|
19660
|
+
viewport;
|
|
19661
|
+
_viewportCulling;
|
|
19662
|
+
constructor(ctx, viewport, viewportCulling, options) {
|
|
19663
|
+
super(ctx, options);
|
|
19664
|
+
this.viewport = viewport;
|
|
19665
|
+
this._viewportCulling = viewportCulling;
|
|
19666
|
+
}
|
|
19667
|
+
get viewportCulling() {
|
|
19668
|
+
return this._viewportCulling;
|
|
19669
|
+
}
|
|
19670
|
+
set viewportCulling(value) {
|
|
19671
|
+
this._viewportCulling = value;
|
|
19672
|
+
}
|
|
19673
|
+
_getVisibleChildren() {
|
|
19674
|
+
if (this._viewportCulling) {
|
|
19675
|
+
return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map((child) => child.num);
|
|
19676
|
+
}
|
|
19677
|
+
return this.getChildrenSortedByPrimaryAxis().map((child) => child.num);
|
|
19678
|
+
}
|
|
19679
|
+
};
|
|
19680
|
+
ScrollBoxRenderable = class ScrollBoxRenderable extends BoxRenderable {
|
|
19681
|
+
static idCounter = 0;
|
|
19682
|
+
internalId = 0;
|
|
19683
|
+
wrapper;
|
|
19684
|
+
viewport;
|
|
19685
|
+
content;
|
|
19686
|
+
horizontalScrollBar;
|
|
19687
|
+
verticalScrollBar;
|
|
19688
|
+
_focusable = true;
|
|
19689
|
+
selectionListener;
|
|
19690
|
+
autoScrollMouseX = 0;
|
|
19691
|
+
autoScrollMouseY = 0;
|
|
19692
|
+
autoScrollThresholdVertical = 3;
|
|
19693
|
+
autoScrollThresholdHorizontal = 3;
|
|
19694
|
+
autoScrollSpeedSlow = 6;
|
|
19695
|
+
autoScrollSpeedMedium = 36;
|
|
19696
|
+
autoScrollSpeedFast = 72;
|
|
19697
|
+
isAutoScrolling = false;
|
|
19698
|
+
cachedAutoScrollSpeed = 3;
|
|
19699
|
+
autoScrollAccumulatorX = 0;
|
|
19700
|
+
autoScrollAccumulatorY = 0;
|
|
19701
|
+
scrollAccumulatorX = 0;
|
|
19702
|
+
scrollAccumulatorY = 0;
|
|
19703
|
+
_stickyScroll;
|
|
19704
|
+
_stickyScrollTop = false;
|
|
19705
|
+
_stickyScrollBottom = false;
|
|
19706
|
+
_stickyScrollLeft = false;
|
|
19707
|
+
_stickyScrollRight = false;
|
|
19708
|
+
_stickyStart;
|
|
19709
|
+
_hasManualScroll = false;
|
|
19710
|
+
_isApplyingStickyScroll = false;
|
|
19711
|
+
scrollAccel;
|
|
19712
|
+
get stickyScroll() {
|
|
19713
|
+
return this._stickyScroll;
|
|
19714
|
+
}
|
|
19715
|
+
set stickyScroll(value) {
|
|
19716
|
+
this._stickyScroll = value;
|
|
19717
|
+
this.updateStickyState();
|
|
19718
|
+
}
|
|
19719
|
+
get stickyStart() {
|
|
19720
|
+
return this._stickyStart;
|
|
19721
|
+
}
|
|
19722
|
+
set stickyStart(value) {
|
|
19723
|
+
this._stickyStart = value;
|
|
19724
|
+
this.updateStickyState();
|
|
19725
|
+
}
|
|
19726
|
+
get scrollTop() {
|
|
19727
|
+
return this.verticalScrollBar.scrollPosition;
|
|
19728
|
+
}
|
|
19729
|
+
set scrollTop(value) {
|
|
19730
|
+
this.verticalScrollBar.scrollPosition = value;
|
|
19731
|
+
if (!this._isApplyingStickyScroll) {
|
|
19732
|
+
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
19733
|
+
if (!this.isAtStickyPosition() && maxScrollTop > 1) {
|
|
19734
|
+
this._hasManualScroll = true;
|
|
19735
|
+
}
|
|
19736
|
+
}
|
|
19737
|
+
this.updateStickyState();
|
|
19738
|
+
}
|
|
19739
|
+
get scrollLeft() {
|
|
19740
|
+
return this.horizontalScrollBar.scrollPosition;
|
|
19741
|
+
}
|
|
19742
|
+
set scrollLeft(value) {
|
|
19743
|
+
this.horizontalScrollBar.scrollPosition = value;
|
|
19744
|
+
if (!this._isApplyingStickyScroll) {
|
|
19745
|
+
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
19746
|
+
if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
|
|
19747
|
+
this._hasManualScroll = true;
|
|
19748
|
+
}
|
|
19749
|
+
}
|
|
19750
|
+
this.updateStickyState();
|
|
19751
|
+
}
|
|
19752
|
+
get scrollWidth() {
|
|
19753
|
+
return this.horizontalScrollBar.scrollSize;
|
|
19754
|
+
}
|
|
19755
|
+
get scrollHeight() {
|
|
19756
|
+
return this.verticalScrollBar.scrollSize;
|
|
19757
|
+
}
|
|
19758
|
+
updateStickyState() {
|
|
19759
|
+
if (!this._stickyScroll)
|
|
19760
|
+
return;
|
|
19761
|
+
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
19762
|
+
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
19763
|
+
if (this.scrollTop <= 0) {
|
|
19764
|
+
this._stickyScrollTop = true;
|
|
19765
|
+
this._stickyScrollBottom = false;
|
|
19766
|
+
} else if (this.scrollTop >= maxScrollTop) {
|
|
19767
|
+
this._stickyScrollTop = false;
|
|
19768
|
+
this._stickyScrollBottom = true;
|
|
19769
|
+
} else {
|
|
19770
|
+
this._stickyScrollTop = false;
|
|
19771
|
+
this._stickyScrollBottom = false;
|
|
19772
|
+
}
|
|
19773
|
+
if (this.scrollLeft <= 0) {
|
|
19774
|
+
this._stickyScrollLeft = true;
|
|
19775
|
+
this._stickyScrollRight = false;
|
|
19776
|
+
} else if (this.scrollLeft >= maxScrollLeft) {
|
|
19777
|
+
this._stickyScrollLeft = false;
|
|
19778
|
+
this._stickyScrollRight = true;
|
|
19779
|
+
} else {
|
|
19780
|
+
this._stickyScrollLeft = false;
|
|
19781
|
+
this._stickyScrollRight = false;
|
|
19782
|
+
}
|
|
19783
|
+
}
|
|
19784
|
+
applyStickyStart(stickyStart) {
|
|
19785
|
+
this._isApplyingStickyScroll = true;
|
|
19786
|
+
switch (stickyStart) {
|
|
19787
|
+
case "top":
|
|
19788
|
+
this._stickyScrollTop = true;
|
|
19789
|
+
this._stickyScrollBottom = false;
|
|
19790
|
+
this.verticalScrollBar.scrollPosition = 0;
|
|
19791
|
+
break;
|
|
19792
|
+
case "bottom":
|
|
19793
|
+
this._stickyScrollTop = false;
|
|
19794
|
+
this._stickyScrollBottom = true;
|
|
19795
|
+
this.verticalScrollBar.scrollPosition = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
19796
|
+
break;
|
|
19797
|
+
case "left":
|
|
19798
|
+
this._stickyScrollLeft = true;
|
|
19799
|
+
this._stickyScrollRight = false;
|
|
19800
|
+
this.horizontalScrollBar.scrollPosition = 0;
|
|
19801
|
+
break;
|
|
19802
|
+
case "right":
|
|
19803
|
+
this._stickyScrollLeft = false;
|
|
19804
|
+
this._stickyScrollRight = true;
|
|
19805
|
+
this.horizontalScrollBar.scrollPosition = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
19806
|
+
break;
|
|
19807
|
+
}
|
|
19808
|
+
this._isApplyingStickyScroll = false;
|
|
19809
|
+
}
|
|
19810
|
+
constructor(ctx, {
|
|
19811
|
+
wrapperOptions,
|
|
19812
|
+
viewportOptions,
|
|
19813
|
+
contentOptions,
|
|
19814
|
+
rootOptions,
|
|
19815
|
+
scrollbarOptions,
|
|
19816
|
+
verticalScrollbarOptions,
|
|
19817
|
+
horizontalScrollbarOptions,
|
|
19818
|
+
stickyScroll = false,
|
|
19819
|
+
stickyStart,
|
|
19820
|
+
scrollX = false,
|
|
19821
|
+
scrollY = true,
|
|
19822
|
+
scrollAcceleration,
|
|
19823
|
+
viewportCulling = true,
|
|
19824
|
+
...options
|
|
19825
|
+
}) {
|
|
19826
|
+
super(ctx, {
|
|
19827
|
+
flexDirection: "row",
|
|
19828
|
+
alignItems: "stretch",
|
|
19829
|
+
...options,
|
|
19830
|
+
...rootOptions
|
|
19831
|
+
});
|
|
19832
|
+
this.internalId = ScrollBoxRenderable.idCounter++;
|
|
19833
|
+
this._stickyScroll = stickyScroll;
|
|
19834
|
+
this._stickyStart = stickyStart;
|
|
19835
|
+
this.scrollAccel = scrollAcceleration ?? new LinearScrollAccel;
|
|
19836
|
+
this.wrapper = new BoxRenderable(ctx, {
|
|
19837
|
+
flexDirection: "column",
|
|
19838
|
+
flexGrow: 1,
|
|
19839
|
+
...wrapperOptions,
|
|
19840
|
+
id: `scroll-box-wrapper-${this.internalId}`
|
|
19841
|
+
});
|
|
19842
|
+
super.add(this.wrapper);
|
|
19843
|
+
this.viewport = new BoxRenderable(ctx, {
|
|
19844
|
+
flexDirection: "column",
|
|
19845
|
+
flexGrow: 1,
|
|
19846
|
+
overflow: "hidden",
|
|
19847
|
+
onSizeChange: () => {
|
|
19848
|
+
this.recalculateBarProps();
|
|
19849
|
+
},
|
|
19850
|
+
...viewportOptions,
|
|
19851
|
+
id: `scroll-box-viewport-${this.internalId}`
|
|
19852
|
+
});
|
|
19853
|
+
this.wrapper.add(this.viewport);
|
|
19854
|
+
this.content = new ContentRenderable(ctx, this.viewport, viewportCulling, {
|
|
19855
|
+
alignSelf: "flex-start",
|
|
19856
|
+
flexShrink: 0,
|
|
19857
|
+
...scrollX ? { minWidth: "100%" } : { minWidth: "100%", maxWidth: "100%" },
|
|
19858
|
+
...scrollY ? { minHeight: "100%" } : { minHeight: "100%", maxHeight: "100%" },
|
|
19859
|
+
onSizeChange: () => {
|
|
19860
|
+
this.recalculateBarProps();
|
|
19861
|
+
},
|
|
19862
|
+
...contentOptions,
|
|
19863
|
+
id: `scroll-box-content-${this.internalId}`
|
|
19864
|
+
});
|
|
19865
|
+
this.viewport.add(this.content);
|
|
19866
|
+
this.verticalScrollBar = new ScrollBarRenderable(ctx, {
|
|
19867
|
+
...scrollbarOptions,
|
|
19868
|
+
...verticalScrollbarOptions,
|
|
19869
|
+
arrowOptions: {
|
|
19870
|
+
...scrollbarOptions?.arrowOptions,
|
|
19871
|
+
...verticalScrollbarOptions?.arrowOptions
|
|
19872
|
+
},
|
|
19873
|
+
id: `scroll-box-vertical-scrollbar-${this.internalId}`,
|
|
19874
|
+
orientation: "vertical",
|
|
19875
|
+
onChange: (position) => {
|
|
19876
|
+
this.content.translateY = -position;
|
|
19877
|
+
if (!this._isApplyingStickyScroll) {
|
|
19878
|
+
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
19879
|
+
if (!this.isAtStickyPosition() && maxScrollTop > 1) {
|
|
19880
|
+
this._hasManualScroll = true;
|
|
19881
|
+
}
|
|
19882
|
+
}
|
|
19883
|
+
this.updateStickyState();
|
|
19884
|
+
}
|
|
19885
|
+
});
|
|
19886
|
+
super.add(this.verticalScrollBar);
|
|
19887
|
+
this.horizontalScrollBar = new ScrollBarRenderable(ctx, {
|
|
19888
|
+
...scrollbarOptions,
|
|
19889
|
+
...horizontalScrollbarOptions,
|
|
19890
|
+
arrowOptions: {
|
|
19891
|
+
...scrollbarOptions?.arrowOptions,
|
|
19892
|
+
...horizontalScrollbarOptions?.arrowOptions
|
|
19893
|
+
},
|
|
19894
|
+
id: `scroll-box-horizontal-scrollbar-${this.internalId}`,
|
|
19895
|
+
orientation: "horizontal",
|
|
19896
|
+
onChange: (position) => {
|
|
19897
|
+
this.content.translateX = -position;
|
|
19898
|
+
if (!this._isApplyingStickyScroll) {
|
|
19899
|
+
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
19900
|
+
if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
|
|
19901
|
+
this._hasManualScroll = true;
|
|
19902
|
+
}
|
|
19903
|
+
}
|
|
19904
|
+
this.updateStickyState();
|
|
19905
|
+
}
|
|
19906
|
+
});
|
|
19907
|
+
this.wrapper.add(this.horizontalScrollBar);
|
|
19908
|
+
this.recalculateBarProps();
|
|
19909
|
+
if (stickyStart && stickyScroll) {
|
|
19910
|
+
this.applyStickyStart(stickyStart);
|
|
19911
|
+
}
|
|
19912
|
+
this.selectionListener = () => {
|
|
19913
|
+
const selection = this._ctx.getSelection();
|
|
19914
|
+
if (!selection || !selection.isSelecting) {
|
|
19915
|
+
this.stopAutoScroll();
|
|
19916
|
+
}
|
|
19917
|
+
};
|
|
19918
|
+
this._ctx.on("selection", this.selectionListener);
|
|
19919
|
+
}
|
|
19920
|
+
onUpdate(deltaTime) {
|
|
19921
|
+
this.handleAutoScroll(deltaTime);
|
|
19922
|
+
}
|
|
19923
|
+
scrollBy(delta, unit = "absolute") {
|
|
19924
|
+
if (typeof delta === "number") {
|
|
19925
|
+
this.verticalScrollBar.scrollBy(delta, unit);
|
|
19926
|
+
} else {
|
|
19927
|
+
this.verticalScrollBar.scrollBy(delta.y, unit);
|
|
19928
|
+
this.horizontalScrollBar.scrollBy(delta.x, unit);
|
|
19929
|
+
}
|
|
19930
|
+
}
|
|
19931
|
+
scrollTo(position) {
|
|
19932
|
+
if (typeof position === "number") {
|
|
19933
|
+
this.scrollTop = position;
|
|
19934
|
+
} else {
|
|
19935
|
+
this.scrollTop = position.y;
|
|
19936
|
+
this.scrollLeft = position.x;
|
|
19937
|
+
}
|
|
19938
|
+
}
|
|
19939
|
+
isAtStickyPosition() {
|
|
19940
|
+
if (!this._stickyScroll || !this._stickyStart) {
|
|
19941
|
+
return false;
|
|
19942
|
+
}
|
|
19943
|
+
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
19944
|
+
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
19945
|
+
switch (this._stickyStart) {
|
|
19946
|
+
case "top":
|
|
19947
|
+
return this.scrollTop === 0;
|
|
19948
|
+
case "bottom":
|
|
19949
|
+
return this.scrollTop >= maxScrollTop;
|
|
19950
|
+
case "left":
|
|
19951
|
+
return this.scrollLeft === 0;
|
|
19952
|
+
case "right":
|
|
19953
|
+
return this.scrollLeft >= maxScrollLeft;
|
|
19954
|
+
default:
|
|
19955
|
+
return false;
|
|
19956
|
+
}
|
|
19957
|
+
}
|
|
19958
|
+
add(obj, index) {
|
|
19959
|
+
return this.content.add(obj, index);
|
|
19960
|
+
}
|
|
19961
|
+
insertBefore(obj, anchor) {
|
|
19962
|
+
return this.content.insertBefore(obj, anchor);
|
|
19963
|
+
}
|
|
19964
|
+
remove(id) {
|
|
19965
|
+
this.content.remove(id);
|
|
19966
|
+
}
|
|
19967
|
+
getChildren() {
|
|
19968
|
+
return this.content.getChildren();
|
|
19969
|
+
}
|
|
19970
|
+
onMouseEvent(event) {
|
|
19971
|
+
if (event.type === "scroll") {
|
|
19972
|
+
let dir = event.scroll?.direction;
|
|
19973
|
+
if (event.modifiers.shift)
|
|
19974
|
+
dir = dir === "up" ? "left" : dir === "down" ? "right" : dir === "right" ? "down" : "up";
|
|
19975
|
+
const baseDelta = event.scroll?.delta ?? 0;
|
|
19976
|
+
const now = Date.now();
|
|
19977
|
+
const multiplier = this.scrollAccel.tick(now);
|
|
19978
|
+
const scrollAmount = baseDelta * multiplier;
|
|
19979
|
+
if (dir === "up") {
|
|
19980
|
+
this.scrollAccumulatorY -= scrollAmount;
|
|
19981
|
+
const integerScroll = Math.trunc(this.scrollAccumulatorY);
|
|
19982
|
+
if (integerScroll !== 0) {
|
|
19983
|
+
this.scrollTop += integerScroll;
|
|
19984
|
+
this.scrollAccumulatorY -= integerScroll;
|
|
19985
|
+
}
|
|
19986
|
+
} else if (dir === "down") {
|
|
19987
|
+
this.scrollAccumulatorY += scrollAmount;
|
|
19988
|
+
const integerScroll = Math.trunc(this.scrollAccumulatorY);
|
|
19989
|
+
if (integerScroll !== 0) {
|
|
19990
|
+
this.scrollTop += integerScroll;
|
|
19991
|
+
this.scrollAccumulatorY -= integerScroll;
|
|
19992
|
+
}
|
|
19993
|
+
} else if (dir === "left") {
|
|
19994
|
+
this.scrollAccumulatorX -= scrollAmount;
|
|
19995
|
+
const integerScroll = Math.trunc(this.scrollAccumulatorX);
|
|
19996
|
+
if (integerScroll !== 0) {
|
|
19997
|
+
this.scrollLeft += integerScroll;
|
|
19998
|
+
this.scrollAccumulatorX -= integerScroll;
|
|
19999
|
+
}
|
|
20000
|
+
} else if (dir === "right") {
|
|
20001
|
+
this.scrollAccumulatorX += scrollAmount;
|
|
20002
|
+
const integerScroll = Math.trunc(this.scrollAccumulatorX);
|
|
20003
|
+
if (integerScroll !== 0) {
|
|
20004
|
+
this.scrollLeft += integerScroll;
|
|
20005
|
+
this.scrollAccumulatorX -= integerScroll;
|
|
20006
|
+
}
|
|
20007
|
+
}
|
|
20008
|
+
const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
20009
|
+
const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
20010
|
+
if (maxScrollTop > 1 || maxScrollLeft > 1) {
|
|
20011
|
+
this._hasManualScroll = true;
|
|
20012
|
+
}
|
|
20013
|
+
}
|
|
20014
|
+
if (event.type === "drag" && event.isSelecting) {
|
|
20015
|
+
this.updateAutoScroll(event.x, event.y);
|
|
20016
|
+
} else if (event.type === "up") {
|
|
20017
|
+
this.stopAutoScroll();
|
|
20018
|
+
}
|
|
20019
|
+
}
|
|
20020
|
+
handleKeyPress(key) {
|
|
20021
|
+
if (this.verticalScrollBar.handleKeyPress(key)) {
|
|
20022
|
+
this._hasManualScroll = true;
|
|
20023
|
+
this.scrollAccel.reset();
|
|
20024
|
+
this.resetScrollAccumulators();
|
|
20025
|
+
return true;
|
|
20026
|
+
}
|
|
20027
|
+
if (this.horizontalScrollBar.handleKeyPress(key)) {
|
|
20028
|
+
this._hasManualScroll = true;
|
|
20029
|
+
this.scrollAccel.reset();
|
|
20030
|
+
this.resetScrollAccumulators();
|
|
20031
|
+
return true;
|
|
20032
|
+
}
|
|
20033
|
+
return false;
|
|
20034
|
+
}
|
|
20035
|
+
resetScrollAccumulators() {
|
|
20036
|
+
this.scrollAccumulatorX = 0;
|
|
20037
|
+
this.scrollAccumulatorY = 0;
|
|
20038
|
+
}
|
|
20039
|
+
startAutoScroll(mouseX, mouseY) {
|
|
20040
|
+
this.stopAutoScroll();
|
|
20041
|
+
this.autoScrollMouseX = mouseX;
|
|
20042
|
+
this.autoScrollMouseY = mouseY;
|
|
20043
|
+
this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
|
|
20044
|
+
this.isAutoScrolling = true;
|
|
20045
|
+
if (!this.live) {
|
|
20046
|
+
this.live = true;
|
|
20047
|
+
}
|
|
20048
|
+
}
|
|
20049
|
+
updateAutoScroll(mouseX, mouseY) {
|
|
20050
|
+
this.autoScrollMouseX = mouseX;
|
|
20051
|
+
this.autoScrollMouseY = mouseY;
|
|
20052
|
+
this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
|
|
20053
|
+
const scrollX = this.getAutoScrollDirectionX(mouseX);
|
|
20054
|
+
const scrollY = this.getAutoScrollDirectionY(mouseY);
|
|
20055
|
+
if (scrollX === 0 && scrollY === 0) {
|
|
20056
|
+
this.stopAutoScroll();
|
|
20057
|
+
} else if (!this.isAutoScrolling) {
|
|
20058
|
+
this.startAutoScroll(mouseX, mouseY);
|
|
20059
|
+
}
|
|
20060
|
+
}
|
|
20061
|
+
stopAutoScroll() {
|
|
20062
|
+
const wasAutoScrolling = this.isAutoScrolling;
|
|
20063
|
+
this.isAutoScrolling = false;
|
|
20064
|
+
this.autoScrollAccumulatorX = 0;
|
|
20065
|
+
this.autoScrollAccumulatorY = 0;
|
|
20066
|
+
if (wasAutoScrolling && !this.hasOtherLiveReasons()) {
|
|
20067
|
+
this.live = false;
|
|
20068
|
+
}
|
|
20069
|
+
}
|
|
20070
|
+
hasOtherLiveReasons() {
|
|
20071
|
+
return false;
|
|
20072
|
+
}
|
|
20073
|
+
handleAutoScroll(deltaTime) {
|
|
20074
|
+
if (!this.isAutoScrolling)
|
|
20075
|
+
return;
|
|
20076
|
+
const scrollX = this.getAutoScrollDirectionX(this.autoScrollMouseX);
|
|
20077
|
+
const scrollY = this.getAutoScrollDirectionY(this.autoScrollMouseY);
|
|
20078
|
+
const scrollAmount = this.cachedAutoScrollSpeed * (deltaTime / 1000);
|
|
20079
|
+
let scrolled = false;
|
|
20080
|
+
if (scrollX !== 0) {
|
|
20081
|
+
this.autoScrollAccumulatorX += scrollX * scrollAmount;
|
|
20082
|
+
const integerScrollX = Math.trunc(this.autoScrollAccumulatorX);
|
|
20083
|
+
if (integerScrollX !== 0) {
|
|
20084
|
+
this.scrollLeft += integerScrollX;
|
|
20085
|
+
this.autoScrollAccumulatorX -= integerScrollX;
|
|
20086
|
+
scrolled = true;
|
|
20087
|
+
}
|
|
20088
|
+
}
|
|
20089
|
+
if (scrollY !== 0) {
|
|
20090
|
+
this.autoScrollAccumulatorY += scrollY * scrollAmount;
|
|
20091
|
+
const integerScrollY = Math.trunc(this.autoScrollAccumulatorY);
|
|
20092
|
+
if (integerScrollY !== 0) {
|
|
20093
|
+
this.scrollTop += integerScrollY;
|
|
20094
|
+
this.autoScrollAccumulatorY -= integerScrollY;
|
|
20095
|
+
scrolled = true;
|
|
20096
|
+
}
|
|
20097
|
+
}
|
|
20098
|
+
if (scrolled) {
|
|
20099
|
+
this._ctx.requestSelectionUpdate();
|
|
20100
|
+
}
|
|
20101
|
+
if (scrollX === 0 && scrollY === 0) {
|
|
20102
|
+
this.stopAutoScroll();
|
|
20103
|
+
}
|
|
20104
|
+
}
|
|
20105
|
+
getAutoScrollDirectionX(mouseX) {
|
|
20106
|
+
const relativeX = mouseX - this.x;
|
|
20107
|
+
const distToLeft = relativeX;
|
|
20108
|
+
const distToRight = this.width - relativeX;
|
|
20109
|
+
if (distToLeft <= this.autoScrollThresholdHorizontal) {
|
|
20110
|
+
return this.scrollLeft > 0 ? -1 : 0;
|
|
20111
|
+
} else if (distToRight <= this.autoScrollThresholdHorizontal) {
|
|
20112
|
+
const maxScrollLeft = this.scrollWidth - this.viewport.width;
|
|
20113
|
+
return this.scrollLeft < maxScrollLeft ? 1 : 0;
|
|
20114
|
+
}
|
|
20115
|
+
return 0;
|
|
20116
|
+
}
|
|
20117
|
+
getAutoScrollDirectionY(mouseY) {
|
|
20118
|
+
const relativeY = mouseY - this.y;
|
|
20119
|
+
const distToTop = relativeY;
|
|
20120
|
+
const distToBottom = this.height - relativeY;
|
|
20121
|
+
if (distToTop <= this.autoScrollThresholdVertical) {
|
|
20122
|
+
return this.scrollTop > 0 ? -1 : 0;
|
|
20123
|
+
} else if (distToBottom <= this.autoScrollThresholdVertical) {
|
|
20124
|
+
const maxScrollTop = this.scrollHeight - this.viewport.height;
|
|
20125
|
+
return this.scrollTop < maxScrollTop ? 1 : 0;
|
|
20126
|
+
}
|
|
20127
|
+
return 0;
|
|
20128
|
+
}
|
|
20129
|
+
getAutoScrollSpeed(mouseX, mouseY) {
|
|
20130
|
+
const relativeX = mouseX - this.x;
|
|
20131
|
+
const relativeY = mouseY - this.y;
|
|
20132
|
+
const distToLeft = relativeX;
|
|
20133
|
+
const distToRight = this.width - relativeX;
|
|
20134
|
+
const distToTop = relativeY;
|
|
20135
|
+
const distToBottom = this.height - relativeY;
|
|
20136
|
+
const minDistance = Math.min(distToLeft, distToRight, distToTop, distToBottom);
|
|
20137
|
+
if (minDistance <= 1) {
|
|
20138
|
+
return this.autoScrollSpeedFast;
|
|
20139
|
+
} else if (minDistance <= 2) {
|
|
20140
|
+
return this.autoScrollSpeedMedium;
|
|
20141
|
+
} else {
|
|
20142
|
+
return this.autoScrollSpeedSlow;
|
|
20143
|
+
}
|
|
20144
|
+
}
|
|
20145
|
+
recalculateBarProps() {
|
|
20146
|
+
const wasApplyingStickyScroll = this._isApplyingStickyScroll;
|
|
20147
|
+
this._isApplyingStickyScroll = true;
|
|
20148
|
+
this.verticalScrollBar.scrollSize = this.content.height;
|
|
20149
|
+
this.verticalScrollBar.viewportSize = this.viewport.height;
|
|
20150
|
+
this.horizontalScrollBar.scrollSize = this.content.width;
|
|
20151
|
+
this.horizontalScrollBar.viewportSize = this.viewport.width;
|
|
20152
|
+
if (this._stickyScroll) {
|
|
20153
|
+
const newMaxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
|
|
20154
|
+
const newMaxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
|
|
20155
|
+
if (this._stickyStart && !this._hasManualScroll) {
|
|
20156
|
+
this.applyStickyStart(this._stickyStart);
|
|
20157
|
+
} else {
|
|
20158
|
+
if (this._stickyScrollTop) {
|
|
20159
|
+
this.scrollTop = 0;
|
|
20160
|
+
} else if (this._stickyScrollBottom && newMaxScrollTop > 0) {
|
|
20161
|
+
this.scrollTop = newMaxScrollTop;
|
|
20162
|
+
}
|
|
20163
|
+
if (this._stickyScrollLeft) {
|
|
20164
|
+
this.scrollLeft = 0;
|
|
20165
|
+
} else if (this._stickyScrollRight && newMaxScrollLeft > 0) {
|
|
20166
|
+
this.scrollLeft = newMaxScrollLeft;
|
|
20167
|
+
}
|
|
20168
|
+
}
|
|
20169
|
+
}
|
|
20170
|
+
this._isApplyingStickyScroll = wasApplyingStickyScroll;
|
|
20171
|
+
process.nextTick(() => {
|
|
20172
|
+
this.requestRender();
|
|
20173
|
+
});
|
|
20174
|
+
}
|
|
20175
|
+
set rootOptions(options) {
|
|
20176
|
+
Object.assign(this, options);
|
|
20177
|
+
this.requestRender();
|
|
20178
|
+
}
|
|
20179
|
+
set wrapperOptions(options) {
|
|
20180
|
+
Object.assign(this.wrapper, options);
|
|
20181
|
+
this.requestRender();
|
|
20182
|
+
}
|
|
20183
|
+
set viewportOptions(options) {
|
|
20184
|
+
Object.assign(this.viewport, options);
|
|
20185
|
+
this.requestRender();
|
|
20186
|
+
}
|
|
20187
|
+
set contentOptions(options) {
|
|
20188
|
+
Object.assign(this.content, options);
|
|
20189
|
+
this.requestRender();
|
|
20190
|
+
}
|
|
20191
|
+
set scrollbarOptions(options) {
|
|
20192
|
+
Object.assign(this.verticalScrollBar, options);
|
|
20193
|
+
Object.assign(this.horizontalScrollBar, options);
|
|
20194
|
+
this.requestRender();
|
|
20195
|
+
}
|
|
20196
|
+
set verticalScrollbarOptions(options) {
|
|
20197
|
+
Object.assign(this.verticalScrollBar, options);
|
|
20198
|
+
this.requestRender();
|
|
20199
|
+
}
|
|
20200
|
+
set horizontalScrollbarOptions(options) {
|
|
20201
|
+
Object.assign(this.horizontalScrollBar, options);
|
|
20202
|
+
this.requestRender();
|
|
20203
|
+
}
|
|
20204
|
+
get scrollAcceleration() {
|
|
20205
|
+
return this.scrollAccel;
|
|
20206
|
+
}
|
|
20207
|
+
set scrollAcceleration(value) {
|
|
20208
|
+
this.scrollAccel = value;
|
|
20209
|
+
}
|
|
20210
|
+
get viewportCulling() {
|
|
20211
|
+
return this.content.viewportCulling;
|
|
20212
|
+
}
|
|
20213
|
+
set viewportCulling(value) {
|
|
20214
|
+
this.content.viewportCulling = value;
|
|
20215
|
+
this.requestRender();
|
|
20216
|
+
}
|
|
20217
|
+
destroySelf() {
|
|
20218
|
+
if (this.selectionListener) {
|
|
20219
|
+
this._ctx.off("selection", this.selectionListener);
|
|
20220
|
+
this.selectionListener = undefined;
|
|
20221
|
+
}
|
|
20222
|
+
super.destroySelf();
|
|
20223
|
+
}
|
|
20224
|
+
};
|
|
20225
|
+
defaultSelectKeybindings = [
|
|
20226
|
+
{ name: "up", action: "move-up" },
|
|
20227
|
+
{ name: "k", action: "move-up" },
|
|
20228
|
+
{ name: "down", action: "move-down" },
|
|
20229
|
+
{ name: "j", action: "move-down" },
|
|
20230
|
+
{ name: "up", shift: true, action: "move-up-fast" },
|
|
20231
|
+
{ name: "down", shift: true, action: "move-down-fast" },
|
|
20232
|
+
{ name: "return", action: "select-current" },
|
|
20233
|
+
{ name: "linefeed", action: "select-current" }
|
|
20234
|
+
];
|
|
20235
|
+
((SelectRenderableEvents2) => {
|
|
20236
|
+
SelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
|
|
20237
|
+
SelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
|
|
20238
|
+
})(SelectRenderableEvents ||= {});
|
|
20239
|
+
SelectRenderable = class SelectRenderable extends Renderable {
|
|
20240
|
+
_focusable = true;
|
|
20241
|
+
_options = [];
|
|
20242
|
+
_selectedIndex = 0;
|
|
20243
|
+
scrollOffset = 0;
|
|
20244
|
+
maxVisibleItems;
|
|
20245
|
+
_backgroundColor;
|
|
20246
|
+
_textColor;
|
|
20247
|
+
_focusedBackgroundColor;
|
|
20248
|
+
_focusedTextColor;
|
|
20249
|
+
_selectedBackgroundColor;
|
|
20250
|
+
_selectedTextColor;
|
|
20251
|
+
_descriptionColor;
|
|
20252
|
+
_selectedDescriptionColor;
|
|
20253
|
+
_showScrollIndicator;
|
|
20254
|
+
_wrapSelection;
|
|
20255
|
+
_showDescription;
|
|
20256
|
+
_font;
|
|
20257
|
+
_itemSpacing;
|
|
20258
|
+
linesPerItem;
|
|
20259
|
+
fontHeight;
|
|
20260
|
+
_fastScrollStep;
|
|
20261
|
+
_keyBindingsMap;
|
|
20262
|
+
_keyAliasMap;
|
|
20263
|
+
_keyBindings;
|
|
20264
|
+
_defaultOptions = {
|
|
20265
|
+
backgroundColor: "transparent",
|
|
20266
|
+
textColor: "#FFFFFF",
|
|
20267
|
+
focusedBackgroundColor: "#1a1a1a",
|
|
20268
|
+
focusedTextColor: "#FFFFFF",
|
|
20269
|
+
selectedBackgroundColor: "#334455",
|
|
20270
|
+
selectedTextColor: "#FFFF00",
|
|
20271
|
+
selectedIndex: 0,
|
|
20272
|
+
descriptionColor: "#888888",
|
|
20273
|
+
selectedDescriptionColor: "#CCCCCC",
|
|
20274
|
+
showScrollIndicator: false,
|
|
20275
|
+
wrapSelection: false,
|
|
20276
|
+
showDescription: true,
|
|
20277
|
+
itemSpacing: 0,
|
|
20278
|
+
fastScrollStep: 5
|
|
20279
|
+
};
|
|
20280
|
+
constructor(ctx, options) {
|
|
20281
|
+
super(ctx, { ...options, buffered: true });
|
|
20282
|
+
this._options = options.options || [];
|
|
20283
|
+
const requestedIndex = options.selectedIndex ?? this._defaultOptions.selectedIndex;
|
|
20284
|
+
this._selectedIndex = this._options.length > 0 ? Math.min(requestedIndex, this._options.length - 1) : 0;
|
|
20285
|
+
this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor);
|
|
20286
|
+
this._textColor = parseColor(options.textColor || this._defaultOptions.textColor);
|
|
20287
|
+
this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor);
|
|
20288
|
+
this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor);
|
|
20289
|
+
this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator;
|
|
20290
|
+
this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection;
|
|
20291
|
+
this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
|
|
20292
|
+
this._font = options.font;
|
|
20293
|
+
this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
|
|
20294
|
+
this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
|
|
20295
|
+
this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
|
|
20296
|
+
this.linesPerItem += this._itemSpacing;
|
|
20297
|
+
this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
|
|
20298
|
+
this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor);
|
|
20299
|
+
this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor);
|
|
20300
|
+
this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor);
|
|
20301
|
+
this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor);
|
|
20302
|
+
this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep;
|
|
20303
|
+
this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
|
|
20304
|
+
this._keyBindings = options.keyBindings || [];
|
|
20305
|
+
const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
|
|
20306
|
+
this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
|
|
20307
|
+
this.requestRender();
|
|
20308
|
+
}
|
|
20309
|
+
renderSelf(buffer, deltaTime) {
|
|
20310
|
+
if (!this.visible || !this.frameBuffer)
|
|
20311
|
+
return;
|
|
20312
|
+
if (this.isDirty) {
|
|
20313
|
+
this.refreshFrameBuffer();
|
|
20314
|
+
}
|
|
20315
|
+
}
|
|
20316
|
+
refreshFrameBuffer() {
|
|
20317
|
+
if (!this.frameBuffer || this._options.length === 0)
|
|
20318
|
+
return;
|
|
20319
|
+
const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor;
|
|
20320
|
+
this.frameBuffer.clear(bgColor);
|
|
20321
|
+
const contentX = 0;
|
|
20322
|
+
const contentY = 0;
|
|
20323
|
+
const contentWidth = this.width;
|
|
20324
|
+
const contentHeight = this.height;
|
|
20325
|
+
const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems);
|
|
20326
|
+
for (let i = 0;i < visibleOptions.length; i++) {
|
|
20327
|
+
const actualIndex = this.scrollOffset + i;
|
|
20328
|
+
const option = visibleOptions[i];
|
|
20329
|
+
const isSelected = actualIndex === this._selectedIndex;
|
|
20330
|
+
const itemY = contentY + i * this.linesPerItem;
|
|
20331
|
+
if (itemY + this.linesPerItem - 1 >= contentY + contentHeight)
|
|
20332
|
+
break;
|
|
20333
|
+
if (isSelected) {
|
|
20334
|
+
const contentHeight2 = this.linesPerItem - this._itemSpacing;
|
|
20335
|
+
this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
|
|
20336
|
+
}
|
|
20337
|
+
const nameContent = `${isSelected ? "▶ " : " "}${option.name}`;
|
|
20338
|
+
const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
|
|
20339
|
+
const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
|
|
20340
|
+
let descX = contentX + 3;
|
|
20341
|
+
if (this._font) {
|
|
20342
|
+
const indicator = isSelected ? "▶ " : " ";
|
|
20343
|
+
this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
|
|
20344
|
+
const indicatorWidth = 2;
|
|
20345
|
+
renderFontToFrameBuffer(this.frameBuffer, {
|
|
20346
|
+
text: option.name,
|
|
20347
|
+
x: contentX + 1 + indicatorWidth,
|
|
20348
|
+
y: itemY,
|
|
20349
|
+
color: nameColor,
|
|
20350
|
+
backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
|
|
20351
|
+
font: this._font
|
|
20352
|
+
});
|
|
20353
|
+
descX = contentX + 1 + indicatorWidth;
|
|
20354
|
+
} else {
|
|
20355
|
+
this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
|
|
20356
|
+
}
|
|
20357
|
+
if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
|
|
20358
|
+
const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor;
|
|
20359
|
+
this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor);
|
|
20360
|
+
}
|
|
20361
|
+
}
|
|
20362
|
+
if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {
|
|
20363
|
+
this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight);
|
|
20364
|
+
}
|
|
20365
|
+
}
|
|
20366
|
+
renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight) {
|
|
20367
|
+
if (!this.frameBuffer)
|
|
20368
|
+
return;
|
|
20369
|
+
const scrollPercent = this._selectedIndex / Math.max(1, this._options.length - 1);
|
|
20370
|
+
const indicatorHeight = Math.max(1, contentHeight - 2);
|
|
20371
|
+
const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight);
|
|
20372
|
+
const indicatorX = contentX + contentWidth - 1;
|
|
20373
|
+
this.frameBuffer.drawText("█", indicatorX, indicatorY, parseColor("#666666"));
|
|
20374
|
+
}
|
|
20375
|
+
get options() {
|
|
20376
|
+
return this._options;
|
|
20377
|
+
}
|
|
20378
|
+
set options(options) {
|
|
20379
|
+
this._options = options;
|
|
20380
|
+
this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1));
|
|
20381
|
+
this.updateScrollOffset();
|
|
20382
|
+
this.requestRender();
|
|
20383
|
+
}
|
|
20384
|
+
getSelectedOption() {
|
|
20385
|
+
return this._options[this._selectedIndex] || null;
|
|
20386
|
+
}
|
|
20387
|
+
getSelectedIndex() {
|
|
20388
|
+
return this._selectedIndex;
|
|
20389
|
+
}
|
|
20390
|
+
moveUp(steps = 1) {
|
|
20391
|
+
const newIndex = this._selectedIndex - steps;
|
|
20392
|
+
if (newIndex >= 0) {
|
|
20393
|
+
this._selectedIndex = newIndex;
|
|
20394
|
+
} else if (this._wrapSelection && this._options.length > 0) {
|
|
20395
|
+
this._selectedIndex = this._options.length - 1;
|
|
20396
|
+
} else {
|
|
20397
|
+
this._selectedIndex = 0;
|
|
20398
|
+
}
|
|
20399
|
+
this.updateScrollOffset();
|
|
20400
|
+
this.requestRender();
|
|
20401
|
+
this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
|
|
20402
|
+
}
|
|
20403
|
+
moveDown(steps = 1) {
|
|
20404
|
+
const newIndex = this._selectedIndex + steps;
|
|
20405
|
+
if (newIndex < this._options.length) {
|
|
20406
|
+
this._selectedIndex = newIndex;
|
|
20407
|
+
} else if (this._wrapSelection && this._options.length > 0) {
|
|
20408
|
+
this._selectedIndex = 0;
|
|
20409
|
+
} else {
|
|
20410
|
+
this._selectedIndex = this._options.length - 1;
|
|
20411
|
+
}
|
|
20412
|
+
this.updateScrollOffset();
|
|
20413
|
+
this.requestRender();
|
|
20414
|
+
this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
|
|
20415
|
+
}
|
|
20416
|
+
selectCurrent() {
|
|
20417
|
+
const selected = this.getSelectedOption();
|
|
20418
|
+
if (selected) {
|
|
20419
|
+
this.emit("itemSelected", this._selectedIndex, selected);
|
|
20420
|
+
}
|
|
20421
|
+
}
|
|
20422
|
+
setSelectedIndex(index) {
|
|
20423
|
+
if (index >= 0 && index < this._options.length) {
|
|
20424
|
+
this._selectedIndex = index;
|
|
20425
|
+
this.updateScrollOffset();
|
|
20426
|
+
this.requestRender();
|
|
20427
|
+
this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
|
|
20428
|
+
}
|
|
20429
|
+
}
|
|
20430
|
+
updateScrollOffset() {
|
|
20431
|
+
if (!this._options)
|
|
20432
|
+
return;
|
|
20433
|
+
const halfVisible = Math.floor(this.maxVisibleItems / 2);
|
|
20434
|
+
const newScrollOffset = Math.max(0, Math.min(this._selectedIndex - halfVisible, this._options.length - this.maxVisibleItems));
|
|
20435
|
+
if (newScrollOffset !== this.scrollOffset) {
|
|
20436
|
+
this.scrollOffset = newScrollOffset;
|
|
20437
|
+
this.requestRender();
|
|
20438
|
+
}
|
|
19185
20439
|
}
|
|
19186
20440
|
onResize(width, height) {
|
|
19187
20441
|
this.maxVisibleItems = Math.max(1, Math.floor(height / this.linesPerItem));
|
|
@@ -20339,11 +21593,21 @@ function getZenApiType2(modelId) {
|
|
|
20339
21593
|
}
|
|
20340
21594
|
return "openai-compatible";
|
|
20341
21595
|
}
|
|
20342
|
-
function buildSystemPrompt2(cwd, history, shellInfo) {
|
|
21596
|
+
function buildSystemPrompt2(cwd, history, shellInfo, repoContextEnabled) {
|
|
20343
21597
|
const historyContext = formatHistory2(history);
|
|
20344
21598
|
const platformPaths = getPlatformPaths(shellInfo.platform);
|
|
20345
21599
|
const shellHints = getShellSyntaxHints(shellInfo.shell);
|
|
20346
21600
|
const platformName = shellInfo.platform === "macos" ? "macOS" : shellInfo.platform === "windows" ? "Windows" : shellInfo.platform === "linux" ? shellInfo.isWSL ? "Linux (WSL)" : "Linux" : "Unknown";
|
|
21601
|
+
let projectContextSection = "";
|
|
21602
|
+
if (repoContextEnabled) {
|
|
21603
|
+
const repoContext = detectRepoContext(cwd);
|
|
21604
|
+
if (repoContext) {
|
|
21605
|
+
projectContextSection = `
|
|
21606
|
+
Project context:
|
|
21607
|
+
${formatRepoContext(repoContext)}
|
|
21608
|
+
`;
|
|
21609
|
+
}
|
|
21610
|
+
}
|
|
20347
21611
|
return `You are a shell command translator. Convert the user's natural language request into a shell command.
|
|
20348
21612
|
|
|
20349
21613
|
Current environment:
|
|
@@ -20352,7 +21616,7 @@ Current environment:
|
|
|
20352
21616
|
- Working directory: ${cwd}
|
|
20353
21617
|
- Home directory: ${shellInfo.homeDir}
|
|
20354
21618
|
${shellInfo.terminalEmulator ? `- Terminal: ${shellInfo.terminalEmulator}` : ""}
|
|
20355
|
-
|
|
21619
|
+
${projectContextSection}
|
|
20356
21620
|
${shellHints}
|
|
20357
21621
|
|
|
20358
21622
|
Recent command history:
|
|
@@ -20363,7 +21627,8 @@ Rules:
|
|
|
20363
21627
|
- No explanations, no markdown, no backticks, no code blocks
|
|
20364
21628
|
- Use the correct syntax for the detected shell (${shellInfo.shell})
|
|
20365
21629
|
- If the request is unclear, make a reasonable assumption
|
|
20366
|
-
- Prefer simple, common commands over complex one-liners
|
|
21630
|
+
- Prefer simple, common commands over complex one-liners${repoContextEnabled ? `
|
|
21631
|
+
- Use project-specific commands when relevant (e.g., use the detected package manager and available scripts)` : ""}
|
|
20367
21632
|
- Use the command history for context (e.g., "do that again", "undo", "delete the file I just created")
|
|
20368
21633
|
- If the user asks something that can't be done with a shell command, output a command that prints a helpful message
|
|
20369
21634
|
- For file operations, prefer safer alternatives when possible
|
|
@@ -20635,9 +21900,9 @@ function getShellInfo2() {
|
|
|
20635
21900
|
}
|
|
20636
21901
|
return cachedShellInfo2;
|
|
20637
21902
|
}
|
|
20638
|
-
async function translateToCommand2(apiKey, model, userInput, cwd, history = []) {
|
|
21903
|
+
async function translateToCommand2(apiKey, model, userInput, cwd, history = [], repoContextEnabled) {
|
|
20639
21904
|
const shellInfo = getShellInfo2();
|
|
20640
|
-
const systemPrompt = buildSystemPrompt2(cwd, history, shellInfo);
|
|
21905
|
+
const systemPrompt = buildSystemPrompt2(cwd, history, shellInfo, repoContextEnabled);
|
|
20641
21906
|
let rawCommand;
|
|
20642
21907
|
if (model.provider === "openrouter") {
|
|
20643
21908
|
rawCommand = await callOpenRouter2(apiKey, model.id, systemPrompt, userInput);
|
|
@@ -20663,6 +21928,7 @@ async function translateToCommand2(apiKey, model, userInput, cwd, history = [])
|
|
|
20663
21928
|
var DEBUG_API2, cachedShellInfo2 = null;
|
|
20664
21929
|
var init_api = __esm(() => {
|
|
20665
21930
|
init_shell();
|
|
21931
|
+
init_repo_context();
|
|
20666
21932
|
DEBUG_API2 = process.env.DEBUG_API === "1";
|
|
20667
21933
|
});
|
|
20668
21934
|
|
|
@@ -20870,6 +22136,9 @@ __export(exports_cli, {
|
|
|
20870
22136
|
});
|
|
20871
22137
|
import { spawn } from "child_process";
|
|
20872
22138
|
import { cwd as getCwd } from "process";
|
|
22139
|
+
function generateMessageId() {
|
|
22140
|
+
return `msg-${++messageIdCounter}`;
|
|
22141
|
+
}
|
|
20873
22142
|
async function main2() {
|
|
20874
22143
|
config = loadConfig2();
|
|
20875
22144
|
history = loadHistory2();
|
|
@@ -21047,44 +22316,65 @@ function createMainUI() {
|
|
|
21047
22316
|
id: "header-row",
|
|
21048
22317
|
flexDirection: "row",
|
|
21049
22318
|
width: "100%",
|
|
22319
|
+
alignItems: "center",
|
|
21050
22320
|
marginBottom: 1
|
|
21051
22321
|
});
|
|
21052
22322
|
mainContainer.add(headerRow);
|
|
21053
22323
|
headerText = new TextRenderable(renderer, {
|
|
21054
22324
|
id: "header-text",
|
|
21055
|
-
content: t`${bold(fg(theme.colors.primary)("magic-shell"))}
|
|
22325
|
+
content: t`${bold(fg(theme.colors.primary)("magic-shell"))}`,
|
|
21056
22326
|
flexGrow: 1
|
|
21057
22327
|
});
|
|
21058
22328
|
headerRow.add(headerText);
|
|
21059
|
-
const
|
|
21060
|
-
id: "
|
|
21061
|
-
|
|
21062
|
-
width: "100%",
|
|
21063
|
-
marginBottom: 1
|
|
22329
|
+
const modelBadge = new TextRenderable(renderer, {
|
|
22330
|
+
id: "model-badge",
|
|
22331
|
+
content: getModelDisplay()
|
|
21064
22332
|
});
|
|
21065
|
-
|
|
21066
|
-
|
|
21067
|
-
id: "
|
|
21068
|
-
content:
|
|
21069
|
-
|
|
22333
|
+
headerRow.add(modelBadge);
|
|
22334
|
+
statusBarText = new TextRenderable(renderer, {
|
|
22335
|
+
id: "status-bar-text",
|
|
22336
|
+
content: getStatusBarContent(),
|
|
22337
|
+
marginBottom: 1
|
|
21070
22338
|
});
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
id: "
|
|
21074
|
-
|
|
22339
|
+
mainContainer.add(statusBarText);
|
|
22340
|
+
chatScrollBox = new ScrollBoxRenderable(renderer, {
|
|
22341
|
+
id: "chat-scroll-box",
|
|
22342
|
+
flexGrow: 1,
|
|
22343
|
+
width: "100%",
|
|
22344
|
+
scrollY: true,
|
|
22345
|
+
scrollX: false,
|
|
22346
|
+
stickyScroll: true,
|
|
22347
|
+
stickyStart: "bottom",
|
|
22348
|
+
rootOptions: {
|
|
22349
|
+
border: true,
|
|
22350
|
+
borderColor: theme.colors.border,
|
|
22351
|
+
borderStyle: "single"
|
|
22352
|
+
},
|
|
22353
|
+
viewportOptions: {
|
|
22354
|
+
backgroundColor: theme.colors.background,
|
|
22355
|
+
paddingLeft: 1,
|
|
22356
|
+
paddingRight: 1,
|
|
22357
|
+
paddingTop: 1
|
|
22358
|
+
},
|
|
22359
|
+
contentOptions: {
|
|
22360
|
+
flexDirection: "column",
|
|
22361
|
+
gap: 1
|
|
22362
|
+
}
|
|
21075
22363
|
});
|
|
21076
|
-
|
|
22364
|
+
mainContainer.add(chatScrollBox);
|
|
22365
|
+
addSystemMessage(getWelcomeMessage());
|
|
21077
22366
|
const inputRow = new BoxRenderable(renderer, {
|
|
21078
22367
|
id: "input-row",
|
|
21079
22368
|
flexDirection: "row",
|
|
21080
22369
|
width: "100%",
|
|
21081
|
-
|
|
22370
|
+
marginTop: 1,
|
|
22371
|
+
alignItems: "center"
|
|
21082
22372
|
});
|
|
21083
22373
|
mainContainer.add(inputRow);
|
|
21084
22374
|
const promptText = new TextRenderable(renderer, {
|
|
21085
22375
|
id: "prompt-text",
|
|
21086
|
-
content: t`${fg(theme.colors.
|
|
21087
|
-
width:
|
|
22376
|
+
content: t`${fg(theme.colors.primary)("~>")} `,
|
|
22377
|
+
width: 3
|
|
21088
22378
|
});
|
|
21089
22379
|
inputRow.add(promptText);
|
|
21090
22380
|
inputField = new InputRenderable(renderer, {
|
|
@@ -21102,58 +22392,269 @@ function createMainUI() {
|
|
|
21102
22392
|
}
|
|
21103
22393
|
});
|
|
21104
22394
|
inputRow.add(inputField);
|
|
21105
|
-
|
|
21106
|
-
id: "
|
|
21107
|
-
content:
|
|
21108
|
-
|
|
21109
|
-
});
|
|
21110
|
-
mainContainer.add(commandPreview);
|
|
21111
|
-
safetyWarning = new TextRenderable(renderer, {
|
|
21112
|
-
id: "safety-warning",
|
|
21113
|
-
content: ""
|
|
22395
|
+
helpBarText = new TextRenderable(renderer, {
|
|
22396
|
+
id: "help-bar-text",
|
|
22397
|
+
content: getHelpBarContent(),
|
|
22398
|
+
marginTop: 1
|
|
21114
22399
|
});
|
|
21115
|
-
mainContainer.add(
|
|
21116
|
-
|
|
21117
|
-
|
|
22400
|
+
mainContainer.add(helpBarText);
|
|
22401
|
+
inputField.on(InputRenderableEvents.ENTER, handleInput);
|
|
22402
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
22403
|
+
inputField.focus();
|
|
22404
|
+
}
|
|
22405
|
+
function getStatusBarContent() {
|
|
22406
|
+
const theme = getTheme2();
|
|
22407
|
+
const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
|
|
22408
|
+
const safeModeIndicator = dryRunMode ? fg(theme.colors.warning)("[DRY RUN]") : fg(theme.colors.success)("Safe");
|
|
22409
|
+
const repoContextIndicator = config.repoContext ? fg(theme.colors.info)("[Repo]") : "";
|
|
22410
|
+
return t`${fg(theme.colors.textMuted)("Provider:")} ${fg(theme.colors.text)(providerName)} ${fg(theme.colors.textMuted)("Model:")} ${fg(theme.colors.text)(currentModel.name)} ${safeModeIndicator}${repoContextIndicator ? " " : ""}${repoContextIndicator}`;
|
|
22411
|
+
}
|
|
22412
|
+
function getHelpBarContent() {
|
|
22413
|
+
const theme = getTheme2();
|
|
22414
|
+
if (awaitingConfirmation) {
|
|
22415
|
+
return t`${fg(theme.colors.warning)("[Enter] Run")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.error)("[Esc] Cancel")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.primary)("[e] Edit")}`;
|
|
22416
|
+
}
|
|
22417
|
+
return t`${fg(theme.colors.textMuted)("Ctrl+X")} ${fg(theme.colors.primary)("P")}${fg(theme.colors.textMuted)(" Palette")} ${fg(theme.colors.primary)("M")}${fg(theme.colors.textMuted)(" Model")} ${fg(theme.colors.primary)("T")}${fg(theme.colors.textMuted)(" Theme")} ${fg(theme.colors.primary)("D")}${fg(theme.colors.textMuted)(" Dry-run")} ${fg(theme.colors.primary)("?")}${fg(theme.colors.textMuted)(" Help")}`;
|
|
22418
|
+
}
|
|
22419
|
+
function getWelcomeMessage() {
|
|
22420
|
+
const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
|
|
22421
|
+
const freeNote = config.provider === "opencode-zen" ? `
|
|
22422
|
+
Free models: grok-code, glm-4.7-free` : "";
|
|
22423
|
+
return `Ready. Using ${providerName}.${freeNote}
|
|
22424
|
+
Type what you want to do, or press Ctrl+X P for command palette.`;
|
|
22425
|
+
}
|
|
22426
|
+
function addSystemMessage(content) {
|
|
22427
|
+
const msg = {
|
|
22428
|
+
id: generateMessageId(),
|
|
22429
|
+
type: "system",
|
|
22430
|
+
content,
|
|
22431
|
+
timestamp: Date.now()
|
|
22432
|
+
};
|
|
22433
|
+
chatMessages.push(msg);
|
|
22434
|
+
renderMessage(msg);
|
|
22435
|
+
return msg;
|
|
22436
|
+
}
|
|
22437
|
+
function addUserMessage(content) {
|
|
22438
|
+
const msg = {
|
|
22439
|
+
id: generateMessageId(),
|
|
22440
|
+
type: "user",
|
|
22441
|
+
content,
|
|
22442
|
+
timestamp: Date.now()
|
|
22443
|
+
};
|
|
22444
|
+
chatMessages.push(msg);
|
|
22445
|
+
renderMessage(msg);
|
|
22446
|
+
return msg;
|
|
22447
|
+
}
|
|
22448
|
+
function addAssistantMessage(content, command, safety) {
|
|
22449
|
+
const msg = {
|
|
22450
|
+
id: generateMessageId(),
|
|
22451
|
+
type: "assistant",
|
|
22452
|
+
content,
|
|
22453
|
+
command,
|
|
22454
|
+
safety,
|
|
22455
|
+
timestamp: Date.now(),
|
|
22456
|
+
executed: false
|
|
22457
|
+
};
|
|
22458
|
+
chatMessages.push(msg);
|
|
22459
|
+
renderMessage(msg);
|
|
22460
|
+
return msg;
|
|
22461
|
+
}
|
|
22462
|
+
function addResultMessage(content, exitCode) {
|
|
22463
|
+
const msg = {
|
|
22464
|
+
id: generateMessageId(),
|
|
22465
|
+
type: "result",
|
|
22466
|
+
content,
|
|
22467
|
+
timestamp: Date.now(),
|
|
22468
|
+
exitCode
|
|
22469
|
+
};
|
|
22470
|
+
chatMessages.push(msg);
|
|
22471
|
+
renderMessage(msg);
|
|
22472
|
+
return msg;
|
|
22473
|
+
}
|
|
22474
|
+
function renderMessage(msg) {
|
|
22475
|
+
const theme = getTheme2();
|
|
22476
|
+
const msgBox = createMessageRenderable(msg, theme);
|
|
22477
|
+
chatScrollBox.add(msgBox);
|
|
22478
|
+
}
|
|
22479
|
+
function createMessageRenderable(msg, theme) {
|
|
22480
|
+
switch (msg.type) {
|
|
22481
|
+
case "user":
|
|
22482
|
+
return createUserMessageRenderable(msg, theme);
|
|
22483
|
+
case "assistant":
|
|
22484
|
+
return createAssistantMessageRenderable(msg, theme);
|
|
22485
|
+
case "result":
|
|
22486
|
+
return createResultMessageRenderable(msg, theme);
|
|
22487
|
+
case "system":
|
|
22488
|
+
default:
|
|
22489
|
+
return createSystemMessageRenderable(msg, theme);
|
|
22490
|
+
}
|
|
22491
|
+
}
|
|
22492
|
+
function createUserMessageRenderable(msg, theme) {
|
|
22493
|
+
const box = new BoxRenderable(renderer, {
|
|
22494
|
+
id: `msg-${msg.id}`,
|
|
21118
22495
|
flexDirection: "row",
|
|
21119
|
-
|
|
21120
|
-
marginBottom: 1
|
|
22496
|
+
width: "100%"
|
|
21121
22497
|
});
|
|
21122
|
-
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
content: t`${fg(theme.colors.warning)("[Enter] Execute")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.error)("[Esc] Cancel")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.primary)("[e] Edit")}`
|
|
22498
|
+
const text = new TextRenderable(renderer, {
|
|
22499
|
+
id: `msg-${msg.id}-text`,
|
|
22500
|
+
content: t`${fg(theme.colors.success)(">")} ${fg(theme.colors.text)(msg.content)}`
|
|
21126
22501
|
});
|
|
21127
|
-
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
22502
|
+
box.add(text);
|
|
22503
|
+
return box;
|
|
22504
|
+
}
|
|
22505
|
+
function createAssistantMessageRenderable(msg, theme) {
|
|
22506
|
+
const isSelected = pendingMessageId === msg.id;
|
|
22507
|
+
const card = new BoxRenderable(renderer, {
|
|
22508
|
+
id: `msg-${msg.id}`,
|
|
22509
|
+
flexDirection: "column",
|
|
22510
|
+
width: "100%",
|
|
21131
22511
|
border: true,
|
|
21132
|
-
borderColor: theme.colors.border,
|
|
22512
|
+
borderColor: isSelected ? theme.colors.primary : theme.colors.border,
|
|
21133
22513
|
borderStyle: "single",
|
|
21134
|
-
|
|
21135
|
-
|
|
22514
|
+
paddingLeft: 1,
|
|
22515
|
+
paddingRight: 1,
|
|
22516
|
+
paddingTop: 0,
|
|
22517
|
+
paddingBottom: 0,
|
|
22518
|
+
backgroundColor: theme.colors.backgroundPanel
|
|
21136
22519
|
});
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
|
|
21141
|
-
outputText = new TextRenderable(renderer, {
|
|
21142
|
-
id: "output-text",
|
|
21143
|
-
content: t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
|
|
21144
|
-
|
|
21145
|
-
${fg(theme.colors.textMuted)("Type what you want to do, or press")} ${fg(theme.colors.primary)("Ctrl+X P")} ${fg(theme.colors.textMuted)("for command palette.")}`
|
|
22520
|
+
const commandText = new TextRenderable(renderer, {
|
|
22521
|
+
id: `msg-${msg.id}-cmd`,
|
|
22522
|
+
content: t`${fg(theme.colors.textMuted)("Command:")} ${fg(theme.colors.text)(msg.command || "")}`
|
|
21146
22523
|
});
|
|
21147
|
-
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
21151
|
-
|
|
22524
|
+
card.add(commandText);
|
|
22525
|
+
if (msg.safety) {
|
|
22526
|
+
const severityColor = getSeverityColor(msg.safety.severity);
|
|
22527
|
+
const severityText = msg.safety.isDangerous ? `${msg.safety.severity.toUpperCase()} risk${msg.safety.reason ? ` - ${msg.safety.reason}` : ""}` : "Low risk";
|
|
22528
|
+
const safetyText = new TextRenderable(renderer, {
|
|
22529
|
+
id: `msg-${msg.id}-safety`,
|
|
22530
|
+
content: t`${fg(severityColor)("●")} ${fg(theme.colors.textMuted)(severityText)}`
|
|
22531
|
+
});
|
|
22532
|
+
card.add(safetyText);
|
|
22533
|
+
}
|
|
22534
|
+
if (isSelected && !msg.executed) {
|
|
22535
|
+
const actionsText = new TextRenderable(renderer, {
|
|
22536
|
+
id: `msg-${msg.id}-actions`,
|
|
22537
|
+
content: t`${fg(theme.colors.warning)("[Enter]")} ${fg(theme.colors.textMuted)("Run")} ${fg(theme.colors.primary)("[c]")} ${fg(theme.colors.textMuted)("Copy")} ${fg(theme.colors.primary)("[e]")} ${fg(theme.colors.textMuted)("Edit")}`
|
|
22538
|
+
});
|
|
22539
|
+
card.add(actionsText);
|
|
22540
|
+
}
|
|
22541
|
+
if (msg.executed) {
|
|
22542
|
+
const execText = new TextRenderable(renderer, {
|
|
22543
|
+
id: `msg-${msg.id}-exec`,
|
|
22544
|
+
content: t`${fg(theme.colors.success)("Executed")}`
|
|
22545
|
+
});
|
|
22546
|
+
card.add(execText);
|
|
22547
|
+
}
|
|
22548
|
+
return card;
|
|
22549
|
+
}
|
|
22550
|
+
function createResultMessageRenderable(msg, theme) {
|
|
22551
|
+
const isSuccess = msg.exitCode === undefined || msg.exitCode === 0;
|
|
22552
|
+
const isExpanded = msg.expanded ?? false;
|
|
22553
|
+
const hasOutput = msg.content && msg.content.trim().length > 0;
|
|
22554
|
+
const outputLines = hasOutput ? msg.content.trim().split(`
|
|
22555
|
+
`) : [];
|
|
22556
|
+
const isLongOutput = outputLines.length > 5;
|
|
22557
|
+
const PREVIEW_LINES = 3;
|
|
22558
|
+
const card = new BoxRenderable(renderer, {
|
|
22559
|
+
id: `msg-${msg.id}`,
|
|
22560
|
+
flexDirection: "column",
|
|
22561
|
+
width: "100%",
|
|
22562
|
+
border: true,
|
|
22563
|
+
borderColor: isSuccess ? theme.colors.success : theme.colors.error,
|
|
22564
|
+
borderStyle: "single",
|
|
22565
|
+
paddingLeft: 1,
|
|
22566
|
+
paddingRight: 1,
|
|
22567
|
+
backgroundColor: theme.colors.backgroundPanel,
|
|
22568
|
+
onMouseDown: isLongOutput ? () => {
|
|
22569
|
+
toggleResultExpand(msg.id);
|
|
22570
|
+
} : undefined
|
|
21152
22571
|
});
|
|
21153
|
-
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
22572
|
+
const statusIcon = isSuccess ? "✓" : "✗";
|
|
22573
|
+
const statusColor = isSuccess ? theme.colors.success : theme.colors.error;
|
|
22574
|
+
const statusLabel = isSuccess ? "Executed successfully" : `Exit code: ${msg.exitCode}`;
|
|
22575
|
+
const expandIcon = isLongOutput ? isExpanded ? "▼" : "▶" : "";
|
|
22576
|
+
const lineCount = isLongOutput ? ` (${outputLines.length} lines)` : "";
|
|
22577
|
+
const statusText = new TextRenderable(renderer, {
|
|
22578
|
+
id: `msg-${msg.id}-status`,
|
|
22579
|
+
content: t`${fg(statusColor)(statusIcon)} ${fg(theme.colors.text)(statusLabel)}${fg(theme.colors.textMuted)(lineCount)} ${fg(theme.colors.primary)(expandIcon)}`
|
|
22580
|
+
});
|
|
22581
|
+
card.add(statusText);
|
|
22582
|
+
if (hasOutput) {
|
|
22583
|
+
let displayContent;
|
|
22584
|
+
if (isExpanded || !isLongOutput) {
|
|
22585
|
+
displayContent = msg.content.trim();
|
|
22586
|
+
} else {
|
|
22587
|
+
const previewLines = outputLines.slice(0, PREVIEW_LINES);
|
|
22588
|
+
displayContent = previewLines.join(`
|
|
22589
|
+
`) + `
|
|
22590
|
+
... ${outputLines.length - PREVIEW_LINES} more lines`;
|
|
22591
|
+
}
|
|
22592
|
+
const outputText = new TextRenderable(renderer, {
|
|
22593
|
+
id: `msg-${msg.id}-output`,
|
|
22594
|
+
content: t`${fg(theme.colors.textMuted)(displayContent)}`
|
|
22595
|
+
});
|
|
22596
|
+
card.add(outputText);
|
|
22597
|
+
if (isLongOutput) {
|
|
22598
|
+
const hintText = new TextRenderable(renderer, {
|
|
22599
|
+
id: `msg-${msg.id}-hint`,
|
|
22600
|
+
content: t`${fg(theme.colors.primary)("[o]")} ${fg(theme.colors.textMuted)(isExpanded ? "Collapse" : "Expand output")}`
|
|
22601
|
+
});
|
|
22602
|
+
card.add(hintText);
|
|
22603
|
+
}
|
|
22604
|
+
}
|
|
22605
|
+
return card;
|
|
22606
|
+
}
|
|
22607
|
+
function createSystemMessageRenderable(msg, theme) {
|
|
22608
|
+
const box = new BoxRenderable(renderer, {
|
|
22609
|
+
id: `msg-${msg.id}`,
|
|
22610
|
+
flexDirection: "column",
|
|
22611
|
+
width: "100%"
|
|
22612
|
+
});
|
|
22613
|
+
const text = new TextRenderable(renderer, {
|
|
22614
|
+
id: `msg-${msg.id}-text`,
|
|
22615
|
+
content: t`${fg(theme.colors.textMuted)(msg.content)}`
|
|
22616
|
+
});
|
|
22617
|
+
box.add(text);
|
|
22618
|
+
return box;
|
|
22619
|
+
}
|
|
22620
|
+
function updateAssistantMessage(msgId, updates) {
|
|
22621
|
+
const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
|
|
22622
|
+
if (msgIndex === -1)
|
|
22623
|
+
return;
|
|
22624
|
+
const msg = chatMessages[msgIndex];
|
|
22625
|
+
Object.assign(msg, updates);
|
|
22626
|
+
chatScrollBox.remove(`msg-${msgId}`);
|
|
22627
|
+
const theme = getTheme2();
|
|
22628
|
+
const newBox = createMessageRenderable(msg, theme);
|
|
22629
|
+
chatScrollBox.add(newBox);
|
|
22630
|
+
}
|
|
22631
|
+
function updateResultMessage(msgId, updates) {
|
|
22632
|
+
const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
|
|
22633
|
+
if (msgIndex === -1)
|
|
22634
|
+
return;
|
|
22635
|
+
const msg = chatMessages[msgIndex];
|
|
22636
|
+
Object.assign(msg, updates);
|
|
22637
|
+
chatScrollBox.remove(`msg-${msgId}`);
|
|
22638
|
+
const theme = getTheme2();
|
|
22639
|
+
const newBox = createMessageRenderable(msg, theme);
|
|
22640
|
+
chatScrollBox.add(newBox);
|
|
22641
|
+
}
|
|
22642
|
+
function toggleResultExpand(msgId) {
|
|
22643
|
+
const msg = chatMessages.find((m) => m.id === msgId);
|
|
22644
|
+
if (!msg || msg.type !== "result")
|
|
22645
|
+
return;
|
|
22646
|
+
const outputLines = msg.content?.trim().split(`
|
|
22647
|
+
`) || [];
|
|
22648
|
+
if (outputLines.length <= 5)
|
|
22649
|
+
return;
|
|
22650
|
+
updateResultMessage(msgId, { expanded: !msg.expanded });
|
|
22651
|
+
}
|
|
22652
|
+
function toggleLastResultExpand() {
|
|
22653
|
+
const resultMessages = chatMessages.filter((m) => m.type === "result");
|
|
22654
|
+
if (resultMessages.length === 0)
|
|
22655
|
+
return;
|
|
22656
|
+
const lastResult = resultMessages[resultMessages.length - 1];
|
|
22657
|
+
toggleResultExpand(lastResult.id);
|
|
21157
22658
|
}
|
|
21158
22659
|
function getModelDisplay() {
|
|
21159
22660
|
const theme = getTheme2();
|
|
@@ -21162,30 +22663,22 @@ function getModelDisplay() {
|
|
|
21162
22663
|
const freeBadge = currentModel.free ? fg(theme.colors.success)(" FREE") : "";
|
|
21163
22664
|
return t`${providerBadge} ${fg(categoryColor)(currentModel.name)}${freeBadge}`;
|
|
21164
22665
|
}
|
|
21165
|
-
function getDryRunStatus() {
|
|
21166
|
-
const theme = getTheme2();
|
|
21167
|
-
if (dryRunMode) {
|
|
21168
|
-
return t`${fg(theme.colors.warning)("[DRY RUN]")} ${fg(theme.colors.textMuted)("Ctrl+X P palette | Ctrl+X M model | Ctrl+X D dry-run")}`;
|
|
21169
|
-
}
|
|
21170
|
-
return t`${fg(theme.colors.textMuted)("Ctrl+X P palette | Ctrl+X M model | Ctrl+X ? help")}`;
|
|
21171
|
-
}
|
|
21172
22666
|
function refreshThemeColors() {
|
|
21173
22667
|
const theme = getTheme2();
|
|
21174
22668
|
renderer.setBackgroundColor(theme.colors.background);
|
|
21175
22669
|
if (headerText) {
|
|
21176
|
-
headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))}
|
|
22670
|
+
headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))}`;
|
|
21177
22671
|
}
|
|
21178
|
-
if (
|
|
21179
|
-
|
|
22672
|
+
if (statusBarText) {
|
|
22673
|
+
statusBarText.content = getStatusBarContent();
|
|
21180
22674
|
}
|
|
21181
|
-
if (
|
|
21182
|
-
|
|
22675
|
+
if (helpBarText) {
|
|
22676
|
+
helpBarText.content = getHelpBarContent();
|
|
21183
22677
|
}
|
|
21184
|
-
if (
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
outputContainer.borderColor = theme.colors.border;
|
|
22678
|
+
if (chatScrollBox) {
|
|
22679
|
+
chatScrollBox.rootOptions = {
|
|
22680
|
+
borderColor: theme.colors.border
|
|
22681
|
+
};
|
|
21189
22682
|
}
|
|
21190
22683
|
if (inputField) {
|
|
21191
22684
|
inputField.focusedBackgroundColor = theme.colors.backgroundPanel;
|
|
@@ -21193,14 +22686,6 @@ function refreshThemeColors() {
|
|
|
21193
22686
|
inputField.placeholderColor = theme.colors.textMuted;
|
|
21194
22687
|
inputField.cursorColor = theme.colors.primary;
|
|
21195
22688
|
}
|
|
21196
|
-
const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
|
|
21197
|
-
const freeModelsNote = config.provider === "opencode-zen" ? `
|
|
21198
|
-
${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
|
|
21199
|
-
if (outputText) {
|
|
21200
|
-
outputText.content = t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
|
|
21201
|
-
|
|
21202
|
-
${fg(theme.colors.textMuted)("Type what you want to do, or press")} ${fg(theme.colors.primary)("Ctrl+X P")} ${fg(theme.colors.textMuted)("for command palette.")}`;
|
|
21203
|
-
}
|
|
21204
22689
|
}
|
|
21205
22690
|
async function handleInput(value) {
|
|
21206
22691
|
const input = value.trim();
|
|
@@ -21211,8 +22696,9 @@ async function handleInput(value) {
|
|
|
21211
22696
|
await handleSpecialCommand(input);
|
|
21212
22697
|
return;
|
|
21213
22698
|
}
|
|
22699
|
+
addUserMessage(input);
|
|
21214
22700
|
if (isDirectCommand(input)) {
|
|
21215
|
-
await
|
|
22701
|
+
await processDirectCommand(input, input);
|
|
21216
22702
|
return;
|
|
21217
22703
|
}
|
|
21218
22704
|
await translateAndProcess(input);
|
|
@@ -21246,38 +22732,50 @@ function isDirectCommand(input) {
|
|
|
21246
22732
|
async function translateAndProcess(input) {
|
|
21247
22733
|
const apiKey = await getApiKey2(config.provider);
|
|
21248
22734
|
if (!apiKey) {
|
|
21249
|
-
|
|
22735
|
+
addSystemMessage("Error: No API key configured. Run !provider to set up.");
|
|
21250
22736
|
return;
|
|
21251
22737
|
}
|
|
21252
|
-
|
|
22738
|
+
const loadingMsg = addSystemMessage("Translating...");
|
|
21253
22739
|
try {
|
|
21254
|
-
const command = await translateToCommand2(apiKey, currentModel, input, currentCwd, history);
|
|
21255
|
-
|
|
22740
|
+
const command = await translateToCommand2(apiKey, currentModel, input, currentCwd, history, config.repoContext);
|
|
22741
|
+
chatScrollBox.remove(`msg-${loadingMsg.id}`);
|
|
22742
|
+
chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
|
|
21256
22743
|
const safety = analyzeCommand2(command, config);
|
|
22744
|
+
const assistantMsg = addAssistantMessage(input, command, safety);
|
|
21257
22745
|
if (safety.isDangerous) {
|
|
21258
|
-
|
|
21259
|
-
pendingCommand = command;
|
|
22746
|
+
pendingMessageId = assistantMsg.id;
|
|
21260
22747
|
awaitingConfirmation = true;
|
|
21261
|
-
|
|
21262
|
-
setOutput(t`${fg("#fbbf24")("Command requires confirmation. Press Enter to execute or Esc to cancel.")}`);
|
|
22748
|
+
helpBarText.content = getHelpBarContent();
|
|
21263
22749
|
} else {
|
|
21264
|
-
|
|
21265
|
-
await processCommand(input, command);
|
|
22750
|
+
await executeAndShowResult(input, command, assistantMsg.id);
|
|
21266
22751
|
}
|
|
21267
22752
|
} catch (error) {
|
|
22753
|
+
chatScrollBox.remove(`msg-${loadingMsg.id}`);
|
|
22754
|
+
chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
|
|
21268
22755
|
const message = error instanceof Error ? error.message : String(error);
|
|
21269
|
-
|
|
22756
|
+
addSystemMessage(`Error: ${message}`);
|
|
22757
|
+
}
|
|
22758
|
+
}
|
|
22759
|
+
async function processDirectCommand(input, command) {
|
|
22760
|
+
const safety = analyzeCommand2(command, config);
|
|
22761
|
+
const assistantMsg = addAssistantMessage(input, command, safety);
|
|
22762
|
+
if (safety.isDangerous) {
|
|
22763
|
+
pendingMessageId = assistantMsg.id;
|
|
22764
|
+
awaitingConfirmation = true;
|
|
22765
|
+
helpBarText.content = getHelpBarContent();
|
|
22766
|
+
} else {
|
|
22767
|
+
await executeAndShowResult(input, command, assistantMsg.id);
|
|
21270
22768
|
}
|
|
21271
22769
|
}
|
|
21272
|
-
async function
|
|
22770
|
+
async function executeAndShowResult(input, command, assistantMsgId) {
|
|
21273
22771
|
if (command.startsWith("cd ")) {
|
|
21274
22772
|
const path2 = command.slice(3).trim().replace(/^["']|["']$/g, "");
|
|
21275
22773
|
try {
|
|
21276
22774
|
const expandedPath = path2.startsWith("~") ? path2.replace("~", process.env.HOME || "") : path2;
|
|
21277
22775
|
process.chdir(expandedPath);
|
|
21278
22776
|
currentCwd = getCwd();
|
|
21279
|
-
|
|
21280
|
-
|
|
22777
|
+
statusBarText.content = getStatusBarContent();
|
|
22778
|
+
addResultMessage(`Changed directory to ${currentCwd}`, 0);
|
|
21281
22779
|
addToHistory({
|
|
21282
22780
|
input,
|
|
21283
22781
|
command,
|
|
@@ -21285,35 +22783,37 @@ async function processCommand(input, command) {
|
|
|
21285
22783
|
timestamp: Date.now()
|
|
21286
22784
|
});
|
|
21287
22785
|
history = loadHistory2();
|
|
22786
|
+
updateAssistantMessage(assistantMsgId, { executed: true });
|
|
21288
22787
|
} catch (err) {
|
|
21289
|
-
|
|
22788
|
+
addResultMessage(`cd: ${err instanceof Error ? err.message : String(err)}`, 1);
|
|
21290
22789
|
}
|
|
21291
22790
|
clearCommandState();
|
|
21292
22791
|
return;
|
|
21293
22792
|
}
|
|
21294
22793
|
if (dryRunMode) {
|
|
21295
|
-
|
|
22794
|
+
addResultMessage(`[DRY RUN] Would execute: ${command}`, 0);
|
|
22795
|
+
updateAssistantMessage(assistantMsgId, { executed: true });
|
|
21296
22796
|
clearCommandState();
|
|
21297
22797
|
return;
|
|
21298
22798
|
}
|
|
21299
|
-
setOutput(t`${fg("#64748b")("Executing...")}`);
|
|
21300
22799
|
try {
|
|
21301
|
-
const
|
|
21302
|
-
|
|
22800
|
+
const { output, exitCode } = await executeCommandWithCode(command);
|
|
22801
|
+
addResultMessage(output || "Command completed successfully", exitCode);
|
|
21303
22802
|
addToHistory({
|
|
21304
22803
|
input,
|
|
21305
22804
|
command,
|
|
21306
|
-
output:
|
|
22805
|
+
output: output.slice(0, 500),
|
|
21307
22806
|
timestamp: Date.now()
|
|
21308
22807
|
});
|
|
21309
22808
|
history = loadHistory2();
|
|
22809
|
+
updateAssistantMessage(assistantMsgId, { executed: true });
|
|
21310
22810
|
} catch (error) {
|
|
21311
22811
|
const message = error instanceof Error ? error.message : String(error);
|
|
21312
|
-
|
|
22812
|
+
addResultMessage(`Error: ${message}`, 1);
|
|
21313
22813
|
}
|
|
21314
22814
|
clearCommandState();
|
|
21315
22815
|
}
|
|
21316
|
-
function
|
|
22816
|
+
function executeCommandWithCode(command) {
|
|
21317
22817
|
return new Promise((resolve3, reject) => {
|
|
21318
22818
|
const child = spawn(command, {
|
|
21319
22819
|
shell: true,
|
|
@@ -21332,23 +22832,16 @@ function executeCommand(command) {
|
|
|
21332
22832
|
reject(error);
|
|
21333
22833
|
});
|
|
21334
22834
|
child.on("close", (code) => {
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
21338
|
-
resolve3(stderr || stdout || `Command exited with code ${code}`);
|
|
21339
|
-
}
|
|
22835
|
+
const exitCode = code ?? 0;
|
|
22836
|
+
const output = stdout || stderr || (exitCode === 0 ? "" : `Command exited with code ${exitCode}`);
|
|
22837
|
+
resolve3({ output, exitCode });
|
|
21340
22838
|
});
|
|
21341
22839
|
});
|
|
21342
22840
|
}
|
|
21343
22841
|
function clearCommandState() {
|
|
21344
|
-
|
|
22842
|
+
pendingMessageId = null;
|
|
21345
22843
|
awaitingConfirmation = false;
|
|
21346
|
-
|
|
21347
|
-
commandPreview.content = "";
|
|
21348
|
-
safetyWarning.content = "";
|
|
21349
|
-
}
|
|
21350
|
-
function setOutput(content) {
|
|
21351
|
-
outputText.content = content;
|
|
22844
|
+
helpBarText.content = getHelpBarContent();
|
|
21352
22845
|
}
|
|
21353
22846
|
async function handleSpecialCommand(input) {
|
|
21354
22847
|
const cmd = input.slice(1).toLowerCase().trim();
|
|
@@ -21364,8 +22857,8 @@ async function handleSpecialCommand(input) {
|
|
|
21364
22857
|
break;
|
|
21365
22858
|
case "dry":
|
|
21366
22859
|
dryRunMode = !dryRunMode;
|
|
21367
|
-
|
|
21368
|
-
|
|
22860
|
+
statusBarText.content = getStatusBarContent();
|
|
22861
|
+
addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
|
|
21369
22862
|
break;
|
|
21370
22863
|
case "config":
|
|
21371
22864
|
await showConfig();
|
|
@@ -21374,65 +22867,74 @@ async function handleSpecialCommand(input) {
|
|
|
21374
22867
|
showHistory();
|
|
21375
22868
|
break;
|
|
21376
22869
|
case "clear":
|
|
21377
|
-
|
|
22870
|
+
clearChat();
|
|
21378
22871
|
break;
|
|
21379
22872
|
default:
|
|
21380
22873
|
if (cmd) {
|
|
21381
|
-
|
|
22874
|
+
addUserMessage(cmd);
|
|
22875
|
+
await processDirectCommand(input, cmd);
|
|
21382
22876
|
}
|
|
21383
22877
|
}
|
|
21384
22878
|
}
|
|
22879
|
+
function clearChat() {
|
|
22880
|
+
for (const msg of chatMessages) {
|
|
22881
|
+
chatScrollBox.remove(`msg-${msg.id}`);
|
|
22882
|
+
}
|
|
22883
|
+
chatMessages = [];
|
|
22884
|
+
addSystemMessage(getWelcomeMessage());
|
|
22885
|
+
}
|
|
21385
22886
|
function showHelp() {
|
|
21386
|
-
const
|
|
21387
|
-
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
${fg(theme.colors.primary)("C")} ${fg(theme.colors.textMuted)("Show config")} ${fg(theme.colors.primary)("L")} ${fg(theme.colors.textMuted)("Clear output")}
|
|
21394
|
-
${fg(theme.colors.primary)("?")} ${fg(theme.colors.textMuted)("This help")} ${fg(theme.colors.primary)("Q")} ${fg(theme.colors.textMuted)("Exit")}
|
|
22887
|
+
const helpText = `Keyboard Shortcuts (Ctrl+X then...):
|
|
22888
|
+
P Command palette M Change model
|
|
22889
|
+
S Switch provider D Toggle dry-run
|
|
22890
|
+
T Change theme R Toggle repo context
|
|
22891
|
+
H Show history L Clear chat
|
|
22892
|
+
C Show config ? This help
|
|
22893
|
+
Q Exit
|
|
21395
22894
|
|
|
21396
|
-
|
|
21397
|
-
|
|
22895
|
+
Other:
|
|
22896
|
+
Ctrl+C Exit / Cancel Esc Close palette
|
|
21398
22897
|
|
|
21399
|
-
|
|
22898
|
+
Tips:
|
|
21400
22899
|
- Type naturally: "list all files" -> ls -la
|
|
21401
22900
|
- Reference history: "do that again", "undo"
|
|
21402
|
-
-
|
|
22901
|
+
- Enable repo context to use project scripts (Ctrl+X R)`;
|
|
22902
|
+
addSystemMessage(helpText);
|
|
21403
22903
|
}
|
|
21404
22904
|
async function showConfig() {
|
|
21405
22905
|
const theme = getTheme2();
|
|
21406
22906
|
const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
|
|
21407
22907
|
const apiKey = await getApiKey2(config.provider);
|
|
21408
|
-
const apiKeyStatus = apiKey ?
|
|
21409
|
-
const freeBadge = currentModel.free ?
|
|
22908
|
+
const apiKeyStatus = apiKey ? "configured" : "not set";
|
|
22909
|
+
const freeBadge = currentModel.free ? " (FREE)" : "";
|
|
21410
22910
|
const shellInfo = getShellInfo2();
|
|
21411
|
-
|
|
22911
|
+
const configText = `Current Configuration
|
|
21412
22912
|
|
|
21413
|
-
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
|
|
21419
|
-
|
|
21420
|
-
|
|
21421
|
-
|
|
21422
|
-
${
|
|
21423
|
-
|
|
22913
|
+
Provider: ${providerName}
|
|
22914
|
+
Model: ${currentModel.name}${freeBadge}
|
|
22915
|
+
Model ID: ${currentModel.id}
|
|
22916
|
+
Category: ${currentModel.category}
|
|
22917
|
+
Theme: ${theme.name}
|
|
22918
|
+
Shell: ${shellInfo.shell} (${shellInfo.shellPath})
|
|
22919
|
+
Platform: ${shellInfo.platform}${shellInfo.isWSL ? " (WSL)" : ""}
|
|
22920
|
+
Safety: ${config.safetyLevel}
|
|
22921
|
+
Dry-run: ${dryRunMode ? "ON" : "OFF"}
|
|
22922
|
+
Repo context: ${config.repoContext ? "ON" : "OFF"}
|
|
22923
|
+
API Key: ${apiKeyStatus}
|
|
22924
|
+
History: ${history.length} commands`;
|
|
22925
|
+
addSystemMessage(configText);
|
|
21424
22926
|
}
|
|
21425
22927
|
function showHistory() {
|
|
21426
22928
|
if (history.length === 0) {
|
|
21427
|
-
|
|
22929
|
+
addSystemMessage("No command history yet.");
|
|
21428
22930
|
return;
|
|
21429
22931
|
}
|
|
21430
22932
|
const recent = history.slice(-10);
|
|
21431
22933
|
const lines = recent.map((entry, i) => {
|
|
21432
22934
|
const date = new Date(entry.timestamp).toLocaleTimeString();
|
|
21433
|
-
return
|
|
22935
|
+
return `${i + 1}. [${date}] ${entry.command}`;
|
|
21434
22936
|
});
|
|
21435
|
-
|
|
22937
|
+
addSystemMessage(`Recent Command History
|
|
21436
22938
|
|
|
21437
22939
|
${lines.join(`
|
|
21438
22940
|
`)}`);
|
|
@@ -21498,10 +23000,10 @@ async function switchProvider() {
|
|
|
21498
23000
|
currentModel = models.find((m) => m.id === config.defaultModel) || models[0];
|
|
21499
23001
|
config.defaultModel = currentModel.id;
|
|
21500
23002
|
saveConfig2(config);
|
|
21501
|
-
|
|
23003
|
+
statusBarText.content = getStatusBarContent();
|
|
21502
23004
|
closeSelector();
|
|
21503
23005
|
const providerName = newProvider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
|
|
21504
|
-
|
|
23006
|
+
addSystemMessage(`Switched to ${providerName}. Model: ${currentModel.name}`);
|
|
21505
23007
|
} else {
|
|
21506
23008
|
closeSelector();
|
|
21507
23009
|
renderer.root.remove("main-container");
|
|
@@ -21567,12 +23069,12 @@ function showModelSelector() {
|
|
|
21567
23069
|
currentModel = option.value;
|
|
21568
23070
|
config.defaultModel = currentModel.id;
|
|
21569
23071
|
saveConfig2(config);
|
|
21570
|
-
|
|
23072
|
+
statusBarText.content = getStatusBarContent();
|
|
21571
23073
|
renderer.root.remove("model-selector-container");
|
|
21572
23074
|
modelSelector = null;
|
|
21573
23075
|
inputField.focus();
|
|
21574
23076
|
const freeBadge = currentModel.free ? " (FREE)" : "";
|
|
21575
|
-
|
|
23077
|
+
addSystemMessage(`Model changed to ${currentModel.name}${freeBadge}`);
|
|
21576
23078
|
});
|
|
21577
23079
|
modelSelector.focus();
|
|
21578
23080
|
}
|
|
@@ -21629,8 +23131,7 @@ function showThemeSelector() {
|
|
|
21629
23131
|
renderer.root.remove("theme-selector-container");
|
|
21630
23132
|
themeSelector = null;
|
|
21631
23133
|
refreshThemeColors();
|
|
21632
|
-
|
|
21633
|
-
setOutput(t`${fg(newTheme.colors.success)(`Theme changed to ${themeName}`)}`);
|
|
23134
|
+
addSystemMessage(`Theme changed to ${themeName}`);
|
|
21634
23135
|
inputField.focus();
|
|
21635
23136
|
});
|
|
21636
23137
|
const escHandler = (key) => {
|
|
@@ -21674,8 +23175,20 @@ function getCommandPaletteOptions() {
|
|
|
21674
23175
|
chord: "d",
|
|
21675
23176
|
action: () => {
|
|
21676
23177
|
dryRunMode = !dryRunMode;
|
|
21677
|
-
|
|
21678
|
-
|
|
23178
|
+
statusBarText.content = getStatusBarContent();
|
|
23179
|
+
addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
|
|
23180
|
+
}
|
|
23181
|
+
},
|
|
23182
|
+
{
|
|
23183
|
+
name: "Toggle Project Context",
|
|
23184
|
+
description: config.repoContext ? "Currently ON (sends script names to AI)" : "Currently OFF",
|
|
23185
|
+
key: "r",
|
|
23186
|
+
chord: "r",
|
|
23187
|
+
action: () => {
|
|
23188
|
+
config.repoContext = !config.repoContext;
|
|
23189
|
+
saveConfig2(config);
|
|
23190
|
+
statusBarText.content = getStatusBarContent();
|
|
23191
|
+
addSystemMessage(`Project context: ${config.repoContext ? "ON - AI can see your package.json scripts, Makefile targets, etc." : "OFF"}`);
|
|
21679
23192
|
}
|
|
21680
23193
|
},
|
|
21681
23194
|
{
|
|
@@ -21700,11 +23213,11 @@ function getCommandPaletteOptions() {
|
|
|
21700
23213
|
action: () => showThemeSelector()
|
|
21701
23214
|
},
|
|
21702
23215
|
{
|
|
21703
|
-
name: "Clear
|
|
21704
|
-
description: "Clear the
|
|
23216
|
+
name: "Clear Chat",
|
|
23217
|
+
description: "Clear the chat history",
|
|
21705
23218
|
key: "l",
|
|
21706
23219
|
chord: "l",
|
|
21707
|
-
action: () =>
|
|
23220
|
+
action: () => clearChat()
|
|
21708
23221
|
},
|
|
21709
23222
|
{
|
|
21710
23223
|
name: "Show Help",
|
|
@@ -21845,24 +23358,44 @@ function handleKeypress(key) {
|
|
|
21845
23358
|
inputField.focus();
|
|
21846
23359
|
return;
|
|
21847
23360
|
}
|
|
21848
|
-
if (awaitingConfirmation) {
|
|
23361
|
+
if (awaitingConfirmation && pendingMessageId) {
|
|
21849
23362
|
clearCommandState();
|
|
21850
|
-
|
|
23363
|
+
addSystemMessage("Command cancelled.");
|
|
21851
23364
|
inputField.focus();
|
|
21852
23365
|
}
|
|
21853
23366
|
}
|
|
21854
|
-
if (key.name === "return" && awaitingConfirmation &&
|
|
21855
|
-
const
|
|
21856
|
-
|
|
21857
|
-
|
|
23367
|
+
if (key.name === "return" && awaitingConfirmation && pendingMessageId) {
|
|
23368
|
+
const msg = chatMessages.find((m) => m.id === pendingMessageId);
|
|
23369
|
+
if (msg && msg.command) {
|
|
23370
|
+
const command = msg.command;
|
|
23371
|
+
const msgId = pendingMessageId;
|
|
23372
|
+
clearCommandState();
|
|
23373
|
+
executeAndShowResult(msg.content, command, msgId);
|
|
23374
|
+
}
|
|
21858
23375
|
}
|
|
21859
|
-
if (key.name === "e" && awaitingConfirmation &&
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
23376
|
+
if (key.name === "e" && awaitingConfirmation && pendingMessageId) {
|
|
23377
|
+
const msg = chatMessages.find((m) => m.id === pendingMessageId);
|
|
23378
|
+
if (msg && msg.command) {
|
|
23379
|
+
inputField.value = msg.command;
|
|
23380
|
+
clearCommandState();
|
|
23381
|
+
inputField.focus();
|
|
23382
|
+
}
|
|
23383
|
+
}
|
|
23384
|
+
if (key.name === "c" && awaitingConfirmation && pendingMessageId) {
|
|
23385
|
+
const msg = chatMessages.find((m) => m.id === pendingMessageId);
|
|
23386
|
+
if (msg && msg.command) {
|
|
23387
|
+
const copyCmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard";
|
|
23388
|
+
const child = spawn(copyCmd, { shell: true });
|
|
23389
|
+
child.stdin?.write(msg.command);
|
|
23390
|
+
child.stdin?.end();
|
|
23391
|
+
addSystemMessage(`Copied to clipboard: ${msg.command}`);
|
|
23392
|
+
}
|
|
23393
|
+
}
|
|
23394
|
+
if (key.name === "o" && !awaitingConfirmation && !commandPalette && !modelSelector) {
|
|
23395
|
+
toggleLastResultExpand();
|
|
21863
23396
|
}
|
|
21864
23397
|
}
|
|
21865
|
-
var renderer, currentModel, config, history, currentCwd, dryRunMode = false,
|
|
23398
|
+
var renderer, currentModel, config, history, currentCwd, dryRunMode = false, chatMessages, messageIdCounter = 0, mainContainer, headerText, statusBarText, chatScrollBox, inputField, helpBarText, modelSelector = null, providerSelector = null, pendingMessageId = null, awaitingConfirmation = false, themeSelector = null, commandPalette = null, chordMode = "none", cli_default;
|
|
21866
23399
|
var init_cli = __esm(async () => {
|
|
21867
23400
|
init_types();
|
|
21868
23401
|
init_config();
|
|
@@ -21873,6 +23406,7 @@ var init_cli = __esm(async () => {
|
|
|
21873
23406
|
currentModel = OPENCODE_ZEN_MODELS2[0];
|
|
21874
23407
|
history = [];
|
|
21875
23408
|
currentCwd = getCwd();
|
|
23409
|
+
chatMessages = [];
|
|
21876
23410
|
if (false) {}
|
|
21877
23411
|
cli_default = main2;
|
|
21878
23412
|
});
|
|
@@ -22182,7 +23716,7 @@ var DEFAULT_CONFIG = {
|
|
|
22182
23716
|
provider: "opencode-zen",
|
|
22183
23717
|
openrouterApiKey: "",
|
|
22184
23718
|
opencodeZenApiKey: "",
|
|
22185
|
-
defaultModel: "
|
|
23719
|
+
defaultModel: "gemini-3-flash",
|
|
22186
23720
|
safetyLevel: "moderate",
|
|
22187
23721
|
dryRunByDefault: false,
|
|
22188
23722
|
blockedCommands: [
|
|
@@ -22193,7 +23727,8 @@ var DEFAULT_CONFIG = {
|
|
|
22193
23727
|
"chmod -R 777 /",
|
|
22194
23728
|
"chown -R"
|
|
22195
23729
|
],
|
|
22196
|
-
confirmedDangerousPatterns: []
|
|
23730
|
+
confirmedDangerousPatterns: [],
|
|
23731
|
+
repoContext: false
|
|
22197
23732
|
};
|
|
22198
23733
|
function ensureConfigDir() {
|
|
22199
23734
|
if (!existsSync(CONFIG_DIR)) {
|
|
@@ -22394,6 +23929,7 @@ function getSeverityMessage(severity) {
|
|
|
22394
23929
|
|
|
22395
23930
|
// src/lib/api.ts
|
|
22396
23931
|
init_shell();
|
|
23932
|
+
init_repo_context();
|
|
22397
23933
|
function getZenApiType(modelId) {
|
|
22398
23934
|
if (modelId.startsWith("gpt-")) {
|
|
22399
23935
|
return "openai-responses";
|
|
@@ -22406,11 +23942,21 @@ function getZenApiType(modelId) {
|
|
|
22406
23942
|
}
|
|
22407
23943
|
return "openai-compatible";
|
|
22408
23944
|
}
|
|
22409
|
-
function buildSystemPrompt(cwd, history, shellInfo) {
|
|
23945
|
+
function buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled) {
|
|
22410
23946
|
const historyContext = formatHistory(history);
|
|
22411
23947
|
const platformPaths = getPlatformPaths(shellInfo.platform);
|
|
22412
23948
|
const shellHints = getShellSyntaxHints(shellInfo.shell);
|
|
22413
23949
|
const platformName = shellInfo.platform === "macos" ? "macOS" : shellInfo.platform === "windows" ? "Windows" : shellInfo.platform === "linux" ? shellInfo.isWSL ? "Linux (WSL)" : "Linux" : "Unknown";
|
|
23950
|
+
let projectContextSection = "";
|
|
23951
|
+
if (repoContextEnabled) {
|
|
23952
|
+
const repoContext = detectRepoContext(cwd);
|
|
23953
|
+
if (repoContext) {
|
|
23954
|
+
projectContextSection = `
|
|
23955
|
+
Project context:
|
|
23956
|
+
${formatRepoContext(repoContext)}
|
|
23957
|
+
`;
|
|
23958
|
+
}
|
|
23959
|
+
}
|
|
22414
23960
|
return `You are a shell command translator. Convert the user's natural language request into a shell command.
|
|
22415
23961
|
|
|
22416
23962
|
Current environment:
|
|
@@ -22419,7 +23965,7 @@ Current environment:
|
|
|
22419
23965
|
- Working directory: ${cwd}
|
|
22420
23966
|
- Home directory: ${shellInfo.homeDir}
|
|
22421
23967
|
${shellInfo.terminalEmulator ? `- Terminal: ${shellInfo.terminalEmulator}` : ""}
|
|
22422
|
-
|
|
23968
|
+
${projectContextSection}
|
|
22423
23969
|
${shellHints}
|
|
22424
23970
|
|
|
22425
23971
|
Recent command history:
|
|
@@ -22430,7 +23976,8 @@ Rules:
|
|
|
22430
23976
|
- No explanations, no markdown, no backticks, no code blocks
|
|
22431
23977
|
- Use the correct syntax for the detected shell (${shellInfo.shell})
|
|
22432
23978
|
- If the request is unclear, make a reasonable assumption
|
|
22433
|
-
- Prefer simple, common commands over complex one-liners
|
|
23979
|
+
- Prefer simple, common commands over complex one-liners${repoContextEnabled ? `
|
|
23980
|
+
- Use project-specific commands when relevant (e.g., use the detected package manager and available scripts)` : ""}
|
|
22434
23981
|
- Use the command history for context (e.g., "do that again", "undo", "delete the file I just created")
|
|
22435
23982
|
- If the user asks something that can't be done with a shell command, output a command that prints a helpful message
|
|
22436
23983
|
- For file operations, prefer safer alternatives when possible
|
|
@@ -22704,9 +24251,9 @@ function getShellInfo() {
|
|
|
22704
24251
|
}
|
|
22705
24252
|
return cachedShellInfo;
|
|
22706
24253
|
}
|
|
22707
|
-
async function translateToCommand(apiKey, model, userInput, cwd, history = []) {
|
|
24254
|
+
async function translateToCommand(apiKey, model, userInput, cwd, history = [], repoContextEnabled) {
|
|
22708
24255
|
const shellInfo = getShellInfo();
|
|
22709
|
-
const systemPrompt = buildSystemPrompt(cwd, history, shellInfo);
|
|
24256
|
+
const systemPrompt = buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled);
|
|
22710
24257
|
let rawCommand;
|
|
22711
24258
|
if (model.provider === "openrouter") {
|
|
22712
24259
|
rawCommand = await callOpenRouter(apiKey, model.id, systemPrompt, userInput);
|
|
@@ -22947,6 +24494,128 @@ function getAnsiColors() {
|
|
|
22947
24494
|
}
|
|
22948
24495
|
loadTheme();
|
|
22949
24496
|
|
|
24497
|
+
// src/lib/update-checker.ts
|
|
24498
|
+
import { homedir as homedir4 } from "os";
|
|
24499
|
+
import { join as join4 } from "path";
|
|
24500
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
24501
|
+
var __dirname = "/Users/austin/code/dxd/magic-shell/src/lib";
|
|
24502
|
+
var PACKAGE_NAME = "@austinthesing/magic-shell";
|
|
24503
|
+
var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
24504
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
24505
|
+
var CONFIG_DIR3 = join4(homedir4(), ".magic-shell");
|
|
24506
|
+
var UPDATE_CHECK_FILE = join4(CONFIG_DIR3, ".update-check");
|
|
24507
|
+
function ensureConfigDir3() {
|
|
24508
|
+
if (!existsSync5(CONFIG_DIR3)) {
|
|
24509
|
+
mkdirSync3(CONFIG_DIR3, { recursive: true });
|
|
24510
|
+
}
|
|
24511
|
+
}
|
|
24512
|
+
function loadUpdateState() {
|
|
24513
|
+
ensureConfigDir3();
|
|
24514
|
+
try {
|
|
24515
|
+
if (existsSync5(UPDATE_CHECK_FILE)) {
|
|
24516
|
+
return JSON.parse(readFileSync4(UPDATE_CHECK_FILE, "utf-8"));
|
|
24517
|
+
}
|
|
24518
|
+
} catch {}
|
|
24519
|
+
return { lastCheck: 0, latestVersion: null, dismissed: null };
|
|
24520
|
+
}
|
|
24521
|
+
function saveUpdateState(state) {
|
|
24522
|
+
ensureConfigDir3();
|
|
24523
|
+
try {
|
|
24524
|
+
writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(state));
|
|
24525
|
+
} catch {}
|
|
24526
|
+
}
|
|
24527
|
+
function getCurrentVersion() {
|
|
24528
|
+
try {
|
|
24529
|
+
const packagePaths = [
|
|
24530
|
+
join4(__dirname, "../../package.json"),
|
|
24531
|
+
join4(__dirname, "../../../package.json"),
|
|
24532
|
+
join4(process.cwd(), "package.json")
|
|
24533
|
+
];
|
|
24534
|
+
for (const path of packagePaths) {
|
|
24535
|
+
if (existsSync5(path)) {
|
|
24536
|
+
const pkg = JSON.parse(readFileSync4(path, "utf-8"));
|
|
24537
|
+
if (pkg.name === PACKAGE_NAME || pkg.name === "magic-shell") {
|
|
24538
|
+
return pkg.version;
|
|
24539
|
+
}
|
|
24540
|
+
}
|
|
24541
|
+
}
|
|
24542
|
+
} catch {}
|
|
24543
|
+
return "0.0.0";
|
|
24544
|
+
}
|
|
24545
|
+
function compareVersions(a, b) {
|
|
24546
|
+
const partsA = a.split(".").map(Number);
|
|
24547
|
+
const partsB = b.split(".").map(Number);
|
|
24548
|
+
for (let i = 0;i < Math.max(partsA.length, partsB.length); i++) {
|
|
24549
|
+
const numA = partsA[i] || 0;
|
|
24550
|
+
const numB = partsB[i] || 0;
|
|
24551
|
+
if (numA > numB)
|
|
24552
|
+
return 1;
|
|
24553
|
+
if (numA < numB)
|
|
24554
|
+
return -1;
|
|
24555
|
+
}
|
|
24556
|
+
return 0;
|
|
24557
|
+
}
|
|
24558
|
+
async function fetchLatestVersion() {
|
|
24559
|
+
try {
|
|
24560
|
+
const controller = new AbortController;
|
|
24561
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
24562
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
24563
|
+
signal: controller.signal,
|
|
24564
|
+
headers: { Accept: "application/json" }
|
|
24565
|
+
});
|
|
24566
|
+
clearTimeout(timeout);
|
|
24567
|
+
if (!response.ok)
|
|
24568
|
+
return null;
|
|
24569
|
+
const data = await response.json();
|
|
24570
|
+
return data.version || null;
|
|
24571
|
+
} catch {
|
|
24572
|
+
return null;
|
|
24573
|
+
}
|
|
24574
|
+
}
|
|
24575
|
+
async function checkForUpdates() {
|
|
24576
|
+
const state = loadUpdateState();
|
|
24577
|
+
const currentVersion = getCurrentVersion();
|
|
24578
|
+
const now = Date.now();
|
|
24579
|
+
if (now - state.lastCheck < CHECK_INTERVAL_MS) {
|
|
24580
|
+
if (state.latestVersion && compareVersions(state.latestVersion, currentVersion) > 0) {
|
|
24581
|
+
if (state.dismissed === state.latestVersion) {
|
|
24582
|
+
return null;
|
|
24583
|
+
}
|
|
24584
|
+
return {
|
|
24585
|
+
hasUpdate: true,
|
|
24586
|
+
currentVersion,
|
|
24587
|
+
latestVersion: state.latestVersion,
|
|
24588
|
+
updateCommand: `bun update -g ${PACKAGE_NAME}`
|
|
24589
|
+
};
|
|
24590
|
+
}
|
|
24591
|
+
return null;
|
|
24592
|
+
}
|
|
24593
|
+
const latestVersion = await fetchLatestVersion();
|
|
24594
|
+
state.lastCheck = now;
|
|
24595
|
+
if (latestVersion) {
|
|
24596
|
+
state.latestVersion = latestVersion;
|
|
24597
|
+
}
|
|
24598
|
+
saveUpdateState(state);
|
|
24599
|
+
if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
|
|
24600
|
+
if (state.dismissed === latestVersion) {
|
|
24601
|
+
return null;
|
|
24602
|
+
}
|
|
24603
|
+
return {
|
|
24604
|
+
hasUpdate: true,
|
|
24605
|
+
currentVersion,
|
|
24606
|
+
latestVersion,
|
|
24607
|
+
updateCommand: `bun update -g ${PACKAGE_NAME}`
|
|
24608
|
+
};
|
|
24609
|
+
}
|
|
24610
|
+
return null;
|
|
24611
|
+
}
|
|
24612
|
+
async function forceCheckForUpdates() {
|
|
24613
|
+
const state = loadUpdateState();
|
|
24614
|
+
state.lastCheck = 0;
|
|
24615
|
+
saveUpdateState(state);
|
|
24616
|
+
return checkForUpdates();
|
|
24617
|
+
}
|
|
24618
|
+
|
|
22950
24619
|
// src/index.ts
|
|
22951
24620
|
loadTheme();
|
|
22952
24621
|
var getColors = () => {
|
|
@@ -22978,6 +24647,10 @@ ${colors.bold}USAGE${colors.reset}
|
|
|
22978
24647
|
msh --provider <name> Set provider (opencode-zen or openrouter)
|
|
22979
24648
|
msh --themes List available themes
|
|
22980
24649
|
msh --theme <name> Set color theme
|
|
24650
|
+
msh --repo-context Enable project context detection
|
|
24651
|
+
msh --no-repo-context Disable project context detection
|
|
24652
|
+
msh --version Show version
|
|
24653
|
+
msh --check-update Check for updates
|
|
22981
24654
|
msh --help Show this help
|
|
22982
24655
|
|
|
22983
24656
|
${colors.bold}EXAMPLES${colors.reset}
|
|
@@ -22990,6 +24663,9 @@ ${colors.bold}EXAMPLES${colors.reset}
|
|
|
22990
24663
|
${colors.dim}# Check what would run${colors.reset}
|
|
22991
24664
|
msh -n "delete all log files"
|
|
22992
24665
|
|
|
24666
|
+
${colors.dim}# Use project context (knows your npm scripts, etc)${colors.reset}
|
|
24667
|
+
msh --repo-context "run the dev server"
|
|
24668
|
+
|
|
22993
24669
|
${colors.dim}# Pipe to clipboard (macOS)${colors.reset}
|
|
22994
24670
|
msh "find large files" | pbcopy
|
|
22995
24671
|
|
|
@@ -23165,7 +24841,7 @@ Try: ${colors.cyan}msh "list all files"${colors.reset}
|
|
|
23165
24841
|
`);
|
|
23166
24842
|
rl.close();
|
|
23167
24843
|
}
|
|
23168
|
-
function
|
|
24844
|
+
function executeCommand(command) {
|
|
23169
24845
|
return new Promise((resolve3) => {
|
|
23170
24846
|
const child = spawn2(command, {
|
|
23171
24847
|
shell: true,
|
|
@@ -23193,6 +24869,27 @@ function executeCommand2(command) {
|
|
|
23193
24869
|
});
|
|
23194
24870
|
});
|
|
23195
24871
|
}
|
|
24872
|
+
function createSpinner(message) {
|
|
24873
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
24874
|
+
let i = 0;
|
|
24875
|
+
const isTTY = process.stderr.isTTY;
|
|
24876
|
+
const interval = isTTY ? setInterval(() => {
|
|
24877
|
+
process.stderr.write(`\r${colors.primary}${frames[i]}${colors.reset} ${colors.dim}${message}${colors.reset}`);
|
|
24878
|
+
i = (i + 1) % frames.length;
|
|
24879
|
+
}, 80) : null;
|
|
24880
|
+
if (!isTTY) {
|
|
24881
|
+
process.stderr.write(`${colors.dim}${message}...${colors.reset}
|
|
24882
|
+
`);
|
|
24883
|
+
}
|
|
24884
|
+
return {
|
|
24885
|
+
stop: () => {
|
|
24886
|
+
if (interval) {
|
|
24887
|
+
clearInterval(interval);
|
|
24888
|
+
process.stderr.write("\r\x1B[K");
|
|
24889
|
+
}
|
|
24890
|
+
}
|
|
24891
|
+
};
|
|
24892
|
+
}
|
|
23196
24893
|
async function translate(query, options) {
|
|
23197
24894
|
const config2 = loadConfig();
|
|
23198
24895
|
const apiKey = await getApiKey(config2.provider);
|
|
@@ -23204,12 +24901,18 @@ async function translate(query, options) {
|
|
|
23204
24901
|
const model = ALL_MODELS.find((m) => m.id === config2.defaultModel) || (config2.provider === "opencode-zen" ? OPENCODE_ZEN_MODELS[0] : OPENROUTER_MODELS[0]);
|
|
23205
24902
|
const history2 = loadHistory();
|
|
23206
24903
|
const cwd = getCwd2();
|
|
24904
|
+
const useRepoContext = options.repoContext ?? config2.repoContext ?? false;
|
|
24905
|
+
const spinner = createSpinner(`Translating with ${model.name}`);
|
|
23207
24906
|
try {
|
|
23208
|
-
const command = await translateToCommand(apiKey, model, query, cwd, history2);
|
|
24907
|
+
const command = await translateToCommand(apiKey, model, query, cwd, history2, useRepoContext);
|
|
24908
|
+
spinner.stop();
|
|
23209
24909
|
if (options.dryRun) {
|
|
23210
24910
|
const safety = analyzeCommand(command, config2);
|
|
23211
24911
|
console.log(`${colors.dim}Query:${colors.reset} ${query}`);
|
|
23212
24912
|
console.log(`${colors.dim}Model:${colors.reset} ${model.name}`);
|
|
24913
|
+
if (useRepoContext) {
|
|
24914
|
+
console.log(`${colors.dim}Project context:${colors.reset} enabled`);
|
|
24915
|
+
}
|
|
23213
24916
|
console.log();
|
|
23214
24917
|
console.log(`${colors.bold}Command:${colors.reset} ${command}`);
|
|
23215
24918
|
if (safety.isDangerous) {
|
|
@@ -23228,33 +24931,64 @@ async function translate(query, options) {
|
|
|
23228
24931
|
console.error(`${colors.yellow}Use -n to preview, or run the command manually.${colors.reset}`);
|
|
23229
24932
|
process.exit(1);
|
|
23230
24933
|
}
|
|
23231
|
-
const result = await
|
|
24934
|
+
const result = await executeCommand(command);
|
|
23232
24935
|
process.exit(result.code);
|
|
23233
24936
|
} else {
|
|
23234
24937
|
console.log(command);
|
|
23235
24938
|
}
|
|
23236
24939
|
} catch (error) {
|
|
24940
|
+
spinner.stop();
|
|
23237
24941
|
const message = error instanceof Error ? error.message : String(error);
|
|
23238
24942
|
console.error(`${colors.red}Error: ${message}${colors.reset}`);
|
|
23239
24943
|
process.exit(1);
|
|
23240
24944
|
}
|
|
23241
24945
|
}
|
|
24946
|
+
async function showUpdateNotification() {
|
|
24947
|
+
try {
|
|
24948
|
+
const update = await checkForUpdates();
|
|
24949
|
+
if (update?.hasUpdate) {
|
|
24950
|
+
console.error(`${colors.cyan}\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510${colors.reset}`);
|
|
24951
|
+
console.error(`${colors.cyan}\u2502${colors.reset} ${colors.bold}Update available!${colors.reset} ${colors.dim}${update.currentVersion}${colors.reset} \u2192 ${colors.green}${update.latestVersion}${colors.reset} ${colors.cyan}\u2502${colors.reset}`);
|
|
24952
|
+
console.error(`${colors.cyan}\u2502${colors.reset} Run: ${colors.yellow}${update.updateCommand}${colors.reset} ${colors.cyan}\u2502${colors.reset}`);
|
|
24953
|
+
console.error(`${colors.cyan}\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518${colors.reset}`);
|
|
24954
|
+
console.error();
|
|
24955
|
+
}
|
|
24956
|
+
} catch {}
|
|
24957
|
+
}
|
|
23242
24958
|
async function main3() {
|
|
23243
24959
|
const args = process.argv.slice(2);
|
|
24960
|
+
const updatePromise = args.length > 0 && !args[0].startsWith("-i") ? showUpdateNotification() : Promise.resolve();
|
|
23244
24961
|
if (args.length === 0 || args[0] === "-i" || args[0] === "--interactive") {
|
|
23245
24962
|
const { default: runTui } = await init_cli().then(() => exports_cli);
|
|
23246
24963
|
await runTui();
|
|
23247
24964
|
return;
|
|
23248
24965
|
}
|
|
23249
24966
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
24967
|
+
await updatePromise;
|
|
23250
24968
|
printHelp();
|
|
23251
24969
|
return;
|
|
23252
24970
|
}
|
|
24971
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
24972
|
+
console.log(`magic-shell v${getCurrentVersion()}`);
|
|
24973
|
+
return;
|
|
24974
|
+
}
|
|
24975
|
+
if (args[0] === "--check-update") {
|
|
24976
|
+
const update = await forceCheckForUpdates();
|
|
24977
|
+
if (update?.hasUpdate) {
|
|
24978
|
+
console.log(`${colors.green}Update available!${colors.reset} ${update.currentVersion} \u2192 ${update.latestVersion}`);
|
|
24979
|
+
console.log(`Run: ${colors.cyan}${update.updateCommand}${colors.reset}`);
|
|
24980
|
+
} else {
|
|
24981
|
+
console.log(`${colors.green}\u2713 You're running the latest version (${getCurrentVersion()})${colors.reset}`);
|
|
24982
|
+
}
|
|
24983
|
+
return;
|
|
24984
|
+
}
|
|
23253
24985
|
if (args[0] === "--setup") {
|
|
24986
|
+
await updatePromise;
|
|
23254
24987
|
await setup();
|
|
23255
24988
|
return;
|
|
23256
24989
|
}
|
|
23257
24990
|
if (args[0] === "--models") {
|
|
24991
|
+
await updatePromise;
|
|
23258
24992
|
printModels();
|
|
23259
24993
|
return;
|
|
23260
24994
|
}
|
|
@@ -23320,8 +25054,24 @@ ${colors.bold}Available Themes${colors.reset}
|
|
|
23320
25054
|
console.log(`${colors.success}\u2713 Theme set to ${themeName}${colors.reset}`);
|
|
23321
25055
|
return;
|
|
23322
25056
|
}
|
|
25057
|
+
if (args[0] === "--repo-context") {
|
|
25058
|
+
const config2 = loadConfig();
|
|
25059
|
+
config2.repoContext = true;
|
|
25060
|
+
saveConfig(config2);
|
|
25061
|
+
console.log(`${colors.success}\u2713 Project context enabled${colors.reset}`);
|
|
25062
|
+
console.log(`${colors.dim}Magic Shell will now detect package.json scripts, Makefile targets, etc.${colors.reset}`);
|
|
25063
|
+
return;
|
|
25064
|
+
}
|
|
25065
|
+
if (args[0] === "--no-repo-context") {
|
|
25066
|
+
const config2 = loadConfig();
|
|
25067
|
+
config2.repoContext = false;
|
|
25068
|
+
saveConfig(config2);
|
|
25069
|
+
console.log(`${colors.success}\u2713 Project context disabled${colors.reset}`);
|
|
25070
|
+
return;
|
|
25071
|
+
}
|
|
23323
25072
|
let execute = false;
|
|
23324
25073
|
let dryRun = false;
|
|
25074
|
+
let repoContext = undefined;
|
|
23325
25075
|
let queryParts = [];
|
|
23326
25076
|
for (let i = 0;i < args.length; i++) {
|
|
23327
25077
|
const arg = args[i];
|
|
@@ -23329,6 +25079,10 @@ ${colors.bold}Available Themes${colors.reset}
|
|
|
23329
25079
|
execute = true;
|
|
23330
25080
|
} else if (arg === "-n" || arg === "--dry-run") {
|
|
23331
25081
|
dryRun = true;
|
|
25082
|
+
} else if (arg === "-r" || arg === "--repo-context") {
|
|
25083
|
+
repoContext = true;
|
|
25084
|
+
} else if (arg === "--no-repo-context") {
|
|
25085
|
+
repoContext = false;
|
|
23332
25086
|
} else if (!arg.startsWith("-")) {
|
|
23333
25087
|
queryParts.push(arg);
|
|
23334
25088
|
}
|
|
@@ -23339,7 +25093,7 @@ ${colors.bold}Available Themes${colors.reset}
|
|
|
23339
25093
|
console.error(`Usage: msh "your query here"`);
|
|
23340
25094
|
process.exit(1);
|
|
23341
25095
|
}
|
|
23342
|
-
await translate(query, { execute, dryRun });
|
|
25096
|
+
await translate(query, { execute, dryRun, repoContext });
|
|
23343
25097
|
}
|
|
23344
25098
|
main3().catch((error) => {
|
|
23345
25099
|
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
|