diff --git a/docs/css/custom.css b/docs/css/custom.css new file mode 100644 index 0000000..4c9a178 --- /dev/null +++ b/docs/css/custom.css @@ -0,0 +1,4 @@ +.termynal-comment { + color: #999; + font-style: italic; +} diff --git a/docs/css/termynal.css b/docs/css/termynal.css new file mode 100644 index 0000000..cb79513 --- /dev/null +++ b/docs/css/termynal.css @@ -0,0 +1,101 @@ +/** + * termynal.js + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +:root { + --color-bg: #252a33; + --color-text: #eee; + --color-text-subtle: #a2a2a2; +} + +[data-termynal] { + width: 750px; + max-width: 100%; + background: var(--color-bg); + color: var(--color-text); + font-size: 18px; + font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + border-radius: 4px; + padding: 75px 45px 35px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +[data-termynal]:before { + content: ''; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; +} + +[data-termynal]:after { + content: 'bash'; + position: absolute; + color: var(--color-text-subtle); + top: 5px; + left: 0; + width: 100%; + text-align: center; +} + +[data-ty] { + display: block; + line-height: 2; +} + +[data-ty]:before { + /* Set up defaults and ensure empty lines are displayed. */ + content: ''; + display: inline-block; + vertical-align: middle; +} + +[data-ty="input"]:before, +[data-ty-prompt]:before { + margin-right: 0.75em; + color: var(--color-text-subtle); +} + +[data-ty="input"]:before { + content: '$'; +} + +[data-ty][data-ty-prompt]:before { + content: attr(data-ty-prompt); +} + +[data-ty-cursor]:after { + content: attr(data-ty-cursor); + font-family: monospace; + margin-left: 0.5em; + -webkit-animation: blink 1s infinite; + animation: blink 1s infinite; +} + + +/* Cursor animation */ + +@-webkit-keyframes blink { + 50% { + opacity: 0; + } +} + +@keyframes blink { + 50% { + opacity: 0; + } +} diff --git a/docs/index.md b/docs/index.md index ae724a9..b9a05a6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,9 +44,13 @@ Typer stands on the shoulders of a giant. Its only internal dependency is +```console +$ pip install typer +---> 100% +Successfully installed typer ``` + ## Example @@ -70,45 +74,33 @@ if __name__ == "__main__": Run your application: -```bash -python main.py -``` +
+```console +// Run your application +$ python main.py -you will get a response like: - -``` +// You get a nice error, you are missing NAME Usage: main.py [OPTIONS] NAME Try "main.py --help" for help. Error: Missing argument "NAME". -``` -Now pass the `NAME` *argument*: +// You get a --help for free +$ python main.py --help -```bash -python main.py Camila -``` - -You will get a response like: - -``` -Hello Camila -``` - -And you automatically get a `--help` command: - -```bash -python main.py --help -``` - -shows: - -``` Usage: main.py [OPTIONS] NAME Options: --help Show this message and exit. + +// Now pass the NAME argument +$ python main.py Camila + +Hello Camila + +// It works! 🎉 ``` +
## Example upgrade @@ -154,15 +146,11 @@ And that will: ### Run the upgraded example -Get the main `--help`: +
+```console +// Check the --help +$ python main.py --help -```bash -python main.py --help -``` - -shows: - -``` Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: @@ -171,80 +159,49 @@ Options: Commands: goodbye hello -``` -You have 2 sub-commands (the 2 functions), `goodbye` and `hello`. +// You have 2 sub-commands (the 2 functions): goodbye and hello -Now get the help for `hello`: +// Now get the --help for hello -```bash -python main.py hello --help -``` +$ python main.py hello --help -shows: - -``` Usage: main.py hello [OPTIONS] NAME Options: --help Show this message and exit. -``` -And now get the help for `goodbye`: +// And now get the --help for goodbye -```bash -python main.py goodbye --help -``` +$ python main.py goodbye --help -shows: - -``` Usage: main.py goodbye [OPTIONS] NAME Options: --formal / --no-formal --help Show this message and exit. -``` -Notice how it automatically creates a `--formal` and `--no-formal` for your `bool` *option*. +// Automatic --formal and --no-formal for the bool option 🎉 ---- +// And if you use it with the hello command -And of course, if you use it, it does what you expect: +$ python main.py hello Camila -```bash -python main.py hello Camila -``` - -shows: - -``` Hello Camila -``` -Then: +// And with the goodbye command -```bash -python main.py goodbye Camila -``` +$ python main.py goodbye Camila -shows: - -``` Bye Camila! -``` -And: +// And with --formal -```bash -python main.py goodbye --formal Camila -``` +$ python main.py goodbye --formal Camila -shows: - -``` Goodbye Ms. Camila. Have a good day. ``` +
### Recap diff --git a/docs/js/custom.js b/docs/js/custom.js new file mode 100644 index 0000000..9b398b8 --- /dev/null +++ b/docs/js/custom.js @@ -0,0 +1,76 @@ +document.querySelectorAll(".use-termynal").forEach(node => { + node.style.display = "block"; + new Termynal(node); +}); +const progressLiteralStart = "---> 100%"; +const promptLiteralStart = "$ "; +const termynalActivateClass = "termy"; + +function loadVisibleTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .codehilite`) + .forEach(node => { + if (node.getBoundingClientRect().top - innerHeight <= 0) { + const text = node.textContent; + const lines = text.split("\n"); + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true + buffer.forEach(line => { + if (line) { + isBlankSpace = false + } + }) + dataValue = {} + if (isBlankSpace) { + dataValue["delay"] = 0 + } + if (buffer.length > 1 && buffer[buffer.length - 1] === "") { + // The last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = buffer.join("
"); + dataValue["value"] = bufferValue + useLines.push(dataValue); + buffer = []; + } + } + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress" + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + const value = line.replace("// ", "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else { + buffer.push(line); + } + } + saveBuffer(); + const div = document.createElement("div"); + node.replaceWith(div); + const termynal = new Termynal(div, { + lineData: useLines + }); + } + }); +} +window.addEventListener("scroll", loadVisibleTermynals); +loadVisibleTermynals(); diff --git a/docs/js/termynal.js b/docs/js/termynal.js new file mode 100644 index 0000000..99daf02 --- /dev/null +++ b/docs/js/termynal.js @@ -0,0 +1,202 @@ +/** + * termynal.js + * A lightweight, modern and extensible animated terminal window, using + * async/await. + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +'use strict'; + +/** Generate a terminal widget. */ +class Termynal { + /** + * Construct the widget's settings. + * @param {(string|Node)=} container - Query selector or container element. + * @param {Object=} options - Custom settings. + * @param {string} options.prefix - Prefix to use for data attributes. + * @param {number} options.startDelay - Delay before animation, in ms. + * @param {number} options.typeDelay - Delay between each typed character, in ms. + * @param {number} options.lineDelay - Delay between each line, in ms. + * @param {number} options.progressLength - Number of characters displayed as progress bar. + * @param {string} options.progressChar – Character to use for progress bar, defaults to █. + * @param {number} options.progressPercent - Max percent of progress. + * @param {string} options.cursor – Character to use for cursor, defaults to ▋. + * @param {Object[]} lineData - Dynamically loaded line data objects. + * @param {boolean} options.noInit - Don't initialise the animation. + */ + constructor(container = '#termynal', options = {}) { + this.container = (typeof container === 'string') ? document.querySelector(container) : container; + this.pfx = `data-${options.prefix || 'ty'}`; + this.startDelay = options.startDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; + this.typeDelay = options.typeDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; + this.lineDelay = options.lineDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; + this.progressLength = options.progressLength + || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; + this.progressChar = options.progressChar + || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; + this.progressPercent = options.progressPercent + || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; + this.cursor = options.cursor + || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; + this.lineData = this.lineDataToElements(options.lineData || []); + if (!options.noInit) this.init() + } + + /** + * Initialise the widget, get lines, clear container and start animation. + */ + init() { + // Appends dynamically loaded lines to existing line elements. + this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); + + /** + * Calculates width and height of Termynal container. + * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. + */ + const containerStyle = getComputedStyle(this.container); + this.container.style.width = containerStyle.width !== '0px' ? + containerStyle.width : undefined; + this.container.style.minHeight = containerStyle.height !== '0px' ? + containerStyle.height : undefined; + + this.container.setAttribute('data-termynal', ''); + this.container.innerHTML = ''; + this.start(); + } + + /** + * Start the animation and rener the lines depending on their data attributes. + */ + async start() { + await this._wait(this.startDelay); + + for (let line of this.lines) { + const type = line.getAttribute(this.pfx); + const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; + + if (type == 'input') { + line.setAttribute(`${this.pfx}-cursor`, this.cursor); + await this.type(line); + await this._wait(delay); + } + + else if (type == 'progress') { + await this.progress(line); + await this._wait(delay); + } + + else { + this.container.appendChild(line); + await this._wait(delay); + } + + line.removeAttribute(`${this.pfx}-cursor`); + } + } + + /** + * Animate a typed line. + * @param {Node} line - The line element to render. + */ + async type(line) { + const chars = [...line.textContent]; + const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; + line.textContent = ''; + this.container.appendChild(line); + + for (let char of chars) { + await this._wait(delay); + line.textContent += char; + } + } + + /** + * Animate a progress bar. + * @param {Node} line - The line element to render. + */ + async progress(line) { + const progressLength = line.getAttribute(`${this.pfx}-progressLength`) + || this.progressLength; + const progressChar = line.getAttribute(`${this.pfx}-progressChar`) + || this.progressChar; + const chars = progressChar.repeat(progressLength); + const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) + || this.progressPercent; + line.textContent = ''; + this.container.appendChild(line); + + for (let i = 1; i < chars.length + 1; i++) { + await this._wait(this.typeDelay); + const percent = Math.round(i / chars.length * 100); + line.textContent = `${chars.slice(0, i)} ${percent}%`; + if (percent>progressPercent) { + break; + } + } + } + + /** + * Helper function for animation delays, called with `await`. + * @param {number} time - Timeout, in ms. + */ + _wait(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + + /** + * Converts line data objects into line elements. + * + * @param {Object[]} lineData - Dynamically loaded lines. + * @param {Object} line - Line data object. + * @returns {Element[]} - Array of line elements. + */ + lineDataToElements(lineData) { + return lineData.map(line => { + let div = document.createElement('div'); + div.innerHTML = `${line.value || ''}`; + + return div.firstElementChild; + }); + } + + /** + * Helper function for generating attributes string. + * + * @param {Object} line - Line data object. + * @returns {string} - String of attributes. + */ + _attributes(line) { + let attrs = ''; + for (let prop in line) { + // Custom add class + if (prop === 'class') { + attrs += ` class=${line[prop]} ` + continue + } + attrs += this.pfx; + + if (prop === 'type') { + attrs += `="${line[prop]}" ` + } else if (prop !== 'value') { + attrs += `-${prop}="${line[prop]}" ` + } + } + + return attrs; + } +} + +/** +* HTML API: If current script has container(s) specified, initialise Termynal. +*/ +if (document.currentScript.hasAttribute('data-termynal-container')) { + const containers = document.currentScript.getAttribute('data-termynal-container'); + containers.split('|') + .forEach(container => new Termynal(container)) +} diff --git a/mkdocs.yml b/mkdocs.yml index 8797a99..ebee15a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,3 +53,11 @@ extra: link: 'https://medium.com/@tiangolo' - type: 'globe' link: 'https://tiangolo.com' + +extra_css: + - 'css/termynal.css' + - 'css/custom.css' + +extra_javascript: + - 'js/termynal.js' + - 'js/custom.js'