diff --git a/index.html b/index.html
index 97f4b02..db3c055 100644
--- a/index.html
+++ b/index.html
@@ -120,6 +120,7 @@
+
@@ -153,6 +154,14 @@
+
+
+
+
+
+
+
+
diff --git a/js/common/settings.js b/js/common/settings.js
new file mode 100644
index 0000000..6f48b5a
--- /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/script.js b/js/script.js
index 1025b1c..32c2a50 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()) {
@@ -420,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) {
@@ -547,12 +561,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'),
}
});
@@ -585,8 +603,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();
diff --git a/package-lock.json b/package-lock.json
index a58ee5d..2df60a3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,13 +12,14 @@
"@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.2.5",
"@codemirror/lang-python": "^6.2.1",
"@fortawesome/fontawesome-free": "^7.1.0",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"chart.js": "^4.5.1",
"codemirror": "^6.0.2",
"file-saver": "^2.0.5",
- "focus-trap": "^7.7.0",
+ "focus-trap": "^7.7.1",
"idb-keyval": "^6.2.2",
"jszip": "^3.10.1"
},
@@ -28,7 +29,7 @@
"vite-plugin-mkcert": "^1.17.9"
},
"optionalDependencies": {
- "@rollup/rollup-linux-x64-gnu": "^4.54.0"
+ "@rollup/rollup-linux-x64-gnu": "^4.55.1"
}
},
"node_modules/@adafruit/ble-file-transfer-js": {
@@ -1157,9 +1158,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.54.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
- "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
"cpu": [
"x64"
],
@@ -1551,12 +1552,12 @@
}
},
"node_modules/focus-trap": {
- "version": "7.7.0",
- "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.0.tgz",
- "integrity": "sha512-DJJDHpEgoSbP8ZE1MNeU2IzCpfFyFdNZZRilqmfH2XiQsPK6PtD8AfJqWzEBudUQB2yHwZc5iq54rjTaGQ+ljw==",
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.1.tgz",
+ "integrity": "sha512-Pkp8m55GjxBLnhBoT6OXdMvfRr4TjMAKLvFM566zlIryq5plbhaTmLAJWTGR0EkRwLjEte1lCOG9MxF1ipJrOg==",
"license": "MIT",
"dependencies": {
- "tabbable": "^6.3.0"
+ "tabbable": "^6.4.0"
}
},
"node_modules/follow-redirects": {
@@ -2099,9 +2100,9 @@
"license": "MIT"
},
"node_modules/tabbable": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
- "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
+ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
diff --git a/package.json b/package.json
index 90248a5..03bb285 100644
--- a/package.json
+++ b/package.json
@@ -24,11 +24,11 @@
"chart.js": "^4.5.1",
"codemirror": "^6.0.2",
"file-saver": "^2.0.5",
- "focus-trap": "^7.7.0",
+ "focus-trap": "^7.7.1",
"idb-keyval": "^6.2.2",
"jszip": "^3.10.1"
},
"optionalDependencies": {
- "@rollup/rollup-linux-x64-gnu": "^4.54.0"
+ "@rollup/rollup-linux-x64-gnu": "^4.55.1"
}
}
diff --git a/sass/layout/_editor.scss b/sass/layout/_editor.scss
deleted file mode 100644
index 3ca2a09..0000000
--- 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 eae8f4f..b9f808e 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 0000000..7e11b1e
--- /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;
+ --puctuation-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;
+ --puctuation-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(--puctuation-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 a53a15e..7eecf56 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';