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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "browser.cpp",
"short_name": "browser.cpp",
"description": "In-browser C++20 IDE powered by Monaco Editor and WASM Clang",
"version": "0.4.6",
"version": "0.4.7",
"minimum_chrome_version": "105",
"icons": {
"16": "icons/icon16.png",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "browser.cpp",
"version": "0.4.6",
"version": "0.4.7",
"description": "In-browser C++20 IDE with WASM Clang toolchain",
"private": true,
"scripts": {
Expand Down
26 changes: 26 additions & 0 deletions scripts/e2e-terminal-stop.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
showInitialPrompt,
startRun,
stopRun,
writeStdout,
} from '../src/ui/terminal.js';

function setupTerminalHarness({
Expand Down Expand Up @@ -123,6 +124,31 @@ test('e2e: clearing during startup does not reveal the initial prompt early', ()
assert.ok(output.indexOf('Clang WASM compiler loaded') < output.indexOf('browser.cpp'));
});

test('e2e: clearing discards typed input and homes the fresh prompt', () => {
const ctx = setupTerminalHarness();

showInitialPrompt();
__handleTerminalKeyForTesting('c', { key: 'c', ctrlKey: false, altKey: false });
__handleTerminalKeyForTesting('a', { key: 'a', ctrlKey: false, altKey: false });
clearTerminal();

assert.equal(__getTerminalStateForTesting().inputBuffer, '');
assert.equal(ctx.clearCalls.length, 1);
assert.equal(ctx.writes.at(-2), '\x1b[2J\x1b[H');
assert.match(ctx.writes.at(-1), /browser\.cpp.*:~\$ /);
});

test('e2e: prompt restoration follows newline-less program output on a new line', () => {
const ctx = setupTerminalHarness();

showInitialPrompt();
onRunStart({ stdinMode: 'interactive' });
writeStdout('program output');
onRunResult({ exitCode: 0 });

assert.match(ctx.writes.join(''), /program output\r\n.*browser\.cpp.*:~\$ /);
});

test('e2e: Ctrl+C while running stops the program once and restores the prompt', async () => {
const ctx = setupTerminalHarness();

Expand Down
58 changes: 51 additions & 7 deletions src/ui/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const C = {
};

const CRLF = '\r\n';
const CLEAR_SCREEN = '\x1b[2J\x1b[H';
const CSI_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g');

/** Maximum number of commands retained in shell history. */
const MAX_HISTORY_SIZE = 200;
Expand All @@ -69,6 +71,8 @@ const TAB_COMMANDS = ['g++ ', 'g++ main.cpp', './a.out', 'clear', 'echo ', 'ls',

let term = null;
let fitAddon = null;
let terminalLineHasContent = false;
const trackedTerminals = new WeakSet();

/** Current line being typed */
let inputBuffer = '';
Expand Down Expand Up @@ -301,7 +305,7 @@ export function createTerminal(container, {
initialPromptShown = false;
busy = true;

term = new Terminal({
setTerminal(new Terminal({
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
fontSize: 13,
lineHeight: 1.4,
Expand Down Expand Up @@ -330,7 +334,7 @@ export function createTerminal(container, {
cursorBlink: true,
scrollback: 5000,
convertEol: false,
});
}));

fitAddon = new FitAddon();
term.loadAddon(fitAddon);
Expand All @@ -357,7 +361,9 @@ export function fitTerminal() {

/** Clear the terminal screen. */
export function clearTerminal() {
term?.clear();
inputBuffer = '';
historyIdx = -1;
clearScreen();
if (!initialPromptShown) return;
writePrompt();
}
Expand All @@ -371,7 +377,7 @@ export function resetTerminalSession(workspace = null) {
inputBuffer = '';
historyIdx = -1;
setWorkspace(workspace);
term?.clear();
clearScreen();
if (initialPromptShown) writePrompt();
}

Expand Down Expand Up @@ -685,7 +691,7 @@ function handleKey({ key, domEvent }) {

// Ctrl+L – clear screen
if (domEvent.ctrlKey && code === 'l') {
term.clear();
clearScreen();
writePrompt();
term.write(inputBuffer);
return;
Expand Down Expand Up @@ -766,7 +772,7 @@ async function executeCommand(cmdLine) {
cmdGxx(args);
break;
case 'clear':
term.clear();
clearScreen();
writePrompt();
break;
case 'echo':
Expand Down Expand Up @@ -1072,9 +1078,47 @@ function cmdHelp() {
// ── Utilities ─────────────────────────────────────────────────────────────────

function writePrompt() {
if (terminalLineHasContent) term?.write(CRLF);
term?.write(`${C.green}${C.bold}browser.cpp${C.reset}${C.dim}:${promptPath()}$ ${C.reset}`);
}

/** Clear the terminal and place the next prompt at the top-left of the screen. */
function clearScreen() {
term?.clear();
terminalLineHasContent = false;
if (initialPromptShown) term?.write(CLEAR_SCREEN);
}

/**
* Keep prompt placement independent of the many existing terminal write sites.
* xterm writes are asynchronous, so record the logical line state as output is queued.
*/
function setTerminal(nextTerm) {
term = nextTerm;
terminalLineHasContent = false;
if (!term || trackedTerminals.has(term)) return;

const terminalInstance = term;
const write = terminalInstance.write.bind(terminalInstance);
terminalInstance.write = (text, ...args) => {
if (term === terminalInstance) trackTerminalLine(text);
return write(text, ...args);
};
trackedTerminals.add(terminalInstance);
}

function trackTerminalLine(text) {
if (typeof text !== 'string') return;

const visibleText = text.replace(CSI_SEQUENCE, '');
const lastLineBreak = Math.max(visibleText.lastIndexOf('\n'), visibleText.lastIndexOf('\r'));
if (lastLineBreak >= 0) {
terminalLineHasContent = visibleText.slice(lastLineBreak + 1).length > 0;
} else if (visibleText.length > 0) {
terminalLineHasContent = true;
}
}

function setRunState(nextRunning) {
if (running === nextRunning) return;
running = nextRunning;
Expand Down Expand Up @@ -1105,7 +1149,7 @@ export function __setTerminalTestHarness({
supportsMessageInteractiveStdin: supportsMessageInteractiveStdinForTest = () => false,
createStdinSessionId: createStdinSessionIdForTest = () => 'stdin-session-test',
} = {}) {
term = terminalInstance || null;
setTerminal(terminalInstance || null);
fitAddon = null;
_onCompile = onCompile;
_onRun = onRun;
Expand Down
Loading