diff --git a/docs/docs/features/display-modes.mdx b/docs/docs/features/display-modes.mdx
index 9f9c581bb1..a2d52e8827 100644
--- a/docs/docs/features/display-modes.mdx
+++ b/docs/docs/features/display-modes.mdx
@@ -62,6 +62,20 @@ export const simpleConfig = {
+## `console`
+
+This mode is mainly useful for embedded playgrounds.
+It shows a single editor together with the [console](./console.mdx) maximized (filling the output area), with the result page collapsed.
+The [tools pane](./tools-pane.mdx) is fixed to show only the console; it cannot be resized or closed.
+The console input is hidden, and the code can be run from the editor.
+This mode is particularly useful for [WASM-based languages](../languages/index.mdx) (e.g. Python, Ruby, PHP, C, ...) whose output is mainly text-based (sent to the console).
+
+Demo: Python with console
+
+
+
## `lite`
Loads a light-weight, minimal code editor, with limited playground features.
diff --git a/src/livecodes/config/build-config.ts b/src/livecodes/config/build-config.ts
index 77eb0fa0b6..71dbcd7038 100644
--- a/src/livecodes/config/build-config.ts
+++ b/src/livecodes/config/build-config.ts
@@ -224,7 +224,7 @@ export const loadParamConfig = (config: Config, params: UrlQueryParams): Partial
paramsConfig.languages = params.languages
.split(',')
.map((lang) => lang.trim())
- .map(getLanguageByAlias)
+ .map((lang) => getLanguageByAlias(lang))
.filter(Boolean) as Language[];
}
diff --git a/src/livecodes/config/validate-config.ts b/src/livecodes/config/validate-config.ts
index 44032cb65e..3355ee5f43 100644
--- a/src/livecodes/config/validate-config.ts
+++ b/src/livecodes/config/validate-config.ts
@@ -30,6 +30,7 @@ export const validateConfig = (config: Partial): Partial => {
'focus',
'lite',
'simple',
+ 'console',
'editor',
'codeblock',
'result',
diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts
index c6ce47fd77..a682ef2d9c 100644
--- a/src/livecodes/core.ts
+++ b/src/livecodes/core.ts
@@ -628,6 +628,7 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => {
focus: '111',
simple: '111',
lite: '111',
+ console: '111',
editor: '110',
codeblock: '010',
result: '001',
@@ -693,14 +694,21 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => {
document.body.classList.toggle('simple-mode', mode === 'simple');
document.body.classList.toggle('focus-mode', mode === 'focus');
document.body.classList.toggle('lite-mode', mode === 'lite');
+ document.body.classList.toggle('console-mode', mode === 'console');
document.body.classList.toggle('result', mode === 'result');
document.body.classList.toggle('no-result', mode === 'editor' || mode === 'codeblock');
- if ((mode === 'full' || mode === 'simple') && !split) {
+ if ((mode === 'full' || mode === 'simple' || mode === 'console') && !split) {
split = createSplitPanes();
}
if (mode === 'focus') {
toolsPane?.setActiveTool('console');
}
+ if (mode === 'console') {
+ toolsPane?.setActiveTool('console');
+ if (!['open', 'full'].includes(toolsPane?.getStatus() || '')) {
+ toolsPane?.maximize();
+ }
+ }
window.dispatchEvent(new Event(customEvents.resizeEditor));
};
@@ -5105,6 +5113,17 @@ const configureSimpleMode = (config: Config) => {
});
};
+const configureConsoleMode = (config: Config) => {
+ setConfig({
+ ...config,
+ tools: {
+ enabled: Array.isArray(config.tools?.enabled) ? config.tools.enabled : ['console'],
+ active: config.tools?.active || 'console',
+ status: config.tools?.status || 'full',
+ },
+ });
+};
+
const configureModes = ({
config,
isEmbed,
@@ -5129,6 +5148,9 @@ const configureModes = ({
if (config.mode === 'simple') {
configureSimpleMode(config);
}
+ if (config.mode === 'console') {
+ configureConsoleMode(config);
+ }
};
const importExternalContent = async (options: {
@@ -5331,7 +5353,9 @@ const initializePlayground = async (
isLite ||
(options?.isEmbed ?? false) ||
initialConfig.mode === 'simple' ||
- params.mode === 'simple';
+ params.mode === 'simple' ||
+ initialConfig.mode === 'console' ||
+ params.mode === 'console';
window.history.replaceState(null, '', './'); // fix URL from "/app" to "/"
await initializeStores(stores, isEmbed);
diff --git a/src/livecodes/languages/__tests__/languages.spec.ts b/src/livecodes/languages/__tests__/languages.spec.ts
index 4896696f0c..99113f0023 100644
--- a/src/livecodes/languages/__tests__/languages.spec.ts
+++ b/src/livecodes/languages/__tests__/languages.spec.ts
@@ -1,4 +1,10 @@
-import { getLanguageByAlias, getLanguageEditorId, getLanguageExtension, mapLanguage } from '..';
+import {
+ getLanguageByAlias,
+ getLanguageEditorId,
+ getLanguageExtension,
+ languageIsEnabled,
+ mapLanguage,
+} from '..';
describe('languages', () => {
test('getLanguageByAlias', () => {
@@ -35,4 +41,30 @@ describe('languages', () => {
expect(mapLanguage('ruby')).toBe('ruby');
expect(mapLanguage('html')).toBe('html');
});
+
+ describe('deprecation warnings', () => {
+ let warn: jest.SpyInstance;
+
+ beforeEach(() => {
+ (window as any).deps.loggedDeprecations = [];
+ warn = jest.spyOn(console, 'warn').mockImplementation(() => {
+ //
+ });
+ });
+
+ afterEach(() => {
+ warn.mockRestore();
+ });
+
+ test('warns when a deprecated language name is used', () => {
+ expect(getLanguageByAlias('ocaml')).toBe('ocaml');
+ expect(warn).toHaveBeenCalled();
+ });
+
+ test('does not warn while enumerating languages', () => {
+ languageIsEnabled('ocaml', { languages: undefined } as any);
+ languageIsEnabled('ocaml', { languages: ['html', 'css', 'javascript'] } as any);
+ expect(warn).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts
index 684ebea1c2..1b830c6fe2 100644
--- a/src/livecodes/languages/utils.ts
+++ b/src/livecodes/languages/utils.ts
@@ -1,8 +1,32 @@
-import type { Compiler, Config, CustomSettings, Language, Processor } from '../models';
+import type {
+ Compiler,
+ Config,
+ CustomSettings,
+ Language,
+ LanguageSpecs,
+ Processor,
+} from '../models';
import { getDocsUrl, getLanguageCustomSettings } from '../utils/utils';
import { highlightjsUrl } from '../vendors';
-export const getLanguageByAlias = (alias: string = ''): Language | undefined => {
+const logDeprecation = (alias: string, language?: LanguageSpecs) => {
+ if (!globalThis.window?.deps) return;
+ window.deps.loggedDeprecations ??= [];
+ const deprecation = language?.deprecation?.(alias.toLowerCase() as Language);
+ if (!deprecation || window.deps.loggedDeprecations.includes(deprecation.old)) return;
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`,
+ `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`,
+ language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '',
+ );
+ window.deps.loggedDeprecations.push(deprecation.old);
+};
+
+export const getLanguageByAlias = (
+ alias: string = '',
+ options: { warn?: boolean } = {},
+): Language | undefined => {
if (!alias) return;
const aliasLowerCase = alias?.toLowerCase();
const language = window.deps.languages.find(
@@ -11,20 +35,10 @@ export const getLanguageByAlias = (alias: string = ''): Language | undefined =>
lang.title.toLowerCase() === aliasLowerCase ||
lang.extensions.map((ext) => ext.toLowerCase()).includes(aliasLowerCase),
);
- if (globalThis.window?.deps) window.deps.loggedDeprecations ??= [];
- const deprecation = language?.deprecation?.(aliasLowerCase as Language);
- if (
- deprecation &&
- window.deps.loggedDeprecations &&
- !window.deps.loggedDeprecations.includes(deprecation.old)
- ) {
- // eslint-disable-next-line no-console
- console.warn(
- `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`,
- `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`,
- language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '',
- );
- window.deps.loggedDeprecations?.push(deprecation.old);
+ // only warn when the alias is actually used (e.g. from config or user selection),
+ // not when enumerating languages (menus, compiler list, starter templates)
+ if (options.warn !== false) {
+ logDeprecation(alias, language);
}
return language?.name;
};
@@ -71,7 +85,7 @@ export const mapLanguage = (
'html';
export const languageIsEnabled = (language: Language, config: Config) => {
- const lang = getLanguageByAlias(language);
+ const lang = getLanguageByAlias(language, { warn: false });
if (!lang) return false;
if (!config.languages) return true;
if (
@@ -80,7 +94,10 @@ export const languageIsEnabled = (language: Language, config: Config) => {
) {
return true;
}
- return config.languages?.map(getLanguageByAlias).filter(Boolean).includes(lang);
+ return config.languages
+ ?.map((alias) => getLanguageByAlias(alias, { warn: false }))
+ .filter(Boolean)
+ .includes(lang);
};
export const processorIsEnabled = (processor: Processor, config: Config) => {
diff --git a/src/livecodes/styles/app.scss b/src/livecodes/styles/app.scss
index 3e706dbb73..b898ad2b20 100644
--- a/src/livecodes/styles/app.scss
+++ b/src/livecodes/styles/app.scss
@@ -1793,7 +1793,8 @@ a.editor-title {
}
}
-.simple-mode {
+.simple-mode,
+.console-mode {
.editor-title:not(.active),
&.embed #toolbar .toolbar-render #result-button,
&.embed #toolbar .toolbar-render #fullscreen-button,
@@ -1842,6 +1843,29 @@ a.editor-title {
}
}
+.console-mode {
+ #toolbar {
+ margin-bottom: 0;
+ transform: none;
+ width: 100%;
+ }
+
+ #zoom-button,
+ .console-close-button,
+ #size-label {
+ display: none !important;
+ }
+
+ #tools-pane-bar {
+ cursor: default;
+ pointer-events: none;
+ }
+
+ #tools-pane-buttons {
+ pointer-events: auto;
+ }
+}
+
.focus-mode {
#app-menu-button-project,
#app-menu-button-settings,
diff --git a/src/livecodes/templates/get-starter-templates.ts b/src/livecodes/templates/get-starter-templates.ts
index d1d1487548..8ee8fc79f7 100644
--- a/src/livecodes/templates/get-starter-templates.ts
+++ b/src/livecodes/templates/get-starter-templates.ts
@@ -28,7 +28,9 @@ const loadTemplates = async (baseUrl: string): Promise =>
export const getStarterTemplates = async (config: Config, baseUrl: string): Promise =>
(await loadTemplates(baseUrl))
.filter((template) => {
- const enabledLanguages = config.languages?.map(getLanguageByAlias).filter(Boolean);
+ const enabledLanguages = config.languages
+ ?.map((language) => getLanguageByAlias(language, { warn: false }))
+ .filter(Boolean);
if (!enabledLanguages) return true;
if (template.name === 'blank') return true;
@@ -38,7 +40,7 @@ export const getStarterTemplates = async (config: Config, baseUrl: string): Prom
template.script?.language,
];
for (const language of templateLanguages) {
- const lang = getLanguageByAlias(language);
+ const lang = getLanguageByAlias(language, { warn: false });
if (!lang || !enabledLanguages.includes(lang)) return false;
}
return true;
diff --git a/src/livecodes/toolspane/tools.ts b/src/livecodes/toolspane/tools.ts
index fdda7d78b3..47704d8944 100644
--- a/src/livecodes/toolspane/tools.ts
+++ b/src/livecodes/toolspane/tools.ts
@@ -310,6 +310,14 @@ export const createToolsPane = (
buttons.id = 'tools-pane-buttons';
buttons.classList.add('tool-buttons');
toolsPaneBar.appendChild(buttons);
+ // prevent starting a drag when clicking the buttons
+ eventsManager.addEventListener(buttons, 'mousedown', (event) => event.stopPropagation(), false);
+ eventsManager.addEventListener(
+ buttons,
+ 'touchstart',
+ (event) => event.stopPropagation(),
+ false,
+ );
const closeButton = document.createElement('button');
closeButton.classList.add('console-close-button');
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index a9f49980a6..6d56b17a02 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -1226,7 +1226,7 @@ export interface AppConfig {
* Sets the [display mode](https://livecodes.io/docs/features/display-modes).
* @default "full"
*/
- mode: 'full' | 'focus' | 'lite' | 'simple' | 'editor' | 'codeblock' | 'result';
+ mode: 'full' | 'focus' | 'lite' | 'simple' | 'console' | 'editor' | 'codeblock' | 'result';
/**
* Sets enabled and active tools and status of [tools pane](https://livecodes.io/docs/features/tools-pane).