Docked auto py-terminal (#1284)

This commit is contained in:
Andrea Giammarchi
2023-03-20 11:22:16 +01:00
committed by GitHub
parent 716254e655
commit e10d055453
5 changed files with 99 additions and 23 deletions

View File

@@ -1,5 +1,14 @@
# Release Notes # Release Notes
2023.XX.X
=========
Features
--------
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
2023.01.1 2023.01.1
========= =========

View File

@@ -4,7 +4,9 @@ This is one of the core plugins in PyScript, which is active by default. With it
## Configuration ## Configuration
You can control how `<py-terminal>` behaves by setting the value of the `terminal` configuration in your `<py-config>`. You can control how `<py-terminal>` behaves by setting the value of the `terminal` configuration in your `<py-config>`, together with the `docked` one.
For the **terminal** field, these are the values:
| value | description | | value | description |
|-------|-------------| |-------|-------------|
@@ -12,11 +14,25 @@ You can control how `<py-terminal>` behaves by setting the value of the `termin
| `true` | Automatically add a `<py-terminal>` to the page | | `true` | Automatically add a `<py-terminal>` to the page |
| `"auto"` | This is the default. Automatically add a `<py-terminal auto>`, to the page. The terminal is initially hidden and automatically shown as soon as something writes to `stdout` and/or `stderr` | | `"auto"` | This is the default. Automatically add a `<py-terminal auto>`, to the page. The terminal is initially hidden and automatically shown as soon as something writes to `stdout` and/or `stderr` |
For the **docked** field, these are the values:
| value | description |
|-------|-------------|
| `false` | Don't dock `<py-terminal>` to the page |
| `true` | Automatically dock a `<py-terminal>` to the page |
| `"docked"` | This is the default. Automatically add a `<py-terminal docked>`, to the page. The terminal, once visible, is automatically shown at the bottom of the page, covering the width of such page |
Please note that **docked** mode is currently used as default only when `terminal="auto"`, or *terminal* default, is used.
In all other cases it's up to the user decide if a terminal should be docked or not.
### Examples ### Examples
```html ```html
<py-config> <py-config>
terminal = true terminal = true
docked = false
</py-config> </py-config>
<py-script> <py-script>

View File

@@ -6,8 +6,25 @@ import { getLogger } from '../logger';
import { type Stdio } from '../stdio'; import { type Stdio } from '../stdio';
import { InterpreterClient } from '../interpreter_client'; import { InterpreterClient } from '../interpreter_client';
type AppConfigStyle = AppConfig & { terminal?: boolean | 'auto'; docked?: boolean | 'docked' };
const logger = getLogger('py-terminal'); const logger = getLogger('py-terminal');
const validate = (config: AppConfigStyle, name: string, default_: string) => {
const value = config[name] as undefined | boolean | string;
if (value !== undefined && value !== true && value !== false && value !== default_) {
const got = JSON.stringify(value);
throw new UserError(
ErrorCode.BAD_CONFIG,
`Invalid value for config.${name}: the only accepted` +
`values are true, false and "${default_}", got "${got}".`,
);
}
if (value === undefined) {
config[name] = default_;
}
};
export class PyTerminalPlugin extends Plugin { export class PyTerminalPlugin extends Plugin {
app: PyScriptApp; app: PyScriptApp;
@@ -16,35 +33,26 @@ export class PyTerminalPlugin extends Plugin {
this.app = app; this.app = app;
} }
configure(config: AppConfig & { terminal?: boolean | 'auto' }) { configure(config: AppConfigStyle) {
// validate the terminal config and handle default values // validate the terminal config and handle default values
const t = config.terminal; validate(config, 'terminal', 'auto');
if (t !== undefined && t !== true && t !== false && t !== 'auto') { validate(config, 'docked', 'docked');
const got = JSON.stringify(t);
throw new UserError(
ErrorCode.BAD_CONFIG,
'Invalid value for config.terminal: the only accepted' +
`values are true, false and "auto", got "${got}".`,
);
}
if (t === undefined) {
config.terminal = 'auto'; // default value
}
} }
beforeLaunch(config: AppConfig & { terminal?: boolean | 'auto' }) { beforeLaunch(config: AppConfigStyle) {
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to // if config.terminal is "yes" or "auto", let's add a <py-terminal> to
// the document, unless it's already present. // the document, unless it's already present.
const t = config.terminal; const { terminal: t, docked: d } = config;
if (t === true || t === 'auto') { const auto = t === true || t === 'auto';
if (document.querySelector('py-terminal') === null) { const docked = d === true || d === 'docked';
if (auto && document.querySelector('py-terminal') === null) {
logger.info('No <py-terminal> found, adding one'); logger.info('No <py-terminal> found, adding one');
const termElem = document.createElement('py-terminal'); const termElem = document.createElement('py-terminal');
if (t === 'auto') termElem.setAttribute('auto', ''); if (auto) termElem.setAttribute('auto', '');
if (docked) termElem.setAttribute('docked', '');
document.body.appendChild(termElem); document.body.appendChild(termElem);
} }
} }
}
afterSetup(_interpreter: InterpreterClient) { afterSetup(_interpreter: InterpreterClient) {
// the Python interpreter has been initialized and we are ready to // the Python interpreter has been initialized and we are ready to
@@ -93,6 +101,10 @@ function make_PyTerminal(app: PyScriptApp) {
this.autoShowOnNextLine = false; this.autoShowOnNextLine = false;
} }
if (this.isDocked()) {
this.classList.add('py-terminal-docked');
}
logger.info('Registering stdio listener'); logger.info('Registering stdio listener');
app.registerStdioListener(this); app.registerStdioListener(this);
} }
@@ -101,9 +113,16 @@ function make_PyTerminal(app: PyScriptApp) {
return this.hasAttribute('auto'); return this.hasAttribute('auto');
} }
isDocked() {
return this.hasAttribute('docked');
}
// implementation of the Stdio interface // implementation of the Stdio interface
stdout_writeline(msg: string) { stdout_writeline(msg: string) {
this.outElem.innerText += msg + '\n'; this.outElem.innerText += msg + '\n';
if (this.isDocked()) {
this.scrollTop = this.scrollHeight;
}
if (this.autoShowOnNextLine) { if (this.autoShowOnNextLine) {
this.classList.remove('py-terminal-hidden'); this.classList.remove('py-terminal-hidden');
this.autoShowOnNextLine = false; this.autoShowOnNextLine = false;

View File

@@ -330,3 +330,20 @@ textarea {
.py-terminal-hidden { .py-terminal-hidden {
display: none; display: none;
} }
/* avoid changing the page layout when the terminal is docked and hidden */
html:has(py-terminal[docked]:not(py-terminal[docked].py-terminal-hidden)) {
padding-bottom: 40vh;
}
py-terminal[docked] {
position: fixed;
bottom: 0;
width: 100vw;
max-height: 40vh;
overflow: auto;
}
py-terminal[docked] .py-terminal {
margin: 0;
}

View File

@@ -131,3 +131,18 @@ class TestPyTerminal(PyScriptTest):
) )
term = self.page.locator("py-terminal") term = self.page.locator("py-terminal")
assert term.count() == 0 assert term.count() == 0
def test_config_docked(self):
"""
config.docked == "docked" is also the default: a <py-terminal auto docked> is
automatically added to the page
"""
self.pyscript_run(
"""
<button id="my-button" py-onClick="print('hello world')">Click me</button>
"""
)
term = self.page.locator("py-terminal")
self.page.locator("button").click()
expect(term).to_be_visible()
assert term.get_attribute("docked") == ""