diff --git a/index.html b/index.html
index 26db8e22..e937f604 100644
--- a/index.html
+++ b/index.html
@@ -90,7 +90,7 @@
-
diff --git a/js/common/dialogs.js b/js/common/dialogs.js
index 74bb0772..4c0295ca 100644
--- a/js/common/dialogs.js
+++ b/js/common/dialogs.js
@@ -241,6 +241,26 @@ class MessageModal extends GenericModal {
}
}
+class InputModal extends GenericModal {
+ _handleOkButton(event) {
+ this._returnValue(this._getElement('inputValueField').value);
+ }
+
+ async open(message, defaultValue="") {
+ let p = super.open();
+ const cancelButton = this._currentModal.querySelector("button.cancel-button");
+ this._addDialogElement('cancelButton', cancelButton, 'click', this._closeModal);
+ const okButton = this._currentModal.querySelector("button.ok-button");
+ this._addDialogElement('okButton', okButton, 'click', this._handleOkButton);
+ const inputValueField = this._currentModal.querySelector("#inputvalue");
+ this._addDialogElement('inputValueField', inputValueField);
+ this._setElementValue('inputValueField', defaultValue);
+ this._currentModal.querySelector("#message").innerHTML = message;
+
+ return p;
+ }
+}
+
class ProgressDialog extends GenericModal {
async open() {
let p = super.open();
@@ -424,5 +444,6 @@ export {
UnsavedDialog,
DiscoveryModal,
ProgressDialog,
- DeviceInfoModal
-};
\ No newline at end of file
+ DeviceInfoModal,
+ InputModal
+};
diff --git a/js/common/file_dialog.js b/js/common/file_dialog.js
index 3e05266d..b96b609b 100644
--- a/js/common/file_dialog.js
+++ b/js/common/file_dialog.js
@@ -1,4 +1,4 @@
-import {GenericModal, ProgressDialog, ButtonValueDialog} from './dialogs.js';
+import {GenericModal, ProgressDialog, ButtonValueDialog, InputModal} from './dialogs.js';
import {readUploadedFileAsArrayBuffer} from './utilities.js';
import {saveAs} from 'file-saver';
import JSZip from 'jszip';
@@ -321,6 +321,11 @@ class FileDialog extends GenericModal {
return selectedItems > 1;
}
+ async _prompt(message, defaultValue="") {
+ const inputModal = new InputModal("input");
+ return await inputModal.open(message, defaultValue);
+ }
+
_updateToolbar() {
this._setElementEnabled('delButton', this._canPerformWritableFileOperation());
this._setElementEnabled('renameButton', !this._multipleItemsSelected() && this._canPerformWritableFileOperation());
@@ -653,7 +658,8 @@ class FileDialog extends GenericModal {
return;
}
oldName = oldName[0];
- let newName = prompt("Enter a new folder name", oldName);
+ let newName = await this._prompt("Enter a new name", oldName);
+
// If cancelled, do nothing
if (!newName) {
return;
@@ -688,7 +694,7 @@ class FileDialog extends GenericModal {
async _handleNewFolderButton() {
if (this._readOnlyMode) return;
// prompt for new folder name
- let folderName = prompt("Enter a new folder name");
+ let folderName = await this._prompt("Enter a new folder name");
// If cancelled, do nothing
if (!folderName) {
return;
diff --git a/js/common/settings.js b/js/common/settings.js
new file mode 100644
index 00000000..6f48b5ad
--- /dev/null
+++ b/js/common/settings.js
@@ -0,0 +1,125 @@
+import {GenericModal} from './dialogs.js';
+
+class SettingsDialog extends GenericModal {
+ constructor(modalId, settingsData) {
+ super(modalId);
+ this._settingsData = settingsData;
+ this._settings = {}
+ }
+
+ async open(settings) {
+ let p = super.open();
+ const cancelButton = this._currentModal.querySelector("button.cancel-button");
+ this._addDialogElement('cancelButton', cancelButton, 'click', this._closeModal);
+ const okButton = this._currentModal.querySelector("button.ok-button");
+ this._addDialogElement('okButton', okButton, 'click', this._handleOkButton);
+
+ const contentDiv = this._currentModal.querySelector("#settings-content");
+ contentDiv.innerHTML = '';
+
+ for (const setting of this._settingsData) {
+ const label = document.createElement('label');
+ label.textContent = setting.label;
+ if (setting.icon) {
+ const icon = document.createElement('i');
+ icon.className = `fa-solid fa-${setting.icon} setting-item-icon`;
+ label.prepend(icon);
+ }
+ label.htmlFor = `setting-${setting.key}`;
+ contentDiv.appendChild(label);
+
+ const control = await this._createControl(setting);
+ control.value = settings[setting.key];
+ contentDiv.appendChild(control);
+ }
+
+ return p;
+ }
+
+ async _handleOkButton() {
+ let settings = {}
+ for (const setting of this._settingsData) {
+ const control = this._currentModal.querySelector(`#setting-${setting.key}`);
+ settings[setting.key] = control.value;
+ }
+ this._returnValue(settings);
+ }
+
+ async _createControl(settingData) {
+ // Return the created control
+ let control;
+ if (settingData.type === 'select') {
+ control = document.createElement('select');
+ for (const optionValue of settingData.options) {
+ const option = document.createElement('option');
+ option.value = optionValue;
+ option.textContent = optionValue.charAt(0).toUpperCase() + optionValue.slice(1);
+ control.appendChild(option);
+ }
+ }
+ control.id = `setting-${settingData.key}`;
+
+ // this will also call this._addDialogElement to add event listeners as needed
+ this._addDialogElement(`setting-${settingData.key}`, control);
+ return control;
+ }
+}
+
+class Settings {
+ // This is a class that handles loading/saving settings as well as providing a settings dialog
+ constructor() {
+ // This will hold the layout/save data for the settings
+ this._settingsData = [
+ { key: 'theme', type: 'select', label: 'Editor Theme', icon: 'palette', options: ['dark', 'light'], default: 'dark' }
+ ];
+ this._settings = {};
+ this._loadSettings();
+
+ this._settingsDialog = new SettingsDialog('settings', this._settingsData);
+ }
+
+ _loadSettings() {
+ // Load all saved settings or defaults
+ for (const setting of this._settingsData) {
+ this._settings[setting.key] = this._loadSetting(setting.key, setting.default);
+ }
+ }
+
+ _saveSettings() {
+ // Save all settings
+ for (const key in this._settings) {
+ this._saveSetting(key, this._settings[key]);
+ }
+ }
+
+ _loadSetting(setting, defaultValue) {
+ let value = JSON.parse(window.localStorage.getItem(setting));
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value;
+ }
+
+ _saveSetting(setting, value) {
+ window.localStorage.setItem(setting, JSON.stringify(value));
+ }
+
+ getSetting(key) {
+ return this._settings[key];
+ }
+
+ async showDialog() {
+ this._settings = await this._settingsDialog.open(this._settings);
+ if (this._settings) {
+ this._saveSettings();
+ return true;
+ }
+ return false;
+ }
+}
+
+
+export {
+ Settings
+};
diff --git a/js/common/utilities.js b/js/common/utilities.js
index 0b9f8811..4ad48244 100644
--- a/js/common/utilities.js
+++ b/js/common/utilities.js
@@ -53,7 +53,7 @@ function isIp() {
// Check if the current url is a Web Workflow, IP, or Test Address and current path is /code/
function isLocal() {
- return (isMdns() || location.hostname == "localhost" || isIp()) && (location.pathname == "/code/");
+ return location.hostname == "localhost" || ((isMdns() || isIp()) && location.pathname == "/code/");
}
// Test to see if browser is running on Microsoft Windows OS
@@ -67,7 +67,7 @@ function isMicrosoftWindows() {
return false;
}
-// Test to see if browser is running on Microsoft Windows OS
+// Test to see if browser is running on Chrome OS
function isChromeOs() {
if (navigator.userAgent.includes("CrOS")) {
return true;
diff --git a/js/layout.js b/js/layout.js
index 8323a673..aa5caf4c 100644
--- a/js/layout.js
+++ b/js/layout.js
@@ -26,12 +26,49 @@ function isSerialVisible() {
return serialPage.classList.contains('active');
}
+function setupPanelFocusHandlers() {
+ // Removing existing handlers to avoid duplicates
+ serialPage.removeEventListener('click', handleActivePanel);
+ editorPage.removeEventListener('click', handleActivePanel);
+
+ // Adding new handlers
+ serialPage.addEventListener('click', handleActivePanel);
+ editorPage.addEventListener('click', handleActivePanel);
+}
+
+function handleActivePanel(event) {
+ const panel = event.currentTarget;
+ setActivePanel(panel);
+}
+
+function setActivePanel(panel) {
+ editorPage.classList.remove('focused-panel');
+ serialPage.classList.remove('focused-panel');
+
+ if (panel === serialPage && isSerialVisible()) {
+ // Serial panel requested and visible
+ serialPage.classList.add('focused-panel');
+ } else if (panel === editorPage && isEditorVisible()) {
+ // Editor panel requested and visible
+ editorPage.classList.add('focused-panel');
+ } else {
+ // Requested panel is not visible, set other panel as focused
+ if (isEditorVisible()) {
+ editorPage.classList.add('focused-panel');
+ } else {
+ serialPage.classList.add('focused-panel');
+ }
+ }
+}
+
async function toggleEditor() {
if (isSerialVisible()) {
editorPage.classList.toggle('active');
saveSetting(SETTING_EDITOR_VISIBLE, isEditorVisible());
updatePageLayout(UPDATE_TYPE_EDITOR);
}
+ setupPanelFocusHandlers();
+ setActivePanel(editorPage);
}
async function toggleSerial() {
@@ -40,6 +77,8 @@ async function toggleSerial() {
saveSetting(SETTING_TERMINAL_VISIBLE, isSerialVisible());
updatePageLayout(UPDATE_TYPE_SERIAL);
}
+ setupPanelFocusHandlers();
+ setActivePanel(serialPage);
}
btnModeEditor.removeEventListener('click', toggleEditor);
@@ -229,4 +268,6 @@ pageSeparator.addEventListener('mousedown', async function (e) {
fixViewportHeight();
window.addEventListener("resize", fixViewportHeight);
-loadPanelSettings();
\ No newline at end of file
+loadPanelSettings();
+setupPanelFocusHandlers();
+setActivePanel(editorPage);
diff --git a/js/script.js b/js/script.js
index 7ae4af0e..b8a09b4f 100644
--- a/js/script.js
+++ b/js/script.js
@@ -1,7 +1,7 @@
import { basicSetup } from "codemirror";
import { EditorView, keymap } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
-import {indentWithTab} from "@codemirror/commands"
+import { indentWithTab } from "@codemirror/commands"
import { python } from "@codemirror/lang-python";
import { syntaxHighlighting, indentUnit } from "@codemirror/language";
import { classHighlighter } from "@lezer/highlight";
@@ -17,9 +17,10 @@ import { WebWorkflow } from './workflows/web.js';
import { isValidBackend, getBackendWorkflow, getWorkflowBackendName } from './workflows/workflow.js';
import { ButtonValueDialog, MessageModal } from './common/dialogs.js';
import { isLocal, switchUrl, getUrlParam } from './common/utilities.js';
+import { Settings } from './common/settings.js';
import { CONNTYPE } from './constants.js';
import './layout.js'; // load for side effects only
-import {setupPlotterChart} from "./common/plotter.js";
+import { setupPlotterChart } from "./common/plotter.js";
import { mainContent, showSerial } from './layout.js';
// Instantiate workflows
@@ -31,6 +32,7 @@ workflows[CONNTYPE.Web] = new WebWorkflow();
let workflow = null;
let unchanged = 0;
let connectionPromise = null;
+let debugMessageAnsi = null;
const btnRestart = document.querySelector('.btn-restart');
const btnHalt = document.querySelector('.btn-halt');
@@ -43,13 +45,15 @@ const btnSave = document.querySelectorAll('.btn-save');
const btnSaveAs = document.querySelectorAll('.btn-save-as');
const btnSaveRun = document.querySelectorAll('.btn-save-run');
const btnInfo = document.querySelector('.btn-info');
+const btnSettings = document.querySelector('.btn-settings');
const terminalTitle = document.getElementById('terminal-title');
const serialPlotter = document.getElementById('plotter');
const messageDialog = new MessageModal("message");
const connectionType = new ButtonValueDialog("connection-type");
+const settings = new Settings();
-const editorTheme = EditorView.theme({}, {dark: true});
+const editorTheme = EditorView.theme({}, {dark: getCssVar('editor-theme-dark').trim() === '1'});
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('mobile-menu-button').addEventListener('click', handleMobileToggle);
@@ -168,6 +172,12 @@ btnInfo.addEventListener('click', async function(e) {
}
});
+btnSettings.addEventListener('click', async function(e) {
+ if (await settings.showDialog()) {
+ applySettings();
+ }
+});
+
// Basic functions used for buttons and hotkeys
async function openFile() {
if (await checkConnected()) {
@@ -229,6 +239,7 @@ async function checkConnected() {
await workflow.showInfo(getDocState());
}
}
+
return true;
}
@@ -419,8 +430,12 @@ async function showMessage(message) {
}
async function debugLog(msg) {
+ if (debugMessageAnsi === null) {
+ const colorCode = getCssVar('debug-message-color').trim();
+ debugMessageAnsi = `\x1b[38;2;${parseInt(colorCode.slice(1,3),16)};${parseInt(colorCode.slice(3,5),16)};${parseInt(colorCode.slice(5,7),16)}m`;
+ }
state.terminal.writeln(''); // get a fresh line without any prior content (a '>>>' prompt might be there without newline)
- state.terminal.writeln(`\x1b[93m${msg}\x1b[0m`);
+ state.terminal.writeln(`${debugMessageAnsi}${msg}\x1b[0m`);
}
function updateUIConnected(isConnected) {
@@ -497,7 +512,7 @@ async function saveFileContents(path) {
saveRetryCount++;
if (saveRetryCount < MAX_SAVE_RETRIES) {
console.log(`Save retry ${saveRetryCount} of ${MAX_SAVE_RETRIES}...`);
- currentTimeout = setTimeout(saveFileContents, 2000);
+ currentTimeout = setTimeout(() => saveFileContents(path), 2000);
} else {
saveRetryCount = 0;
await showMessage(`Saving file '${workflow.currentFilename}' failed after multiple attempts. Check your connection and try again.`);
@@ -561,12 +576,16 @@ editor = new EditorView({
parent: document.querySelector('#editor')
});
+function getCssVar(varName) {
+ return window.getComputedStyle(document.body).getPropertyValue("--" + varName);
+}
+
async function setupXterm() {
state.terminal = new Terminal({
theme: {
- background: '#333',
- foreground: '#ddd',
- cursor: '#ddd',
+ background: getCssVar('background-color'),
+ foreground: getCssVar('terminal-text-color'),
+ cursor: getCssVar('terminal-text-color'),
}
});
@@ -599,8 +618,40 @@ function loadParameterizedContent() {
return documentState;
}
+function applySettings() {
+ // ----- Themes -----
+ const theme = settings.getSetting('theme');
+ // Remove all theme-[option] classes from body
+ document.body.classList.forEach((className) => {
+ if (className.startsWith('theme-')) {
+ document.body.classList.remove(className);
+ }
+ });
+
+ // Add the selected theme class
+ document.body.classList.add(`theme-${theme}`);
+
+ // Apply to EditorView.theme dark parameter
+ editor.darkTheme = getCssVar('editor-theme-dark').trim() === '1';
+
+ // Apply to xterm
+ state.terminal.options.theme = {
+ background: getCssVar('background-color'),
+ foreground: getCssVar('terminal-text-color'),
+ cursor: getCssVar('terminal-text-color'),
+ };
+
+ debugMessageAnsi = null;
+
+ // Note: Debug Message color is applied on next debug message or reload
+ // I'm not sure how to go through the xterm's existing content and change escape sequences
+ // Changing the CSS style reverts to the old style on terminal update/redraw
+
+}
+
document.addEventListener('DOMContentLoaded', async (event) => {
await setupXterm();
+ applySettings();
btnConnect.forEach((element) => {
element.addEventListener('click', async function(e) {
e.preventDefault();
@@ -636,7 +687,6 @@ document.addEventListener('DOMContentLoaded', async (event) => {
}
}
} else {
- //await showMessage("USB Workflow is currently experiencing issues. See
GitHub issue #203 for more details. Please use Web Workflow.");
await checkConnected();
}
});
diff --git a/js/workflows/workflow.js b/js/workflows/workflow.js
index 76d9c7dd..1c334bed 100644
--- a/js/workflows/workflow.js
+++ b/js/workflows/workflow.js
@@ -1,7 +1,8 @@
import {REPL} from '@adafruit/circuitpython-repl-js';
+//import {REPL} from '../../../circuitpython-repl-js/repl.js';
import {FileHelper} from '../common/file.js';
-import {UnsavedDialog} from '../common/dialogs.js';
+import {ButtonValueDialog, UnsavedDialog} from '../common/dialogs.js';
import {FileDialog, FILE_DIALOG_OPEN, FILE_DIALOG_SAVE} from '../common/file_dialog.js';
import {CONNTYPE, CONNSTATE} from '../constants.js';
import {plotValues} from '../common/plotter.js'
@@ -42,6 +43,7 @@ class Workflow {
this.disconnectCallback = null;
this.loadEditor = null;
this.connectDialog = null;
+ this._okCancelDialog = new ButtonValueDialog("ok-cancel");
this._connected = false;
this.currentFilename = null;
this.fileHelper = null;
@@ -91,9 +93,27 @@ class Workflow {
}
async restartDevice() {
+ if (await this.safeMode()) {
+ let result = await this._okCancelDialog.open("Device is currently in safe mode. Reboot device?");
+ if (result === "ok") {
+ console.log("Rebooting device from safe mode");
+ await this.rebootDevice();
+ }
+ }
await this.repl.softRestart();
}
+ async rebootDevice() {
+ let code = `
+try:
+ import microcontroller
+ microcontroller.reset()
+except ImportError:
+ pass
+`;
+ await this.showBusy(this.repl.runCode(code));
+ }
+
async haltScript() {
await this.repl.interruptCode();
}
@@ -315,6 +335,20 @@ class Workflow {
return false;
}
+ async safeMode() {
+ let code = `
+try:
+ import supervisor
+ print(supervisor.runtime.safe_mode_reason is not supervisor.SafeModeReason.NONE)
+except ImportError:
+ print(False)
+`;
+ let result = await this.showBusy(this.repl.runCode(code));
+ let isSafeMode = result.match("True") != null;
+
+ return isSafeMode;
+ }
+
async parseParams() {
return true;
}
diff --git a/sass/base/_base.scss b/sass/base/_base.scss
index d56a3e2d..60d964e5 100644
--- a/sass/base/_base.scss
+++ b/sass/base/_base.scss
@@ -88,3 +88,23 @@ h5 {
background-color: #d8d8d8;
color: #888;
}
+
+@media print {
+ body {
+ visibility: hidden;
+ }
+ #editor-page.focused-panel #editor, #serial-page.focused-panel #terminal {
+ visibility: visible;
+ position: absolute;
+ left: 0;
+ top: 0;
+ font-size: 2em;
+ .xterm-rows {
+ font-size: 1.2em;
+ &> div {
+ height: 1.2em !important;
+ line-height: 1.2em !important;
+ }
+ }
+ }
+}
diff --git a/sass/layout/_editor.scss b/sass/layout/_editor.scss
deleted file mode 100644
index 3ca2a09f..00000000
--- a/sass/layout/_editor.scss
+++ /dev/null
@@ -1,98 +0,0 @@
-
-.cm-editor {
- color: #ddd;
- background-color: #333;
- line-height: 1.5;
- font-family: 'Operator Mono', 'Source Code Pro', Menlo, Monaco, Consolas, Courier New, monospace;
- max-height: calc(100vh - 2*4em - 5em);
-
- .cm-content {
- caret-color: orange;
- }
-
- .cm-comment {
- font-style: italic;
- color: #676B79;
- }
-
- .cm-operator {
- color: #f3f3f3;
- }
-
- .cm-string {
- color: #19F9D8;
- }
-
- .cm-string-2 {
- color: #FFB86C;
- }
-
- .cm-tag {
- color: #ff2c6d;
- }
-
- .cm-meta {
- color: #b084eb;
- }
-
- &.cm-focused .cm-cursor {
- border-left-color: orange;
- }
-
- &.cm-focused .cm-selectionBackground,
- ::selection {
- background-color: orange;
- }
-
- &.ͼ3.cm-focused .cm-scroller .cm-selectionLayer .cm-selectionBackground {
- background-color: #99eeff33;
- }
-
- .cm-gutters {
- background-color: #292a2b;
- color: #ddd;
- border: none;
- }
-
- .cm-scroller {
- overflow: auto;
- }
-
- /* Highlight Tags */
- .tok-comment {
- color: #7F848E;
- }
-
- .tok-variableName {
- color: #61AFEF;
- }
-
- .tok-operator {
- color: #56B6C2;
- }
-
- .tok-string {
- color: #98C379;
- }
-
- .tok-punctuation {
- color: #fff;
- }
-
- .tok-number {
- color: #E5C07B,
- }
-
- .tok-keyword {
- color: #C678DD,
- }
-
- .tok-propertyName {
- color: #D19A66;
- }
-
- .tok-atom,
- .tok-bool {
- color: #E06C75;
- }
-}
diff --git a/sass/layout/_layout.scss b/sass/layout/_layout.scss
index eae8f4f3..b9f808ec 100644
--- a/sass/layout/_layout.scss
+++ b/sass/layout/_layout.scss
@@ -52,10 +52,6 @@
display: flex;
}
}
-
- &.unsaved .file-path {
- color: #f60;
- }
}
#editor-bar, #serial-bar {
@@ -73,14 +69,12 @@
#editor-page {
#editor {
flex: 1 1 0%;
- background: #333;
}
}
#serial-page {
#plotter {
flex: 2 1 0;
- background: #777;
position: relative;
width: 99%;
overflow: hidden;
@@ -92,7 +86,6 @@
}
#terminal {
flex: 1 1 0%;
- background: #333;
position: relative;
width: 100%;
overflow: hidden;
@@ -103,18 +96,7 @@
cursor: default;
position: absolute;
inset: 0;
- scrollbar-color: var(--highlight) var(--dark);
- scrollbar-width: thin;
width: initial !important;
-
- &::-webkit-scrollbar {
- background-color: var(--dark);
- width: 5px;
- }
-
- &::-webkit-scrollbar-thumb {
- background: var(--highlight);
- }
}
}
#buffer-size{
@@ -169,6 +151,26 @@
text-decoration: underline;
}
}
+
+ &.settings-dialog {
+ #settings-content {
+ padding-top: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 300px;
+ label {
+ i {
+ margin-right: 3px;
+ }
+ margin-right: 20px;
+ white-space: nowrap;
+ }
+ input, select {
+ width: 100%;
+ }
+ }
+ }
}
.mode-button {
diff --git a/sass/layout/_themes.scss b/sass/layout/_themes.scss
new file mode 100644
index 00000000..500f1fd5
--- /dev/null
+++ b/sass/layout/_themes.scss
@@ -0,0 +1,177 @@
+@use "../base/variables" as *;
+
+// Dark Theme (default)
+:root {
+ --editor-theme-dark: 1; // General dark theme flag
+ --background-color: #333;
+ --plotter-background: #777;
+ --border-style: none;
+ --terminal-text-color: #ddd;
+ --punctuation-color: #fff;
+ --operator-color: #f3f3f3;
+ --gutter-color: #292a2b;
+ --gutter-active-line-color: #222227;
+ --gutter-text-color: #ddd;
+ --debug-message-color: #fce94f;
+ --unsaved-file-color: #f60;
+}
+
+// Light Theme
+.theme-light {
+ --editor-theme-dark: 0; // General dark theme flag
+ --background-color: #f8f8f8;
+ --plotter-background: #ccc;
+ --border-style: 1px solid #{$gray-border};
+ --terminal-text-color: #333;
+ --punctuation-color: #000;
+ --gutter-color: #ddd;
+ --gutter-active-line-color: #ccc;
+ --gutter-text-color: #222;
+ --debug-message-color: #FF9900;
+}
+
+// Styles applied to both themes
+
+#main-content {
+ &.unsaved .file-path {
+ color: var(--unsaved-file-color);
+ }
+}
+
+#footer-bar {
+ border-top: var(--border-style);
+}
+
+#editor-bar, #serial-bar {
+ border-bottom: var(--border-style);
+}
+
+#editor-page {
+ #editor {
+ background: var(--background-color);
+ }
+}
+
+#serial-page {
+ #plotter {
+ background: var(--plotter-background);
+ }
+ #terminal {
+ background: var(--background-color);
+ .xterm .xterm-viewport {
+ scrollbar-color: var(--highlight) var(--dark);
+ scrollbar-width: thin;
+
+ &::-webkit-scrollbar {
+ background-color: var(--dark);
+ width: 5px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--highlight);
+ }
+ }
+ }
+}
+
+.cm-editor {
+ color: #ddd;
+ background-color: var(--background-color);
+ line-height: 1.5;
+ font-family: 'Operator Mono', 'Source Code Pro', Menlo, Monaco, Consolas, Courier New, monospace;
+ max-height: calc(100vh - 2*4em - 5em);
+
+ .cm-content {
+ caret-color: orange;
+ }
+
+ .cm-comment {
+ font-style: italic;
+ color: #676B79;
+ }
+
+ .cm-operator {
+ color: var(--operator-color);
+ }
+
+ .cm-string {
+ color: #19F9D8;
+ }
+
+ .cm-string-2 {
+ color: #FFB86C;
+ }
+
+ .cm-tag {
+ color: #ff2c6d;
+ }
+
+ .cm-meta {
+ color: #b084eb;
+ }
+
+ &.cm-focused .cm-cursor {
+ border-left-color: orange;
+ }
+
+ &.cm-focused .cm-selectionBackground,
+ ::selection {
+ background-color: orange;
+ }
+
+ &.ͼ3.cm-focused .cm-scroller .cm-selectionLayer .cm-selectionBackground {
+ background-color: #99eeff33;
+ }
+
+ .cm-gutters {
+ background-color: var(--gutter-color);
+ color: var(--gutter-text-color);
+ border: none;
+ }
+
+ .cm-activeLineGutter {
+ background-color: var(--gutter-active-line-color);
+ }
+
+ .cm-scroller {
+ overflow: auto;
+ }
+
+ /* Highlight Tags */
+ .tok-comment {
+ color: #7F848E;
+ }
+
+ .tok-variableName {
+ color: #61AFEF;
+ }
+
+ .tok-operator {
+ color: #56B6C2;
+ }
+
+ .tok-string {
+ color: #98C379;
+ }
+
+ .tok-punctuation {
+ color: var(--punctuation-color);
+ }
+
+ .tok-number {
+ color: #E5C07B,
+ }
+
+ .tok-keyword {
+ color: #C678DD,
+ }
+
+ .tok-propertyName {
+ color: #D19A66;
+ }
+
+ .tok-atom,
+ .tok-bool {
+ color: #E06C75;
+ }
+}
diff --git a/sass/style.scss b/sass/style.scss
index a53a15e0..7eecf567 100644
--- a/sass/style.scss
+++ b/sass/style.scss
@@ -12,6 +12,6 @@
@use 'layout/layout';
@use 'layout/grid';
-@use 'layout/editor';
+@use 'layout/themes';
@use 'layout/header';
@use 'layout/header_mobile';