Ability to switch between a combined TX/RX terminal and separate terminals.
@@ -300,6 +304,9 @@ export default observer((props: Props) => {
Found a bug? Have a awesome feature you'd like added to NinjaTerm? Open an issue on GitHub.
+ {/* ========================================================================== */}
+ {/* CONTRIBUTORS */}
+ {/* ========================================================================== */}
ContributorsThanks to Zac Frank for user-interaction guidance and tips!
diff --git a/src/model/App.tsx b/src/model/App.tsx
index cf15659e..480f7a08 100644
--- a/src/model/App.tsx
+++ b/src/model/App.tsx
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
// eslint-disable-next-line max-classes-per-file
-import { makeAutoObservable, runInAction } from 'mobx';
+import { makeAutoObservable, reaction, runInAction } from 'mobx';
import { closeSnackbar } from 'notistack';
import ReactGA from 'react-ga4';
import { Button } from '@mui/material';
@@ -13,13 +13,13 @@ import Snackbar from './Snackbar/Snackbar';
import Graphing from './Graphing/Graphing';
import Logging from './Logging/Logging';
import FakePortsController from './FakePorts/FakePortsController';
-import AppStorage from './Storage/AppStorage';
import { PortState } from './Settings/PortConfigurationSettings/PortConfigurationSettings';
import Terminals from './Terminals/Terminals';
import SingleTerminal from './Terminals/SingleTerminal/SingleTerminal';
import { BackspaceKeyPressBehavior, DeleteKeyPressBehavior, EnterKeyPressBehavior } from './Settings/TxSettings/TxSettings';
import { SelectionController, SelectionInfo } from './SelectionController/SelectionController';
import { isRunningOnWindows } from './Util/Util';
+import { LastUsedSerialPort, ProfileManager } from './ProfileManager/ProfileManager';
declare global {
interface String {
@@ -61,16 +61,11 @@ export enum PortType {
FAKE,
}
-class LastUsedSerialPort {
- serialPortInfo: Partial = {};
- portState: PortState = PortState.CLOSED;
-}
-
const tipsToDisplayOnStartup = [
'TIP: Use Ctrl-Shift-C to copy text \nfrom the terminal, and Ctrl-Shift-V to paste.',
'TIP: Change the type of data displayed between ASCII, HEX and other number types in Settings → RX Settings.',
'TIP: Press Ctrl-Shift-B to send the "break" signal.',
-]
+];
export class App {
settings: Settings;
@@ -120,7 +115,7 @@ export class App {
fakePortController: FakePortsController = new FakePortsController(this);
- appStorage: AppStorage = new AppStorage();
+ profileManager: ProfileManager;
selectionController: SelectionController = new SelectionController();
@@ -137,7 +132,8 @@ export class App {
// Read out the version number from package.json
this.version = packageDotJson['version'];
- this.settings = new Settings(this.appStorage, this.fakePortController);
+ this.profileManager = new ProfileManager(this);
+ this.settings = new Settings(this.profileManager, this.fakePortController);
this.snackbar = new Snackbar();
@@ -166,16 +162,27 @@ export class App {
});
}
+ // Listen for changes to the last applied profile name, and update the app title
+ reaction(() => this.profileManager.lastAppliedProfileName, this.onLastAppliedProfileNameChanged);
+ this.onLastAppliedProfileNameChanged();
+
makeAutoObservable(this); // Make sure this near the end
}
+ onLastAppliedProfileNameChanged = () => {
+ console.log('onLastAppliedProfileNameChanged() called. this.profileManager.lastAppliedProfileName=', this.profileManager.lastAppliedProfileName);
+
+ // Set the title of the app to the last applied profile name
+ document.title = `NinjaTerm - ${this.profileManager.lastAppliedProfileName}`;
+ };
+
/**
* Called once when the React UI is loaded (specifically, when the App is rendered, by using a useEffect()).
*
* This is used to do things that can only be done once the UI is ready, e.g. enqueueSnackbar items.
*/
async onAppUiLoaded() {
- if (this.settings.portConfiguration.config.resumeConnectionToLastSerialPortOnStartup) {
+ if (this.settings.portConfiguration.resumeConnectionToLastSerialPortOnStartup) {
await this.tryToLoadPreviouslyUsedPort();
}
@@ -185,13 +192,23 @@ export class App {
this.snackbar.sendToSnackbar(tipsToDisplayOnStartup[randomIndex], 'info');
}
+ /**
+ * Set the port which will be used if open() is called.
+ *
+ * @param port The serial port to set as the selected port.
+ */
+ setSelectedPort = (port: SerialPort) => {
+ this.port = port;
+ this.serialPortInfo = port.getInfo();
+ };
+
onSerialPortConnected(serialPort: SerialPort) {
console.log('onSerialPortConnected() called.');
if (this.portState === PortState.CLOSED_BUT_WILL_REOPEN) {
// Check to see if this is the serial port we want to reopen
- const lastUsedPortInfo: LastUsedSerialPort = this.appStorage.getData('lastUsedSerialPort');
+ const lastUsedPortInfo = this.profileManager.currentAppConfig.lastUsedSerialPort;
if (lastUsedPortInfo === null) {
return;
}
@@ -216,7 +233,7 @@ export class App {
let approvedPorts = await navigator.serial.getPorts();
// const lastUsedSerialPort = this.appStorage.data.lastUsedSerialPort;
- const lastUsedSerialPort: LastUsedSerialPort = this.appStorage.getData('lastUsedSerialPort') as LastUsedSerialPort;
+ const lastUsedSerialPort = this.profileManager.currentAppConfig.lastUsedSerialPort;
if (lastUsedSerialPort === null) {
// Did not find last used serial port data in local storage, so do nothing
return;
@@ -243,7 +260,7 @@ export class App {
this.serialPortInfo = approvedPortInfo;
if (lastUsedSerialPort.portState === PortState.OPENED) {
- await this.openPort(false);
+ await this.openPort({ silenceSnackbar: true });
this.snackbar.sendToSnackbar(`Automatically opening last used port with info=${lastUsedPortInfoStr}.`, 'success');
} else if (lastUsedSerialPort.portState === PortState.CLOSED) {
this.snackbar.sendToSnackbar(`Automatically selecting last used port with info=${lastUsedPortInfoStr}.`, 'success');
@@ -286,14 +303,11 @@ export class App {
this.serialPortInfo = this.port.getInfo();
// Save the info for this port, so we can automatically re-open
// it on app re-open in the future
- let lastUsedSerialPort: LastUsedSerialPort = this.appStorage.getData('lastUsedSerialPort');
- if (lastUsedSerialPort === null) {
- lastUsedSerialPort = new LastUsedSerialPort();
- }
- lastUsedSerialPort.serialPortInfo = this.serialPortInfo;
- this.appStorage.saveData('lastUsedSerialPort', lastUsedSerialPort);
+ let lastUsedSerialPort = this.profileManager.currentAppConfig.lastUsedSerialPort;
+ lastUsedSerialPort.serialPortInfo = JSON.parse(JSON.stringify(this.serialPortInfo));
+ this.profileManager.saveAppConfig();
});
- if (this.settings.portConfiguration.config.connectToSerialPortAsSoonAsItIsSelected) {
+ if (this.settings.portConfiguration.connectToSerialPortAsSoonAsItIsSelected) {
await this.openPort();
// Go to the terminal pane, only if opening was successful
if (this.portState === PortState.OPENED) {
@@ -308,19 +322,31 @@ export class App {
/**
* Opens the selected serial port using settings from the Port Configuration view.
*
- * @param printSuccessMsg If true, a success message will be printed to the snackbar.
+ * @param obj Optional object with the following properties:
+ * @param obj.silenceSnackbar If true, the snackbar will not be shown when the port is opened successfully.
+ * @returns {Promise} A promise that contains true if the port was opened successfully, false otherwise.
*/
- async openPort(printSuccessMsg = true) {
+ async openPort({ silenceSnackbar = false } = {}) {
if (this.lastSelectedPortType === PortType.REAL) {
// Show the circular progress modal when trying to open the port. If the port opening is going to fail, sometimes it takes
// a few seconds for awaiting open() to complete, so this prevents the user from trying to open the port again while we wait
this.setShowCircularProgressModal(true);
try {
+ // Convert from our flow control enum to the Web Serial API's
+ let flowControlType: FlowControlType;
+ if (this.settings.portConfiguration.flowControl === 'none') {
+ flowControlType = 'none';
+ } else if (this.settings.portConfiguration.flowControl === 'hardware') {
+ flowControlType = 'hardware';
+ } else {
+ throw Error(`Unsupported flow control type ${this.settings.portConfiguration.flowControl}.`);
+ }
await this.port?.open({
- baudRate: this.settings.portConfiguration.config.baudRate, // This might be custom
- dataBits: this.settings.portConfiguration.config.numDataBits,
- parity: this.settings.portConfiguration.config.parity as ParityType,
- stopBits: this.settings.portConfiguration.config.stopBits,
+ baudRate: this.settings.portConfiguration.baudRate, // This might be custom
+ dataBits: this.settings.portConfiguration.numDataBits,
+ parity: this.settings.portConfiguration.parity as ParityType,
+ stopBits: this.settings.portConfiguration.stopBits,
+ flowControl: flowControlType,
bufferSize: 10000,
}); // Default buffer size is only 256 (presumably bytes), which is not enough regularly causes buffer overrun errors
} catch (error) {
@@ -328,28 +354,26 @@ export class App {
if (error.name === 'NetworkError') {
const msg = 'Serial port is already in use by another program.\n' + 'Reported error from port.open():\n' + `${error}`;
this.snackbar.sendToSnackbar(msg, 'error');
- console.log(msg);
+ console.error(msg);
} else {
const msg = `Unrecognized DOMException error with name=${error.name} occurred when trying to open serial port.\n` + 'Reported error from port.open():\n' + `${error}`;
this.snackbar.sendToSnackbar(msg, 'error');
- console.log(msg);
+ console.error(msg);
}
} else {
// Type of error not recognized or seen before
const msg = `Unrecognized error occurred when trying to open serial port.\n` + 'Reported error from port.open():\n' + `${error}`;
this.snackbar.sendToSnackbar(msg, 'error');
- console.log(msg);
+ console.error(msg);
}
- console.log('Disabling modal');
this.setShowCircularProgressModal(false);
// An error occurred whilst calling port.open(), so DO NOT continue, port
// cannot be considered open
- return;
+ return false;
}
- console.log('Open success!');
- if (printSuccessMsg) {
+ if (!silenceSnackbar) {
this.snackbar.sendToSnackbar('Serial port opened.', 'success');
}
@@ -361,9 +385,9 @@ export class App {
this.closedPromise = this.readUntilClosed();
});
- const lastUsedSerialPort: LastUsedSerialPort = this.appStorage.getData('lastUsedSerialPort');
+ const lastUsedSerialPort = this.profileManager.currentAppConfig.lastUsedSerialPort;
lastUsedSerialPort.portState = PortState.OPENED;
- this.appStorage.saveData('lastUsedSerialPort', lastUsedSerialPort);
+ this.profileManager.saveAppConfig();
// Create custom GA4 event to see how many ports have
// been opened in NinjaTerm :-)
@@ -380,6 +404,8 @@ export class App {
this.terminals.txTerminal.clearPartialNumberBuffer();
this.terminals.rxTerminal.clearPartialNumberBuffer();
this.terminals.txRxTerminal.clearPartialNumberBuffer();
+
+ return true;
}
/** Continuously reads from the serial port until:
@@ -448,7 +474,7 @@ export class App {
// fatal error from the serial port which has caused us to close. In this case, handle
// the clean-up and state transition here.
if (this.keepReading === true) {
- if (this.settings.portConfiguration.config.reopenSerialPortIfUnexpectedlyClosed) {
+ if (this.settings.portConfiguration.reopenSerialPortIfUnexpectedlyClosed) {
this.setPortState(PortState.CLOSED_BUT_WILL_REOPEN);
} else {
this.setPortState(PortState.CLOSED);
@@ -484,7 +510,7 @@ export class App {
this.numBytesReceived += rxData.length;
}
- async closePort(goToReopenState = false) {
+ async closePort({ goToReopenState = false, silenceSnackbar = false } = {}) {
if (this.lastSelectedPortType === PortType.REAL) {
this.keepReading = false;
// Force reader.read() to resolve immediately and subsequently
@@ -503,14 +529,14 @@ export class App {
// this.setPortState(PortState.CLOSED);
this.portState = PortState.CLOSED;
}
- this.snackbar.sendToSnackbar('Serial port closed.', 'success');
+ if (!silenceSnackbar) {
+ this.snackbar.sendToSnackbar('Serial port closed.', 'success');
+ }
this.reader = null;
this.closedPromise = null;
- // this.appStorage.data.lastUsedSerialPort.portState = PortState.CLOSED;
- // this.appStorage.saveData();
- const lastUsedSerialPort: LastUsedSerialPort = this.appStorage.getData('lastUsedSerialPort');
+ const lastUsedSerialPort = this.profileManager.currentAppConfig.lastUsedSerialPort;
lastUsedSerialPort.portState = PortState.CLOSED;
- this.appStorage.saveData('lastUsedSerialPort', lastUsedSerialPort);
+ this.profileManager.saveAppConfig();
} else if (this.lastSelectedPortType === PortType.FAKE) {
this.fakePortController.closePort();
} else {
@@ -552,7 +578,7 @@ export class App {
let text = await navigator.clipboard.readText();
// Convert CRLF to LF if setting is enabled
- if (this.settings.generalSettings.config.whenPastingOnWindowsReplaceCRLFWithLF && isRunningOnWindows()) {
+ if (this.settings.generalSettings.whenPastingOnWindowsReplaceCRLFWithLF && isRunningOnWindows()) {
text = text.replace(/\r\n/g, '\n');
}
@@ -638,9 +664,7 @@ export class App {
* @param terminalSelectionWasIn The terminal that the selection was wholly contained within.
* @returns Text extracted from the terminal rows, suitable for copying to the clipboard.
*/
- private extractClipboardTextFromTerminal(
- selectionInfo: SelectionInfo,
- terminalSelectionWasIn: SingleTerminal): string {
+ private extractClipboardTextFromTerminal(selectionInfo: SelectionInfo, terminalSelectionWasIn: SingleTerminal): string {
// Extract number from end of the row ID
// row ID is in form -row-
const firstRowIdNumOnly = parseInt(selectionInfo.firstRowId.split('-').slice(-1)[0]);
@@ -661,7 +685,7 @@ export class App {
// a text editor and it won't have additional new lines added just because the text wrapped in
// the terminal. New lines will only be added if the terminal row was created because of
// a new line character or an ANSI escape sequence (e.g. cursor down).
- if (i !== firstRowIndex && (terminalRow.wasCreatedDueToWrapping === false || !this.settings.generalSettings.config.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping)) {
+ if (i !== firstRowIndex && (terminalRow.wasCreatedDueToWrapping === false || !this.settings.generalSettings.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping)) {
textToCopy += '\n';
}
@@ -714,7 +738,7 @@ export class App {
// use keyCode, but this method is deprecated!
const bytesToWrite: number[] = [];
// List of allowed symbols, includes space char also
- const symbols = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/? ";
+ const symbols = '`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/? ';
// List of all alphanumeric chars
const alphabeticChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqurstuvwxyz';
@@ -727,28 +751,11 @@ export class App {
// Ctrl-Shift-B: Send break signal
//===========================================================
else if (event.ctrlKey && event.shiftKey && event.key === 'B') {
- // TODO: Get types for setSignals() and remove ts-ignore
- try {
- // @ts-ignore
- await this.port.setSignals({ break: true });
- // 200ms seems like a standard break time
- await new Promise(resolve => setTimeout(resolve, 200));
- // @ts-ignore
- await this.port.setSignals({ break: false });
- // Emit message to user
- this.snackbar.sendToSnackbar('Break signal sent.', 'success');
- }
- // As per https://wicg.github.io/serial/#dom-serialport-setsignals
- // If the operating system fails to change the state of any of these signals for any reason, queue a
- // global task on the relevant global object of this using the serial port task source to reject promise with a "NetworkError" DOMException.
- catch (error) {
- this.snackbar.sendToSnackbar(`Error sending break signal. error: ${error}.`, 'error');
- }
- }
- else if (event.ctrlKey) {
+ await this.sendBreakSignal();
+ } else if (event.ctrlKey) {
// Most presses with the Ctrl key held down should do nothing. One exception is
// if sending 0x01-0x1A when Ctrl-A through Ctrl-Z is pressed is enabled
- if (this.settings.txSettings.config.send0x01Thru0x1AWhenCtrlAThruZPressed && event.key.length === 1 && alphabeticChars.includes(event.key)) {
+ if (this.settings.txSettings.send0x01Thru0x1AWhenCtrlAThruZPressed && event.key.length === 1 && alphabeticChars.includes(event.key)) {
// Ctrl-A through Ctrl-Z is has been pressed
// Send 0x01 through 0x1A, which is easily done by getting the char, converting to
// uppercase if lowercase and then subtracting 64
@@ -758,23 +765,23 @@ export class App {
return;
}
} else if (event.altKey) {
- if (this.settings.txSettings.config.sendEscCharWhenAltKeyPressed && event.key.length === 1 && alphabeticChars.includes(event.key)) {
+ if (this.settings.txSettings.sendEscCharWhenAltKeyPressed && event.key.length === 1 && alphabeticChars.includes(event.key)) {
// Alt-A through Alt-Z is has been pressed
// Send ESC char (0x1B) followed by the char
- bytesToWrite.push(0x1B);
+ bytesToWrite.push(0x1b);
bytesToWrite.push(event.key.charCodeAt(0));
} else {
// Alt key was pressed with another key, but we don't want to do anything with it
return;
}
} else if (event.key === 'Enter') {
- if (this.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_LF) {
- bytesToWrite.push(0x0A);
- } else if (this.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CR) {
- bytesToWrite.push(0x0D);
- } else if (this.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CRLF) {
- bytesToWrite.push(0x0D);
- bytesToWrite.push(0x0A);
+ if (this.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_LF) {
+ bytesToWrite.push(0x0a);
+ } else if (this.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CR) {
+ bytesToWrite.push(0x0d);
+ } else if (this.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CRLF) {
+ bytesToWrite.push(0x0d);
+ bytesToWrite.push(0x0a);
} else {
throw Error('Unsupported enter key press behavior!');
}
@@ -791,21 +798,21 @@ export class App {
//===========================================================
else if (event.key === 'Backspace') {
// Work out whether to send BS (0x08) or DEL (0x7F) based on settings
- if (this.settings.txSettings.config.backspaceKeyPressBehavior === BackspaceKeyPressBehavior.SEND_BACKSPACE) {
+ if (this.settings.txSettings.backspaceKeyPressBehavior === BackspaceKeyPressBehavior.SEND_BACKSPACE) {
bytesToWrite.push(0x08);
- } else if (this.settings.txSettings.config.backspaceKeyPressBehavior === BackspaceKeyPressBehavior.SEND_DELETE) {
- bytesToWrite.push(0x7F);
+ } else if (this.settings.txSettings.backspaceKeyPressBehavior === BackspaceKeyPressBehavior.SEND_DELETE) {
+ bytesToWrite.push(0x7f);
} else {
throw Error('Unsupported backspace key press behavior!');
}
} else if (event.key === 'Delete') {
// Delete also has the option of sending [ESC][3~
- if (this.settings.txSettings.config.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_BACKSPACE) {
+ if (this.settings.txSettings.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_BACKSPACE) {
bytesToWrite.push(0x08);
- } else if (this.settings.txSettings.config.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_DELETE) {
- bytesToWrite.push(0x7F);
- } else if (this.settings.txSettings.config.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_VT_SEQUENCE) {
- bytesToWrite.push(0x1B, '['.charCodeAt(0), '3'.charCodeAt(0), '~'.charCodeAt(0));
+ } else if (this.settings.txSettings.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_DELETE) {
+ bytesToWrite.push(0x7f);
+ } else if (this.settings.txSettings.deleteKeyPressBehavior === DeleteKeyPressBehavior.SEND_VT_SEQUENCE) {
+ bytesToWrite.push(0x1b, '['.charCodeAt(0), '3'.charCodeAt(0), '~'.charCodeAt(0));
} else {
throw Error('Unsupported delete key press behavior!');
}
@@ -836,6 +843,28 @@ export class App {
await this.writeBytesToSerialPort(Uint8Array.from(bytesToWrite));
};
+ /**
+ * Sends a break signal to the serial port for 200ms. Port must be open otherwise an error will be shown.
+ */
+ async sendBreakSignal() {
+ // TODO: Get types for setSignals() and remove ts-ignore
+ try {
+ // @ts-ignore
+ await this.port.setSignals({ break: true });
+ // 200ms seems like a standard break time
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ // @ts-ignore
+ await this.port.setSignals({ break: false });
+ // Emit message to user
+ this.snackbar.sendToSnackbar('Break signal sent.', 'success');
+ } catch (error) {
+ // As per https://wicg.github.io/serial/#dom-serialport-setsignals
+ // If the operating system fails to change the state of any of these signals for any reason, queue a
+ // global task on the relevant global object of this using the serial port task source to reject promise with a "NetworkError" DOMException.
+ this.snackbar.sendToSnackbar(`Error sending break signal. error: ${error}.`, 'error');
+ }
+ }
+
/**
* Writes bytes to the serial port. Also:
* - Sends the data to the TX terminal view
@@ -854,7 +883,7 @@ export class App {
this.terminals.txTerminal.parseData(bytesToWrite);
// Check if local TX echo is enabled, and if so, send the data to
// the combined single terminal.
- if (this.settings.rxSettings.config.localTxEcho) {
+ if (this.settings.rxSettings.localTxEcho) {
this.terminals.txRxTerminal.parseData(bytesToWrite);
}
@@ -890,8 +919,8 @@ export class App {
onClick={() => {
updateSw(true);
}}
- color='info'
- variant='text'
+ color="info"
+ variant="text"
sx={{
color: 'rgb(33, 150, 243)',
backgroundColor: 'white',
@@ -903,8 +932,8 @@ export class App {
onClick={() => {
closeSnackbar(snackbarId);
}}
- color='info'
- variant='text'
+ color="info"
+ variant="text"
sx={{
color: 'white',
// backgroundColor: 'white'
diff --git a/src/model/FakePorts/FakePortsController.tsx b/src/model/FakePorts/FakePortsController.tsx
index 0746ad2c..1a8806d5 100644
--- a/src/model/FakePorts/FakePortsController.tsx
+++ b/src/model/FakePorts/FakePortsController.tsx
@@ -430,7 +430,7 @@ export default class FakePortsController {
'graph data, x=2, y=10, 0.5points/s',
'Sends data that can be graphed.',
() => {
- app.settings.rxSettings.config.ansiEscapeCodeParsingEnabled = false;
+ app.settings.rxSettings.ansiEscapeCodeParsingEnabled = false;
let testCharIdx = 0;
const intervalId = setInterval(() => {
const rxData = new TextEncoder().encode('x=2,y=10\n');
@@ -610,8 +610,8 @@ export default class FakePortsController {
app.settings.rxSettings.setDataType(DataType.NUMBER);
app.settings.rxSettings.setNumberType(NumberType.INT16);
app.settings.rxSettings.setInsertNewLineOnValue(false);
- app.settings.rxSettings.config.numberSeparator.setDispValue(' ');
- app.settings.rxSettings.config.numberSeparator.apply();
+ app.settings.rxSettings.numberSeparator.setDispValue(' ');
+ app.settings.rxSettings.numberSeparator.apply();
app.settings.rxSettings.setPadValues(true);
app.settings.rxSettings.setPaddingCharacter(PaddingCharacter.ZERO);
@@ -648,8 +648,8 @@ export default class FakePortsController {
app.settings.rxSettings.setDataType(DataType.NUMBER);
app.settings.rxSettings.setNumberType(NumberType.FLOAT32);
app.settings.rxSettings.setInsertNewLineOnValue(false);
- app.settings.rxSettings.config.numberSeparator.setDispValue(' ');
- app.settings.rxSettings.config.numberSeparator.apply();
+ app.settings.rxSettings.numberSeparator.setDispValue(' ');
+ app.settings.rxSettings.numberSeparator.apply();
app.settings.rxSettings.setPadValues(true);
app.settings.rxSettings.setPaddingCharacter(PaddingCharacter.ZERO);
@@ -687,8 +687,8 @@ export default class FakePortsController {
app.settings.rxSettings.setDataType(DataType.NUMBER);
app.settings.rxSettings.setNumberType(NumberType.FLOAT32);
app.settings.rxSettings.setInsertNewLineOnValue(false);
- app.settings.rxSettings.config.numberSeparator.setDispValue(' ');
- app.settings.rxSettings.config.numberSeparator.apply();
+ app.settings.rxSettings.numberSeparator.setDispValue(' ');
+ app.settings.rxSettings.numberSeparator.apply();
app.settings.rxSettings.setPadValues(true);
app.settings.rxSettings.setPaddingCharacter(PaddingCharacter.ZERO);
@@ -731,7 +731,7 @@ export default class FakePortsController {
this.app.snackbar.sendToSnackbar('Fake serial port opened.', 'success');
// Go to terminal view
- if (this.app.settings.portConfiguration.config.connectToSerialPortAsSoonAsItIsSelected) {
+ if (this.app.settings.portConfiguration.connectToSerialPortAsSoonAsItIsSelected) {
this.app.setShownMainPane(MainPanes.TERMINAL);
}
}
diff --git a/src/model/ProfileManager/ProfileManager.spec.ts b/src/model/ProfileManager/ProfileManager.spec.ts
new file mode 100644
index 00000000..abc5931a
--- /dev/null
+++ b/src/model/ProfileManager/ProfileManager.spec.ts
@@ -0,0 +1,27 @@
+import { expect, test, describe, beforeEach } from "vitest";
+
+import { ProfileManager } from "./ProfileManager";
+import { App } from "../App";
+
+beforeEach(() => {
+ // Clear local storage, because otherwise jsdom persists storage
+ // between tests
+ window.localStorage.clear();
+});
+
+describe("profile manager tests", () => {
+ test("default profile should be created", () => {
+ const app = new App();
+ const profileManager = new ProfileManager(app);
+ expect(profileManager.profiles.length).toEqual(1);
+ expect(profileManager.profiles[0].name).toEqual("Default profile");
+ });
+
+ test("new profile can be created", () => {
+ const app = new App();
+ const profileManager = new ProfileManager(app);
+ profileManager.newProfile();
+ expect(profileManager.profiles.length).toEqual(2);
+ expect(profileManager.profiles[1].name).toEqual("New profile 1");
+ });
+});
diff --git a/src/model/ProfileManager/ProfileManager.ts b/src/model/ProfileManager/ProfileManager.ts
new file mode 100644
index 00000000..bf49526e
--- /dev/null
+++ b/src/model/ProfileManager/ProfileManager.ts
@@ -0,0 +1,278 @@
+import { makeAutoObservable } from "mobx";
+
+import { DisplaySettingsConfig } from "../Settings/DisplaySettings/DisplaySettings";
+import { GeneralSettingsConfig } from "../Settings/GeneralSettings/GeneralSettings";
+import { PortConfigurationConfig, PortState } from "../Settings/PortConfigurationSettings/PortConfigurationSettings";
+import { RxSettingsConfig } from "../Settings/RxSettings/RxSettings";
+import { TxSettingsConfig } from "../Settings/TxSettings/TxSettings";
+import { MacroControllerConfig } from "../Terminals/RightDrawer/Macros/MacroController";
+import { App } from "../App";
+import { VariantType } from "notistack";
+
+export class LastUsedSerialPort {
+ serialPortInfo: Partial = {};
+ portState: PortState = PortState.CLOSED;
+}
+
+/**
+ * Everything in this class must be POD (plain old data) and serializable to JSON.
+ */
+export class RootConfig {
+ version = 1;
+
+ terminal = {
+ macroController: new MacroControllerConfig(),
+ };
+
+ lastUsedSerialPort: LastUsedSerialPort = new LastUsedSerialPort();
+
+ settings = {
+ portSettings: new PortConfigurationConfig(),
+ txSettings: new TxSettingsConfig(),
+ rxSettings: new RxSettingsConfig(),
+ displaySettings: new DisplaySettingsConfig(),
+ generalSettings: new GeneralSettingsConfig(),
+ };
+}
+
+/**
+ * This class represents a serial port profile. It is used to store use-specific
+ * settings for the application (e.g. all the settings to talk to a particular
+ * embedded device). The class is serializable to JSON.
+ */
+export class Profile {
+ name: string = "";
+ rootConfig: RootConfig = new RootConfig();
+
+ constructor(name: string) {
+ this.name = name;
+ makeAutoObservable(this);
+ }
+}
+
+const PROFILES_STORAGE_KEY = "profiles";
+
+export class ProfileManager {
+ app: App;
+
+ profiles: Profile[] = [];
+
+ /**
+ * Represents the current application configuration. This is saved regularly so that when the app reloads,
+ * it can restore the last known configuration.
+ */
+ currentAppConfig: RootConfig = new RootConfig();
+
+ _profileChangeCallbacks: (() => void)[] = [];
+
+ /**
+ * Represents the name of the last profile that was applied to the app. Used for displaying
+ * in various places such as the toolbar.
+ */
+ lastAppliedProfileName: string = "No profile";
+
+ constructor(app: App) {
+ this.app = app;
+
+ addEventListener("storage", (event) => {
+ console.log("Caught storage event. event.key: ", event.key, " event.newValue: ", event.newValue);
+
+ if (event.key === PROFILES_STORAGE_KEY) {
+ console.log("Profiles changed. Reloading...");
+ this._loadProfilesFromStorage();
+ }
+ });
+
+ // Read in profiles
+ this._loadProfilesFromStorage();
+
+ // Load current app config
+ const currentAppConfigJson = window.localStorage.getItem("currentAppConfig");
+ let currentAppConfig: RootConfig;
+ if (currentAppConfigJson === null) {
+ // No config key found in users store, create one!
+ currentAppConfig = new RootConfig();
+ // Save just-created config back to store.
+ window.localStorage.setItem("currentAppConfig", JSON.stringify(this.currentAppConfig));
+ } else {
+ currentAppConfig = JSON.parse(currentAppConfigJson);
+ console.log("Loading current app config from local storage. currentAppConfig: ", currentAppConfig);
+ }
+ this.currentAppConfig = currentAppConfig;
+
+ makeAutoObservable(this);
+ }
+
+ setActiveProfile = (profile: Profile) => {
+ // this.activeProfile = profile;
+ // Need to tell the rest of the app to update
+ this._profileChangeCallbacks.forEach((callback) => {
+ callback();
+ });
+ };
+
+ registerOnProfileLoad = (callback: () => void) => {
+ this._profileChangeCallbacks.push(callback);
+ };
+
+ _loadProfilesFromStorage = () => {
+ const profilesJson = window.localStorage.getItem(PROFILES_STORAGE_KEY);
+ // let profileManagerData: ProfileManagerData;
+ let profiles: Profile[];
+ if (profilesJson === null) {
+ // No config key found in users store, create one!
+ profiles = [];
+ profiles.push(new Profile("Default profile"));
+ console.log("No profiles found in local storage. Creating default profile.");
+ // Save just-created config back to store.
+ window.localStorage.setItem(PROFILES_STORAGE_KEY, JSON.stringify(profiles));
+ } else {
+ profiles = JSON.parse(profilesJson);
+ }
+ // Only support the 1 active config for now
+ // this.activeProfile = this.profiles[0];
+
+ // Load data into class
+ this.profiles = profiles;
+ };
+
+ saveProfiles = () => {
+ console.log("Saving profiles...");
+ window.localStorage.setItem(PROFILES_STORAGE_KEY, JSON.stringify(this.profiles));
+ };
+
+ /**
+ * Save all profiles to local storage.
+ */
+ saveAppConfig = () => {
+ console.log("Saving app config...");
+ window.localStorage.setItem("currentAppConfig", JSON.stringify(this.currentAppConfig));
+ };
+
+ /**
+ * Create a new profile (with default config) and add it to the list of profiles.
+ */
+ newProfile = () => {
+ // Calculate name for new profile, in the form "New profile X" where X is the next number
+ let nextProfileNum = 1;
+ const newProfileName = "New profile";
+ let newProfileNameToCheck = newProfileName + " " + nextProfileNum;
+ while (this.profiles.find((profile) => profile.name === newProfileNameToCheck) !== undefined) {
+ nextProfileNum++;
+ newProfileNameToCheck = newProfileName + " " + nextProfileNum;
+ }
+ // At this point newProfileNameToCheck is the name we want
+ const newProfile = new Profile(newProfileNameToCheck);
+ this.profiles.push(newProfile);
+ this.saveProfiles();
+
+ // Automatically save the current app state to the newly created profile
+ // and silence the snackbar message
+ this.saveCurrentAppConfigToProfile(this.profiles.length - 1, true);
+ };
+
+ /**
+ * Delete the profile at the provided index and save the profiles to local storage.
+ * @param profileIdx The index of the profile to delete.
+ */
+ deleteProfile = (profileIdx: number) => {
+ this.profiles.splice(profileIdx, 1);
+ this.saveProfiles();
+ };
+
+ /**
+ * Apply the profile at the provided index to the current app config (i.e. update the app
+ * to reflect the profile).
+ * @param profileIdx The index of the profile to apply to the app.
+ */
+ applyProfileToApp = async (profileIdx: number) => {
+ const profile = this.profiles[profileIdx];
+
+ // Check the last connected serial port of the profile and compare with
+ // currently connected one
+ const profileSerialPortInfoJson = JSON.stringify(profile.rootConfig.lastUsedSerialPort.serialPortInfo);
+ const currentSerialPortInfoJson = JSON.stringify(this.currentAppConfig.lastUsedSerialPort.serialPortInfo);
+
+ let weNeedToConnect = false;
+ let matchedAvailablePorts: SerialPort[] = [];
+ let snackbarMessage = `Profile "${profile.name}" loaded.`;
+ let snackbarVariant: VariantType = 'success';
+ if (profileSerialPortInfoJson == "{}") {
+ weNeedToConnect = false;
+ } else if (profileSerialPortInfoJson === currentSerialPortInfoJson) {
+ // Same serial port, no need to disconnect and connect
+ weNeedToConnect = false;
+ } else {
+ // They are both different and the profile one is non-empty. Check to see if the profile ports is available
+ console.log("Port infos are both different and non-empty. Checking if ports are available...");
+ const availablePorts = await navigator.serial.getPorts();
+ matchedAvailablePorts = availablePorts.filter((port) => JSON.stringify(port.getInfo()) === profileSerialPortInfoJson);
+
+ if (matchedAvailablePorts.length === 0) {
+ // The profile port is not available
+ weNeedToConnect = false;
+ snackbarMessage += '\nNo available port matches the profile port info. No connecting to any.';
+ snackbarVariant = 'warning';
+ } else if (matchedAvailablePorts.length === 1) {
+ // The profile port is available
+ weNeedToConnect = true;
+ } else {
+ // There are multiple ports that match the profile port, to ambiguous, do
+ // not connect to any
+ weNeedToConnect = false;
+ snackbarMessage += '\nMultiple available ports info match the profile port info. Not connecting to any.';
+ snackbarVariant = 'warning';
+ }
+ }
+
+ // Only disconnect if we have found a valid port to connect to
+ if (weNeedToConnect) {
+ if (this.app.portState === PortState.OPENED) {
+ console.log('Closing port...');
+ await this.app.closePort({silenceSnackbar: true});
+ } else if (this.app.portState === PortState.CLOSED_BUT_WILL_REOPEN) {
+ this.app.stopWaitingToReopenPort();
+ }
+ }
+ console.log('Port closed.');
+ // Update the current app config from the provided profile,
+ // and then save this new app config
+ this.currentAppConfig = JSON.parse(JSON.stringify(profile.rootConfig));
+ this.saveAppConfig();
+
+ // Need to tell the rest of the app to update
+ this._profileChangeCallbacks.forEach((callback) => {
+ callback();
+ });
+
+ this.lastAppliedProfileName = profile.name;
+
+ // Now connect to the port if we need to
+ if (weNeedToConnect) {
+ console.log('Setting selected port...', matchedAvailablePorts[0]);
+ this.app.setSelectedPort(matchedAvailablePorts[0]);
+ console.log('Opening port...');
+ await this.app.openPort({silenceSnackbar: true});
+ snackbarMessage += '\nConnected to port with info: "' + profileSerialPortInfoJson + '".';
+ }
+
+ // Post message to snackbar
+ this.app.snackbar.sendToSnackbar(snackbarMessage, snackbarVariant);
+ };
+
+ /**
+ * Save the current app config to the provided profile and the save the profiles to local storage.
+ * @param profileIdx The index of the profile to save the current app config to.
+ */
+ saveCurrentAppConfigToProfile = (profileIdx: number, noSnackbar=false) => {
+ console.log("Saving current app config to profile...");
+ const profile = this.profiles[profileIdx];
+ profile.rootConfig = JSON.parse(JSON.stringify(this.currentAppConfig));
+ this.saveProfiles();
+
+ // Post message to snackbar
+ if (!noSnackbar) {
+ this.app.snackbar.sendToSnackbar('Profile "' + profile.name + '" saved.', "success");
+ }
+ };
+}
diff --git a/src/model/Settings/DisplaySettings/DisplaySettings.ts b/src/model/Settings/DisplaySettings/DisplaySettings.ts
index b1685bf3..e9ff9606 100644
--- a/src/model/Settings/DisplaySettings/DisplaySettings.ts
+++ b/src/model/Settings/DisplaySettings/DisplaySettings.ts
@@ -1,8 +1,8 @@
-import { makeAutoObservable } from 'mobx';
-import { boolean, z } from 'zod';
+import { makeAutoObservable } from "mobx";
+import { z } from "zod";
-import { ApplyableNumberField } from 'src/view/Components/ApplyableTextField';
-import AppStorage from 'src/model/Storage/AppStorage';
+import { ApplyableNumberField } from "src/view/Components/ApplyableTextField";
+import { ProfileManager } from "src/model/ProfileManager/ProfileManager";
/** Enumerates the different possible ways the TX and RX data
* can be displayed. One of these may be active at any one time.
@@ -16,88 +16,96 @@ export enum DataViewConfiguration {
export const dataViewConfigEnumToDisplayName: {
[key: string]: string;
} = {
- [DataViewConfiguration.SINGLE_TERMINAL]: 'Single terminal',
- [DataViewConfiguration.SEPARATE_TX_RX_TERMINALS]: 'Separate TX/RX terminals',
+ [DataViewConfiguration.SINGLE_TERMINAL]: "Single terminal",
+ [DataViewConfiguration.SEPARATE_TX_RX_TERMINALS]: "Separate TX/RX terminals",
};
+export class DisplaySettingsConfig {
+ version = 1;
+ charSizePx = 14;
+ verticalRowPaddingPx = 5;
+ terminalWidthChars = 120;
+ scrollbackBufferSizeRows = 2000;
+ dataViewConfiguration = DataViewConfiguration.SINGLE_TERMINAL;
+}
+
export default class DisplaySettings {
- appStorage: AppStorage;
+ profileManager: ProfileManager;
// 14px is a good default size for the terminal text
- charSizePx = new ApplyableNumberField('14', z.coerce.number().int().min(1));
+ charSizePx = new ApplyableNumberField("14", z.coerce.number().int().min(1));
/**
* The amount of vertical padding to apply (in pixels) to apply above and below the characters in each row. The char size plus this row padding determines the total row height. Decrease for a denser display of data.
*/
- verticalRowPaddingPx = new ApplyableNumberField('5', z.coerce.number().int().min(1));
+ verticalRowPaddingPx = new ApplyableNumberField("5", z.coerce.number().int().min(1));
- terminalWidthChars = new ApplyableNumberField('120', z.coerce.number().int().min(1));
+ terminalWidthChars = new ApplyableNumberField("120", z.coerce.number().int().min(1));
- scrollbackBufferSizeRows = new ApplyableNumberField('2000', z.coerce.number().int().min(1));
+ scrollbackBufferSizeRows = new ApplyableNumberField("2000", z.coerce.number().int().min(1));
dataViewConfiguration = DataViewConfiguration.SINGLE_TERMINAL;
-
- constructor(appStorage: AppStorage) {
- this.appStorage = appStorage;
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
this.charSizePx.setOnApplyChanged(() => {
- this.saveConfig();
+ this._saveConfig();
});
this.verticalRowPaddingPx.setOnApplyChanged(() => {
- this.saveConfig();
+ this._saveConfig();
});
this.terminalWidthChars.setOnApplyChanged(() => {
- this.saveConfig();
+ this._saveConfig();
});
this.scrollbackBufferSizeRows.setOnApplyChanged(() => {
- this.saveConfig();
+ this._saveConfig();
+ });
+ this._loadConfig();
+ this.profileManager.registerOnProfileLoad(() => {
+ this._loadConfig();
});
makeAutoObservable(this);
- this.loadConfig();
}
setDataViewConfiguration = (value: DataViewConfiguration) => {
this.dataViewConfiguration = value;
- this.saveConfig();
- }
-
- saveConfig = () => {
- // TODO: Update this to match the style used in RX settings (and others)
- const config = {
- charSizePx: this.charSizePx.dispValue,
- verticalRowPadding: this.verticalRowPaddingPx.dispValue,
- terminalWidthChars: this.terminalWidthChars.dispValue,
- scrollbackBufferSizeRows: this.scrollbackBufferSizeRows.dispValue,
- dataViewConfiguration: this.dataViewConfiguration,
- };
-
- this.appStorage.saveConfig(['settings', 'display'], config);
- }
-
- loadConfig = () => {
- const config = this.appStorage.getConfig(['settings', 'display']);
- if (config === null) {
- return;
- }
- if (config.charSizePx !== undefined) {
- this.charSizePx.dispValue = config.charSizePx;
- this.charSizePx.apply();
- }
- if (config.verticalRowPadding !== undefined) {
- this.verticalRowPaddingPx.dispValue = config.verticalRowPadding;
- this.verticalRowPaddingPx.apply();
- }
- if (config.terminalWidthChars !== undefined) {
- this.terminalWidthChars.dispValue = config.terminalWidthChars;
- this.terminalWidthChars.apply();
+ this._saveConfig();
+ };
+
+ _saveConfig = () => {
+ let config = this.profileManager.currentAppConfig.settings.displaySettings;
+
+ config.charSizePx = this.charSizePx.appliedValue;
+ config.verticalRowPaddingPx = this.verticalRowPaddingPx.appliedValue;
+ config.terminalWidthChars = this.terminalWidthChars.appliedValue;
+ config.scrollbackBufferSizeRows = this.scrollbackBufferSizeRows.appliedValue;
+ config.dataViewConfiguration = this.dataViewConfiguration;
+
+ this.profileManager.saveAppConfig();
+ };
+
+ _loadConfig = () => {
+ let configToLoad = this.profileManager.currentAppConfig.settings.displaySettings;
+ //===============================================
+ // UPGRADE PATH
+ //===============================================
+ const latestVersion = new DisplaySettingsConfig().version;
+ if (configToLoad.version === latestVersion) {
+ } else {
+ console.log(`Out-of-date config version ${configToLoad.version} found.` + ` Updating to version ${latestVersion}.`);
+ this._saveConfig();
+ configToLoad = this.profileManager.currentAppConfig.settings.displaySettings;
}
- if (config.scrollbackBufferSizeRows !== undefined) {
- this.scrollbackBufferSizeRows.dispValue = config.scrollbackBufferSizeRows;
- this.scrollbackBufferSizeRows.apply();
- }
- if (config.dataViewConfiguration !== undefined) {
- this.dataViewConfiguration = config.dataViewConfiguration;
- }
- }
+
+ this.charSizePx.setDispValue(configToLoad.charSizePx.toString());
+ this.charSizePx.apply();
+ this.verticalRowPaddingPx.setDispValue(configToLoad.verticalRowPaddingPx.toString());
+ this.verticalRowPaddingPx.apply();
+ this.terminalWidthChars.setDispValue(configToLoad.terminalWidthChars.toString());
+ this.terminalWidthChars.apply();
+ this.scrollbackBufferSizeRows.setDispValue(configToLoad.scrollbackBufferSizeRows.toString());
+ this.scrollbackBufferSizeRows.apply();
+ this.dataViewConfiguration = configToLoad.dataViewConfiguration;
+ };
}
diff --git a/src/model/Settings/GeneralSettings/GeneralSettings.ts b/src/model/Settings/GeneralSettings/GeneralSettings.ts
index 369e5652..87149099 100644
--- a/src/model/Settings/GeneralSettings/GeneralSettings.ts
+++ b/src/model/Settings/GeneralSettings/GeneralSettings.ts
@@ -1,11 +1,7 @@
import { makeAutoObservable } from "mobx";
+import { ProfileManager } from "src/model/ProfileManager/ProfileManager";
-import AppStorage from "src/model/Storage/AppStorage";
-import { createSerializableObjectFromConfig, updateConfigFromSerializable } from "src/model/Util/SettingsLoader";
-
-const CONFIG_KEY = ['settings', 'general-settings'];
-
-class Config {
+export class GeneralSettingsConfig {
/**
* Increment this version number if you need to update this data in this class.
* This will cause the app to ignore whatever is in local storage and use the defaults,
@@ -15,60 +11,57 @@ class Config {
whenPastingOnWindowsReplaceCRLFWithLF = true;
whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = true;
-
- constructor() {
- makeAutoObservable(this); // Make sure this is at the end of the constructor
- }
}
export default class RxSettings {
- appStorage: AppStorage;
+ profileManager: ProfileManager;
- config = new Config();
+ whenPastingOnWindowsReplaceCRLFWithLF = true;
+ whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = true;
- constructor(appStorage: AppStorage) {
- this.appStorage = appStorage;
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
this._loadConfig();
+ this.profileManager.registerOnProfileLoad(() => {
+ this._loadConfig();
+ });
makeAutoObservable(this); // Make sure this is at the end of the constructor
}
setWhenPastingOnWindowsReplaceCRLFWithLF = (value: boolean) => {
- this.config.whenPastingOnWindowsReplaceCRLFWithLF = value;
+ this.whenPastingOnWindowsReplaceCRLFWithLF = value;
this._saveConfig();
};
setWhenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = (value: boolean) => {
- this.config.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = value;
+ this.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = value;
this._saveConfig();
};
- _loadConfig = () => {
- let deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
+ _saveConfig = () => {
+ let config = this.profileManager.currentAppConfig.settings.generalSettings;
+ config.whenPastingOnWindowsReplaceCRLFWithLF = this.whenPastingOnWindowsReplaceCRLFWithLF;
+ config.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = this.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping;
+
+ this.profileManager.saveAppConfig();
+ };
+
+ _loadConfig = () => {
+ let configToLoad = this.profileManager.currentAppConfig.settings.generalSettings;
//===============================================
// UPGRADE PATH
//===============================================
- if (deserializedConfig === null) {
- // No data exists, create
- console.log(`No config found in local storage for key ${CONFIG_KEY}. Creating...`);
- this._saveConfig();
- return;
- } else if (deserializedConfig.version === this.config.version) {
- console.log(`Up-to-date config found for key ${CONFIG_KEY}.`);
+ const latestVersion = new GeneralSettingsConfig().version;
+ if (configToLoad.version === latestVersion) {
+ // Do nothing
} else {
- console.error(`Out-of-date config version ${deserializedConfig.version} found for key ${CONFIG_KEY}.` +
- ` Updating to version ${this.config.version}.`);
+ console.log(`Out-of-date config version ${configToLoad.version} found.` + ` Updating to version ${latestVersion}.`);
this._saveConfig();
- deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
+ configToLoad = this.profileManager.currentAppConfig.settings.generalSettings;
}
- // At this point we are confident that the deserialized config matches what
- // this classes config object wants, so we can go ahead and update.
- updateConfigFromSerializable(deserializedConfig, this.config);
- };
-
- _saveConfig = () => {
- const serializableConfig = createSerializableObjectFromConfig(this.config);
- this.appStorage.saveConfig(CONFIG_KEY, serializableConfig);
+ this.whenPastingOnWindowsReplaceCRLFWithLF = configToLoad.whenPastingOnWindowsReplaceCRLFWithLF;
+ this.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping = configToLoad.whenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping;
};
}
diff --git a/src/model/Settings/PortConfigurationSettings/PortConfigurationSettings.ts b/src/model/Settings/PortConfigurationSettings/PortConfigurationSettings.ts
index cde6276f..861c88a4 100644
--- a/src/model/Settings/PortConfigurationSettings/PortConfigurationSettings.ts
+++ b/src/model/Settings/PortConfigurationSettings/PortConfigurationSettings.ts
@@ -1,10 +1,8 @@
import { makeAutoObservable } from 'mobx';
-
-import { App } from 'src/model/App';
-import AppStorage from 'src/model/Storage/AppStorage';
-import { createSerializableObjectFromConfig, updateConfigFromSerializable } from 'src/model/Util/SettingsLoader';
import { z } from 'zod';
+import { ProfileManager } from 'src/model/ProfileManager/ProfileManager';
+
export enum PortState {
CLOSED,
CLOSED_BUT_WILL_REOPEN,
@@ -31,9 +29,12 @@ export type StopBits = 1 | 1.5 | 2;
export const STOP_BIT_OPTIONS: StopBits[] = [1, 2];
-const CONFIG_KEY = ['settings', 'port-configuration-settings'];
+export enum FlowControl {
+ NONE = 'none',
+ HARDWARE = 'hardware',
+};
-class Config {
+export class PortConfigurationConfig {
/**
* Increment this version number if you need to update this data in this class.
* This will cause the app to ignore whatever is in local storage and use the defaults,
@@ -49,24 +50,20 @@ class Config {
stopBits: StopBits = 1;
+ flowControl = FlowControl.NONE;
+
connectToSerialPortAsSoonAsItIsSelected = true;
resumeConnectionToLastSerialPortOnStartup = true;
reopenSerialPortIfUnexpectedlyClosed = true;
-
- constructor() {
- makeAutoObservable(this); // Make sure this is at the end of the constructor
- }
}
export default class PortConfiguration {
- appStorage: AppStorage;
-
- config = new Config();
+ profileManager: ProfileManager;
- baudRateInputValue = this.config.baudRate.toString();
+ baudRateInputValue: string;
/**
* Set min. baud rate to 1 and max. baud rate to 2,000,000. Most systems won't actually
@@ -76,14 +73,35 @@ export default class PortConfiguration {
baudRateValidation = z.coerce.number().int().min(1).max(2000000);
baudRateErrorMsg = '';
- constructor(appStorage: AppStorage) {
- this.appStorage = appStorage;
+ baudRate = 115200;
+
+ numDataBits = 8;
+
+ parity = Parity.NONE;
+
+ stopBits: StopBits = 1;
+
+ flowControl = FlowControl.NONE;
+
+ connectToSerialPortAsSoonAsItIsSelected = true;
+
+ resumeConnectionToLastSerialPortOnStartup = true;
+
+ reopenSerialPortIfUnexpectedlyClosed = true;
+
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
+ this.baudRateInputValue = this.baudRate.toString();
+ // this.config =
this._loadConfig();
+ this.profileManager.registerOnProfileLoad(() => {
+ this._loadConfig();
+ });
makeAutoObservable(this);
}
setBaudRate = (baudRate: number) => {
- this.config.baudRate = baudRate;
+ this.baudRate = baudRate;
this._saveConfig();
}
@@ -106,65 +124,82 @@ export default class PortConfiguration {
if (typeof numDataBits !== 'number') {
throw new Error("numDataBits must be a number");
}
- this.config.numDataBits = numDataBits;
+ this.numDataBits = numDataBits;
this._saveConfig();
}
setParity = (parity: Parity) => {
- this.config.parity = parity;
+ this.parity = parity;
this._saveConfig();
}
setStopBits = (stopBits: StopBits) => {
- this.config.stopBits = stopBits;
+ this.stopBits = stopBits;
+ this._saveConfig();
+ }
+
+ setFlowControl = (flowControl: FlowControl) => {
+ this.flowControl = flowControl;
this._saveConfig();
}
setConnectToSerialPortAsSoonAsItIsSelected = (value: boolean) => {
- this.config.connectToSerialPortAsSoonAsItIsSelected = value;
+ this.connectToSerialPortAsSoonAsItIsSelected = value;
this._saveConfig();
}
setResumeConnectionToLastSerialPortOnStartup = (value: boolean) => {
- this.config.resumeConnectionToLastSerialPortOnStartup = value;
+ this.resumeConnectionToLastSerialPortOnStartup = value;
this._saveConfig();
}
setReopenSerialPortIfUnexpectedlyClosed = (value: boolean) => {
- this.config.reopenSerialPortIfUnexpectedlyClosed = value;
+ this.reopenSerialPortIfUnexpectedlyClosed = value;
this._saveConfig();
}
_loadConfig = () => {
- let deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
-
+ let configToLoad = this.profileManager.currentAppConfig.settings.portSettings
//===============================================
// UPGRADE PATH
//===============================================
- if (deserializedConfig === null) {
- // No data exists, create
- console.log(`No config found in local storage for key ${CONFIG_KEY}. Creating...`);
- this._saveConfig();
- return;
- } else if (deserializedConfig.version === this.config.version) {
- console.log(`Up-to-date config found for key ${CONFIG_KEY}.`);
+ const latestVersion = new PortConfigurationConfig().version;
+ if (configToLoad.version === latestVersion) {
+ // Do nothing
} else {
- console.error(`Out-of-date config version ${deserializedConfig.version} found for key ${CONFIG_KEY}.` +
- ` Updating to version ${this.config.version}.`);
+ console.log(`Out-of-date config version ${configToLoad.version} found.` +
+ ` Updating to version ${latestVersion}.`);
this._saveConfig();
- deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
+ configToLoad = this.profileManager.currentAppConfig.settings.portSettings
}
// At this point we are confident that the deserialized config matches what
// this classes config object wants, so we can go ahead and update.
- updateConfigFromSerializable(deserializedConfig, this.config);
-
- this.setBaudRateInputValue(this.config.baudRate.toString());
+ this.baudRate = configToLoad.baudRate;
+ this.numDataBits = configToLoad.numDataBits;
+ this.parity = configToLoad.parity;
+ this.stopBits = configToLoad.stopBits;
+ this.flowControl = configToLoad.flowControl;
+ this.connectToSerialPortAsSoonAsItIsSelected = configToLoad.connectToSerialPortAsSoonAsItIsSelected;
+ this.resumeConnectionToLastSerialPortOnStartup = configToLoad.resumeConnectionToLastSerialPortOnStartup;
+ this.reopenSerialPortIfUnexpectedlyClosed = configToLoad.reopenSerialPortIfUnexpectedlyClosed;
+
+ this.setBaudRateInputValue(this.baudRate.toString());
};
_saveConfig = () => {
- const serializableConfig = createSerializableObjectFromConfig(this.config);
- this.appStorage.saveConfig(CONFIG_KEY, serializableConfig);
+ let config = this.profileManager.currentAppConfig.settings.portSettings;
+
+ config.baudRate = this.baudRate;
+ config.numDataBits = this.numDataBits;
+ config.parity = this.parity;
+ config.stopBits = this.stopBits;
+ config.flowControl = this.flowControl;
+ config.connectToSerialPortAsSoonAsItIsSelected = this.connectToSerialPortAsSoonAsItIsSelected;
+ config.resumeConnectionToLastSerialPortOnStartup = this.resumeConnectionToLastSerialPortOnStartup;
+ config.reopenSerialPortIfUnexpectedlyClosed = this.reopenSerialPortIfUnexpectedlyClosed;
+
+ this.profileManager.saveAppConfig();
};
/**
@@ -174,12 +209,16 @@ export default class PortConfiguration {
* @returns The short hand serial port config for displaying to the user.
*/
get shortSerialConfigName() {
+ return PortConfiguration.computeShortSerialConfigName(this.baudRate, this.numDataBits, this.parity, this.stopBits);
+ }
+
+ static computeShortSerialConfigName(baudRate: number, numDataBits: number, parity: Parity, stopBits: StopBits) {
let output = '';
- output += this.config.baudRate.toString();
+ output += baudRate.toString();
output += ' ';
- output += this.config.numDataBits.toString();
- output += this.config.parity[0]; // Take first letter of parity, e.g. (n)one, (e)ven, (o)dd
- output += this.config.stopBits.toString();
+ output += numDataBits.toString();
+ output += parity[0]; // Take first letter of parity, e.g. (n)one, (e)ven, (o)dd
+ output += stopBits.toString();
return output;
}
}
diff --git a/src/model/Settings/ProfileSettings/ProfileSettings.ts b/src/model/Settings/ProfileSettings/ProfileSettings.ts
new file mode 100644
index 00000000..308a7c3a
--- /dev/null
+++ b/src/model/Settings/ProfileSettings/ProfileSettings.ts
@@ -0,0 +1,103 @@
+import { GridRowSelectionModel } from '@mui/x-data-grid';
+import { makeAutoObservable } from 'mobx';
+import { z } from 'zod';
+
+import { ProfileManager } from 'src/model/ProfileManager/ProfileManager';
+
+export default class ProfilesSettings {
+ profileManager: ProfileManager;
+
+ profileNameText = '';
+ profileNameErrorMsg = '';
+
+ /**
+ * This should either be an array of 0 elements (no profile is selected) or an array of 1 element (the index of the selected profile).
+ */
+ selectedProfiles: GridRowSelectionModel = [];
+
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
+ makeAutoObservable(this);
+ }
+
+ setProfileName(name: string) {
+ this.profileNameText = name;
+
+ // If there are no selected profiles, don't show an error
+ if (this.selectedProfiles.length === 0) {
+ this.profileNameErrorMsg = '';
+ return;
+ }
+
+ // Validate the profile name
+ const schema = z.string().trim().min(1, { message: 'Must contain a least 1 non-whitespace character.' }).max(50, { message: 'Must be less or equal to 50 characters.' })
+ const validation = schema.safeParse(name);
+ if (!validation.success) {
+ this.profileNameErrorMsg = validation.error.errors[0].message;
+ return;
+ }
+
+ this.profileNameErrorMsg = '';
+
+ if (this.selectedProfiles.length === 0) {
+ // Nothing to do if no profile is selected
+ return;
+ }
+ const selectedProfile = this.profileManager.profiles[this.selectedProfiles[0] as number];
+ if (selectedProfile.name === this.profileNameText) {
+ // The profile name hasn't changed so nothing to do
+ return;
+ }
+ // If we get here profile name has changed
+ selectedProfile.name = this.profileNameText;
+ // Profile name has changed so save the profiles
+ this.profileManager.saveProfiles();
+ }
+
+ setSelectedProfiles(selectedProfiles: GridRowSelectionModel) {
+ // The length should either be 0 or 1
+ if (selectedProfiles.length > 1) {
+ throw new Error('Only one profile can be selected at a time.');
+ }
+
+ this.selectedProfiles = selectedProfiles;
+
+ // Whenever the selected profile changes, update the profile name field to match the selected profile
+ let name;
+ if (this.selectedProfiles.length === 1) {
+ name = this.profileManager.profiles[this.selectedProfiles[0] as number].name;
+ } else {
+ name = '';
+ }
+ // this.profileName.setDispValue(name);
+ // this.profileName.apply();
+ this.setProfileName(name);
+ }
+
+ loadProfile = async () => {
+ if (this.selectedProfiles.length !== 1) {
+ throw new Error('Expected there to be one profile selected.');
+ }
+ let selectedProfileIdx = this.selectedProfiles[0];
+ await this.profileManager.applyProfileToApp(selectedProfileIdx as number);
+ };
+
+ /**
+ * Save the current app state to the selected profile.
+ */
+ saveCurrentAppStateToProfile() {
+ if (this.selectedProfiles.length !== 1) {
+ throw new Error('Expected there to be one profile selected.');
+ }
+ let selectedProfileIdx = this.selectedProfiles[0];
+ this.profileManager.saveCurrentAppConfigToProfile(selectedProfileIdx as number);
+ }
+
+ deleteProfile() {
+ if (this.selectedProfiles.length !== 1) {
+ throw new Error('Expected there to be one profile selected.');
+ }
+ let selectedProfileIdx = this.selectedProfiles[0];
+ this.profileManager.deleteProfile(selectedProfileIdx as number);
+ }
+}
diff --git a/src/model/Settings/RxSettings/RxSettings.ts b/src/model/Settings/RxSettings/RxSettings.ts
index 26b935be..dc1f72eb 100644
--- a/src/model/Settings/RxSettings/RxSettings.ts
+++ b/src/model/Settings/RxSettings/RxSettings.ts
@@ -1,9 +1,8 @@
import { makeAutoObservable } from "mobx";
import { z } from "zod";
-import AppStorage from "src/model/Storage/AppStorage";
import { ApplyableNumberField, ApplyableTextField } from "src/view/Components/ApplyableTextField";
-import { createSerializableObjectFromConfig, updateConfigFromSerializable } from "src/model/Util/SettingsLoader";
+import { ProfileManager } from "src/model/ProfileManager/ProfileManager";
export enum DataType {
ASCII,
@@ -78,7 +77,7 @@ export enum Endianness {
BIG_ENDIAN = 'Big Endian', // MSB is sent first.
}
-class Config {
+export class RxSettingsConfig {
/**
* Increment this version number if you need to update this data in this class.
* This will cause the app to ignore whatever is in local storage and use the defaults,
@@ -91,6 +90,58 @@ class Config {
*/
dataType = DataType.ASCII;
+ // ASCII-SPECIFIC SETTINGS
+ ansiEscapeCodeParsingEnabled = true;
+ maxEscapeCodeLengthChars = 10;
+ localTxEcho = false;
+ newLineCursorBehavior = NewLineCursorBehavior.CARRIAGE_RETURN_AND_NEW_LINE;
+ swallowNewLine = true;
+ carriageReturnCursorBehavior = CarriageReturnCursorBehavior.DO_NOTHING;
+ swallowCarriageReturn = true;
+ nonVisibleCharDisplayBehavior = NonVisibleCharDisplayBehaviors.ASCII_CONTROL_GLYPHS_AND_HEX_GLYPHS;
+
+ // NUMBER-SPECIFIC SETTINGS
+ numberType = NumberType.HEX;
+ endianness = Endianness.LITTLE_ENDIAN;
+ numberSeparator = " ";
+ preventValuesWrappingAcrossRows = true;
+ insertNewLineOnMatchedValue = false;
+ newLineMatchValueAsHex = "00";
+ newLinePlacementOnHexValue = NewLinePlacementOnHexValue.BEFORE;
+ padValues = true;
+ paddingCharacter = PaddingCharacter.ZERO;
+
+ /**
+ * Set to -1 for automatic padding, which will pad up to the largest possible value
+ * for the selected number type.
+ */
+ numPaddingChars = -1;
+
+ // HEX SPECIFIC SETTINGS
+ numBytesPerHexNumber = 1;
+ hexCase = HexCase.UPPERCASE;
+ prefixHexValuesWith0x = false;
+
+ // FLOAT SPECIFIC SETTINGS
+ floatStringConversionMethod = FloatStringConversionMethod.TO_STRING;
+ floatNumOfDecimalPlaces = 5;
+
+ constructor() {
+ makeAutoObservable(this); // Make sure this is at the end of the constructor
+ }
+}
+
+const CONFIG_KEY = ['settings', 'rx-settings'];
+
+export default class RxSettings {
+
+ profileManager: ProfileManager;
+
+ /**
+ * How to interpret the received data from the serial port.
+ */
+ dataType = DataType.ASCII;
+
// ASCII-SPECIFIC SETTINGS
ansiEscapeCodeParsingEnabled = true;
maxEscapeCodeLengthChars = new ApplyableNumberField("10", z.coerce.number().min(2));
@@ -132,72 +183,134 @@ class Config {
floatStringConversionMethod = FloatStringConversionMethod.TO_STRING;
floatNumOfDecimalPlaces = new ApplyableNumberField("5", z.coerce.number().min(0).max(100).int());
- constructor() {
- makeAutoObservable(this); // Make sure this is at the end of the constructor
- }
-}
-
-const CONFIG_KEY = ['settings', 'rx-settings'];
-
-export default class RxSettings {
- appStorage: AppStorage;
-
- config = new Config();
- constructor(appStorage: AppStorage) {
- this.appStorage = appStorage;
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
this._loadConfig();
- makeAutoObservable(this); // Make sure this is at the end of the constructor
-
- this.config.maxEscapeCodeLengthChars.setOnApplyChanged(() => {
+ this.profileManager.registerOnProfileLoad(() => {
+ this._loadConfig();
+ });
+ this.maxEscapeCodeLengthChars.setOnApplyChanged(() => {
this._saveConfig();
});
- this.config.numberSeparator.setOnApplyChanged(() => {
+ this.numberSeparator.setOnApplyChanged(() => {
this._saveConfig();
});
- this.config.newLineMatchValueAsHex.setOnApplyChanged(() => {
+ this.newLineMatchValueAsHex.setOnApplyChanged(() => {
this._saveConfig();
});
- this.config.numPaddingChars.setOnApplyChanged(() => {
+ this.numPaddingChars.setOnApplyChanged(() => {
this._saveConfig();
});
- this.config.floatNumOfDecimalPlaces.setOnApplyChanged(() => {
+ this.floatNumOfDecimalPlaces.setOnApplyChanged(() => {
this._saveConfig();
});
+ makeAutoObservable(this); // Make sure this is at the end of the constructor
}
_loadConfig = () => {
- let deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
-
+ let configToLoad = this.profileManager.currentAppConfig.settings.rxSettings
//===============================================
// UPGRADE PATH
//===============================================
- if (deserializedConfig === null) {
- // No data exists, create
- console.log(`No config found in local storage for key ${CONFIG_KEY}. Creating...`);
- this._saveConfig();
- return;
- } else if (deserializedConfig.version === this.config.version) {
- console.log(`Up-to-date config found for key ${CONFIG_KEY}.`);
+ const latestVersion = new RxSettingsConfig().version;
+ if (configToLoad.version === latestVersion) {
+ // Do nothing
} else {
- console.error(`Out-of-date config version ${deserializedConfig.version} found for key ${CONFIG_KEY}.` +
- ` Updating to version ${this.config.version}.`);
+ console.log(`Out-of-date config version ${configToLoad.version} found.` +
+ ` Updating to version ${latestVersion}.`);
this._saveConfig();
- deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
+ configToLoad = this.profileManager.currentAppConfig.settings.rxSettings
}
- // At this point we are confident that the deserialized config matches what
- // this classes config object wants, so we can go ahead and update.
- updateConfigFromSerializable(deserializedConfig, this.config);
+ /**
+ * How to interpret the received data from the serial port.
+ */
+ this.dataType = configToLoad.dataType;
+
+ // ASCII-SPECIFIC SETTINGS
+ this.ansiEscapeCodeParsingEnabled = configToLoad.ansiEscapeCodeParsingEnabled;
+ this.maxEscapeCodeLengthChars.setDispValue(configToLoad.maxEscapeCodeLengthChars.toString());
+ this.maxEscapeCodeLengthChars.apply();
+ this.localTxEcho = configToLoad.localTxEcho;
+ this.newLineCursorBehavior = configToLoad.newLineCursorBehavior;
+ this.swallowNewLine = configToLoad.swallowNewLine;
+ this.carriageReturnCursorBehavior = configToLoad.carriageReturnCursorBehavior;
+ this.swallowCarriageReturn = configToLoad.swallowCarriageReturn;
+ this.nonVisibleCharDisplayBehavior = configToLoad.nonVisibleCharDisplayBehavior;
+
+ // NUMBER-SPECIFIC SETTINGS
+ this.numberType = configToLoad.numberType;
+ this.endianness = configToLoad.endianness;
+ this.numberSeparator.setDispValue(configToLoad.numberSeparator);
+ this.numberSeparator.apply();
+ this.preventValuesWrappingAcrossRows = configToLoad.preventValuesWrappingAcrossRows;
+ this.insertNewLineOnMatchedValue = configToLoad.insertNewLineOnMatchedValue;
+ this.newLineMatchValueAsHex.setDispValue(configToLoad.newLineMatchValueAsHex);
+ this.newLineMatchValueAsHex.apply();
+ this.newLinePlacementOnHexValue = configToLoad.newLinePlacementOnHexValue;
+ this.padValues = configToLoad.padValues;
+ this.paddingCharacter = configToLoad.paddingCharacter;
+
+ /**
+ * Set to -1 for automatic padding, which will pad up to the largest possible value
+ * for the selected number type.
+ */
+ this.numPaddingChars.setDispValue(configToLoad.numPaddingChars.toString());
+ this.numPaddingChars.apply();
+
+ // HEX SPECIFIC SETTINGS
+ this.numBytesPerHexNumber.setDispValue(configToLoad.numBytesPerHexNumber.toString());
+ this.numBytesPerHexNumber.apply();
+ this.hexCase = configToLoad.hexCase;
+ this.prefixHexValuesWith0x = configToLoad.prefixHexValuesWith0x;
+
+ // FLOAT SPECIFIC SETTINGS
+ this.floatStringConversionMethod = configToLoad.floatStringConversionMethod;
+ this.floatNumOfDecimalPlaces.setDispValue(configToLoad.floatNumOfDecimalPlaces.toString());
+ this.floatNumOfDecimalPlaces.apply();
};
_saveConfig = () => {
- const serializableConfig = createSerializableObjectFromConfig(this.config);
- this.appStorage.saveConfig(CONFIG_KEY, serializableConfig);
+ let config = this.profileManager.currentAppConfig.settings.rxSettings;
+ config.dataType = this.dataType;
+
+ // ASCII-SPECIFIC SETTINGS
+ config.ansiEscapeCodeParsingEnabled = this.ansiEscapeCodeParsingEnabled;
+ config.maxEscapeCodeLengthChars = this.maxEscapeCodeLengthChars.appliedValue;
+ config.localTxEcho = this.localTxEcho;
+ config.newLineCursorBehavior = this.newLineCursorBehavior;
+ config.swallowNewLine = this.swallowNewLine;
+ config.carriageReturnCursorBehavior = this.carriageReturnCursorBehavior;
+ config.swallowCarriageReturn = this.swallowCarriageReturn;
+ config.nonVisibleCharDisplayBehavior = this.nonVisibleCharDisplayBehavior;
+
+ // NUMBER-SPECIFIC SETTINGS
+ config.numberType = this.numberType;
+ config.endianness = this.endianness;
+ config.numberSeparator = this.numberSeparator.appliedValue;
+ config.preventValuesWrappingAcrossRows = this.preventValuesWrappingAcrossRows;
+ config.insertNewLineOnMatchedValue = this.insertNewLineOnMatchedValue;
+ config.newLineMatchValueAsHex = this.newLineMatchValueAsHex.appliedValue;
+ config.newLinePlacementOnHexValue = this.newLinePlacementOnHexValue;
+ config.padValues = this.padValues;
+ config.paddingCharacter = this.paddingCharacter;
+ config.numPaddingChars = this.numPaddingChars.appliedValue;
+
+ // HEX SPECIFIC SETTINGS
+ config.numBytesPerHexNumber = this.numBytesPerHexNumber.appliedValue;
+ config.hexCase = this.hexCase;
+ config.prefixHexValuesWith0x = this.prefixHexValuesWith0x;
+
+ // FLOAT SPECIFIC SETTINGS
+ config.floatStringConversionMethod = this.floatStringConversionMethod;
+ config.floatNumOfDecimalPlaces = this.floatNumOfDecimalPlaces.appliedValue;
+
+ this.profileManager.saveAppConfig();
};
setDataType = (value: DataType) => {
- this.config.dataType = value;
+ this.dataType = value;
this._saveConfig();
};
@@ -206,37 +319,37 @@ export default class RxSettings {
//=================================================================
setAnsiEscapeCodeParsingEnabled = (value: boolean) => {
- this.config.ansiEscapeCodeParsingEnabled = value;
+ this.ansiEscapeCodeParsingEnabled = value;
this._saveConfig();
};
setLocalTxEcho = (value: boolean) => {
- this.config.localTxEcho = value;
+ this.localTxEcho = value;
this._saveConfig();
};
setNewLineCursorBehavior = (value: NewLineCursorBehavior) => {
- this.config.newLineCursorBehavior = value;
+ this.newLineCursorBehavior = value;
this._saveConfig();
};
setSwallowNewLine = (value: boolean) => {
- this.config.swallowNewLine = value;
+ this.swallowNewLine = value;
this._saveConfig();
};
setCarriageReturnBehavior = (value: CarriageReturnCursorBehavior) => {
- this.config.carriageReturnCursorBehavior = value;
+ this.carriageReturnCursorBehavior = value;
this._saveConfig();
};
setSwallowCarriageReturn = (value: boolean) => {
- this.config.swallowCarriageReturn = value;
+ this.swallowCarriageReturn = value;
this._saveConfig();
};
setNonVisibleCharDisplayBehavior = (value: NonVisibleCharDisplayBehaviors) => {
- this.config.nonVisibleCharDisplayBehavior = value;
+ this.nonVisibleCharDisplayBehavior = value;
this._saveConfig();
};
@@ -245,37 +358,37 @@ export default class RxSettings {
//=================================================================
setNumberType = (value: NumberType) => {
- this.config.numberType = value;
+ this.numberType = value;
this._saveConfig();
}
setEndianness = (value: Endianness) => {
- this.config.endianness = value;
+ this.endianness = value;
this._saveConfig();
};
setPreventHexValuesWrappingAcrossRows = (value: boolean) => {
- this.config.preventValuesWrappingAcrossRows = value;
+ this.preventValuesWrappingAcrossRows = value;
this._saveConfig();
};
setInsertNewLineOnValue = (value: boolean) => {
- this.config.insertNewLineOnMatchedValue = value;
+ this.insertNewLineOnMatchedValue = value;
this._saveConfig();
};
setNewLinePlacementOnValue = (value: NewLinePlacementOnHexValue) => {
- this.config.newLinePlacementOnHexValue = value;
+ this.newLinePlacementOnHexValue = value;
this._saveConfig();
};
setPadValues = (value: boolean) => {
- this.config.padValues = value;
+ this.padValues = value;
this._saveConfig();
}
setPaddingCharacter = (value: PaddingCharacter) => {
- this.config.paddingCharacter = value;
+ this.paddingCharacter = value;
this._saveConfig();
}
@@ -284,12 +397,12 @@ export default class RxSettings {
//=================================================================
setHexCase = (value: HexCase) => {
- this.config.hexCase = value;
+ this.hexCase = value;
this._saveConfig();
};
setPrefixHexValuesWith0x = (value: boolean) => {
- this.config.prefixHexValuesWith0x = value;
+ this.prefixHexValuesWith0x = value;
this._saveConfig();
};
@@ -298,7 +411,7 @@ export default class RxSettings {
//=================================================================
setFloatStringConversionMethod = (value: FloatStringConversionMethod) => {
- this.config.floatStringConversionMethod = value;
+ this.floatStringConversionMethod = value;
this._saveConfig();
};
@@ -312,10 +425,14 @@ export default class RxSettings {
* @returns The descriptive name as a string.
*/
getDataTypeNameForToolbarDisplay = () => {
- if (this.config.dataType === DataType.ASCII) {
+ return RxSettings.computeDataTypeNameForToolbarDisplay(this.dataType, this.numberType);
+ };
+
+ static computeDataTypeNameForToolbarDisplay = (dataType: DataType, numberType: NumberType) => {
+ if (dataType === DataType.ASCII) {
return "ASCII";
} else {
- return this.config.numberType;
+ return numberType;
}
- };
+ }
}
diff --git a/src/model/Settings/Settings.tsx b/src/model/Settings/Settings.tsx
index e8b6f2e3..5659a275 100644
--- a/src/model/Settings/Settings.tsx
+++ b/src/model/Settings/Settings.tsx
@@ -3,14 +3,14 @@
import { makeAutoObservable } from 'mobx';
// eslint-disable-next-line import/no-cycle
-import { App } from '../App';
import TxSettings from './TxSettings/TxSettings';
import RxSettings from './RxSettings/RxSettings';
import DisplaySettings from './DisplaySettings/DisplaySettings';
import PortConfiguration from './PortConfigurationSettings/PortConfigurationSettings';
import GeneralSettings from './GeneralSettings/GeneralSettings';
-import AppStorage from '../Storage/AppStorage';
import FakePortsController from 'src/model/FakePorts/FakePortsController';
+import ProfilesSettings from './ProfileSettings/ProfileSettings';
+import { ProfileManager } from '../ProfileManager/ProfileManager';
@@ -20,11 +20,10 @@ export enum SettingsCategories {
RX_SETTINGS,
DISPLAY,
GENERAL,
+ PROFILES,
}
export class Settings {
- appStorage: AppStorage;
-
fakePortsController: FakePortsController;
activeSettingsCategory: SettingsCategories =
@@ -40,21 +39,23 @@ export class Settings {
generalSettings: GeneralSettings;
+ profilesSettings: ProfilesSettings;
+
/**
* Constructor for the Settings class.
*
* @param appStorage Needed to load/save settings into local storage.
* @param fakePortController Needed to show the hidden fake port dialog.
*/
- constructor(appStorage: AppStorage, fakePortController: FakePortsController) {
- this.appStorage = appStorage;
+ constructor(profileManager: ProfileManager, fakePortController: FakePortsController) {
this.fakePortsController = fakePortController;
- this.portConfiguration = new PortConfiguration(appStorage);
- this.txSettings = new TxSettings(appStorage);
- this.rxSettings = new RxSettings(appStorage);
- this.displaySettings = new DisplaySettings(appStorage);
- this.generalSettings = new GeneralSettings(appStorage);
+ this.portConfiguration = new PortConfiguration(profileManager);
+ this.txSettings = new TxSettings(profileManager);
+ this.rxSettings = new RxSettings(profileManager);
+ this.displaySettings = new DisplaySettings(profileManager);
+ this.generalSettings = new GeneralSettings(profileManager);
+ this.profilesSettings = new ProfilesSettings(profileManager);
makeAutoObservable(this); // Make sure this is at the end of the constructor
}
diff --git a/src/model/Settings/TxSettings/TxSettings.ts b/src/model/Settings/TxSettings/TxSettings.ts
index 0f2e4bc0..6ba7eb9d 100644
--- a/src/model/Settings/TxSettings/TxSettings.ts
+++ b/src/model/Settings/TxSettings/TxSettings.ts
@@ -1,7 +1,5 @@
import { makeAutoObservable } from 'mobx';
-
-import AppStorage from 'src/model/Storage/AppStorage';
-import { createSerializableObjectFromConfig, updateConfigFromSerializable } from 'src/model/Util/SettingsLoader';
+import { ProfileManager } from 'src/model/ProfileManager/ProfileManager';
export enum EnterKeyPressBehavior {
SEND_LF,
@@ -20,13 +18,13 @@ export enum DeleteKeyPressBehavior {
SEND_VT_SEQUENCE,
}
-class Config {
+export class TxSettingsConfig {
/**
* Increment this version number if you need to update this data in this class.
* This will cause the app to ignore whatever is in local storage and use the defaults,
* updating to this new version.
*/
- version = 2;
+ version = 1;
enterKeyPressBehavior = EnterKeyPressBehavior.SEND_LF;
@@ -53,78 +51,103 @@ class Config {
* This emulates standard meta key behavior in most terminals.
*/
sendEscCharWhenAltKeyPressed = true;
-
- constructor() {
- makeAutoObservable(this); // Make sure this is at the end of the constructor
- }
}
-const CONFIG_KEY = ['settings', 'tx-settings'];
-
export default class DataProcessingSettings {
- appStorage: AppStorage;
+ profileManager: ProfileManager;
- config = new Config();
+ enterKeyPressBehavior = EnterKeyPressBehavior.SEND_LF;
- constructor(appStorage: AppStorage) {
- this.appStorage = appStorage;
- this._loadSettings();
+ /**
+ * What to do when the user presses the backspace key.
+ */
+ backspaceKeyPressBehavior = BackspaceKeyPressBehavior.SEND_BACKSPACE;
+
+ /**
+ * What to do when the user presses the delete key.
+ */
+ deleteKeyPressBehavior = DeleteKeyPressBehavior.SEND_VT_SEQUENCE;
+
+ /**
+ * If true, hex bytes 0x01-0x1A will be sent when the user
+ * presses Ctrl+A thru Ctrl+Z
+ */
+ send0x01Thru0x1AWhenCtrlAThruZPressed = true;
+
+ /**
+ * If true, [ESC] + will be sent when the user presses
+ * Alt- (e.g. Alt-A will send the bytes 0x1B 0x41).
+ *
+ * This emulates standard meta key behavior in most terminals.
+ */
+ sendEscCharWhenAltKeyPressed = true;
+
+ constructor(profileManager: ProfileManager) {
+ this.profileManager = profileManager;
+ this._loadConfig();
+ this.profileManager.registerOnProfileLoad(() => {
+ this._loadConfig();
+ });
makeAutoObservable(this); // Make sure this is at the end of the constructor
}
- _loadSettings = () => {
- let deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
-
+ _loadConfig = () => {
+ let configToLoad = this.profileManager.currentAppConfig.settings.txSettings;
//===============================================
// UPGRADE PATH
//===============================================
- if (deserializedConfig === null) {
- // No data exists, create
- console.log(`No config found in local storage for key ${CONFIG_KEY}. Creating...`);
- this._saveSettings();
- return;
- } else if (deserializedConfig.version === this.config.version) {
- console.log(`Up-to-date config found for key ${CONFIG_KEY}.`);
+ const latestVersion = new TxSettingsConfig().version;
+ if (configToLoad.version === latestVersion) {
+ // Do nothing
} else {
- console.error(`Out-of-date config version ${deserializedConfig.version} found for key ${CONFIG_KEY}.` +
- ` Updating to version ${this.config.version}.`);
- this._saveSettings();
- deserializedConfig = this.appStorage.getConfig(CONFIG_KEY);
+ console.log(`Out-of-date config version ${configToLoad.version} found.` +
+ ` Updating to version ${latestVersion}.`);
+ this._saveConfig();
+ configToLoad = this.profileManager.currentAppConfig.settings.txSettings
}
- // At this point we are confident that the deserialized config matches what
- // this classes config object wants, so we can go ahead and update.
- updateConfigFromSerializable(deserializedConfig, this.config);
+ this.enterKeyPressBehavior = configToLoad.enterKeyPressBehavior;
+ this.backspaceKeyPressBehavior = configToLoad.backspaceKeyPressBehavior;
+ this.deleteKeyPressBehavior = configToLoad.deleteKeyPressBehavior;
+ this.send0x01Thru0x1AWhenCtrlAThruZPressed = configToLoad.send0x01Thru0x1AWhenCtrlAThruZPressed;
+ this.sendEscCharWhenAltKeyPressed = configToLoad.sendEscCharWhenAltKeyPressed;
};
- _saveSettings = () => {
- const serializableConfig = createSerializableObjectFromConfig(this.config);
- this.appStorage.saveConfig(CONFIG_KEY, serializableConfig);
+ _saveConfig = () => {
+ let config = this.profileManager.currentAppConfig.settings.txSettings;
+
+ config.enterKeyPressBehavior = this.enterKeyPressBehavior;
+ config.backspaceKeyPressBehavior = this.backspaceKeyPressBehavior;
+ config.deleteKeyPressBehavior = this.deleteKeyPressBehavior;
+ config.send0x01Thru0x1AWhenCtrlAThruZPressed = this.send0x01Thru0x1AWhenCtrlAThruZPressed;
+ config.sendEscCharWhenAltKeyPressed = this.sendEscCharWhenAltKeyPressed;
+
+ this.profileManager.saveAppConfig();
};
setEnterKeyPressBehavior = (value: EnterKeyPressBehavior) => {
- this.config.enterKeyPressBehavior = value;
- this._saveSettings();
+ this.enterKeyPressBehavior = value;
+ this._saveConfig();
};
setBackspaceKeyPressBehavior = (value: BackspaceKeyPressBehavior) => {
- this.config.backspaceKeyPressBehavior = value;
- this._saveSettings();
+ this.backspaceKeyPressBehavior = value;
+ this._saveConfig();
};
setDeleteKeyPressBehavior = (value: DeleteKeyPressBehavior) => {
- this.config.deleteKeyPressBehavior = value;
- this._saveSettings();
+ this.deleteKeyPressBehavior = value;
+ this._saveConfig();
};
setSend0x01Thru0x1AWhenCtrlAThruZPressed = (value: boolean) => {
- this.config.send0x01Thru0x1AWhenCtrlAThruZPressed = value;
- this._saveSettings();
+ this.send0x01Thru0x1AWhenCtrlAThruZPressed = value;
+ this._saveConfig();
}
setSendEscCharWhenAltKeyPressed = (value: boolean) => {
- this.config.sendEscCharWhenAltKeyPressed = value;
- this._saveSettings();
+ this.sendEscCharWhenAltKeyPressed = value;
+ this._saveConfig();
}
}
diff --git a/src/model/Storage/AppStorage.spec.ts b/src/model/Storage/AppStorage.spec.ts
deleted file mode 100644
index 4fed1721..00000000
--- a/src/model/Storage/AppStorage.spec.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { expect, test, describe, beforeEach } from 'vitest'
-
-import AppStorage from './AppStorage';
-
-beforeEach(() => {
- // Clear local storage, because otherwise jsdom persists storage
- // between tests
- window.localStorage.clear();
-})
-
-describe('config tests', () => {
- test('get config returns null when nothing stored there', () => {
- const appStorage = new AppStorage();
- const value = appStorage.getConfig(['prop1', 'prop2']);
- expect(value).toEqual(null);
- })
-
- test('basic get and set works', () => {
- const appStorage = new AppStorage();
- appStorage.saveConfig(['prop1', 'prop2'], 'hello');
- const value = appStorage.getConfig(['prop1', 'prop2']);
- expect(value).toEqual('hello');
- })
-
- test('can get parent key and see child', () => {
- const appStorage = new AppStorage();
- appStorage.saveConfig(['prop1', 'prop2'], 'hello');
- const value = appStorage.getConfig(['prop1']);
- expect(value).toEqual({ prop2: 'hello' });
- })
-
- test('can read back root config', () => {
- const appStorage = new AppStorage();
- appStorage.saveConfig(['prop1', 'prop2'], 'hello');
- const value = appStorage.getConfig([]);
- expect(value).toEqual({ prop1: { prop2: 'hello' } });
- })
-
- test('can set root config', () => {
- const appStorage = new AppStorage();
- appStorage.saveConfig([], 'hello');
- const value = appStorage.getConfig([]);
- expect(value).toEqual('hello');
- })
-});
diff --git a/src/model/Storage/AppStorage.ts b/src/model/Storage/AppStorage.ts
deleted file mode 100644
index 89ff7503..00000000
--- a/src/model/Storage/AppStorage.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-
-class Config {
- name: string = '';
- configData: any = {};
-}
-
-/**
- * This class manages the local storage (persistance browser based key/object data store)
- * for the application. It is used to store both general data (saveData/getData) and configurations
- * (saveConfig/getConfig).
- */
-export default class AppStorage {
-
- configs: Config[] = [];
-
- activeConfig: Config;
-
- constructor() {
- // Read in configurations
- const configsStr = window.localStorage.getItem('configs');
- if (configsStr === null) {
- // No config key found in users store, create one!
- const defaultConfig = new Config();
- this.configs.push(defaultConfig);
- // Save just-created config back to store. Not strictly needed as it
- // will be saved as soon as any changes are made, but this feels
- // cleaner.
- window.localStorage.setItem('configs', JSON.stringify(this.configs));
- } else {
- this.configs = JSON.parse(configsStr);
- }
- // Only support the 1 active config for now
- this.activeConfig = this.configs[0];
-
- }
-
- /**
- * Saves arbitrary data to local storage. Settings should be stored using
- * saveConfig/getConfig instead.
- * @param key The key to save the data at.
- * @param data The data to save.
- */
- saveData = (key: string, data: any) => {
- window.localStorage.setItem(key, JSON.stringify(data));
- }
-
- /**
- * Gets arbitrary data from local storage. Settings should be retrieved using
- * saveConfig/getConfig instead.
- *
- * @param key The key to retrieve data from.
- * @returns The data, or null if not found.
- */
- getData = (key: string): any => {
- const value = window.localStorage.getItem(key);
- if (value === null) {
- return null;
- }
- return JSON.parse(value);
- }
-
- /**
- * Saves a particular setting to the active config. This is stored
- * in the local storage of the browser.
- *
- * @param keys Array of strings the defines the "path" to the setting. If various
- * parts of the key do not exist, empty objects will be created.
- * @param data Value of the setting.
- */
- saveConfig(keys: string[], data: any) {
- let obj = this.activeConfig.configData;
- // Walk down the active config object using
- // the array of keys
- for (let i = 0; i < keys.length - 1; i++) {
- const key = keys[i];
- // Make sure key exists, if not
- // create it
- if (obj[key] === undefined) {
- obj[key] = {};
- }
- obj = obj[key];
- }
- // If no keys were provided, we are writing to the entire
- // config object
- if (keys.length === 0) {
- this.activeConfig.configData = data;
- } else {
- obj[keys[keys.length - 1]] = data;
- }
- const valueToWrite = JSON.stringify(this.configs);
- window.localStorage.setItem('configs', valueToWrite);
- }
-
- /**
- * Gets a particular setting from the active config.
- *
- * @param keys Array of strings the defines the "path" to the setting.
- * @returns The value of the setting, or null if not found.
- */
- getConfig(keys: string[]): any {
- let obj = this.activeConfig.configData;
- // Walk down the active config object using
- // the array of keys
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- // Make sure key exists, if not
- // create it
- if (obj[key] === undefined) {
- return null;
- }
- obj = obj[key];
- }
- return obj;
- }
-
-
-
-}
diff --git a/src/model/Terminals/RightDrawer/Macros/Macro.spec.ts b/src/model/Terminals/RightDrawer/Macros/Macro.spec.ts
index c1aaa623..dbb65a64 100644
--- a/src/model/Terminals/RightDrawer/Macros/Macro.spec.ts
+++ b/src/model/Terminals/RightDrawer/Macros/Macro.spec.ts
@@ -79,15 +79,15 @@ describe('macro tests', () => {
expect(bytes).toStrictEqual(Uint8Array.from([0x00, 0xFF, 0xAB, 0x32, 0x68, 0x91]));
});
- test('toJSON() and fromJSON() work', () => {
+ test('toConfig() and fromConfig() work', () => {
let macro = new Macro('M1', () => '\n');
macro.setDataType(MacroDataType.HEX);
macro.setData('1234');
- const json = JSON.stringify(macro);
+ const macroConfig = macro.toConfig();
// Create new macro
let newMacro = new Macro('M1', () => '\n');
- newMacro.fromJSON(json);
+ newMacro.loadConfig(macroConfig);
expect(newMacro.data).toBe(macro.data);
expect(newMacro.dataType).toBe(macro.dataType);
diff --git a/src/model/Terminals/RightDrawer/Macros/Macro.ts b/src/model/Terminals/RightDrawer/Macros/Macro.ts
index 49191042..71361c89 100644
--- a/src/model/Terminals/RightDrawer/Macros/Macro.ts
+++ b/src/model/Terminals/RightDrawer/Macros/Macro.ts
@@ -1,6 +1,4 @@
import { makeAutoObservable } from "mobx";
-import { App } from "src/model/App";
-import { z } from "zod";
import { stringToUint8Array } from 'src/model/Util/Util';
export enum MacroDataType {
@@ -8,6 +6,15 @@ export enum MacroDataType {
HEX = "HEX",
}
+export class MacroConfig {
+ version = 1;
+ name = '';
+ dataType = MacroDataType.ASCII;
+ data = '';
+ processEscapeChars = true;
+ sendOnEnterValueForEveryNewLineInTextBox = true;
+}
+
export class Macro {
name: string;
@@ -169,9 +176,23 @@ export class Macro {
return this.data.length !== 0 && this.errorMsg === '';
}
- fromJSON = (json: string) => {
- const objFromJson = JSON.parse(json);
- Object.assign(this, objFromJson);
+ loadConfig = (config: MacroConfig) => {
+ this.name = config.name;
+ this.data = config.data;
+ this.dataType = config.dataType;
+ this.processEscapeChars = config.processEscapeChars;
+ this.sendOnEnterValueForEveryNewLineInTextBox = config.sendOnEnterValueForEveryNewLineInTextBox;
+ }
+
+ toConfig = (): MacroConfig => {
+ return {
+ version: 1,
+ name: this.name,
+ data: this.data,
+ dataType: this.dataType,
+ processEscapeChars: this.processEscapeChars,
+ sendOnEnterValueForEveryNewLineInTextBox: this.sendOnEnterValueForEveryNewLineInTextBox,
+ };
}
setOnChange(onChange: () => void) {
diff --git a/src/model/Terminals/RightDrawer/Macros/MacroController.ts b/src/model/Terminals/RightDrawer/Macros/MacroController.ts
index 27286c1f..d8ba0c4d 100644
--- a/src/model/Terminals/RightDrawer/Macros/MacroController.ts
+++ b/src/model/Terminals/RightDrawer/Macros/MacroController.ts
@@ -1,14 +1,14 @@
import { makeAutoObservable } from "mobx";
+
import { App } from "src/model/App";
-import { Macro, MacroDataType } from "./Macro";
+import { Macro, MacroConfig, MacroDataType } from "./Macro";
import { EnterKeyPressBehavior } from "src/model/Settings/TxSettings/TxSettings";
const NUM_MACROS = 8;
-const CONFIG_KEY = ["macros"];
const CONFIG_VERSION = 1;
-class Config {
+export class MacroControllerConfig {
/**
* Increment this version number if you need to update this data in this class.
* This will cause the app to ignore whatever is in local storage and use the defaults,
@@ -16,11 +16,21 @@ class Config {
*/
version = CONFIG_VERSION;
+ macroConfigs: MacroConfig[] = [];
+
constructor() {
- // makeAutoObservable(this); // Make sure this is at the end of the constructor
+ // Create 8 macros by default and put some example data in the first two. This will
+ // only be applied the first time the user runs the app, after then it will load
+ // saved config from local storage
+ this.macroConfigs = [];
+ for (let i = 0; i < NUM_MACROS; i++) {
+ let macroConfig = new MacroConfig();
+ this.macroConfigs.push(macroConfig);
+ }
+ this.macroConfigs[0].data = 'Hello\\n';
+ this.macroConfigs[1].data = 'deadbeef';
+ this.macroConfigs[1].dataType = MacroDataType.HEX;
}
-
- macros: any[] = [];
}
export class MacroController {
@@ -42,6 +52,10 @@ export class MacroController {
this._loadConfig();
}
+ /**
+ * Recreate the macros array with the given number of macros.
+ * @param numMacros The number of macros to put into the macros array.
+ */
recreateMacros(numMacros: number) {
// Remove all elements from macroArray
@@ -54,11 +68,11 @@ export class MacroController {
new Macro(
`M${i + 1}`,
() => {
- if (this.app.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_LF) {
+ if (this.app.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_LF) {
return "\n";
- } else if (this.app.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CR) {
+ } else if (this.app.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CR) {
return "\r";
- } else if (this.app.settings.txSettings.config.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CRLF) {
+ } else if (this.app.settings.txSettings.enterKeyPressBehavior === EnterKeyPressBehavior.SEND_CRLF) {
return "\r\n";
} else {
throw new Error("Unknown enter key press behavior");
@@ -68,68 +82,62 @@ export class MacroController {
)
);
}
-
- // Add some example data. Don't use setData because this will trigger a save
- this.macrosArray[0].data = 'Hello\\n';
- this.macrosArray[1].data = 'deadbeef';
- this.macrosArray[1].dataType = MacroDataType.HEX;
}
setMacroToDisplayInModal(macro: Macro) {
- console.log("Set macro to display in modal:", macro);
this.macroToDisplayInModal = macro;
}
setIsModalOpen(isOpen: boolean) {
- console.log("Set isModalOpen:", isOpen);
this.isModalOpen = isOpen;
}
+ /**
+ * Send the provided macro data to the serial port.
+ * @param macro The macro to send.
+ */
send(macro: Macro) {
- console.log("Send macro data:", macro.data);
// Send the data to the serial port
// If the user presses enter in the multiline text field, it will add a newline character
// (0x0A or 10) to the string.
let outputData;
outputData = macro.dataToBytes();
- console.log("Data:", outputData);
this.app.writeBytesToSerialPort(outputData);
}
_saveConfig = () => {
- let config = new Config();
- config.macros = this.macrosArray.map((macro) => {
- return JSON.stringify(macro);
+
+ let config = this.app.profileManager.currentAppConfig.terminal.macroController;
+
+ config.macroConfigs = this.macrosArray.map((macro) => {
+ return macro.toConfig();
});
- console.log("Saving config: ", config);
- this.app.appStorage.saveConfig(CONFIG_KEY, config);
+
+ this.app.profileManager.saveAppConfig();
};
_loadConfig() {
- let deserializedConfig = this.app.appStorage.getConfig(CONFIG_KEY);
+ let configToLoad = this.app.profileManager.currentAppConfig.terminal.macroController;
//===============================================
// UPGRADE PATH
//===============================================
- if (deserializedConfig === null) {
- // No data exists, create
- console.log(`No config found in local storage for key "${CONFIG_KEY}". Creating...`);
- this._saveConfig();
- return;
- } else if (deserializedConfig.version === CONFIG_VERSION) {
- console.log(`Up-to-date config found for key "${CONFIG_KEY}".`);
+ const latestVersion = new MacroControllerConfig().version;
+ if (configToLoad.version === latestVersion) {
+ // Do nothing
} else {
- console.error(`Out-of-date config version ${deserializedConfig.version} found for key "${CONFIG_KEY}".` + ` Updating to version ${CONFIG_VERSION}.`);
+ console.log(`Out-of-date config version ${configToLoad.version} found.` +
+ ` Updating to version ${latestVersion}.`);
this._saveConfig();
- return;
+ configToLoad = this.app.profileManager.currentAppConfig.terminal.macroController;
}
// If we get here we loaded a valid config. Apply config.
- this.recreateMacros(deserializedConfig.macros.length);
- for (let i = 0; i < deserializedConfig.macros.length; i++) {
- const macroData = deserializedConfig.macros[i];
+ this.recreateMacros(configToLoad.macroConfigs.length);
+ for (let i = 0; i < configToLoad.macroConfigs.length; i++) {
+ const macroConfig = configToLoad.macroConfigs[i];
let macro = this.macrosArray[i];
- macro.fromJSON(macroData);
+ macro.loadConfig(macroConfig);
};
}
}
diff --git a/src/model/Terminals/SingleTerminal/SingleTerminal.spec.ts b/src/model/Terminals/SingleTerminal/SingleTerminal.spec.ts
index 1423266b..ec0fef54 100644
--- a/src/model/Terminals/SingleTerminal/SingleTerminal.spec.ts
+++ b/src/model/Terminals/SingleTerminal/SingleTerminal.spec.ts
@@ -7,17 +7,20 @@ import RxSettings, {
NonVisibleCharDisplayBehaviors,
} from 'src/model/Settings/RxSettings/RxSettings';
import DisplaySettings from 'src/model/Settings/DisplaySettings/DisplaySettings';
-import AppStorage from 'src/model/Storage/AppStorage';
+import { ProfileManager } from 'src/model/ProfileManager/ProfileManager';
+import { App } from 'src/model/App';
describe('single terminal tests', () => {
- let appStorage: AppStorage;
+ let app: App;
+ let profileManager: ProfileManager;
let dataProcessingSettings: RxSettings;
let displaySettings: DisplaySettings;
let singleTerminal: SingleTerminal;
beforeEach(async () => {
- appStorage = new AppStorage();
- dataProcessingSettings = new RxSettings(appStorage);
- displaySettings = new DisplaySettings(appStorage);
+ app = new App();
+ profileManager = new ProfileManager(app);
+ dataProcessingSettings = new RxSettings(profileManager);
+ displaySettings = new DisplaySettings(profileManager);
singleTerminal = new SingleTerminal(
'test-terminal',
true,
diff --git a/src/model/Terminals/SingleTerminal/SingleTerminal.ts b/src/model/Terminals/SingleTerminal/SingleTerminal.ts
index be4b1433..b7545cd2 100644
--- a/src/model/Terminals/SingleTerminal/SingleTerminal.ts
+++ b/src/model/Terminals/SingleTerminal/SingleTerminal.ts
@@ -144,7 +144,7 @@ export default class SingleTerminal {
this.onTerminalKeyDown = onTerminalKeyDown;
autorun(() => {
- if (!this.rxSettings.config.ansiEscapeCodeParsingEnabled) {
+ if (!this.rxSettings.ansiEscapeCodeParsingEnabled) {
// ANSI escape code parsing has been disabled
// Flush any partial ANSI escape code
for (let idx = 0; idx < this.partialEscapeCode.length; idx += 1) {
@@ -178,7 +178,7 @@ export default class SingleTerminal {
this.isFocused = false;
// Register listener for whenever the number type is changed
- reaction(() => this.rxSettings.config.numberType, this.clearPartialNumberBuffer);
+ reaction(() => this.rxSettings.numberType, this.clearPartialNumberBuffer);
makeAutoObservable(this);
}
@@ -245,12 +245,12 @@ export default class SingleTerminal {
// prepending onto dataAsStr for further processing
// let dataAsStr = String.fromCharCode.apply(null, Array.from(data));
- if (this.rxSettings.config.dataType === DataType.ASCII) {
+ if (this.rxSettings.dataType === DataType.ASCII) {
this.parseAsciiData(data);
- } else if (this.rxSettings.config.dataType === DataType.NUMBER) {
+ } else if (this.rxSettings.dataType === DataType.NUMBER) {
this._parseDataAsNumber(data);
} else {
- throw Error(`Data type ${this.rxSettings.config.dataType} not supported by parseData().`);
+ throw Error(`Data type ${this.rxSettings.dataType} not supported by parseData().`);
}
// Right at the end of adding everything, limit the num. of max. rows in the terminal
@@ -279,14 +279,14 @@ export default class SingleTerminal {
// NEW LINE HANDLING
//========================================================================
- const newLineBehavior = this.rxSettings.config.newLineCursorBehavior;
+ const newLineBehavior = this.rxSettings.newLineCursorBehavior;
// Don't want to interpret new lines if we are half-way through processing an ANSI escape code
if (this.inIdleState && rxByte === "\n".charCodeAt(0)) {
// If swallow is disabled, print the new line character. Do this before
// performing any cursor movements, as we want the new line char to
// at the end of the existing line, rather than the start of the new
// line
- if (!this.rxSettings.config.swallowNewLine) {
+ if (!this.rxSettings.swallowNewLine) {
this._addVisibleChar(rxByte);
}
@@ -315,13 +315,13 @@ export default class SingleTerminal {
// CARRIAGE RETURN HANDLING
//========================================================================
- const carriageReturnCursorBehavior = this.rxSettings.config.carriageReturnCursorBehavior;
+ const carriageReturnCursorBehavior = this.rxSettings.carriageReturnCursorBehavior;
// Don't want to interpret new lines if we are half-way through processing an ANSI escape code
if (this.inIdleState && rxByte === "\r".charCodeAt(0)) {
// If swallow is disabled, print the carriage return character. Do this before
// performing any cursor movements, as we want the carriage return char to
// at the end line, rather than at the start
- if (!this.rxSettings.config.swallowCarriageReturn) {
+ if (!this.rxSettings.swallowCarriageReturn) {
this._addVisibleChar(rxByte);
}
@@ -345,7 +345,7 @@ export default class SingleTerminal {
}
// Check if ANSI escape code parsing is disabled, and if so, skip parsing
- if (!this.rxSettings.config.ansiEscapeCodeParsingEnabled) {
+ if (!this.rxSettings.ansiEscapeCodeParsingEnabled) {
this._addVisibleChar(rxByte);
continue;
}
@@ -390,7 +390,7 @@ export default class SingleTerminal {
// When we get to the end of parsing, check that if we are still
// parsing an escape code, and we've hit the escape code length limit,
// then bail on escape code parsing. Emit partial code as data and go back to IDLE
- const maxEscapeCodeLengthChars = this.rxSettings.config.maxEscapeCodeLengthChars.appliedValue;
+ const maxEscapeCodeLengthChars = this.rxSettings.maxEscapeCodeLengthChars.appliedValue;
// const maxEscapeCodeLengthChars = 10;
if (this.inAnsiEscapeCode && this.partialEscapeCode.length === maxEscapeCodeLengthChars) {
@@ -564,7 +564,7 @@ export default class SingleTerminal {
* @param data The data to parse and display.
*/
_parseDataAsNumber(data: Uint8Array) {
- // console.log("_parseDataAsNumber() called. data=", data, "length: ", data.length, "selectedNumberType=", this.rxSettings.config.numberType);
+ // console.log("_parseDataAsNumber() called. data=", data, "length: ", data.length, "selectedNumberType=", this.rxSettings.numberType);
for (let idx = 0; idx < data.length; idx += 1) {
const rxByte = data[idx];
@@ -576,23 +576,23 @@ export default class SingleTerminal {
//========================================================================
// CREATE NUMBER PART OF STRING
//========================================================================
- const isLittleEndian = this.rxSettings.config.endianness === Endianness.LITTLE_ENDIAN;
+ const isLittleEndian = this.rxSettings.endianness === Endianness.LITTLE_ENDIAN;
// HEX
//============
- if (this.rxSettings.config.numberType === NumberType.HEX) {
- if (this.partialNumberBuffer.length < this.rxSettings.config.numBytesPerHexNumber.appliedValue) {
+ if (this.rxSettings.numberType === NumberType.HEX) {
+ if (this.partialNumberBuffer.length < this.rxSettings.numBytesPerHexNumber.appliedValue) {
// Wait for enough bytes for the hex number as specified by the user
continue;
}
// Got enough bytes, loop through and convert to hex
for (let idx = 0; idx < this.partialNumberBuffer.length; idx += 1) {
let byteIdx;
- if (this.rxSettings.config.endianness === Endianness.LITTLE_ENDIAN) {
+ if (this.rxSettings.endianness === Endianness.LITTLE_ENDIAN) {
byteIdx = this.partialNumberBuffer.length - 1 - idx;
- } else if (this.rxSettings.config.endianness === Endianness.BIG_ENDIAN) {
+ } else if (this.rxSettings.endianness === Endianness.BIG_ENDIAN) {
byteIdx = idx;
} else {
- throw Error("Invalid endianness setting: " + this.rxSettings.config.endianness);
+ throw Error("Invalid endianness setting: " + this.rxSettings.endianness);
}
let partialHexString = this.partialNumberBuffer[byteIdx].toString(16);
partialHexString = partialHexString.padStart(2, '0');
@@ -600,19 +600,19 @@ export default class SingleTerminal {
}
this.partialNumberBuffer = [];
// Set case of hex string
- if (this.rxSettings.config.hexCase === HexCase.UPPERCASE) {
+ if (this.rxSettings.hexCase === HexCase.UPPERCASE) {
numberStr = numberStr.toUpperCase();
- } else if (this.rxSettings.config.hexCase === HexCase.LOWERCASE) {
+ } else if (this.rxSettings.hexCase === HexCase.LOWERCASE) {
numberStr = numberStr.toLowerCase();
} else {
- throw Error("Invalid hex case setting: " + this.rxSettings.config.hexCase);
+ throw Error("Invalid hex case setting: " + this.rxSettings.hexCase);
}
numberAsBigInt = BigInt('0x' + numberStr);
// "0x" is added later after the padding step if enabled
}
// UINT8
//============
- else if (this.rxSettings.config.numberType === NumberType.UINT8) {
+ else if (this.rxSettings.numberType === NumberType.UINT8) {
// Even though for a uint8 we could directly convert the byte to a string,
// we use this Array method to be consistent with the other number types
const uint8Array = Uint8Array.from(this.partialNumberBuffer);
@@ -623,7 +623,7 @@ export default class SingleTerminal {
}
// INT8
//============
- else if (this.rxSettings.config.numberType === NumberType.INT8) {
+ else if (this.rxSettings.numberType === NumberType.INT8) {
const uint8Array = Uint8Array.from(this.partialNumberBuffer);
const dataView = new DataView(uint8Array.buffer);
numberStr = dataView.getInt8(0).toString(10);
@@ -632,7 +632,7 @@ export default class SingleTerminal {
}
// UINT16
//============
- else if (this.rxSettings.config.numberType === NumberType.UINT16) {
+ else if (this.rxSettings.numberType === NumberType.UINT16) {
if (this.partialNumberBuffer.length < 2) {
// We need to wait for another byte to come in before we can convert
// the two bytes to a single number
@@ -647,7 +647,7 @@ export default class SingleTerminal {
}
// INT16
//============
- else if (this.rxSettings.config.numberType === NumberType.INT16) {
+ else if (this.rxSettings.numberType === NumberType.INT16) {
if (this.partialNumberBuffer.length < 2) {
// We need to wait for another byte to come in before we can convert
// the two bytes to a single number
@@ -662,7 +662,7 @@ export default class SingleTerminal {
}
// UINT32
//============
- else if (this.rxSettings.config.numberType === NumberType.UINT32) {
+ else if (this.rxSettings.numberType === NumberType.UINT32) {
if (this.partialNumberBuffer.length < 4) {
continue;
}
@@ -674,7 +674,7 @@ export default class SingleTerminal {
}
// INT32
//============
- else if (this.rxSettings.config.numberType === NumberType.INT32) {
+ else if (this.rxSettings.numberType === NumberType.INT32) {
if (this.partialNumberBuffer.length < 4) {
continue;
}
@@ -686,7 +686,7 @@ export default class SingleTerminal {
}
// UINT64
//============
- else if (this.rxSettings.config.numberType === NumberType.UINT64) {
+ else if (this.rxSettings.numberType === NumberType.UINT64) {
if (this.partialNumberBuffer.length < 8) {
continue;
}
@@ -698,7 +698,7 @@ export default class SingleTerminal {
}
// INT64
//============
- else if (this.rxSettings.config.numberType === NumberType.INT64) {
+ else if (this.rxSettings.numberType === NumberType.INT64) {
if (this.partialNumberBuffer.length < 8) {
continue;
}
@@ -710,7 +710,7 @@ export default class SingleTerminal {
}
// FLOAT32
//============
- else if (this.rxSettings.config.numberType === NumberType.FLOAT32) {
+ else if (this.rxSettings.numberType === NumberType.FLOAT32) {
if (this.partialNumberBuffer.length < 4) {
continue;
}
@@ -719,17 +719,17 @@ export default class SingleTerminal {
// toFixed gives a fixed number of decimal places
// toString gives a variable amount depending on the number
const number = dataView.getFloat32(0, isLittleEndian);
- if (this.rxSettings.config.floatStringConversionMethod === FloatStringConversionMethod.TO_STRING) {
+ if (this.rxSettings.floatStringConversionMethod === FloatStringConversionMethod.TO_STRING) {
numberStr = number.toString();
- } else if (this.rxSettings.config.floatStringConversionMethod === FloatStringConversionMethod.TO_FIXED) {
- numberStr = number.toFixed(this.rxSettings.config.floatNumOfDecimalPlaces.appliedValue);
+ } else if (this.rxSettings.floatStringConversionMethod === FloatStringConversionMethod.TO_FIXED) {
+ numberStr = number.toFixed(this.rxSettings.floatNumOfDecimalPlaces.appliedValue);
}
numberAsBigInt = BigInt(dataView.getUint32(0, isLittleEndian));
this.partialNumberBuffer = [];
}
// FLOAT64
//============
- else if (this.rxSettings.config.numberType === NumberType.FLOAT64) {
+ else if (this.rxSettings.numberType === NumberType.FLOAT64) {
if (this.partialNumberBuffer.length < 8) {
continue;
}
@@ -738,10 +738,10 @@ export default class SingleTerminal {
// toFixed gives a fixed number of decimal places
// toString gives a variable amount depending on the number
const number = dataView.getFloat64(0, isLittleEndian);
- if (this.rxSettings.config.floatStringConversionMethod === FloatStringConversionMethod.TO_STRING) {
+ if (this.rxSettings.floatStringConversionMethod === FloatStringConversionMethod.TO_STRING) {
numberStr = number.toString();
- } else if (this.rxSettings.config.floatStringConversionMethod === FloatStringConversionMethod.TO_FIXED) {
- numberStr = number.toFixed(this.rxSettings.config.floatNumOfDecimalPlaces.appliedValue);
+ } else if (this.rxSettings.floatStringConversionMethod === FloatStringConversionMethod.TO_FIXED) {
+ numberStr = number.toFixed(this.rxSettings.floatNumOfDecimalPlaces.appliedValue);
}
numberAsBigInt = BigInt(dataView.getBigUint64(0, isLittleEndian));
this.partialNumberBuffer = [];
@@ -749,66 +749,66 @@ export default class SingleTerminal {
// INVALID
//============
else {
- throw Error("Invalid number type: " + this.rxSettings.config.numberType);
+ throw Error("Invalid number type: " + this.rxSettings.numberType);
}
// console.log('Converted numberStr=', numberStr);
//========================================================================
// ADD PADDING
//========================================================================
- if (this.rxSettings.config.padValues) {
+ if (this.rxSettings.padValues) {
// If padding is set to automatic, pad to the largest possible value for the selected number type
let paddingChar = " ";
- if (this.rxSettings.config.paddingCharacter === PaddingCharacter.ZERO) {
+ if (this.rxSettings.paddingCharacter === PaddingCharacter.ZERO) {
paddingChar = "0";
- } else if (this.rxSettings.config.paddingCharacter === PaddingCharacter.WHITESPACE) {
+ } else if (this.rxSettings.paddingCharacter === PaddingCharacter.WHITESPACE) {
paddingChar = " ";
} else {
- throw Error("Invalid padding character setting: " + this.rxSettings.config.paddingCharacter);
+ throw Error("Invalid padding character setting: " + this.rxSettings.paddingCharacter);
}
// If padding is set to automatic, pad to the largest possible value for the selected number type
- let numPaddingChars = this.rxSettings.config.numPaddingChars.appliedValue;
+ let numPaddingChars = this.rxSettings.numPaddingChars.appliedValue;
if (numPaddingChars === -1) {
- if (this.rxSettings.config.numberType === NumberType.HEX) {
+ if (this.rxSettings.numberType === NumberType.HEX) {
numPaddingChars = 2;
- } else if (this.rxSettings.config.numberType === NumberType.UINT8) {
+ } else if (this.rxSettings.numberType === NumberType.UINT8) {
// Numbers 0 to 255, so 3 chars
numPaddingChars = 3;
- } else if (this.rxSettings.config.numberType === NumberType.INT8) {
+ } else if (this.rxSettings.numberType === NumberType.INT8) {
// Numbers -128 to 127, so 4 chars
numPaddingChars = 4;
- } else if (this.rxSettings.config.numberType === NumberType.UINT16) {
+ } else if (this.rxSettings.numberType === NumberType.UINT16) {
// Numbers 0 to 65535, so 5 chars
numPaddingChars = 5;
- } else if (this.rxSettings.config.numberType === NumberType.INT16) {
+ } else if (this.rxSettings.numberType === NumberType.INT16) {
// Numbers -32768 to 32767, so 6 chars
numPaddingChars = 6;
- } else if (this.rxSettings.config.numberType === NumberType.UINT32) {
+ } else if (this.rxSettings.numberType === NumberType.UINT32) {
// Numbers 0 to 4294967296, so 10 chars
numPaddingChars = 10;
- } else if (this.rxSettings.config.numberType === NumberType.INT32) {
+ } else if (this.rxSettings.numberType === NumberType.INT32) {
// Numbers -2147483648 to 2147483647, so 11 chars
numPaddingChars = 11;
- } else if (this.rxSettings.config.numberType === NumberType.UINT64) {
+ } else if (this.rxSettings.numberType === NumberType.UINT64) {
// Numbers 0 to 18,446,744,073,709,551,615, so 20 chars
numPaddingChars = 20;
- } else if (this.rxSettings.config.numberType === NumberType.INT64) {
+ } else if (this.rxSettings.numberType === NumberType.INT64) {
// Numbers -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807, so 20 chars
numPaddingChars = 20;
- } else if (this.rxSettings.config.numberType === NumberType.FLOAT32) {
+ } else if (this.rxSettings.numberType === NumberType.FLOAT32) {
// Arbitrarily choose 6 for floats if it's set to "auto" (-1)
numPaddingChars = 6;
- } else if (this.rxSettings.config.numberType === NumberType.FLOAT64) {
+ } else if (this.rxSettings.numberType === NumberType.FLOAT64) {
numPaddingChars = 6;
} else {
- throw Error("Invalid number type: " + this.rxSettings.config.numberType);
+ throw Error("Invalid number type: " + this.rxSettings.numberType);
}
}
// Handle negative numbers combined with zeroes padding by padding after the negative sign
// (padding negative numbers with spaces is handled the same way as positive numbers, the padding
// goes before the negative sign).
- if (this.rxSettings.config.paddingCharacter == PaddingCharacter.ZERO && numberStr[0] === "-") {
+ if (this.rxSettings.paddingCharacter == PaddingCharacter.ZERO && numberStr[0] === "-") {
numberStr = numberStr.slice(1);
numPaddingChars -= 1; // Negative sign takes up one padding char
numberStr = '-' + numberStr.padStart(numPaddingChars, paddingChar);
@@ -820,7 +820,7 @@ export default class SingleTerminal {
// console.log('After padding, numberStr=', numberStr);
// Add 0x if hex and setting is enabled
- if (this.rxSettings.config.numberType === NumberType.HEX && this.rxSettings.config.prefixHexValuesWith0x) {
+ if (this.rxSettings.numberType === NumberType.HEX && this.rxSettings.prefixHexValuesWith0x) {
numberStr = "0x" + numberStr;
}
@@ -831,7 +831,7 @@ export default class SingleTerminal {
// Only prevent numerical value wrapping mid-value if:
// 1) Setting is enabled
// 2) The terminal column width is high enough to fit an entire value in it
- if (this.rxSettings.config.preventValuesWrappingAcrossRows && this.displaySettings.terminalWidthChars.appliedValue >= numberStr.length) {
+ if (this.rxSettings.preventValuesWrappingAcrossRows && this.displaySettings.terminalWidthChars.appliedValue >= numberStr.length) {
// Create a new terminal row if the hex value will not fit on existing row
const currRow = this.terminalRows[this.cursorPosition[0]];
const numColsLeftOnRow = this.displaySettings.terminalWidthChars.appliedValue - this.cursorPosition[1];
@@ -850,14 +850,14 @@ export default class SingleTerminal {
// Work out if we need to insert a new line because the numerical value matches the new line value
// in the settings
let insertNewLine = false;
- if (this.rxSettings.config.insertNewLineOnMatchedValue) {
- const valueToInsertNewLineAsNum = BigInt('0x' + this.rxSettings.config.newLineMatchValueAsHex.appliedValue);
+ if (this.rxSettings.insertNewLineOnMatchedValue) {
+ const valueToInsertNewLineAsNum = BigInt('0x' + this.rxSettings.newLineMatchValueAsHex.appliedValue);
if (numberAsBigInt == valueToInsertNewLineAsNum) {
insertNewLine = true;
}
}
- if (insertNewLine && this.rxSettings.config.newLinePlacementOnHexValue === NewLinePlacementOnHexValue.BEFORE) {
+ if (insertNewLine && this.rxSettings.newLinePlacementOnHexValue === NewLinePlacementOnHexValue.BEFORE) {
// Insert new line before hex value
this._cursorDown(1, false); // Not due to wrapping
this._cursorLeft(this.cursorPosition[1]);
@@ -869,11 +869,11 @@ export default class SingleTerminal {
this._addVisibleChar(numberStr.charCodeAt(charIdx));
}
// Append the hex separator string
- for (let charIdx = 0; charIdx < this.rxSettings.config.numberSeparator.appliedValue.length; charIdx += 1) {
- this._addVisibleChar(this.rxSettings.config.numberSeparator.appliedValue.charCodeAt(charIdx));
+ for (let charIdx = 0; charIdx < this.rxSettings.numberSeparator.appliedValue.length; charIdx += 1) {
+ this._addVisibleChar(this.rxSettings.numberSeparator.appliedValue.charCodeAt(charIdx));
}
- if (insertNewLine && this.rxSettings.config.newLinePlacementOnHexValue === NewLinePlacementOnHexValue.AFTER) {
+ if (insertNewLine && this.rxSettings.newLinePlacementOnHexValue === NewLinePlacementOnHexValue.AFTER) {
// Insert new line after hex value
this._cursorDown(1, false); // Not due to wrapping
this._cursorLeft(this.cursorPosition[1]);
@@ -1070,7 +1070,7 @@ export default class SingleTerminal {
// console.log('addVisibleChar() called. rxByte=', rxByte);
const terminalChar = new TerminalChar();
- const nonVisibleCharDisplayBehavior = this.rxSettings.config.nonVisibleCharDisplayBehavior;
+ const nonVisibleCharDisplayBehavior = this.rxSettings.nonVisibleCharDisplayBehavior;
if (rxByte >= 0x20 && rxByte <= 0x7e) {
// Is printable ASCII character, no shifting needed
diff --git a/src/view/AppView.tsx b/src/view/AppView.tsx
index ea6cb461..ec702d56 100644
--- a/src/view/AppView.tsx
+++ b/src/view/AppView.tsx
@@ -1,43 +1,43 @@
-import { observer } from "mobx-react-lite";
+import { observer } from 'mobx-react-lite';
-import { Backdrop, Box, CircularProgress, IconButton, Tooltip } from "@mui/material";
-import { ThemeProvider, createTheme } from "@mui/material/styles";
-import SettingsIcon from "@mui/icons-material/Settings";
-import TimelineIcon from "@mui/icons-material/Timeline";
-import TerminalIcon from "@mui/icons-material/Terminal";
-import SaveAsIcon from "@mui/icons-material/SaveAs";
-import CssBaseline from "@mui/material/CssBaseline";
-import { SnackbarProvider } from "notistack";
+import { Backdrop, Box, CircularProgress, IconButton, Tooltip } from '@mui/material';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import SettingsIcon from '@mui/icons-material/Settings';
+import TimelineIcon from '@mui/icons-material/Timeline';
+import TerminalIcon from '@mui/icons-material/Terminal';
+import SaveAsIcon from '@mui/icons-material/SaveAs';
+import CssBaseline from '@mui/material/CssBaseline';
+import { SnackbarProvider } from 'notistack';
// I got the following error here:
// error TS2307: Cannot find module 'virtual:pwa-register' or its corresponding type declarations.
// even with "vite-plugin-pwa/client" in the types array inside tsconfig.json. So getting typescript
// to ignore this import for now.
// @ts-ignore:next-line
-import { registerSW } from "virtual:pwa-register";
+import { registerSW } from 'virtual:pwa-register';
-import { App, MainPanes } from "../model/App";
-import { PortState } from "../model/Settings/PortConfigurationSettings/PortConfigurationSettings";
-import "./App.css";
-import SettingsDialog from "./Settings/SettingsView";
-import TerminalView from "./Terminals/TerminalsView";
-import GraphView from "./Graphing/GraphingView";
-import LogoImage from "./logo192.png";
-import styles from "./AppView.module.css";
-import FakePortDialogView from "./FakePorts/FakePortDialogView";
-import { useEffect } from "react";
-import LoggingView from "./Logging/LoggingView";
-import { SelectionController, SelectionInfo } from "../model/SelectionController/SelectionController";
-import "src/model/WindowTypes";
-import { DataType } from "src/model/Settings/RxSettings/RxSettings";
+import { App, MainPanes } from '../model/App';
+import { PortState } from '../model/Settings/PortConfigurationSettings/PortConfigurationSettings';
+import './App.css';
+import SettingsDialog from './Settings/SettingsView';
+import TerminalView from './Terminals/TerminalsView';
+import GraphView from './Graphing/GraphingView';
+import LogoImage from './logo192.png';
+import styles from './AppView.module.css';
+import FakePortDialogView from './FakePorts/FakePortDialogView';
+import { useEffect } from 'react';
+import LoggingView from './Logging/LoggingView';
+import { SelectionController, SelectionInfo } from '../model/SelectionController/SelectionController';
+import 'src/model/WindowTypes';
+import { DataType } from 'src/model/Settings/RxSettings/RxSettings';
// Create dark theme for MUI
const darkTheme = createTheme({
palette: {
- mode: "dark",
+ mode: 'dark',
background: {
- default: "#202020",
- paper: "#202020",
+ default: '#202020',
+ paper: '#202020',
// paper: deepOrange[900],
},
// primary: {
@@ -57,7 +57,7 @@ const darkTheme = createTheme({
tooltip: {
// Override default font size for all tool-tips, as default is a little
// to small
- fontSize: "0.8rem",
+ fontSize: '0.8rem',
},
},
},
@@ -70,16 +70,16 @@ const darkTheme = createTheme({
*/
const portStateToToolbarStatusProperties: { [key in PortState]: any } = {
[PortState.CLOSED]: {
- color: "red",
- text: "Port CLOSED",
+ color: 'red',
+ text: 'Port CLOSED',
},
[PortState.CLOSED_BUT_WILL_REOPEN]: {
- color: "orange",
- text: "Port CLOSED (will reopen)",
+ color: 'orange',
+ text: 'Port CLOSED (will reopen)',
},
[PortState.OPENED]: {
- color: "green",
- text: "Port OPENED",
+ color: 'green',
+ text: 'Port OPENED',
},
};
@@ -166,27 +166,32 @@ const AppView = observer((props: Props) => {
}}
tabIndex={-1}
style={{
- height: "100%",
- display: "flex",
- padding: "10px 10px 10px 0px", // No padding on left
- outline: "none", // Prevent weird white border when selected
+ height: '100%',
+ display: 'flex',
+ padding: '5px 5px 5px 0px', // No padding on left
+ outline: 'none', // Prevent weird white border when selected
+ overflow: 'hidden', // Prevent scrollbars from appearing, force internal elements
+ // to scroll instead
}}
>
{/* TX/RX ACTIVITY INDICATORS */}
{/* Use the key prop here to make React consider this a new element everytime the number of bytes changes. This will re-trigger the flashing animation as desired. Wrap each indicator in another box, so that the keys don't collide (because they might be the same). */}
@@ -297,25 +306,23 @@ const AppView = observer((props: Props) => {
{/* PORT CONFIG */}
{/* Show port configuration in short hand, e.g. "115200 8n1" */}
- {app.settings.portConfiguration.shortSerialConfigName}
+ {app.settings.portConfiguration.shortSerialConfigName}
{/* PORT STATE */}
- {portStateToToolbarStatusProperties[app.portState].text}
-
+ {portStateToToolbarStatusProperties[app.portState].text}
+
{/* The SnackBar's position in the DOM does not matter, it is not positioned in the doc flow.
Anchor to the bottom right as a terminals cursor will typically be in the bottom left */}
-
+
{/* The backdrop is not in the normal document flow. Shown as modal. Used when we want to indicate to the
user that we are doing something and block them from clicking on anything (e.g. when opening port) */}
- theme.zIndex.drawer + 1 }} open={app.showCircularProgressModal}>
+ theme.zIndex.drawer + 1 }} open={app.showCircularProgressModal}>
-
-
);
diff --git a/src/view/Settings/GeneralSettings/GeneralSettingsView.tsx b/src/view/Settings/GeneralSettings/GeneralSettingsView.tsx
index 6fedd18d..e2ccabd3 100644
--- a/src/view/Settings/GeneralSettings/GeneralSettingsView.tsx
+++ b/src/view/Settings/GeneralSettings/GeneralSettingsView.tsx
@@ -1,4 +1,4 @@
-import { Checkbox, FormControl, FormControlLabel, FormLabel, InputAdornment, Radio, RadioGroup, TextField, Tooltip } from "@mui/material";
+import { Checkbox, FormControlLabel, Tooltip } from "@mui/material";
import { observer } from "mobx-react-lite";
import GeneralSettings from "src/model/Settings/GeneralSettings/GeneralSettings";
@@ -33,7 +33,7 @@ function GeneralSettingsView(props: Props) {
{
generalSettings.setWhenCopyingToClipboardDoNotAddLFIfRowWasCreatedDueToWrapping(e.target.checked);
}}
@@ -53,7 +53,7 @@ function GeneralSettingsView(props: Props) {
{
generalSettings.setWhenPastingOnWindowsReplaceCRLFWithLF(e.target.checked);
}}
diff --git a/src/view/Settings/PortConfigurationSettings/PortConfigurationSettingsView.tsx b/src/view/Settings/PortConfigurationSettings/PortConfigurationSettingsView.tsx
index f834f9bb..b3a1d3cb 100644
--- a/src/view/Settings/PortConfigurationSettings/PortConfigurationSettingsView.tsx
+++ b/src/view/Settings/PortConfigurationSettings/PortConfigurationSettingsView.tsx
@@ -12,14 +12,22 @@ import {
FormControlLabel,
Autocomplete,
TextField,
-} from '@mui/material';
-import { OverridableStringUnion } from '@mui/types';
-import { observer } from 'mobx-react-lite';
+} from "@mui/material";
+import { OverridableStringUnion } from "@mui/types";
+import { observer } from "mobx-react-lite";
-import { App, PortType } from 'src/model/App';
-import { PortState, DEFAULT_BAUD_RATES, NUM_DATA_BITS_OPTIONS, Parity, STOP_BIT_OPTIONS, StopBits } from 'src/model/Settings/PortConfigurationSettings/PortConfigurationSettings';
-import { portStateToButtonProps } from 'src/view/Components/PortStateToButtonProps';
-import styles from './PortConfigurationSettingsView.module.css';
+import { App, PortType } from "src/model/App";
+import {
+ PortState,
+ DEFAULT_BAUD_RATES,
+ NUM_DATA_BITS_OPTIONS,
+ Parity,
+ STOP_BIT_OPTIONS,
+ StopBits,
+ FlowControl,
+} from "src/model/Settings/PortConfigurationSettings/PortConfigurationSettings";
+import { portStateToButtonProps } from "src/view/Components/PortStateToButtonProps";
+import styles from "./PortConfigurationSettingsView.module.css";
interface Props {
app: App;
@@ -29,110 +37,147 @@ function PortConfigurationView(props: Props) {
const { app } = props;
return (
-
{/* =============================================================== */}
{/* SELECT PORT BUTTON */}
{/* =============================================================== */}
@@ -213,7 +243,7 @@ function PortConfigurationView(props: Props) {
// Only let user select a new port if current one is closed
disabled={app.portState !== PortState.CLOSED}
data-testid="request-port-access"
- sx={{ width: '150px' }}
+ sx={{ width: "150px" }}
>
Select Port
@@ -223,15 +253,8 @@ function PortConfigurationView(props: Props) {
+ Profiles let you save and load settings and configuration to quickly switch between projects. Almost all settings are saved with each profile. The last selected serial port
+ will be saved with the profile, and NinjaTerm will attempt to reconnect to that port when the profile is loaded (because of the limited information about the serial ports
+ available in the browser, it might not be enough to uniquely identify the port).
+
diff --git a/src/view/Terminals/SingleTerminal/SingleTerminalView.tsx b/src/view/Terminals/SingleTerminal/SingleTerminalView.tsx
index 1c13b178..048b8771 100644
--- a/src/view/Terminals/SingleTerminal/SingleTerminalView.tsx
+++ b/src/view/Terminals/SingleTerminal/SingleTerminalView.tsx
@@ -244,7 +244,7 @@ export default observer((props: Props) => {
data-testid={testId + '-outer'}
style={{
flexGrow: 1,
- marginBottom: '10px',
+ // marginBottom: '10px',
padding: '15px', // This is what adds some space between the outside edges of the terminal and the shown text in the react-window
boxSizing: 'border-box',
overflowY: 'hidden',
diff --git a/src/view/Terminals/TerminalsView.tsx b/src/view/Terminals/TerminalsView.tsx
index 643ac91a..cdae87c1 100644
--- a/src/view/Terminals/TerminalsView.tsx
+++ b/src/view/Terminals/TerminalsView.tsx
@@ -13,23 +13,23 @@ import {
Tooltip,
Typography,
useMediaQuery,
-} from "@mui/material";
-import CancelIcon from "@mui/icons-material/Cancel";
-import DeleteIcon from "@mui/icons-material/Delete";
-import VisibilityIcon from "@mui/icons-material/Visibility";
-import { OverridableStringUnion } from "@mui/types";
-import KofiButton from "kofi-button";
-import { observer } from "mobx-react-lite";
-import { ResizableBox } from "react-resizable";
-import "react-resizable/css/styles.css";
+} from '@mui/material';
+import CancelIcon from '@mui/icons-material/Cancel';
+import DeleteIcon from '@mui/icons-material/Delete';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import { OverridableStringUnion } from '@mui/types';
+import KofiButton from 'kofi-button';
+import { observer } from 'mobx-react-lite';
+import { ResizableBox } from 'react-resizable';
+import 'react-resizable/css/styles.css';
-import { App, PortType } from "src/model/App";
-import { PortState } from "src/model/Settings/PortConfigurationSettings/PortConfigurationSettings";
-import SingleTerminalView from "./SingleTerminal/SingleTerminalView";
-import { DataViewConfiguration, dataViewConfigEnumToDisplayName } from "src/model/Settings/DisplaySettings/DisplaySettings";
-import ApplyableTextFieldView from "src/view/Components/ApplyableTextFieldView";
-import { portStateToButtonProps } from "src/view/Components/PortStateToButtonProps";
-import RightDrawerView from "./RightDrawer/RightDrawerView";
+import { App, PortType } from 'src/model/App';
+import { PortState } from 'src/model/Settings/PortConfigurationSettings/PortConfigurationSettings';
+import SingleTerminalView from './SingleTerminal/SingleTerminalView';
+import { DataViewConfiguration, dataViewConfigEnumToDisplayName } from 'src/model/Settings/DisplaySettings/DisplaySettings';
+import ApplyableTextFieldView from 'src/view/Components/ApplyableTextFieldView';
+import { portStateToButtonProps } from 'src/view/Components/PortStateToButtonProps';
+import RightDrawerView from './RightDrawer/RightDrawerView';
interface Props {
app: App;
@@ -38,7 +38,7 @@ interface Props {
export default observer((props: Props) => {
const { app } = props;
- const isSmallScreen = useMediaQuery((theme) => (theme as any).breakpoints.down("lg"));
+ const isSmallScreen = useMediaQuery((theme) => (theme as any).breakpoints.down('lg'));
// TERMINAL CREATION
// ==========================================================================
@@ -50,11 +50,11 @@ export default observer((props: Props) => {
} else if (app.settings.displaySettings.dataViewConfiguration === DataViewConfiguration.SEPARATE_TX_RX_TERMINALS) {
// Shows 2 terminals, 1 for TX data and 1 for RX data
terminals = (
-
-
+
+
-
+
@@ -63,11 +63,11 @@ export default observer((props: Props) => {
throw Error(`Unsupported data view configuration. dataViewConfiguration=${app.settings.displaySettings.dataViewConfiguration}.`);
}
- const buttonSx = {
+ const responsiveButtonStyle = {
// minWidth: isSmallScreen ? '10px' : '180px',
- "& .MuiButton-startIcon": {
- marginRight: isSmallScreen ? "0px" : undefined,
- marginLeft: isSmallScreen ? "0px" : undefined,
+ '& .MuiButton-startIcon': {
+ marginRight: isSmallScreen ? '0px' : undefined,
+ marginLeft: isSmallScreen ? '0px' : undefined,
},
};
@@ -85,27 +85,29 @@ export default observer((props: Props) => {
id="terminal-view-outer"
style={{
flexGrow: 1,
- display: "flex",
- flexDirection: "column",
+ display: 'flex',
+ flexDirection: 'column',
// overflowY: hidden important so that the single terminal panes get smaller when the
// window height is made smaller. Without this, scrollbars appear.
// The negative margin and then positive padding cancel each over out...BUT they
// do let the outer glow on a focused terminal still show. Without this, it would
// be clipped because we set the overflow to be hidden
- overflowY: "hidden",
- margin: "-10px",
- padding: "10px",
+ // UPDATE 2024-05-30: Removed the margin/padding thing and the glow still works???
+ overflowY: 'hidden',
+ // margin: '-10px',
+ // padding: '10px',
}}
>
-
{/* ==================================================================== */}
@@ -115,7 +117,7 @@ export default observer((props: Props) => {
variant="outlined"
color={
portStateToButtonProps[app.portState].color as OverridableStringUnion<
- "inherit" | "primary" | "secondary" | "success" | "error" | "info" | "warning",
+ 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning',
ButtonPropsColorOverrides
>
}
@@ -125,18 +127,18 @@ export default observer((props: Props) => {
} else if (app.portState === PortState.CLOSED_BUT_WILL_REOPEN) {
app.stopWaitingToReopenPort();
} else if (app.portState === PortState.OPENED) {
- app.closePort(false);
+ app.closePort();
} else {
throw Error(`Unsupported port state. portState=${app.portState}`);
}
}}
startIcon={portStateToButtonProps[app.portState].icon}
disabled={app.portState === PortState.CLOSED && app.port === null && app.lastSelectedPortType === PortType.REAL}
- sx={buttonSx}
+ sx={responsiveButtonStyle}
data-testid="open-close-button"
>
{/* Specify a width to prevent it resizing when the text changes */}
- {isSmallScreen ? "" : portStateToButtonProps[app.portState].text}
+ {isSmallScreen ? '' : portStateToButtonProps[app.portState].text}
{/* ==================================================================== */}
{/* CLEAR DATA BUTTON */}
@@ -147,9 +149,9 @@ export default observer((props: Props) => {
onClick={() => {
app.clearAllData();
}}
- sx={buttonSx}
+ sx={responsiveButtonStyle}
>
- {isSmallScreen ? "" : "Clear"}
+ {isSmallScreen ? '' : 'Clear'}
{/* ======================================================= */}
{/* FILTER TEXT INPUT */}
@@ -174,13 +176,14 @@ export default observer((props: Props) => {
variant="outlined"
applyableTextField={app.terminals.filterText}
InputProps={{
+ style: { height: '35px' },
endAdornment: (
{
- app.terminals.filterText.setDispValue("");
+ app.terminals.filterText.setDispValue('');
app.terminals.filterText.apply();
}}
>
@@ -189,7 +192,7 @@ export default observer((props: Props) => {
),
}}
- sx={{ width: "200px" }}
+ sx={{ width: '200px' }}
/>
@@ -208,8 +211,8 @@ export default observer((props: Props) => {
}
}}
startIcon={}
- sx={buttonSx}
- style={{ width: '200px' }}
+ // sx={responsiveButtonStyle}
+ sx={responsiveButtonStyle}
data-testid="show-hide-side-panel-button"
>
{/* Specify a width to prevent it resizing when the text changes */}
@@ -218,18 +221,26 @@ export default observer((props: Props) => {
{/* ============================ VERSION NUMBER =========================== */}
{/* Push to right hand side of screen */}
- v{app.version}
+ v{app.version}
{/* ============================ Ko-Fi "Donate" button =========================== */}
-
-
+
+
{terminals}
{/* ==================================================================== */}
{/* RIGHT DRAWER (wrapped in a div so we can hide it all) */}
{/* ==================================================================== */}
-
-
+
+
diff --git a/tests/Macros.spec.ts b/tests/Macros.spec.ts
new file mode 100644
index 00000000..322d64c0
--- /dev/null
+++ b/tests/Macros.spec.ts
@@ -0,0 +1,100 @@
+import { expect, test } from '@playwright/test';
+
+import { AppTestHarness } from './Util';
+
+test.describe('macros', () => {
+ test('default macros are present', async ({ page }) => {
+ const appTestHarness = new AppTestHarness(page);
+ await appTestHarness.setupPage();
+ await appTestHarness.openPortAndGoToTerminalView();
+
+ await expect(await page.getByTestId('macro-data-0')).toHaveValue('Hello\\n');
+ // Click on macro's "more settings"
+ await page.getByTestId('macro-more-settings-0').click();
+
+ // Make sure the ASCII radio button is selected
+ await expect(await page.getByTestId('macro-data-type-ascii-rb')).toBeChecked();
+
+ // Close the modal
+ await page.getByTestId('macro-settings-modal-close-button').click();
+
+ // Make sure MACRO 1 is set to HEX and has the value "deadbeef"
+ await expect(await page.getByTestId('macro-data-1')).toHaveValue('deadbeef');
+ await page.getByTestId('macro-more-settings-1').click();
+ await expect(await page.getByTestId('macro-data-type-hex-rb')).toBeChecked();
+ await page.getByTestId('macro-settings-modal-close-button').click();
+
+ // Now change the value of MACRO 0
+ await page.getByTestId('macro-data-0').fill('new value');
+
+ // Refresh the page
+ await page.reload();
+
+ // Make sure the value of MACRO 0 is still "new value"
+ await expect(await page.getByTestId('macro-data-0')).toHaveValue('new value');
+ });
+
+ test('macros are remembered across refresh', async ({ page }) => {
+ const appTestHarness = new AppTestHarness(page);
+ await appTestHarness.setupPage();
+ await appTestHarness.openPortAndGoToTerminalView();
+
+ // Change the value of MACRO 0
+ await page.getByTestId('macro-data-0').fill('new value');
+
+ // Refresh the page
+ await page.reload();
+
+ // Make sure the value of MACRO 0 is still "new value"
+ await expect(await page.getByTestId('macro-data-0')).toHaveValue('new value');
+ });
+
+ test('macro sends out correct ASCII data', async ({ page }) => {
+ const appTestHarness = new AppTestHarness(page);
+ await appTestHarness.setupPage();
+ await appTestHarness.openPortAndGoToTerminalView();
+
+ await page.getByTestId('macro-data-0').fill('abc123\\n');
+ // Hit the send button
+ await page.getByTestId('macro-0-send-button').click();
+
+ const utf8EncodeText = new TextEncoder();
+ const expectedText = utf8EncodeText.encode('abc123\n');
+ expect(appTestHarness.writtenData).toEqual(Array.from(expectedText));
+ });
+
+ test('turning off "process escape chars" works', async ({ page }) => {
+ const appTestHarness = new AppTestHarness(page);
+ await appTestHarness.setupPage();
+ await appTestHarness.openPortAndGoToTerminalView();
+
+ await page.getByTestId('macro-more-settings-0').click();
+ // Uncheck the process escape chars checkbox
+ await page.getByTestId('macro-process-escape-chars-cb').uncheck();
+ await page.getByTestId('macro-settings-modal-close-button').click();
+
+ await page.getByTestId('macro-data-0').fill('abc123\\n');
+ await page.getByTestId('macro-0-send-button').click();
+
+ const utf8EncodeText = new TextEncoder();
+ // The \n should not be processed into LF, should still be separate \ and n chars
+ const expectedText = utf8EncodeText.encode('abc123\\n');
+ expect(appTestHarness.writtenData).toEqual(Array.from(expectedText));
+ });
+
+ test('macro sends out correct hex data', async ({ page }) => {
+ const appTestHarness = new AppTestHarness(page);
+ await appTestHarness.setupPage();
+ await appTestHarness.openPortAndGoToTerminalView();
+
+ // Change macro 0 to hex
+ await page.getByTestId('macro-more-settings-0').click();
+ // Check the hex radio button
+ await page.getByTestId('macro-data-type-hex-rb').click();
+ await page.getByTestId('macro-settings-modal-close-button').click();
+ await page.getByTestId('macro-data-0').fill('78abff');
+ await page.getByTestId('macro-0-send-button').click();
+
+ expect(appTestHarness.writtenData).toEqual(Array.from([0x78, 0xAB, 0xFF]));
+ });
+});