Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terminal Widget based on xtermjs #2090

Merged
merged 27 commits into from Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f1a0270
first iteration based on Custom Externsions Guide
MarcSkovMadsen Mar 16, 2021
88cc733
wip
MarcSkovMadsen Mar 16, 2021
73226c3
poc
MarcSkovMadsen Mar 16, 2021
280d4bb
more functionality
MarcSkovMadsen Mar 18, 2021
21e05bb
changed to widget, add clear method, clean up
MarcSkovMadsen Mar 18, 2021
f54155c
add js and css
MarcSkovMadsen Mar 19, 2021
19356ca
add use cases
MarcSkovMadsen Mar 19, 2021
6a67983
support links and special characters
MarcSkovMadsen Mar 20, 2021
0b536ba
support resize
MarcSkovMadsen Mar 20, 2021
cd12bbd
small fixes
MarcSkovMadsen Mar 20, 2021
c5d72aa
got linux pty integration working
MarcSkovMadsen Mar 21, 2021
3eb8f99
work on TerminalSubProcess to make it easy to run subprocesses
MarcSkovMadsen Mar 21, 2021
3e621ab
clean up
MarcSkovMadsen Mar 22, 2021
65f8b0b
solve todos
MarcSkovMadsen Mar 23, 2021
7d7b2a4
got notebook working
MarcSkovMadsen Mar 23, 2021
0d6ead1
Works in notebook. But not in Jupyter Lab
MarcSkovMadsen Mar 23, 2021
754dcf1
Clean up Terminal implementation
philippjfr Jun 8, 2021
6e2e8c5
Fixed tests
philippjfr Jun 8, 2021
e8a41e8
Fixes for API and layout
philippjfr Jun 8, 2021
801c453
Clean up file API
philippjfr Jun 8, 2021
7305bae
Fix flake
philippjfr Jun 8, 2021
241d2a5
Fix clone
philippjfr Jun 8, 2021
fdad5cc
Make import lazy
philippjfr Jun 8, 2021
0d3ec2a
Windows fix
philippjfr Jun 8, 2021
288cd78
Skip Terminal
philippjfr Jun 8, 2021
1ec9fdf
Skip test on windows
philippjfr Jun 8, 2021
01af298
Another windows skip
philippjfr Jun 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
282 changes: 282 additions & 0 deletions examples/reference/widgets/Terminal.ipynb
@@ -0,0 +1,282 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"import uuid\n",
"import logging\n",
"import panel as pn\n",
"\n",
"pn.extension('terminal')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``Terminal`` provides a way to display outputs or logs from running processes as well as an interactive terminal based on for example Bash, Python or IPython. The Terminal is based on [Xterm.js](https://xtermjs.org/) which enables\n",
"\n",
"- Terminal apps that just work: Xterm.js works with most terminal apps such as bash, vim and tmux, this includes support for curses-based apps and mouse event support\n",
"- Performance: Xterm.js is really fast, it even includes a GPU-accelerated renderer\n",
"- Rich unicode support: Supports CJK, emojis and IMEs\n",
"\n",
"[![Xterm.js](https://raw.githubusercontent.com/xtermjs/xterm.js/master/logo-full.png)](https://xtermjs.org/)\n",
"\n",
"### Terminal Widget\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"- **``clear``** (action): Clears the Terminal.\n",
"- **``options``** (dict) Initial Options for the Terminal Constructor. cf. https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/\n",
"- **``output``** (str): System *output* written to the Terminal.\n",
"- **``value``** (str): User *input* received from the Terminal.\n",
"- **``write_to_console``** (boolean): If True output is additionally written to the server console. Default value is False.\n",
"\n",
"#### Methods\n",
"\n",
"* **``write``**: Writes the specified string object to the Terminal.\n",
"\n",
"### Terminal Subprocess\n",
"\n",
"The `Terminal.subprocess` property makes it easy for you to run subprocesses like `ls`, `ls -l`, `bash`, `python` and `ipython` in the terminal. \n",
"\n",
"#### Parameters\n",
"\n",
"- **``args``** (str, list): The arguments used to run the subprocess. This may be a string or a list. The string cannot contain spaces. See [subprocess.run](https://docs.python.org/3/library/subprocess.html) for more details.\n",
"- **``kwargs``** (dict): Any other arguments to run the subprocess. See [subprocess.run](https://docs.python.org/3/library/subprocess.html) for more details.\n",
"- **``running``** (boolean, readonly): Whether or not the subprocess is running. Defaults to False.\n",
"- **``run``** (action): Executes `subprocess.run` in a child process using the args and kwargs parameters provided as arguments or as parameter values on the instance. The child process is running in a *pseudo terminal* ([pty](https://docs.python.org/3/library/pty.html)) which is then connected to the Terminal.\n",
"- **``kill``** (action): Kills the subprocess if it is running.\n",
"\n",
"___"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"terminal = pn.widgets.Terminal(\n",
" \"Welcome to the Panel Terminal!\\nI'm based on xterm.js\\n\\n\",\n",
" options={\"cursorBlink\": True},\n",
" height=300, sizing_mode='stretch_width'\n",
")\n",
"\n",
"terminal"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Writing strings to the terminal"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"terminal.write(\"This is written directly to the terminal.\\n\")\n",
"terminal.write(\"Danish Characters: æøåÆØÅ\\n\")\n",
"terminal.write(\"Emoji: Python 🐍 Panel ❤️ 😊 \\n\")\n",
"terminal.write(\"Links: https://panel.holoviz.org\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Writing stdout to the terminal"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"sys.stdout = terminal\n",
"print(\"This print statement is redirected from stdout to the Panel Terminal\")\n",
"\n",
"sys.stdout = sys.__stdout__\n",
"print(\"This print statement is again redirected to the server console\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Logging to the terminal"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"logger = logging.getLogger(\"terminal\")\n",
"logger.setLevel(logging.DEBUG)\n",
"\n",
"stream_handler = logging.StreamHandler(terminal) # NOTE THIS\n",
"stream_handler.terminator = \" \\n\"\n",
"formatter = logging.Formatter(\"%(asctime)s [%(levelname)s]: %(message)s\")\n",
"\n",
"stream_handler.setFormatter(formatter)\n",
"stream_handler.setLevel(logging.DEBUG)\n",
"logger.addHandler(stream_handler)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"logger.info(\"Hello Info Logger\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Streaming to the terminal\n",
"\n",
"We only do this to a reduced amount as you can reach a general rate limit in the notebook."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for i in range(0, 50):\n",
" logger.info(uuid.uuid4()) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Run SubProcess and direct output to Terminal"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"terminal.subprocess.run(\"ls\", \"-l\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let us review the output so far since a static rendering of this page will not dynamically update the contents of the terminal displayed above:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"terminal"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Clear the Terminal"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"terminal.clear()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Run an Interactive Process in the Terminal\n",
"\n",
"You can run interactive processes like `bash`, `python`, `ipython` or similar."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"subprocess_terminal = pn.widgets.Terminal(\n",
" options={\"cursorBlink\": True},\n",
" height=300, sizing_mode='stretch_width'\n",
")\n",
"\n",
"run_python = pn.widgets.Button(name=\"Run Python\", button_type=\"success\")\n",
"run_python.on_click(\n",
" lambda x: subprocess_terminal.subprocess.run(\"python\")\n",
")\n",
"\n",
"kill = pn.widgets.Button(name=\"Kill Python\", button_type=\"danger\")\n",
"kill.on_click(\n",
" lambda x: subprocess_terminal.subprocess.kill()\n",
")\n",
"\n",
"pn.Column(\n",
" pn.Row(run_python, kill, subprocess_terminal.subprocess.param.running),\n",
" subprocess_terminal,\n",
" sizing_mode='stretch_both',\n",
" height=500\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
4 changes: 2 additions & 2 deletions panel/_templates/doc_nb_js.js
Expand Up @@ -4,12 +4,12 @@
var render_items = {{ render_items }};
root.Bokeh.embed.embed_items_notebook(docs_json, render_items);
}
if (root.Bokeh !== undefined && root.Bokeh.Panel !== undefined{% for req in requirements %} && root['{{ req }}'] !== undefined {% endfor %}{% if ipywidget %}&& (root.Bokeh.Models.registered_names().indexOf("ipywidgets_bokeh.widget.IPyWidget") > -1){% endif %}) {
if (root.Bokeh !== undefined && root.Bokeh.Panel !== undefined{% for reqs in requirements %} && ({% for req in reqs %}{% if loop.index0 > 0 %}||{% endif %} root['{{ req }}'] !== undefined{% endfor %}){% endfor %}{% if ipywidget %}&& (root.Bokeh.Models.registered_names().indexOf("ipywidgets_bokeh.widget.IPyWidget") > -1){% endif %}) {
embed_document(root);
} else {
var attempts = 0;
var timer = setInterval(function(root) {
if (root.Bokeh !== undefined && root.Bokeh.Panel !== undefined{% for req in requirements %} && root['{{ req }}'] !== undefined{% endfor %}{% if ipywidget %}&& (root.Bokeh.Models.registered_names().indexOf("ipywidgets_bokeh.widget.IPyWidget") > -1){% endif %}) {
if (root.Bokeh !== undefined && root.Bokeh.Panel !== undefined{% for reqs in requirements %} && ({% for req in reqs %}{% if loop.index0 > 0 %} || {% endif %}root['{{ req }}'] !== undefined{% endfor %}){% endfor %}{% if ipywidget %}&& (root.Bokeh.Models.registered_names().indexOf("ipywidgets_bokeh.widget.IPyWidget") > -1){% endif %}) {
clearInterval(timer);
embed_document(root);
} else if (document.readyState == "complete") {
Expand Down
19 changes: 10 additions & 9 deletions panel/config.py
Expand Up @@ -318,7 +318,6 @@ def oauth_extra_params(self):
return _config._oauth_extra_params



if hasattr(_config.param, 'objects'):
_params = _config.param.objects()
else:
Expand Down Expand Up @@ -346,18 +345,20 @@ class panel_extension(_pyviz_extension):
'ace': 'panel.models.ace',
'echarts': 'panel.models.echarts',
'ipywidgets': 'ipywidgets_bokeh.widget',
'perspective': 'panel.models.perspective'
'perspective': 'panel.models.perspective',
'terminal': 'panel.models.terminal',
}

# Check whether these are loaded before rendering
_globals = {
'deckgl': 'deck',
'echarts': 'echarts',
'katex': 'katex',
'mathjax': 'MathJax',
'plotly': 'Plotly',
'vega': 'vega',
'vtk': 'vtk'
'deckgl': ['deck'],
'echarts': ['echarts'],
'katex': ['katex'],
'mathjax': ['MathJax'],
'plotly': ['Plotly'],
'vega': ['vega'],
'vtk': ['vtk'],
'terminal': ['Terminal', 'xtermjs'],
}

_loaded_extensions = []
Expand Down
1 change: 1 addition & 0 deletions panel/models/index.ts
Expand Up @@ -22,6 +22,7 @@ export {ReactiveHTML} from "./reactive_html"
export {SingleSelect} from "./singleselect"
export {SpeechToText} from "./speech_to_text"
export {State} from "./state"
export {Terminal} from "./terminal"
export {TextToSpeech} from "./text_to_speech"
export {TrendIndicator} from "./trend"
export {VegaPlot} from "./vega"
Expand Down
51 changes: 51 additions & 0 deletions panel/models/terminal.py
@@ -0,0 +1,51 @@
from collections import OrderedDict

from bokeh.core.properties import Any, Dict, Int, String
from bokeh.models import HTMLBox

from ..io.resources import bundled_files
from ..util import classproperty


XTERM_JS = "https://unpkg.com/xterm@4.11.0/lib/xterm.js"
XTERM_LINKS_JS = "https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"


class Terminal(HTMLBox):
"""Custom Terminal Model"""

options = Dict(String, Any)
input = String()
output = String()

_clears = Int()
_value_repeats = Int()

__css_raw__ = ["https://unpkg.com/xterm@4.11.0/css/xterm.css"]

@classproperty
def __css__(cls):
return bundled_files(cls, 'css')

__javascript_raw__ = [XTERM_JS, XTERM_LINKS_JS]

@classproperty
def __javascript__(cls):
return bundled_files(cls)

@classproperty
def __js_skip__(cls):
return {
'xtermjs': cls.__javascript__[0:1],
'xtermjs-weblinks': cls.__javascript__[2:3],
}

__js_require__ = {
'paths': OrderedDict([
("xtermjs", XTERM_JS[:-3]),
("xtermjs-weblinks", XTERM_LINKS_JS[:-3]),
]),
'exports': {
"xtermjs": "xtermjs",
"xtermjs-weblinks": "xtermjsweblinks",},
}