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
13 changes: 9 additions & 4 deletions app/[docs_id]/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,15 @@ function CodeComponent({
} else {
// inline
return (
<code
className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md"
{...props}
/>
<InlineCode>{String(props.children || "").replace(/\n$/, "")}</InlineCode>
);
}
}

export function InlineCode({ children }: { children: ReactNode }) {
return (
<code className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md">
{children}
</code>
);
}
12 changes: 8 additions & 4 deletions app/terminal/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,14 @@ export function EditorComponent(props: EditorProps) {
return (
<div className="border border-accent border-2 shadow-md m-2 rounded-box overflow-hidden">
<div className="flex flex-row items-center bg-base-200">
<div className="font-mono text-sm mt-2 mb-1 ml-4 mr-2">
{props.filename}
{props.readonly && <span className="font-sans ml-2">(編集不可)</span>}
</div>
<span className="mt-2 mb-1 ml-3 mr-2 text-sm text-left">
<span>
{props.readonly
? "出力されたファイル(編集不可):"
: "ファイルを編集:"}
</span>
<span className="font-mono ml-2">{props.filename}</span>
</span>
<button
className={clsx(
"btn btn-xs btn-soft btn-warning mt-1 mb-1",
Expand Down
69 changes: 57 additions & 12 deletions app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export function ExecFile(props: ExecProps) {
});
const { files, clearExecResult, addExecOutput } = useEmbedContext();

const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
const { ready, runFiles, getCommandlineStr, runtimeInfo, interrupt } =
useRuntime(props.language);

// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
const [executionState, setExecutionState] = useState<
Expand Down Expand Up @@ -82,13 +83,16 @@ export function ExecFile(props: ExecProps) {
]);

return (
<div className="border border-accent border-2 shadow-md m-2 rounded-box overflow-hidden relative">
<div className="bg-base-200 flex items-center">
<div className="border border-accent border-2 shadow-md m-2 rounded-box relative">
<div className="bg-base-200 flex items-center rounded-t-box">
<button
/* daisyuiのbtnはheightがvar(--size)で固定。
ここでは最小でそのサイズ、ただし親コンテナがそれより大きい場合に大きくしたい
→ heightを解除し、min-heightをデフォルトのサイズと同じにする */
className="btn btn-soft btn-accent rounded-none h-[unset]! min-h-(--size) self-stretch"
className={clsx(
"btn btn-soft btn-accent h-[unset]! min-h-(--size) self-stretch",
"rounded-none rounded-tl-[calc(var(--radius-box)-2px)]"
)}
onClick={() => {
if (!ready) {
clearTerminal(terminalInstanceRef.current!);
Expand All @@ -98,17 +102,58 @@ export function ExecFile(props: ExecProps) {
)
);
}
setExecutionState("triggered");
if (executionState === "idle") {
setExecutionState("triggered");
}
if (executionState === "executing" && interrupt) {
// Ctrl+C
interrupt();
terminalInstanceRef.current!.write("^C");
}
}}
disabled={!termReady || executionState !== "idle"}
disabled={
!termReady ||
!(
executionState === "idle" ||
(executionState === "executing" && interrupt)
)
}
>
▶ 実行
{executionState === "idle" ? "▶ 実行" : "■ 停止"}
</button>
<code className="text-left break-all text-sm my-1 ml-4 mr-1">
<code className="text-left break-all text-sm my-1 ml-4">
{getCommandlineStr?.(props.filenames)}
</code>
<div className="ml-1 mr-1 tooltip tooltip-secondary tooltip-bottom z-1">
{/*なぜかわからないがz-1がないと後ろに隠れてしまう*/}
<div className="tooltip-content bg-secondary/60 backdrop-blur-xs">
ブラウザ上で動作する
<span className="mx-0.5">
{runtimeInfo?.prettyLangName || props.language}
</span>
{runtimeInfo?.version && (
<span className="mr-0.5">{runtimeInfo?.version}</span>
)}
の実行環境です。
<br />
左上の実行ボタンを押して、このページ内の
{props.filenames.map((fname) => (
<span key={fname}>
<span className="font-mono mx-0.5">{fname}</span>
</span>
))}
に書かれている内容を実行します。
</div>
<button
className={clsx(
"btn btn-xs btn-soft btn-secondary rounded-full cursor-help"
)}
>
</button>
</div>
</div>
<div className="bg-base-300 p-4 pt-2 relative">
<div className="bg-base-300 p-4 pr-1 pt-2 relative rounded-b-box">
{/*
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
Expand All @@ -130,10 +175,10 @@ export function ExecFile(props: ExecProps) {
)}
ref={terminalRef}
/>
{executionState !== "idle" && (
<div className="absolute z-10 inset-0 cursor-wait" />
)}
</div>
{executionState !== "idle" && (
<div className="absolute z-10 inset-0 cursor-wait" />
)}
</div>
);
}
146 changes: 106 additions & 40 deletions app/terminal/repl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Terminal } from "@xterm/xterm";
import { useEmbedContext } from "./embedContext";
import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime";
import clsx from "clsx";
import { InlineCode } from "@/[docs_id]/markdown";

export type ReplOutputType =
| "stdout"
Expand Down Expand Up @@ -97,6 +98,7 @@ export function ReplTerminal({
runCommand,
checkSyntax,
splitReplExamples,
runtimeInfo,
} = useRuntime(language);
const { tabSize, prompt, promptMore, returnPrefix } = langConstants(language);
if (!prompt) {
Expand Down Expand Up @@ -129,6 +131,10 @@ export function ReplTerminal({
// REPLのユーザー入力
const inputBuffer = useRef<string[]>([]);

const [executionState, setExecutionState] = useState<"idle" | "executing">(
"idle"
);

// inputBufferを更新し、画面に描画する
const updateBuffer = useCallback(
(newBuffer: (() => string[]) | null, insertBefore?: () => void) => {
Expand Down Expand Up @@ -219,6 +225,7 @@ export function ReplTerminal({
const command = inputBuffer.current.join("\n").trim();
inputBuffer.current = [];
const commandId = addReplCommand(terminalId, command);
setExecutionState("executing");
let executionDone = false;
await runtimeMutex.runExclusive(async () => {
await runCommand(command, (output) => {
Expand All @@ -233,6 +240,7 @@ export function ReplTerminal({
addReplOutput(terminalId, commandId, output);
});
});
setExecutionState("idle");
executionDone = true;
updateBuffer(() => [""]);
}
Expand Down Expand Up @@ -378,51 +386,109 @@ export function ReplTerminal({
]);

return (
<div className="bg-base-300 border border-accent border-2 shadow-md m-2 p-4 pr-1 rounded-box relative h-max">
<div className="bg-base-300 border border-accent border-2 shadow-md m-2 rounded-box h-max">
<div className="bg-base-200 flex items-center rounded-t-box">
<button
/* daisyuiのbtnはheightがvar(--size)で固定。
ここでは最小でそのサイズ、ただし親コンテナがそれより大きい場合に大きくしたい
→ heightを解除し、min-heightをデフォルトのサイズと同じにする */
className={clsx(
"btn btn-soft btn-accent h-[unset]! min-h-(--size) self-stretch",
"rounded-none rounded-tl-[calc(var(--radius-box)-2px)]"
)}
onClick={() => {
// Ctrl+C
if (terminalInstanceRef.current && runtimeInterrupt) {
runtimeInterrupt();
terminalInstanceRef.current.write("^C");
}
}}
disabled={
!termReady ||
initCommandState !== "done" ||
executionState !== "executing"
}
>
■ 停止
</button>
<span className="text-sm my-1 ml-3 text-left">
{runtimeInfo?.prettyLangName || language} 実行環境
</span>
<div className="ml-1 tooltip tooltip-secondary tooltip-bottom">
<div className="tooltip-content bg-secondary/60 backdrop-blur-xs">
ブラウザ上で動作する
<span className="mx-0.5">
{runtimeInfo?.prettyLangName || language}
</span>
{runtimeInfo?.version && (
<span className="mr-0.5">{runtimeInfo?.version}</span>
)}
のREPL実行環境です。
<br />
プロンプト (<InlineCode>{prompt?.trimEnd()}</InlineCode>)
の後にコマンドを入力し、
<kbd className="kbd kbd-sm text-base-content">Enter</kbd>
キーで実行します。
<br />
<kbd className="kbd kbd-sm text-base-content">Ctrl</kbd>+
<kbd className="kbd kbd-sm text-base-content">C</kbd>
または左上の停止ボタンで実行中のコマンドを中断できます。
</div>
<button
className={clsx(
"btn btn-xs btn-soft btn-secondary rounded-full cursor-help"
)}
>
</button>
</div>
</div>
{/*
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
*/}
<pre
className={clsx(
"font-mono overflow-auto cursor-wait",
"min-h-26", // xterm.jsで5行分の高さ
initCommandState !== "initializing" && "hidden"
)}
>
{initContent + "\n\n"}
</pre>
{terminalInstanceRef.current &&
termReady &&
initCommandState === "idle" && (
<div
className="absolute z-10 inset-0 cursor-pointer"
onClick={() => {
if (!runtimeReady) {
hideCursor(terminalInstanceRef.current!);
terminalInstanceRef.current!.write(
systemMessageColor(
"(初期化しています...しばらくお待ちください)"
)
);
terminalInstanceRef.current!.focus();
}
setInitCommandState("triggered");
}}
/>
)}
{(initCommandState === "triggered" ||
initCommandState === "executing") && (
<div className="absolute z-10 inset-0 cursor-wait" />
)}
<div
className={clsx(
initCommandState === "initializing" &&
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
"absolute invisible"
<div className="relative p-4 pr-1 pt-2">
<pre
className={clsx(
"font-mono overflow-auto cursor-wait",
"min-h-26", // xterm.jsで5行分の高さ
initCommandState !== "initializing" && "hidden"
)}
>
{initContent + "\n\n"}
</pre>
{terminalInstanceRef.current &&
termReady &&
initCommandState === "idle" && (
<div
className="absolute z-10 inset-0 cursor-pointer"
onClick={() => {
if (!runtimeReady) {
hideCursor(terminalInstanceRef.current!);
terminalInstanceRef.current!.write(
systemMessageColor(
"(初期化しています...しばらくお待ちください)"
)
);
terminalInstanceRef.current!.focus();
}
setInitCommandState("triggered");
}}
/>
)}
{(initCommandState === "triggered" ||
initCommandState === "executing") && (
<div className="absolute z-10 inset-0 cursor-wait" />
)}
ref={terminalRef}
/>
<div
className={clsx(
initCommandState === "initializing" &&
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
"absolute invisible"
)}
ref={terminalRef}
/>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions app/terminal/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface RuntimeContext {
onOutput: (output: ReplOutput) => void
) => Promise<void>;
getCommandlineStr?: (filenames: string[]) => string;
runtimeInfo?: RuntimeInfo;
}
export interface RuntimeInfo {
prettyLangName: string;
version?: string;
}
export interface LangConstants {
tabSize: number;
Expand Down
Loading