diff --git a/README.md b/README.md index 002e7e18..638e9f1a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Docker Hub](https://img.shields.io/docker/pulls/x1unix/go-playground.svg)](https://hub.docker.com/r/x1unix/go-playground) [![Docker Hub](https://img.shields.io/docker/v/x1unix/go-playground.svg?sort=semver)](https://hub.docker.com/r/x1unix/go-playground) +[![Better Stack Badge](https://uptime.betterstack.com/status-badges/v1/monitor/100nk.svg)](https://uptime.betterstack.com/?utm_source=status_badge) [![Coverage Status](https://coveralls.io/repos/github/x1unix/go-playground/badge.svg?branch=dev)](https://coveralls.io/github/x1unix/go-playground?branch=dev) [![Goreportcard](https://goreportcard.com/badge/github.com/x1unix/go-playground)](https://goreportcard.com/report/github.com/x1unix/go-playground) [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) @@ -15,6 +16,7 @@ Improved Go Playground powered by Monaco Editor and React - [https://goplay.tool * 🌚 Dark theme * 💡 Code autocomplete * ⌨️ VIM mode support +* 🌈 Color and image output * 💾 Load and save files * 📔 Snippets and tutorials * ⚙ Customization (fonts, ligatures, etc) diff --git a/web/package.json b/web/package.json index 7ddd12ef..f07304c3 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,11 @@ "@types/jest": "^27.4.0", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", + "@xterm/addon-canvas": "^0.6.0-beta.1", + "@xterm/addon-fit": "^0.9.0-beta.1", + "@xterm/addon-image": "^0.7.0-beta.1", + "@xterm/addon-webgl": "^0.17.0-beta.1", + "@xterm/xterm": "^5.4.0-beta.1", "axios": "^1.6.0", "circular-dependency-plugin": "^5.2.2", "clsx": "^1.1.1", diff --git a/web/src/components/core/Header/Header.tsx b/web/src/components/core/Header/Header.tsx index 16c6b3ef..36c6247d 100644 --- a/web/src/components/core/Header/Header.tsx +++ b/web/src/components/core/Header/Header.tsx @@ -4,16 +4,18 @@ import { ICommandBarItemProps, Stack, } from '@fluentui/react'; -import apiClient, { VersionsInfo } from "~/services/api"; +import apiClient, { type VersionsInfo } from "~/services/api"; import { newAddNotificationAction, NotificationType } from "~/store/notifications"; -import SettingsModal, { SettingsChanges } from '~/components/settings/SettingsModal'; +import SettingsModal, { type SettingsChanges } from '~/components/settings/SettingsModal'; import ThemeableComponent from '~/components/utils/ThemeableComponent'; import AboutModal from '~/components/modals/AboutModal'; import RunTargetSelector from '~/components/inputs/RunTargetSelector'; import SharePopup from '~/components/utils/SharePopup'; + +import { dispatchTerminalSettingsChange } from '~/store/terminal'; import { Connect, - Dispatcher, + type Dispatcher, dispatchToggleTheme, formatFileDispatcher, newCodeImportDispatcher, @@ -25,6 +27,7 @@ import { saveFileDispatcher, shareSnippetDispatcher } from '~/store'; + import { getSnippetsMenuItems, SnippetMenuItem } from './utils'; import './Header.css'; @@ -242,6 +245,10 @@ export class Header extends ThemeableComponent { this.props.dispatch(newSettingsChangeDispatcher(changes.settings)); } + if (changes.terminal) { + this.props.dispatch(dispatchTerminalSettingsChange(changes.terminal)); + } + this.setState({ showSettings: false }); } diff --git a/web/src/components/core/Panel/PanelHeader.tsx b/web/src/components/core/Panel/PanelHeader.tsx index 897679ab..cbe0fabb 100644 --- a/web/src/components/core/Panel/PanelHeader.tsx +++ b/web/src/components/core/Panel/PanelHeader.tsx @@ -1,6 +1,6 @@ import React, {useContext} from 'react'; import {ITheme, ThemeContext} from '@fluentui/react'; -import PanelAction, {PanelActionProps} from '@components/core/Panel/PanelAction'; +import PanelAction, {PanelActionProps} from '~/components/core/Panel/PanelAction'; import './PanelHeader.css'; interface Props { diff --git a/web/src/components/inspector/Console/Console.css b/web/src/components/inspector/Console/Console.css new file mode 100644 index 00000000..3c17b8a8 --- /dev/null +++ b/web/src/components/inspector/Console/Console.css @@ -0,0 +1,48 @@ +.app-Console { + flex: 1 1 auto; + position: relative; + box-sizing: border-box; + + --terminal-padding-x: 15px; +} + +.app-Console__xterm { + position: absolute; + inset: 0; +} + +.app-Console .terminal > * { + padding: 0 var(--terminal-padding-x); +} + +/** + xterm.js canvas plugin fixes. + + The plugin places a few canvas elements inside with absolute positioning + relative to parent and doesn't respect parent's paddings. + */ +.app-Console .xterm .xterm-screen canvas { + transform: translate3d(var(--terminal-padding-x), 0, 0); +} + +/** +Copy button for touch devices + */ +.app-Console__copy { + position: absolute; + display: none; + z-index: 999; + top: 0; + right: var(--terminal-padding-x); +} + +.app-Console__copy[hidden] { + display: none; +} + +/* Enable copy button only on touch devices */ +@media (hover: none) { + .app-Console__copy { + display: block; + } +} diff --git a/web/src/components/inspector/Console/Console.tsx b/web/src/components/inspector/Console/Console.tsx new file mode 100644 index 00000000..acd00946 --- /dev/null +++ b/web/src/components/inspector/Console/Console.tsx @@ -0,0 +1,239 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import copy from 'copy-to-clipboard'; + +import {DefaultButton, useTheme} from '@fluentui/react'; + +import type {ITerminalAddon, ITerminalOptions} from '@xterm/xterm'; +import {FitAddon} from '@xterm/addon-fit'; +import {ImageAddon} from '@xterm/addon-image'; +import {CanvasAddon} from '@xterm/addon-canvas'; +import {WebglAddon} from '@xterm/addon-webgl'; + +import type {StatusState} from '~/store'; +import {RenderingBackend} from '~/store/terminal'; +import {useXtermTheme, XTerm} from '~/components/utils/XTerm'; + +import {formatEvalEvent} from './format'; +import {createDebounceResizeObserver} from './utils'; + +import './Console.css'; + +const RESIZE_DELAY = 50; + +const imageAddonConfig = { + enableSizeReports: true, + sixelSupport: true, + sixelScrolling: true, + iipSupport: true, +}; + +const config: ITerminalOptions = { + convertEol: true, +}; + +interface Props { + status?: StatusState + fontFamily: string + fontSize: number + backend: RenderingBackend +} + +const getAddonFromBackend = (backend: RenderingBackend): ITerminalAddon | null => { + switch (backend) { + case RenderingBackend.WebGL: + return new WebglAddon(); + case RenderingBackend.Canvas: + return new CanvasAddon(); + default: + return null; + } +} + +const CopyButton: React.FC<{ + onClick?: () => void + hidden?: boolean +}> = ({onClick, hidden}) => { + const theme = useTheme(); + const styles = useMemo(() => ({ + root: { + color: theme?.palette.neutralPrimary, + marginLeft: 'auto', + marginTop: '4px', + marginRight: '2px', + padding: '4px 8px', + minWidth: 'initial' + }, + rootHovered: { + color: theme?.palette.neutralDark + } + }), [theme]); + return ( +